diff --git a/.gitignore b/.gitignore index 60d1372d36..118d11be55 100644 --- a/.gitignore +++ b/.gitignore @@ -278,6 +278,7 @@ $RECYCLE.BIN/ /src/Plugins/*-sym /src/Tests/*-sym /src/Presentation/SmartStore.Web/Themes/*-sym +/src/Presentation/SmartStore.Web/Themes/HP ########### @@ -319,3 +320,4 @@ Kopie von* src/SmartStoreNET.Packager.sln Log.txt +src/Presentation/SmartStore.Web/Themes/FlexMuseo/ diff --git a/CREDITS.txt b/CREDITS.txt index d2b18f6a98..5ea4c4a6fb 100644 --- a/CREDITS.txt +++ b/CREDITS.txt @@ -1,11 +1,8 @@ SmartStore.NET -Copyright 1999-2014 SmartStore AG +Copyright 1999-2018 SmartStore AG http://www.smartstore.com | https://github.com/smartstoreag/SmartStoreNET -SmartStore.NET is a fork of the ASP.NET open source e-commerce solution -nopCommerce (http://www.nopcommerce.com). - SmartStore.NET includes works distributed under the licenses listed below. Please refer to the specific resources for more detailed information about the authors, copyright notices, and licenses. @@ -14,7 +11,7 @@ copyright notices, and licenses. 51Degrees --------------------------------------------- WebSite: http://51degrees.mobi/ -Copyright: Copyright 2010 - 2013 51Degrees.mobi Limited +Copyright: Copyright 2010 - 2018 51Degrees.mobi Limited License: Mozilla Public License 2.0 (MPL-2.0) @@ -28,7 +25,7 @@ License: BSD License ASP.NET MVC --------------------------------------------- -Website: http://aspnet.codeplex.com/wikipage?title=MVC&referringTitle=Home +Website: https://github.com/aspnet/AspNetWebStack Copyright: Copyright (c) 2008-2011 Microsoft Corporation License: MICROSOFT ASP.NET MODEL VIEW CONTROLLER 3 EULA http://go.microsoft.com/fwlink/?LinkID=207621 @@ -36,31 +33,30 @@ License: MICROSOFT ASP.NET MODEL VIEW CONTROLLER 3 EULA Autofac --------------------------------------------- -Website: http://code.google.com/p/autofac/ -Copyright: Copyright (c) 2007-2014 Autofac Contributors - http://code.google.com/p/autofac/wiki/Contributing +Website: https://autofac.org/ +Copyright: Copyright (c) 2007-2018 Autofac Contributors License: MIT AutoMapper --------------------------------------------- WebSite: http://www.automapper.org -Copyright: Copyright � 2008-2013 Jimmy Bogard and other contributors +Copyright: Copyright � 2008-2018 Jimmy Bogard and other contributors License: MIT Bundle Transformer --------------------------------------------- -Website: https://bundletransformer.codeplex.com +Website: https://github.com/Taritsyn/BundleTransformer Copyright: Copyright Andrey Taritsyn 2014 License: Apache License 2.0 (Apache) -CKEditor +summernote --------------------------------------------- -WebSite: http://ckeditor.com/ -Copyright: © 2014 CKSource - Frederico Knabben -License: GNU Library General Public License (LGPL) +WebSite: https://summernote.org/ +Copyright: Copyright (c) 2015~ Summernote Team (https://github.com/orgs/summernote/people) +License: MIT DotNetOpenAuth @@ -72,28 +68,28 @@ License: MS-PL DotNetZip --------------------------------------------- -Website: http://dotnetzip.codeplex.com/ +Website: https://archive.codeplex.com/?p=dotnetzip Copyright: License: MS-PL Entity Framework --------------------------------------------- -Website: https://entityframework.codeplex.com -Copyright: Copyright (c) 2012-2014 Microsoft Corporation +Website: https://github.com/aspnet/EntityFramework6 +Copyright: Copyright (c) 2012-2018 Microsoft Corporation License: Apache License 2.0 (Apache) EPPlus --------------------------------------------- -Website: https://epplus.codeplex.com/ +Website: https://github.com/JanKallman/EPPlus Copyright: Copyright (C) 2011 Jan K�llman License: GNU Library General Public License (LGPL) Fluent Validation --------------------------------------------- -Website: https://fluentvalidation.codeplex.com/ +Website: https://github.com/JeremySkinner/FluentValidation License: Apache License 2.0 (Apache) @@ -104,11 +100,11 @@ Copyright: Copyright (C) 2014 Glimpse contributors (http://getglimpse.com/Co License: Apache License 2.0 (Apache) -ImageResizer.NET +ImageProcessor --------------------------------------------- -Website: https://imageresizing.net/ -Copyright: Copyright (c) 2012 Imazen -License: Freedom License +Website: http://imageprocessor.org/ +Copyright: Copyright (c) 2018 James Jackson-South +License: Apache License 2.0 (Apache) JavaScriptEngineSwitcher @@ -132,16 +128,9 @@ Copyright: Copyright (c) 2007-2009 Ariel Flesler License: MIT -jQuery UI (Core) ---------------------------------------------- -Website: http://docs.jquery.com/UI -Copyright: Copyright (c) 2010 lib/jquery.ui/AUTHORS.txt (http://jqueryui.com/about) -License: MIT - - -JSON.NET +Json.NET --------------------------------------------- -Website: http://james.newtonking.com/ +Website: https://www.newtonsoft.com/json Copyright: Copyright (c) 2007 James Newton-King License: MIT @@ -181,30 +170,30 @@ License: MIT NuGet --------------------------------------------- -Website: http://nuget.codeplex.com -Copyright: Copyright 2010-2011 Outercurve Foundation +Website: https://github.com/nuget/home +Copyright: Copyright 2010-2018 Outercurve Foundation License: Apache Software Foundation License 2.0 nUnit --------------------------------------------- -Website: http://www.nunit.org/index.php +Website: http://nunit.org/ Copyright: Copyright (c) 2002-2007 NUnit.org - Portions Copyright (c) 2002-2008 Charlie Poole or Copyright (c) 2002-2004 James W. Newkirk, Michael C. Two, Alexei A. Vorontsov or Copyright (c) 2000-2002 Philip A. Craig + Portions Copyright (c) 2002-2018 Charlie Poole or Copyright (c) 2002-2004 James W. Newkirk, Michael C. Two, Alexei A. Vorontsov or Copyright (c) 2000-2002 Philip A. Craig License: Derived from zlib: http://nunit.org/index.php?p=license&r=2.4 PhotoSwipe --------------------------------------------- Website: http://photoswipe.com/ -Copyright: Copyright (c) 2014-2016 Dmitry Semenov, http://dimsemenov.com +Copyright: Copyright (c) 2014-2018 Dmitry Semenov, http://dimsemenov.com License: MIT slick carousel --------------------------------------------- Website: http://kenwheeler.github.io/slick/ -Copyright: Copyright (c) 2014 Ken Wheeler +Copyright: Copyright (c) 2018 Ken Wheeler License: MIT @@ -216,7 +205,7 @@ License: GPL v3.0 Twitter Bootstrap --------------------------------------------- -Website: http://twitter.github.io/bootstrap/ +Website: https://getbootstrap.com/ License: Apache License v2.0 diff --git a/SmartStoreNET.Tasks.Targets b/SmartStoreNET.Tasks.Targets index 62630bf366..dd6a1a1895 100644 --- a/SmartStoreNET.Tasks.Targets +++ b/SmartStoreNET.Tasks.Targets @@ -104,7 +104,7 @@ - + diff --git a/build.bat b/build.bat index e8c6a8c006..8d1b2b2f19 100644 --- a/build.bat +++ b/build.bat @@ -1,42 +1,37 @@ +for /f "usebackq tokens=1* delims=: " %%i in (`lib\vswhere\vswhere -latest -requires Microsoft.Component.MSBuild`) do ( + if /i "%%i"=="installationPath" set InstallDir=%%j +) + FOR %%b in ( - "%VS140COMNTOOLS%..\..\VC\vcvarsall.bat" - "%ProgramFiles(x86)%\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" - "%ProgramFiles%\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" - - "%VS120COMNTOOLS%..\..\VC\vcvarsall.bat" - "%ProgramFiles(x86)%\Microsoft Visual Studio 12.0\VC\vcvarsall.bat" - "%ProgramFiles%\Microsoft Visual Studio 12.0\VC\vcvarsall.bat" - - "%VS110COMNTOOLS%..\..\VC\vcvarsall.bat" - "%ProgramFiles(x86)%\Microsoft Visual Studio 11.0\VC\vcvarsall.bat" - "%ProgramFiles%\Microsoft Visual Studio 11.0\VC\vcvarsall.bat" - ) do ( - if exist %%b ( - call %%b x86 - goto findmsbuild - ) + "%InstallDir%\Common7\Tools\VsMSBuildCmd.bat" + "%VS140COMNTOOLS%\Common7\Tools\vsvars32.bat" + ) do ( + if exist %%b ( + call %%b + goto findmsbuild + ) ) - + echo "Unable to detect suitable environment. Build may not succeed." :findmsbuild SETLOCAL ENABLEDELAYEDEXPANSION -FOR %%p in ( - "%ProgramFiles(x86)%\MSBuild\14.0\Bin" - "%ProgramFiles%\MSBuild\14.0\Bin" - ) do ( - if exist %%p ( - if not defined MsBuildPath ( - SET "MsBuildPath=%%~p" - goto build - ) - ) +if exist "%InstallDir%\MSBuild\15.0\Bin\MSBuild.exe" ( + if not defined MsBuildPath ( + SET "MsBuildPath=%InstallDir%\MSBuild\15.0\Bin\MsBuild.exe" + goto build + ) ) -echo "Unable to detect suitable MsBuild version (14.0). Build may not succeed." + +echo "Unable to detect suitable MsBuild version (15.0). Build may not succeed." :build +cd /d %~dp0 + +echo "Restoring NuGet packages" +lib\nuget\nuget.exe restore "src\SmartStoreNET.Full-sym.sln" -call "!MsBuildPath!\msbuild.exe" SmartStoreNET.proj /p:DebugSymbols=false /p:DebugType=None /P:SlnName=SmartStoreNET /maxcpucount %* \ No newline at end of file +call "!MsBuildPath!" SmartStoreNET.proj /p:SlnName=SmartStoreNET /m /p:DebugSymbols=false /p:DebugType=None /maxcpucount %* diff --git a/changelog.md b/changelog.md index 34f9f5076a..0460583aba 100644 --- a/changelog.md +++ b/changelog.md @@ -1,21 +1,84 @@ -# Release Notes +# Release Notes ## SmartStore.NET 3.1.0 -### Bugfixes -* #1268 Data importer always inserts new pictures and does not detect equal pictures while importing -* OutputCache computes ambigous cache keys for blog pages -* #1142 Customer import creates role multiple times -* #1244 Variant query model binder cannot handle types text and datepicker +### Breaking changes +* Message template customizations are lost due to the new template engine. You have to customize the templates again. No automatic migration, sorry :-( +* Amazon Pay: The plugin has been changed to new *Login and pay with Amazon* services. A registration at Amazon and new access data are necessary for its use. The old access data can no longer be used. +* (Dev) Calls to cache methods `Keys()` and `RemoveByPattern()` require glob chars to be present now (supported glob-styles see [https://redis.io/commands/keys](https://redis.io/commands/keys)). Previously these methods appended `*` to the passed pattern, which made pattern matching rather unflexible. +* (Dev) Hook framework now passes `IHookedEntity` interface instead of `HookedEntity` class +* (Dev) Completely removed all `EntityInserted`, `EntityUpdated` and `EntityDeleted` legacy events. We were using DbSaveHooks anyway, which provides a much more powerful and way faster pub-sub mechanism for database operations. + +### Highlights +* New [Liquid](https://github.com/Shopify/liquid/wiki/Liquid-for-Designers) based template engine +* Multi-configurable rounding of order total ("cash rounding"). Can be adjusted and activated separately for each currency and payment method. +* (Perf) Picture service: new processing and caching strategy! Thumbnails are not created synchronously during the main request anymore, instead a new middleware route defers processing until an image is actually requested by any client. +* MegaMenu shrinker and *Brands* virtual menu item +* Address formatting templates by country +* Connection to translate.smartstore.com. For available languages, localized resources can be downloaded and installed directly. +* **Amazon Pay**: + * Supports merchants registered in the USA and Japan + * External authentication via *Login with Amazon* button in shop frontend + * Several improvements through the new *Login and pay with Amazon* services -### Improvements -* #1141 Clearer backend order list. Added more infos like payment method. -* #1248 New payment integration guidelines for Sofort\Klarna -* TwitterAuth: better error handling and enhanced admin instruction -* ### New Features +* 1203 MegaMenu shrinker and *Brands* virtual menu item +* [Summernote](https://summernote.org/) is now the primary HTML editor * #431 Added option to randomize the display order for slides on each request * #1258 Add option to filter shipping and payment methods by a specific customer role * #1247 Allow to import non system customer roles in customer import +* #1117 Added an option to display a dropdown menu for manufacturers +* #1203 Added an option to define a maximum number of elements in the main menu for the first hierarchy of the catalog navigation +* GMC: column chooser for edit grid +* #1100 Customer can register in frontend via "Login with Amazon" button +* **Web API**: + * #1292 Added endpoint to get order in PDF format + * Added endpoint to complete an order + * #1364 Added endpoints for MeasureWeight and MeasureDimension +* Added options to include option names of specification and product attributes in the search index +* #441 added option to specify that additional shipping surcharges are considered only once. +* #1295 Sales tracking (tracking pixel) for Billiger.de +* XML and CSV export of shopping cart and wishlist items +* #1363 Make storing of IP addresses optional +* #729 Option for automatic order amount capturing when the shipping status changed to "shipped" +* (Dev) ILocalizationFileResolver: responsible for finding localization files for client scripts +* #998 GMC: Find a way to map attribute combination values to feed export values + +### Improvements +* Target .NET Framework changed: 4.5.2 > 4.6.1. +* Lower memory consumption +* #649 Media FileSystem provider: segmenting files in subfolders to increase IO perf with huge amount of files +* #1141 Clearer backend order list. Added more infos like payment method. +* #1248 New payment integration guidelines for Sofort\Klarna +* TwitterAuth: better error handling and enhanced admin instruction +* #1181 Debitoor: Add option to display shipping address on invoices +* Moved RoundPricesDuringCalculation setting to currency entity +* #1100 Use new "Login with Amazon" services to initialize an Amazon payment +* #1285 Copy product: Add option to add more than one copy +* (Perf) Many improvements in hooking framework +* #1294 Swiss PostFinance: External payment page too small on mobile devices. Added setting for mobile device template URL, pre-configured with PostFinance template. +* #1143 Make shipping methods suitable for multi-stores +* #1320 Image import: Find out the content type of image URLs by response header rather than file extension (which is sometimes missing) +* #1219 Recently viewed products list should respect setting to hide manufacturer names + +### Bugfixes +* #1268 Data importer always inserts new pictures and does not detect equal pictures while importing +* OutputCache computes ambiguous cache keys for blog pages +* #1142 Customer import creates role multiple times +* #1244 Variant query model binder cannot handle types text and datepicker +* #1273 Attribute formatter should consider setting CatalogSettings.ShowVariantCombinationPriceAdjustment +* Product entity picker should use the wildcard search to find products +* Hook framework should run hooks with `ImportantAttribute` when hooking was disabled per scope +* #1297 Web API: Parsing the timestamp may fail due to the different accuracy of the milliseconds +* Debitoor: VAT amount could be transmitted as miscellaneous for deliveries abroad. +* Prices with discounts limited to categories and customer groups were shown to all users in product lists +* #1330 MegaSearch: Missing variant facets if the variant value is not unique +* Back-in-stock subscription form was already submitted when opening the popup dialog +* Associated products of a grouped product were displayed in the wrong order +* Payment-Filter: Fixed "The cast to value type 'System.Decimal' failed because the materialized value is null" +* The tax value per tax rate was not updated when adding\removing a product to\from the order. +* The option to send manually was ignored when sending e-mails +* #528 LimitedToStores is required on payment provider rather than plugin level +* #1318 Disabled preselected attribute combination permanently hides the shopping cart button, even if another combination is selected. ## SmartStore.NET 3.0.3 @@ -36,7 +99,9 @@ * Fixed shipping computation method ignoring deactivated PricesIncludeTax setting * **Debitoor**: Fixed missing tax rates on Debitoor invoice for net prices * #1224 Notifier wasn't working in plugin controllers -* #1205 Server cannot append header after HTTP headers have been sent +* #1205 Server cannot append header after +* +* headers have been sent * #1154 Left offcanvas navigation does not open when in checkout progress * #1212 Export: FTP publishing should consider directory structure * #1253 Product PDF exporter only exports one picture and ignores the picture number profile setting diff --git a/lib/SmartStore.Licensing/SmartStore.Licensing.dll b/lib/SmartStore.Licensing/SmartStore.Licensing.dll index 7481fd670f..2c82c7853f 100644 Binary files a/lib/SmartStore.Licensing/SmartStore.Licensing.dll and b/lib/SmartStore.Licensing/SmartStore.Licensing.dll differ diff --git a/lib/nuget/nuget.exe b/lib/nuget/nuget.exe new file mode 100644 index 0000000000..9f8781de0d Binary files /dev/null and b/lib/nuget/nuget.exe differ diff --git a/lib/vswhere/vswhere.exe b/lib/vswhere/vswhere.exe new file mode 100644 index 0000000000..3d91a17dc4 Binary files /dev/null and b/lib/vswhere/vswhere.exe differ diff --git a/src/Libraries/SmartStore.Core/BaseEntity.cs b/src/Libraries/SmartStore.Core/BaseEntity.cs index 5dc11d0699..c649970d6d 100644 --- a/src/Libraries/SmartStore.Core/BaseEntity.cs +++ b/src/Libraries/SmartStore.Core/BaseEntity.cs @@ -30,11 +30,12 @@ public Type GetUnproxiedType() // it's a proxied type t = t.BaseType; } + return t; } /// - /// Transient objects are not associated with an item already in storage. For instance, + /// Transient objects are not associated with an item already in storage. For instance, /// a Product entity is transient if its Id is 0. /// public virtual bool IsTransientRecord() diff --git a/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs b/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs index a2e151ca65..c28fc3d0ff 100644 --- a/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs +++ b/src/Libraries/SmartStore.Core/Caching/ICacheManager.cs @@ -69,17 +69,34 @@ public interface ICacheManager void Remove(string key); /// - /// Scans for all all keys in the underlying cache + /// Scans for all keys matching the input pattern /// /// A key pattern. Can be null. - /// The sequence of matching keys - string[] Keys(string pattern); + /// An array of matching key names + /// + /// Supported glob-style patterns: + /// - h?llo matches hello, hallo and hxllo + /// - h*llo matches hllo and heeeello + /// - h[ae]llo matches hello and hallo, but not hillo + /// - h[^e]llo matches hallo, hbllo, ... but not hello + /// - h[a-b]llo matches hallo and hbllo + /// + IEnumerable Keys(string pattern); /// - /// Removes items by pattern + /// Removes all entries with keys matching the input pattern /// - /// pattern - void RemoveByPattern(string pattern); + /// Glob pattern + /// Count of removed cache items + /// + /// Supported glob-style patterns: + /// - h?llo matches hello, hallo and hxllo + /// - h*llo matches hllo and heeeello + /// - h[ae]llo matches hello and hallo, but not hillo + /// - h[^e]llo matches hallo, hbllo, ... but not hello + /// - h[a-b]llo matches hallo and hbllo + /// + int RemoveByPattern(string pattern); /// /// Clear all cache data diff --git a/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs b/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs index 63e6949a95..89a2c707e6 100644 --- a/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs +++ b/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs @@ -7,6 +7,8 @@ using SmartStore.Core.Async; using System.Collections; using System.Collections.Generic; +using SmartStore.Utilities; +using System.Text.RegularExpressions; namespace SmartStore.Core.Caching { @@ -121,7 +123,7 @@ public void Remove(string key) _cache.Remove(key); } - public string[] Keys(string pattern) + public IEnumerable Keys(string pattern) { Guard.NotEmpty(pattern, nameof(pattern)); @@ -132,12 +134,14 @@ public string[] Keys(string pattern) return keys.ToArray(); } - return keys.Where(x => x.StartsWith(pattern, StringComparison.OrdinalIgnoreCase)).ToArray(); + var wildcard = new Wildcard(pattern, RegexOptions.IgnoreCase); + return keys.Where(x => wildcard.IsMatch(x)); } - public void RemoveByPattern(string pattern) + public int RemoveByPattern(string pattern) { var keysToRemove = Keys(pattern); + int count = 0; lock (_cache) { @@ -145,9 +149,12 @@ public void RemoveByPattern(string pattern) foreach (string key in keysToRemove) { _cache.Remove(key); + count++; } } - } + + return count; + } public void Clear() { diff --git a/src/Libraries/SmartStore.Core/Caching/NullCache.cs b/src/Libraries/SmartStore.Core/Caching/NullCache.cs index d04b698ca7..ae8d19b285 100644 --- a/src/Libraries/SmartStore.Core/Caching/NullCache.cs +++ b/src/Libraries/SmartStore.Core/Caching/NullCache.cs @@ -65,13 +65,14 @@ public void Remove(string key) { } - public string[] Keys(string pattern) + public IEnumerable Keys(string pattern) { return new string[0]; } - public void RemoveByPattern(string pattern) + public int RemoveByPattern(string pattern) { + return 0; } public void Clear() diff --git a/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs b/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs index 3cb799c251..ec6fc43daf 100644 --- a/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs +++ b/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs @@ -178,7 +178,11 @@ public virtual IEnumerable GetCacheControlTagsFor(BaseEntity entity) } else if (type == typeof(ProductVariantAttributeValue)) { - yield return "p" + ((ProductVariantAttributeValue)entity).ProductVariantAttribute.ProductId; + var pva = ((ProductVariantAttributeValue)entity).ProductVariantAttribute; + if (pva != null) + { + yield return "p" + pva.ProductId; + } } else if (type == typeof(ProductVariantAttributeCombination)) { diff --git a/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs b/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs index fe31dd4eac..6542f2a4f6 100644 --- a/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs +++ b/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs @@ -7,6 +7,7 @@ using SmartStore.Core.Configuration; using SmartStore.Core.Data; using SmartStore.Core.Infrastructure.DependencyManagement; +using SmartStore.Utilities; namespace SmartStore.Core.Caching { @@ -155,7 +156,7 @@ public static void ObserveSettingProperty( { Guard.NotNull(propertyAccessor, nameof(propertyAccessor)); - var key = typeof(TSetting).Name + "." + propertyAccessor.ExtractPropertyInfo().Name; + var key = TypeHelper.NameOf(propertyAccessor, true); observer.ObserveSetting(key, invalidationAction); } } diff --git a/src/Libraries/SmartStore.Core/Caching/RequestCache.cs b/src/Libraries/SmartStore.Core/Caching/RequestCache.cs index e4936c2837..cb05f3f288 100644 --- a/src/Libraries/SmartStore.Core/Caching/RequestCache.cs +++ b/src/Libraries/SmartStore.Core/Caching/RequestCache.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text.RegularExpressions; using System.Web; +using SmartStore.Utilities; namespace SmartStore.Core.Caching { @@ -100,19 +101,20 @@ public IEnumerable Keys(string pattern) var prefixLen = RegionName.Length; pattern = pattern.NullEmpty() ?? "*"; + var wildcard = new Wildcard(pattern, RegexOptions.IgnoreCase); var enumerator = items.GetEnumerator(); while (enumerator.MoveNext()) { - string key = enumerator.Key as string; - if (key == null) - continue; - if (key.StartsWith(RegionName)) + if (enumerator.Key is string key) { - key = key.Substring(prefixLen); - if (pattern == "*" || key.StartsWith(pattern, StringComparison.OrdinalIgnoreCase)) + if (key.StartsWith(RegionName)) { - yield return key; + key = key.Substring(prefixLen); + if (pattern == "*" || wildcard.IsMatch(key)) + { + yield return key; + } } } } diff --git a/src/Libraries/SmartStore.Core/Collections/MultiMap.cs b/src/Libraries/SmartStore.Core/Collections/MultiMap.cs index 4d6c07ab28..6c8f85fc78 100644 --- a/src/Libraries/SmartStore.Core/Collections/MultiMap.cs +++ b/src/Libraries/SmartStore.Core/Collections/MultiMap.cs @@ -190,7 +190,7 @@ public virtual void AddRange(TKey key, IEnumerable values) { CheckNotReadonly(); - this[key].AddRange(values); + this[key].AddRange(values); } /// diff --git a/src/Libraries/SmartStore.Core/Collections/Querystring.cs b/src/Libraries/SmartStore.Core/Collections/Querystring.cs index 83184c1fef..ba5a3cc895 100644 --- a/src/Libraries/SmartStore.Core/Collections/Querystring.cs +++ b/src/Libraries/SmartStore.Core/Collections/Querystring.cs @@ -49,6 +49,7 @@ public static string ExtractQuerystring(string s) return s.Substring(s.IndexOf("?") + 1); } } + return s; } diff --git a/src/Libraries/SmartStore.Core/Collections/TreeNode.cs b/src/Libraries/SmartStore.Core/Collections/TreeNode.cs index 9a235e5817..e9710fac8c 100644 --- a/src/Libraries/SmartStore.Core/Collections/TreeNode.cs +++ b/src/Libraries/SmartStore.Core/Collections/TreeNode.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; @@ -21,16 +22,22 @@ public TreeNode(TValue value) } public TreeNode(TValue value, IEnumerable children) + : this(value) { - Value = value; - AppendRange(children); + if (children != null && children.Any()) + { + AppendRange(children); + } } public TreeNode(TValue value, IEnumerable> children) + : this(value) { // for serialization - Value = value; - AppendRange(children); + if (children != null && children.Any()) + { + AppendRange(children); + } } public TValue Value @@ -48,12 +55,32 @@ protected override TreeNode CreateInstance() value = ((ICloneable)value).Clone(); } - return new TreeNode(value); + var clonedNode = new TreeNode(value); + + // Assign or clone Metadata + if (_metadata != null && _metadata.Count > 0) + { + foreach (var kvp in _metadata) + { + var metadataValue = kvp.Value is ICloneable + ? ((ICloneable)kvp.Value).Clone() + : kvp.Value; + clonedNode.SetMetadata(kvp.Key, metadataValue); + } + } + + if (_id != null) + { + clonedNode._id = _id; + } + + return clonedNode; } - public TreeNode Append(TValue value) + public TreeNode Append(TValue value, object id = null) { var node = new TreeNode(value); + node._id = id; this.Append(node); return node; } @@ -63,9 +90,17 @@ public void AppendRange(IEnumerable values) values.Each(x => Append(x)); } - public TreeNode Prepend(TValue value) + public void AppendRange(IEnumerable values, Func idSelector) + { + Guard.NotNull(idSelector, nameof(idSelector)); + + values.Each(x => Append(x, idSelector(x))); + } + + public TreeNode Prepend(TValue value, object id = null) { var node = new TreeNode(value); + node._id = id; this.Prepend(node); return node; } @@ -115,6 +150,8 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist object objValue = null; object objChildren = null; + string id = null; + Dictionary metadata = null; reader.Read(); while (reader.TokenType == JsonToken.PropertyName) @@ -125,11 +162,21 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist reader.Read(); objValue = serializer.Deserialize(reader, valueType); } + else if (string.Equals(a, "Metadata", StringComparison.OrdinalIgnoreCase)) + { + reader.Read(); + metadata = serializer.Deserialize>(reader); + } else if (string.Equals(a, "Children", StringComparison.OrdinalIgnoreCase)) { reader.Read(); objChildren = serializer.Deserialize(reader, sequenceType); } + if (string.Equals(a, "Id", StringComparison.OrdinalIgnoreCase)) + { + reader.Read(); + id = serializer.Deserialize(reader); + } else { reader.Skip(); @@ -138,25 +185,63 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist reader.Read(); } - var treeNode = Activator.CreateInstance(objectType, new object[] { objValue, objChildren }); + var ctorParams = objChildren != null + ? new object[] { objValue, objChildren } + : new object[] { objValue }; + + var treeNode = Activator.CreateInstance(objectType, ctorParams); + + // Set Metadata + if (metadata != null && metadata.Count > 0) + { + var metadataProp = FastProperty.GetProperty(objectType, "Metadata", PropertyCachingStrategy.Cached); + metadataProp.SetValue(treeNode, metadata); + + if (id.HasValue()) + { + var idProp = FastProperty.GetProperty(objectType, "Id", PropertyCachingStrategy.Cached); + idProp.SetValue(treeNode, id); + } + } return treeNode; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { - var valueProp = FastProperty.GetProperty(value.GetType(), "Value", PropertyCachingStrategy.Cached); - var childrenProp = FastProperty.GetProperty(value.GetType(), "Children", PropertyCachingStrategy.Cached); - writer.WriteStartObject(); { + // Id + if (GetPropValue("Id", value) is object o) + { + writer.WritePropertyName("Id"); + serializer.Serialize(writer, o); + } + + // Value writer.WritePropertyName("Value"); - serializer.Serialize(writer, valueProp.GetValue(value)); + serializer.Serialize(writer, GetPropValue("Value", value)); + + // Metadata + if (GetPropValue("Metadata", value) is IDictionary dict && dict.Count > 0) + { + writer.WritePropertyName("Metadata"); + serializer.Serialize(writer, dict); + } - writer.WritePropertyName("Children"); - serializer.Serialize(writer, childrenProp.GetValue(value)); + // Children + if (GetPropValue("HasChildren", value) is bool b && b == true) + { + writer.WritePropertyName("Children"); + serializer.Serialize(writer, GetPropValue("Children", value)); + } } writer.WriteEndObject(); } + + private object GetPropValue(string name, object instance) + { + return FastProperty.GetProperty(instance.GetType(), name, PropertyCachingStrategy.Cached).GetValue(instance); + } } } diff --git a/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs b/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs index c88fe441da..695866869a 100644 --- a/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs +++ b/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs @@ -14,13 +14,79 @@ public abstract class TreeNodeBase where T : TreeNodeBase private int? _depth = null; private int _index = -1; - private IDictionary _metadata; + protected object _id; + private IDictionary> _idNodeMap; + + protected IDictionary _metadata; private readonly static ContextState> _contextState = new ContextState>("TreeNodeBase.ThreadMetadata"); public TreeNodeBase() { } + #region Id + + public object Id + { + get + { + return _id; + } + set + { + if (_parent != null) + { + var map = GetIdNodeMap(); + + if (_id != value) + { + if (_id != null && map.ContainsKey(_id)) + { + // Remove old id from map + map.Remove(_id); + } + } + + if (value != null) + { + map[value] = this; + } + } + + _id = value; + } + } + + public T SelectNodeById(object id) + { + if (id == null || IsLeaf) + return null; + + var map = GetIdNodeMap(); + var node = (T)map?.Get(id); + + if (node != null && !this.IsAncestorOfOrSelf(node)) + { + // Found node is NOT a child of this node + return null; + } + + return node; + } + + private IDictionary> GetIdNodeMap() + { + var map = this.Root._idNodeMap; + if (map == null) + { + map = this.Root._idNodeMap = new Dictionary>(); + } + + return map; + } + + #endregion + #region Metadata public IDictionary Metadata @@ -139,8 +205,18 @@ private void AttachTo(T newParent, int? index) if (_parent != null) { + // Detach from parent _parent.Remove((T)this); } + else + { + // Is a root node with a map: get rid of it. + if (_idNodeMap != null) + { + _idNodeMap.Clear(); + _idNodeMap = null; + } + } if (index == null) { @@ -155,6 +231,16 @@ private void AttachTo(T newParent, int? index) } _parent = newParent; + + // Set id in new id-node map + if (_id != null) + { + var map = GetIdNodeMap(); + if (map != null) + { + map[_id] = (T)this; + } + } } [JsonIgnore] @@ -379,11 +465,10 @@ public IEnumerable Trail var node = (T)this; do { - trail.Add(node); + trail.Insert(0, node); node = node._parent; } while (node != null); - - trail.Reverse(); + return trail; } } @@ -528,6 +613,16 @@ public void Remove(T node) var list = node._parent?._children; if (list.Remove(node)) { + // Remove id from id node map + if (node._id != null) + { + var map = node.GetIdNodeMap(); + if (map != null && map.ContainsKey(node._id)) + { + map.Remove(node._id); + } + } + FixIndexes(list, node._index, -1); node._index = -1; @@ -544,6 +639,12 @@ public void Clear() { _children.Clear(); } + + var map = GetIdNodeMap(); + if (map != null) + { + map.Clear(); + } } public void Traverse(Action action, bool includeSelf = false) @@ -576,7 +677,7 @@ public void TraverseParents(Action action, bool includeSelf = false) } } - protected IEnumerable FlattenNodes(bool includeSelf = true) + public IEnumerable FlattenNodes(bool includeSelf = true) { return this.FlattenNodes(null, includeSelf); } diff --git a/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs b/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs index f4347bbf8c..4ad5aebee4 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs @@ -224,9 +224,7 @@ public static FastProperty GetProperty( Guard.NotNull(type, nameof(type)); Guard.NotEmpty(propertyName, nameof(propertyName)); - FastProperty fastProperty = null; - - if (TryGetCachedProperty(type, propertyName, cachingStrategy == PropertyCachingStrategy.EagerCached, out fastProperty)) + if (TryGetCachedProperty(type, propertyName, cachingStrategy == PropertyCachingStrategy.EagerCached, out var fastProperty)) { return fastProperty; } @@ -254,9 +252,7 @@ public static FastProperty GetProperty( { Guard.NotNull(propertyInfo, nameof(propertyInfo)); - FastProperty fastProperty = null; - - if (TryGetCachedProperty(propertyInfo.ReflectedType, propertyInfo.Name, cachingStrategy == PropertyCachingStrategy.EagerCached, out fastProperty)) + if (TryGetCachedProperty(propertyInfo.ReflectedType, propertyInfo.Name, cachingStrategy == PropertyCachingStrategy.EagerCached, out var fastProperty)) { return fastProperty; } @@ -446,27 +442,34 @@ public static Action MakeFastPropertySetter(PropertyInfo propert /// instance, then a copy /// is returned. /// + /// Key selector + /// When true, converts all nested objects to dictionaries also /// /// The implementation of FastProperty will cache the property accessors per-type. This is /// faster when the the same type is used multiple times with ObjectToDictionary. /// - public static IDictionary ObjectToDictionary(object value, Func keySelector = null) + public static IDictionary ObjectToDictionary(object value, Func keySelector = null, bool deep = false) { - var dictionary = value as IDictionary; - if (dictionary != null) + if (value is IDictionary dictionary) { return new Dictionary(dictionary, StringComparer.OrdinalIgnoreCase); } - keySelector = keySelector ?? new Func(key => key); - dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); if (value != null) { + keySelector = keySelector ?? new Func(key => key); + foreach (var prop in GetProperties(value).Values) { - dictionary[keySelector(prop.Name)] = prop.GetValue(value); + var propValue = prop.GetValue(value); + if (deep && propValue != null && prop.Property.PropertyType.IsPlainObjectType()) + { + propValue = ObjectToDictionary(propValue, deep: true); + } + + dictionary[keySelector(prop.Name)] = propValue; } } @@ -536,8 +539,7 @@ protected static IDictionary GetVisibleProperties( ConcurrentDictionary> allPropertiesCache, ConcurrentDictionary> visiblePropertiesCache) { - IDictionary result; - if (visiblePropertiesCache.TryGetValue(type, out result)) + if (visiblePropertiesCache.TryGetValue(type, out var result)) { return result; } @@ -617,18 +619,17 @@ protected static IDictionary GetProperties( // part of the sequence of properties returned by this method. type = Nullable.GetUnderlyingType(type) ?? type; - IDictionary fastProperties; - if (!cache.TryGetValue(type, out fastProperties)) + return cache.GetOrAdd(type, Get); + + IDictionary Get(Type t) { - var candidates = GetCandidateProperties(type); - fastProperties = candidates.Select(p => createPropertyHelper(p)).ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase); - cache.TryAdd(type, fastProperties); + var candidates = GetCandidateProperties(t); + var fastProperties = candidates.Select(p => createPropertyHelper(p)).ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase); + return fastProperties; } - - return fastProperties; } - private static IEnumerable GetCandidateProperties(Type type) + internal static IEnumerable GetCandidateProperties(Type type) { // We avoid loading indexed properties using the Where statement. var properties = type.GetRuntimeProperties().Where(IsCandidateProperty); diff --git a/src/Libraries/SmartStore.Core/ComponentModel/HybridExpando.cs b/src/Libraries/SmartStore.Core/ComponentModel/HybridExpando.cs index 1049481e35..010ef62aae 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/HybridExpando.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/HybridExpando.cs @@ -37,10 +37,17 @@ using System.Dynamic; using System.Reflection; using System.Collections; +using SmartStore.Utilities; namespace SmartStore.ComponentModel { - /// + public enum MemberOptMethod + { + Allow, + Disallow + } + + /// /// Class that provides extensible properties and methods to an /// existing object when cast to dynamic. This /// dynamic object stores 'extra' properties in a dictionary or @@ -71,22 +78,33 @@ public class HybridExpando : DynamicObject, IDictionary /// private Type _instanceType; - /// - /// String Dictionary that contains the extra dynamic values - /// stored on this object/instance - /// - /// Using PropertyBag to support XML Serialization of the dictionary - public PropertyBag Properties = new PropertyBag(); + /// + /// Adjusted property list for the wrapped instance type after white/black-list members has been applied. + /// + private IDictionary _instanceProps; + private readonly static IDictionary EmptyProps = new Dictionary(); - /// - /// This constructor just works off the internal dictionary and any - /// public properties of this object. - /// - /// Note you can subclass Expando. - /// - public HybridExpando() + /// + /// String Dictionary that contains the extra dynamic values + /// stored on this object/instance + /// + /// Using PropertyBag to support XML Serialization of the dictionary + public PropertyBag Properties = new PropertyBag(); + + private readonly HashSet _optMembers; + private readonly MemberOptMethod _optMethod; + + private readonly bool _returnNullWhenFalsy; + + /// + /// This constructor just works off the internal dictionary and any + /// public properties of this object. + /// + /// Note you can subclass HybridExpando. + /// + public HybridExpando(bool returnNullWhenFalsy = false) { - Initialize(this); + _returnNullWhenFalsy = returnNullWhenFalsy; } /// @@ -97,16 +115,42 @@ public HybridExpando() /// check native properties and only check the Dictionary! /// /// - public HybridExpando(object instance) + public HybridExpando(object instance, bool returnNullWhenFalsy = false) { - Initialize(instance); + Guard.NotNull(instance, nameof(instance)); + + _returnNullWhenFalsy = returnNullWhenFalsy; + Initialize(instance); } - - protected void Initialize(object instance) + + /// + /// Allows passing in an existing instance variable to 'extend' + /// along with a list of member names to allow or disallow. + /// + /// + public HybridExpando(object instance, IEnumerable optMembers, MemberOptMethod optMethod, bool returnNullWhenFalsy = false) + { + Guard.NotNull(instance, nameof(instance)); + + _returnNullWhenFalsy = returnNullWhenFalsy; + Initialize(instance); + + _optMethod = optMethod; + + if (optMembers is HashSet h) + { + _optMembers = h; + } + else + { + _optMembers = new HashSet(optMembers); + } + } + + protected void Initialize(object instance) { _instance = instance; - if (instance != null) - _instanceType = instance.GetType(); + _instanceType = instance?.GetType(); } protected object WrappedObject @@ -116,19 +160,29 @@ protected object WrappedObject public override IEnumerable GetDynamicMemberNames() { - foreach (var prop in this.GetProperties(false)) - yield return prop.Key; - } + foreach (var kvp in this.Properties.Keys) + { + yield return kvp; + } + + foreach (var kvp in GetInstanceProperties()) + { + if (!this.Properties.ContainsKey(kvp.Key)) + { + yield return kvp.Key; + } + } + } - /// - /// Try to retrieve a member by name first from instance properties - /// followed by the collection entries. - /// - /// - /// - /// - public override bool TryGetMember(GetMemberBinder binder, out object result) + /// + /// Try to retrieve a member by name first from instance properties + /// followed by the collection entries. + /// + /// + /// + /// + public override bool TryGetMember(GetMemberBinder binder, out object result) { return TryGetMemberCore(binder.Name, out result); } @@ -136,27 +190,33 @@ public override bool TryGetMember(GetMemberBinder binder, out object result) protected virtual bool TryGetMemberCore(string name, out object result) { result = null; + bool exists = false; // first check the Properties collection for member - if (Properties.Keys.Contains(name)) + if (Properties.ContainsKey(name)) { result = Properties[name]; - return true; + exists = true; } // Next check for public properties via Reflection - if (_instance != null) + if (!exists && _instance != null) { try { - return GetProperty(_instance, name, out result); + exists = GetProperty(_instance, name, out result); } catch { } } + // Falsy check + if (_returnNullWhenFalsy && result != null && !CommonHelper.IsTruthy(result)) + { + result = null; + } + // failed to retrieve a property - result = null; - return false; + return exists; } @@ -175,6 +235,13 @@ public override bool TrySetMember(SetMemberBinder binder, object value) protected virtual bool TrySetMemberCore(string name, object value) { // first check to see if there's a native property to set + if (Properties.ContainsKey(name)) + { + Properties[name] = value; + return true; + } + + // Check to see if there's a native property to set if (_instance != null) { try @@ -198,6 +265,19 @@ protected virtual bool TrySetMemberCore(string name, object value) /// /// /// + public void Override(string name, object value = null) + { + Guard.NotEmpty(name, nameof(name)); + Properties[name] = value; + } + + /// + /// Dynamic invocation method. Currently allows only for Reflection based + /// operation (no ability to add methods dynamically). + /// + /// + /// + /// /// public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { @@ -226,8 +306,7 @@ public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, o /// protected bool GetProperty(object instance, string name, out object result) { - var fastProp = _instanceType != null ? FastProperty.GetProperty(_instanceType, name, PropertyCachingStrategy.EagerCached) : null; - if (fastProp != null) + if (GetInstanceProperties().TryGetValue(name, out var fastProp)) { result = fastProp.GetValue(instance ?? this); return true; @@ -246,12 +325,11 @@ protected bool GetProperty(object instance, string name, out object result) /// protected bool SetProperty(object instance, string name, object value) { - var fastProp = _instanceType != null ? FastProperty.GetProperty(_instanceType, name, PropertyCachingStrategy.EagerCached) : null; - if (fastProp != null) + if (GetInstanceProperties().TryGetValue(name, out var fastProp)) { fastProp.SetValue(instance ?? this, value); return true; - } + } return false; } @@ -267,7 +345,7 @@ protected bool SetProperty(object instance, string name, object value) protected bool InvokeMethod(object instance, string name, object[] args, out object result) { // Look at the instanceType - var mi = _instanceType != null ? _instanceType.GetMethod(name, BindingFlags.Instance | BindingFlags.Public) : null; + var mi = _instanceType?.GetMethod(name, BindingFlags.Instance | BindingFlags.Public); if (mi != null) { result = mi.Invoke(instance ?? this, args); @@ -302,8 +380,7 @@ public object this[string key] { get { - object result = null; - if (!TryGetMemberCore(key, out result)) + if (!TryGetMemberCore(key, out var result)) { throw new KeyNotFoundException(); } @@ -324,14 +401,14 @@ public object this[string key] /// public IEnumerable> GetProperties(bool includeInstanceProperties = false) { - foreach (var key in this.Properties.Keys) + foreach (var kvp in this.Properties) { - yield return new KeyValuePair(key, this.Properties[key]); + yield return kvp; } - if (includeInstanceProperties && _instance != null) + if (includeInstanceProperties) { - foreach (var prop in FastProperty.GetProperties(_instance).Values) + foreach (var prop in GetInstanceProperties().Values) { if (!this.Properties.ContainsKey(prop.Name)) { @@ -339,9 +416,32 @@ public IEnumerable> GetProperties(bool includeInsta } } } - } + private IDictionary GetInstanceProperties() + { + if (_instance == null) + { + return EmptyProps; + } + + if (_instanceProps == null) + { + var props = FastProperty.GetProperties(_instance) as IDictionary; + + if (_optMembers != null) + { + props = props + .Where(x => _optMethod == MemberOptMethod.Allow ? _optMembers.Contains(x.Key) : !_optMembers.Contains(x.Key)) + .ToDictionary(x => x.Key, x => x.Value); + } + + _instanceProps = props; + } + + return _instanceProps; + } + /// /// Checks whether a property exists in the Property collection /// or as a property on the instance @@ -367,10 +467,10 @@ public bool Contains(string propertyName, bool includeInstanceProperties = false return true; } - if (includeInstanceProperties && _instance != null) + if (includeInstanceProperties) { - return FastProperty.GetProperties(_instance).ContainsKey(propertyName); - } + return GetInstanceProperties().ContainsKey(propertyName); + } return false; } @@ -397,15 +497,9 @@ int ICollection>.Count { get { - var count = Properties.Count; - if (_instanceType != null) - { - count += FastProperty.GetProperties(_instanceType).Count; + return GetDynamicMemberNames().Count(); } - - return count; } - } bool ICollection>.IsReadOnly { diff --git a/src/Libraries/SmartStore.Core/ComponentModel/MiniMapper.cs b/src/Libraries/SmartStore.Core/ComponentModel/MiniMapper.cs new file mode 100644 index 0000000000..40e6412f8f --- /dev/null +++ b/src/Libraries/SmartStore.Core/ComponentModel/MiniMapper.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Reflection; +using System.Globalization; +using SmartStore.Utilities; + +namespace SmartStore.ComponentModel +{ + /// + /// A very simple object mapper utility which tries to map properties of the same name between two objects. + /// If matched properties has different types, the mapper tries to convert them. + /// If conversion fails, the property is skipped (no exception is thrown). + /// MiniMapper cannot handle sequence and predefined types. + /// + public static class MiniMapper + { + public static TTo Map(TFrom from, CultureInfo culture = null) + where TFrom : class + where TTo : class + { + var to = Map(from, typeof(TTo), culture); + return (TTo)to; + } + + //public static void Map(TFrom from, TTo to, CultureInfo culture = null) + // where TFrom : class + // where TTo : class + //{ + // Map((object)from, (object)to, culture); + //} + + public static object Map(TFrom from, Type toType, CultureInfo culture = null) + where TFrom : class + { + Guard.NotNull(toType, nameof(toType)); + Guard.HasDefaultConstructor(toType); + + var target = Activator.CreateInstance(toType); + + Map(from, target, culture); + return target; + } + + public static void Map(TFrom from, TTo to, CultureInfo culture = null) + where TFrom : class + where TTo : class + { + Guard.NotNull(from, nameof(from)); + Guard.NotNull(to, nameof(to)); + + if (object.ReferenceEquals(from, to)) + { + // Cannot map the same instance + return; + } + + var fromType = from.GetType(); + var toType = to.GetType(); + + ValidateType(fromType); + ValidateType(toType); + + if (culture == null) + { + culture = CultureInfo.CurrentCulture; + } + + var toProps = GetFastPropertiesFor(toType).ToArray(); + + foreach (var toProp in toProps) + { + var fromProp = FastProperty.GetProperty(fromType, toProp.Name, PropertyCachingStrategy.Uncached); + if (fromProp == null) + { + continue; + } + + object value = null; + try + { + // Get the value from source instance and try to convert it to target props type + value = fromProp.GetValue(from).Convert(toProp.Property.PropertyType, culture); + + // Set it + toProp.SetValue(to, value); + } + catch { } + } + } + + private static IEnumerable GetFastPropertiesFor(Type type) + { + return FastProperty.GetCandidateProperties(type) + .Select(pi => FastProperty.GetProperty(pi, PropertyCachingStrategy.Uncached)) + .Where(pi => pi.IsPublicSettable); + } + + private static void ValidateType(Type type) + { + if (type.IsPredefinedType()) + { + throw new InvalidOperationException("Mapping from or to predefined types is not possible. Type was: {0}".FormatInvariant(type.FullName)); + } + + if (type.IsSequenceType()) + { + throw new InvalidOperationException("Mapping from or to sequence types is not possible. Type was: {0}".FormatInvariant(type.FullName)); + } + } + } +} diff --git a/src/Libraries/SmartStore.Core/ComponentModel/PropertyBag.cs b/src/Libraries/SmartStore.Core/ComponentModel/PropertyBag.cs index bec72dcea5..328ef76ce0 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/PropertyBag.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/PropertyBag.cs @@ -288,8 +288,7 @@ public void ReadXml(System.Xml.XmlReader reader) /// XML String or Null if it fails public string ToXml() { - string xml = null; - SerializationUtils.SerializeObject(this, out xml); + SerializationUtils.SerializeObject(this, out var xml); return xml; } @@ -306,8 +305,7 @@ public bool FromXml(string xml) if (string.IsNullOrEmpty(xml)) return true; - var result = SerializationUtils.DeSerializeObject(xml, this.GetType()) as PropertyBag; - if (result != null) + if (SerializationUtils.DeSerializeObject(xml, this.GetType()) is PropertyBag result) { foreach (var item in result) { diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/DictionaryConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/DictionaryConverter.cs new file mode 100644 index 0000000000..a7e3c60887 --- /dev/null +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/DictionaryConverter.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Globalization; +using System.Web.Routing; +using SmartStore.Utilities; + +namespace SmartStore.ComponentModel +{ + public class DictionaryTypeConverter : TypeConverterBase where T : IDictionary + { + public DictionaryTypeConverter() + : base(typeof(object)) + { + } + + public override bool CanConvertFrom(Type type) + { + return !type.IsPredefinedType() && type.IsClass; + } + + public override bool CanConvertTo(Type type) + { + return !type.IsPredefinedType() && DictionaryConverter.CanCreateType(type); + } + + public override object ConvertFrom(CultureInfo culture, object value) + { + // Obj > Dict + var dict = CommonHelper.ObjectToDictionary(value); + var to = typeof(T); + + if (to == typeof(RouteValueDictionary)) + { + return new RouteValueDictionary(dict); + } + if (to == typeof(Dictionary)) + { + return (Dictionary)dict; + } + else if (to == typeof(ExpandoObject)) + { + var expando = new ExpandoObject(); + expando.Merge(dict); + return expando; + } + else + { + return dict; + } + } + + public override object ConvertTo(CultureInfo culture, string format, object value, Type to) + { + // Dict > Obj + if (value is IDictionary dict) + { + return DictionaryConverter.CreateAndPopulate(to, dict, out var problems); + } + + return base.ConvertTo(culture, format, value, to); + } + } +} diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EmailAddressConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EmailAddressConverter.cs new file mode 100644 index 0000000000..5a2c45755b --- /dev/null +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EmailAddressConverter.cs @@ -0,0 +1,43 @@ +using System; +using System.Globalization; +using SmartStore.Core.Email; + +namespace SmartStore.ComponentModel +{ + public class EmailAddressConverter : TypeConverterBase + { + public EmailAddressConverter() : base(typeof(object)) + { + } + + public override bool CanConvertFrom(Type type) + { + return type == typeof(string); + } + + public override bool CanConvertTo(Type type) + { + return type == typeof(string); + } + + public override object ConvertFrom(CultureInfo culture, object value) + { + if (value is string str && str.HasValue()) + { + return new EmailAddress(str); + } + + return base.ConvertFrom(culture, value); + } + + public override object ConvertTo(CultureInfo culture, string format, object value, Type to) + { + if (to == typeof(string) && value is EmailAddress address) + { + return address.ToString(); + } + + return base.ConvertTo(culture, format, value, to); + } + } +} diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs index 974b7cc973..5687317362 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs @@ -122,9 +122,8 @@ public override object ConvertTo(CultureInfo culture, string format, object valu if (to == typeof(string)) { string result = string.Empty; - var enumerable = value as IEnumerable; - if (enumerable != null) + if (value is IEnumerable enumerable) { // we don't use string.Join() because it doesn't support invariant culture foreach (var token in enumerable) diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/NullableConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/NullableConverter.cs index d11c53409f..8488e2aa3e 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/NullableConverter.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/NullableConverter.cs @@ -68,7 +68,7 @@ public override bool CanConvertFrom(Type type) public override bool CanConvertTo(Type type) { - Console.WriteLine("NullableConverter can convert to {0}: {1}".FormatInvariant(type.Name, UnderlyingTypeConverter.CanConvertTo(type))); + //Console.WriteLine("NullableConverter can convert to {0}: {1}".FormatInvariant(type.Name, UnderlyingTypeConverter.CanConvertTo(type))); if (type == this.UnderlyingType) { diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ShippingOptionConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ShippingOptionConverter.cs index b79c9a5f2f..54df72e96b 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ShippingOptionConverter.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ShippingOptionConverter.cs @@ -32,10 +32,9 @@ public override bool CanConvertTo(Type type) public override object ConvertFrom(CultureInfo culture, object value) { - if (value is string) + if (value is string str) { object result = null; - string str = value as string; if (!String.IsNullOrEmpty(str)) { try diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterFactory.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterFactory.cs index b9f0f504a4..299ab07a07 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterFactory.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterFactory.cs @@ -2,8 +2,11 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; +using System.Dynamic; +using System.Web.Routing; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Shipping; +using SmartStore.Core.Email; namespace SmartStore.ComponentModel { @@ -33,6 +36,14 @@ private static void CreateDefaultConverters() _typeConverters.TryAdd(typeof(IList), converter); _typeConverters.TryAdd(typeof(List), converter); _typeConverters.TryAdd(typeof(ProductBundleItemOrderData), new ProductBundleDataConverter(false)); + + converter = new DictionaryTypeConverter>(); + _typeConverters.TryAdd(typeof(IDictionary), converter); + _typeConverters.TryAdd(typeof(Dictionary), converter); + _typeConverters.TryAdd(typeof(RouteValueDictionary), new DictionaryTypeConverter()); + _typeConverters.TryAdd(typeof(ExpandoObject), new DictionaryTypeConverter()); + + _typeConverters.TryAdd(typeof(EmailAddress), new EmailAddressConverter()); } public static void RegisterConverter(ITypeConverter typeConverter) @@ -57,8 +68,7 @@ public static ITypeConverter RemoveConverter(Type type) { Guard.NotNull(type, nameof(type)); - ITypeConverter converter = null; - _typeConverters.TryRemove(type, out converter); + _typeConverters.TryRemove(type, out var converter); return converter; } @@ -78,8 +88,7 @@ public static ITypeConverter GetConverter(Type type) { Guard.NotNull(type, nameof(type)); - ITypeConverter converter; - if (_typeConverters.TryGetValue(type, out converter)) + if (_typeConverters.TryGetValue(type, out var converter)) { return converter; } diff --git a/src/Libraries/SmartStore.Core/Configuration/ISettingService.cs b/src/Libraries/SmartStore.Core/Configuration/ISettingService.cs index e2c1337143..2f5f65575c 100644 --- a/src/Libraries/SmartStore.Core/Configuration/ISettingService.cs +++ b/src/Libraries/SmartStore.Core/Configuration/ISettingService.cs @@ -71,15 +71,22 @@ bool SettingExists(T settings, /// Store identifier for which settigns should be loaded T LoadSetting(int storeId = 0) where T : ISettings, new(); - /// - /// Set setting value - /// - /// Type - /// Key - /// Value + /// + /// Load settings + /// + /// Setting class type + /// Store identifier for which settigns should be loaded + ISettings LoadSetting(Type settingType, int storeId = 0); + + /// + /// Set setting value + /// + /// Type + /// Key + /// Value /// Store identifier - /// A value indicating whether to clear cache after setting update - void SetSetting(string key, T value, int storeId = 0, bool clearCache = true); + /// A value indicating whether to clear cache after setting update + void SetSetting(string key, T value, int storeId = 0, bool clearCache = true); /// /// Save settings object @@ -89,6 +96,13 @@ bool SettingExists(T settings, /// Store identifier void SaveSetting(T settings, int storeId = 0) where T : ISettings, new(); + /// + /// Save settings object + /// + /// Setting instance + /// Store identifier + void SaveSetting(ISettings settings, int storeId = 0); + /// /// Save settings object /// diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/DbLoadHook.cs b/src/Libraries/SmartStore.Core/Data/Hooks/DbLoadHook.cs index 6cebe26609..b2e9d20731 100644 --- a/src/Libraries/SmartStore.Core/Data/Hooks/DbLoadHook.cs +++ b/src/Libraries/SmartStore.Core/Data/Hooks/DbLoadHook.cs @@ -6,6 +6,7 @@ public abstract class DbLoadHook : IDbLoadHook { public virtual void OnLoaded(BaseEntity entity) { + throw new NotImplementedException(); } } } diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/DbSaveHook.cs b/src/Libraries/SmartStore.Core/Data/Hooks/DbSaveHook.cs index b77114ed60..656ebc60be 100644 --- a/src/Libraries/SmartStore.Core/Data/Hooks/DbSaveHook.cs +++ b/src/Libraries/SmartStore.Core/Data/Hooks/DbSaveHook.cs @@ -2,10 +2,9 @@ namespace SmartStore.Core.Data.Hooks { - public abstract class DbSaveHook : IDbSaveHook - where TEntity : class + public abstract class DbSaveHook : IDbSaveHook where TEntity : class { - public void OnBeforeSave(HookedEntity entry) + public virtual void OnBeforeSave(IHookedEntity entry) { var entity = entry.Entity as TEntity; switch (entry.InitialState) @@ -22,16 +21,19 @@ public void OnBeforeSave(HookedEntity entry) } } - protected virtual void OnInserting(TEntity entity, HookedEntity entry) + protected virtual void OnInserting(TEntity entity, IHookedEntity entry) { + throw new NotImplementedException(); } - protected virtual void OnUpdating(TEntity entity, HookedEntity entry) + protected virtual void OnUpdating(TEntity entity, IHookedEntity entry) { + throw new NotImplementedException(); } - protected virtual void OnDeleting(TEntity entity, HookedEntity entry) + protected virtual void OnDeleting(TEntity entity, IHookedEntity entry) { + throw new NotImplementedException(); } public virtual void OnBeforeSaveCompleted() @@ -39,7 +41,7 @@ public virtual void OnBeforeSaveCompleted() } - public void OnAfterSave(HookedEntity entry) + public virtual void OnAfterSave(IHookedEntity entry) { var entity = entry.Entity as TEntity; switch (entry.InitialState) @@ -56,16 +58,19 @@ public void OnAfterSave(HookedEntity entry) } } - protected virtual void OnInserted(TEntity entity, HookedEntity entry) + protected virtual void OnInserted(TEntity entity, IHookedEntity entry) { + throw new NotImplementedException(); } - protected virtual void OnUpdated(TEntity entity, HookedEntity entry) + protected virtual void OnUpdated(TEntity entity, IHookedEntity entry) { + throw new NotImplementedException(); } - protected virtual void OnDeleted(TEntity entity, HookedEntity entry) + protected virtual void OnDeleted(TEntity entity, IHookedEntity entry) { + throw new NotImplementedException(); } public virtual void OnAfterSaveCompleted() diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs b/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs index 770f115e1e..8152d3fd3e 100644 --- a/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs +++ b/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using SmartStore.Collections; using SmartStore.Core.Logging; @@ -14,17 +12,21 @@ public class DefaultDbHookHandler : IDbHookHandler private readonly IList> _loadHooks; private readonly IList> _saveHooks; - private readonly Multimap _loadHooksRequestCache = new Multimap(); - private readonly Multimap _saveHooksRequestCache = new Multimap(); + private readonly Multimap _hooksRequestCache = new Multimap(); // Prevents repetitive hooking of the same entity/state/[pre|post] combination within a single request private readonly HashSet _hookedEntities = new HashSet(); - private readonly static ConcurrentDictionary _hookableEntities = new ConcurrentDictionary(); private static HashSet _importantLoadHookTypes; private static HashSet _importantSaveHookTypes; private readonly static object _lock = new object(); + // Contains all IDbHook/EntityType/State/Stage combinations in which + // the implementor threw either NotImplementedException or NotSupportedException. + // This boosts performance because these VOID combinations are not processed again + // and frees us mostly from the obligation always to detect changes. + private readonly static HashSet _voidHooks = new HashSet(); + public DefaultDbHookHandler(IEnumerable> hooks) { _hooks = hooks; @@ -72,58 +74,64 @@ public bool HasImportantSaveHooks() return _importantSaveHookTypes.Any(); } - public void TriggerLoadHooks(BaseEntity entity, bool importantHooksOnly) + public IEnumerable TriggerLoadHooks(BaseEntity entity, bool importantHooksOnly) { + Guard.NotNull(entity, nameof(entity)); + + var processedHooks = new HashSet(); + if (!_loadHooks.Any() || (importantHooksOnly && !this.HasImportantLoadHooks())) { - return; + return processedHooks; } - if (entity == null || !IsHookableEntity(entity)) - { - return; - } + var entityType = entity.GetUnproxiedType(); - var loadHooks = GetLoadHookInstancesFor(entity, importantHooksOnly); - foreach (var hook in loadHooks) + var hooks = GetLoadHookInstancesFor(entityType, importantHooksOnly); + foreach (var hook in hooks) { // call hook try { hook.OnLoaded(entity); + processedHooks.Add(hook); + } + catch (Exception ex) when (ex is NotImplementedException || ex is NotSupportedException) + { + RegisterVoidHook(hook, entityType, EntityState.Unchanged, HookStage.Load); } catch (Exception ex) { Logger.ErrorFormat(ex, "LoadHook exception ({0})", hook.GetType().FullName); } } + + return processedHooks; } - public bool TriggerPreSaveHooks(IEnumerable entries, bool importantHooksOnly) + public IEnumerable TriggerPreSaveHooks(IEnumerable entries, bool importantHooksOnly, out bool anyStateChanged) { - bool anyStateChanged = false; + Guard.NotNull(entries, nameof(entries)); - if (entries != null) - { - // Skip entities explicitly marked as unhookable - entries = entries.Where(IsHookableEntry); - } - - if (entries == null || !entries.Any() || !_saveHooks.Any() || (importantHooksOnly && !this.HasImportantSaveHooks())) - return false; + anyStateChanged = false; var processedHooks = new HashSet(); + if (!entries.Any() || !_saveHooks.Any() || (importantHooksOnly && !this.HasImportantSaveHooks())) + return processedHooks; + foreach (var entry in entries) { var e = entry; // Prevents access to modified closure - var entity = e.Entity; - if (HandledAlready(entity, e.InitialState, false)) + + if (HandledAlready(e, HookStage.PreSave)) { // Prevent repetitive hooking of the same entity/state/pre combination within a single request continue; } - var hooks = GetSaveHookInstancesFor(entity, importantHooksOnly); + + var hooks = GetSaveHookInstancesFor(e, HookStage.PreSave, importantHooksOnly); + foreach (var hook in hooks) { // call hook @@ -133,6 +141,10 @@ public bool TriggerPreSaveHooks(IEnumerable entries, bool importan hook.OnBeforeSave(e); processedHooks.Add(hook); } + catch (Exception ex) when (ex is NotImplementedException || ex is NotSupportedException) + { + RegisterVoidHook(hook, e.EntityType, e.InitialState, HookStage.PreSave); + } catch (Exception ex) { Logger.ErrorFormat(ex, "PreSaveHook exception ({0})", hook.GetType().FullName); @@ -149,33 +161,31 @@ public bool TriggerPreSaveHooks(IEnumerable entries, bool importan processedHooks.Each(x => x.OnBeforeSaveCompleted()); - return anyStateChanged; + return processedHooks; } - public void TriggerPostSaveHooks(IEnumerable entries, bool importantHooksOnly) + public IEnumerable TriggerPostSaveHooks(IEnumerable entries, bool importantHooksOnly) { - if (entries != null) - { - // Skip entities explicitly marked as unhookable - entries = entries.Where(IsHookableEntry); - } - - if (entries == null || !entries.Any() || !_saveHooks.Any() || (importantHooksOnly && !this.HasImportantSaveHooks())) - return; + Guard.NotNull(entries, nameof(entries)); var processedHooks = new HashSet(); + if (!entries.Any() || !_saveHooks.Any() || (importantHooksOnly && !this.HasImportantSaveHooks())) + return processedHooks; + foreach (var entry in entries) { var e = entry; // Prevents access to modified closure - var entity = e.Entity; - if (HandledAlready(entity, e.InitialState, true)) + + if (HandledAlready(e, HookStage.PostSave)) { // Prevent repetitive hooking of the same entity/state/post combination within a single request continue; } - var postHooks = GetSaveHookInstancesFor(entity, importantHooksOnly); - foreach (var hook in postHooks) + + var hooks = GetSaveHookInstancesFor(e, HookStage.PostSave, importantHooksOnly); + + foreach (var hook in hooks) { // call hook try @@ -184,6 +194,10 @@ public void TriggerPostSaveHooks(IEnumerable entries, bool importa hook.OnAfterSave(e); processedHooks.Add(hook); } + catch (Exception ex) when (ex is NotImplementedException || ex is NotSupportedException) + { + RegisterVoidHook(hook, e.EntityType, e.InitialState, HookStage.PostSave); + } catch (Exception ex) { Logger.ErrorFormat(ex, "PostSaveHook exception ({0})", hook.GetType().FullName); @@ -192,108 +206,131 @@ public void TriggerPostSaveHooks(IEnumerable entries, bool importa } processedHooks.Each(x => x.OnAfterSaveCompleted()); + + return processedHooks; } - [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] - private IEnumerable GetLoadHookInstancesFor(BaseEntity entity, bool importantOnly) + private IEnumerable GetLoadHookInstancesFor(Type entityType, bool importantOnly) { - return GetHookInstancesFor(entity, importantOnly, - _loadHooks, - _loadHooksRequestCache, + return GetHookInstancesFor( + entityType, + EntityState.Unchanged, + HookStage.Load, + importantOnly, + _loadHooks, _importantLoadHookTypes); } - [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] - private IEnumerable GetSaveHookInstancesFor(BaseEntity entity, bool importantOnly) + private IEnumerable GetSaveHookInstancesFor(IHookedEntity entry, HookStage stage, bool importantOnly) { - return GetHookInstancesFor(entity, importantOnly, - _saveHooks, - _saveHooksRequestCache, + return GetHookInstancesFor( + entry.EntityType, + entry.InitialState, + stage, + importantOnly, + _saveHooks, _importantSaveHookTypes); } - [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] private IEnumerable GetHookInstancesFor( - BaseEntity entity, + Type entityType, + EntityState entityState, + HookStage stage, bool importantOnly, IList> hookList, - Multimap requestCache, - HashSet importantTypes) where THook : IDbHook + HashSet importantHookTypes) where THook : IDbHook { - if (entity == null) + IEnumerable hooks; + + if (entityType == null) { return Enumerable.Empty(); - } - - IEnumerable hooks; + } - var hookedType = entity.GetUnproxiedType(); + // For request cache lookup + var requestKey = new RequestHookKey(entityType, entityState, stage, importantOnly); - if (requestCache.ContainsKey(hookedType)) + if (_hooksRequestCache.ContainsKey(requestKey)) { - hooks = requestCache[hookedType]; + hooks = _hooksRequestCache[requestKey]; } else { - hooks = hookList.Where(x => x.Metadata.HookedType.IsAssignableFrom(hookedType)).Select(x => (THook)x.Value); - requestCache.AddRange(hookedType, hooks); - } - - if (importantOnly && hooks.Any()) - { - hooks = hooks.Where(x => importantTypes.Contains(x.GetType())); + hooks = hookList + // Reduce by entity types which can be processed by this hook + .Where(x => x.Metadata.HookedType.IsAssignableFrom(entityType)) + // When importantOnly, only include hook types with [ImportantAttribute] + .Where(x => !importantOnly || importantHookTypes.Contains(x.Metadata.ImplType)) + // Exclude void hooks (hooks known to be useless for the current EntityType/State/Stage combination) + .Where(x => !_voidHooks.Contains(new HookKey(x.Metadata.ImplType, entityType, entityState, stage))) + .Select(x => x.Value) + .ToArray(); + + _hooksRequestCache.AddRange(requestKey, hooks); } - return hooks; + return hooks.Cast(); } - private bool IsHookableEntry(HookedEntity entry) + private bool HandledAlready(IHookedEntity entry, HookStage stage) { var entity = entry.Entity; - if (entity == null) - { + + if (entity == null || entity.IsTransientRecord()) return false; + + var key = new HookedEntityKey(entry.EntityType, entity.Id, entry.InitialState, stage); + if (_hookedEntities.Contains(key)) + { + return true; } - return IsHookableEntity(entity); + _hookedEntities.Add(key); + return false; } - private bool IsHookableEntity(BaseEntity entity) + private void RegisterVoidHook(IDbHook hook, Type entityType, EntityState entityState, HookStage stage) { - var isHookable = _hookableEntities.GetOrAdd(entity.GetUnproxiedType(), t => - { - var attr = t.GetAttribute(true); - if (attr != null) - { - return attr.IsHookable; - } + var hookType = hook.GetType(); - // Entities are hookable by default - return true; - }); + // Unregister from request cache (if cached) + _hooksRequestCache.Remove(new RequestHookKey(entityType, entityState, stage, false), hook); + _hooksRequestCache.Remove(new RequestHookKey(entityType, entityState, stage, true), hook); - return isHookable; + lock (_lock) + { + // Add to static void hooks set + _voidHooks.Add(new HookKey(hookType, entityType, entityState, stage)); + } } - private bool HandledAlready(BaseEntity entity, EntityState initialState, bool isPostSaveHook) + enum HookStage { - if (entity.IsTransientRecord()) - return false; + Load, + PreSave, + PostSave + } - var key = new HookedEntityKey(entity.GetUnproxiedType(), entity.Id, initialState, isPostSaveHook); - if (_hookedEntities.Contains(key)) + class HookedEntityKey : Tuple + { + public HookedEntityKey(Type entityType, int entityId, EntityState initialState, HookStage stage) + : base(entityType, entityId, initialState, stage) { - return true; } + } - _hookedEntities.Add(key); - return false; + class RequestHookKey : Tuple + { + public RequestHookKey(Type entityType, EntityState entityState, HookStage stage, bool importantOnly) + : base(entityType, entityState, stage, importantOnly) + { + } } - class HookedEntityKey : Tuple + class HookKey : Tuple { - public HookedEntityKey(Type entityType, int entityId, EntityState initialState, bool isPostSaveHook) - : base(entityType, entityId, initialState, isPostSaveHook) + public HookKey(Type hookType, Type entityType, EntityState entityState, HookStage stage) + : base(hookType, entityType, entityState, stage) { } } diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/HookedEntity.cs b/src/Libraries/SmartStore.Core/Data/Hooks/HookedEntity.cs index 7e5d1b165b..2175d189d9 100644 --- a/src/Libraries/SmartStore.Core/Data/Hooks/HookedEntity.cs +++ b/src/Libraries/SmartStore.Core/Data/Hooks/HookedEntity.cs @@ -1,18 +1,61 @@ -using System.Data.Entity.Infrastructure; +using System; +using System.Data.Entity.Infrastructure; +using EfState = System.Data.Entity.EntityState; namespace SmartStore.Core.Data.Hooks { - public sealed class HookedEntity + public interface IHookedEntity { + /// + /// Gets the hooked entity entry + /// + DbEntityEntry Entry { get; } + + BaseEntity Entity { get; } + + Type EntityType { get; } + + /// + /// Gets or sets the initial (presave) state of the hooked entity. + /// The setter is for internal use only, don't invoke! + /// + EntityState InitialState { get; set; } + + /// + /// Gets or sets the current state of the hooked entity + /// + EntityState State { get; set; } + + /// + /// Gets a value indicating whether the entity state has changed during hooking. + /// + bool HasStateChanged { get; } + + /// + /// Gets a value indicating whether a property has been modified. + /// + /// Name of the property + bool IsPropertyModified(string propertyName); + + /// + /// Gets a value indicating whether the entity is in soft deleted state. + /// This is the case when the entity is an instance of + /// and the value of its Deleted property is true AND has changed since tracking. + /// But when the entity is not in modified state the snapshot comparison is omitted. + /// + bool IsSoftDeleted { get; } + } + + public class HookedEntity : IHookedEntity + { + private Type _entityType; + public HookedEntity(DbEntityEntry entry) { Entry = entry; InitialState = (EntityState)entry.State; } - /// - /// Gets the hooked entity entry - /// public DbEntityEntry Entry { get; @@ -24,18 +67,20 @@ public BaseEntity Entity get { return Entry.Entity as BaseEntity; } } - /// - /// Gets the initial (presave) state of the hooked entity - /// + public Type EntityType + { + get + { + return _entityType ?? (_entityType = this.Entity?.GetUnproxiedType()); + } + } + public EntityState InitialState { get; - internal set; + set; } - /// - /// Gets or sets the current state of the hooked entity - /// public EntityState State { get @@ -44,13 +89,10 @@ public EntityState State } set { - Entry.State = (System.Data.Entity.EntityState)((int)value); + Entry.State = (EfState)((int)value); } } - /// - /// Gets a value indicating whether the entity state has changed during hooking. - /// public bool HasStateChanged { get @@ -59,10 +101,6 @@ public bool HasStateChanged } } - /// - /// Gets a value indicating whether a property has been modified. - /// - /// Name of the property public bool IsPropertyModified(string propertyName) { Guard.NotEmpty(propertyName, nameof(propertyName)); @@ -80,5 +118,21 @@ public bool IsPropertyModified(string propertyName) return false; } + + public bool IsSoftDeleted + { + get + { + var entity = Entry.Entity as ISoftDeletable; + if (entity != null) + { + return Entry.State == EfState.Modified + ? entity.Deleted && IsPropertyModified("Deleted") + : entity.Deleted; + } + + return false; + } + } } } diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/IDbHookHandler.cs b/src/Libraries/SmartStore.Core/Data/Hooks/IDbHookHandler.cs index a4c44aaf71..ed010a5313 100644 --- a/src/Libraries/SmartStore.Core/Data/Hooks/IDbHookHandler.cs +++ b/src/Libraries/SmartStore.Core/Data/Hooks/IDbHookHandler.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace SmartStore.Core.Data.Hooks { @@ -11,24 +12,27 @@ public interface IDbHookHandler /// /// Triggers all load hooks for a single entity /// - /// + /// Whether to trigger only hooks marked with the attribute /// The loaded entity - void TriggerLoadHooks(BaseEntity entity, bool importantHooksOnly); + /// The list of actually processed hook instances + IEnumerable TriggerLoadHooks(BaseEntity entity, bool importantHooksOnly); /// /// Triggers all pre action hooks /// /// Entries - /// - /// true if the state of any entry changed - bool TriggerPreSaveHooks(IEnumerable entries, bool importantHooksOnly); + /// Whether to trigger only hooks marked with the attribute + /// true if the state of any entry changed + /// The list of actually processed hook instances + IEnumerable TriggerPreSaveHooks(IEnumerable entries, bool importantHooksOnly, out bool anyStateChanged); /// /// Triggers all post action hooks /// /// Entries - /// - void TriggerPostSaveHooks(IEnumerable entries, bool importantHooksOnly); + /// Whether to trigger only hooks marked with the attribute + /// The list of actually processed hook instances + IEnumerable TriggerPostSaveHooks(IEnumerable entries, bool importantHooksOnly); } public sealed class NullDbHookHandler : IDbHookHandler @@ -50,17 +54,20 @@ public bool HasImportantSaveHooks() return false; } - public void TriggerLoadHooks(BaseEntity entity, bool importantHooksOnly) + public IEnumerable TriggerLoadHooks(BaseEntity entity, bool importantHooksOnly) { + return Enumerable.Empty(); } - public bool TriggerPreSaveHooks(IEnumerable entries, bool importantHooksOnly) + public IEnumerable TriggerPreSaveHooks(IEnumerable entries, bool importantHooksOnly, out bool anyStateChanged) { - return false; + anyStateChanged = false; + return Enumerable.Empty(); } - public void TriggerPostSaveHooks(IEnumerable entries, bool importantHooksOnly) + public IEnumerable TriggerPostSaveHooks(IEnumerable entries, bool importantHooksOnly) { + return Enumerable.Empty(); } } } diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/IDbSaveHook.cs b/src/Libraries/SmartStore.Core/Data/Hooks/IDbSaveHook.cs index 1df608bb5e..3a1161aa39 100644 --- a/src/Libraries/SmartStore.Core/Data/Hooks/IDbSaveHook.cs +++ b/src/Libraries/SmartStore.Core/Data/Hooks/IDbSaveHook.cs @@ -4,12 +4,16 @@ namespace SmartStore.Core.Data.Hooks { /// /// A hook that is executed before and after a database save operation. + /// An implementor should raise or + /// to signal the hook handler + /// that it never should process the hook again for the current + /// EntityType/State/Stage combination. /// public interface IDbSaveHook : IDbHook { - void OnBeforeSave(HookedEntity entry); + void OnBeforeSave(IHookedEntity entry); - void OnAfterSave(HookedEntity entry); + void OnAfterSave(IHookedEntity entry); /// /// Called after all entities in the current unit of work has been handled right before saving changes to the database diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/ImportantAttribute.cs b/src/Libraries/SmartStore.Core/Data/Hooks/ImportantAttribute.cs index 097e871f58..585988ee7c 100644 --- a/src/Libraries/SmartStore.Core/Data/Hooks/ImportantAttribute.cs +++ b/src/Libraries/SmartStore.Core/Data/Hooks/ImportantAttribute.cs @@ -3,7 +3,7 @@ namespace SmartStore.Core.Data.Hooks { /// - /// Indicates that a hook instance should run in any case, even if hooking has been turned off. + /// Indicates that a hook instance should run in any case, even when hooking has been turned off. /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public sealed class ImportantAttribute : Attribute diff --git a/src/Libraries/SmartStore.Core/Data/IDbContext.cs b/src/Libraries/SmartStore.Core/Data/IDbContext.cs index a64a660a5c..f043ccf175 100644 --- a/src/Libraries/SmartStore.Core/Data/IDbContext.cs +++ b/src/Libraries/SmartStore.Core/Data/IDbContext.cs @@ -14,7 +14,6 @@ public interface IDbContext DbSet Set() where TEntity : BaseEntity; int SaveChanges(); - Task SaveChangesAsync(); IList ExecuteStoredProcedureList(string commandText, params object[] parameters) @@ -38,9 +37,6 @@ IList ExecuteStoredProcedureList(string commandText, params ob /// The result returned by the database after executing the command. int ExecuteSqlCommand(string sql, bool doNotEnsureTransaction = false, int? timeout = null, params object[] parameters); - /// Executes sql by using SQL-Server Management Objects which supports GO statements. - int ExecuteSqlThroughSmo(string sql); - string Alias { get; } // increasing performance on bulk operations @@ -67,6 +63,37 @@ IList ExecuteStoredProcedureList(string commandText, params ob /// bool AutoCommitEnabled { get; set; } + /// + /// Detects changes made to the properties and relationships of POCO entities. + /// Please note that normally DetectChanges is called automatically by many of the methods of + /// DbContext and its related classes such that it is rare that this method will need to be called explicitly. + /// However, it may be desirable, usually for performance reasons, to turn off this automatic + /// calling of DetectChanges using the AutoDetectChangesEnabled flag. + /// + void DetectChanges(); + + /// + /// Checks whether the underlying ORM mapper is currently in the process of detecting changes. + /// + /// + bool IsDetectingChanges(); + + /// + /// Gets a value indicating whether the given entity was modified since it has been attached to the context + /// + /// The entity to check + /// true if the entity was modified, false otherwise + bool IsModified(BaseEntity entity); + + /// + /// Determines whether an entity property has changed since it was attached. + /// + /// Entity + /// The property name to check + /// The previous/original property value if change was detected + /// true if property has changed, false otherwise + bool TryGetModifiedProperty(BaseEntity entity, string propertyName, out object originalValue); + /// /// Gets a list of modified properties for the specified entity /// @@ -121,8 +148,8 @@ IList ExecuteStoredProcedureList(string commandText, params ob /// /// Type of entity /// The entity instance - /// The new state - void ChangeState(TEntity entity, System.Data.Entity.EntityState newState) where TEntity : BaseEntity; + /// The requested new state + void ChangeState(TEntity entity, System.Data.Entity.EntityState requestedState) where TEntity : BaseEntity; /// /// Reloads the entity from the database overwriting any property values with values from the database. diff --git a/src/Libraries/SmartStore.Core/Data/IRepository.cs b/src/Libraries/SmartStore.Core/Data/IRepository.cs index cae856f654..8337168c58 100644 --- a/src/Libraries/SmartStore.Core/Data/IRepository.cs +++ b/src/Libraries/SmartStore.Core/Data/IRepository.cs @@ -94,25 +94,6 @@ public partial interface IRepository where T : BaseEntity [Obsolete("Use the extension method from 'SmartStore.Core, SmartStore.Core.Data' instead")] IQueryable Expand(IQueryable query, Expression> path); - /// - /// Gets a value indicating whether the given entity was modified since it has been attached to the context - /// - /// The entity to check - /// true if the entity was modified, false otherwise - bool IsModified(T entity); - - /// - /// Gets a list of modified properties for the specified entity - /// - /// The entity instance for which to get modified properties for - /// - /// A dictionary, where the key is the name of the modified property - /// and the value is its ORIGINAL value (which was tracked when the entity - /// was attached to the context the first time) - /// Returns an empty dictionary if no modification could be detected. - /// - IDictionary GetModifiedProperties(T entity); - /// /// Returns the data context associated with the repository. /// diff --git a/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs b/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs index 6c1b34c4e9..a3d54c9f75 100644 --- a/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs +++ b/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs @@ -16,7 +16,7 @@ public static T GetFirst(this IRepository rs, Func predicate) whe public static IEnumerable GetMany(this IRepository rs, IEnumerable ids) where T : BaseEntity { - foreach (var chunk in ids.Chunk()) + foreach (var chunk in ids.Slice(128)) { var items = rs.Table.Where(a => chunk.Contains(a.Id)).ToList(); foreach (var item in items) @@ -34,8 +34,6 @@ public static void Delete(this IRepository rs, int id) where T : BaseEntit var entity = rs.Create(); entity.Id = id; - rs.Attach(entity); - // must downcast 'cause of Rhino mocks stub rs.Context.ChangeState((BaseEntity)entity, System.Data.Entity.EntityState.Deleted); } @@ -77,7 +75,7 @@ public static int DeleteAll(this IRepository rs, Expression> if (cascade) { var records = query.ToList(); - foreach (var chunk in records.Chunk(500)) + foreach (var chunk in records.Slice(500)) { rs.DeleteRange(chunk.ToList()); count += rs.Context.SaveChanges(); @@ -86,7 +84,7 @@ public static int DeleteAll(this IRepository rs, Expression> else { var ids = query.Select(x => new { Id = x.Id }).ToList(); - foreach (var chunk in ids.Chunk(500)) + foreach (var chunk in ids.Slice(500)) { rs.DeleteRange(chunk.Select(x => x.Id)); count += rs.Context.SaveChanges(); diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs index fae11804e3..2ced0c35c9 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs @@ -137,6 +137,11 @@ public CatalogSettings() /// public bool ShowLinkedAttributeValueImage { get; set; } + /// + /// Gets or sets a value indicating how many menu items will be displayed + /// + public int? MaxItemsToDisplayInCatalogMenu { get; set; } + /// /// Gets or sets a value indicating whether product sorting is enabled /// diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/Category.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/Category.cs index d179c36611..bbe46d5043 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/Category.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/Category.cs @@ -5,11 +5,7 @@ using System.Diagnostics; using System.Runtime.Serialization; using SmartStore.Core.Domain.Discounts; -using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Media; -using SmartStore.Core.Domain.Security; -using SmartStore.Core.Domain.Seo; -using SmartStore.Core.Domain.Stores; namespace SmartStore.Core.Domain.Catalog { @@ -18,7 +14,7 @@ namespace SmartStore.Core.Domain.Catalog /// [DataContract] [DebuggerDisplay("{Id}: {Name} (Parent: {ParentCategoryId})")] - public partial class Category : BaseEntity, IAuditable, ISoftDeletable, ILocalizedEntity, ISlugSupported, IAclSupported, IStoreMappingSupported, IPagingOptions + public partial class Category : BaseEntity, ICategoryNode, IAuditable, ISoftDeletable, IPagingOptions { private ICollection _appliedDiscounts; diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/CategoryNode.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/CategoryNode.cs new file mode 100644 index 0000000000..1450373772 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/CategoryNode.cs @@ -0,0 +1,39 @@ +using System; +using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Security; +using SmartStore.Core.Domain.Seo; +using SmartStore.Core.Domain.Stores; + +namespace SmartStore.Core.Domain.Catalog +{ + public interface ICategoryNode : ILocalizedEntity, ISlugSupported, IAclSupported, IStoreMappingSupported + { + int Id { get; } + int ParentCategoryId { get; } + string Name { get; } + string Alias { get; } + int? PictureId { get; } + bool Published { get; } + int DisplayOrder { get; } + DateTime UpdatedOnUtc { get; } + string BadgeText { get; } + int BadgeStyle { get; } + } + + [Serializable] + public class CategoryNode : ICategoryNode + { + public int Id { get; set; } + public int ParentCategoryId { get; set; } + public string Name { get; set; } + public string Alias { get; set; } + public int? PictureId { get; set; } + public bool Published { get; set; } + public int DisplayOrder { get; set; } + public DateTime UpdatedOnUtc { get; set; } + public string BadgeText { get; set; } + public int BadgeStyle { get; set; } + public bool SubjectToAcl { get; set; } + public bool LimitedToStores { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs index b435148ae6..45d8f6b372 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs @@ -845,6 +845,12 @@ public bool BasePriceHasValue [DataMember] public bool BundlePerItemShoppingCart { get; set; } + /// + /// Gets or sets the main picture id + /// + [DataMember] + public int? MainPictureId { get; set; } + /// /// Gets or sets the product type /// @@ -868,7 +874,7 @@ public string ProductTypeLabelHint switch (ProductType) { case ProductType.SimpleProduct: - return "smnet-hide"; + return "secondary d-none"; case ProductType.GroupedProduct: return "success"; case ProductType.BundledProduct: diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductAttribute.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductAttribute.cs index 70ba01ab30..72742f23b3 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductAttribute.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductAttribute.cs @@ -46,11 +46,23 @@ public partial class ProductAttribute : BaseEntity, ILocalizedEntity, ISearchAli [DataMember] public int DisplayOrder { get; set; } + /// + /// Gets or sets the facet template hint. Only effective in accordance with MegaSearchPlus plugin. + /// + [DataMember] + public FacetTemplateHint FacetTemplateHint { get; set; } + + /// + /// Specifies whether option names should be included in the search index. Only effective in accordance with MegaSearchPlus plugin. + /// + [DataMember] + public bool IndexOptionNames { get; set; } + /// - /// Gets or sets the facet template hint + /// Gets or sets export mappings. /// [DataMember] - public FacetTemplateHint FacetTemplateHint { get; set; } + public string ExportMappings { get; set; } /// /// Gets or sets the prooduct attribute option sets diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/SpecificationAttribute.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/SpecificationAttribute.cs index 47e0fdd312..c5dd44386d 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/SpecificationAttribute.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/SpecificationAttribute.cs @@ -58,10 +58,16 @@ public partial class SpecificationAttribute : BaseEntity, ILocalizedEntity, ISea [DataMember] public FacetTemplateHint FacetTemplateHint { get; set; } - /// - /// Gets or sets the specification attribute options - /// - [DataMember] + /// + /// Specifies whether option names should be included in the search index. Only effective in accordance with MegaSearchPlus plugin. + /// + [DataMember] + public bool IndexOptionNames { get; set; } + + /// + /// Gets or sets the specification attribute options + /// + [DataMember] public virtual ICollection SpecificationAttributeOptions { get { return _specificationAttributeOptions ?? (_specificationAttributeOptions = new HashSet()); } diff --git a/src/Libraries/SmartStore.Core/Domain/Common/Address.cs b/src/Libraries/SmartStore.Core/Domain/Common/Address.cs index ec942ed6de..c4723d0e6e 100644 --- a/src/Libraries/SmartStore.Core/Domain/Common/Address.cs +++ b/src/Libraries/SmartStore.Core/Domain/Common/Address.cs @@ -109,7 +109,6 @@ public class Address : BaseEntity, ICloneable [DataMember] public virtual StateProvince StateProvince { get; set; } - public object Clone() { var addr = new Address() @@ -134,5 +133,18 @@ public object Clone() }; return addr; } + + public static string DefaultAddressFormat + { + get + { + return @"{{ Salutation }} {{ Title }} {{ FirstName }} {{ LastName }} +{{ Company }} +{{ Street1 }} +{{ Street2 }} +{{ ZipCode }} {{ City }} +{{ Country | Upcase }}"; + } + } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Common/AddressSettings.cs b/src/Libraries/SmartStore.Core/Domain/Common/AddressSettings.cs index 9fa5d46521..6d39656c46 100644 --- a/src/Libraries/SmartStore.Core/Domain/Common/AddressSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Common/AddressSettings.cs @@ -19,7 +19,9 @@ public AddressSettings() CityEnabled = true; CityRequired = true; CountryEnabled = true; + CountryRequired = true; StateProvinceEnabled = true; + StateProvinceRequired = false; PhoneEnabled = true; PhoneRequired = true; FaxEnabled = true; @@ -95,15 +97,25 @@ public AddressSettings() /// public bool CountryEnabled { get; set; } - /// - /// Gets or sets a value indicating whether 'State / province' is enabled - /// - public bool StateProvinceEnabled { get; set; } + /// + /// Gets or sets a value indicating whether 'Country' is required + /// + public bool CountryRequired { get; set; } - /// - /// Gets or sets a value indicating whether 'Phone number' is enabled - /// - public bool PhoneEnabled { get; set; } + /// + /// Gets or sets a value indicating whether 'State / province' is enabled + /// + public bool StateProvinceEnabled { get; set; } + + /// + /// Gets or sets a value indicating whether 'State / province' is required + /// + public bool StateProvinceRequired { get; set; } + + /// + /// Gets or sets a value indicating whether 'Phone number' is enabled + /// + public bool PhoneEnabled { get; set; } /// /// Gets or sets a value indicating whether 'Phone number' is required /// diff --git a/src/Libraries/SmartStore.Core/Domain/Common/AdminAreaSettings.cs b/src/Libraries/SmartStore.Core/Domain/Common/AdminAreaSettings.cs index 2458d9faa7..466c1bbe79 100644 --- a/src/Libraries/SmartStore.Core/Domain/Common/AdminAreaSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Common/AdminAreaSettings.cs @@ -9,13 +9,10 @@ public AdminAreaSettings() { GridPageSize = 25; DisplayProductPictures = true; - RichEditorFlavor = "RichEditor"; } public int GridPageSize { get; set; } public bool DisplayProductPictures { get; set; } - - public string RichEditorFlavor { get; set; } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Common/CompanyInformationSettings.cs b/src/Libraries/SmartStore.Core/Domain/Common/CompanyInformationSettings.cs index 43c6a211cb..9c6430e25a 100644 --- a/src/Libraries/SmartStore.Core/Domain/Common/CompanyInformationSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Common/CompanyInformationSettings.cs @@ -94,6 +94,5 @@ public class CompanyInformationSettings : ISettings /// Gets or sets the tax number that will be used /// public string TaxNumber { get; set; } - } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Common/SocialSettings.cs b/src/Libraries/SmartStore.Core/Domain/Common/SocialSettings.cs index 7fd9c789ac..46980652d0 100644 --- a/src/Libraries/SmartStore.Core/Domain/Common/SocialSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Common/SocialSettings.cs @@ -11,6 +11,8 @@ public SocialSettings() GooglePlusLink = "#"; TwitterLink = "#"; PinterestLink = "#"; + //YoutubeLink = "#"; + //InstagramLink = "#"; } /// @@ -42,5 +44,10 @@ public SocialSettings() /// Gets or sets the youtube link /// public string YoutubeLink { get; set; } - } + + /// + /// Gets or sets the instagram link + /// + public string InstagramLink { get; set; } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/Customer.cs b/src/Libraries/SmartStore.Core/Domain/Customers/Customer.cs index fcd900df2e..c4fa6cc228 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/Customer.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/Customer.cs @@ -251,56 +251,66 @@ public virtual ICollection ForumPosts get { return _forumPosts ?? (_forumPosts = new HashSet()); } protected set { _forumPosts = value; } } - - #endregion - #region Addresses + #endregion - public virtual void RemoveAddress(Address address) - { - if (this.Addresses.Contains(address)) - { - if (this.BillingAddress == address) this.BillingAddress = null; - if (this.ShippingAddress == address) this.ShippingAddress = null; - - this.Addresses.Remove(address); - } - } + #region Utils - #endregion - - #region Reward points - - public void AddRewardPointsHistoryEntry(int points, string message = "", - Order usedWithOrder = null, decimal usedAmount = 0M) - { - int newPointsBalance = this.GetRewardPointsBalance() + points; - - var rewardPointsHistory = new RewardPointsHistory() - { - Customer = this, - UsedWithOrder = usedWithOrder, - Points = points, - PointsBalance = newPointsBalance, - UsedAmount = usedAmount, - Message = message, - CreatedOnUtc = DateTime.UtcNow - }; - - this.RewardPointsHistory.Add(rewardPointsHistory); - } - /// - /// Gets reward points balance - /// - public int GetRewardPointsBalance() - { - int result = 0; - if (this.RewardPointsHistory.Count > 0) - result = this.RewardPointsHistory.OrderByDescending(rph => rph.CreatedOnUtc).ThenByDescending(rph => rph.Id).FirstOrDefault().PointsBalance; - return result; - } + /// + /// Gets a string identifier for the customer's roles by joining all role ids + /// + /// true ignores all inactive roles + /// The identifier + public string GetRolesIdent(bool onlyActiveCustomerRoles = true) + { + return string.Join(",", this.CustomerRoles.Where(x => !onlyActiveCustomerRoles || x.Active).Select(x => x.Id)); + } + + public virtual void RemoveAddress(Address address) + { + if (this.Addresses.Contains(address)) + { + if (this.BillingAddress == address) this.BillingAddress = null; + if (this.ShippingAddress == address) this.ShippingAddress = null; + + this.Addresses.Remove(address); + } + } + + public void AddRewardPointsHistoryEntry( + int points, + string message = "", + Order usedWithOrder = null, + decimal usedAmount = 0M) + { + int newPointsBalance = this.GetRewardPointsBalance() + points; + + var rewardPointsHistory = new RewardPointsHistory() + { + Customer = this, + UsedWithOrder = usedWithOrder, + Points = points, + PointsBalance = newPointsBalance, + UsedAmount = usedAmount, + Message = message, + CreatedOnUtc = DateTime.UtcNow + }; + + this.RewardPointsHistory.Add(rewardPointsHistory); + } - #endregion - } + /// + /// Gets reward points balance + /// + public int GetRewardPointsBalance() + { + int result = 0; + if (this.RewardPointsHistory.Count > 0) + result = this.RewardPointsHistory.OrderByDescending(rph => rph.CreatedOnUtc).ThenByDescending(rph => rph.Id).FirstOrDefault().PointsBalance; + return result; + } + + #endregion + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs index ce4671c6e2..c14bbfa1a3 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs @@ -1,9 +1,8 @@ - -using SmartStore.Core.Configuration; +using SmartStore.Core.Configuration; namespace SmartStore.Core.Domain.Customers { - public class CustomerSettings : ISettings + public class CustomerSettings : ISettings { public CustomerSettings() { @@ -14,7 +13,7 @@ public CustomerSettings() HashedPasswordFormat = "SHA1"; PasswordMinLength = 6; UserRegistrationType = UserRegistrationType.Standard; - AvatarMaximumSizeBytes = 20000; + AvatarMaximumSizeBytes = 512000; DefaultAvatarEnabled = true; CustomerNameFormat = CustomerNameFormat.ShowFirstName; CustomerNameFormatMaxLength = 64; @@ -24,7 +23,8 @@ public CustomerSettings() NewsletterEnabled = true; OnlineCustomerMinutes = 20; StoreLastVisitedPage = true; - DisplayPrivacyAgreementOnContactUs = false; + StoreLastIpAddress = true; + DisplayPrivacyAgreementOnContactUs = false; } /// @@ -152,10 +152,15 @@ public CustomerSettings() /// public bool StoreLastVisitedPage { get; set; } - /// - /// Gets or sets a value indicating whether to display a checkbox to the customer where he can agree to privacy terms - /// - public bool DisplayPrivacyAgreementOnContactUs { get; set; } + /// + /// Gets or sets a value indicating whether to store last IP address for each customer + /// + public bool StoreLastIpAddress { get; set; } + + /// + /// Gets or sets a value indicating whether to display a checkbox to the customer where he can agree to privacy terms + /// + public bool DisplayPrivacyAgreementOnContactUs { get; set; } #region Form fields diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs index d294316f42..c795d66377 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs @@ -12,7 +12,8 @@ public enum ExportEntityType Manufacturer, Customer, Order, - NewsLetterSubscription + NewsLetterSubscription, + ShoppingCartItem } /// diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportFilter.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportFilter.cs index 23d08fd03c..1b00fe2e27 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportFilter.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportFilter.cs @@ -171,5 +171,14 @@ public class ExportFilter public bool? IsActiveSubscriber { get; set; } #endregion + + #region Shopping Cart + + /// + /// Filter by shopping cart type identifier + /// + public int? ShoppingCartTypeId { get; set; } + + #endregion } } diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProjection.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProjection.cs index 2c82c70afe..fd41da5fb4 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProjection.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProjection.cs @@ -187,5 +187,14 @@ public ExportOrderStatusChange OrderStatusChange } #endregion + + #region Shopping Cart Item + + /// + /// Whether to export bundle products + /// + public bool NoBundleProducts { get; set; } + + #endregion } } diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/SyncMapping.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/SyncMapping.cs index a297bc8f85..d8067400cb 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/SyncMapping.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/SyncMapping.cs @@ -5,6 +5,7 @@ using System.Runtime.Serialization; using System.Text; using System.Threading.Tasks; +using SmartStore.Core.Data.Hooks; namespace SmartStore.Core.Domain.DataExchange { @@ -12,6 +13,7 @@ namespace SmartStore.Core.Domain.DataExchange /// Holds info about a synchronization operation with an external system /// [DataContract] + [Hookable(false)] public partial class SyncMapping : BaseEntity { public SyncMapping() diff --git a/src/Libraries/SmartStore.Core/Domain/Directory/Country.cs b/src/Libraries/SmartStore.Core/Domain/Directory/Country.cs index 117a487a07..45c1dacca7 100644 --- a/src/Libraries/SmartStore.Core/Domain/Directory/Country.cs +++ b/src/Libraries/SmartStore.Core/Domain/Directory/Country.cs @@ -1,20 +1,18 @@ using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using SmartStore.Core.Domain.Localization; -using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Domain.Stores; namespace SmartStore.Core.Domain.Directory { - /// - /// Represents a country - /// + /// + /// Represents a country + /// [DataContract] public partial class Country : BaseEntity, ILocalizedEntity, IStoreMappingSupported { private ICollection _stateProvinces; - private ICollection _restrictedShippingMethods; - /// /// Gets or sets the name @@ -75,24 +73,20 @@ public partial class Country : BaseEntity, ILocalizedEntity, IStoreMappingSuppor /// [DataMember] public bool LimitedToStores { get; set; } - - /// - /// Gets or sets the state/provinces - /// - public virtual ICollection StateProvinces + + /// + /// Gets or sets the international mailing address format + /// + [DataMember, MaxLength] + public string AddressFormat { get; set; } + + /// + /// Gets or sets the state/provinces + /// + public virtual ICollection StateProvinces { get { return _stateProvinces ?? (_stateProvinces = new HashSet()); } protected set { _stateProvinces = value; } } - - /// - /// Gets or sets the restricted shipping methods - /// - public virtual ICollection RestrictedShippingMethods - { - get { return _restrictedShippingMethods ?? (_restrictedShippingMethods = new HashSet()); } - protected set { _restrictedShippingMethods = value; } - } } - } diff --git a/src/Libraries/SmartStore.Core/Domain/Directory/Currency.cs b/src/Libraries/SmartStore.Core/Domain/Directory/Currency.cs index e303f27f62..c20e16638a 100644 --- a/src/Libraries/SmartStore.Core/Domain/Directory/Currency.cs +++ b/src/Libraries/SmartStore.Core/Domain/Directory/Currency.cs @@ -1,7 +1,7 @@ using System; +using System.Runtime.Serialization; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Stores; -using System.Runtime.Serialization; namespace SmartStore.Core.Domain.Directory { @@ -11,6 +11,11 @@ namespace SmartStore.Core.Domain.Directory [DataContract] public partial class Currency : BaseEntity, IAuditable, ILocalizedEntity, IStoreMappingSupported { + public Currency() + { + RoundNumDecimals = 2; + } + /// /// Gets or sets the name /// @@ -76,5 +81,39 @@ public partial class Currency : BaseEntity, IAuditable, ILocalizedEntity, IStore /// [DataMember] public string DomainEndings { get; set; } - } + + #region Rounding + + /// + /// Gets or sets a value indicating whether rounding of order items is enabled + /// + [DataMember] + public bool RoundOrderItemsEnabled { get; set; } + + /// + /// Gets or sets the number of decimal places to round to + /// + [DataMember] + public int RoundNumDecimals { get; set; } + + /// + /// Gets or sets a value indicating whether to round the order total + /// + [DataMember] + public bool RoundOrderTotalEnabled { get; set; } + + /// + /// Gets or sets the smallest denomination. The order total is rounded to the nearest multiple of it. + /// + [DataMember] + public decimal RoundOrderTotalDenominator { get; set; } + + /// + /// Gets or sets the order total rounding rule. + /// + [DataMember] + public CurrencyRoundingRule RoundOrderTotalRule { get; set; } + + #endregion Rounding + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Directory/CurrencyRoundingRule.cs b/src/Libraries/SmartStore.Core/Domain/Directory/CurrencyRoundingRule.cs new file mode 100644 index 0000000000..06b80a4f24 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/Directory/CurrencyRoundingRule.cs @@ -0,0 +1,25 @@ +namespace SmartStore.Core.Domain.Directory +{ + public enum CurrencyRoundingRule + { + /// + /// E.g. denomination 0.05: 9.225 will round to 9.20 + /// + RoundMidpointDown = 0, + + /// + /// E.g. denomination 0.05: 9.225 will round to 9.25 + /// + RoundMidpointUp, + + /// + /// E.g. denomination 0.05: 9.24 will round to 9.20 + /// + AlwaysRoundDown, + + /// + /// E.g. denomination 0.05: 9.26 will round to 9.30 + /// + AlwaysRoundUp + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/Directory/MeasureDimension.cs b/src/Libraries/SmartStore.Core/Domain/Directory/MeasureDimension.cs index 87a37d30d1..dfd825307d 100644 --- a/src/Libraries/SmartStore.Core/Domain/Directory/MeasureDimension.cs +++ b/src/Libraries/SmartStore.Core/Domain/Directory/MeasureDimension.cs @@ -1,28 +1,35 @@ +using System.Runtime.Serialization; + namespace SmartStore.Core.Domain.Directory { - /// - /// Represents a measure dimension - /// - public partial class MeasureDimension : BaseEntity + /// + /// Represents a measure dimension + /// + [DataContract] + public partial class MeasureDimension : BaseEntity { - /// - /// Gets or sets the name - /// - public string Name { get; set; } + /// + /// Gets or sets the name + /// + [DataMember] + public string Name { get; set; } - /// - /// Gets or sets the system keyword - /// - public string SystemKeyword { get; set; } + /// + /// Gets or sets the system keyword + /// + [DataMember] + public string SystemKeyword { get; set; } - /// - /// Gets or sets the ratio - /// - public decimal Ratio { get; set; } + /// + /// Gets or sets the ratio + /// + [DataMember] + public decimal Ratio { get; set; } - /// - /// Gets or sets the display order - /// - public int DisplayOrder { get; set; } + /// + /// Gets or sets the display order + /// + [DataMember] + public int DisplayOrder { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Directory/MeasureWeight.cs b/src/Libraries/SmartStore.Core/Domain/Directory/MeasureWeight.cs index 973e62a417..ec9b31da59 100644 --- a/src/Libraries/SmartStore.Core/Domain/Directory/MeasureWeight.cs +++ b/src/Libraries/SmartStore.Core/Domain/Directory/MeasureWeight.cs @@ -1,28 +1,35 @@ +using System.Runtime.Serialization; + namespace SmartStore.Core.Domain.Directory { - /// - /// Represents a measure weight - /// - public partial class MeasureWeight : BaseEntity + /// + /// Represents a measure weight + /// + [DataContract] + public partial class MeasureWeight : BaseEntity { - /// - /// Gets or sets the name - /// - public string Name { get; set; } + /// + /// Gets or sets the name + /// + [DataMember] + public string Name { get; set; } - /// - /// Gets or sets the system keyword - /// - public string SystemKeyword { get; set; } + /// + /// Gets or sets the system keyword + /// + [DataMember] + public string SystemKeyword { get; set; } - /// - /// Gets or sets the ratio - /// - public decimal Ratio { get; set; } + /// + /// Gets or sets the ratio + /// + [DataMember] + public decimal Ratio { get; set; } - /// - /// Gets or sets the display order - /// - public int DisplayOrder { get; set; } + /// + /// Gets or sets the display order + /// + [DataMember] + public int DisplayOrder { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Localization/ILocalizedEntity.cs b/src/Libraries/SmartStore.Core/Domain/Localization/ILocalizedEntity.cs index 8d5d0fd1ca..9a873cc70b 100644 --- a/src/Libraries/SmartStore.Core/Domain/Localization/ILocalizedEntity.cs +++ b/src/Libraries/SmartStore.Core/Domain/Localization/ILocalizedEntity.cs @@ -6,6 +6,5 @@ namespace SmartStore.Core.Domain.Localization /// public interface ILocalizedEntity { - - } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Localization/Language.cs b/src/Libraries/SmartStore.Core/Domain/Localization/Language.cs index ae4864b0c8..8e1259aff8 100644 --- a/src/Libraries/SmartStore.Core/Domain/Localization/Language.cs +++ b/src/Libraries/SmartStore.Core/Domain/Localization/Language.cs @@ -1,7 +1,8 @@ using System.Collections.Generic; -using SmartStore.Core.Domain.Stores; using System.Runtime.Serialization; using System.Diagnostics; +using System.Globalization; +using SmartStore.Core.Domain.Stores; namespace SmartStore.Core.Domain.Localization { @@ -71,5 +72,21 @@ public virtual ICollection LocaleStringResources protected set { _localeStringResources = value; } } + public string GetTwoLetterISOLanguageName() + { + if (UniqueSeoCode.HasValue()) + { + return UniqueSeoCode; + } + + try + { + var ci = new CultureInfo(LanguageCulture); + return ci.TwoLetterISOLanguageName; + } + catch { } + + return null; + } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Media/MediaSettings.cs b/src/Libraries/SmartStore.Core/Domain/Media/MediaSettings.cs index 52f7c53542..2bf9d0f3ee 100644 --- a/src/Libraries/SmartStore.Core/Domain/Media/MediaSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Media/MediaSettings.cs @@ -6,7 +6,7 @@ public class MediaSettings : ISettings { public MediaSettings() { - AvatarPictureSize = 85; + AvatarPictureSize = 250; ProductThumbPictureSize = 250; CategoryThumbPictureSize = 250; ManufacturerThumbPictureSize = 250; @@ -19,14 +19,15 @@ public MediaSettings() CartThumbBundleItemPictureSize = 32; MiniCartThumbPictureSize = ProductThumbPictureSize; VariantValueThumbPictureSize = 70; - MaximumImageSize = 1280; + MaximumImageSize = 2048; DefaultPictureZoomEnabled = true; PictureZoomType = "window"; DefaultImageQuality = 90; MultipleThumbDirectories = true; DefaultThumbnailAspectRatio = 1; + AutoGenerateAbsoluteUrls = true; } - + public int AvatarPictureSize { get; set; } public int ProductThumbPictureSize { get; set; } public int ProductDetailsPictureSize { get; set; } @@ -64,5 +65,10 @@ public MediaSettings() /// Geta or sets a vaue indicating whether single (/media/thumbs/) or multiple (/media/thumbs/0001/ and /media/thumbs/0002/) directories will used for picture thumbs /// public bool MultipleThumbDirectories { get; set; } - } + + /// + /// Generates absolute media urls based upon current request uri instead of relative urls. + /// + public bool AutoGenerateAbsoluteUrls { get; set; } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Media/PictureType.cs b/src/Libraries/SmartStore.Core/Domain/Media/PictureType.cs index 3da89ff17a..84d245f3be 100644 --- a/src/Libraries/SmartStore.Core/Domain/Media/PictureType.cs +++ b/src/Libraries/SmartStore.Core/Domain/Media/PictureType.cs @@ -3,12 +3,13 @@ namespace SmartStore.Core.Domain.Media /// /// Represents a picture item type /// - public enum PictureType + public enum FallbackPictureType { - /// - /// Entities (products, categories, manufacturers) - /// - Entity = 1, + NoFallback = 0, + /// + /// Entities (products, categories, manufacturers) + /// + Entity = 1, /// /// Avatar /// diff --git a/src/Libraries/SmartStore.Core/Domain/Messages/EmailAccount.cs b/src/Libraries/SmartStore.Core/Domain/Messages/EmailAccount.cs index 859a7bd74f..d39d745ef8 100644 --- a/src/Libraries/SmartStore.Core/Domain/Messages/EmailAccount.cs +++ b/src/Libraries/SmartStore.Core/Domain/Messages/EmailAccount.cs @@ -1,4 +1,5 @@ using System; +using SmartStore.Core.Email; namespace SmartStore.Core.Domain.Messages { @@ -70,5 +71,10 @@ object ICloneable.Clone() { return this.MemberwiseClone(); } + + public EmailAddress ToEmailAddress() + { + return new EmailAddress(this.Email, this.DisplayName); + } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Messages/EmailAccountSettings.cs b/src/Libraries/SmartStore.Core/Domain/Messages/EmailAccountSettings.cs index e53573cb1d..8a4d73f784 100644 --- a/src/Libraries/SmartStore.Core/Domain/Messages/EmailAccountSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Messages/EmailAccountSettings.cs @@ -9,6 +9,10 @@ public class EmailAccountSettings : ISettings /// public int DefaultEmailAccountId { get; set; } - } - + /// + /// Gets or sets a folder where mail messages should be saved (instead of sending them). + /// For debug and test purposes only. + /// + public string PickupDirectoryLocation { get; set; } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Messages/Events.cs b/src/Libraries/SmartStore.Core/Domain/Messages/Events.cs index 8d1cbea6af..6827d1a1cc 100644 --- a/src/Libraries/SmartStore.Core/Domain/Messages/Events.cs +++ b/src/Libraries/SmartStore.Core/Domain/Messages/Events.cs @@ -81,42 +81,4 @@ public override int GetHashCode() return (_email != null ? _email.GetHashCode() : 0); } } - - /// - /// A container for tokens that are added. - /// - /// - public class EntityTokensAddedEvent where T : BaseEntity - { - private readonly T _entity; - private readonly IList _tokens; - - public EntityTokensAddedEvent(T entity, IList tokens) - { - _entity = entity; - _tokens = tokens; - } - - public T Entity { get { return _entity; } } - public IList Tokens { get { return _tokens; } } - } - - /// - /// A container for tokens that are added. - /// - /// - public class MessageTokensAddedEvent - { - private readonly MessageTemplate _message; - private readonly IList _tokens; - - public MessageTokensAddedEvent(MessageTemplate message, IList tokens) - { - _message = message; - _tokens = tokens; - } - - public MessageTemplate Message { get { return _message; } } - public IList Tokens { get { return _tokens; } } - } } diff --git a/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplate.cs b/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplate.cs index 75f66564ae..d939cc684b 100644 --- a/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplate.cs +++ b/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplate.cs @@ -1,4 +1,6 @@ -using SmartStore.Core.Domain.Localization; +using System.ComponentModel.DataAnnotations; +using System.Web.Mvc; +using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Media; using SmartStore.Core.Domain.Stores; @@ -9,24 +11,40 @@ namespace SmartStore.Core.Domain.Messages /// public partial class MessageTemplate : BaseEntity, ILocalizedEntity, IStoreMappingSupported { - /// + /// /// Gets or sets the name /// public string Name { get; set; } - /// - /// Gets or sets the BCC Email addresses - /// - public string BccEmailAddresses { get; set; } + [StringLength(500), Required] + public string To { get; set; } - /// - /// Gets or sets the subject - /// - public string Subject { get; set; } + [StringLength(500)] + public string ReplyTo { get; set; } + + /// + /// A comma separated list of required model types (e.g.: Product, Order, Customer, GiftCard) + /// + [StringLength(500)] + public string ModelTypes { get; set; } + + [MaxLength] + public string LastModelTree { get; set; } + + /// + /// Gets or sets the BCC Email addresses + /// + public string BccEmailAddresses { get; set; } + + /// + /// Gets or sets the subject + /// + public string Subject { get; set; } /// /// Gets or sets the body /// + [AllowHtml] public string Body { get; set; } /// diff --git a/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplateNames.cs b/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplateNames.cs new file mode 100644 index 0000000000..d768f0c2dc --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplateNames.cs @@ -0,0 +1,39 @@ +using System; + +namespace SmartStore.Core.Domain.Messages +{ + public class MessageTemplateNames + { + public const string CustomerRegistered = "NewCustomer.Notification"; + public const string CustomerWelcome = "Customer.WelcomeMessage"; + public const string CustomerEmailValidation = "Customer.EmailValidationMessage"; + public const string CustomerPasswordRecovery = "Customer.PasswordRecovery"; + public const string OrderPlacedStoreOwner = "OrderPlaced.StoreOwnerNotification"; + public const string OrderPlacedCustomer = "OrderPlaced.CustomerNotification"; + public const string ShipmentSentCustomer = "ShipmentSent.CustomerNotification"; + public const string ShipmentDeliveredCustomer = "ShipmentDelivered.CustomerNotification"; + public const string OrderCompletedCustomer = "OrderCompleted.CustomerNotification"; + public const string OrderCancelledCustomer = "OrderCancelled.CustomerNotification"; + public const string OrderNoteAddedCustomer = "Customer.NewOrderNote"; + public const string RecurringPaymentCancelledStoreOwner = "RecurringPaymentCancelled.StoreOwnerNotification"; + public const string NewsLetterSubscriptionActivation = "NewsLetterSubscription.ActivationMessage"; + public const string NewsLetterSubscriptionDeactivation = "NewsLetterSubscription.DeactivationMessage"; + public const string ShareProduct = "Service.EmailAFriend"; + public const string ShareWishlist = "Wishlist.EmailAFriend"; + public const string ProductQuestion = "Product.AskQuestion"; + public const string NewReturnRequestStoreOwner = "NewReturnRequest.StoreOwnerNotification"; + public const string ReturnRequestStatusChangedCustomer = "ReturnRequestStatusChanged.CustomerNotification"; + public const string NewForumTopic = "Forums.NewForumTopic"; + public const string NewForumPost = "Forums.NewForumPost"; + public const string NewPrivateMessage = "Customer.NewPM"; + public const string GiftCardCustomer = "GiftCard.Notification"; + public const string ProductReviewStoreOwner = "Product.ProductReview"; + public const string QuantityBelowStoreOwner = "QuantityBelow.StoreOwnerNotification"; + public const string NewVatSubmittedStoreOwner = "NewVATSubmitted.StoreOwnerNotification"; + public const string BlogCommentStoreOwner = "Blog.BlogComment"; + public const string NewsCommentStoreOwner = "News.NewsComment"; + public const string BackInStockCustomer = "Customer.BackInStock"; + public const string SystemCampaign = "System.Campaign"; + public const string SystemContactUs = "System.ContactUs"; + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplatesSettings.cs b/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplatesSettings.cs deleted file mode 100644 index 43394df849..0000000000 --- a/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplatesSettings.cs +++ /dev/null @@ -1,36 +0,0 @@ -using SmartStore.Core.Configuration; - -namespace SmartStore.Core.Domain.Messages -{ - public class MessageTemplatesSettings : ISettings - { - public MessageTemplatesSettings() - { - Color1 = "#3A87AD"; - Color2 = "#F7F7F7"; - Color3 = "#F5F5F5"; - } - - /// - /// Gets or sets a value indicating whether to replace message tokens according to case invariant rules - /// - public bool CaseInvariantReplacement { get; set; } - - /// - /// Gets or sets a color1 in hex format ("#hhhhhh") to use in workflow message formatting - /// - public string Color1 { get; set; } - - /// - /// Gets or sets a color2 in hex format ("#hhhhhh") to use in workflow message formatting - /// - public string Color2 { get; set; } - - /// - /// Gets or sets a color3 in hex format ("#hhhhhh") to use in workflow message formatting - /// - public string Color3 { get; set; } - - } - -} diff --git a/src/Libraries/SmartStore.Core/Domain/Messages/QueuedEmail.cs b/src/Libraries/SmartStore.Core/Domain/Messages/QueuedEmail.cs index d3f7e63b93..be65e172b7 100644 --- a/src/Libraries/SmartStore.Core/Domain/Messages/QueuedEmail.cs +++ b/src/Libraries/SmartStore.Core/Domain/Messages/QueuedEmail.cs @@ -20,31 +20,16 @@ public partial class QueuedEmail : BaseEntity /// public string From { get; set; } - /// - /// Gets or sets the FromName property - /// - public string FromName { get; set; } - /// /// Gets or sets the To property /// public string To { get; set; } - /// - /// Gets or sets the ToName property - /// - public string ToName { get; set; } - /// /// Gets or sets the ReplyTo property /// public string ReplyTo { get; set; } - /// - /// Gets or sets the ReplyToName property - /// - public string ReplyToName { get; set; } - /// /// Gets or sets the CC /// diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/Order.cs b/src/Libraries/SmartStore.Core/Domain/Orders/Order.cs index 116ad9f447..29be2034f6 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/Order.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/Order.cs @@ -230,6 +230,12 @@ protected virtual SortedDictionary ParseTaxRates(string taxRat [DataMember] public decimal OrderDiscount { get; set; } + /// + /// /// Gets or sets the order total rounding amount + /// + [DataMember] + public decimal OrderTotalRounding { get; set; } + /// /// Gets or sets the order total /// diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/ShoppingCartItem.cs b/src/Libraries/SmartStore.Core/Domain/Orders/ShoppingCartItem.cs index 5d178fd2d6..a7c3c91273 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/ShoppingCartItem.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/ShoppingCartItem.cs @@ -65,10 +65,10 @@ public partial class ShoppingCartItem : BaseEntity, IAuditable /// public DateTime UpdatedOnUtc { get; set; } - /// - /// Gets the log type - /// - public ShoppingCartType ShoppingCartType + /// + /// Gets or sets the shopping cart type + /// + public ShoppingCartType ShoppingCartType { get { diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/ShoppingCartSettings.cs b/src/Libraries/SmartStore.Core/Domain/Orders/ShoppingCartSettings.cs index ec80e6506d..d1e837b4ec 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/ShoppingCartSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/ShoppingCartSettings.cs @@ -23,7 +23,6 @@ public ShoppingCartSettings() EmailWishlistEnabled = true; MiniShoppingCartEnabled = true; ShowProductImagesInMiniShoppingCart = true; - //RoundPricesDuringCalculation = false; ShowBasePrice = true; ShowDeliveryTimes = true; ShowShortDesc = true; @@ -142,12 +141,6 @@ public ShoppingCartSettings() /// Gets or sets a value indicating whether to show product images in the mini-shopping cart block /// public bool ShowProductImagesInMiniShoppingCart { get; set; } - - //Round is already an issue. - /// - /// Gets or sets a value indicating whether to round calculated prices and total during calculation - /// - public bool RoundPricesDuringCalculation { get; set; } /// /// Gets or sets a value indicating whether to show a legal hint in the order summary diff --git a/src/Libraries/SmartStore.Core/Domain/Payments/CapturePaymentReason.cs b/src/Libraries/SmartStore.Core/Domain/Payments/CapturePaymentReason.cs new file mode 100644 index 0000000000..39d599f41d --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/Payments/CapturePaymentReason.cs @@ -0,0 +1,18 @@ +namespace SmartStore.Core.Domain.Payments +{ + /// + /// The reason for automatic capturing of the payment amount. + /// + public enum CapturePaymentReason + { + /// + /// Capture payment because the order has been marked as shipped. + /// + OrderShipped = 0, + + /// + /// Capture payment because the order has been marked as delivered. + /// + OrderDelivered + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/Payments/PaymentMethod.cs b/src/Libraries/SmartStore.Core/Domain/Payments/PaymentMethod.cs index e078434cc6..ba4b86162d 100644 --- a/src/Libraries/SmartStore.Core/Domain/Payments/PaymentMethod.cs +++ b/src/Libraries/SmartStore.Core/Domain/Payments/PaymentMethod.cs @@ -1,5 +1,6 @@ using System.Runtime.Serialization; using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Stores; namespace SmartStore.Core.Domain.Payments { @@ -7,7 +8,7 @@ namespace SmartStore.Core.Domain.Payments /// Represents a payment method /// [DataContract] - public partial class PaymentMethod : BaseEntity, ILocalizedEntity + public partial class PaymentMethod : BaseEntity, ILocalizedEntity, IStoreMappingSupported { /// /// Gets or sets the payment method system name @@ -20,5 +21,18 @@ public partial class PaymentMethod : BaseEntity, ILocalizedEntity /// [DataMember] public string FullDescription { get; set; } + + /// + /// Gets or sets a value indicating whether to round the order total. Also known as "Cash rounding". + /// + /// + [DataMember] + public bool RoundOrderTotalEnabled { get; set; } + + /// + /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores + /// + [DataMember] + public bool LimitedToStores { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Payments/PaymentSettings.cs b/src/Libraries/SmartStore.Core/Domain/Payments/PaymentSettings.cs index 3303f6ff04..9ba68a9c34 100644 --- a/src/Libraries/SmartStore.Core/Domain/Payments/PaymentSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Payments/PaymentSettings.cs @@ -31,5 +31,10 @@ public PaymentSettings() /// Gets or sets a value indicating whether we should bypass the payment method info page /// public bool BypassPaymentMethodInfo { get; set; } - } + + /// + /// Gets or sets the reason for automatic payment capturing + /// + public CapturePaymentReason? CapturePaymentReason { get; set; } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Security/IAclSupported.cs b/src/Libraries/SmartStore.Core/Domain/Security/IAclSupported.cs index 60661ea0df..872f42384d 100644 --- a/src/Libraries/SmartStore.Core/Domain/Security/IAclSupported.cs +++ b/src/Libraries/SmartStore.Core/Domain/Security/IAclSupported.cs @@ -5,9 +5,9 @@ namespace SmartStore.Core.Domain.Security /// public partial interface IAclSupported { - /// - /// Gets or sets a value indicating whether the entity is subject to ACL - /// - bool SubjectToAcl { get; set; } + /// + /// Gets or sets a value indicating whether the entity is subject to ACL + /// + bool SubjectToAcl { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Seo/ISlugSupported.cs b/src/Libraries/SmartStore.Core/Domain/Seo/ISlugSupported.cs index bb59b5581e..6b52764cec 100644 --- a/src/Libraries/SmartStore.Core/Domain/Seo/ISlugSupported.cs +++ b/src/Libraries/SmartStore.Core/Domain/Seo/ISlugSupported.cs @@ -6,6 +6,5 @@ namespace SmartStore.Core.Domain.Seo /// public interface ISlugSupported { - - } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingMethod.cs b/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingMethod.cs index 078b12a20c..ca5d7b1207 100644 --- a/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingMethod.cs +++ b/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingMethod.cs @@ -1,7 +1,6 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Stores; namespace SmartStore.Core.Domain.Shipping { @@ -9,10 +8,8 @@ namespace SmartStore.Core.Domain.Shipping /// Represents a shipping method (used for offline shipping rate computation methods) /// [DataContract] - public partial class ShippingMethod : BaseEntity, ILocalizedEntity - { - private ICollection _restrictedCountries; - + public partial class ShippingMethod : BaseEntity, ILocalizedEntity, IStoreMappingSupported + { /// /// Gets or sets the name /// @@ -31,17 +28,16 @@ public partial class ShippingMethod : BaseEntity, ILocalizedEntity [DataMember] public int DisplayOrder { get; set; } + /// + /// Gets or sets whether to ignore charges + /// [DataMember] public bool IgnoreCharges { get; set; } - /// - /// Gets or sets the restricted countries - /// + /// + /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores + /// [DataMember] - public virtual ICollection RestrictedCountries - { - get { return _restrictedCountries ?? (_restrictedCountries = new HashSet()); } - protected set { _restrictedCountries = value; } - } + public bool LimitedToStores { get; set; } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingSettings.cs b/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingSettings.cs index 36c147ab76..1dc2bfbf54 100644 --- a/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingSettings.cs @@ -57,5 +57,10 @@ public ShippingSettings() /// Gets or sets a value indicating whether to display shipping options during checkout process only if more then one option is available /// public bool SkipShippingIfSingleOption { get; set; } + + /// + /// Gets or sets a value indicating whether to charge only the highest shipping surcharge of products + /// + public bool ChargeOnlyHighestProductShippingSurcharge { get; set; } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs b/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs index 0d9fe6be96..da3f0315cd 100644 --- a/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs +++ b/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs @@ -104,7 +104,49 @@ public HttpSecurityMode GetSecurityMode(bool? useSsl = null) return HttpSecurityMode.Ssl; } } + return HttpSecurityMode.Unsecured; } + + private string _secureHost; + private string _unsecureHost; + + /// + /// Gets the store host name + /// + /// + /// If false, returns the default unsecured url. + /// If true, returns the secure url, but only if SSL is enabled for the store. + /// + /// The host name + public string GetHost(bool secure) + { + return secure + ? _secureHost ?? (_secureHost = GetHostInternal(true)) + : _unsecureHost ?? (_unsecureHost = GetHostInternal(false)); + } + + private string GetHostInternal(bool secure) + { + var host = string.Empty; + + if (secure && SslEnabled) + { + if (SecureUrl.HasValue()) + { + host = SecureUrl; + } + else + { + host = Url.Replace("http:/", "https:/"); + } + } + else + { + host = Url; + } + + return host.EnsureEndsWith("/"); + } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Themes/ThemeVariable.cs b/src/Libraries/SmartStore.Core/Domain/Themes/ThemeVariable.cs index 12dad624d9..2befa5532e 100644 --- a/src/Libraries/SmartStore.Core/Domain/Themes/ThemeVariable.cs +++ b/src/Libraries/SmartStore.Core/Domain/Themes/ThemeVariable.cs @@ -6,13 +6,11 @@ // codehint: sm-add (whole file) namespace SmartStore.Core.Domain.Themes -{ - +{ public class ThemeVariable : BaseEntity { - /// - /// Gets or sets the theme the attribute belongs to + /// Gets or sets the theme the variable belongs to /// public string Theme { get; set; } diff --git a/src/Libraries/SmartStore.Core/Email/DefaultEmailSender.cs b/src/Libraries/SmartStore.Core/Email/DefaultEmailSender.cs index 0ef3fbfea3..15fc63095f 100644 --- a/src/Libraries/SmartStore.Core/Email/DefaultEmailSender.cs +++ b/src/Libraries/SmartStore.Core/Email/DefaultEmailSender.cs @@ -4,22 +4,27 @@ using System.Text; using System.Net.Mail; using System.Net.Mime; -using System.Net; using System.IO; -using System.ComponentModel; using System.Threading.Tasks; +using SmartStore.Core.Domain.Messages; namespace SmartStore.Core.Email { public class DefaultEmailSender : IEmailSender { + private readonly EmailAccountSettings _emailAccountSettings; + + public DefaultEmailSender(EmailAccountSettings emailAccountSettings) + { + _emailAccountSettings = emailAccountSettings; + } - /// - /// Builds System.Net.Mail.Message - /// - /// SmartStore.Email.Message - /// System.Net.Mail.Message - protected virtual MailMessage BuildMailMessage(EmailMessage original) + /// + /// Builds System.Net.Mail.Message + /// + /// SmartStore.Email.Message + /// System.Net.Mail.Message + protected virtual MailMessage BuildMailMessage(EmailMessage original) { MailMessage msg = new MailMessage(); @@ -61,9 +66,9 @@ protected virtual MailMessage BuildMailMessage(EmailMessage original) return msg; } - #region IMailSender Members + #region IMailSender Members - public void SendEmail(SmtpContext context, EmailMessage message) + public void SendEmail(SmtpContext context, EmailMessage message) { Guard.NotNull(context, nameof(context)); Guard.NotNull(message, nameof(message)); @@ -72,6 +77,7 @@ public void SendEmail(SmtpContext context, EmailMessage message) { using (var client = context.ToSmtpClient()) { + ApplySettings(client); client.Send(msg); } } @@ -83,6 +89,7 @@ public Task SendEmailAsync(SmtpContext context, EmailMessage message) Guard.NotNull(message, nameof(message)); var client = context.ToSmtpClient(); + ApplySettings(client); var msg = this.BuildMailMessage(message); return client.SendMailAsync(msg).ContinueWith(t => @@ -92,7 +99,17 @@ public Task SendEmailAsync(SmtpContext context, EmailMessage message) }); } - #endregion + private void ApplySettings(SmtpClient client) + { + var pickupDirLocation = _emailAccountSettings.PickupDirectoryLocation; + if (pickupDirLocation.HasValue() && client.DeliveryMethod != SmtpDeliveryMethod.SpecifiedPickupDirectory && Path.IsPathRooted(pickupDirLocation)) + { + client.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory; + client.PickupDirectoryLocation = pickupDirLocation; + client.EnableSsl = false; + } + } - } + #endregion + } } diff --git a/src/Libraries/SmartStore.Core/Email/EmailAddress.cs b/src/Libraries/SmartStore.Core/Email/EmailAddress.cs index 3920dbc087..8699174caf 100644 --- a/src/Libraries/SmartStore.Core/Email/EmailAddress.cs +++ b/src/Libraries/SmartStore.Core/Email/EmailAddress.cs @@ -1,43 +1,56 @@ using System; using System.Net.Mail; -using System.Text; namespace SmartStore.Core.Email { public class EmailAddress { - public string Address { get; set; } - public string DisplayName { get; set; } + private readonly MailAddress _inner; - public EmailAddress(string address) + public EmailAddress(string address) { - this.Address = address; + _inner = new MailAddress(address); } public EmailAddress(string address, string displayName) { - this.Address = address; - this.DisplayName = displayName; - } + _inner = new MailAddress(address, displayName); + } - public override int GetHashCode() + public EmailAddress(MailAddress address) { - return this.ToString().GetHashCode(); + _inner = address; } - public override string ToString() + public string Address { - if (this.DisplayName.IsEmpty()) - { - return this.Address; - } + get { return _inner.Address; } + } - return "{0} [{1}]".FormatCurrent(this.DisplayName, this.Address); + public string DisplayName + { + get { return _inner.DisplayName; } + } + + public string User + { + get { return _inner.User; } } - public MailAddress ToMailAddress() + public string Host + { + get { return _inner.Host; } + } + + public override int GetHashCode() => _inner.GetHashCode(); + + public override string ToString() => _inner.ToString(); + + public MailAddress ToMailAddress() => _inner; + + public static implicit operator string(EmailAddress obj) { - return new MailAddress(this.Address, this.DisplayName); + return obj.ToString(); } - } + } } diff --git a/src/Libraries/SmartStore.Core/Email/EmailMessage.cs b/src/Libraries/SmartStore.Core/Email/EmailMessage.cs index b4a76ea19a..cdc83e6efb 100644 --- a/src/Libraries/SmartStore.Core/Email/EmailMessage.cs +++ b/src/Libraries/SmartStore.Core/Email/EmailMessage.cs @@ -8,10 +8,8 @@ namespace SmartStore.Core.Email { - public class EmailMessage : ICloneable { - public EmailMessage() { this.BodyFormat = MailBodyFormat.Html; diff --git a/src/Libraries/SmartStore.Core/Events/CommonMessages/EntityDeleted.cs b/src/Libraries/SmartStore.Core/Events/CommonMessages/EntityDeleted.cs deleted file mode 100644 index c8c417ecb8..0000000000 --- a/src/Libraries/SmartStore.Core/Events/CommonMessages/EntityDeleted.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace SmartStore.Core.Events -{ - /// - /// A container for passing entities that have been deleted. This is not used for entities that are deleted logicaly via a bit column. - /// - /// - public class EntityDeleted : ComparableObject where T : BaseEntity - { - - public EntityDeleted(T entity) - { - this.Entity = entity; - } - - [ObjectSignature] - public T Entity { get; private set; } - } -} diff --git a/src/Libraries/SmartStore.Core/Events/CommonMessages/EntityInserted.cs b/src/Libraries/SmartStore.Core/Events/CommonMessages/EntityInserted.cs deleted file mode 100644 index eab3b81404..0000000000 --- a/src/Libraries/SmartStore.Core/Events/CommonMessages/EntityInserted.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace SmartStore.Core.Events -{ - /// - /// A container for entities that have been inserted. - /// - /// - public class EntityInserted : ComparableObject where T : BaseEntity - { - - public EntityInserted(T entity) - { - this.Entity = entity; - } - - [ObjectSignature] - public T Entity { get; private set; } - } -} diff --git a/src/Libraries/SmartStore.Core/Events/CommonMessages/EntityUpdated.cs b/src/Libraries/SmartStore.Core/Events/CommonMessages/EntityUpdated.cs deleted file mode 100644 index a392a04cf7..0000000000 --- a/src/Libraries/SmartStore.Core/Events/CommonMessages/EntityUpdated.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace SmartStore.Core.Events -{ - /// - /// A container for entities that are updated. - /// - /// - public class EntityUpdated : ComparableObject where T : BaseEntity - { - - public EntityUpdated(T entity) - { - this.Entity = entity; - } - - [ObjectSignature] - public T Entity { get; private set; } - - } -} diff --git a/src/Libraries/SmartStore.Core/Events/IConsumer.cs b/src/Libraries/SmartStore.Core/Events/IConsumer.cs index ca12ff8653..5fee38c56e 100644 --- a/src/Libraries/SmartStore.Core/Events/IConsumer.cs +++ b/src/Libraries/SmartStore.Core/Events/IConsumer.cs @@ -3,6 +3,6 @@ namespace SmartStore.Core.Events { public interface IConsumer { - void HandleEvent(T eventMessage); + void HandleEvent(T message); } } diff --git a/src/Libraries/SmartStore.Core/Events/IEventPublisher.cs b/src/Libraries/SmartStore.Core/Events/IEventPublisher.cs index 225808124f..54fb88e357 100644 --- a/src/Libraries/SmartStore.Core/Events/IEventPublisher.cs +++ b/src/Libraries/SmartStore.Core/Events/IEventPublisher.cs @@ -5,22 +5,4 @@ public interface IEventPublisher { void Publish(T eventMessage); } - - public static class IEventPublisherExtensions - { - public static void EntityInserted(this IEventPublisher eventPublisher, T entity) where T : BaseEntity - { - eventPublisher.Publish(new EntityInserted(entity)); - } - - public static void EntityUpdated(this IEventPublisher eventPublisher, T entity) where T : BaseEntity - { - eventPublisher.Publish(new EntityUpdated(entity)); - } - - public static void EntityDeleted(this IEventPublisher eventPublisher, T entity) where T : BaseEntity - { - eventPublisher.Publish(new EntityDeleted(entity)); - } - } } diff --git a/src/Libraries/SmartStore.Core/Extensions/DecimalExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/DecimalExtensions.cs index aa6dd811e3..83e67555a0 100644 --- a/src/Libraries/SmartStore.Core/Extensions/DecimalExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/DecimalExtensions.cs @@ -1,21 +1,11 @@ using System; using System.Globalization; +using SmartStore.Core.Domain.Directory; namespace SmartStore -{ +{ public static class DecimalExtensions { - /// - /// Rounds and formats a decimal culture invariant - /// - /// The decimal - /// Rounding decimal number - /// Formated value - public static string FormatInvariant(this decimal value, int decimals = 2) - { - return Math.Round(value, decimals).ToString("0.00", CultureInfo.InvariantCulture); - } - /// /// Calculates the tax (percentage) from a gross and a net value. /// @@ -26,21 +16,124 @@ public static string FormatInvariant(this decimal value, int decimals = 2) public static decimal ToTaxPercentage(this decimal inclTax, decimal exclTax, int? decimals = null) { if (exclTax == decimal.Zero) + { return decimal.Zero; + } var result = ((inclTax / exclTax) - 1.0M) * 100.0M; return (decimals.HasValue ? Math.Round(result, decimals.Value) : result); } + /// + /// Converts to smallest currency uint, e.g. cents + /// + /// Handling of the midway between two numbers. "ToEven" round down, "AwayFromZero" round up. + /// Smallest currency unit + public static int ToSmallestCurrencyUnit(this decimal value, MidpointRounding midpoint = MidpointRounding.AwayFromZero) + { + var result = Math.Round(value * 100, 0, midpoint); + return Convert.ToInt32(result); + } + /// - /// Converts to smallest currency uint, e.g. cents + /// Round decimal to the nearest multiple of denomination /// - /// Smallest currency unit - public static int ToSmallestCurrencyUnit(this decimal value, MidpointRounding rounding = MidpointRounding.AwayFromZero) + /// Value to round + /// Denomination + /// Handling of the midway between two numbers. "ToEven" round down, "AwayFromZero" round up. + /// Rounded value + public static decimal RoundToNearest(this decimal value, decimal denomination, MidpointRounding midpoint = MidpointRounding.AwayFromZero) { - var result = Math.Round(value * 100, 0, MidpointRounding.AwayFromZero); - return Convert.ToInt32(result); + if (denomination == decimal.Zero) + { + return value; + } + + return Math.Round(value / denomination, midpoint) * denomination; + } + + /// + /// Round decimal up or down to the nearest multiple of denomination + /// + /// Value to round + /// Denomination + /// true round to, false round down + /// Rounded value + public static decimal RoundToNearest(this decimal value, decimal denomination, bool roundUp) + { + if (denomination == decimal.Zero) + { + return value; + } + + var roundedValueBase = roundUp + ? Math.Ceiling(value / denomination) + : Math.Floor(value / denomination); + + return Math.Round(roundedValueBase) * denomination; + } + + /// + /// Round decimal up or down to the nearest multiple of denomination if activated for currency + /// + /// Value to round + /// Currency. Rounding must be activated for this currency. + /// The rounding amount + /// Rounded value + public static decimal RoundToNearest(this decimal value, Currency currency, out decimal roundingAmount) + { + var oldValue = value; + + switch (currency.RoundOrderTotalRule) + { + case CurrencyRoundingRule.RoundMidpointUp: + value = value.RoundToNearest(currency.RoundOrderTotalDenominator, MidpointRounding.AwayFromZero); + break; + case CurrencyRoundingRule.AlwaysRoundDown: + value = value.RoundToNearest(currency.RoundOrderTotalDenominator, false); + break; + case CurrencyRoundingRule.AlwaysRoundUp: + value = value.RoundToNearest(currency.RoundOrderTotalDenominator, true); + break; + case CurrencyRoundingRule.RoundMidpointDown: + default: + value = value.RoundToNearest(currency.RoundOrderTotalDenominator, MidpointRounding.ToEven); + break; + } + + roundingAmount = value - Math.Round(oldValue, 2); + + return value; + } + + /// + /// Rounds a value if rounding is enabled for the currency + /// + /// Value to round + /// Currency + /// Rounded value + public static decimal RoundIfEnabledFor(this decimal value, Currency currency) + { + Guard.NotNull(currency, nameof(currency)); + + if (currency.RoundOrderItemsEnabled) + { + return Math.Round(value, currency.RoundNumDecimals); + } + + return value; + } + + /// + /// Rounds and formats a decimal culture invariant + /// + /// Value to round + /// Rounding decimal number + /// Rounded and formated value + public static string FormatInvariant(this decimal value, int decimals = 2) + { + return Math.Round(value, decimals).ToString("0.00", CultureInfo.InvariantCulture); } } } diff --git a/src/Libraries/SmartStore.Core/Extensions/DictionaryExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/DictionaryExtensions.cs index 03eb3408f2..b490bb012d 100644 --- a/src/Libraries/SmartStore.Core/Extensions/DictionaryExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/DictionaryExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; using System.Web; using System.Web.Routing; @@ -49,20 +50,38 @@ public static void Merge(this IDictionary instance, public static void AppendInValue(this IDictionary instance, string key, string separator, object value) { - instance[key] = !instance.ContainsKey(key) ? value.ToString() : (instance[key] + separator + value); - } + AddInValue(instance, key, separator, value, false); + } public static void PrependInValue(this IDictionary instance, string key, string separator, object value) { - instance[key] = !instance.ContainsKey(key) ? value.ToString() : (value + separator + instance[key]); + AddInValue(instance, key, separator, value, true); } + private static void AddInValue(IDictionary instance, string key, string separator, object value, bool prepend = false) + { + var valueStr = value.ToString(); + + if (!instance.ContainsKey(key)) + { + instance[key] = valueStr; + } + else + { + var arr = instance[key].ToString().Trim().Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries).AsEnumerable(); + var arrValue = valueStr.Trim().Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries).AsEnumerable(); + + arr = prepend ? arrValue.Union(arr) : arr.Union(arrValue); + + instance[key] = string.Join(separator, arr); + } + } + public static TValue Get(this IDictionary instance, TKey key) { Guard.NotNull(instance, nameof(instance)); - TValue val; - instance.TryGetValue(key, out val); + instance.TryGetValue(key, out var val); return val; } diff --git a/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs index b07872caf9..87abdd2e99 100644 --- a/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs @@ -12,6 +12,101 @@ namespace SmartStore { + public static class CollectionSlicer + { + /// + /// Slices the iteration over an enumerable by the given slice sizes. + /// + /// + /// The source sequence to slice + /// + /// Slice sizes. At least one size is required. Multiple sizes result in differently sized slices, + /// whereat the last size is used for the "rest" (if any) + /// + /// The sliced enumerable + public static IEnumerable> Slice(this IEnumerable source, params int[] sizes) + { + if (!sizes.Any(step => step != 0)) + { + throw new InvalidOperationException("Can't slice a collection with step length 0."); + } + + return new Slicer(source.GetEnumerator(), sizes).Slice(); + } + } + + internal sealed class Slicer + { + private readonly IEnumerator _iterator; + private readonly int[] _sizes; + private volatile bool _hasNext; + private volatile int _currentSize; + private volatile int _index; + + public Slicer(IEnumerator iterator, int[] sizes) + { + _iterator = iterator; + _sizes = sizes; + _index = 0; + _currentSize = 0; + _hasNext = true; + } + + public int Index + { + get { return _index; } + } + + public IEnumerable> Slice() + { + var length = _sizes.Length; + var index = 1; + var size = 0; + + for (var i = 0; _hasNext; ++i) + { + if (i < length) + { + size = _sizes[i]; + _currentSize = size - 1; + } + + while (_index < index && _hasNext) + { + _hasNext = MoveNext(); + } + + if (_hasNext) + { + yield return new List(SliceInternal()); + index += size; + } + } + } + + private IEnumerable SliceInternal() + { + if (_currentSize == -1) yield break; + yield return _iterator.Current; + + for (var count = 0; count < _currentSize && _hasNext; ++count) + { + _hasNext = MoveNext(); + + if (_hasNext) + { + yield return _iterator.Current; + } + } + } + + private bool MoveNext() + { + ++_index; + return _iterator.MoveNext(); + } + } + [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] public static class EnumerableExtensions { @@ -39,44 +134,6 @@ internal static ReadOnlyCollection Empty #region IEnumerable - private class Status - { - public bool EndOfSequence; - } - - private static IEnumerable TakeOnEnumerator(IEnumerator enumerator, int count, Status status) - { - while (--count > 0 && (enumerator.MoveNext() || !(status.EndOfSequence = true))) - { - yield return enumerator.Current; - } - } - - - /// - /// Slices the iteration over an enumerable by the given chunk size. - /// - /// - /// - /// SIze of chunk - /// The sliced enumerable - public static IEnumerable> Chunk(this IEnumerable items, int chunkSize = 100) - { - if (chunkSize < 1) - { - throw new ArgumentException("Chunks should not be smaller than 1 element"); - } - var status = new Status { EndOfSequence = false }; - using (var enumerator = items.GetEnumerator()) - { - while (!status.EndOfSequence) - { - yield return TakeOnEnumerator(enumerator, chunkSize, status); - } - } - } - - /// /// Performs an action on each item while iterating through a list. /// This is a handy shortcut for foreach(item in list) { ... } @@ -115,19 +172,16 @@ public static ReadOnlyCollection AsReadOnly(this IEnumerable source) if (source == null || !source.Any()) return DefaultReadOnlyCollection.Empty; - var readOnly = source as ReadOnlyCollection; - if (readOnly != null) + if (source is ReadOnlyCollection readOnly) { return readOnly; } - - var list = source as List; - if (list != null) + else if (source is List list) { return list.AsReadOnly(); } - return new ReadOnlyCollection(source.ToArray()); + return new ReadOnlyCollection(source.ToList()); } /// diff --git a/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs index 9f13cd8931..f4452fb5ed 100644 --- a/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs @@ -69,19 +69,6 @@ public static string ToHexString(this byte[] bytes, int length = 0) return sb.ToString(); } - public static T GetMergedDataValue(this IMergedData mergedData, string key, T defaultValue) - { - if (mergedData.MergedDataValues != null && !mergedData.MergedDataIgnore) - { - object value; - - if (mergedData.MergedDataValues.TryGetValue(key, out value)) - return (T)value; - } - - return defaultValue; - } - /// /// Append grow if string builder is empty. Append delimiter and grow otherwise. /// diff --git a/src/Libraries/SmartStore.Core/Extensions/StreamExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/StreamExtensions.cs index 997e758209..5a93a85543 100644 --- a/src/Libraries/SmartStore.Core/Extensions/StreamExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/StreamExtensions.cs @@ -26,17 +26,19 @@ public static bool ToFile(this Stream srcStream, string path) return false; const int BuffSize = 32768; - bool result = true; + var result = true; Stream dstStream = null; - byte[] buffer = new Byte[BuffSize]; + var buffer = new byte[BuffSize]; try { - using (dstStream = File.OpenWrite(path)) - { + using (dstStream = File.Open(path, FileMode.Create)) + { int len; - while ((len = srcStream.Read(buffer, 0, BuffSize)) > 0) - dstStream.Write(buffer, 0, len); + while ((len = srcStream.Read(buffer, 0, BuffSize)) > 0) + { + dstStream.Write(buffer, 0, len); + } } } catch @@ -52,7 +54,7 @@ public static bool ToFile(this Stream srcStream, string path) } } - return (result && System.IO.File.Exists(path)); + return (result && File.Exists(path)); } public static bool ContentsEqual(this Stream src, Stream other) diff --git a/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs index 6ac93e8fc2..0ea3b68f72 100644 --- a/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs @@ -9,7 +9,8 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; -using HtmlAgilityPack; +using AngleSharp.Dom; +using AngleSharp.Parser.Html; namespace SmartStore { @@ -374,6 +375,190 @@ public static string Truncate(this string value, int maxLength, string suffix = } } + /// + /// Removes all redundant whitespace (empty lines, double space etc.). + /// Use ~! literal to keep whitespace wherever necessary. + /// + /// Input + /// The compacted string + public static string Compact(this string input, bool removeEmptyLines = false) + { + Guard.NotNull(input, nameof(input)); + + var sb = new StringBuilder(); + var lines = GetLines(input.Trim(), true, removeEmptyLines).ToArray(); + + foreach (var line in lines) + { + var len = line.Length; + var sbLine = new StringBuilder(len); + var isChar = false; + var isLiteral = false; // When we detect the ~! literal + int i = 0; + var eof = false; + + for (i = 0; i < len; i++) + { + var c = line[i]; + + eof = i == len - 1; + + if (Char.IsWhiteSpace(c)) + { + // Space, Tab etc. + if (isChar) + { + // If last char not empty, append the space. + sbLine.Append(' '); + } + + isLiteral = false; + isChar = false; + } + else + { + // Char or Literal (~!) + + isLiteral = c == '~' && !eof && line[i + 1] == '!'; + isChar = true; + + if (isLiteral) + { + sbLine.Append(' '); + i++; // skip next "!" char + } + else + { + sbLine.Append(c); + } + } + } + + // Append the compacted and trimmed line + sb.AppendLine(sbLine.ToString().Trim().Trim(',')); + } + + return sb.ToString().Trim(); + } + + /// + /// Splits the input string by carriage return. + /// + /// The string to split + /// A sequence with string items per line + public static IEnumerable GetLines(this string input, bool trimLines = false, bool removeEmptyLines = false) + { + if (input.IsEmpty()) + { + yield break; + } + + using (var sr = new StringReader(input)) + { + string line; + while ((line = sr.ReadLine()) != null) + { + if (trimLines) + { + line = line.Trim(); + } + + if (removeEmptyLines && IsEmpty(line)) + { + continue; + } + + yield return line; + } + } + } + + ///// + ///// Removes all redundant whitespace (empty lines, double space etc.). + ///// Use ~! literal to keep whitespace wherever necessary. + ///// + ///// Input + ///// The compacted string + //public static string Compact(this string input) + //{ + // Guard.NotNull(input, nameof(input)); + + // var isNewLine = false; + // var isBlank = false; + // var isChar = false; + // var isLiteral = false; // When we detect the ~! literal + // var len = input.Length; + // int i = 0; + // var eof = false; + + // var sb = new StringBuilder(); + + // for (i = 0; i < len; i++) + // { + // var c = input[i]; + + // eof = i == len - 1; + + // if (Char.IsWhiteSpace(c)) + // { + // if (c == '\r' && !eof && input[i + 1] == '\n') + // { + // // \r\n detected, don't double-check + // continue; + // } + + // if (c == '\r' || c == '\n') + // { + // // New line + // if (i > 0 && sb[sb.Length - 1] == ' ') + // { + // // If NewLine is detected, trim end (all trailing whitespace) + // sb.Remove(sb.Length - 1, 1); + // } + // } + // else + // { + // // Space, tab etc. + // if (isChar) + // { + // // If last char not empty, append the space... + // sb.Append(' '); + // } + // } + + // isLiteral = false; + // isChar = false; + // isBlank = true; + // isNewLine = c == '\r' || c == '\n'; + // } + // else // No WhiteSpace + // { + // if (isNewLine) + // { + // // First non-blank char in current line: write NewLine first. + // sb.AppendLine(); + // } + + // isLiteral = c == '~' && !eof && input[i + 1] == '!'; + // isChar = true; + // isNewLine = false; + // isBlank = false; + + // if (isLiteral) + // { + // sb.Append(' '); + // i++; // skip next "!" char + // } + // else + // { + // sb.Append(c); + // } + // } + // } + + // return sb.ToString(); + //} + /// /// Ensure that a string starts with a string. /// @@ -456,29 +641,28 @@ public static string RemoveHtml(this string source) if (source.IsEmpty()) return string.Empty; - var doc = new HtmlDocument() - { - OptionOutputOriginalCase = true, - OptionFixNestedTags = true, - OptionAutoCloseOnEnd = true, - OptionDefaultStreamEncoding = Encoding.UTF8 - }; - - doc.LoadHtml(source); - var nodes = doc.DocumentNode.Descendants().Where(n => - n.NodeType == HtmlNodeType.Text && - n.ParentNode.Name != "script" && - n.ParentNode.Name != "style" && - n.ParentNode.Name != "svg"); + var ignoreTags = new HashSet(StringComparer.OrdinalIgnoreCase) { "script", "style", "svg", "img" }; + + var parser = new HtmlParser(); + var doc = parser.Parse(source); + + var treeWalker = doc.CreateTreeWalker(doc.Body, FilterSettings.Text); var sb = new StringBuilder(); - foreach (var node in nodes) + + var node = treeWalker.ToNext(); + while (node != null) { - var text = node.InnerText; - if (text.HasValue()) + if (!ignoreTags.Contains(node.Parent.NodeName)) { - sb.AppendLine(node.InnerText); + var text = node.TextContent; + if (text.HasValue()) + { + sb.AppendLine(text); + } } + + node = treeWalker.ToNext(); } return sb.ToString().HtmlDecode(); @@ -556,18 +740,27 @@ public static string[] SplitSafe(this string value, string separator) /// true: success, false: failure [DebuggerStepThrough] [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")] - public static bool SplitToPair(this string value, out string strLeft, out string strRight, string delimiter) + public static bool SplitToPair(this string value, out string leftPart, out string rightPart, string delimiter, bool splitAfterLast = false) { - int idx = -1; - if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(delimiter) || (idx = value.IndexOf(delimiter)) == -1) + leftPart = value; + rightPart = ""; + + if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(delimiter)) { - strLeft = value; - strRight = ""; return false; } - strLeft = value.Substring(0, idx); - strRight = value.Substring(idx + delimiter.Length); + var idx = splitAfterLast + ? value.LastIndexOf(delimiter) + : value.IndexOf(delimiter); + + if (idx == -1) + { + return false; + } + + leftPart = value.Substring(0, idx); + rightPart = value.Substring(idx + delimiter.Length); return true; } @@ -725,8 +918,9 @@ public static string Replace(this string value, int x1, int x2, string replaceBy { if (value.HasValue() && x1 > 0 && x2 > x1 && x2 < value.Length) { - return value.Substring(0, x1) + (replaceBy == null ? "" : replaceBy) + value.Substring(x2 + 1); + return value.Substring(0, x1) + (replaceBy.EmptyNull()) + value.Substring(x2 + 1); } + return value; } diff --git a/src/Libraries/SmartStore.Core/Extensions/TypeExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/TypeExtensions.cs index b92b4039f4..d2b270c7e5 100644 --- a/src/Libraries/SmartStore.Core/Extensions/TypeExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/TypeExtensions.cs @@ -4,6 +4,7 @@ using System.Reflection; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Collections; namespace SmartStore { @@ -25,13 +26,12 @@ public static string AssemblyQualifiedNameWithoutVersion(this Type type) return null; } - public static bool IsSequenceType(this Type seqType) + public static bool IsSequenceType(this Type type) { - return ( - (((seqType != typeof(string)) - && (seqType != typeof(byte[]))) - && (seqType != typeof(char[]))) - && (FindIEnumerable(seqType) != null)); + if (type == typeof(string)) + return false; + + return type.IsArray || typeof(IEnumerable).IsAssignableFrom(type); } public static bool IsPredefinedSimpleType(this Type type) @@ -40,6 +40,7 @@ public static bool IsPredefinedSimpleType(this Type type) { return true; } + if (type.IsEnum) { return true; @@ -54,6 +55,7 @@ public static bool IsStruct(this Type type) { return !type.IsPredefinedSimpleType(); } + return false; } @@ -81,7 +83,12 @@ public static bool IsPredefinedType(this Type type) return true; } - public static bool IsInteger(this Type type) + public static bool IsPlainObjectType(this Type type) + { + return type.IsClass && !type.IsSequenceType() && !type.IsPredefinedType(); + } + + public static bool IsInteger(this Type type) { switch (Type.GetTypeCode(type)) { @@ -99,9 +106,16 @@ public static bool IsInteger(this Type type) } } - public static bool IsNullable(this Type type) + public static bool IsNullable(this Type type, out Type wrappedType) { - return type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + wrappedType = null; + + if (type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + wrappedType = type.GetGenericArguments()[0]; + } + + return false; } public static bool IsConstructable(this Type type) @@ -133,6 +147,7 @@ public static bool IsAnonymous(this Type type) } } } + return false; } @@ -209,11 +224,12 @@ private static bool IsSubClassInternal(Type initialType, Type currentType, Type /// public static Type GetNonNullableType(this Type type) { - if (!IsNullable(type)) + if (!IsNullable(type, out var wrappedType)) { return type; } - return type.GetGenericArguments()[0]; + + return wrappedType; } /// @@ -341,8 +357,10 @@ internal static Type FindIEnumerable(this Type seqType) { if (seqType == null || seqType == typeof(string)) return null; + if (seqType.IsArray) return typeof(IEnumerable<>).MakeGenericType(seqType.GetElementType()); + if (seqType.IsGenericType) { var args = seqType.GetGenericArguments(); @@ -353,6 +371,7 @@ internal static Type FindIEnumerable(this Type seqType) return ienum; } } + Type[] ifaces = seqType.GetInterfaces(); if (ifaces.Length > 0) { @@ -363,8 +382,10 @@ internal static Type FindIEnumerable(this Type seqType) return ienum; } } + if (seqType.BaseType != null && seqType.BaseType != typeof(object)) return FindIEnumerable(seqType.BaseType); + return null; } } diff --git a/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs b/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs index b223e44a64..5238183265 100644 --- a/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs +++ b/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs @@ -136,31 +136,11 @@ public override string UserHostAddress get { return null; } } - public override string RawUrl - { - get { return this.ApplicationPath; } - } - - public override bool IsSecureConnection - { - get { return false; } - } - - public override bool IsAuthenticated - { - get - { - return false; - } - } - - public override string[] UserLanguages - { - get - { - return new string[] { }; - } - } + public override string RawUrl => this.ApplicationPath; + public override bool IsSecureConnection => false; + public override bool IsAuthenticated => false; + public override string[] UserLanguages => new string[] { }; + public override string UserAgent => "SmartStore.NET"; public override RequestContext RequestContext { diff --git a/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs b/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs index 6f03a2da49..98e482d393 100644 --- a/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs +++ b/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs @@ -3,23 +3,21 @@ using System.Text.RegularExpressions; using System.Web; using System.Linq; +using AngleSharp; +using AngleSharp.Parser.Html; +using AngleSharp.Parser.Css; namespace SmartStore.Core.Html { /// - /// Represents a HTML helper + /// Utility class for html manipulation or creation /// public partial class HtmlUtils { - #region Fields - private readonly static Regex paragraphStartRegex = new Regex("

", RegexOptions.IgnoreCase); - private readonly static Regex paragraphEndRegex = new Regex("

", RegexOptions.IgnoreCase); + private readonly static Regex _paragraphStartRegex = new Regex("

", RegexOptions.IgnoreCase); + private readonly static Regex _paragraphEndRegex = new Regex("

", RegexOptions.IgnoreCase); //private static Regex ampRegex = new Regex("&(?!(?:#[0-9]{2,4};|[a-z0-9]+;))", RegexOptions.Compiled | RegexOptions.IgnoreCase); - #endregion - - #region Utilities - private static string EnsureOnlyAllowedHtml(string text) { if (String.IsNullOrEmpty(text)) @@ -56,9 +54,7 @@ private static bool IsValidTag(string tag, string tags) return allowedTags.Any(aTag => tag == aTag); } - #endregion - #region Methods /// /// Formats the text /// @@ -70,9 +66,14 @@ private static bool IsValidTag(string tag, string tags) /// A value indicating whether to resolve links /// A value indicating whether to add "noFollow" tag /// Formatted text - public static string FormatText(string text, bool stripTags, - bool convertPlainTextToHtml, bool allowHtml, - bool allowBBCode, bool resolveLinks, bool addNoFollowTag) + public static string FormatText( + string text, + bool stripTags, + bool convertPlainTextToHtml, + bool allowHtml, + bool allowBBCode, + bool resolveLinks, + bool addNoFollowTag) { if (String.IsNullOrEmpty(text)) @@ -178,8 +179,7 @@ public static string ConvertPlainTextToHtml(string text) /// A value indicating whether to decode text /// A value indicating whether to replace anchor text (remove a tag from the following url Name and output only the string "Name") /// Formatted text - public static string ConvertHtmlToPlainText(string text, - bool decode = false, bool replaceAnchorTags = false) + public static string ConvertHtmlToPlainText(string text, bool decode = false, bool replaceAnchorTags = false) { if (String.IsNullOrEmpty(text)) return string.Empty; @@ -257,8 +257,8 @@ public static string ConvertPlainTextToParagraph(string text) if (String.IsNullOrEmpty(text)) return string.Empty; - text = paragraphStartRegex.Replace(text, string.Empty); - text = paragraphEndRegex.Replace(text, "\n"); + text = _paragraphStartRegex.Replace(text, string.Empty); + text = _paragraphEndRegex.Replace(text, "\n"); text = text.Replace("\r\n", "\n").Replace("\r", "\n"); text = text + "\n\n"; text = text.Replace("\n\n", "\n"); @@ -271,8 +271,39 @@ public static string ConvertPlainTextToParagraph(string text) builder.AppendFormat("

{0}

\n", str); } } + return builder.ToString(); } - #endregion + + /// + /// Converts all occurences of pixel-based inline font-size expression to relative 'em' + /// + /// + /// + /// + public static string RelativizeFontSizes(string html, int baseFontSizePx = 16) + { + Guard.NotEmpty(html, nameof(html)); + Guard.IsPositive(baseFontSizePx, nameof(baseFontSizePx)); + + var parser = new HtmlParser(new AngleSharp.Configuration().WithCss()); + var doc = parser.Parse(html); + + var nodes = doc.QuerySelectorAll("*[style]"); + foreach (var node in nodes) + { + if (node.Style.FontSize is string s && s.EndsWith("px")) + { + var size = s.Substring(0, s.Length - 2).Convert(); + if (size > 0) + { + //node.Style.FontSize = Math.Round(((double)size / (double)baseFontSizePx), 4) + "em"; + node.Style.FontSize = "{0}em".FormatInvariant(Math.Round(((double)size / (double)baseFontSizePx), 4)); + } + } + } + + return doc.Body.InnerHtml; + } } } diff --git a/src/Libraries/SmartStore.Core/IMergedData.cs b/src/Libraries/SmartStore.Core/IMergedData.cs index fa1b2e61b5..8233a55987 100644 --- a/src/Libraries/SmartStore.Core/IMergedData.cs +++ b/src/Libraries/SmartStore.Core/IMergedData.cs @@ -1,4 +1,7 @@ using System.Collections.Generic; +using System.Web.Hosting; +using SmartStore.Core.Data; +using SmartStore.Core.Infrastructure; namespace SmartStore.Core { @@ -7,4 +10,38 @@ public interface IMergedData bool MergedDataIgnore { get; set; } Dictionary MergedDataValues { get; } } + + public static class IMergedDataExtensions + { + public static T GetMergedDataValue(this IMergedData mergedData, string key, T defaultValue) + { + if (mergedData.MergedDataValues == null) + return defaultValue; + + if (mergedData.MergedDataIgnore) + return defaultValue; + + if (mergedData is BaseEntity && HostingEnvironment.IsHosted) + { + // This is absolutely bad coding! But I don't see any alternatives. + // When the passed object is a (EF)-trackable entity, + // we cannot return the merged value while EF performs + // change detection, because entity properties could be set to modified, + // where in fact nothing has changed. + var dbContext = EngineContext.Current.Resolve(); + if (dbContext.IsDetectingChanges()) + { + return defaultValue; + } + } + + object value; + if (mergedData.MergedDataValues.TryGetValue(key, out value)) + { + return (T)value; + } + + return defaultValue; + } + } } diff --git a/src/Libraries/SmartStore.Core/IO/IFile.cs b/src/Libraries/SmartStore.Core/IO/IFile.cs index 53f86533eb..6d86299fc8 100644 --- a/src/Libraries/SmartStore.Core/IO/IFile.cs +++ b/src/Libraries/SmartStore.Core/IO/IFile.cs @@ -1,4 +1,5 @@ using System; +using System.Drawing; using System.IO; using System.Threading.Tasks; @@ -6,11 +7,46 @@ namespace SmartStore.Core.IO { public interface IFile { - string Path { get; } - string Name { get; } + /// + /// The path relative to the storage root + /// + string Path { get; } + + /// + /// The path without the file part, but with trailing slash + /// + string Directory { get; } + + /// + /// File name including extension + /// + string Name { get; } + + /// + /// File name excluding extension + /// + string Title { get; } + + /// + /// Size in bytes + /// long Size { get; } + + /// + /// Expressed as UTC time + /// DateTime LastUpdated { get; } - string FileType { get; } + + /// + /// File extension including dot + /// + string Extension { get; } + + /// + /// Dimensions, if the file is an image. + /// + Size Dimensions { get; } + bool Exists { get; } /// diff --git a/src/Libraries/SmartStore.Core/IO/IFileSystem.cs b/src/Libraries/SmartStore.Core/IO/IFileSystem.cs index da424d28a7..c90b886ffb 100644 --- a/src/Libraries/SmartStore.Core/IO/IFileSystem.cs +++ b/src/Libraries/SmartStore.Core/IO/IFileSystem.cs @@ -7,6 +7,11 @@ namespace SmartStore.Core.IO { public interface IFileSystem { + /// + /// Checks whether the underlying storage is remote, like 'Azure' for example. + /// + bool IsCloudStorage { get; } + /// /// Gets the root path /// @@ -16,8 +21,12 @@ public interface IFileSystem /// Retrieves the public URL for a given file within the storage provider. /// /// The relative path within the storage provider. + /// + /// If true and the storage is in the cloud, returns the actual remote cloud URL to the resource. + /// If false, retrieves an app relative URL to delegate further processing to the media middleware (which can handle remote files) + /// /// The public URL. - string GetPublicUrl(string path); + string GetPublicUrl(string path, bool forCloud = false); /// /// Retrieves the path within the storage provider for a given public url. @@ -64,12 +73,24 @@ public interface IFileSystem /// If the file or the folder is not found. IFolder GetFolderForFile(string path); + /// + /// Retrieves the count of files within a path. + /// + /// The relative path to the folder in which to retrieve file count. + /// The file pattern to match + /// Optional. Files matching the predicate are excluded. + /// Whether to count files in all subfolders also + /// Total count of files. + long CountFiles(string path, string pattern, Func predicate, bool deep = true); + /// /// Performs a deep search for files within a path. /// /// The relative path to the folder in which to process file search. + /// The file pattern to match + /// Whether to search in all subfolders also /// Matching file names - IEnumerable SearchFiles(string path, string pattern); + IEnumerable SearchFiles(string path, string pattern, bool deep = true); /// /// Lists the files within a storage provider's path. diff --git a/src/Libraries/SmartStore.Core/IO/IFileSystemExtensions.cs b/src/Libraries/SmartStore.Core/IO/IFileSystemExtensions.cs index 673dab05dd..755aee74e1 100644 --- a/src/Libraries/SmartStore.Core/IO/IFileSystemExtensions.cs +++ b/src/Libraries/SmartStore.Core/IO/IFileSystemExtensions.cs @@ -222,5 +222,68 @@ public static bool TryCreateFolder(this IFileSystem fileSystem, string path) return true; } + /// + /// Checks whether the name of the file is unique within its directory. + /// When given file exists, this method appends [1...n] to the file title until + /// the check returns false. + /// + /// The path of file to check + /// An object containing the unique file's info, or null if method returns false + /// + /// false when does not exist yet. true otherwise. + /// + public static bool CheckFileUniqueness(this IFileSystem fileSystem, string path, out IFile uniqueFile) + { + Guard.NotEmpty(path, nameof(path)); + + uniqueFile = null; + + var file = fileSystem.GetFile(path); + if (!file.Exists) + { + return false; + } + + var pattern = string.Concat(file.Title, "-*", file.Extension); + var dir = file.Directory; + var files = new HashSet(fileSystem.SearchFiles(dir, pattern, false).Select(x => Path.GetFileName(x))); + + int i = 1; + while (true) + { + var newFileName = string.Concat(file.Title, "-", i, file.Extension); + if (!files.Contains(newFileName)) + { + // Found our gap + uniqueFile = fileSystem.GetFile(string.Concat(dir, newFileName)); + return true; + } + + i++; + } + } + + /// + /// Retrieves the count of files within a path. + /// + /// The relative path to the folder in which to retrieve file count. + /// The file pattern to match + /// Whether to count files in all subfolders also + /// Total count of files. + public static long CountFiles(this IFileSystem fileSystem, string path, string pattern, bool deep = true) + { + return fileSystem.CountFiles(path, pattern, null, deep); + } + + /// + /// Retrieves the count of files within a path. + /// + /// The relative path to the folder in which to retrieve file count. + /// Whether to count files in all subfolders also + /// Total count of files. + public static long CountFiles(this IFileSystem fileSystem, string path, bool deep = true) + { + return fileSystem.CountFiles(path, "*", null, deep); + } } } diff --git a/src/Libraries/SmartStore.Core/IO/ImageHeader.cs b/src/Libraries/SmartStore.Core/IO/ImageHeader.cs new file mode 100644 index 0000000000..c359438b7d --- /dev/null +++ b/src/Libraries/SmartStore.Core/IO/ImageHeader.cs @@ -0,0 +1,373 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Text; + +namespace SmartStore.Core.IO +{ + /// + /// Taken from http://stackoverflow.com/questions/111345/getting-image-dimensions-without-reading-the-entire-file/111349 + /// Minor improvements including supporting unsigned 16-bit integers when decoding Jfif and added logic + /// to load the image using new Bitmap if reading the headers fails + /// + public static class ImageHeader + { + internal class UnknownImageFormatException : ArgumentException + { + public UnknownImageFormatException(string paramName = "", Exception e = null) + : base("Could not recognise image format.", paramName, e) + { + } + } + + private static Dictionary> _imageFormatDecoders = new Dictionary>() + { + { new byte[] { 0x42, 0x4D }, DecodeBitmap }, + { new byte[] { 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 }, DecodeGif }, + { new byte[] { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 }, DecodeGif }, + { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }, DecodePng }, + // { new byte[] { 0xff, 0xd8 }, DecodeJfif }, + //{ new byte[] { 0xff, 0xd8, 0xff, 0xe0 }, DecodeJpeg }, + //{ new byte[] { 0xff }, DecodeJpeg2 }, + }; + + private static int _maxMagicBytesLength = 0; + + static ImageHeader() + { + _maxMagicBytesLength = _imageFormatDecoders.Keys.OrderByDescending(x => x.Length).First().Length; + } + + /// + /// Gets the dimensions of an image. + /// + /// The path of the image to get the dimensions for. + /// The dimensions of the specified image. + public static Size GetDimensions(string path) + { + if (!File.Exists(path)) + { + throw new FileNotFoundException("File '{0}' does not exist.".FormatInvariant(path)); + } + + var mime = MimeTypes.MapNameToMimeType(path); + return GetDimensions(File.OpenRead(path), mime, false); + } + + /// + /// Gets the dimensions of an image. + /// + /// The bytes of the image to get the dimensions for. + /// The MIME type of the image. Can be null. + /// The dimensions of the specified image. + public static Size GetDimensions(byte[] buffer, string mime = null) + { + if (buffer == null || buffer.Length == 0) + { + return Size.Empty; + } + + return GetDimensions(new MemoryStream(buffer), mime, false); + } + + /// + /// Gets the dimensions of an image. + /// + /// The stream of the image to get the dimensions for. + /// If false, the passed stream will get disposed + /// The dimensions of the specified image. + public static Size GetDimensions(Stream input, bool leaveOpen = true) + { + return GetDimensions(input, null, leaveOpen); + } + + /// + /// Gets the dimensions of an image. + /// + /// The stream of the image to get the dimensions for. + /// The MIME type of the image. Can be null. + /// If false, the passed stream will get disposed + /// The dimensions of the specified image. + public static Size GetDimensions(Stream input, string mime, bool leaveOpen = true) + { + Guard.NotNull(input, nameof(input)); + + var gdip = false; + + if (!input.CanSeek || input.Length == 0) + { + return Size.Empty; + } + + try + { + if (mime == "image/jpeg") + { + // Reading JPEG header does not work reliably + gdip = true; + return GetDimensionsByGdip(input); + } + + using (var reader = new BinaryReader(input, Encoding.Unicode, true)) + { + return GetDimensions(reader); + } + } + catch (Exception ex) + { + if (gdip) + { + throw ex; + } + + // something went wrong with fast image access, + // so get original size the classic way + try + { + input.Seek(0, SeekOrigin.Begin); + return GetDimensionsByGdip(input); + } + catch + { + throw ex; + } + } + finally + { + if (!leaveOpen) + { + input.Dispose(); + } + } + } + + /// + /// Gets the dimensions of an image. + /// + /// The path of the image to get the dimensions of. + /// The dimensions of the specified image. + /// The image was of an unrecognised format. + public static Size GetDimensions(BinaryReader binaryReader) + { + byte[] magicBytes = new byte[_maxMagicBytesLength]; + for (int i = 0; i < _maxMagicBytesLength; i += 1) + { + magicBytes[i] = binaryReader.ReadByte(); + foreach (var kvPair in _imageFormatDecoders) + { + if (StartsWith(magicBytes, kvPair.Key)) + { + return kvPair.Value(binaryReader); + } + } + } + + throw new UnknownImageFormatException("binaryReader"); + } + + private static Size GetDimensionsByGdip(Stream input) + { + using (var b = Image.FromStream(input, false, false)) + { + return new Size(b.Width, b.Height); + } + } + + private static bool StartsWith(byte[] thisBytes, byte[] thatBytes) + { + for (int i = 0; i < thatBytes.Length; i += 1) + { + if (thisBytes[i] != thatBytes[i]) + { + return false; + } + } + + return true; + } + + private static short ReadLittleEndianInt16(BinaryReader binaryReader) + { + byte[] bytes = new byte[sizeof(short)]; + + for (int i = 0; i < sizeof(short); i += 1) + { + bytes[sizeof(short) - 1 - i] = binaryReader.ReadByte(); + } + return BitConverter.ToInt16(bytes, 0); + } + + private static ushort ReadLittleEndianUInt16(BinaryReader binaryReader) + { + byte[] bytes = new byte[sizeof(ushort)]; + + for (int i = 0; i < sizeof(ushort); i += 1) + { + bytes[sizeof(ushort) - 1 - i] = binaryReader.ReadByte(); + } + return BitConverter.ToUInt16(bytes, 0); + } + + private static int ReadLittleEndianInt32(BinaryReader binaryReader) + { + byte[] bytes = new byte[sizeof(int)]; + for (int i = 0; i < sizeof(int); i += 1) + { + bytes[sizeof(int) - 1 - i] = binaryReader.ReadByte(); + } + return BitConverter.ToInt32(bytes, 0); + } + + private static Size DecodeBitmap(BinaryReader binaryReader) + { + binaryReader.ReadBytes(16); + int width = binaryReader.ReadInt32(); + int height = binaryReader.ReadInt32(); + return new Size(width, height); + } + + private static Size DecodeGif(BinaryReader binaryReader) + { + int width = binaryReader.ReadInt16(); + int height = binaryReader.ReadInt16(); + return new Size(width, height); + } + + private static Size DecodePng(BinaryReader binaryReader) + { + binaryReader.ReadBytes(8); + int width = ReadLittleEndianInt32(binaryReader); + int height = ReadLittleEndianInt32(binaryReader); + return new Size(width, height); + } + + #region Experiments + + private static Size DecodeJpeg(BinaryReader reader) + { + // For JPEGs, we need to read the first 12 bytes of each chunk. + // We'll read those 12 bytes at buf+2...buf+14, i.e. overwriting the existing buf. + + var buf = (new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }).Concat(reader.ReadBytes(20)).ToArray(); + + using (var f = new MemoryStream(buf)) + { + if (buf[6] == (byte)'J' && buf[7] == (byte)'F' && buf[8] == (byte)'I' && buf[9] == (byte)'F') + { + var len = buf.Length; + long pos = 2; + while (buf[2] == 0xFF) + { + if (buf[3] == 0xC0 || buf[3] == 0xC1 || buf[3] == 0xC2 || buf[3] == 0xC3 || buf[3] == 0xC9 || buf[3] == 0xCA || buf[3] == 0xCB) break; + pos += 2 + (buf[4] << 8) + buf[5]; + if (pos + 12 > len) break; + //fseek(f, pos, SEEK_SET); + f.Seek(pos, SeekOrigin.Begin); + //fread(buf + 2, 1, 12, f); + f.Read(buf, 0, 12); + } + } + } + + // JPEG: (first two bytes of buf are first two bytes of the jpeg file; rest of buf is the DCT frame + if (buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF) + { + var height = (buf[7] << 8) + buf[8]; + var width = (buf[9] << 8) + buf[10]; + + return new Size(width, height); + } + + throw new UnknownImageFormatException(); + } + + private static Size DecodeJpeg2(BinaryReader reader) + { + bool found = false; + bool eof = false; + + while (!found || eof) + { + // read 0xFF and the type + //reader.ReadByte(); + byte type = reader.ReadByte(); + + // get length + int len = 0; + switch (type) + { + // start and end of the image + case 0xD8: + case 0xD9: + len = 0; + break; + + // restart interval + case 0xDD: + len = 2; + break; + + // the next two bytes is the length + default: + int lenHi = reader.ReadByte(); + int lenLo = reader.ReadByte(); + len = (lenHi << 8 | lenLo) - 2; + break; + } + + // EOF? + if (type == 0xD9) + eof = true; + + // process the data + if (len > 0) + { + // read the data + byte[] data = reader.ReadBytes(len); + + // this is what we are looking for + if (type == 0xC0) + { + int width = data[1] << 8 | data[2]; + int height = data[3] << 8 | data[4]; + return new Size(width, height); + } + } + } + + throw new UnknownImageFormatException(); + } + + private static Size DecodeJfif(BinaryReader reader) + { + while (reader.ReadByte() == 0xff) + { + byte marker = reader.ReadByte(); + short chunkLength = ReadLittleEndianInt16(reader); + if (marker == 0xc0) + { + reader.ReadByte(); + int height = ReadLittleEndianInt16(reader); + int width = ReadLittleEndianInt16(reader); + return new Size(width, height); + } + + if (chunkLength < 0) + { + ushort uchunkLength = (ushort)chunkLength; + reader.ReadBytes(uchunkLength - 2); + } + else + { + reader.ReadBytes(chunkLength - 2); + } + } + + throw new UnknownImageFormatException(); + } + + #endregion + } +} diff --git a/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs b/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs index d1ebc00c61..3187971ec6 100644 --- a/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs +++ b/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Drawing; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -13,7 +14,8 @@ public class LocalFileSystem : IFileSystem { private string _root; private string _publicPath; // /Shop/base - private string _storagePath; // C:\SMNET\base + private string _storagePath; // C:\SMNET\base + private bool _isCloudStorage; // When public URL is outside of current app public LocalFileSystem() : this(string.Empty, string.Empty) @@ -38,6 +40,11 @@ protected internal LocalFileSystem(string basePath, string publicPath) _root = basePath; } + public bool IsCloudStorage + { + get { return _isCloudStorage; } + } + private void NormalizeStoragePath(ref string basePath, bool basePathIsAbsolute) { if (basePathIsAbsolute) @@ -66,7 +73,12 @@ private string NormalizePublicPath(string publicPath, string basePath, bool base { if (publicPath.IsEmpty() || (!publicPath.StartsWith("~/") && !publicPath.IsWebUrl(true))) { - throw new ArgumentException("When the base path is a fully qualified path, the public path must not be empty, and either be a fully qualified URL or a virtual path (e.g.: ~/Media)", nameof(publicPath)); + var streamMedia = CommonHelper.GetAppSetting("sm:StreamRemoteMedia", true); + if (!streamMedia) + { + throw new ArgumentException(@"When the base path is a fully qualified path and remote media streaming is disabled, + the public path must not be empty, and either be a fully qualified URL or a virtual path (e.g.: ~/Media)", nameof(publicPath)); + } } } @@ -80,12 +92,14 @@ private string NormalizePublicPath(string publicPath, string basePath, bool base return appVirtualPath + publicPath.Substring(1); } - if (publicPath.IsEmpty()) + if (publicPath.IsEmpty() && !basePathIsAbsolute) { // > /MyAppRoot/Media return appVirtualPath + basePath; } + _isCloudStorage = true; + return publicPath; } @@ -124,7 +138,7 @@ static string Fix(string path) : path.TrimStart('/', '\\'); } - public string GetPublicUrl(string path) + public string GetPublicUrl(string path, bool forCloud = false) { return MapPublic(path); } @@ -188,12 +202,26 @@ public IFolder GetFolderForFile(string path) return new LocalFolder(Fix(folderPath), fileInfo.Directory); } - public IEnumerable SearchFiles(string path, string pattern) + public long CountFiles(string path, string pattern, Func predicate, bool deep = true) { - // get relative from absolute path + var files = SearchFiles(path, pattern, deep).AsParallel(); + + if (predicate != null) + { + return files.Count(predicate); + } + else + { + return files.Count(); + } + } + + public IEnumerable SearchFiles(string path, string pattern, bool deep = true) + { + // Get relative from absolute path var index = _storagePath.EmptyNull().Length; - return Directory.EnumerateFiles(MapStorage(path), pattern, SearchOption.AllDirectories) + return Directory.EnumerateFiles(MapStorage(path), pattern, deep ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) .Select(x => x.Substring(index)); } @@ -207,10 +235,9 @@ public IEnumerable ListFiles(string path) } return directoryInfo - .GetFiles() + .EnumerateFiles() .Where(fi => !IsHidden(fi)) - .Select(fi => new LocalFile(Path.Combine(Fix(path), fi.Name), fi)) - .ToList(); + .Select(fi => new LocalFile(Path.Combine(Fix(path), fi.Name), fi)); } public IEnumerable ListFolders(string path) @@ -234,10 +261,9 @@ public IEnumerable ListFolders(string path) } return directoryInfo - .GetDirectories() + .EnumerateDirectories() .Where(di => !IsHidden(di)) - .Select(di => new LocalFolder(Path.Combine(Fix(path), di.Name), di)) - .ToList(); + .Select(di => new LocalFolder(Path.Combine(Fix(path), di.Name), di)); } private static bool IsHidden(FileSystemInfo di) @@ -456,6 +482,7 @@ private class LocalFile : IFile { private readonly string _path; private readonly FileInfo _fileInfo; + private Size? _dimensions; public LocalFile(string path, FileInfo fileInfo) { @@ -468,11 +495,21 @@ public string Path get { return _path; } } + public string Directory + { + get { return _path.Substring(0, _path.Length - Name.Length); } + } + public string Name { get { return _fileInfo.Name; } } + public string Title + { + get { return System.IO.Path.GetFileNameWithoutExtension(_fileInfo.Name); } + } + public long Size { get { return _fileInfo.Length; } @@ -483,11 +520,32 @@ public DateTime LastUpdated get { return _fileInfo.LastWriteTime; } } - public string FileType + public string Extension { get { return _fileInfo.Extension; } } + public Size Dimensions + { + get + { + if (_dimensions == null) + { + try + { + var mime = MimeTypes.MapNameToMimeType(_fileInfo.Name); + _dimensions = ImageHeader.GetDimensions(OpenRead(), mime, false); + } + catch + { + _dimensions = new Size(); + } + } + + return _dimensions.Value; + } + } + public bool Exists { get { return _fileInfo.Exists; } diff --git a/src/Libraries/SmartStore.Core/Linq/Expanders/LambdaPathExpander.cs b/src/Libraries/SmartStore.Core/Linq/Expanders/LambdaPathExpander.cs index 337d1032ae..ce8c87c8b1 100644 --- a/src/Libraries/SmartStore.Core/Linq/Expanders/LambdaPathExpander.cs +++ b/src/Libraries/SmartStore.Core/Linq/Expanders/LambdaPathExpander.cs @@ -2,14 +2,11 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Linq.Dynamic; -using System.ComponentModel; using System.Reflection; using SmartStore.Utilities; namespace SmartStore.Linq { - public class LambdaPathExpander : IPathExpander { private readonly IList _expands; diff --git a/src/Libraries/SmartStore.Core/Linq/PredicateBuilder.cs b/src/Libraries/SmartStore.Core/Linq/PredicateBuilder.cs index 7a6bdf9b88..06101c7c6d 100644 --- a/src/Libraries/SmartStore.Core/Linq/PredicateBuilder.cs +++ b/src/Libraries/SmartStore.Core/Linq/PredicateBuilder.cs @@ -4,7 +4,6 @@ namespace SmartStore.Linq { - public static class PredicateBuilder { public static Expression> True() @@ -32,7 +31,5 @@ public static Expression> And(this Expression> ex return Expression.Lambda> (Expression.And(expr1.Body, invokedExpr), expr1.Parameters); } - } - } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Localization/ILocalizationFileResolver.cs b/src/Libraries/SmartStore.Core/Localization/ILocalizationFileResolver.cs new file mode 100644 index 0000000000..e734c92653 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Localization/ILocalizationFileResolver.cs @@ -0,0 +1,44 @@ +using System; + +namespace SmartStore.Core.Localization +{ + /// + /// Responsible for finding a localization file for client scripts + /// + public interface ILocalizationFileResolver + { + /// + /// Tries to find a matching localization file for a given culture in the following order + /// (assuming is 'de-DE', is 'lang-*.js' and is 'en-US'): + /// + /// Exact match > lang-de-DE.js + /// Neutral culture > lang-de.js + /// Any region for language > lang-de-CH.js + /// Exact match for fallback culture > lang-en-US.js + /// Neutral fallback culture > lang-en.js + /// Any region for fallback language > lang-en-GB.js + /// + /// + /// The ISO culture code to get a localization file for, e.g. 'de-DE' + /// The virtual path to search in + /// The pattern to match, e.g. 'lang-*.js'. The wildcard char MUST exist. + /// + /// Whether caching should be enabled. If false, no attempt is made to read from cache, nor writing the result to the cache. + /// Cache duration is 24 hours. Automatic eviction on file change is NOT performed. + /// + /// Optional. + /// Result + LocalizationFileResolveResult Resolve( + string culture, + string virtualPath, + string pattern, + bool cache = true, + string fallbackCulture = "en"); + } + + public class LocalizationFileResolveResult + { + public string Culture { get; set; } + public string VirtualPath { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Core/Localization/LocalizationFileResolver.cs b/src/Libraries/SmartStore.Core/Localization/LocalizationFileResolver.cs new file mode 100644 index 0000000000..fc02435c1e --- /dev/null +++ b/src/Libraries/SmartStore.Core/Localization/LocalizationFileResolver.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Web; +using SmartStore.Core.Caching; +using SmartStore.Utilities; + +namespace SmartStore.Core.Localization +{ + public class LocalizationFileResolver : ILocalizationFileResolver + { + private readonly ICacheManager _cache; + + public LocalizationFileResolver(ICacheManager cache) + { + _cache = cache; + } + + public LocalizationFileResolveResult Resolve( + string culture, + string virtualPath, + string pattern, + bool cache = true, + string fallbackCulture = "en") + { + Guard.NotEmpty(culture, nameof(culture)); + Guard.NotEmpty(virtualPath, nameof(virtualPath)); + Guard.NotEmpty(pattern, nameof(pattern)); + + if (pattern.IndexOf('*') < 0) + { + throw new ArgumentException("The pattern must contain a wildcard char for substitution, e.g. 'lang-*.js'.", nameof(pattern)); + } + + virtualPath = FixPath(virtualPath); + var cacheKey = "core:locfile:" + virtualPath.ToLower() + pattern + "/" + culture; + string result = null; + + if (cache && _cache.Contains(cacheKey)) + { + result = _cache.Get(cacheKey); + return result != null ? CreateResult(result, virtualPath, pattern) : null; + } + + if (!LocalizationHelper.IsValidCultureCode(culture)) + { + throw new ArgumentException($"'{culture}' is not a valid culture code.", nameof(culture)); + } + + var ci = CultureInfo.GetCultureInfo(culture); + var directory = new DirectoryInfo(CommonHelper.MapPath(virtualPath, false)); + + if (!directory.Exists) + { + throw new DirectoryNotFoundException($"Path '{virtualPath}' does not exist."); + } + + // 1: Match passed culture + result = ResolveMatchingFile(ci, directory, pattern); + + if (result == null && fallbackCulture.HasValue() && culture != fallbackCulture) + { + if (!LocalizationHelper.IsValidCultureCode(fallbackCulture)) + { + throw new ArgumentException($"'{culture}' is not a valid culture code.", nameof(fallbackCulture)); + } + + // 2: Match fallback culture + ci = CultureInfo.GetCultureInfo(fallbackCulture); + result = ResolveMatchingFile(ci, directory, pattern); + } + + if (cache) + { + _cache.Put(cacheKey, result, TimeSpan.FromHours(24)); + } + + if (result.HasValue()) + { + return CreateResult(result, virtualPath, pattern); + } + + return null; + } + + private string ResolveMatchingFile(CultureInfo ci, DirectoryInfo directory, string pattern) + { + string result = null; + + // 1: Exact match + // ----------------------------------------------------- + var fileName = pattern.Replace("*", ci.Name); + if (File.Exists(Path.Combine(directory.FullName, fileName))) + { + result = ci.Name; + } + + // 2: Match neutral culture, e.g. de-DE > de + // ----------------------------------------------------- + if (result == null && !ci.IsNeutralCulture && ci.Parent != null) + { + ci = ci.Parent; + fileName = pattern.Replace("*", ci.Name); + if (File.Exists(Path.Combine(directory.FullName, fileName))) + { + result = ci.Name; + } + } + + // 2: Match any region, e.g. de-DE > de-CH + // ----------------------------------------------------- + if (result == null && ci.IsNeutralCulture) + { + // Convert pattern to Regex: "lang-*.js" > "^lang.(.+?).js$" + var rgPattern = "^" + pattern.Replace("*", @"(.+?)") + "$"; + var rgFileName = new Regex(rgPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); + + foreach (var fi in directory.EnumerateFiles(pattern.Replace("*", ci.Name + "-*"), SearchOption.TopDirectoryOnly)) + { + var culture = rgFileName.Match(fi.Name).Groups[1].Value; + if (LocalizationHelper.IsValidCultureCode(culture)) + { + result = culture; + break; + } + } + } + + return result; + } + + private string FixPath(string virtualPath) + { + return VirtualPathUtility.ToAppRelative(virtualPath).EnsureEndsWith("/"); + } + + private LocalizationFileResolveResult CreateResult(string culture, string virtualPath, string pattern) + { + var fileName = pattern.Replace("*", culture); + return new LocalizationFileResolveResult + { + Culture = culture, + VirtualPath = VirtualPathUtility.ToAbsolute(virtualPath + fileName) + }; + } + } +} diff --git a/src/Libraries/SmartStore.Core/Localization/LocalizedString.cs b/src/Libraries/SmartStore.Core/Localization/LocalizedString.cs index 6450a522cd..a09c159735 100644 --- a/src/Libraries/SmartStore.Core/Localization/LocalizedString.cs +++ b/src/Libraries/SmartStore.Core/Localization/LocalizedString.cs @@ -44,7 +44,18 @@ public string Text get { return _localized; } } - public static implicit operator string(LocalizedString obj) + /// + /// Returns a js encoded string which already contains delimiters. + /// + public IHtmlString JsText + { + get + { + return System.Web.Mvc.MvcHtmlString.Create(_localized.EncodeJsString()); + } + } + + public static implicit operator string(LocalizedString obj) { return obj.Text; } diff --git a/src/Libraries/SmartStore.Core/Localization/Localizer.cs b/src/Libraries/SmartStore.Core/Localization/Localizer.cs index 82e62d9d24..bb96ea085b 100644 --- a/src/Libraries/SmartStore.Core/Localization/Localizer.cs +++ b/src/Libraries/SmartStore.Core/Localization/Localizer.cs @@ -1,4 +1,5 @@ namespace SmartStore.Core.Localization { public delegate LocalizedString Localizer(string key, params object[] args); + public delegate LocalizedString LocalizerEx(string key, int languageId, params object[] args); } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Localization/NullLocalizer.cs b/src/Libraries/SmartStore.Core/Localization/NullLocalizer.cs index f325439740..0b43a01c1d 100644 --- a/src/Libraries/SmartStore.Core/Localization/NullLocalizer.cs +++ b/src/Libraries/SmartStore.Core/Localization/NullLocalizer.cs @@ -1,22 +1,26 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; namespace SmartStore.Core.Localization { public static class NullLocalizer { - private static readonly Localizer s_instance; - + private static readonly Localizer _instance; + private static readonly LocalizerEx _instanceEx; + static NullLocalizer() { - s_instance = (format, args) => new LocalizedString((args == null || args.Length == 0) ? format : string.Format(format, args)); + _instance = (format, args) => new LocalizedString((args == null || args.Length == 0) ? format : string.Format(format, args)); + _instanceEx = (format, languageId, args) => new LocalizedString((args == null || args.Length == 0) ? format : string.Format(format, args)); } public static Localizer Instance { - get { return s_instance; } + get { return _instance; } + } + + public static LocalizerEx InstanceEx + { + get { return _instanceEx; } } } } diff --git a/src/Libraries/SmartStore.Core/Logging/Notifier.cs b/src/Libraries/SmartStore.Core/Logging/Notifier.cs index be0e903d61..ed2a00ba94 100644 --- a/src/Libraries/SmartStore.Core/Logging/Notifier.cs +++ b/src/Libraries/SmartStore.Core/Logging/Notifier.cs @@ -6,7 +6,6 @@ namespace SmartStore.Core.Logging { - public interface INotifier { void Add(NotifyType type, LocalizedString message, bool durable = true); diff --git a/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs b/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs index bdf0c42bbd..10da774027 100644 --- a/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs +++ b/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Linq; using System.Text; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Logging; @@ -75,7 +76,10 @@ public void Log(LogLevel level, Exception exception, string message, object[] ar if (message.HasValue()) { - var msg = (args == null ? message : message.FormatInvariant(args)); + var msg = args != null && args.Any() + ? message.FormatInvariant(args) + : message; + _traceSource.TraceEvent(type, (int)type, "{0}: {1}".FormatCurrent(type.ToString().ToUpper(), msg)); } } diff --git a/src/Libraries/SmartStore.Core/PagedList.cs b/src/Libraries/SmartStore.Core/PagedList.cs index 565ff19c3b..87af23cda8 100644 --- a/src/Libraries/SmartStore.Core/PagedList.cs +++ b/src/Libraries/SmartStore.Core/PagedList.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Linq; namespace SmartStore.Core @@ -148,6 +149,16 @@ public PagedList(int pageIndex, int pageSize, int totalItemsCount) : base(pageIndex, pageSize, totalItemsCount) { } - } + + public static PagedList Create(IEnumerable source, int pageIndex, int pageSize) + { + return new PagedList(source, pageIndex, pageSize); + } + + public static PagedList Create(IEnumerable source, int pageIndex, int pageSize, int totalCount) + { + return new PagedList(source, pageIndex, pageSize, totalCount); + } + } } diff --git a/src/Libraries/SmartStore.Core/PagedList`T.cs b/src/Libraries/SmartStore.Core/PagedList`T.cs index 2001a777f1..1614557a1e 100644 --- a/src/Libraries/SmartStore.Core/PagedList`T.cs +++ b/src/Libraries/SmartStore.Core/PagedList`T.cs @@ -19,6 +19,7 @@ public class PagedList : IPagedList, IReadOnlyList, IReadOnlyCollection private List _list; + /// The 0-based page index public PagedList(IEnumerable source, int pageIndex, int pageSize) { Guard.NotNull(source, "source"); @@ -26,6 +27,7 @@ public PagedList(IEnumerable source, int pageIndex, int pageSize) Init(source.AsQueryable(), pageIndex, pageSize, null); } + /// The 0-based page index public PagedList(IEnumerable source, int pageIndex, int pageSize, int totalCount) { Guard.NotNull(source, "source"); diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs b/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs index 240a788883..35b2b3e8c9 100644 --- a/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs +++ b/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs @@ -42,11 +42,16 @@ public PluginDescriptor(Assembly referencedAssembly, FileInfo originalAssemblyFi /// public string PhysicalPath { get; set; } - /// - /// Gets the file name of the brand image (without path) - /// or an empty string if no image is specified - /// - public string BrandImageFileName + /// + /// The virtual path of the runtime plugin + /// + public string VirtualPath { get; set; } + + /// + /// Gets the file name of the brand image (without path) + /// or an empty string if no image is specified + /// + public string BrandImageFileName { get { diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs b/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs index 072b362276..6a95a30aae 100644 --- a/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs +++ b/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs @@ -15,6 +15,7 @@ using SmartStore.Core.Packaging; using SmartStore.Utilities; using SmartStore.Utilities.Threading; +using SmartStore.Core.Data; // Contributor: Umbraco (http://www.umbraco.com). Thanks a lot! // SEE THIS POST for full details of what this does @@ -185,7 +186,7 @@ public static void Initialize() } } - if (dirty) + if (dirty && DataSettings.DatabaseIsInstalled()) { // Save current hash of all deployed plugins to disk var hash = ComputePluginsHash(_referencedPlugins.Values.OrderBy(x => x.FolderName).ToArray()); @@ -259,6 +260,8 @@ private static PluginDescriptor LoadPluginDescriptor(DirectoryInfo d, ICollectio throw new SmartException("The plugin descriptor '{0}' does not define a plugin assembly file name. Try assigning the plugin a file name and recompile.".FormatInvariant(descriptionFile.FullName)); } + descriptor.VirtualPath = _pluginsPath + "/" + descriptor.FolderName; + // Set 'Installed' property descriptor.Installed = installedPluginSystemNames.Contains(descriptor.SystemName); diff --git a/src/Libraries/SmartStore.Core/RouteInfo.cs b/src/Libraries/SmartStore.Core/RouteInfo.cs index d58573ebd5..84827ddd44 100644 --- a/src/Libraries/SmartStore.Core/RouteInfo.cs +++ b/src/Libraries/SmartStore.Core/RouteInfo.cs @@ -2,9 +2,11 @@ using System.Collections.Generic; using System.Linq; using System.Web.Routing; +using Newtonsoft.Json; namespace SmartStore { + [JsonConverter(typeof(RouteInfoConverter))] public class RouteInfo { public RouteInfo(RouteInfo cloneFrom) @@ -39,6 +41,7 @@ public RouteInfo(string action, RouteValueDictionary routeValues) { } + [JsonConstructor] public RouteInfo(string action, string controller, RouteValueDictionary routeValues) { Guard.NotEmpty(action, nameof(action)); @@ -66,6 +69,65 @@ public RouteValueDictionary RouteValues get; private set; } + } + + #region JsonConverter + + public class RouteInfoConverter : JsonConverter + { + public override bool CanWrite + { + get { return false; } + } + public override bool CanConvert(Type objectType) + { + return objectType == typeof(RouteInfo); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + string action = null; + string controller = null; + RouteValueDictionary routeValues = null; + + reader.Read(); + while (reader.TokenType == JsonToken.PropertyName) + { + string a = reader.Value.ToString(); + if (string.Equals(a, "Action", StringComparison.OrdinalIgnoreCase)) + { + reader.Read(); + action = serializer.Deserialize(reader); + } + else if (string.Equals(a, "Controller", StringComparison.OrdinalIgnoreCase)) + { + reader.Read(); + controller = serializer.Deserialize(reader); + } + else if (string.Equals(a, "RouteValues", StringComparison.OrdinalIgnoreCase)) + { + reader.Read(); + routeValues = serializer.Deserialize(reader); + } + else + { + reader.Skip(); + } + + reader.Read(); + } + + var routeInfo = Activator.CreateInstance(objectType, new object[] { action, controller, routeValues }); + + return routeInfo; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotSupportedException(); + } } + + #endregion } diff --git a/src/Libraries/SmartStore.Core/Search/SearchQuery.cs b/src/Libraries/SmartStore.Core/Search/SearchQuery.cs index cdfabda9bf..6cd4f78b9e 100644 --- a/src/Libraries/SmartStore.Core/Search/SearchQuery.cs +++ b/src/Libraries/SmartStore.Core/Search/SearchQuery.cs @@ -25,12 +25,12 @@ public SearchQuery() { } - public SearchQuery(string field, string term, SearchMode mode = SearchMode.StartsWith, bool escape = false, bool isFuzzySearch = false) + public SearchQuery(string field, string term, SearchMode mode = SearchMode.Contains, bool escape = false, bool isFuzzySearch = false) : base(field.HasValue() ? new[] { field } : null, term, mode, escape, isFuzzySearch) { } - public SearchQuery(string[] fields, string term, SearchMode mode = SearchMode.StartsWith, bool escape = false, bool isFuzzySearch = false) + public SearchQuery(string[] fields, string term, SearchMode mode = SearchMode.Contains, bool escape = false, bool isFuzzySearch = false) : base(fields, term, mode, escape, isFuzzySearch) { } @@ -41,7 +41,7 @@ public class SearchQuery : ISearchQuery where TQuery : class, ISearchQue private readonly Dictionary _facetDescriptors; private Dictionary _customData; - protected SearchQuery(string[] fields, string term, SearchMode mode = SearchMode.StartsWith, bool escape = false, bool isFuzzySearch = false) + protected SearchQuery(string[] fields, string term, SearchMode mode = SearchMode.Contains, bool escape = false, bool isFuzzySearch = false) { Fields = fields; Term = term; diff --git a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj index cca810df42..c9344cd7ff 100644 --- a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj +++ b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj @@ -10,7 +10,7 @@ Properties SmartStore.Core SmartStore.Core - v4.5.2 + v4.6.1 512 @@ -62,6 +62,9 @@ MinimumRecommendedRules.ruleset + + ..\..\packages\AngleSharp.0.9.9\lib\net45\AngleSharp.dll + ..\..\packages\Autofac.4.5.0\lib\net45\Autofac.dll True @@ -71,15 +74,11 @@ True - False - ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll + ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll + True - False - ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll - - - ..\..\packages\HtmlAgilityPack.1.4.9.5\lib\Net45\HtmlAgilityPack.dll + ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll True @@ -120,6 +119,9 @@ + + ..\..\packages\System.ValueTuple.4.4.0\lib\net461\System.ValueTuple.dll + False @@ -179,7 +181,10 @@ + + + @@ -203,6 +208,7 @@ + @@ -217,6 +223,7 @@ + @@ -224,8 +231,10 @@ + + @@ -246,6 +255,7 @@ + @@ -258,6 +268,8 @@ + + @@ -396,9 +408,6 @@ - - - @@ -454,12 +463,18 @@ + + + + + + - + @@ -505,7 +520,6 @@ - @@ -664,6 +678,7 @@ + diff --git a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs index cb55e7d3ed..47c338ac27 100644 --- a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs +++ b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs @@ -24,7 +24,7 @@ public static class SmartStoreVersion new Version("3.0") }; - private const string HELP_BASEURL = "http://docs.smartstore.com/display/"; + private const string HELP_BASEURL = "https://docs.smartstore.com/display/"; static SmartStoreVersion() { diff --git a/src/Libraries/SmartStore.Core/Templating/DefaultTemplateManager.cs b/src/Libraries/SmartStore.Core/Templating/DefaultTemplateManager.cs new file mode 100644 index 0000000000..8395d04364 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Templating/DefaultTemplateManager.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace SmartStore.Templating +{ + public partial class DefaultTemplateManager : ITemplateManager + { + private readonly ConcurrentDictionary _templates; + private readonly ITemplateEngine _engine; + + public DefaultTemplateManager(ITemplateEngine engine) + { + _templates = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + _engine = engine; + } + + public IReadOnlyDictionary All() + { + return _templates; + } + + public bool Contains(string name) + { + Guard.NotEmpty(name, nameof(name)); + + return _templates.ContainsKey(name); + } + + public ITemplate Get(string name) + { + Guard.NotEmpty(name, nameof(name)); + + _templates.TryGetValue(name, out var template); + return template; + } + + public void Put(string name, ITemplate template) + { + Guard.NotEmpty(name, nameof(name)); + Guard.NotNull(template, nameof(template)); + + _templates[name] = template; + } + + public ITemplate GetOrAdd(string name, Func sourceFactory) + { + Guard.NotEmpty(name, nameof(name)); + Guard.NotNull(sourceFactory, nameof(sourceFactory)); + + return _templates.GetOrAdd(name, key => + { + return _engine.Compile(sourceFactory()); + }); + } + + public bool TryRemove(string name, out ITemplate template) + { + Guard.NotEmpty(name, nameof(name)); + + return _templates.TryRemove(name, out template); + } + + public void Clear() + { + _templates.Clear(); + } + } +} diff --git a/src/Libraries/SmartStore.Core/Templating/ITemplate.cs b/src/Libraries/SmartStore.Core/Templating/ITemplate.cs new file mode 100644 index 0000000000..3014ba0e20 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Templating/ITemplate.cs @@ -0,0 +1,27 @@ +using System; + +namespace SmartStore.Templating +{ + /// + /// A compiled template intended to be stored in a singleton storage. + /// + public interface ITemplate + { + /// + /// Gets the original template source + /// + string Source { get; } + + /// + /// Renders the template in + /// + /// + /// The model object which contains the data for the template. + /// Can be a subclass of , + /// a plain class object, or an anonymous type. + /// + /// Provider to use for formatting numbers, dates, money etc. + /// The processed template result + string Render(object model, IFormatProvider formatProvider); + } +} diff --git a/src/Libraries/SmartStore.Core/Templating/ITemplateEngine.cs b/src/Libraries/SmartStore.Core/Templating/ITemplateEngine.cs new file mode 100644 index 0000000000..56e5868375 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Templating/ITemplateEngine.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using SmartStore.Core; + +namespace SmartStore.Templating +{ + /// + /// Represents a model object for testing purposes. A test model + /// becomes necessary when the database does not contain any data + /// for the previews/tested model. + /// + public interface ITestModel + { + /// + /// The name of the model to use as hash key, e.g. Product, Order, Customer etc. + /// + string ModelName { get; } + } + + /// + /// Responsible for compiling and rendering the templates + /// + public interface ITemplateEngine + { + /// + /// Compiles a template for faster rendering + /// + /// The template source + /// Compiled template + ITemplate Compile(string source); + + /// + /// Directly renders a template source + /// + /// The template source + /// + /// The model object which contains the data for the template. + /// Can be a subclass of , + /// a plain class object, or an anonymous type. + /// + /// Provider to use for formatting numbers, dates, money etc. + /// The processed template result + string Render(string source, object model, IFormatProvider formatProvider); + + /// + /// Creates a test model for the passed entity to be used during preview and test. + /// + /// The entity to create a test model for. + /// The model prefix + /// An object which implements and contains some test data for the declared properties of + ITestModel CreateTestModelFor(BaseEntity entity, string modelPrefix); + } +} diff --git a/src/Libraries/SmartStore.Core/Templating/ITemplateManager.cs b/src/Libraries/SmartStore.Core/Templating/ITemplateManager.cs new file mode 100644 index 0000000000..affe05aeb1 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Templating/ITemplateManager.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; + +namespace SmartStore.Templating +{ + /// + /// Responsible for managing all compiled templates + /// + public interface ITemplateManager + { + /// + /// Gets all compiled templates + /// + /// + IReadOnlyDictionary All(); + + /// + /// Checks whether a compiled template exists in the storage + /// + /// Template name + /// true when the template exists + bool Contains(string name); + + /// + /// Gets a compiled template matching the passed name + /// + /// Template name to get. + /// An instance of or null + ITemplate Get(string name); + + /// + /// Saves a compiled template in the storage. Any existing template gets overridden. + /// + /// The name to use as template key + /// The compiled template + void Put(string name, ITemplate template); + + /// + /// Either gets a template with the passed name, or compiles and stores + /// a template if it does not exist yet. + /// + /// Template name + /// The factory used to generate/obtain the template source. + /// The compiled template + ITemplate GetOrAdd(string name, Func sourceFactory); + + /// + /// Attempts to remove and return the compiled template that has the specified name. + /// + /// The name of the template to remove and return. + /// The removed template or null + /// true if the template was removed, false otherwise. + bool TryRemove(string name, out ITemplate template); + + /// + /// Removes all compiled templates from the storage. + /// + void Clear(); + } +} diff --git a/src/Libraries/SmartStore.Core/Templating/NullTemplateEngine.cs b/src/Libraries/SmartStore.Core/Templating/NullTemplateEngine.cs new file mode 100644 index 0000000000..04f166dbf9 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Templating/NullTemplateEngine.cs @@ -0,0 +1,49 @@ +using System; +using SmartStore.Core; + +namespace SmartStore.Templating +{ + public class NullTemplateEngine : ITemplateEngine + { + private readonly static ITemplateEngine _instance = new NullTemplateEngine(); + + public static ITemplateEngine Instance => _instance; + + public ITemplate Compile(string template) + { + return new NullTemplate(template); + } + + public string Render(string source, object data, IFormatProvider formatProvider) + { + return source; + } + + public ITestModel CreateTestModelFor(BaseEntity entity, string modelPrefix) + { + return new NullTestModel(); + } + + internal class NullTestModel : ITestModel + { + public string ModelName => "TestModel"; + } + + internal class NullTemplate : ITemplate + { + private readonly string _source; + + public NullTemplate(string source) + { + _source = source; + } + + public string Source => _source; + + public string Render(object data, IFormatProvider formatProvider) + { + return _source; + } + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/IThemeContext.cs b/src/Libraries/SmartStore.Core/Themes/IThemeContext.cs similarity index 94% rename from src/Presentation/SmartStore.Web.Framework/Theming/IThemeContext.cs rename to src/Libraries/SmartStore.Core/Themes/IThemeContext.cs index a019d3761b..1162ab331d 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/IThemeContext.cs +++ b/src/Libraries/SmartStore.Core/Themes/IThemeContext.cs @@ -1,6 +1,5 @@ -using SmartStore.Core.Themes; - -namespace SmartStore.Web.Framework.Theming + +namespace SmartStore.Core.Themes { /// /// Work context diff --git a/src/Libraries/SmartStore.Core/Themes/InheritedThemeFileResult.cs b/src/Libraries/SmartStore.Core/Themes/InheritedThemeFileResult.cs index 87052c3e25..09c40503e8 100644 --- a/src/Libraries/SmartStore.Core/Themes/InheritedThemeFileResult.cs +++ b/src/Libraries/SmartStore.Core/Themes/InheritedThemeFileResult.cs @@ -35,5 +35,10 @@ public class InheritedThemeFileResult public string ResultThemeName { get; set; } public bool IsExplicit { get; set; } + + /// + /// The query string, e.g. '?explicit' + /// + public string Query { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Themes/ThemeManifest.cs b/src/Libraries/SmartStore.Core/Themes/ThemeManifest.cs index 2528df7b9e..739c8a40b3 100644 --- a/src/Libraries/SmartStore.Core/Themes/ThemeManifest.cs +++ b/src/Libraries/SmartStore.Core/Themes/ThemeManifest.cs @@ -9,10 +9,8 @@ namespace SmartStore.Core.Themes { - public class ThemeManifest : ComparableObject { - internal ThemeManifest() { } @@ -321,5 +319,4 @@ public enum ThemeManifestState MissingBaseTheme = -1, Active = 0, } - } diff --git a/src/Libraries/SmartStore.Core/Themes/ThemeManifestMaterializer.cs b/src/Libraries/SmartStore.Core/Themes/ThemeManifestMaterializer.cs index 2cf7283992..6fadd1910e 100644 --- a/src/Libraries/SmartStore.Core/Themes/ThemeManifestMaterializer.cs +++ b/src/Libraries/SmartStore.Core/Themes/ThemeManifestMaterializer.cs @@ -7,8 +7,7 @@ using SmartStore.Collections; namespace SmartStore.Core.Themes -{ - +{ internal class ThemeManifestMaterializer { private readonly ThemeManifest _manifest; @@ -171,7 +170,5 @@ private ThemeVariableType ConvertVarType(string type, XmlElement affected, out s return result; } - } - } diff --git a/src/Libraries/SmartStore.Core/Themes/ThemeVariableInfo.cs b/src/Libraries/SmartStore.Core/Themes/ThemeVariableInfo.cs index e6db1d7aec..0fbb373a9d 100644 --- a/src/Libraries/SmartStore.Core/Themes/ThemeVariableInfo.cs +++ b/src/Libraries/SmartStore.Core/Themes/ThemeVariableInfo.cs @@ -3,14 +3,12 @@ using System.Linq; namespace SmartStore.Core.Themes -{ - +{ /// /// Represents deserialized metadata for a theme variable /// public class ThemeVariableInfo : DisposableObject { - /// /// Gets the variable name as specified in the config file /// @@ -59,5 +57,4 @@ protected override void OnDispose(bool disposing) } } } - } diff --git a/src/Libraries/SmartStore.Core/Utilities/ColorHelper.cs b/src/Libraries/SmartStore.Core/Utilities/ColorHelper.cs deleted file mode 100644 index 50dfc83261..0000000000 --- a/src/Libraries/SmartStore.Core/Utilities/ColorHelper.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Drawing; - -namespace SmartStore.Utilities -{ - public static class ColorHelper - { - public static int GetPerceivedBrightness(string htmlColor) - { - if (String.IsNullOrEmpty(htmlColor)) - htmlColor = "#ffffff"; - - return GetPerceivedBrightness(ColorTranslator.FromHtml(htmlColor)); - } - - /// - /// Calculates the perceived brightness of a color. - /// - /// The color - /// - /// A number in the range of 0 (black) to 255 (White). - /// For text contrast colors, an optimal cutoff value is 130. - /// - public static int GetPerceivedBrightness(Color color) - { - return (int)Math.Sqrt( - color.R * color.R * .241 + - color.G * color.G * .691 + - color.B * color.B * .068); - } - } -} diff --git a/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs b/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs index 9db88283f4..a6d0649e24 100644 --- a/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs @@ -1,7 +1,9 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Configuration; using System.Dynamic; +using System.Linq; using System.Globalization; using System.IO; using System.Security.Cryptography; @@ -180,7 +182,7 @@ public static IDictionary ObjectToDictionary(object obj) return FastProperty.ObjectToDictionary( obj, - key => key.Replace("_", "-")); + key => key.Replace("_", "-").Replace("@", "")); } /// @@ -230,5 +232,38 @@ private static bool TryAction(Func func, out T output) return false; } } + + public static bool IsTruthy(object value) + { + if (value == null) + return false; + + switch (value) + { + case string x: + return x.HasValue(); + case bool x: + return x == true; + case DateTime x: + return x > DateTime.MinValue; + case TimeSpan x: + return x > TimeSpan.MinValue; + case Guid x: + return x != Guid.Empty; + case IComparable x: + return x.CompareTo(0) != 0; + case IEnumerable x: + return x.Any(); + case IEnumerable x: + return x.GetEnumerator().MoveNext(); + } + + if (value.GetType().IsNullable(out var wrappedType)) + { + return IsTruthy(Convert.ChangeType(value, wrappedType)); + } + + return true; + } } } diff --git a/src/Libraries/SmartStore.Core/Utilities/DictionaryConverter.cs b/src/Libraries/SmartStore.Core/Utilities/DictionaryConverter.cs index fc2686b62f..a9da555717 100644 --- a/src/Libraries/SmartStore.Core/Utilities/DictionaryConverter.cs +++ b/src/Libraries/SmartStore.Core/Utilities/DictionaryConverter.cs @@ -9,7 +9,6 @@ namespace SmartStore.Utilities { - [Serializable] public class ConvertProblem { @@ -76,7 +75,6 @@ private static string CreateMessage(ICollection problems) public static class DictionaryConverter { - public static bool CanCreateType(Type itemType) { return itemType.IsClass && itemType.GetConstructor(Type.EmptyTypes) != null; @@ -248,9 +246,9 @@ private static void WriteToProperty(object item, FastProperty prop, object value return; } - if (pi.PropertyType.IsNullable()) + if (pi.PropertyType.IsNullable(out var wrappedType)) { - destType = pi.PropertyType.GetGenericArguments()[0]; + destType = wrappedType; } prop.SetValue(item, value.Convert(destType)); diff --git a/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs b/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs index e086c63c76..c4c458a569 100644 --- a/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs +++ b/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using System.Web; using SmartStore.Core; +using SmartStore.Core.IO; using SmartStore.Core.Logging; namespace SmartStore.Utilities @@ -22,7 +23,7 @@ public class FileDownloadManager public FileDownloadManager(HttpRequestBase httpRequest) { - this._httpRequest = httpRequest; + _httpRequest = httpRequest; } /// @@ -149,29 +150,45 @@ private async Task ProcessUrl(FileDownloadManagerContext context, HttpClient cli { try { - //HttpResponseMessage response = await client.GetAsync(item.Url, HttpCompletionOption.ResponseHeadersRead); - //Task task = response.Content.ReadAsStreamAsync(); + var count = 0; + var canceled = false; + var bytes = new byte[_bufferSize]; - Task task = client.GetStreamAsync(item.Url); - await task; + using (var response = await client.GetAsync(item.Url)) + { + if (response.IsSuccessStatusCode && response.Content.Headers.ContentType != null) + { + var contentType = response.Content.Headers.ContentType.MediaType; + if (contentType.HasValue() && !contentType.IsCaseInsensitiveEqual(item.MimeType)) + { + // Update mime type and local path. + var extension = MimeTypes.MapMimeTypeToExtension(contentType).NullEmpty() ?? ".jpg"; - int count; - bool canceled = false; - byte[] bytes = new byte[_bufferSize]; + item.MimeType = contentType; + item.Path = Path.ChangeExtension(item.Path, extension.EnsureStartsWith(".")); + } + } - using (var srcStream = task.Result) - using (var dstStream = File.OpenWrite(item.Path)) - { - while ((count = srcStream.Read(bytes, 0, bytes.Length)) != 0 && !canceled) + //Task task = client.GetStreamAsync(item.Url); + Task task = response.Content.ReadAsStreamAsync(); + await task; + + using (var srcStream = task.Result) + using (var dstStream = File.Open(item.Path, FileMode.Create)) { - dstStream.Write(bytes, 0, count); + while ((count = srcStream.Read(bytes, 0, bytes.Length)) != 0 && !canceled) + { + dstStream.Write(bytes, 0, count); - if (context.CancellationToken != null && context.CancellationToken.IsCancellationRequested) - canceled = true; + if (context.CancellationToken != null && context.CancellationToken.IsCancellationRequested) + { + canceled = true; + } + } } - } - item.Success = (!task.IsFaulted && !canceled); + item.Success = (!task.IsFaulted && !canceled); + } } catch (Exception exception) { @@ -199,9 +216,9 @@ public FileDownloadResponse(byte[] data, string fileName, string contentType) { Guard.NotNull(data, nameof(data)); - this.Data = data; - this.FileName = fileName; - this.ContentType = contentType; + Data = data; + FileName = fileName; + ContentType = contentType; } /// @@ -308,7 +325,7 @@ public bool HasTimedOut public override string ToString() { - string str = "Result: {0} {1}{2}, {3}".FormatInvariant( + var str = "Result: {0} {1}{2}, {3}".FormatInvariant( Success, ExceptionStatus.ToString(), ErrorMessage.HasValue() ? " ({0})".FormatInvariant(ErrorMessage) : "", diff --git a/src/Libraries/SmartStore.Core/Utilities/ImagingHelper.cs b/src/Libraries/SmartStore.Core/Utilities/ImagingHelper.cs new file mode 100644 index 0000000000..3d6ef4d3c5 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Utilities/ImagingHelper.cs @@ -0,0 +1,67 @@ +using System; +using System.Drawing; + +namespace SmartStore.Utilities +{ + public static class ImagingHelper + { + public static int GetPerceivedBrightness(string htmlColor) + { + if (String.IsNullOrEmpty(htmlColor)) + htmlColor = "#ffffff"; + + return GetPerceivedBrightness(ColorTranslator.FromHtml(htmlColor)); + } + + /// + /// Calculates the perceived brightness of a color. + /// + /// The color + /// + /// A number in the range of 0 (black) to 255 (White). + /// For text contrast colors, an optimal cutoff value is 130. + /// + public static int GetPerceivedBrightness(Color color) + { + return (int)Math.Sqrt( + color.R * color.R * .241 + + color.G * color.G * .691 + + color.B * color.B * .068); + } + + /// + /// Recalculates an image size while keeping aspect ratio + /// + /// Original size + /// New max size + /// The rescaled size + public static Size Rescale(Size original, int maxSize) + { + Guard.IsPositive(maxSize, nameof(maxSize)); + + return Rescale(original, new Size(maxSize, maxSize)); + } + + /// + /// Recalculates an image size while keeping aspect ratio + /// + /// Original size + /// New max size + /// The rescaled size + public static Size Rescale(Size original, Size maxSize) + { + if (original.IsEmpty || maxSize.IsEmpty || (original.Width <= maxSize.Width && original.Height <= maxSize.Height)) + { + return original; + } + + // Figure out the ratio + double ratioX = (double)maxSize.Width / (double)original.Width; + double ratioY = (double)maxSize.Height / (double)original.Height; + // use whichever multiplier is smaller + double ratio = ratioX < ratioY ? ratioX : ratioY; + + return new Size(Convert.ToInt32(original.Width * ratio), Convert.ToInt32(original.Height * ratio)); + } + } +} diff --git a/src/Libraries/SmartStore.Core/Utilities/Inflector.cs b/src/Libraries/SmartStore.Core/Utilities/Inflector.cs index f02ecb3784..a9f1c73e39 100644 --- a/src/Libraries/SmartStore.Core/Utilities/Inflector.cs +++ b/src/Libraries/SmartStore.Core/Utilities/Inflector.cs @@ -12,10 +12,17 @@ namespace SmartStore.Utilities // TODO: Inflector ist leider Englisch! Irgend 'ne Chance das zu lokalisieren??!! public static class Inflector { - #region fields - private static readonly List _plurals = new List(); + #region fields + + private static readonly Regex HtmlCaseRegex = new Regex( + "(? _plurals = new List(); private static readonly List _singulars = new List(); private static readonly List _uncountables = new List(); + #endregion #region ..ctor @@ -220,12 +227,17 @@ public static string Camelize(string lowercaseAndUnderscoredWord) return Uncapitalize(Pascalize(lowercaseAndUnderscoredWord)); } - /// - /// Makes an underscored form from the expression in the string. - /// - /// string. The word to underscore. - /// string. The word with underscore seperators. - public static string Underscore(string pascalCasedWord) + public static string Handleize(string input) + { + return HtmlCaseRegex.Replace(input, "-$1$2").ToLowerInvariant(); + } + + /// + /// Makes an underscored form from the expression in the string. + /// + /// string. The word to underscore. + /// string. The word with underscore seperators. + public static string Underscore(string pascalCasedWord) { return Regex.Replace( Regex.Replace( diff --git a/src/Libraries/SmartStore.Core/Utilities/Retry.cs b/src/Libraries/SmartStore.Core/Utilities/Retry.cs index 0ea4828d63..7c42ace8d6 100644 --- a/src/Libraries/SmartStore.Core/Utilities/Retry.cs +++ b/src/Libraries/SmartStore.Core/Utilities/Retry.cs @@ -21,13 +21,13 @@ public static void Run(Action operation, int attempts, TimeSpan? wait = null, Ac { Guard.NotNull(operation, nameof(operation)); - Func wrapper = () => + Run(Operation, attempts, wait, onFailed); + + bool Operation() { operation(); return true; - }; - - Run(wrapper, attempts, wait, onFailed); + } } /// diff --git a/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs b/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs index 3dec92a6d4..6613ba70f6 100644 --- a/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs @@ -5,7 +5,6 @@ namespace SmartStore.Utilities { - public static class SeoHelper { private static readonly string _okChars = "abcdefghijklmnopqrstuvwxyz1234567890 _-/"; @@ -1101,5 +1100,4 @@ private static string ToUnichar(string hexString) return returnChar; } } - } diff --git a/src/Libraries/SmartStore.Core/Utilities/SmartSyndicationFeed.cs b/src/Libraries/SmartStore.Core/Utilities/SmartSyndicationFeed.cs index c20685226e..fe2a4dc1b4 100644 --- a/src/Libraries/SmartStore.Core/Utilities/SmartSyndicationFeed.cs +++ b/src/Libraries/SmartStore.Core/Utilities/SmartSyndicationFeed.cs @@ -58,7 +58,7 @@ public SyndicationItem CreateItem(string title, string synopsis, string url, Dat return item; } - public bool AddEnclosue(SyndicationItem item, Picture picture, string pictureUrl) + public bool AddEnclosure(SyndicationItem item, Picture picture, string pictureUrl) { if (picture != null && pictureUrl.HasValue()) { @@ -67,6 +67,7 @@ public bool AddEnclosue(SyndicationItem item, Picture picture, string pictureUrl if ((picture.MediaStorageId ?? 0) != 0) { + // TODO: (mc) But what about other storage provider? // do not care about storage provider pictureLength = picture.MediaStorage.Data.LongLength; } diff --git a/src/Libraries/SmartStore.Core/Utilities/Throttle.cs b/src/Libraries/SmartStore.Core/Utilities/Throttle.cs new file mode 100644 index 0000000000..d18b698186 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Utilities/Throttle.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmartStore.Core.Utilities +{ + public static class Throttle + { + private readonly static ConcurrentDictionary _checks = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Performs a throttled check. + /// + /// Identifier for the check process + /// Interval between actual checks + /// The check factory + /// Check result + public static bool Check(string key, TimeSpan interval, Func check) + { + return Check(key, interval, false, check); + } + + /// + /// Performs a throttled check. + /// + /// Identifier for the check process + /// Interval between actual checks + /// + /// The check factory + /// Check result + public static bool Check(string key, TimeSpan interval, bool recheckWhenFalse, Func check) + { + Guard.NotEmpty(key, nameof(key)); + Guard.NotNull(check, nameof(check)); + + bool added = false; + var now = DateTime.UtcNow; + + var entry = _checks.GetOrAdd(key, x => + { + added = true; + return new CheckEntry { Value = check(), NextCheckUtc = (now + interval) }; + }); + + if (added) + { + return entry.Value; + } + + var ok = entry.Value; + var isOverdue = (!ok && recheckWhenFalse) || (now > entry.NextCheckUtc); + + if (isOverdue) + { + // Check is overdue: recheck + ok = check(); + _checks.TryUpdate(key, new CheckEntry { Value = ok, NextCheckUtc = (now + interval) }, entry); + } + + return ok; + } + + class CheckEntry + { + public bool Value { get; set; } + public DateTime NextCheckUtc { get; set; } + } + } +} diff --git a/src/Libraries/SmartStore.Core/Utilities/TypeHelper.cs b/src/Libraries/SmartStore.Core/Utilities/TypeHelper.cs index b5db831c2c..2fb3f5bc09 100644 --- a/src/Libraries/SmartStore.Core/Utilities/TypeHelper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/TypeHelper.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Globalization; using System.IO; +using System.Linq.Expressions; namespace SmartStore.Utilities { @@ -36,5 +37,25 @@ public static Type GetElementType(Type type) return type; } + /// + /// Exctracts and returns the name of a property accessor lambda + /// + /// The containing type + /// The accessor lambda + /// When true, returns the result as '[TyoeName].[PropertyName]'. + /// The property name + public static string NameOf(Expression> propertyAccessor, bool includeTypeName = false) + { + Guard.NotNull(propertyAccessor, nameof(propertyAccessor)); + + var name = propertyAccessor.ExtractPropertyInfo().Name; + + if (includeTypeName) + { + return typeof(T).Name + "." + name; + } + + return name; + } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Utilities/Wildcard.cs b/src/Libraries/SmartStore.Core/Utilities/Wildcard.cs index 6a31c4dd38..9e7f21007e 100644 --- a/src/Libraries/SmartStore.Core/Utilities/Wildcard.cs +++ b/src/Libraries/SmartStore.Core/Utilities/Wildcard.cs @@ -6,7 +6,6 @@ namespace SmartStore.Utilities { - /// /// This class is used to use wildcards and number ranges while /// searching in text. * is used of any chars, ? for one char and @@ -14,8 +13,6 @@ namespace SmartStore.Utilities /// public class Wildcard : Regex { - #region Fields - /// /// This flag determines whether the parser is forward /// direction or not. @@ -24,16 +21,16 @@ public class Wildcard : Regex private readonly string _pattern; - #endregion - - #region Ctor - - /// - /// Initializes a new instance of the class. - /// - /// The wildcard pattern. - public Wildcard(string pattern) - : this(pattern, RegexOptions.None) + /// + /// Initializes a new instance of the class. + /// + /// The wildcard pattern. + /// + /// Specifies whether number ranges (e.g. 1234-5678) should + /// be converted to a regular expression pattern. + /// + public Wildcard(string pattern, bool parseNumberRanges = false) + : this(pattern, RegexOptions.None, parseNumberRanges) { } @@ -41,9 +38,13 @@ public Wildcard(string pattern) /// Initializes a new instance of the class. /// /// The wildcard pattern. + /// + /// Specifies whether number ranges (e.g. 1234-5678) should + /// be converted to a regular expression pattern. + /// /// The regular expression options. - public Wildcard(string pattern, RegexOptions options) - : this(WildcardToRegex(pattern), options, Timeout.InfiniteTimeSpan) + public Wildcard(string pattern, RegexOptions options, bool parseNumberRanges = false) + : this(WildcardToRegex(pattern, parseNumberRanges), options, Timeout.InfiniteTimeSpan) { } @@ -54,8 +55,6 @@ internal Wildcard(string parsedPattern, RegexOptions options, TimeSpan matchTime _pattern = parsedPattern; } - #endregion - public string Pattern { get @@ -64,35 +63,36 @@ public string Pattern } } - #region Private Implementation /// /// Searches all number range terms and converts them /// to a regular expression term. /// /// The wildcard pattern. /// A converted regular expression term. - private static string WildcardToRegex(string pattern) + private static string WildcardToRegex(string pattern, bool parseNumberRanges) { m_isForward = true; - //escape and beginning - pattern = "^" + Escape(pattern); - //replace * with .* - pattern = pattern.Replace("\\*", ".*"); - //$ is for end position and replace ? with a . - pattern = pattern.Replace("\\?", ".") + "$"; - - //convert the number ranges into regular expression - var re = new Regex("[0-9]+-[0-9]+"); - MatchCollection collection = re.Matches(pattern); - foreach (Match match in collection) - { - string[] split = match.Value.Split(new char[] { '-' }); - int leadingZeroesCount = split[0].TakeWhile(x => x == '0').Count(); - int min = Int32.Parse(split[0]); - int max = Int32.Parse(split[1]); - pattern = pattern.Replace(match.Value, ConvertNumberRange(min, max, leadingZeroesCount)); - } + // Replace ? with . and * with .* + // Prepend ^, append $ + // Escape all chars except []^ + pattern = ToGlobPattern(pattern); + + // convert the number ranges into regular expression + if (parseNumberRanges) + { + var re = new Regex("[0-9]+-[0-9]+"); + MatchCollection collection = re.Matches(pattern); + foreach (Match match in collection) + { + string[] split = match.Value.Split(new char[] { '-' }); + int leadingZeroesCount = split[0].TakeWhile(x => x == '0').Count(); + int min = Int32.Parse(split[0]); + int max = Int32.Parse(split[1]); + + pattern = pattern.Replace(match.Value, ConvertNumberRange(min, max, leadingZeroesCount)); + } + } return pattern; } @@ -243,7 +243,113 @@ private static int ExtractDigit(int value, int digit) { return Int32.Parse(value.ToString()[digit].ToString()); } - #endregion - } + + #region Escaping + + /* -------------------------------------------------------------- + Stuff here partly copied over from .NET's internal RegexParser + class and modified for performance reasons: we don't want to escape + '[^]' chars, but Regex.Escape() does. Besides, wen need + '*' and '?' as wildcard chars. + -------------------------------------------------------------- */ + + const byte W = 6; // wildcard char + const byte Q = 5; // quantifier + const byte S = 4; // ordinary stopper + const byte Z = 3; // ScanBlank stopper + const byte X = 2; // whitespace + const byte E = 1; // should be escaped + + /* + * For categorizing ASCII characters. + */ + private static readonly byte[] _category = new byte[] + { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0,0,0,0,0,0,0,0,0,X,X,0,X,X,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + // ! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? + X,0,0,Z,S,0,0,0,S,S,W,Q,0,0,S,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,W, + // @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,S,0,0,0, + // ' a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,Q,S,0,0,0 + }; + + private static bool IsMetachar(char ch) + { + return (ch <= '|' && _category[ch] >= E); + } + + private static bool IsGlob(char ch) + { + return (ch <= '|' && _category[ch] >= W); + } + + private static string ToGlobPattern(string input) + { + Guard.NotNull(input, nameof(input)); + + for (int i = 0; i < input.Length; i++) + { + if (IsMetachar(input[i])) + { + var sb = new StringBuilder("^"); + char ch = input[i]; + int lastpos; + + sb.Append(input, 0, i); + do + { + if (IsGlob(ch)) + { + sb.Append('.'); // '?' > '.' + if (ch == '*') sb.Append('*'); // '*' > '.*' + } + else + { + sb.Append('\\'); + switch (ch) + { + case '\n': + ch = 'n'; + break; + case '\r': + ch = 'r'; + break; + case '\t': + ch = 't'; + break; + case '\f': + ch = 'f'; + break; + } + sb.Append(ch); + } + + i++; + lastpos = i; + + while (i < input.Length) + { + ch = input[i]; + if (IsMetachar(ch)) + break; + + i++; + } + + sb.Append(input, lastpos, i - lastpos); + } while (i < input.Length); + + sb.Append('$'); + return sb.ToString(); + } + } + + return '^' + input + '$'; + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Core/WebHelper.cs b/src/Libraries/SmartStore.Core/WebHelper.cs index 93211cba26..4d6c93c601 100644 --- a/src/Libraries/SmartStore.Core/WebHelper.cs +++ b/src/Libraries/SmartStore.Core/WebHelper.cs @@ -25,7 +25,7 @@ public partial class WebHelper : IWebHelper private static object s_lock = new object(); private static bool? s_optimizedCompilationsEnabled; private static AspNetHostingPermissionLevel? s_trustLevel; - private static readonly Regex s_staticExts = new Regex(@"(.*?)\.(css|js|png|jpg|jpeg|gif|scss|less|bmp|html|htm|xml|pdf|doc|xls|rar|zip|ico|eot|svg|ttf|woff|otf|axd|ashx)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex s_staticExts = new Regex(@"(.*?)\.(css|js|png|jpg|jpeg|gif|webp|scss|less|liquid|bmp|html|htm|xml|pdf|doc|xls|rar|zip|7z|ico|eot|svg|ttf|woff|otf|axd|ashx)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex s_htmlPathPattern = new Regex(@"(?<=(?:href|src)=(?:""|'))(?!https?://)(?[^(?:""|')]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Multiline); private static readonly Regex s_cssPathPattern = new Regex(@"url\('(?.+)'\)", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Multiline); private static ConcurrentDictionary s_safeLocalHostNames = new ConcurrentDictionary(); @@ -60,7 +60,7 @@ public virtual string GetUrlReferrer() public virtual string GetClientIdent() { var ipAddress = this.GetCurrentIpAddress(); - var userAgent = _httpContext.Request != null ? _httpContext.Request.UserAgent : string.Empty; + var userAgent = _httpContext.Request?.UserAgent.EmptyNull(); if (ipAddress.HasValue() && userAgent.HasValue()) { diff --git a/src/Libraries/SmartStore.Core/app.config b/src/Libraries/SmartStore.Core/app.config index 954929b4f5..8c178073c2 100644 --- a/src/Libraries/SmartStore.Core/app.config +++ b/src/Libraries/SmartStore.Core/app.config @@ -10,6 +10,10 @@ + + + + - + diff --git a/src/Libraries/SmartStore.Core/packages.config b/src/Libraries/SmartStore.Core/packages.config index 0a614d89f9..4820921437 100644 --- a/src/Libraries/SmartStore.Core/packages.config +++ b/src/Libraries/SmartStore.Core/packages.config @@ -1,9 +1,9 @@  + - - + @@ -14,4 +14,5 @@ + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Caching/EfDbCache.cs b/src/Libraries/SmartStore.Data/Caching/EfDbCache.cs index d3b8dca4d0..ea10af6d3b 100644 --- a/src/Libraries/SmartStore.Data/Caching/EfDbCache.cs +++ b/src/Libraries/SmartStore.Data/Caching/EfDbCache.cs @@ -23,7 +23,7 @@ public partial class EfDbCache : IDbCache typeof(QueuedEmail).Name }; - private const string KEYPREFIX = "efcache:"; + private const string KEYPREFIX = "efcache:*"; private readonly object _lock = new object(); private bool _enabled; @@ -361,8 +361,14 @@ private static string HashKey(string key) using (var sha = new SHA1CryptoServiceProvider()) { - key = Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(key))); - return KEYPREFIX + "data:" + key; + try + { + return KEYPREFIX + "data:" + Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(key))); + } + catch + { + return KEYPREFIX + "data:" + key; + } } } } diff --git a/src/Libraries/SmartStore.Data/EfRepository.cs b/src/Libraries/SmartStore.Data/EfRepository.cs index 819be62936..0d23c3df36 100644 --- a/src/Libraries/SmartStore.Data/EfRepository.cs +++ b/src/Libraries/SmartStore.Data/EfRepository.cs @@ -8,12 +8,10 @@ using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Data.Caching; +using EfState = System.Data.Entity.EntityState; namespace SmartStore.Data { - /// - /// Entity Framework repository - /// public partial class EfRepository : IRepository where T : BaseEntity { private readonly IDbContext _context; @@ -34,6 +32,7 @@ public virtual IQueryable Table { return this.Entities.AsNoTracking(); } + return this.Entities; } } @@ -71,30 +70,32 @@ public virtual T Attach(T entity) public virtual void Insert(T entity) { - if (entity == null) - throw new ArgumentNullException("entity"); + Guard.NotNull(entity, nameof(entity)); - this.Entities.Add(entity); + this.Entities.Add(entity); if (this.AutoCommitEnabledInternal) - _context.SaveChanges(); + { + _context.SaveChanges(); + } } public virtual void InsertRange(IEnumerable entities, int batchSize = 100) { try { - if (entities == null) - throw new ArgumentNullException("entities"); + Guard.NotNull(entities, nameof(entities)); - if (entities.Any()) + if (entities.Any()) { if (batchSize <= 0) { // insert all in one step this.Entities.AddRange(entities); if (this.AutoCommitEnabledInternal) - _context.SaveChanges(); + { + _context.SaveChanges(); + } } else { @@ -107,7 +108,9 @@ public virtual void InsertRange(IEnumerable entities, int batchSize = 100) if (i % batchSize == 0) { if (this.AutoCommitEnabledInternal) - _context.SaveChanges(); + { + _context.SaveChanges(); + } i = 0; saved = true; } @@ -117,7 +120,9 @@ public virtual void InsertRange(IEnumerable entities, int batchSize = 100) if (!saved) { if (this.AutoCommitEnabledInternal) - _context.SaveChanges(); + { + _context.SaveChanges(); + } } } } @@ -130,10 +135,9 @@ public virtual void InsertRange(IEnumerable entities, int batchSize = 100) public virtual void Update(T entity) { - if (entity == null) - throw new ArgumentNullException("entity"); + Guard.NotNull(entity, nameof(entity)); - SetEntityStateToModifiedIfApplicable(entity); + ChangeStateToModifiedIfApplicable(entity); if (this.AutoCommitEnabledInternal) { @@ -143,13 +147,12 @@ public virtual void Update(T entity) public virtual void UpdateRange(IEnumerable entities) { - if (entities == null) - throw new ArgumentNullException("entities"); + Guard.NotNull(entities, nameof(entities)); - entities.Each(entity => + foreach (var entity in entities) { - SetEntityStateToModifiedIfApplicable(entity); - }); + ChangeStateToModifiedIfApplicable(entity); + } if (this.AutoCommitEnabledInternal) { @@ -157,51 +160,56 @@ public virtual void UpdateRange(IEnumerable entities) } } - private void SetEntityStateToModifiedIfApplicable(T entity) + private void ChangeStateToModifiedIfApplicable(T entity) { if (entity.IsTransientRecord()) return; - + var entry = InternalContext.Entry(entity); - if (entry.State < System.Data.Entity.EntityState.Added || (this.AutoCommitEnabledInternal && !InternalContext.Configuration.AutoDetectChangesEnabled)) + + if (entry.State == EfState.Detached) { - entry.State = System.Data.Entity.EntityState.Modified; + // Entity was detached before or was explicitly constructed. + // This unfortunately sets all properties to modified. + entry.State = EfState.Modified; + } + else if (entry.State == EfState.Unchanged) + { + // We simply do nothing here, because it is ensured now that DetectChanges() + // gets implicitly called prior SaveChanges(). + + //if (this.AutoCommitEnabledInternal && !ctx.Configuration.AutoDetectChangesEnabled) + //{ + // _context.DetectChanges(); + //} } } public virtual void Delete(T entity) { - if (entity == null) - throw new ArgumentNullException("entity"); + Guard.NotNull(entity, nameof(entity)); - if (InternalContext.Entry(entity).State == System.Data.Entity.EntityState.Detached) - { - this.Entities.Attach(entity); - } - - this.Entities.Remove(entity); + InternalContext.Entry(entity).State = EfState.Deleted; if (this.AutoCommitEnabledInternal) - _context.SaveChanges(); + { + _context.SaveChanges(); + } } public virtual void DeleteRange(IEnumerable entities) { - if (entities == null) - throw new ArgumentNullException("entities"); + Guard.NotNull(entities, nameof(entities)); - entities.Each(entity => + foreach (var entity in entities) { - if (InternalContext.Entry(entity).State == System.Data.Entity.EntityState.Detached) - { - this.Entities.Attach(entity); - } - }); - - this.Entities.RemoveRange(entities); + InternalContext.Entry(entity).State = EfState.Deleted; + } if (this.AutoCommitEnabledInternal) + { _context.SaveChanges(); + } } [Obsolete("Use the extension method from 'SmartStore.Core, SmartStore.Core.Data' instead")] @@ -222,26 +230,6 @@ public IQueryable Expand(IQueryable query, Expression GetModifiedProperties(T entity) - { - return InternalContext.GetModifiedProperties(entity); - } - public virtual IDbContext Context { get { return _context; } @@ -274,6 +262,7 @@ private DbSet Entities { _entities = _context.Set(); } + return _entities as DbSet; } } diff --git a/src/Libraries/SmartStore.Data/Extensions/BaseEntityExtensions.cs b/src/Libraries/SmartStore.Data/Extensions/BaseEntityExtensions.cs deleted file mode 100644 index a576bec1cf..0000000000 --- a/src/Libraries/SmartStore.Data/Extensions/BaseEntityExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Data.Entity.Core.Objects; -using SmartStore.Core; - -namespace SmartStore.Data -{ - public static class BaseEntityExtensions - { - /// - /// Get unproxied entity type - /// - /// If your Entity Framework context is proxy-enabled, - /// the runtime will create a proxy instance of your entities, - /// i.e. a dynamically generated class which inherits from your entity class - /// and overrides its virtual properties by inserting specific code useful for example - /// for tracking changes and lazy loading. - /// - /// - /// - public static Type GetUnproxiedEntityType(this BaseEntity entity) - { - var userType = ObjectContext.GetObjectType(entity.GetType()); - return userType; - } - } -} diff --git a/src/Libraries/SmartStore.Data/Extensions/DbContextExtensions.cs b/src/Libraries/SmartStore.Data/Extensions/DbContextExtensions.cs index 6a0aba947f..4e7437f334 100644 --- a/src/Libraries/SmartStore.Data/Extensions/DbContextExtensions.cs +++ b/src/Libraries/SmartStore.Data/Extensions/DbContextExtensions.cs @@ -6,7 +6,6 @@ namespace SmartStore { - public class SqlServerInfo { public string ProductVersion { get; set; } diff --git a/src/Libraries/SmartStore.Data/Extensions/DbEntityEntryExtensions.cs b/src/Libraries/SmartStore.Data/Extensions/DbEntityEntryExtensions.cs new file mode 100644 index 0000000000..0a64a2aa15 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Extensions/DbEntityEntryExtensions.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Data.Entity.Infrastructure; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using SmartStore.ComponentModel; +using SmartStore.Core; +using SmartStore.Core.Data; +using SmartStore.Utilities; +using EfState = System.Data.Entity.EntityState; + +namespace SmartStore.Data +{ + public static class DbEntityEntryExtensions + { + public static void ReloadEntity(this DbEntityEntry entry) + { + try + { + entry.Reload(); + } + catch + { + // Can occur when entity has been detached in the meantime (for whatever fucking reasons) + if (entry.State == EfState.Detached) + { + entry.State = EfState.Unchanged; + entry.Reload(); + } + } + } + + /// + /// Gets a dictionary with modified properties for the specified entity + /// + /// The entity entry instance for which to get modified properties for + /// + /// A dictionary, where the key is the name of the modified property + /// and the value is its ORIGINAL value (which was tracked when the entity + /// was attached to the context the first time) + /// Returns an empty dictionary if no modification could be detected. + /// + public static IDictionary GetModifiedProperties(this DbEntityEntry entry, IDbContext ctx) + { + var props = GetModifiedPropertyEntries(entry, ctx).ToDictionary(k => k.Name, v => v.OriginalValue); + + //System.Diagnostics.Debug.WriteLine("GetModifiedProperties: " + String.Join(", ", props.Select(x => x.Key))); + + return props; + } + + /// + /// Checks whether an entity entry has any modified property. + /// Only entities in state are scanned for changes. + /// Merged values provided by the are ignored. + /// + /// The entry instance + /// The data context + /// true if any property has changed, false otherwise + public static bool HasChanges(this DbEntityEntry entry, IDbContext ctx) + { + var hasChanges = GetModifiedPropertyEntries(entry, ctx).Any(); + return hasChanges; + } + + internal static IEnumerable GetModifiedPropertyEntries(this DbEntityEntry entry, IDbContext ctx) + { + // Be aware of the entity state. you cannot get modified properties for detached entities. + EnsureChangesDetected(entry, ctx); + + if (entry.State != EfState.Modified) + { + yield break; + } + + foreach (var name in entry.CurrentValues.PropertyNames) + { + var prop = entry.Property(name); + if (prop != null && PropIsModified(prop, ctx)) + { + // INFO: under certain conditions DbPropertyEntry.IsModified returns true, even when values are equal + yield return prop; + } + } + } + + public static bool IsPropertyModified(this DbEntityEntry entry, IDbContext ctx, string propertyName) + { + object originalValue; + return TryGetModifiedProperty(entry, ctx, propertyName, out originalValue); + } + + public static bool TryGetModifiedProperty(this DbEntityEntry entry, IDbContext ctx, string propertyName, out object originalValue) + { + Guard.NotEmpty(propertyName, nameof(propertyName)); + + EnsureChangesDetected(entry, ctx); + + originalValue = null; + + if (entry.State != EfState.Modified) + { + return false; + } + + var prop = entry.Property(propertyName); + if (prop != null && PropIsModified(prop, ctx)) + { + // INFO: under certain conditions DbPropertyEntry.IsModified returns true, even when values are equal + originalValue = prop.OriginalValue; + return true; + } + + return false; + } + + private static void EnsureChangesDetected(DbEntityEntry entry, IDbContext ctx) + { + var state = entry.State; + + if (ctx.AutoDetectChangesEnabled && state == EfState.Modified) + return; + + if ((ctx as ObjectContextBase)?.IsInSaveOperation == true) + return; + + if (state == EfState.Unchanged || state == EfState.Modified) + { + // When AutoDetectChanges is off we cannot be sure whether the entity is really unchanged, + // because no detection was performed to verify this. + DetectChangesInProperties(entry, ctx); + } + } + + public static void DetectChangesInProperties(this DbEntityEntry entry, IDbContext ctx) + { + ctx.DetectChanges(); + + #region Experimental + + //// ChangeDetection for single entity: calls DbEntityEntry.InternalEntry > ObjectStateEntry._stateEntry.DetectChangesInProperties(bool) + //var invoked = false; + + //try + //{ + // var internalEntry = FastProperty.GetProperty(entry.GetType(), "InternalEntry").GetValue(entry); + // if (internalEntry != null) + // { + // var objectStateEntry = FastProperty.GetProperty(internalEntry.GetType(), "ObjectStateEntry").GetValue(internalEntry); + // if (objectStateEntry != null) + // { + // var innerStateEntry = objectStateEntry.GetType().GetField("_stateEntry", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(objectStateEntry); + // if (innerStateEntry != null) + // { + // var dcMethod = innerStateEntry.GetType().GetMethod("DetectChangesInProperties", BindingFlags.NonPublic | BindingFlags.Instance); + // if (dcMethod != null) + // { + // //var requiresScalarChangeTracking = (bool)FastProperty.GetProperty(objectStateEntry.GetType(), "RequiresScalarChangeTracking").GetValue(objectStateEntry); + // //var requiresComplexChangeTracking = (bool)FastProperty.GetProperty(objectStateEntry.GetType(), "RequiresComplexChangeTracking").GetValue(objectStateEntry); + // dcMethod.Invoke(innerStateEntry, new object[] { false }); + // invoked = true; + // } + // } + // } + // } + //} + //finally + //{ + // if (!invoked) ctx.DetectChanges(); + //} + + #endregion + } + + private static bool PropIsModified(DbPropertyEntry prop, IDbContext ctx) + { + // INFO: "CurrentValue" cannot be used for entities in the Deleted state. + // INFO: "OriginalValues" cannot be used for entities in the Added state. + //return !AreEqual(prop.CurrentValue, prop.OriginalValue); + return ctx.AutoDetectChangesEnabled + ? prop.IsModified + : !AreEqual(prop.CurrentValue, prop.OriginalValue); + } + + private static bool AreEqual(object cur, object orig) + { + if (cur == null && orig == null) + return true; + + return orig != null + ? orig.Equals(cur) + : cur.Equals(orig); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Extensions/IDbContextExtensions.cs b/src/Libraries/SmartStore.Data/Extensions/IDbContextExtensions.cs index d296d4fdaf..3cce9d4032 100644 --- a/src/Libraries/SmartStore.Data/Extensions/IDbContextExtensions.cs +++ b/src/Libraries/SmartStore.Data/Extensions/IDbContextExtensions.cs @@ -1,16 +1,21 @@ using System; using System.Data.Entity; using System.Data.Entity.Infrastructure; +using System.Data.SqlClient; using System.Linq; +using System.Linq.Expressions; +using System.Text.RegularExpressions; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Smo; using SmartStore.Core; using SmartStore.Core.Data; +using SmartStore.Data; +using EfState = System.Data.Entity.EntityState; namespace SmartStore { - public static class IDbContextExtensions - { - + { /// /// Loads the database copy. /// @@ -23,63 +28,40 @@ public static T LoadDatabaseCopy(this IDbContext context, T currentCopy) wher return InnerGetCopy(context, currentCopy, e => e.GetDatabaseValues()); } - private static T InnerGetCopy(IDbContext context, T currentCopy, Func, DbPropertyValues> func) where T : BaseEntity - { - // Get the database context - DbContext dbContext = CastOrThrow(context); - - // Get the entity tracking object - DbEntityEntry entry = GetEntityOrReturnNull(currentCopy, dbContext); - - // The output - T output = null; - - // Try and get the values - if (entry != null) { - DbPropertyValues dbPropertyValues = func(entry); - if(dbPropertyValues != null) { - output = dbPropertyValues.ToObject() as T; - } - } - - return output; - } - /// - /// Gets the entity or return null. + /// Loads the original copy. /// /// + /// The context. /// The current copy. - /// The db context. /// - private static DbEntityEntry GetEntityOrReturnNull(T currentCopy, DbContext dbContext) where T : BaseEntity + public static T LoadOriginalCopy(this IDbContext context, T currentCopy) where T : BaseEntity { - return dbContext.ChangeTracker.Entries().Where(e => e.Entity == currentCopy).FirstOrDefault(); + return InnerGetCopy(context, currentCopy, e => e.OriginalValues); } - private static DbContext CastOrThrow(IDbContext context) + public static DbEntityEntry GetEntry(this IDbContext context, T entity) where T : BaseEntity { - DbContext output = (context as DbContext); - - if(output == null) - { - throw new InvalidOperationException("Context does not support operation."); - } + var entry = CastOrThrow(context).Entry(entity); + return entry; + } - return output; - } + public static bool IsPropertyModified(this IDbContext ctx, T entity, Expression> propertySelector) + where T : BaseEntity + { + object originalValue; + return TryGetModifiedProperty(ctx, entity, propertySelector, out originalValue); + } - /// - /// Loads the original copy. - /// - /// - /// The context. - /// The current copy. - /// - public static T LoadOriginalCopy(this IDbContext context, T currentCopy) where T : BaseEntity + public static bool TryGetModifiedProperty(this IDbContext ctx, T entity, Expression> propertySelector, out object originalValue) + where T : BaseEntity { - return InnerGetCopy(context, currentCopy, e => e.OriginalValues); - } + Guard.NotNull(entity, nameof(entity)); + Guard.NotNull(propertySelector, nameof(propertySelector)); + + var propertyName = propertySelector.ExtractMemberInfo().Name; + return ctx.TryGetModifiedProperty(entity, propertyName, out originalValue); + } /// /// Executes the DBCC SHRINKDATABASE(0) command against the SQL Server (Express) database @@ -100,5 +82,105 @@ public static bool ShrinkDatabase(this IDbContext context) return false; } - } + + /// + /// Executes sql by using SQL-Server Management Objects which supports GO statements. + /// + public static int ExecuteSqlThroughSmo(this IDbContext ctx, string sql) + { + Guard.NotEmpty(sql, "sql"); + + int result = 0; + + try + { + bool isSqlServer = DataSettings.Current.IsSqlServer; + + if (!isSqlServer) + { + result = ctx.ExecuteSqlCommand(sql); + } + else + { + using (var sqlConnection = new SqlConnection(ObjectContextBase.GetConnectionString())) + { + var serverConnection = new ServerConnection(sqlConnection); + var server = new Server(serverConnection); + + result = server.ConnectionContext.ExecuteNonQuery(sql); + } + } + } + catch (Exception) + { + // remove the GO statements + sql = Regex.Replace(sql, @"\r{0,1}\n[Gg][Oo]\r{0,1}\n", "\n"); + + result = ctx.ExecuteSqlCommand(sql); + } + return result; + } + + #region Utils + + private static bool IsInSaveOperation(this IDbContext context) + { + return (context as ObjectContextBase)?.IsInSaveOperation == true; + } + + private static T InnerGetCopy(IDbContext context, T currentCopy, Func, DbPropertyValues> func) where T : BaseEntity + { + // Get the database context + var dbContext = CastOrThrow(context); + + // Get the entity tracking object + DbEntityEntry entry = GetEntityOrDefault(currentCopy, dbContext); + + // The output + T output = null; + + // Try and get the values + if (entry != null) + { + DbPropertyValues dbPropertyValues = func(entry); + if (dbPropertyValues != null) + { + output = dbPropertyValues.ToObject() as T; + } + } + + return output; + } + + /// + /// Gets the entity or return null. + /// + /// + /// The current copy. + /// The db context. + /// + private static DbEntityEntry GetEntityOrDefault(T currentCopy, DbContext dbContext) where T : BaseEntity + { + return dbContext.ChangeTracker.Entries().Where(e => e.Entity == currentCopy).FirstOrDefault(); + } + + private static DbContext CastOrThrow(IDbContext context) + { + return CastOrThrow(context); + } + + private static T CastOrThrow(IDbContext context) where T : DbContext + { + var dbContext = (context as T); + + if (dbContext == null) + { + throw new InvalidOperationException("Context does not support operation."); + } + + return dbContext; + } + + #endregion + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Extensions/MiscExtensions.cs b/src/Libraries/SmartStore.Data/Extensions/MiscExtensions.cs index 70efa11f18..5feec4be90 100644 --- a/src/Libraries/SmartStore.Data/Extensions/MiscExtensions.cs +++ b/src/Libraries/SmartStore.Data/Extensions/MiscExtensions.cs @@ -8,6 +8,5 @@ public static bool IsEntityFrameworkProvider(this IQueryProvider provider) { return provider.GetType().FullName == "System.Data.Objects.ELinq.ObjectQueryProvider"; } - } } diff --git a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductAttributeMap.cs b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductAttributeMap.cs index d2cb42ee35..466c56ebd7 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductAttributeMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Catalog/ProductAttributeMap.cs @@ -7,10 +7,11 @@ public partial class ProductAttributeMap : EntityTypeConfiguration pa.Id); - this.Property(pa => pa.Alias).HasMaxLength(100); - this.Property(pa => pa.Name).IsRequired(); - } + ToTable("ProductAttribute"); + HasKey(pa => pa.Id); + Property(pa => pa.Alias).HasMaxLength(100); + Property(pa => pa.Name).IsRequired(); + Property(pa => pa.ExportMappings).IsMaxLength(); + } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Mapping/Directory/CurrencyMap.cs b/src/Libraries/SmartStore.Data/Mapping/Directory/CurrencyMap.cs index 18ba293a92..b5f2dabb34 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Directory/CurrencyMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Directory/CurrencyMap.cs @@ -15,6 +15,7 @@ public CurrencyMap() this.Property(c => c.CustomFormatting).HasMaxLength(50); this.Property(c => c.Rate).HasPrecision(18, 8); // // With virtual currencies (e.g. BitCoin) being so precise, we need to store rates up to 8 decimal places this.Property(c => c.DomainEndings).HasMaxLength(1000); + this.Property(o => o.RoundOrderTotalDenominator).HasPrecision(18, 4); } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Mapping/Messages/QueuedEmailMap.cs b/src/Libraries/SmartStore.Data/Mapping/Messages/QueuedEmailMap.cs index a3e66d1e84..a7cfe9ea32 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Messages/QueuedEmailMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Messages/QueuedEmailMap.cs @@ -11,11 +11,8 @@ public QueuedEmailMap() this.HasKey(qe => qe.Id); this.Property(qe => qe.From).IsRequired().HasMaxLength(500); - this.Property(qe => qe.FromName).HasMaxLength(500); this.Property(qe => qe.To).IsRequired().HasMaxLength(500); - this.Property(qe => qe.ToName).HasMaxLength(500); this.Property(qe => qe.ReplyTo).HasMaxLength(500); - this.Property(qe => qe.ReplyToName).HasMaxLength(500); this.Property(qe => qe.CC).HasMaxLength(500); this.Property(qe => qe.Bcc).HasMaxLength(500); this.Property(qe => qe.Subject).HasMaxLength(1000); diff --git a/src/Libraries/SmartStore.Data/Mapping/Orders/OrderMap.cs b/src/Libraries/SmartStore.Data/Mapping/Orders/OrderMap.cs index 660eec1f46..b06180e269 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Orders/OrderMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Orders/OrderMap.cs @@ -22,6 +22,7 @@ public OrderMap() this.Property(o => o.PaymentMethodAdditionalFeeTaxRate).HasPrecision(18, 4); this.Property(o => o.OrderTax).HasPrecision(18, 4); this.Property(o => o.OrderDiscount).HasPrecision(18, 4); + this.Property(o => o.OrderTotalRounding).HasPrecision(18, 4); this.Property(o => o.OrderTotal).HasPrecision(18, 4); this.Property(o => o.RefundedAmount).HasPrecision(18, 4); this.Property(o => o.OrderNumber).IsOptional(); diff --git a/src/Libraries/SmartStore.Data/Mapping/Shipping/ShippingMethodMap.cs b/src/Libraries/SmartStore.Data/Mapping/Shipping/ShippingMethodMap.cs index 94cf2af062..723fe6fbce 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Shipping/ShippingMethodMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Shipping/ShippingMethodMap.cs @@ -3,18 +3,14 @@ namespace SmartStore.Data.Mapping.Shipping { - public class ShippingMethodMap : EntityTypeConfiguration + public class ShippingMethodMap : EntityTypeConfiguration { public ShippingMethodMap() { - this.ToTable("ShippingMethod"); - this.HasKey(sm => sm.Id); + ToTable("ShippingMethod"); + HasKey(sm => sm.Id); - this.Property(sm => sm.Name).IsRequired().HasMaxLength(400); - - this.HasMany(sm => sm.RestrictedCountries) - .WithMany(c => c.RestrictedShippingMethods) - .Map(m => m.ToTable("ShippingMethodRestrictions")); + Property(sm => sm.Name).IsRequired().HasMaxLength(400); } } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201504171629262_V22Final.cs b/src/Libraries/SmartStore.Data/Migrations/201504171629262_V22Final.cs index 23b0f56a14..58b0e21624 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201504171629262_V22Final.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201504171629262_V22Final.cs @@ -71,7 +71,6 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.AddOrUpdate("Common.Next", "Next", "Weiter"); - builder.AddOrUpdate("Admin.Common.BackToConfiguration", "Back to configuration", "Zur�ck zur Konfiguration"); @@ -90,7 +89,6 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.AddOrUpdate("Admin.Common.UnknownError", "An unknown error has occurred.", "Es ist ein unbekannter Fehler aufgetreten."); - builder.AddOrUpdate("Plugins.Feed.FreeShippingThreshold", "Free shipping threshold", "Kostenloser Versand ab", diff --git a/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.cs b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.cs index d168932260..903a904222 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.cs @@ -5,7 +5,7 @@ namespace SmartStore.Data.Migrations using Core.Domain; using Core.Domain.DataExchange; using Setup; - using Utilities; + using SmartStore.Utilities; public partial class ExportRevision : DbMigration, ILocaleResourcesProvider, IDataSeeder { diff --git a/src/Libraries/SmartStore.Data/Migrations/201710102038287_CurrencyRounding.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201710102038287_CurrencyRounding.Designer.cs new file mode 100644 index 0000000000..a07392cfe4 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201710102038287_CurrencyRounding.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.1.3-40302")] + public sealed partial class CurrencyRounding : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(CurrencyRounding)); + + string IMigrationMetadata.Id + { + get { return "201710102038287_CurrencyRounding"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201710102038287_CurrencyRounding.cs b/src/Libraries/SmartStore.Data/Migrations/201710102038287_CurrencyRounding.cs new file mode 100644 index 0000000000..7cfdcbb63b --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201710102038287_CurrencyRounding.cs @@ -0,0 +1,119 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using Setup; + + public partial class CurrencyRounding : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.Order", "OrderTotalRounding", c => c.Decimal(nullable: false, precision: 18, scale: 4)); + AddColumn("dbo.Currency", "RoundOrderItemsEnabled", c => c.Boolean(nullable: false)); + AddColumn("dbo.Currency", "RoundNumDecimals", c => c.Int(nullable: false, defaultValue: 2)); + AddColumn("dbo.Currency", "RoundOrderTotalEnabled", c => c.Boolean(nullable: false)); + AddColumn("dbo.Currency", "RoundOrderTotalDenominator", c => c.Decimal(nullable: false, precision: 18, scale: 4)); + AddColumn("dbo.Currency", "RoundOrderTotalRule", c => c.Int(nullable: false)); + AddColumn("dbo.PaymentMethod", "RoundOrderTotalEnabled", c => c.Boolean(nullable: false)); + } + + public override void Down() + { + DropColumn("dbo.PaymentMethod", "RoundOrderTotalEnabled"); + DropColumn("dbo.Currency", "RoundOrderTotalRule"); + DropColumn("dbo.Currency", "RoundOrderTotalDenominator"); + DropColumn("dbo.Currency", "RoundOrderTotalEnabled"); + DropColumn("dbo.Currency", "RoundNumDecimals"); + DropColumn("dbo.Currency", "RoundOrderItemsEnabled"); + DropColumn("dbo.Order", "OrderTotalRounding"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + + context.SaveChanges(); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Common.Round", "Round", "Runden"); + builder.AddOrUpdate("ShoppingCart.Totals.Rounding", "Rounding", "Rundung"); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Directory.CurrencyRoundingRule.RoundMidpointDown", + "Round midpoint down (e.g. 0.05 rounding: 9.225 will round to 9.20)", + "Mittelwert abrunden (z.B. 0,05 Rundung: 9,225 wird auf 9,20 gerundet)"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Directory.CurrencyRoundingRule.RoundMidpointUp", + "Round midpoint up (e.g. 0.05 rounding: 9.225 will round to 9.25)", + "Mittelwert aufrunden (z.B. 0,05 Rundung: 9,225 wird auf 9,25 gerundet)"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Directory.CurrencyRoundingRule.AlwaysRoundDown", + "Always round down (e.g. 0.05 rounding: 9.24 will round to 9.20)", + "Immer abrunden (z.B. 0,05 Rundung: 9,24 wird auf 9,20 gerundet)"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Directory.CurrencyRoundingRule.AlwaysRoundUp", + "Always round up (e.g. 0.05 rounding, 9.26 will round to 9.30)", + "Immer aufrunden (z.B. 0,05 Rundung: 9,26 wird auf 9,30 gerundet)"); + + builder.AddOrUpdate("Admin.Configuration.Currencies.NoPaymentMethodsEnabledRounding", + "Regardless of the currency configuration, the order totals are only rounded if it has been enabled for the selected payment method. Click Edit for the desired payment methods and activate the rounding option.", + "Unabh�ngig von der W�hrungs-Konfiguration werden Bestellsummen erst gerundet, wenn das f�r die gew�hlte Zahlart auch vorgesehen ist. Klicken Sie bei den gew�nschten Zahlarten auf Bearbeiten und aktivieren Sie die Rundungs-Option."); + + builder.AddOrUpdate("Admin.Configuration.Currencies.PaymentMethodsEnabledRoundingList", + "Payment methods for which order total rounding is enabled", + "Zahlarten, bei denen Bestellsummen-Rundung aktiviert ist"); + + builder.AddOrUpdate("Admin.Configuration.Currencies.Fields.RoundOrderItemsEnabled", + "Round all order item amounts", + "Betr�ge aller Bestellpositionen runden", + "Specifies whether to round all order item amounts (products, fees, tax etc.)", + "Legt fest, ob die Betr�ge aller Bestellpositionen gerundet werden sollen (Produkte, Geb�hren, Steuern etc.)"); + + builder.AddOrUpdate("Admin.Configuration.Currencies.Fields.RoundNumDecimals", + "Number of decimal digits", + "Anzahl Dezimalstellen", + "Specifies the number of decimal digits to round to (Default: 2)", + "Legt fest, auf wieviele Dezimalstellen gerundet werden soll (Standard: 2)"); + + builder.AddOrUpdate("Admin.Configuration.Currencies.Fields.RoundOrderTotalEnabled", + "Round order total amount", + "Bestellsumme runden", + "Specifies whether to round the order total amount.", + "Legt fest, ob die Bestellsumme gerundet werden soll."); + + builder.AddOrUpdate("Admin.Configuration.Currencies.Fields.RoundOrderTotalDenominator", + "Round to", + "Runden nach", + "Specifies the nearest multiple of the smallest chosen amount to round the order total to. 0.05 for example will round 9.43 up to 9.45.", + "Legt das n�chste Vielfache des kleinsten, gew�hlten Betrages fest, auf den die Bestellsumme gerundet werden soll. Bei 0,05 wird z.B. 9,43 auf 9,45 gerundet."); + + builder.AddOrUpdate("Admin.Configuration.Currencies.Fields.RoundOrderTotalRule", + "Rounding rule", + "Rundungsregel", + "Specifies the rule for rounding the order total amount.", + "Legt die Regel f�r das Runden der Bestellsumme fest."); + + builder.AddOrUpdate("Admin.Configuration.Payment.Methods.RoundOrderTotalEnabled", + "Round order total amount (if enabled)", + "Bestellsumme runden, sofern aktiviert", + "Specifies whether to round the order total in accordance with currency configuration if this payment method was selected in checkout.", + "Legt fest, ob die Bestellsumme gem�� W�hrungs-Konfiguration gerundet werden soll, wenn diese Zahlart im Checkout gew�hlt wurde."); + + builder.AddOrUpdate("Admin.Configuration.Currencies.Fields.RoundOrderItemsEnabled.Validation", + "The number of decimal digits must be between 0 and 8.", + "Die Anzahl der Dezimalstellen muss zwischen 0 und 8 liegen."); + + builder.Delete( + "Admin.Configuration.Settings.ShoppingCart.RoundPricesDuringCalculation", + "Admin.Configuration.Settings.ShoppingCart.RoundPricesDuringCalculation.Hint"); + + builder.AddOrUpdate("Admin.Orders.Fields.OrderTotalRounding", + "Rounding", + "Rundung", + "The amount by which the order total was rounded up or down.", + "Der Betrag, um den der Auftragswert auf- bzw. abgerundet wurde."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201710102038287_CurrencyRounding.resx b/src/Libraries/SmartStore.Data/Migrations/201710102038287_CurrencyRounding.resx new file mode 100644 index 0000000000..b1a1ddce2b --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201710102038287_CurrencyRounding.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/201710252016556_IndexOptionNames.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201710252016556_IndexOptionNames.Designer.cs new file mode 100644 index 0000000000..2024642a96 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201710252016556_IndexOptionNames.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.1.3-40302")] + public sealed partial class IndexOptionNames : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(IndexOptionNames)); + + string IMigrationMetadata.Id + { + get { return "201710252016556_IndexOptionNames"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201710252016556_IndexOptionNames.cs b/src/Libraries/SmartStore.Data/Migrations/201710252016556_IndexOptionNames.cs new file mode 100644 index 0000000000..d7f9caa6a4 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201710252016556_IndexOptionNames.cs @@ -0,0 +1,55 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using Setup; + + public partial class IndexOptionNames : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.SpecificationAttribute", "IndexOptionNames", c => c.Boolean(nullable: false)); + AddColumn("dbo.ProductAttribute", "IndexOptionNames", c => c.Boolean(nullable: false)); + } + + public override void Down() + { + DropColumn("dbo.ProductAttribute", "IndexOptionNames"); + DropColumn("dbo.SpecificationAttribute", "IndexOptionNames"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + + context.SaveChanges(); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Search.Fields.SpecificationAttributeOptionName", + "Option name of specification attributes", + "Optionsname von Spezifikationsattributen"); + + builder.AddOrUpdate("Search.Fields.ProductAttributeOptionName", + "Option name of product attributes", + "Optionsname von Produktattributen"); + + builder.AddOrUpdate("Admin.Catalog.Attributes.SpecificationAttributes.Fields.IndexOptionNames", + "Index option names", + "Optionsnamen indexieren", + "Specifies whether option names should be included in the search index so that products can be found by them. This setting is only effective by using the 'MegaSearchPlus' plugin. Changes will take effect after next update of the search index.", + "Legt fest, ob Optionsnamen mit in den Suchindex aufgenommen werden sollen, damit Produkte �ber sie gefunden werden k�nnen. Diese Einstellung ist nur unter Verwendung des 'MegaSearchPlus' Plugins wirksam. �nderungen werden nach der n�chsten Aktualisierung des Suchindex wirksam."); + + builder.AddOrUpdate("Admin.Catalog.Attributes.ProductAttributes.Fields.IndexOptionNames", + "Index option names", + "Optionsnamen indexieren", + "Specifies whether option names should be included in the search index so that products can be found by them. This setting is only effective by using the 'MegaSearchPlus' plugin. Changes will take effect after next update of the search index.", + "Legt fest, ob Optionsnamen mit in den Suchindex aufgenommen werden sollen, damit Produkte �ber sie gefunden werden k�nnen. Diese Einstellung ist nur unter Verwendung des 'MegaSearchPlus' Plugins wirksam. �nderungen werden nach der n�chsten Aktualisierung des Suchindex wirksam."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201710252016556_IndexOptionNames.resx b/src/Libraries/SmartStore.Data/Migrations/201710252016556_IndexOptionNames.resx new file mode 100644 index 0000000000..c9afccbfca --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201710252016556_IndexOptionNames.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/201711112331162_ProductMainPictureId.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201711112331162_ProductMainPictureId.Designer.cs new file mode 100644 index 0000000000..794345d94f --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201711112331162_ProductMainPictureId.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.1.3-40302")] + public sealed partial class ProductMainPictureId : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(ProductMainPictureId)); + + string IMigrationMetadata.Id + { + get { return "201711112331162_ProductMainPictureId"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201711112331162_ProductMainPictureId.cs b/src/Libraries/SmartStore.Data/Migrations/201711112331162_ProductMainPictureId.cs new file mode 100644 index 0000000000..a3073c4e43 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201711112331162_ProductMainPictureId.cs @@ -0,0 +1,30 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + using Setup; + using Utilities; + + public partial class ProductMainPictureId : DbMigration, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.Product", "MainPictureId", c => c.Int()); + } + + public override void Down() + { + DropColumn("dbo.Product", "MainPictureId"); + } + + public bool RollbackOnFailure + { + get { return true; } + } + + public void Seed(SmartObjectContext context) + { + DataMigrator.FixProductMainPictureIds(context, true); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201711112331162_ProductMainPictureId.resx b/src/Libraries/SmartStore.Data/Migrations/201711112331162_ProductMainPictureId.resx new file mode 100644 index 0000000000..b02bf9dbde --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201711112331162_ProductMainPictureId.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/201711222311112_MoveFsMedia.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201711222311112_MoveFsMedia.Designer.cs new file mode 100644 index 0000000000..1a420850cf --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201711222311112_MoveFsMedia.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.1.3-40302")] + public sealed partial class MoveFsMedia : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(MoveFsMedia)); + + string IMigrationMetadata.Id + { + get { return "201711222311112_MoveFsMedia"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201711222311112_MoveFsMedia.cs b/src/Libraries/SmartStore.Data/Migrations/201711222311112_MoveFsMedia.cs new file mode 100644 index 0000000000..c8dd751181 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201711222311112_MoveFsMedia.cs @@ -0,0 +1,119 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + using System.Web.Hosting; + using System.Linq; + using System.Collections.Generic; + using SmartStore.Core.Domain.Configuration; + using SmartStore.Data.Setup; + using SmartStore.Utilities; + using Core.Infrastructure; + using Core.IO; + using System.Text.RegularExpressions; + using System.IO; + using Core.Data; + + public partial class MoveFsMedia : DbMigration, IDataSeeder + { + const int DirMaxLength = 4; + + public override void Up() + { + } + + public override void Down() + { + } + + public bool RollbackOnFailure + { + get { return true; } + } + + public void Seed(SmartObjectContext context) + { + if (!HostingEnvironment.IsHosted) + return; + + // Move the whole media folder to new location at first + MoveMediaFolder(); + + // Check whether FS storage provider is active... + var setting = context.Set().FirstOrDefault(x => x.Name == "Media.Storage.Provider"); + if (setting == null || !setting.Value.IsCaseInsensitiveEqual("MediaStorage.SmartStoreFileSystem")) + { + // DB provider is active: no need to move anything. + return; + } + + // What a huge, fucking hack! > IMediaFileSystem is defined in an + // assembly which we don't reference from here. But it also implements + // IFileSystem, which we can cast to. + var fsType = Type.GetType("SmartStore.Services.Media.IMediaFileSystem, SmartStore.Services"); + var fs = EngineContext.Current.Resolve(fsType) as IFileSystem; + + // Pattern for file matching. E.g. matches 0000234-0.png + var rg = new Regex(@"^([0-9]{7})-0[.](.{3,4})$", RegexOptions.Compiled | RegexOptions.Singleline); + + var subfolders = new Dictionary(); + + // Get root files + var files = fs.ListFiles("").ToList(); + foreach (var chunk in files.Slice(500)) + { + foreach (var file in chunk) + { + var match = rg.Match(file.Name); + if (match.Success) + { + var name = match.Groups[1].Value; + var ext = match.Groups[2].Value; + // The new file name without trailing -0 + var newName = string.Concat(name, ".", ext); + // The subfolder name, e.g. 0024, when file name is 0024893.png + var dirName = name.Substring(0, DirMaxLength); + + string subfolder = null; + if (!subfolders.TryGetValue(dirName, out subfolder)) + { + // Create subfolder "Storage/0000" + subfolder = fs.Combine("Storage", dirName); + fs.TryCreateFolder(subfolder); + subfolders[dirName] = subfolder; + } + + // Build destination path + var destinationPath = fs.Combine(subfolder, newName); + + // Move the file now! + fs.RenameFile(file.Path, destinationPath); + } + } + } + } + + private void MoveMediaFolder() + { + // Moves "~/Media/{Tenant}" to "~/App_Data/Tenants/{Tenant}/Media" + // This is relevant for local file system only. For cloud storages (like Azure) + // we don't need to move the main folder. + + var sourceDir = new DirectoryInfo(CommonHelper.MapPath("~/Media/" + DataSettings.Current.TenantName, false)); + var destinationDir = new DirectoryInfo(CommonHelper.MapPath("~/App_Data/Tenants/" + DataSettings.Current.TenantName + "/Media", false)); + + if (!sourceDir.Exists) + { + // Source (legacy media folder) does not exist, for whatever reasons. Nothing to move here. + return; + } + + //if (!destinationDir.Exists) + //{ + // destinationDir.Create(); + //} + + sourceDir.MoveTo(destinationDir.FullName); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201711222311112_MoveFsMedia.resx b/src/Libraries/SmartStore.Data/Migrations/201711222311112_MoveFsMedia.resx new file mode 100644 index 0000000000..b02bf9dbde --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201711222311112_MoveFsMedia.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/201711291017168_SyncStringResources.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201711291017168_SyncStringResources.Designer.cs new file mode 100644 index 0000000000..4f5a89531e --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201711291017168_SyncStringResources.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 SyncStringResources : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(SyncStringResources)); + + string IMigrationMetadata.Id + { + get { return "201711291017168_SyncStringResources"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201711291017168_SyncStringResources.cs b/src/Libraries/SmartStore.Data/Migrations/201711291017168_SyncStringResources.cs new file mode 100644 index 0000000000..4799f3aecd --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201711291017168_SyncStringResources.cs @@ -0,0 +1,166 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Collections.Generic; + using System.Data.Entity.Migrations; + using System.Linq; + using SmartStore.Collections; + using SmartStore.Core.Domain.Localization; + using SmartStore.Data.Setup; + + public partial class SyncStringResources : DbMigration, IDataSeeder + { + private static HashSet _unusedNames = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "Enums.SmartStore.Core.Domain.Catalog.AttributeControlType.ColorSquares", + "Plugins.Payments.PayPal.IpnLogInfo", + "Plugins.Widgets.TrustedShopsCustomerProtection.Prepared", + "Plugins.Shipping.ByWeight.SmallQuantityThresholdNotReached", + "Products.ProductNotAddedToTheCart.Link" + }; + + private static Dictionary _missingEnglishResources = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Enums.SmartStore.Plugin.Shipping.Fedex.DropoffType.BusinessServiceCenter", "Business service center" }, + { "Enums.SmartStore.Plugin.Shipping.Fedex.DropoffType.DropBox", "Drop box" }, + { "Enums.SmartStore.Plugin.Shipping.Fedex.DropoffType.RegularPickup", "Regular pickup" }, + { "Enums.SmartStore.Plugin.Shipping.Fedex.DropoffType.RequestCourier", "Request courier" }, + { "Enums.SmartStore.Plugin.Shipping.Fedex.DropoffType.Station", "Station" }, + { "Enums.SmartStore.Plugin.Shipping.Fedex.PackingType.PackByDimensions", "Pack by dimensions" }, + { "Enums.SmartStore.Plugin.Shipping.Fedex.PackingType.PackByOneItemPerPackage", "Pack by one item per package" }, + { "Enums.SmartStore.Plugin.Shipping.Fedex.PackingType.PackByVolume", "Pack by volume" }, + { "Order.OrderDetails", "Order Details" }, + { "Plugins.SmartStore.AccardaKar.Amount", "Amount" }, + { "Plugins.SmartStore.Exports.BMEcatProductXml.ExportProfileInfo", "Please click here to view existing BMEcat export profiles or to create a new one." }, + { "Plugins.Description.SmartStore.DiscountRules", "Provides standard discount rules, e.g. \"Country is\", \"Customer group is\", \"Has amount x spent\" etc." }, + { "Plugins.FriendlyName.DiscountRequirement.BillingCountryIs", "Country of invoice address is" }, + { "Plugins.FriendlyName.DiscountRequirement.HadSpentAmount", "Customer has spent amount x" }, + { "Plugins.FriendlyName.DiscountRequirement.HasAllProducts", "Customer has the following products in his shopping cart" }, + { "Plugins.FriendlyName.DiscountRequirement.HasOneProduct", "Customer has one of the following products in his shopping cart" }, + { "Plugins.FriendlyName.DiscountRequirement.MustBeAssignedToCustomerRole", "The customer must be assigned to a customer group" }, + { "Plugins.FriendlyName.DiscountRequirement.ShippingCountryIs", "Country of delivery address is" }, + { "Plugins.FriendlyName.DiscountRequirement.Store", "Limited to specific shop" }, + { "Plugins.FriendlyName.SmartStore.DiscountRules", "Standard discount rules" }, + { "Plugins.FriendlyName.DiscountRequirement.HasPaymentMethod", "Customer selected specific payment method" }, + { "Plugins.FriendlyName.SmartStore.DiscountRules.HasPaymentMethod", "\"Customer has selected certain payment method\" discount rule" }, + { "Plugins.FriendlyName.DiscountRequirement.HasShippingOption", "Customer selected specific shipping method" }, + { "Plugins.FriendlyName.SmartStore.DiscountRules.HasShippingOption", "\"Customer has selected certain shipping method\" discount rule" }, + { "Plugins.FriendlyName.DiscountRequirement.PurchasedAllProducts", "Customer has already purchased the following products" }, + { "Plugins.FriendlyName.DiscountRequirement.PurchasedOneProduct", "The customer has already purchased one of the following products" }, + { "Plugins.FriendlyName.SmartStore.DiscountRules.PurchasedProducts", "\"Customer has already purchased the following products\" discount rule" }, + { "Plugins.SmartStore.GoogleRemarketing.ConversionId.Hint", "You'll find your conversion id in the admin area of google adwords" }, + { "Plugins.Description.SmartStore.OfflinePayment", "Provides offline payment methods, e.g. direct debit, credit card, invoice, prepayment, cash on delivery etc." }, + { "Plugins.FriendlyName.SmartStore.OfflinePayment", "Offline payment methods" }, + { "Plugins.FriendlyName.SmartStore.OutputCache", "Output Cache" }, + { "Plugins.Description.SmartStore.OutputCache", "Allows the temporary storage of entire shop pages and thus contributes to a considerable increase in performance." }, + { "Plugins.FriendlyName.OutputCacheProvider.Memory", "Local memory" }, + { "Plugins.FriendlyName.OutputCacheProvider.Database", "Database" }, + { "Plugins.FriendlyName.SmartStore.PostFinanceECommerce", "PostFinance" }, + { "Plugins.Description.SmartStore.PostFinanceECommerce", "Enables payment with the swiss PostFinance." }, + { "Plugins.SmartStore.ShopConnector.FinalResult", "{0}... {1} of {2} processed. {3} success, {4} failed, {5} skipped. {6} added, {7} updated." }, + { "Plugins.Widgets.TrustedShopsCustomerProtection.Password", "Trusted Shops password" }, + { "Plugins.Widgets.TrustedShopsCustomerProtection.Password.Hint", "Please enter the password provided by Trusted Shops here." }, + { "Plugins.Widgets.TrustedShopsCustomerProtection.UserName", "Trusted Shops user name" }, + { "Plugins.Widgets.TrustedShopsCustomerProtection.UserName.Hint", "Please enter the user name provided by Trusted Shops here." }, + }; + + public override void Up() + { + } + + public override void Down() + { + } + + public bool RollbackOnFailure + { + get { return true; } + } + + public void Seed(SmartObjectContext context) + { + var resourceSet = context.Set(); + var allLanguages = context.Set().ToList(); + + // Accidents. + var accidents = resourceSet.Where(x => x.ResourceName == "Admin.Configuration.ActivityLog.ActivityLogTy pe").ToList(); + if (accidents.Any()) + { + accidents.Each(x => x.ResourceName = "Admin.Configuration.ActivityLog.ActivityLogType"); + context.SaveChanges(); + } + + // Remove unused resources that could be included in the German set. + var unusedResources = resourceSet.Where(x => _unusedNames.Contains(x.ResourceName)).ToList(); + if (unusedResources.Any()) + { + resourceSet.RemoveRange(unusedResources); + context.SaveChanges(); + unusedResources.Clear(); + } + + // Remove duplicate resources. + foreach (var language in allLanguages) + { + var resources = resourceSet.Where(x => x.LanguageId == language.Id).ToList(); + var deleteResources = new List(); + var resourcesMap = new Multimap(StringComparer.OrdinalIgnoreCase); + resources.Each(x => resourcesMap.Add(x.ResourceName, x)); + + foreach (var item in resourcesMap) + { + if (item.Value.Count > 1) + { + // First is ok, rest is bad. + foreach (var resource in item.Value.OrderByDescending(x => x.IsTouched).Skip(1)) + { + deleteResources.Add(resource); + } + } + } + + if (deleteResources.Any()) + { + resourceSet.RemoveRange(deleteResources); + context.SaveChanges(); + deleteResources.Clear(); + } + } + + // Remove resources that are not included in the German set. + // Unfortunately we cannot do that. We have no information about the origin of a resource. We would delete resources of other developers. + + // Add resources included in the German set but missing in the English set. + var deLanguage = allLanguages.FirstOrDefault(x => x.LanguageCulture.IsCaseInsensitiveEqual("de-DE")); + var enLanguage = allLanguages.FirstOrDefault(x => x.LanguageCulture.IsCaseInsensitiveEqual("en-US")); + if (deLanguage != null && enLanguage != null) + { + var deResources = resourceSet.AsNoTracking().Where(x => x.LanguageId == deLanguage.Id).ToList(); + + var enNames = resourceSet + .Where(x => x.LanguageId == enLanguage.Id) + .Select(x => x.ResourceName) + .Distinct() + .ToList(); + var enNamesSet = new HashSet(enNames, StringComparer.OrdinalIgnoreCase); + + foreach (var resource in deResources) + { + if (!enNames.Contains(resource.ResourceName) && _missingEnglishResources.TryGetValue(resource.ResourceName, out string value)) + { + resourceSet.Add(new LocaleStringResource + { + LanguageId = enLanguage.Id, + ResourceName = resource.ResourceName, + ResourceValue = value, + IsFromPlugin = resource.IsFromPlugin + }); + } + } + + context.SaveChanges(); + deResources.Clear(); + } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201711291017168_SyncStringResources.resx b/src/Libraries/SmartStore.Data/Migrations/201711291017168_SyncStringResources.resx new file mode 100644 index 0000000000..e15616dbf6 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201711291017168_SyncStringResources.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/201712081631552_Liquid.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201712081631552_Liquid.Designer.cs new file mode 100644 index 0000000000..a4efc6b562 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201712081631552_Liquid.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 Liquid : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(Liquid)); + + string IMigrationMetadata.Id + { + get { return "201712081631552_Liquid"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201712081631552_Liquid.cs b/src/Libraries/SmartStore.Data/Migrations/201712081631552_Liquid.cs new file mode 100644 index 0000000000..450133d11a --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201712081631552_Liquid.cs @@ -0,0 +1,115 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + using System.Web.Hosting; + using System.Linq; + using SmartStore.Core.Data; + using SmartStore.Core.Domain.Localization; + using SmartStore.Data.Setup; + using SmartStore.Data.Utilities; + + public partial class Liquid : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.MessageTemplate", "To", c => c.String(nullable: false, maxLength: 500, defaultValue: " ")); + AddColumn("dbo.MessageTemplate", "ReplyTo", c => c.String(maxLength: 500)); + AddColumn("dbo.MessageTemplate", "ModelTypes", c => c.String(maxLength: 500)); + AddColumn("dbo.MessageTemplate", "LastModelTree", c => c.String()); + DropColumn("dbo.QueuedEmail", "FromName"); + DropColumn("dbo.QueuedEmail", "ToName"); + DropColumn("dbo.QueuedEmail", "ReplyToName"); + } + + public override void Down() + { + AddColumn("dbo.QueuedEmail", "ReplyToName", c => c.String(maxLength: 500)); + AddColumn("dbo.QueuedEmail", "ToName", c => c.String(maxLength: 500)); + AddColumn("dbo.QueuedEmail", "FromName", c => c.String(maxLength: 500)); + DropColumn("dbo.MessageTemplate", "LastModelTree"); + DropColumn("dbo.MessageTemplate", "ModelTypes"); + DropColumn("dbo.MessageTemplate", "ReplyTo"); + DropColumn("dbo.MessageTemplate", "To"); + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + context.SaveChanges(); + + if (HostingEnvironment.IsHosted && DataSettings.DatabaseIsInstalled()) + { + // Import all xml templates on disk + var converter = new MessageTemplateConverter(context); + var language = ResolveMasterLanguage(context); + converter.ImportAll(language); + + DropDefaultValueConstraint(context); + } + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.Delete( + "Admin.System.QueuedEmails.Fields.FromName", + "Admin.System.QueuedEmails.Fields.FromName.Hint", + "Admin.System.QueuedEmails.Fields.ToName", + "Admin.System.QueuedEmails.Fields.ToName.Hint"); + + builder.AddOrUpdate("Admin.System.QueuedEmails.Fields.ReplyTo", + "Reply to", + "Antwort an", + "Reply-To address of the email.", + "Antwortadresse der E-Mail."); + + builder.AddOrUpdate("Common.Error.NoMessageTemplate", + "The message template '{0}' does not exist.", + "Die Nachrichtenvorlage '{0}' existiert nicht."); + + builder.AddOrUpdate("Admin.ContentManagement.MessageTemplates.NoModelTree", + "Variables are unknown until at least one message of the current type has either been sent or previewed.", + "Variablen sind erst bekannt, wenn mind. eine Nachricht vom aktuellen Typ entweder gesendet oder getestet wurde."); + + builder.AddOrUpdate("Admin.Promotions.Campaigns.Fields.AllowedTokens", + "Allowed template variables", + "Erlaubte Template Variablen", + "Inserts the selected variable in the HTML document.", + "F�gt die gew�hlte Variable in das HTML-Dokument ein."); + } + + private Language ResolveMasterLanguage(SmartObjectContext context) + { + var query = context.Set().OrderBy(x => x.DisplayOrder); + + var language = query + .Where(x => (x.UniqueSeoCode == "de" || x.UniqueSeoCode == "en") && x.Published) + .FirstOrDefault(); + + if (language == null) + { + language = query.Where(x => x.Published).FirstOrDefault(); + } + + return language; + } + + private void DropDefaultValueConstraint(SmartObjectContext context) + { + // During migration we created a new NON-Nullable column ("To") + // with a default value contraint of ' ', otherwise column creation + // would have failed. Now we need to get rid of this constraint. + + if (DataSettings.Current.IsSqlServer) + { + string sql = @"DECLARE @name nvarchar(100) +SELECT @name = [name] from sys.objects WHERE type = 'D' and parent_object_id = object_id('MessageTemplate') +IF (@name is not null) BEGIN EXEC ('ALTER TABLE [MessageTemplate] Drop Constraint [' + @name +']') END"; + + context.ExecuteSqlCommand(sql); + } + } + + public bool RollbackOnFailure => true; + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201712081631552_Liquid.resx b/src/Libraries/SmartStore.Data/Migrations/201712081631552_Liquid.resx new file mode 100644 index 0000000000..22e4900e92 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201712081631552_Liquid.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 + + + H4sIAAAAAAAEAOy923IcObIg+L5m+w8yPc2snZFKqi6zPm1VO0ZSpEQ7kshmUtI5/UILZoIkWpER2XHhpdb2y/ZhP2l+YQDEDQE47ojIpJQPpUoGHA7A4XA4HA73//X//f+//8/HdfriHhUlzrM/Xr559cvLFyhb5iuc3f7xsq5u/sdfX/7P//v//D9+P16tH1987eB+pXCkZlb+8fKuqjZ/e/26XN6hdVK+WuNlkZf5TfVqma9fJ6v89dtffvn312/evEYExUuC68WL3y/qrMJrxP4gfx7l2RJtqjpJP+UrlJbtd1KyYFhffE7WqNwkS/THy8U6KapFlRfo1bukSl6+OEhxQrqxQOnNyxdJluVVUpFO/u1LiRZVkWe3iw35kKSXTxtE4G6StERt5/82gNuO45e3dByvh4odqmVdVvnaEeGbX1vCvBare5H3ZU84QrpjQuLqiY6ake+Pl5f5Bi9fvhBb+ttRWlCoEWmPGH0JGM5esXpl879/eyEA/VvPFIQnXpH//u3FUZ1WdYH+yFBdFUn6by/O6+sUL/8DPV3m31H2R1anKd9T0ldSNvpAPp0X+QYV1dMFumn7f7p6+eL1uN5rsWJfjavTDO40q359+/LFZ9J4cp2inhE4QrBRvUcZKpIKrc6TqkJFRnEgRkqpdaGtxVNZoTX93bVJ+I+so5cvPiWPH1F2W9398ZL8fPniBD+iVfel7ceXDJNlRypVRY1MTZ2WTWPtlDatHeZ5ipIMGKMBWbZM6xU6zRaYoEw2wfjK86QsH/JiRQoqtCS0DEXZIZycsJe4SqefvsN89TR5I59QlZDlQclWztLYO1QuC7xppNcM7c0zVx/xmiyL1WXOpEMZyskXKFuh4qD8hle3qArF1mD5R55NT4emqW9FsiG7dUUkotR3m/qLu/xhNG9hIz8kzI2KCPKlwHnBRLx+s7AQHpfJbeS5+P31sJXrN/jk8YjsXLd58eSzzSePrzgM+51e3ZZhj//LL79YTbIje73D5SZNns4oy7swqjX/LFBVsbE48w4RCTf4ti4Y9KsWz56DvDno7TQc9DVJ6xg7hWOzjFZm6nrx7Md8maT4T7Tq2vTg3hZHw7wSwj0bq9tqpsNtagEVK8lu6+TWkUUAPHTqEKHP+yKvN/ML6L79bTU91/r+nNzjW8ZAipl8+eICpQygvMObxjgjr6yrAfykyNcXeQot6B7qapHXxZKOLzeCXiYFU6/9ZErfrUBR0uLZSxB1W4aN8M1Ey6WdmZbq2p14ivZJ7X/VaIHyI4JD13qEg9tJmtyerslgT3CKDOT+zW60hiNulYaexyIfutlaKu/Cz4m+KrhWZjLR3UzGBSqZjCvVAlSAVMtQFWAvG0diVAndCV1/7UxAHUVBE3DuJaxZ1oVqVx2tt3N06Vrf0hHmtKSr6zytb3EmCRFT1cu8XkLCJ55WFS4UQN3KKEK8hMI5Kta4pIvzAi2ZUd9ZICzQsqb2ulcirr0gULcV6WbK9fBvcyv29rffpmh7sIZO3rJy8R4x3kYFXVbwti7y8JVQZVjCekhpDRvAgxYxj8rHYNhWL1/xiPar13v1TrSCTgqEFoRVN6y9MOX5Mnk8fkTrTfCtF0HUKuIUjzAF+qoHywrfB18+ddfvDfOH4YoqH22FkigZJhZM4onDUo75aRc5Wf/uAolWK9m/eyGkbivWWWKbqkjrEzH5hXk0owO9Mz/LPpD1cc5U+jBsB2maP7yvUVmRc8nXvApGGGATke6JyLp7RxjzS9U7NdE/L/HaWPc4W3nWDLU1+R3bqKQBj2mjAlmlG5VCGpxW7JPaB1n5QMSZslNN+VUjRsfd4opkkS6UB8vwBlmQJG9Q7OW5RkYRKj1PWf65Xl+j4uyGSrAybABTGHWb5RO2xKC1Dy1Blz4RcjGDjkbpE6Cu+MU47qwCDJQNKthgOcEjDpIWPKJoMuPFYVKitgOUup2i2/nQGVdnQybnNWqxBUw1+xDbmjglyAQRxfyw3yXUbXU0el/jvtXmt+u1Z0naNd1AxriCPCaznE7eioVXetyGTvJinVShG3aHbZGk1eRdP1itcXaUr9ecx/CEzyKi2ZgObm5wislyCaV2HIvTO5SiCO8oOsPVwXKZ14AL9xS2qzh89DEpq9PNwWpFjmi65wy2DiMGiVcgKinPMvA86exsUlYf81uc+R5QSX3GRURUK1F4KwQtSRV3E53ov+LABjVALpV2fwDEVW89xGlKprmfe103RVigr2MQdYcFONdei5qertstzNWg0cj9FmEkLVsJCKnYYTdWPWqlQRiGUBPbfDOl6/HxI1VokvSgru6oSrNkQLpTjq4GOA1WFaQ5savlOkFEDajX53lZwWPri8GByKVSrwEQry42D0fVfWTl6k6Oi+FeCjCu3WRnfriHrAjs3LhE6pdQ7NqlC0TOGIRF/sVMtGDXRiBgF2EIqasKMPcuPyTF6jzHWVV+wAQJvXEH+y3BKXqvhgPGoAF2HUl31Wm110jAgPgTYNQCUAR0FYGLu5zVPyKH2FOilMF9F6FA8iuBJNqrIYPMOj1BPd4hrdd59qpFsD/Tq9sih7+6izTwQ7ypPsFFWUWyRZv18VkaMlkx4rRC1swmyaZ/j35ET5yF/FjIeCdYIfLtHmdL+SxuaJF70TvZsFpZ82auht5O3tA/8IYqf0lqeJwQ6Zr8Ls9Qc5szvYxIHmdqKcyAoD6aNWsI3Ns7zaGHGXZ0oUjSQsRyZ+WDX6TazgmQchdHAMqOjqHCbhE6cjkrG+9wgZZU33zV4tjrG+q2DBvmRO+8mLNL2VpyonjOlNG8IR/yj4gS8bSc4xXY5V2BkG2Dv0ZokAhaVOCl0JinY1B9/U+y1C7zr0mwzXoX3oLN5YF0QY71ZA4I7o5rP6HqLlfYxMYwV0PlRrzhsYXPDA0cF41VnA/s/EagGNZorwB2RxBA7jwIFXTOHdPD5xlMi0CYuv0+pBFL+n1oqtAtM4bGiieXTm8zQuujO7oSJpFKnGCZQx6J2qSDCPNb32NlN0DBHGHaL291WyojQ1yfxqm01evrAt1jk1UuzhX3LuhBXudcz/1cWvzaXT/IIa2/8o3gl9bi2q95zZpvSRW66G3cS2xft5vccA42G8J64YsvqqPKl81qEqNVf8cU2wVCdZum9JXwWtaHaX7be7A5L2lau3zF4dgN3+K2M5focXq3PDp4alGO58XcYQRZiqP11QA4sBNULrESCBTMRk1XAniIItjvBeq2Yj1QjHVn6dhspMDQPobNltmDYy11O1uL74j3M/VUxvMqPtLL5Hb6qNjbedBoGfjb1tRhbmzXAn/HGVnkGGQT3Qhy+xRsRfHaDUXjp3bLjPIetscKvYmVCsHtegwRFqKoyFf1srogp3H04HOOS6qE9OjVCM9uKH5tl3Zlg9S30hBuFiX1Iqm4Wz0/onxA6eamTv8LlZeEU9IoyD7nPrjU7/+a6Ycf//HcetVDcs/+IAD5wR8I5fxalcfS0iIjh/NjjcVYVedKWM+KESlryC9a7aqFPW7t6B8qffbnBaM0pM2FP6srSJss9DSLSBxDyn7FJSbQp9kK3+NVnaTpU6gesp0LsMVdTjThGfXEE9K/Odub9Z1jx7VovUkjPFGMG2CmwxPvHnJ/oInzFJcd87uHVmyHinbab7SnRb2OdtSPhLFDx5QoYdDBfYyHtPdwOljuWvDsxfd6+kWXZPVNsqQqSEH20cropRun2fcV1i3wOI2clu/xTXWUFMGXPR2eGNoKffSFC3RW3RGKN9tJhNxmDOeg/BgeakcRajXVjenLSqIbHaxWQh+Cx3RavssfsjRPwu/JWzyhM/clS5sF3iEMHuOnzi3+7EbC6RlSqUVz/LjBTVKod8mTiNMOBXv5zlDEYPsPSblIiNaEYs3qGJvjAxXSGxoS5eC2QIhXHH07M0I2i9XktLygobiLCA7RPaKjp2WKmk6Fyjge4zkqcB68+nqcbO9niAPXyilzKT/OaJUIAT5iRuwl8hTTlZekHcbGGbC3X6MlXlPb1HlBfrUppP/68sWCxo4n+6dH96MFcDktj8tgcnL5EEMZh6g49F4yuydLk6BrfA6DT25Vvvz+9zppbR1BIrs5rjGMB/cJJnVxymENdA8De+q9YeEs4sg/5g/NqNsYK8HOg3mFb56YQeAkL7o+HiJy+gpDfJgsv7OEpzRLenBcInoapBhPG1qSE0h/6A3WKNihn8wSXtfrOJPUYEwe42HssCwqtImB6YlevxQ5yzPf77vUKt01NCp31VvwCgl4ojxnQqsWK0YzKOtEFtAOHtZPh3VVDcaVANlCob/h8i7FZRUHaSv8UkQWL9nXRvYr78tfcjph6PAy2L42QhJ9Bz5LV9M20B7Mjtg19ERtLDYET5I6DsQeZ+/XQS/vPTw0eFytn4cnps6cd5xVqCij8FcrtkeY0cRM0Qr2Wdskh69LjJo1GbzhER0CldVBRUTndV2ho3x9jbP2WjMiE5I+E5nHYvdRF+IUh58YviF8ezfdUhyf46Kj/4ZXE2L/MC1t+p0mVJ70iMKESbwbm3iPS+LEvdwhN3l5gPgeFU+0sqP1qNMHiQomXzTbbBvksF3gmxujsf3XKCE1mxc2ZzdnBb7FmWOHqcdTu11GsZP0+D6hpKwLRGmooUCUvKh9mwdr3n81dE/o0dIfY9R2pK2zVYrYFaTBZBjnPqRp7xwVNGJYLEvVCCmlRmycfKizcHs7zs4xu+0yLQN12NZm9++1AdAfqiu9aqEv8+FaZPCBUkNJfk8aUOcoo2N5oPPnupJgJQcuEUTllCbBubql8fJa2+cxoNxhvlzZ2xGQpwddw8bqmIAS2FXL+Er/PxWoyk1OCe/KM93ZsTHJGrwAO8OtxpFRBFENQILz7Dh/za3tOw+o6T8EphoDCOs5jlZ4aYfQwmh6L0CoOi6Cefa5d9OI5/iq9QoN7S+zEdy0EYP7g6W2/3AVzXj0FVTjM9TyHG/zIEcjUEdwsjzlipXilIfxlKZficpM1G7opK/tvqaeZoIsaqlmyaaq51SJqJ0G7jBa1yF6j4s/SWl5cAwoMyFfruTCEZBzqD3Be0HTWRFU7u4YQtlhAcy1y7yVDehuXwxxhlQo8YIMEeR132m4XuFaWl28/7X3vFe3ZfBDtw204H5jxGYmhi/Rl5IeDpdkuBG8pruOyRijmx27plzNEM6+Cdt4/Nr6/5XknLfJMz70oLftUMI0WUyebmaYBy9bZaE8OmCixHP1VDJZHy5zw1FMthfwFXQmiAFO2pa0wK6bU4/DfDaT2xXq6MYzArUY0hjee1SDvWcK85B5GJIlydrO0qJq17P6fXPfJlwDGAcEqB4KCB0WJqq1JgS87OtQ7BUMdVsGBWOih2703Vmklk0XR/O9UTrMK8Kns7aYrG5N1xERW1pUT8MDLF9/OpzMkJiuXfixnv7tn9ZFStBJ3/7GchW2vBuS+kD4GP8psrGFE2AfdfAyJ1sqWlYiql51tuvB2diJdLIweuzYf5Fkt1rfxTgzHPmNbHw/nh1+Qbhr3iDx/Fx22a/kJiGK31eMHj5NksYl9s20x8HQfDsNnCKtbd2NEIaDVvb3gR0QF6xSKJODVIoAYZGKuh44nyU+oRVOXrX19wcJjfhqSHSIs6QYnq60f9ksJJO/7RqN3gxAW9gUR5WR76Tde8WRP6Rd/i6Un+AUZfoj0a+RXlp/Rg+hm8NpeVkkWYljPMeMKdDZcqWcDEW3tBVqPBL47qiVSWNA7uYIKJfvjSAgz/tWJ1cDWRrDEEZXA0F0e0nmMQk9xTOPZC+j1W2RZZX4iGbtInQN1eW/FwsRu/absm3grqmO2vOdKLwUUG/Bp7jiV8lHR2Ed1x/M1FdNYEF7Ic1dswSsWx7NftWq29qOTX5WmyPHCXsj7W4Zafdm1Z0zq+4tjc/e0hjDlh3bmOjtmGE2KcKOHDGUurGHvqzSQeWSkgQCBalIgANEFOdIDt9eZdKKEEauCEcUkewXdYoWT2WF1ga9LFI2jA2a3OmwTfUcKQ1gH/UqDrohweRl3sdhJavWUSMZ0PSH4OlTmXCNJk+UefpMwrM13M2GWQ+K3XKrnjjO0/EjEVS8YWqGy7fBfX06VznlHql1rPPbe0aPKAMSqPKI9ruNui3DRmD5Dt7ZwzrNiw/o8WuS1vO33uroH3O650wdAyBicuayveLXniZdTdjDU91wK/aAa7/i1G1FMmSPHlaHIosWUXIZM9CF+onMlnNhGD3HZk59gVPCgvzrzEBnNbxCl3f1+jpLcLBrWZvRZHcMPT+ihUZtTumYouERy4AQQq0rfpPQBIdQVzMHitDUdTW5CNEspo2AobizMkbMiHjRxrVlNQD7rke5cTNNdUy9Q0C9V0PUbQ1ECw4s3BE9GiJ2LgjO7l4StaO5h4lpoOZl4XZkqXHdWsjhkJUc4TmbgGm/TtVtRdLwYz1VOS1PiNZTDwlXtqiPqUON9RxqES1qAFaHi+p/KNaeDDjBPu8T38rU35g7fCTXGgDbXj5MLh94cv8UMmLMrZaR2caV9OHZRn8o1qG6wgTyIyTGnE3/Y8oSOORYuFSB8e7ly+TyBSZ8c7MXJbNJo+tCUWBt3nm2owSfem7BE9i8mGOH/lMscMuAgbYD03GBx2j16GxJoMPiSBctqiC5qB+qt1jUod1LRVdxFpxIKo5XTuRAFlGiwcczuzfR60dXx+FeRfMFZ51BZrmFcbUSf2ECHiSTvl0FnoE+HtUlYe6DYwIpHl1+7yX3XsYG+kGalOK4KrprzK1kiapFXlQcLiZTxgU+WLsnPB/w4McwoB6XuppFshV6bIQL/eB+Te+lVsN71BaEr7grhQjwENPCZXIbbkcgSPZC1lvIxnr2Z9LaosWXV2hUYAz6EN7UxHAP51kN8j0vq9tafK8ndxl7X2Gdm1ikh56csfaczLYxDVikV5gx8yG6ZhfUY3NMJmjwmnPMHWhwz9jN3GEHZYlvM7KKuqe1cyQR3krGvNOSpQcPd1yMYz8fbA7/uU4jnF8MMi9esnWm+Z/V1dkNQ8oOJxFVX9v0XLrsKIbMXbZVVbZi6/oT3PlNl4XGY7C+twa22Vp0bRsSudhW9Rm2Mf2LtZY4HkTA6yge0V73U7e1lddRdk8HYr5O+nGeQkUN7yUu6vhHsP3aU7cVSXFq0US7o6Mx6smn9Wb6SPWnZfuwNvjRC7cvZVWRpxTbToZAc9dofNLLWW7i3gqLyHM+4wHtrrZ1XEeov96yGKqImF3POqUJZDWulJLXSANtfduEgnokMSx8ETeT/S5isYvMk6VjO5eKc74xjXuBF08VfL6XduIqbq/rFkh/bQHA22wZNtVUUtKqblThODQUUUwOSPcC01uUxboTCFTKo6heUy4lhQLmtAwDJYnD0KU+8AvQRABNZUuJosMwgVyJLlP28sR+qQ9TG+72vwOq3V/i3HFFCkvOAitNTpLmXnL1T7KI1mjC2H7f2I3qDA2FPRWfTLdlp9EYppqPOPvOBQ/cSqwgDz14FzYwu33cZguMaYluvd9jm6MZ2v1mppN8IN1+iJ0sjp1gv5EB7ew3sp9oI5Nt17tiBLe8FLCzpHttZ+/yhyzNk5V3eqwOwX6T0qzblkbva9y32vx2jUlXog7XlyI4ECCAarJdqGtrqhSO9BqVTAZFO/lYLLIpxmno+JGMqZzjMmGft7Gt6p+3seNwZeJGEEDaAmCoIBl/iVHR+nx7n096HHs5r24rkhoER7p31df9sxvEcTN2fGXgHD0tXdbNGmzSIIxuGXtuBcD8Y5hxFAUlQN/q1Rh0EAEwhCQDFGAxvZGHJnooqJvnCv8cGSKGUeUC3WP08AGlm5s6zVBZhhtUJJTR5NcL+mCD47luqlpN5KWNpGh6F7rUvyVlO8Dod+FNB3UHJonAV0JV6YhkqKE6FJmqBTEg0X7Ko3ztmVmJ1n7FodgNHms7Y8rsF0eB7BpDj9N7YVJKe4XJVXJ7hxFkdG5irwbAga2hcomJQaBglvXMEtHz6z4zhMG6lWS3NXRqcHVRjrQG3YNGFDGWo6sHGBNdc7caLR/BoiLLgR42wbOn4cSerTxrNvlKGwkRnC/iYLMp8nu0avEdAa9KXWM85VV8pJFTj0ZN/7DP+Os3MuUe28lS5R7LtsQBarzBjgrB3XUM4XpW4vZn2E3LSwsQna20qoKXFkBNhEWWpAd1dUf3tCY+ywVaEr71OT11CZNf6RDvVQaNEGopGKoyHK+5VD+TWpjpLDeju8Ha6BNxm2wvM2ds+YzyMuO+eZo6WC7JMXWeBsmf93iFiimTyRotY6Dg1AmSq6HmIEmtKkhbgF2toBPXSV7U6/O89DERsLrlqx7FXoSq27rMN3gZy/4d43Xp/IeZ0/OD1apgFtCJPW6eQ840rXzplxQoTORSSXIAIK7aI0PB2NbQRR4Q6uRQrukmBxQuz9rOBAk0hmMv0dRtMSrtjESjsxXD92tRX/8TLXXS8S/TxOP43CyEMqz7XzE5gQUaMJKyoj0J9qJr8cSa4g5fI3fdDFE/xobQiEfljjAuhmWtAOO1J5g62AKBvWM/dV1rAMK3gEDpvxf86rYYgd4Xeb2ZOBPB20gBXEW3vRmNkp9bvg6UyXE2BypBo5xDfsQt4ufK2DysYbU0v+KBBGnOlcHSnAcIl+ZtJ4JEOsOxl+saIfPDS+Mfco1Hvn/Uiwz4HslJXIj3R0p54iUu2rl1lhRNxNjmf3shoW6LEcgY0zvShQNtK/h9SVTP41A8hzhNCbFaQ2iwsYIswY0GnQV5F4Qt6mgdiYPtPHmit8lRkTWe0lPeJCk45qguCpQtn45IzRkabRq7SKpBAda7qf/Vey1cJo/thhrD8PY1MacKiChWFvV1RURiepot00uKdiKf/lFjx4/zNHZJGyNzs6TuTHONcNToPCNtpc48I2wbm3VkpCGHpeze2Eg4kl0EUxUhSU8Qmpqm6panJrC65amp3eLX3X9GlHCTM2m31qdthYmVC9LOiosIOWFTkzVBdHwyCLRyTbXi3MxDUqzOc5xV5TdUIMLh4e7DR3do+T2vhwf6c57apcZnSU7SaTmx3OUPbm5wipPwMC79UWQzOQ2Y/zY9PxH0RwUiovKI8NZYTfNmKYKJYph+ImmXZ9H4JdpM5+uclN/RSjUlk47w6P7+7SwNHT9ucNG8cs2zIYHWTG3+F0qmpye/vpq0Ke/QNQ4ONMChOliyLfpDnq5m4A+54ZkYk2v4MMm+z3LWFtqcRcTwbZ4ezdkcexkzBDmZo8nT62QG5aLdTZkC2L+WnXrd1+TsUeA/maRh8UWSJf05qAazNz3LklE1foFKLs3OhBJ+Q2328xJcbnSm0S7q615Hn3fI53WxvEtKNOddwXmCfV8pdsYWIebGZPPSNkdNAUTgbOqKC+Uxo4H6HUpRhPh9u3sTyp+ELxC95eMsCFY3JB8SGkKqNRh9zqs+V3go0egrmk11eYdJ/xLymT2M+pBkq7N7j5OV8sp2fNsEXt2yNXolAg7Xt1C55PEBArn6Fmr9H5sWINfHcYmia94Oj52N60uZ3KIPuKR5DeFIWQDgVXsZzYXLUkJJ1+IaUOiCXDeI9/iGnRKNg4AAr76UaPUNV3fSYMzQ0qAsqrgOjtWib041/M2epEr9F4qkzorlXj0jUkORoaovVvRsKIJ7xpW79uwCrRBaoxUvIY8b9R7oKA9lZAojsDQYcw3X4dEdVv3ouSuVyT4ukToqFPv0amMljSVIUdoJAAqhJ0KFxVQFBaFHctgGTQkKzL3jjbqt/jo30HzcSLVQG3SQ0mfcbe132KGGYZPtPsqxKbXQrvpCK1OmUBCsOj6WXF4rHdYWfB3qIGz7Za5uq6NX6Aod7aAxkLXJICa6tZxIoHTUtNd2hxoGRbf7KK1LPbSrQBEUrTmUdqsBwTpdkMAJFzJ7waJuqzPKcVIBCjdoJ51iOPxN7AdxWnadPVhW+D6JYOrqEB7l9WYmi/kFIcaGBh+fxSTYtzZPoJ4Fyug5do6RNU3NM6xP5LDFon1N3A5N3N5xB7NNbtuYO5EByn1LtjI7SRu47WhgSaodz5WqjjwiBahyV1bBB23JXA+DXtLsY7HaHMYJkYJfuUTR88+3mvJI1HwzXLFw7FO7IvcNTe55PMdoZhnJ1J7SnW2j0RGnJtm4talptxUf2nl9Z7t8LxHC6Z6WHbJoavxHskCyIemU4xGISusm0549hxjeBNbZKkVEzUqm95JoBPwRi/cXi7+VitJBWeZL6vK86pQV+NYjopak0vxMWlW4fdfhohG8rgEuIq0VUk1OlAH3uZwTRSrUdO08Rk6U/uIyUK+kKPZ6pbqtKNpgM0/BEsl9M2ZPSvPB2WMnz7QGSWB3sQ8uN+Di32uxARfrQdGzAXz7Najhu0ghAxqCx8BxmKRJNuQv878Lmth0O5cBbSLJoHVkg7xdILc2HZwkNrTA8W+cIjsE2Ywm4nVT58bjIQs7j5reh2gv/9RtRdFBLotk+Z1QfCYvcvYCOO7pjvEM8vVNf4dSfI+KJ8/qs+s+1s514qJX+N65uvypHUF5iKtBBsgdHAEonQDHUEEhq3iUMYTS3uhuXpGMTlE86Xxy9sWxlxsXQ8x1oFqv8GrxPK8Qdswu0L9q5JWOojUQjNDs14FmHcQIjBZtEcQ6LsW5fLpASZlnJ3nRcNP8ZpCWfxEze0e5IAjrgP5llt3UipkAp/P2qJKbm/blxdQXK6s1zszvhv/yS5TEJiPZFic+3g693/M9UXM0UZylIQjg3AmCBR42c6acHZFNKMyfQsS039l06z/CznaeFK1+43hD2NzneVTkpziGh2U0C2Qct5DtxG8iSxIVhIeoW8RkFss4SsdzEMUDcytOF2MpdcXD82cMJRhw0lDDRn1jLDUE7SVKIHO/vS2yuvtdqRXgmlcFY+5ylEtfKX6b/yYoodrvguq2DI7VtrmQXS226LEin9ab6SOcUAfof9W4iJAendrQKHzL8LHwnpaXyePxI+Ko4YuKIDoibHCbF0/RNuKjPKuKPI2ha8RLrXBaMk8vFEqwyRIhSEKIPY2Drc4w7BUgEgdxbVtHsk1bVwwyV8OtRJTpDN9esKvbkig2cR6uiXYKppIfrP5J+Ia3nkRXzpv7vBkaOi0JJrLs0TKCl2qAQLWXXPPLLFHldBZ2nlcLy7qgfN3Gcgp+1q5AuJda6rZEkv3IgSzEsSoMlyAPXcmVeVOmXR3AuGlZMeo6i7fA9itLw8RPyxQ1W3PgaqCIzlGB8+D4E8yThuELdF1cVGTale4vWzpbRIqdeJrhCifpLksyvotWUuxqXEMtukaARnk1hna1oCn3/7nFshz8zFGee4llxvTk08f81kMik1q31L+Iw7KXxuq2ODLt0iVOvNjWuyGYBDKDS5mDuZLgh9WrAZPkkg426m0E3xB0EQGVa3sb50JbImMMcULh9yJF3VYTFZr06CEvdAGq30xjqDGYhyZKDHycUWBHHcuaj8O2wv0WqDe957cf0T1Kw9OK5kVlfg1k7Vzl2PwJAZ8tmM+mj/YavNA8FQqTO8wt+lLo/Dbe/BbJx+0GFQUqZmksqssFlQ7aRyoTGdI/VNXGmLzgTQxyfSmN8bps96AoSpJKOdIqRfGUIZa0nM+z4bGlNGnVX0mo9vuLui2eTsGRk2IdsNgMhpvNNni5o6Yg7UqU+Bdcl2ooaZVqQIPW7HnBosj0+7rvgh3j2a9WzWqN4Y1LeSjWSh1iSwQrqPX1P9FS6/j/22ReVvObcqjDVRLBKao1nx8+NQnBIiLsI2KG4pxIhvJsDMrRsVi5GsMPglQDJklSHayryYoPy2LuPQ+t7PsAZOo5Bxlmw+pS8XpI/75uOfzcC391W+2pNti9MM6Fm+ftn9oIrcnl0rPHFZDHRSqUTbcSRNhDpOUdWtUpukzK7x5sT6uVr3gke6ZXt2U4ItuaLlyZm7CLznwUy2KSZ8ePG8qR+nepb+I8eWxuGZSt/HWHjM+g6rs5y46LIlzJ+Uw0vova5/ntx4S9FS0qz7rH2cq31Xq5JHzi2y5PtukY7LT8gFdkYYdOEPnzlq6Kc0TkuBT21K6u2docadAX+UMrrfth4yyhHhFHeUZdD1C2fPrE0LG2xmsO7oCHQG3yn6KVryK3TInCn3ulDllQDwuC7lWPZL+jqdtqSB+qxTVYtuNt3h0dqHYVOo5Tos+mEdVIvm8KZ4CWR6/GoLw7AAQBOASAYEGq5ZciZBXmr/r6+wX4Iy/ARVrfzt9qtNdkSXZbk63ZbQbsc2BRxsDLkCey1MErz16JmPaLaupFRUb0vsjrzfzMTVqev9FRIsD5bM8etwjWq+/yDq3R16TAFJWPdYTWL1+N0OzXnbotRqgInDvLq8aw1fA2znltSu5nuH10N/auuvnfntsn50PX+C9a96mpdLwyjWU5o8dzg8NZnBX8IS+1IekiWVw+5rf5OV7SFbA7sRY+VOv0MF9xKtB08ZjyrCLrpQto/BlVD3nxffLZPS8wEUxPbAkftXat8HhYDOfx4/KOnAoQzYTljVoT9EfZCBwIiI7wSluLiwhkApZDAxlruEc1kmfGPDIBXDGkEZR+LGPQsEBHfbec99J3uEBL+vLrVYdkv6Oq2zLer01jQGwmxpD397dJgrHaJ9v7q+928jGnCMLJamOGPcmLNZl01sK07YVGARKFFl1g5d1Ww3tM6cIeFjVQHCOTbcfZiszsDBrWRV5nqz4edRlJEWVYP9frdtkFvmYf+sgeyMfs44D1HcryNc6Sarg+jR7rRmjyouZEBwsi00pLBkcmvAGIeGD9lLBQhYHn1hbLfrNVt/UD3DxMaDxp3+d3z2+cmbGtX74aIdrzo7qtEaGal7FmtprsfaBdEt9Y4d8j7B7WnP33GtVodUy4ND2oqmR55xnQp/VpKV+BCPecrm6LI1jwQwXSEzIJVMcfsSrdqoUZkUBdVeRkCEM0nbJ3ggE3DoM6ymVY7rydbMxcQWfRiR7HfyL6uMEzcqqW0QonLY+YJkD9DAIrHF1AGXHVgA+2HjWUZOvRgLoarPiRO3R+XM00CB7acjCjKq6D4lA6jGlUyzQk7qvliPgaQRa5UT+j7Fz7/Uoj+QucF8FZECg7ze88f5nP3+YF2qRPURo2WIiOJm/icLmcvA3zY8tIugW9EZv+Piym2W5BFuhlgYNjLRI0Xhm+GgG+XNK00MGqKspWn5KsTtL0ye1oo9vnht0Ffq0VeZ8T4/zZ74y2A+JJbhrR1RgYHMgIRrdPjwGDtudxt/z3Zx7PfoM2LFPdkeG3SW6v2rsO00nJsnGzN8f8IzzPC/HBj+vFS0lmMg6BnM/rZWmIKzdRy439alGmzmJeJt47dJOQVU12VbYWuIuayGaxo2S9SfCtT6ifXl51OPaySt2WQVpM5WVs1DEnajiSzrnNu+Ogq3br9dcuoku0JnuK10OJfhkKqPar0Xs1TmRr/JEP5fRxLcu8Nf2TffqYummuQDFkqtEU0OjjTdgK7fjiRLT8sQwDsV6sxTyTR/aiCjriKw/2bzzuhIbab4Nq/2qsbb3BfUYP5UdE5XNgZMl+n4Mx7rc7dVswxYJDTW7p/B1HnsQ1WE7okvMJJSVh2ibtW5Av9gjTfr1o1otePZwoLv2Ww+JfUNJN7ZXt7CDsukzekeWblX47jLRSemT7xbJfLD/SYjldb/KC5rW+wV4vxEf194tj1xbHSZ6uokW2d22bcARtM46HdRxMcZ7wfsebsI5cJt9RGIbmPctZFn7OJIvkBKN0Rf+a4TFLxxVHeXaDb+ti7LY5le3h+JEIG95PcsLnwGm9zvpHHBO3doFKIk9PsxudXS9OU22MVIKcRkmN9x54HMAVuO8fbTFXY/Dhxl8NJd35a0DDQtE+ZUv/1zuUO7v3x684VPtNVbOsozzhafhDH3PozTRvb61eD020P7PABY/VlrR2RvMPSalzqf9LvIe5p46BS5taXZ+m3jNYY3QDlzZz09knW/pZruxdmR6psHyHNmnum/laRLGXaOq22l0pVKRtZ1HHevU7n1IzMGWM84WFr1Ok4C9m16Y4DRnjFEUJVFRVm8siyco1ZrHXY0wFhHP8OIwJJRjM46DbWKEMT8TizMmivm6O9ZO3ZH3dHYkRWHsWqYciDi7ONTJdj/gefeIiqAS4/Pk4DmqiEbWmPeBkJW7MVz3wcK5SwUinKiVgmCf1Y5h9clR/r3io29ph++RE0ROo2Ka/WlJNL7ctn/RHCQ6X32NC2K1GETgt202xW7yBbj+RjLdhZiyZhwjvzHFGJH/SbXEOY2nHPY2Jdhar6Ywq/9nNTYkCHzUwt7EwFIdJtbxb4D8D7wHOySJvwtzujlMdTX7Cspq56I/RXh7+A28OiuVdDMcgWmsIfx6uiw3KEfxWL0gfE1/mGRW3aAb6kY6lNNCroRSqZHwD/WGy/H6akfWy/B7ognhEhGKa375SYNwrmuq2PDzkwC1qVS/DJVWkdLnbSEytYD0wPbUJVlqBxgruQXHZhDmNpK9jHkgLaj2ODj5InFBP75uERb4uAp7tdLIEQrcXJOq2tnNq/IrRQyQz3675ghFGRLd58RSBl0VUez7e8/FsfNwK9whsLGDac/Gei2fjYqYokYnolCBvJh4j2vOwuq3+VPEm0unkbRie6Xf8Ii9LooOn4Vwmotrz2a7ymZo76jXHG3+vEwZG3cSKPG1uxk/LkzS5LXu83uwCYI/GMUSykwWTPlEbP0eVMSk/ofU1KjqbxAZnGV1jLOvZHy9/kSg/An9HpmGVP2Q9/BuZwg0tNfQ9SZaoWuRFkyzCn7ALlBTLu1cMXfmKx7pFgn7AVUkjSNtSlEEdcPBv9PAfk2uU8vCyQ994xkaStK3zq++sdfrgB0z94aJOHY96i/N3dIeW36/zR2q1t5vBxjJkO3+f6zVN5npBnZ1Vc2g1H5cYFecEEzpK0mXdmJa6mPXRhJW6kS1OUZu+3m52zlGxJHsTfZrWVvhNX+Fg9U9Crcbhs5vSXzzmB06XETwzclopvoEtzgrrxie82uRkAb/j9wjDDI0qftnYLqSD9CF5KlnlUWsGechV49ryEYimIOvBUy0EQ1S1tMU5P0zza9tppk4nRFFFlGeR7SQ3J9wAKalzdgxfi/zDFXVL21TvML2QPmcRFO2m6RPpCN6Q7tKEXnSAo8pW6t5BWeZLzEjYKS0sEVljorhAJbuquOryrwv9P85WL5orDG2t4cJj8GyFKrx80YyIEJMo3X+8/L+k4ds22F8zcw12QxAaeTMeE2nkLHuHqGfAiwPmyUJNzuUyWckHHULR1fhLu2hoBERyZigJexA5KR8FcbYk85a6DEVAYnmipJ3smxNL3qENyuhh0GUObfrR1YH70zcrENNEu99fc8xqwcP4T2ZOYn2zZGCwipJ7eWhn1oWben58qx3HXEyrnbdnwbFE9W13oQu0zItVf4dNR1kquVZfDeJcsYYL4xpaA5iXBzC05EKsPE3NK3oEBZIip/u0w/BHCJ/VUgW7PsPqBOfgeSxI0vODrHxAxRXjEx1TcHAqPmtAXLmNRwzwG8TAu8FrQMdn4jZgLmxapvBb47XFHWaP6BtrzRXRvYgOtqzQ6og6urLEEiouMVeFOHJcy4UrLdqDtgFWaFKSXChGTn2IeYCTA+pVh17ZaQgapAsP6EQWsAV7Smx/1WpHMMPa1c6RTfttla0t4taD3MiMAhzEhi2ICwOKWO1Z75dXr2TDjhcLKfowA/MoaPqc2GYsekzTPF4tcVlojBtgJK2UjM9OYH9mZCqQ1jbtjypujcF6Z+jhiYuKA2RQiLUG12173gIwA4xlx7Q+Yz/EKX1M1zVg7OYYPjoVBPT2pJBXlwc1WEidrBp88E39FSvo6NHC+pBFakZjUNg9Bco0ihkklmm+rPZD7rHJVuTVYZrf0nsMs4FHgoT4sgNyYUgZ8bMy9ii7PwMLKufkWRh9aO+P8jV7iNgzjo5LRGAVB7ZwrkwooQf4UMXgu8GHqhHMxIqq+bFpvquzPRMkZs+sximsleZCABg0RjZwTpZICDXAieNU21MdC3S9mcPGqKGzTfNiAvftcFbjktuNpeMJJQOA4CB3jSCdmAxuA7J6w8i3L+/0Q5iDN7XzZGUGb6rsDGO2Dv+2TCO+r52CMYU3uXIbu8+Y4yFsgTHH82TFmMNT+u1YUdqnokZZKQKCZ+UWxumQLOK1l4zx9l5VJ+Y42yro+hyk2jtcNum6DzZkUmgit3Y0ups9XSWIqTp4F6bStgFZX+wY14E0fDgB49qCgCFS8HAu5ADxb2Od6Toyw1rT0fl5rjd+RC5LblRvulU3bgY6WtlzdBCd2m3WhURdlemo07dgr3NFoEn34wL9q8YFauJhGTsN1XKhTFRl0bZ/AF0BOPMkesk6K9LNIPSsSGTTj67+tg9R3W342c1ZgW9xZjpFifCaY5TH+UnCvg0PBUNf5jsJqWht0wOh6tbZjIgnfI+KJxZGzMQFPHBkBhuhhkQa38/JWQzqzYz8BdHZSnhx9bbNWYd1tkrRaYXWB1VV4Ou6Qk0k26uhxMRwNjg0fKis7sGgVl1RqzjcmHfVwOQywvnWggsLWN0L9bV2Z4G0Q7G0l6rqWS2EIM4X2nuGRlTTWLbB1/As2vPyto2r8oDcGXk+FvZh3piqhborW+G9Z2jMb9vvTcq9WdPABFIFDbf52PiVzTiYYndGUCpHMR+XKufL6pjV1tkZLrWUiSL8xDz6jLdy1Ri2wKDPV4iO7gpGZmoDBykrahjW92LH2KSjyX1nWNg4ovl42TifNl3h6+0UZ1sKX6jOTPz8jAWxbhxbYuDnK5AXG7TEN7iJN9VbPGwZWF9bw8pwRQ+mNvTgGbK33YjmY3S7OX4OLA+P5KzJnqHgSBX7eeACn5Br0Di9KPfoDvRW02pZbn+pBAx3hoUTwBs2vYMx7OhGomVwP9muJ+/Wthxtt6zXGrz0t7/iwse+9R3Lhm/811+DZ9ursNuWL5NbTRgwGTby5TqPWa2CkeKIUb4anF+TAidZ1U/LUb6+xhkDdHI9sMWjIZwGhQdNrTu0bV8G147OJxdc59SmZ7vkAaEbn+WBzgLFTnD8Mz7fOQxrN5bGMzzpWYyqS9bxJcNBq4LHsxNLY9QhYH2MBr7NzQDq6G5wPDSnNj3j6+0a7/vuAB5iPwJD/0gCfnek+jMW5cI5q1yg/pRhNtY54NCwOVDdg9OtOqHm+h02ynkMcL6l4DL3DstiZ8xvoj1Dw7NuDKrBZL9WwteJrhsWq0W9ZHd13VgMeGurx4InfNbQgGXbq0m1cVpvNUYE21Gnfowdxnp029e0foi9RRwcyxh0peJWR9bUInNYJQxPhKWi74962ZhW7c6uHqsBb28lWfGHw6oSUWx7cTkZomytTT53NjtlN9qyceh5W4Bo0sQ0T1Z2oQBBaDAIQQvoFJ4BRL61aIDa7szAXlpa27S/S/EArxYJTUHYs4VJwIzBI0svATl0Capg3/iyC+7LjNILprRNB8Y1t8ZhfXLiUc4wJYfB4BCH9ZAuPKZA75jAjPHZtnVA/VBmYFH9VNl0gK+3AwxqulaRICdgy+d4WaLs/axM+HwvRC7QPUYPtrd6Y2jN3tsAeuzAQgvPiRW1I5hv24bn6Nmx5AeUbm7qNKOpSsZMZcVByupGpuVqevOvunU1Q8NLZsfY2jiwufncOM8OjN9U3Br7f0YPJQtuYMxBIkFCTN0BuTCxjPhZ5SBRdn8GrlTOiU3bW89BQnvfpa3oGUfHJSKwigM9cpCA6AE+VDH4bvChagQzsaJqfmya7+psP3ucXT5yGDx6/rQt5R0/fqxQkSXpQV3dUfI2D0aEXOhK2ljVhkilq+hCPrsOPKuka05DmmG9O82xi21kawLgJC/qNUudZGRwGRTi5h7KhXUB1E58GsUarO7EDJylJu7zYaPLfIOXlnw0hlUyEgNz5iQB+ZZYCe7FXLwEE/j5MNMV+/d9kdcbPSdxgEo2cuYgHinAPlzfdm7PVPV/LsYD5sOm6aHWLgixhmsshEwz5CnEV4NZxXw7yndA1+cVeKP5sOa7HVC/OH4xa0ncgOOrYBxyFfeBfL0jLKgYw6w6nDw/Ns2zCltjxbNiZZ9JHQKGWJHBubAhiNg+gXok/U3XixkYSUddm+bHNbfMUcbzwBgsIhc9T5sH3PfZuO5Znhi6nDxfyuQWfcCkN8VTn+hH7Uipq6XL6sRX8Ml9BTeoSdO0e1xqNZQZmNZqDm36sfW0TuBIGsnnxE/NMp6Le5vWANYFZfaO8u1oENti2tG82XSCVdju5s7uu/Q8KsApt3fXC3QR7/NhQUXP59rh5bl4Tsxmcp+TICdguOfoMqfs/axs9wxd5d7jm+ooKVZX56TLd0mJVt9wdTdwkIpdDPUgtuyquHClqRmVWIS4P97DCstezcB7ltNgxYkghq0z5kiJ6FnIxC9gLR1T+mqN+gYB9lStgu3LUKuhzMjT2jm06UdXZ7d4+Au/xNwYeVR1Nm4et/p8FFH7wWyLqcH5tOnMqOJ21dbPeYVszkgDnFJlpSDOKiuH9/mwpqLncymr8lzs/hnpAj2Q5XOeEwRlt36MtnddJYgNAXgXhtQ296ys9DYjmYFbbebvWVjwoYHYKQLGmtZiDzz/uDTkt2DkZl1SpNzhDXM/1xNpDAYmNmkhnJKYjLE+n+0F7vgM6xWeh93fXLp+N2fmjlVMfDGC1jGdqy0ObgDK2aHg6t1hQXAIM3IiOEc27fcItuteQLuxsfZYEaAjOhuImO19VuKZ3bQ9mUt3VtHYlqc2O+C5coGqusgu0L9qZPMyAgaH1QEO0k1zBpt4ZjqzbgyzaMu6eXoWevLQaUu5p6oQ/dFeTAHopJnkrNmjpGj2siF3vUY/UdaBtZQxuJumom5KfX/IjWGyzcKiZ7OoIcapsOnFUGuLKrIwEuO2oawxORM+z/3DOIxt8Ouz3EWkUZjcKlQVJufU5+hrYRrENtj0GXpenOdp+jWvyCjaB9b0w0FWPmhEqqYOGI5IAHcKQ6RpCuLWofM7x7AWQ5mBZy3mzopt+1rbU9Lv0PJ7XosxsaXPaqXdEgGoxIN1nVR629Yh7UEa485xu+vwZmB91/m2UjLEylu0pizrgpDi9jx5YlbG0wxTvKZ7HU0t2LYyruBmXtE1Zn+xEeVkZtWZWcwlFjNg1Q+u3s5wYXeFJ7GNLY+oENjwptfduWXzALealsb2hbLr6LbA/qb5tumSWHdrq4FO6z2Z+4/57RX3m/KMcgFo6kA8z4G48LmuFcimKHR+5zjbYjwzMLPF3Nn0Qqi6E+xrtLNBwBMx7PM0rOlGMDNvPktzmhUXmrjPket8uW0nshpsidGeLYOxeCKL+rpcFrhJ6GgXZQ2sogwZw0M7h46Bm9pS6DVtZ2ZgNDPxnwXbkfHeJxX6hErqlH91UuRrI99p6sAB4XlwtzDw6obmZzuL3sxhQjUT36YXfL1dYb7L3JX1hhqTMh7XzNbZTu7L/Ewnk92mD0Ot7Z0pbm5wSr6gK5NPjQQJniY6IKezhIR59thXyi7McRJQEdbqbLplp8GDZSpEgqaD0pxKIXD4XJq6X08q0DsGU9/+UUE/jllOp7p5ctHjaL3tuXxUeUHTZ+F1UjwdPy7vkuwWXZCldlQXpInlk9r3w1QTdAKhlZw8P4ytgKzb9n0aUWjdpxnY0HoWbPqiQbMbDMr+cOPMUZX4LDlGv2VeBDszNxOCBHfgvlH9rbHd32tUo9XxOsHpQVUlyzt2pXOCNTu3ugrEdiC0CxtqmnPNmrvtzdw8lBmY2Dx9VmdkvMXNHB6CVfZwc9UZmXg3kozb921r3Pms049zQ7pqBrbUB2ZVVTBwpic/jpsAuHDU5507IJlGMi/PgvNl0wW+3i5wKrf4eBZzk288YeaTqnyrADtrVsxOcbN+RFsTxcCc2vSFq7Y19j5db/KiIn27YcrO8g6t6hRdJuV3JV+rq0AMPYJ2YWRNM9Crfr7n0xy3zB2agQHNxLfpRFsPZ7e05taY7/jRmfnUVeA8ip7Mp2lmO8xn7tAMzGcm/rNjPtJQmjc+mx2b6HlCrqBmvAHWnfeAdiA9VMfg29+6TUOZjWfVs2ZnnWJVtsaqh8ny+2lGzmzL724eP6aKEOsq6rhwsLHZZ+UKaTuaGZjZdj5turL1y3XVYEwPjw31Zubp5/ga2XIsW2TonX+bfEzqVE+kTkVqoKJTbdZJUZ1d/xMtK1qEHsnkL9k6S7IsrxiWv30p0VFaUD4p/3hZFbWscVDUC1TxOeDKly+a7xx/tSn3JJYVqiePR0mFbvMCIxBLX/5kxEV+0ce4EJq2yIjiY75MUvwnWrUzCHdKhDJ37WOS3dbJLYytLbPrHFpUBXtxXDLmU3ZPgDMiP0fFGpcl7rKDQ4hFGCNS3pEAQjj25DD1ME9TsFfku1Xl5pG1CkX31t1ySLrhGJG0nj8gTXpnKVNHqOFRsWqaMosV04ZA+oSquxyc8jGEGSGRIoisinsiYcGejQCsic3EVVbpaN6CGFEepvktTXkJ4erKzNzUSHOQlbpt1YCiS6kE4RiyqJnooxOd1nLzHC+rugBxtEVGFPy1DYRnfC9mR11dt0YQ5t4lWX2TMFhw3fLl1hNHo7LhAq0VfAmAmVGjFN+j4ukSr8Fh8+W2VBwCTWkIyYfvckXbP9c/wWmlEK+GOraNatl9DGPB9Q28iTcAMFvUiw1a4hu8ZIpVP2RNI3AFs9AFq50xXRWUwRp4z8bsm7El3mUCKnJDqS2ir0mBk2yIKnGUr69xlqiIY65lbPjvdcK+fMkwKBr4ct9ROHTdtgkb3P5IW3YkADboB2jfhqwb8Z0BFvHEYRraqDamLaD1hgLFf+8pZTpUYVSQQy6sgfWFRjSf0UOp2ji6MiOS40ci4LMkPairO3qUbcSB+oyhgzc21ic0hzBzWeZt0CgPtnxCeTOiRIXBrhfvi7zeKHvBSo2IWBgTCEcbE8ZS4eES3cA7MJyv1YAdSKMDY4czIVli1yG0o59qIXDJ6WzQ0EQtSjRNuhwDGjlBA0wvMJGDxXlQpdAOkektkagoNo6hbxwtFwEZHuYoQLWxb+Mwg3D/xHCQplOYGLgKPI3JccZc0Sp3HFUENSNtoXgxKm5SxANybMMGt9l8MsRfAC0ofFgMe1TM5qlH1wRsMZrpwF7Z9EZ6hq2U/OOLAqMGwz94hHWW8eNSE9W6Z28gvYZ3hKbVyN3Rgytx5LlgnMlUrVVwL9IMaL4UGjR9oXnjQRkiSpZWJIgwZr3uDq0R0yuvYXvqCMDCWJfD5pX24YnROMdeQiiMT/3jEptOfEqYxFX2pS03M3ojQNTmzBGAxUEO8KeDT3SgX6Q9egNSs5bNOaOCWvXII9hoVVxvEnwLCp+uzMIiyETJJVpvUoWgEECsziMfUUXOByYRCUNa9Dkp6wJ9Q/j2DiTjCMAW3TtMuKFU9FSEMSIdudhBGAWXRtPye8qWutU3FFuc9Ma+LPDpTvQ8skKqGa7gY2Sy/sM3uuBlgOpW3sHYrOV+AM7W4P+kQyzCWFvcNDgFEAu1j4KtNPckYwjzwIu8LEnFVINShJGQcrfl+lvVq+FOlqujuV4dKoiX+3wALE293i+kH7jyvlfyH7BtonMC4ZsY7qVFZ44xsWwJyd+Km6kIQxvGB1ZS0k+8zDdRD8Y+MenEi/Ur4eJcJp+hhnqQ+ooQGQHPAA0RDfgBQgpjDSdmnqZa1hsDaIbCw4GUaTwSdNQYoZiai4bw841PBDx0HkTfcw5SNfzep8JABB4VQAaQkh4kGLsrXBEZSGThkuwznLeETBWLWurRmStDtJM8LzT0s2gBWlbDiMPJynttXPXeJQAlQUDN0CB4kF6CY4mOXCDOiSnUhf/R0EYEUY9AgITowXkQaUghIpqJCIKTj5oUY0DzOMZTG0yWMTqAOHqu86BQ7wrNdVQmDwClHowMDBGGc1vTEAbABVBFSeQQghziNOWyA+qoIoBaDGdcIwJ9BIQzEan1cRteVmioJMGaRyVW0dFp8MizIJeEWKMFxqBX58en1QJlIPVAJFiINJxnoYYmMqqJtULa4FG+Zq+LBgdHmB4SnH4cIngww4BIAfooSe2jNTdOhONgJJDqDMFplF4AHFSiey9HnQYNIQPoIrhhhtNm5Gd51ftbAtSBITVDAiuAFBLdQXWEgrFCJw4FughU6qxPZipBb4m04xEeEUWikvBISMYaZRNrzZI6LpJgNHuLAArKHs5fVLdNiaimZZfOTebqYLNJMVpd5nw/ZaJo4dWj0lWDiMU5pGtopcUKbevKKfCgHG8y17ERCKceEwQOUUhwFdZQCcQ4N1cJ3bVhrHEVFy4Y1YzJXmPE0J6nm5UohByEoA0Ne2iXUXaVYlKuxzmtXO8bhh9GaEgGVrAYIVQvAuFAtADt4HHG0iI6W83ZzVmBb3GmUSMkUOOOL9bQKBJWGoSEb2IDU9fs+CGMmkAjOPNoePBg0oyQQWw0fuwTizbKRzZX/CMfJcmsqhsHb4NFQ2Dd4yIz5a0aV8vE0YOp+LPSdsJ8SFBWcSDAuKYVxR1JLLQw7VYjt+5ERh8CTkm6WYnWH3KGB3JKmsmwxmFJVTQUszyWKTFPfcwQGzYzmQRqPyozg3mSa1buGh14xm8llURT1zEOUllVQ0b705uxkRlOIVAfzHwIgrsN1MyPYYSclS3hJ6gWhDRUNA5ZX19DXOXzWjOZDW1OS3Ddm+Ir1UtgwH/AA43GE8AdG+h7oH9frXNF8OgAdA1txxTR14n+objr4tFi8+VuPYVnXGbajljPqYKpAo7Knecue06vPinzYOazLQcdfE7mcallVNP/WFTRvO63tS1YozBSwBaThtL6GAfmSbDuwnx2DF2XzPu3Te0gqph38innZFYlSteRcSgLr+kYoQiiCo9pSxMz6gIwO0JokKmmyGOF+C8Lj7XgRed5uR4IRXIlFmnIa1XdSAIbLBqyw8FXzJS3alY9C1NoqKJGpe6p/aTokLjSSIPLYYI8JkfXsMUUaTgjvjiyWTjmut6Cw2bJRJVU21osYOihK0Whw1zo8TiTSYvOYY66uBjuE6XvgXrSjFwScGCzVKkc9SZb5cjuzDa3mtNFvjI6acKAmst/CB70JRjicul8CUB08zlqXi2S9SZFQ6AwNfsIkOY5H1cIZiEBHXSwVJHcgz590LNxqmuAPgpI9YDgChB9+LBsGgopEM7wcnJoWXN+kIFshqI5IziTZdZzwAW6x+jB4kAlABpXwBg+2HUexjojiT6gdHNTpxl9DzMqMNJMXdNyuEoEcamqbkazNlXNeJC7i7mofdYiA6lHJ8FC9OKiQGoIJaOa+FkLbbB7KjIEo4TpIcHpxyGCB3MRiBSgj5LUIQ/rjKEEFJDq4cAVIryqmz1UgC7YqPaVnV1F9ZCt6kMUNYRT1VDZrsmJX+b1MVi11AWg1OOSgSG68ZFhNUQCkM1BERZN1kwSAcwwjDG0kihdnFsTVQR0c5Dlig9yq6AJD2MYAQeqoEZiJgOPBKDBKGhvTOZoYwZrOaOBsZrHZixxeKLBBRNDomiQ0OBDL+vEBgdns9a5scQQHRw6FYfABPagDYsbbPF4HoRTDwYCh2jThZbW0AVENfGD+aZNnTwVIEzd10lRaxrMJTuhONxXQ04Z9YscuILG3KWrp3uRIwb7tnieA7eheZ4zGSXbqOmWZGygHcfX8MuEBGwaAKgHM7PvEmQHHSW9RBDD+hkglavQfE4VMc1DAo31SAay6bzGauRMiHlsRV20/atz0uO7pESrb7i646Lny6QxVVEPzlATIhuXKUBDNRNiFTvFOtVDSRCuhkQGahrCFcwDBevp6OcgmfRtAKRUzlEsSn7hJ9SanONajuMdVZ6SsOOGJpd7NJeFQfRzIAY5NUAqJV6bWMMk8ThMU5IASL6hVUe18Ooh6apBlFLkDtEQTdvCxHos1LZxhZoruQ3Wc9k44vWdK88QmcwSrSShAKEe1xhQFeYSfCCvwzPlwuSTz1wNKW3URBgDmscwgteRxKyiwSihtwQqKnsfnbuQo0aLgghoOgML8EHnaRHXxEaFUaYhgzQHIXUCAqoAS4VxPiSt1AGRTiy0h86bWUgJa3FXZMFIjrdPM7OTmGbKEJ9BB64TIcpasGyS0mNp5ZMa+VzxFaQ+6NalGthhkLrVGUi/udao1LDGPqGEdRiXxloRSLGZ/FzyNP2aVyypArsu5fPFA84tGnCNq4m6Vrgbiwa3Ihq7IrC7z44AZqe7AhLiATuEbV2NfLdEARJZlYJPt5/YtgctciCTYAS9ZZxe7+o0wxVOUs0RSldBp3Bo6sHKjJQcUKvP6NBPe5oHkx9eyYkLzcRU1rUfuAqFDYltT/2WLQJUN06qT2D5IQXjlZSOUaa5Dlw9aE0tMNT8KAmlhpg6vJCOKWWkjEo+nX4EwtkNTKcVeVFqLl3IRBNLWphoYBz7nGOWMoiaXY9gaIM3BFhJ6WIhJGQzuVrAyCem3DhP6tVJka91pNOBa7Q1dS342YWQ3VXrz6xGPS/pLnMHwnHA1mMb6kQmGod4YpL1mXSvNEYUGUgjYEVYUFxzyX11wlrCNbHBpM/ha3zDo4DUbTxQBXgvS22cbRUIZ3Bppo/K6BMZvE6Kp+PH5V2S3aILMk1DRl7gkG+spDmTm+rC6ZxyU5B6M16QmkNO4rikZH9Y03AMbTnIUaUYVBsjnJpcYBLkqxMMr1ENtHp06koQuVTpmzXk0zQw8QtFuGXTe1eLWq6DNb2EjUbVud/Icr24GufD1tJ1DGs1yFEVAw2tKTdGCtBLyAE+EUeOspLbMiRfyZVNeBJMyY58O+CTdvWUeRB3lKX7arG8Q6s6RZdJ+R2iqgZaPUx1JYiOYl5xDf00iKHLaa48zpstF8ppoNUDVFeCn2RZU06DeDbKDcnXr867rOkqugGwpsHJVdQ0G+WKN5INwAxJQe1s+ORPg/PAa00mxjrqsZqqgpnnlFntNUQ1NjR1kj5F+5q7SFMV98FqbiajEjXuPeXvr5v69O4vwRkq+rLfX1OhsU7aD7+/JiBLtKnqJP2Ur1BadgWfEnaRWg412y8vFptkSa+x/sfi5YvHdZqVf7y8q6rN316/Lhnq8tUaL4u8zG+qV8t8/TpZ5a/f/vLLv79+8+b1usHxejkyQvwu9LZvqdHthFKaMWSFTnBRVu+SKrlOSjIvR6u1BLYgR5zq7PqfaFmxS9BHgQF+74ncNdhGuWjeXsmTSKGpyb0Dp7/bkyBtip2mXtE+vQKflg00PCHDonKKjRBxc62oR2oulkmaFOdt4vlOSViRkedpvc6Gv0XmU9dePJUVWtPfYyz8d3tsp2VTr319N+rWuMgBZ7ZM6xUiCwaT6slGQCuVuvT2PCnLh7xYkYIK0WTZYp8BAHv8XeUx0uGrPaZLXKXCBLWf7HEc5qunMYrmiz2GT6hK/gM9PTSGLR7TuMQN4zvUS0AZ6ajQDS9AM+6zPa6PeE1Ya3WZd4YVHqNUaI/3AmUrVByU3/CKSXserVhmj7Wp8Y88E4bOf3fF9q1INq0HCYR0VOyKe3GXPwAzJRW64j3M6b2+uKDFMoe1XOC8IBJaWMv9V8e1fJncAsuZfZUx/f5a2DLEXem1tC0JSoK4ydltgcmjNoekw07YY5LMnDb7oa72NLuivB+67oTvcLlJk6fWfYbHNC7ZmdkmH6jrV9hEt0g8JllZc1cnmDlsjVG0nxyUL0oEcSD9x51hjY856Tz+E63a7oeKAxGfj1CwwDEN5zR9EHEMXx0UizbQlYiL/+6AjRIEESWsjYQywiiUeWBVIHTHBaybUcHucH0fhyyI1xUB1mxYXFl1V2Vi1+OjOm2yLkNs3Rfa4/2S4X/VaIFyeugfYxWK7HGepMnt6Zr0h97byUMHih1U+yoV9Hn6YftHjvP6OsXlnagVc59/YAWnkTKLqmAO7iWz5UXYxwSMvluZEc00az7uHtT1Xl5O4xJ3jMCuIRS5mH2oR9t5WrPEyWN7D1/igvEyr5fSuuI+78wqOEfFGpclHsIBhqwAEZsH95tR7OpuF9d0OmRc5XENX3eGg0wBQO25R+d3Z8E5+uq7yjUnBULdI1JB5RiVOBiUksfjR7TeCMY57rMTrnb7bp5NCAhHZfZYmcu+gK375n650Dh0QncLTcnUK3hbkjtP00BpTTD4SGiw2nPQR2LJ+PZqA2KSvmg7Wji1kJ9lH4gYPGfOf6MOCmUO6zVN84f3LHjAZf41r8SlKxfPcW5QW9EImxMGR18q4c5xXOJi41mB+Pjv853mtihvuve9oVIHftdsKXtUlaeRQLRFEUP3bU7J87leX6Pi7OZrE7JqhGpc9AOf2aX366GMyL9v92RHPYrpmLJZBhBrDiXxJ85AY3LEJb/Pbv6bSrdvZ+6/B+j33d3yTLTumhWx8N8dlNZN/ypr1KXhs4sCfLDZFPm9bGcYvjuMs0BkL1udZdI2Ny5xMNNuVgqM45LZuVRkzsM0v21TbXjwpbb2RDzZNHdJ3dfGU8UXOPgCkSHQKORir/jvW5+lc12WHxthra8/kaRuGpXE9PB5Xq+vZvQy4/DfHbAllWS26L7ZY2lTJP0XIseHKhGuSqRCZ7yfczXavmy3uJtLGhXK6FpUk/J8076C84dCB0+upGxHI3hxcd+3Po9c2iaPqdPWnnYvkYXLuGR7u1OX4UocJ/99544ocUzhAWry3Prx+xorNOSmxEFvLGnOKfHAPHx1MNw0Lw5HNpvm0za8trs6J3mxTmSVQCp1x7xI0grG2pQ4mPxWa5x1kmhs7RuVOF2KwhcTowKHHnahJERCjgrmvpR4h1IkvRvoP7pfbvTPjaH7jb5wW5eUHxNyNoBPtELRNs+htCsf81ucgUZcudQNcxd/SolcAtiZvWoItBKyVykiyFhsVcqa0+xURAbWVSI/LOG/z3saYw/W5MXIfXbjRRnV8HXeXZNsEJskE70Xuo8ueIiAKyT/Wu6z08VQhci3e5wtATdrodChj9I7kCPHNyDtQngj7rTdV2dMb0FMb10w/QNvqOknSWUnS6HIQU+5yzPUXFcIagpf4LB+kkcIG/d5jn1nWycNtgZCne/bleRz0FDVnEZ6y7LNVa6xe+qyTSoIXGEPRa44YQcescxhb3nIP6KqQsVpCfg4y6UOmO8KhHS4gXKnS0pU4CWIWSxzkNs1e699mX9NBEV4XPLcHJ9/MAeBjtE/oeouD3QkHePyeT9mQLCrMkr5+tnz5XN87jy9zWiAuDsalEK8xRwX7Q5n8rpdIGPyqHz4Ul9/KhNvPCU6wjZ8fV2gewwcxMYlz02cb4m5uzvaML7usHjef8NVp+HmuD6huxFKg6mKrVG1BLTIocgBZ+sE0tY9ku2IMISDLMgrcyNKIBcv9FuBKM2XH8cndB9KJea7xmd3lD/vwnlF8CDwdxiY2T+AjkThHdAVudyAFWRk7D07iwEAOt0oYOxb+YpLfJ2i02yF7/GqTtJUkPsgwKwPFO5Y0EPFupdLHWxtdZoqEUuF27xp7JgIrYm+Jt8PAsXbfmDR1VErnzDEfoMx8BbTnbp3gczlCNSuRAh3JavxelvUa1jD4oq91CsFehjCvffMCQ6mDwjhNQZ1I0ogDxvhwVK4uhqXbF85WXyvhQ7SDw7rI8nqm2RJY2YUZEeroAsQFYx9K+8r8TF888XFg6HLKC86LwzfHfrT1oGUBrHMxRv2XzUu0Fl1h4peBxP8YiEI5xYGdQPGPyp3WL81kVtk4S+ponGwWgnYxLVshHaZ3S6PgTi7w3cHo0tbR5xZ/ruD/1iWNsuTS7Uw8iQDyl3W32P34EqBH4Zwp8bx4wYXzBj2LnkqYcqIMO6tMPcUhgFaW2ooB+0mKRcJUbYQzDJAsYtPB19TutSXSp16TV0OD24LhGTdVC5182/sK8peskCxy7rsUymKC5MrcJFfbaWjp2WKPqLstroTJRgE4dvCOSpwLs2jCsajFaZgMDSSJIYgnDz27vDmOEvI+U8SiqMiF5zq8BBimZOPCKYrOUm72s2VjeQzooDans/naXlcSrRln1yMiX34UJHNhCInnYwanrN7smJJ5ebSUcSuBHIxY+bL73+vE2a6Ee2YoyLnCw9W/+A+wWlyjVMJvRrKryV4EDCEwzzgTINdLnU4DeQPzdhbZ07p8gEodzol4ZsnZu84yYuuf4eInE2lk5Ia0OHCIll+Z0GPaXx/6dGfWOh43lbnQJAO3vbpEtRtMlMImVq8rtfwvMMQri0kj6YWRAj7Fro6iwoJoTrHJa4YWeaDIk/l4D1QuYNuhFeo61mLQlCPIABHPkKrFgMWt2qg2EkK0X34sH46rKtKcrOQSp0xf8PlXYrLSoNeBHGgTCN7U0SW/zk5l8qWQhjC4fKEnA5ZVbwU34ONSlzssRIqZxxn6QpAM3x1tg4f0RtryC7cFDjsyRu0xEkK9G5c4oexv568xGvg8lIL6ddie4FpbE+Ec+Cw1tx6nFWoKCFGgwCctAAqiEdYEMQ+WkAni4BlezpAp5PpJUbNOhQko1DkpN+gsjqoqgJf1xU6ytfXOGPnfWAcRmCnsRCZ2KQfPNhsUiyenUAAe/zfEL69E9NUtN8cqAOce91Put/wSkTSfnKgFzCeD87j6fcIvXjRgHm0pRMsSqBtejpGdSuL9C5wl6K0aMaK71HxRCdRsicKZe468pcMSzf7YpnrXlReJgW+uYGuUkAAZ9fLs5uzAt/iTOGCyRe7nOJK1G7FgNFJLvXA/AklZV0gSlcF9hGERwsHa9llTCr0wEt/aHHzAA7462yVInYRLVtupUJXvOeooAENYIOfAsSzDUoDfRM9hPco8sZsSPYO7Uh4MBetC2fnmN1xypa2UdHOuHn1ikyQn1eHxcPRS111Gk8v+m+YP1PXY/BuSihzuoEh3LckJJKceIQi956qEEPl7tghkSaW/Thuse0NdUm2yU2eyW/KoHKnvRnE6oetmwXmGsIOQyp+HUM4eqQ0d7BEe4I8UvjCnZF6kZLGBWSMe37p4qg/pYxl+OqijMd9UHaYV0QfVmIFil2Ut9UtpFYNnx1xLaon0Y+Q/+5ikMaJZIRmn1wMqw0XqvxSofK9g6ceV+PSrbrPlUsdMMOqpVatVPeSsBz+U7SB9189HVrLy3yBUrSsYPwmWPf+n0EXeVKh48XARZJJD0lHBVt3zp7YRPl8XFd30SAW3+z3PExsN0mdVl8xevgk6a9S4c6ogq30DHzn1CDxeeekqjmNGtg2d4izRMzjJBS53G2tkXybP3x9jrcRC5TTrIqZpO+OCly8xD4jwW+m/eTkuVYkWYklx89RwTZFwCe0wgnlcOAptFi2MwKA71iYFOAxeYgCffVp5AHttyCp2ZedmZ3WAyKOlB7h8n+UOrfMjhivPeYR4tmFpeCfIAWudA6Tz0rXVt9VA1Bssw1PBZUBQgWzN0Loce1NBc/PVLA/Iv/oR+RYZpst3xW3V1xN9J8Y18YcwoAbZC2WidTntmnVtZofNm4gF3WKVPHRLcCd3G3hm9xRgcNdRxO3VBHmTS51sai2T79g1ECxy+VuWRHhzQQtn7tZfvynhvNp7Vz5FBuC8GoheaIM0gXCVLUiQPm01E0AuN1qwPxmiQkD3fT0AA4OA49VkcgnY+7z7ohkzncxUBZzmHyEsLb6rh5pSN28+IAevyZpLXlcjIqcVZuPOSmUJTZftE116bRsTfKiKbH/vDM83oq+xkOPuudFsQIN6PwNQTocu28Lal0eYYxSobtXNOwP7aMQwXrP7njChQuiCWOk4bRCRf8wRtiP5VKH0wxeocu7en2dSbk5hCJ7nG2wujG2/uOWzrw/9Fl1V4R6z4MNS0aW8QL2GCLfiHKaHWDoACyy1dkHlVa5biBS1jK+wAMfU52USPtSF6XlvECNIVCOZzIq2jU+j+RwOsbm43dqxLD7eovKtc7Pqe60PCEitx5ijol8JRX/wDdk7RjjXZQBCP2ZdhvXZjEZl++/7qJrz8ChDMyCAtzgJXt8wGm3EVgZRu3P1Lb4dp+94ZE09j3JSmuAdbxTbFQw6e2gWObqttv5eyg8d0fFP/Ci0k1WYOoaDWafTDZO6KZZUXAf7Ph//uxz4U9I4p+OmxftgEl2VLDjq2OKdRFtRTwfA/1ucuiUu8M0+9lJskTVIi8qCee4xBFj55L1AYtGWqDYQaXNVuixEdv0g5jSTSrdGVnQzjlLBRRB1yR4/BVLsPK21/l25+VrUuAkA6NERZkvDX7/eXRCOpFOE5gfIDxy/zwZBmJEBfzR4nI916g4B2WJbzO06l1eRTUCKHfyg3w28aROSxYBWGDs4et2rAWDpvyfa+H2UChykFMTxKJmOtdZXZ3dMBRMV4Riv8ogO7P78awTts/xmDx2NH31besm6rU+Sf7jvYPQxNrdJCpdPD3uOZp420pKsxZUbo+dxlMhn6TMDPx3FwbukgmJHDx899iuuPDkyqttAeYHNg2LEx5lzUVYbLOvshiGqt3bqqYxRsXfrPbGKIu12V7YELi4q3TAG2G96pDtqoIYZ1fcLS6ZgEOiccfMmhPAm6YZHkM+t10k5uNh9owCeFnhbJY7WP2zLis5651U6GBkY8YwFWK5dB5Hxfl2TXaTCimyowIH0yfOvisznUuFUz4o2K2DKCPnJKfR5i482pFUgW5S6Sr2QSFaIbC9XN3L1b1c/Qnk6pCyOESG9rl83eWluuo0srFr732NxculUYnDA6Gyzzn8pRDuW8Qy935KKAPxQfEJxTIXWZlVqInJL0pMrsDpSR0QGdAnLODxI2m/lKw13GcX2bgPMbijIQa5/GchAqxH4yHBNHV3/9oBDPLgEdhBF9LDP4xHvPfOMfxPjpJ0WafMPagJ4yE+kpKKd2aZEGFVhr/v77B4LBJ11WnWyMcku60BQcZ/d7g+k2PHOceNYy/NJW9WtyxINCWEsDWyLw7rIOLr7V3PJ9PEucvXTTQt+YZlKHLAudkU+T1atXWPZOcqGMLhMJxX5kaUQC5Hn2li4sV/gf9jx5nc0o5AFeEiS9KDurojjbbvGC7QktEyZJfQYfbYOdzQTbObdBqMSrNxDIK1lqJ6tJ/cjjeUKqcrSpMbLBpYoHJ37K2ZxtQIAGbf1hmd2Mv8OxKWIf/dEdvBkpwHShXOUamT1n2PV6hQxQyEyndmtZ/kRb0+z8vA++kejcc61tSdZtFe5hu8FFH0H7e1+OWEV665rk7PD1YrsimL7hLD521u1s8uXA7jS8YWEdYGw+O7OBSVp1kdrEURRf9xa6uDkgCy748KHI4oTU4l4XTSfXRQyjvpOdbC+68ONxiYnISFu4vmk8vhtqxow/Lhdvjujk01k1C5O3YWvRLE25TsZZabzErChZWvnJpVRL0v8noDyqm+5Dm7gX7u9x5RsnSftyGk6NIE1adRwV5gWfLMPsrhRGobEwER1DaGx1ccKipPIxN3T4L92Nw9lfV0S6umfUAZsmCa6XRfK4p60ywT1hj0kn1U4IhPdhnhPm/vyjXOyavNG9HaEESccqnLxVeTjkCBGih2nJdFlVS1hFcocu8vjFYudTAhNjkgYMRSoTPe5tZZaZ9UAblz3FFdFChbPh1JyWhhCJcWmnoXRCiLmPkS9z5fJo/tfgSZF9RQLr6JYOwM7rMrX9fXVV4l6Wm2TEnHIPYWITxbOH40tdBDuLdwSev3aXt0Y4EhA1vUjg2GdG2xFQmasYkQni1oxiJCeLZA6sprD4bwlE9EzmOqYybpCUIgySzAY7QNEtMCPEbbIJktwB0sqU0VQTMdvjryB8x1PpwGZ54Qilx7R5fxBam6kt7SQuU+2FVYXbBdoBvSBbSCYvKIZS5YH5JidZ7jrCq/oQIRzhHdexQgDrvoHVp+z+vhkYjyFKmHDGhRDmajAHHXDVTOYlC5g6vQzQ1OMZB5dVTgoe9vFPr+xtk5ip46yIogx2oibI4Ii0AKjB7SxX2yWAGO4v1XN0yygjt8dcQEjNlvhJ+S8jta6ampgnHr89H9/Vu5x81XN0zHjxtcNE6reSYGRgMBfPH/F0oAKovlfhz8DhdoWb1D11j0xlMBuRi4+moHS7Y/fchTwNilggppCeIgNZRXS4dJ9l0+yIEA3vjlxQoC+OE/PVKjpmVeWNscl0rMfbkX9tPrRDS+ioXu+wLTSVpvUHiHGEM4rLSaaKQF/pMtU/ZaJVlCoed1cOGtyUyqhwxv8QKVUpQuE6yLdNzQx6kaesIQIS1AI1JDOflZ9FqeZkAaMBcX9mJ5l5RIaeMFAVxObRj2Qh8VuNsToTcjYpk7VnqcI0t6U1fcyxOVDdC6kst1Upy05c/h0oc/Rl2gdYIz6bipALFv40NCXxa2poDPedWHmB+3owFzkHvLJdpUl3eY9Dghn5kX8ockW53dS4cAPejOXHB1JoQvJTmvfcBlFZ4ZDEDpkx7MDs0012Fxk6kzuQpetzhuT95LfkvM9R7fsDNbROYCUPowlx2aaZira1vEwn93kNolWn3D1R3IZFKhG14g8Qn3+Sdg3Di8GsCfsz2RblVAjluAtJ5qKHfuh24VxTKHnRmwELtbhk/LrgcsIHkCRHYBANzHTg7DG+h8BpW7aFtLvKFhEWQ9VijywAm8wRLLHHRxlNGThqxuc99dsQEdHBU4WCVRWUqJdPqPLtw0kJ0pnFD4XwngB5aqvcSI4FDl+axeU3dCxyraoMIZaiiaX588jxgXI17kCRqRn4WaAO+65VIPzOBNtlzqQklVf337qu6nbx/BK3SPi/LuDNRsr+CgFSC+bYBkUIA4qAzGq9nQK9kp8op0oaeAGAdCkctG1VVVqj0AgIvv7xJlQ2AvOWycVOzQdyI/vwHZhfjvDk6dLG893WUEd07uu7N8PaJveCEJ2xTs1vZMFJHAE3qPxnd7hutOuT2Hb6Wsz+NnBrmzWGVulHl3+QS+DeCLf2BNkbcKx7EbyRh9zEZWWKbh1Lje680QBKnUfnPFckhKMilY2LjIze4Eneb57/Of5Z7dCqI3V02wppBV02HxSferrLrbkvyySJbfycCgi1KxzAErdX2E9JRRgeNtJoKvXcUyp7tJli4QRCsV/gSrJ9xYwWMKWEVzmiz6NoG79+67hwEEXpvO9uNnEyn6AlV1kdEEXyg0jNAIlZfaoq0/ERtFerYXm4HiKlLx7F4XKCnz7CQvmtkSbe1CoQteNu2Ind1FE4dU6I1X7T2iBXSfNzhApVzqwqnJzU1z8BWYdfjuYPVZrXEGuhWOS1wozS1f+DGkAuR5uhNtbdPP2Z9HRPjG2PjH2Lw2fxOK3Zbc50nRqi7y212+xNU+B2Ecl7goOwONoStpqHxrh/Ydz3ndjYqwMCpoIhAphDgMsY3ddi/vgOdgQQJPQuch8SxwTCPy6L+C/TZx80LY5YzBzCKQXKeoFRUwbjWUS78vk8fjRySRYVTgdGl5RNbObV48SREOx0Ueom9H8yeractu35BI2O6ry4XcswzGI0mHCNnsYJwxRNesGeyk1iWNAwKYUz7uM7yFSYWtWbGWdUEfg7ZPJ2LdxkFY/W7kLDFNs+7E5uVzuVy+fzjgzHVxuS0Cm80m15+WKfqIslvppTlf4IjvHBU4l9xwhCLHuy1WWwyewhc4meMip8mJpznFer13muEKJym4wMWyH3idswkgBR/z27AlziHyWN3a2tMsbK5J8PwhF2/L6gS/kHe3Yj9n5qSYozEoi94SxKQwhomMweyVc5s0ShDH46I5TxPHGTVUCP3pP+4MCwXLNT95NqMcI019RPcolZx6ue9O1viiAn22xiX2GGm+PxDhqMBh497AyWQ2Pslk4t4OkJFIyY/7jy5HmhtUFKiQcI0KtmlpJ7x1Kx6fu2/2WD5U1QYKMcF/d3JbBB60DV93RiSxEOx8XJEI4eB5dL5R4fU4JtrbuDbl91ly6baWdqzcQ9EyfD03Ve68YK9WWnkfxvFjXB7sbkKw25f6J0W+VnG3WObCmSqc4xKntR0lk1WE/HPlBUqAe7zE8WatNTMcPjUhtaQnWmKxF+7+RbISPQfxA0uMPuZo4LGvQ+Nz4FPXncgmAScc8Eo0EMtQBhnu1Ga7bXmOLe/Qqk7RZVJ+D/Qa4zD5eIxpq0/DNeGH+QPC5VK+cfbJRcDk2fHjhjKqHGJZKHMQ/lL4XdfQu66GCs32vTnLjotCFPyjAodZI5vYRS2LY/67w4ksYV6nRSXhG5e4YTzOViC+7rtj/2qWxxjuIVfm2Ed5RrjPLlvwB7xaifmXh69OXoK3lNXPUbEELtqFQne8oDVFKnSwN+QPX1Ehr1r++85I+oNlGiPle4/Gyw6srDuNgG/aFnEMX10xyRsG/939jH2Rp8pI9F2Zy0I8XaXSTWHzbWfY8EsRhQ17NB5sqKn7c7HhIq2FmKbNl6049ikSM+gTMmwryB7KUIGXkdyPRWw+QfeMKHads/8DPTXZNUeYhq9OmCQkLvWB0JHOYSNdTVZb4uPLO7RGX5MCU6U+jIlHqDw42FB/GvZljQqnpObTnIfIn4jhWsfsIDsD/eljYIDr7aplQbrDdLy7XJQpeIDnvztgo06E8h0t99nh9lJOXP7BOWt5fpuf4yVNZgBc3/NF23z28KFap4f5Stof+e8u3kxZRRi7C2bxGVUPefFddG6CYZyc3cmCe2KrpUvEKT/bg2GcWzl+XN4R/Q6xJAX6xlSgOyPa2k6F+pd3Y/N5x6GsuqtCTpdd1i+rrBwn0iNIJF3sH3NSKGUyGhW5nvdP8mKdVBUWE0nIpfO9h1Iu0Po6xeWduHtwn7cpWHfpCaxy1DlNCXLM8lAK8yIUuRgc62w1xCIGN3gVjGMrn+v1O7QkojctAfyjUp/+Mwd7Q//HMN6tvENZvsZZUonWbh2cd2sXtSg1QICd2baYaGg/RVDMWzBf/VxZfdfNF5ENc8/jNDfKYhzomsSj8vFM0tefhn1GjaoScSmB3DyBlaGepcJtieMtMeHfa1SjFUuVcFBVyfIu/NUdiNKDKS3xTMOcXOMiIqHITS4lt4gqwDIjSoUuC0l8JNh8cVgiWL5J67456GxS+GzXwNnhZ6JPeI1k543hqwMmtMJJOysibcSyXVzO0RZx2NKdbTcpcF5gMULQ8NXN0VV2b3VzahVdWd3eYWzSJxFF/9HhmCek+z1ySvF7uBROcuzD/G641NIndIR92eZxd0F4/pLFQhDsuf1nN1xA17jPDmo02yuXYAZDscyph6tPSVYnafokdZIr2RkhyA81TArymDzEoL76RIcyOR2WcyKs1rAk78ajArc7C/nKwkm654Xo0Ma+uL3TyqQBDV9d1K2ylN/EDl9dfUQXpThfw2en8b1DN0mdVkSqrQg3Ysn2pADZmXV7lKw3Cb4NfJjWYfG5AlBW3dUrgB95l32mwcpaT9hLtCaiMtS5SUDmwdNGDLvK2ruhRH/KVyhtokCMT4DcdzfH8aZmgQTyCEVOinqjZzSvdcSOAsXPUbzEc5WcRkme6ibRX/lWYRxsaW8gsw9Q7IP7rR732xDcv+px/6rGvaUt4TN6KD+iqkJFvPf3ME6PDcIW0UT7BNi6/CZfBzfv4cjtYeKMRonncev1CSVlXaAmPmeocsSh8lKNtPV3VTGaIjDTBb10kDx/sJOq9OxCnLbz/w6TJstggSxi82dIDYo9T/7gPHm63uQFjRt/g0PfFIxQeXCjof6usuJJnq6gkEz8d7ebUShOI//d1c8FwjcucTUhRnhm/h0Lj4eaLw7n9eS7lGP8u5vTPvMVPMvEkw7/3ekh0wlG6Yr+JZzHhCJ3bjjKsxt8WxfAPb4CxGFGH6sika/Suc8uTu8UQecWJvi6j4pcLCtlnVan2Y1kXBm+O/BdE7iC9IGGrpDUV6l0ZwT14ilbxnH8GxD5+P3pak90wxTN7W+R18USSS8Ouc/bciFkL0EeKxndqMB1pB+S8g4aavPd1SH+VIrNO3x2xbWoCoVjfVfiivEwz1MIX/PdRbPMluA5eVSwM2Lh+JEqTe/QJs0jBLAXsfncQRtRTCMlWr1Rfo/Uf55TKYylJsXd/IZZgVRCufR533aHvsmk8VkviyQr15jFc4JopoIJa8XchqsS2RyKZcdNsczpqqY54UiXNd1n1ysS+D7J/zKJ1QRvlMYl277KobyN79En6VXdqMBpLUouHt23Hdu3otgdRqi8d6y93YGRl4iBColJoaVCV0Od3Eu/pxXkz3tMRqd8vQGUu9z5tqKwZQXh6lco3ILNJPjsqpl9MrGSRs59dpojKlclQwX/3X3GG/OGbKaAyrelXp3d3JRI2Gq6b443+8B9vpP/Q1It7xb4T4GHuc8OM0CWEwvGMaZ7/3Xb2+dRvt6waK86LUIJ5HqD+g+8OSiWd9KNrFzqgDlFSSbGUuo/7syWfZgsv59mZNaX3+O5FSiQemzj1pim2dBjhT0/j5h7O3J+pucWWpt6K90kLLZMEck9EsDoc+9qhWZXFc+vGD3IJ8nh6w98W9rlgY7DTSI2Lw9yE4o9F+0cF7WyPA4TCci80oQYMOxZaOdY6ALRqVq1UxeatZbH5ZWzVo9gMoN/oxG9UWhKb7ywvVVge/tD89NRkZflAqVpFI4SsflsbEYUPy5XTc0DB2WZLzFzFJH3JlS01wxN0O0rPsA5OS9rNiJDTWnbEeB5cID7VrJZQtPcVXPRDTCeFc9IyCEmomTuexXc4cukuEXQSrHqMI/LsbO/vwb5wZ5lFneY/dnEzLm6QGVV4CXZEI6oMad5n65xR7GoLTmhjOo0gFIcRWAKzG0F8o3QQASusehyIOc01JubaTqGveJscbowmzK0HE+zgenyo5ppK2MNnP4eYYSJBzoXNtEtnq1N9CFO6eVEn77WYrbFKqopd5nrMc5AmgrIYk67gDqMMZ07dpRnK0zn88Vp+blO0z9e3iRpKV4nmEYfzDxEW2Y3AlcHm02K6YvG1uBh2FT09UQ26qA7Y4oFO+kaCJyrHnUEbtJ2M3DfaIk1tzyRh8RbUx25QqiqYgwezIs5Ru3sNH+MexrGIjyu7bNJf7J14pChloo5FAdUK2p32HeaJfpOhnFDi2ZuRuia7aycya3+FAuBKyymA4zNeVVGHHpI9SKodeeizDbBtDWF09pyoaqhUjcdLRUw+l08avwoFokL9JAUq/McZ1X5AZN+EDXlS4lW33B111pedeZwY2XZAC5VseALY0OBMzDGFYFPzB3exVOKiQzxBE5nq3E54kp1YpxxBaSBfCRiiylxRNy7yEDm8ZtZqDPJ0+dcCc5QIYL0Nv/2S/932X1ow/+ymE3lUI+6fa4TRpBykyyZRW+FTnBRVpTTrpMSNSAvX5y3vpKd921rgP1XekQUPfqKqwMgiju+QWV1mX9H2R8v3/7y5u3LFyxTNg0DlN68fPG4TrPyb0s2jUmW5RUb+h8v76pq87fXr0vWYvlqjZdFXuY31atlvn6drPLXBNevr9+8eY1W69di9RatFZZf/r3DUparUdBf7qaqZZPLfIOXL1+Izf3tNFuhxz9e/j8v/t8xw/3+H0jilI6DLtDNCxWz/f5arPg7wLC0Y3+8xJTebK2zZJDs/rTxrqZQiA3h5QvKk9RNuOfL11r0vNtz00x2nxTLu6T4b+vk8b/z+KpCzlgn9bZ1eW7p12C8pp6ojv06zZZpvUKn2QITdMkmCFfZvQUiBRWipvAQdMPDoggEu8RVGof0TbiyCIg+oSppY2GU0RCOEhhEwhmPdlL0M3/uuEBERBQH5Te8ohtoAKYGwz/yLM4YG3TfimTT5pBT9M0e1+IufxjNgf8oD3OqagWuyz6QOicvHXGw4dDTuTvF+St2/d6SPPZm459gh4H2lpcvPiWPH1F2W9398fIvv/zijHTsE2M739ZTRBSyJk/bfnqIKuc+PW12W8MaslNQOn/46JPMsvzhP1Gv6v8M0z3EwOAaUQ+bVvrbi9P/vJKIdUXfx9DUQf/2gq3Cv714Q4jk2h0++Xv0Dv3Fp0Ms9eOQlVxcGHF69ivtWaAI7Hs6VSffRuuktziwX8stH/0MS9gosd/4TFRLwKM6pZdvhh3BGf2XDP+rRguUN+ledchdddGTNLk9XZOud0+Oteh/+8UV/0WVhmilEY8WXG5WfyQTa07Ngm8i8lygsrHN/QSLMmAj62v2kvcXj42rI/Yk+lyHPKJed1rSlFXnaX2Ls4CT6Gl5mddL9ZoIOKeJ3rc/AxtbGQIDDYtjbvztN2fUw+k5CLE1I4wub39SJgietJMCoe5aJGT/ukwejx/RehNkWiNI2n2wiYwEboM24qeLqh5inG5WSsNc/nj81luIeMzT9GdYDVvf2WPL5D7OdQSjchSVlNqRz7IPOY0WdBu0CA7SNH94X6OyImrB17wKQuanKUMWrKSgF6uIhTpo8NBowRWm8+pG7+NsFQmT97nESUAcZOUD0lokfhQxQUfrLiKaWjsiHj7X62tUnN3QhVOGcPzEZ8zeqa67x/rxuYsPv+LGYUPNIC473YxclOzOlnYHuIPNpsjvw7aQcTgZtWS0M1axYO5eyJx5+Gdi3iYVUtNQzQyCmKG8wZQOrpPUB4rVWvuc+bFNthQXqcplJBbek7xYJ5XLLZka1yJJq9j9PFitcXaUr9ec20Ggn1GUc+DBzQ1OMeHyMNKFnwLfIRZqboRCJxh8T5ldpueJDpraLoexEE0+qdmEHHbCqxEqoWdv3Hum33tcOtZj8rpJLauP+S3OYp0PCD7G10TiK1G6Ur1D6DE+4OWomyIkIVDpQzbUgRx83bojY7Dvj7WW0S+XH1/JINtW3fkE75RfJvOrtjqk2S7LaMhAXccLE3UeSLI4/qdteAmPo05XMWhlE/TonHq1Z0sPm59QPaQnR5wvYxBBWynwJiayt1GQ/QNvzvOySlLILcDPIHmXZ6gxZMRZvcljRGwBx1T7k2Ub2+YnkPmTeMIwG3LZagvBxugyyp3XQ94kFj4tJ3CgubwrELLH/6srfrJ+UIGXAm4vS3qTf+My/5oEHWG26EizDUP8OCDUXjD4+pxHfsISi4FObzNCvaM7+mJzEvbhVZqfgXumU0C3uKtdXxfoHoMHpFADyHPwSzxM81uqav4M/Lt1jwW7w7Od2cfmVaO9OtbaoIPccLv7qhbXEW9Q9ZHfn/MqNsohZE3gHrWbvgu6F6q6bX7nHquGdjaic/kcR9PzNu7VTyCD26HSFkKv5QrSBfYaqY+XHobxKy4xgSYkx/d4VSdp+hTCOJPo2os7lhE27jI8IdCxcUa/2ewYp02tEDbV8TzrOhyxDi17ET5SSzoPc6Kio4co2slFQl8wL+p1JNUkCr4O2SWR+6kw2MD+xULZ25gOlrvy7mvxvY6+RJIhzibZYSrYxm1/oe/RhfcVBk6iEzZ4Wr7HN9VRUgSdUzsc4Tv7BfpXjQt0Vt2h4nwUadQ38gjDNygJesFKDvruwqqmk1PhJVUaDlYrocmg7p+W7/KHLM2TMDNCiyNsar5kabN8O3RBI/vU3SGd3Uj4vNxrWyTHjxtcsFXyLnlSYbSyRLYImXsGQxjO3R+ScpHQDJoxZnWMyeNWVqgfci1LBkZ9Dw9uC4R4rc9nXCNEl+gxlnPcBVrWRRF469QjOXpapqgRG2Hyjsd3jgqcBy7THiPb/BnaoIV1ym7r+jTLIbIs1nM3ImRZtMEk7bA1hv7+iI6WeJ2kNBAf+VWyiHpv/krOs/QZNtkmPboexbPytDwug0jIRUkKYxKi6lCLZnZPlhhB1lyCBR6raMbYv9dJaxoIkOTNaYrhO7hPMKmLUw5ngCEd7KPX7oWzaOP9mD80Y239AMOmgWj/+OaJncBP8qLr3yEiB6oQtDQrMAtHRuNmBjoG08OdMglywKSw7YvMDF7X6xgT0+BLHmPh63AsKrQJx8NioBZ5SvEE6SR4hbqetSiDHTzQqsWIUXx9m6xlCnxYPx3WVZWrAiXYygUK/Q2Xdykuq3CErcBKEVl8ZPcZmYS8LNDkUMFQ4WWQqWqEIPr+eJaupm2gPUsdscvKidpYbAieJPUeiNXVEddGf410idcxLoB43O21UiTMnSXuOKtQUQbzYiuiR1jRxAzUivFZ2ySHokuMmrUbtLERDQGV1UFVFfi6rtBRvr7GGTvWTcqspP9dFpOyzWISMopvCN/eTbd8x2ex6Oi/4dWE2D9MS5t+V4otc3rEcQVOrAuVOE44Ez9D25VnuuDI8T0qnuisutuXxrVDrEudevolw+I1r0U/xrWDHh+wxsrLpMA3N6rbAd4r2P3ZXOMsd3ZzVuBbnHl72w0IQsZ7mJSoVSeCrT89rk8oKesC0dnQEs89KGHfxMGa9xWKvS32zdAf46Y8zMyHdbZKURNiG7B1ht7vNOjPUXFaoXUM29sIISVDTHyLu7wx55FdKuxyAWfnmN3kKW0VAS8IO63oZ/DZMXqy+IRO7AgYfqPypaS8sySDC/Tl6NMQStiia2NdU/5iytIgu4tuiu3taEk2qk2e8Y9fvMwjEpZY/tvdJDGXBba2wxh1wEP1IYcbGfs3bvv8CWH+ddQVzhWxFStFdrs6zCsyvdGxJqtbUAXxx7aongbfML+rApwYDcjOOna7TuJ4E+599rqzNvPJjXM9CetuNqHjuoohxx7q0rnAf6o419mBsbzMFyhFy0pE7BHauUNxNr4uixV4jJ1rLpLs1nBv48EfEd1u49old9C58VnYrWIZ6XbZ/nWT1Gn1FaOHT36xGOxfgTSC62fQ3dqhHuIsGcKVE5Jesw9+mxqZXf4eXCGzwoz+HtvA2KzvgWCBcprBIzNppb/6eP5+Rg8h8uW0vCySrMSiu5/FNt2v0isOSVDOJP26D+6Sz4PPT2iFkzbbrLsmM649QRgsvoGfQezQJL1maRPhJd/PJMrBp3c2enpXMWiFTXdM2IXX+fxzlJ+BmSax6UQ/PnOTsjcgxDUg7A/9hr49v0P//kz8o52Jg4wvzjes7W3R+ifJaNAN28PZpq8ZqtKIdL+oU6QOb+0XQGWDJr/9bGMVwmGrXC2E3SujGMguUFkRgcukIp9EzcFRQYf0XPnQ1WuqOMTJE2WH5qlQbOQdhRU7XCD2dvsJovDxY1Uk/BlyCisg7zL3M8g7o8r/m7vGf5SnefEBPYJpOUORt/t7k9A1svtatNCHZWu3ttY4XS0cjc8Wddj6GZh0y0aO1kHOtxNC9aCuRHrduZzSO1PtxzVHHCfLi9AJwjjhlLB5/0Qk6KRFXwRe3tXr64yLhO+DqA2qtf1T3w92KvOX1z2DNPzyM4jvYfD/u71va44bV9L8Kx39uLFxert3T8TEhGciZFlqK8K2dCTZPbMvFXQVVOKYRdaQLFk6v35A8AYQiTvAi6iXbquYSGQmPiTumbbOsy7r5Dk7uzs+nW7ZkMmNG6+rAnvceosqyBK+wd6abiVOPFMQ3L3SeTXSlXTLh1hcYgd56mMfzWyfzRS7azuBmRi/tLktDsyZ0m84phqUvAx/iLdE3G4MeUN0cETDhq83uSwCdkm5uc1PqqOrek4ofs+lf8uzvQkhXn2F2xDRT+EgMeca+gasvy9YziaBt/oNgfkzXV8LyvoNMbNT6XgQ4oj+NeB+HdDSdcSuw4PP2dBltEXlXZaXVCU2uhM+7e2cj7Fb2giiTz0qVD+ESCbUtNJ9tF9r/zO852Vq2W9RHkcpGEFnDRYPECsbDlQ9Tvxtx2tmIUMm2Qch0uFuHYPI/K1CsKAZS4rNcVYU8T7FPbC9YBggmOFbiJ3B1gsJg+p2ZjTtor8/hPuPg59E0t5CzZJ53PWpvH4gLImSISY0NB7WMMCGuMciOSH2c9Fk0ZdYAkwK1wBUV9fY/r8z2uZzRC7u9b5zQ0PA6U3fsC5rqX1tSlXRMfBPVBR455QEdaoQp7V5v6jp4kFrW8rjicUIIPpDKa+pD1hV5w8STcXfbpr3LHTjbFr5GvEWumk1ODgp7tDbjM96Z2LS8c6x5dfQ6mLMu7cVzcvxLHckNz/Eu6cXnzb3/X2rW29V7f4LNyOdzsp/VHGy6TZCRZa3/cINd+TQE5irGrL5FKc/PKUANt9gcV1xNge/q/GbQ/2tnSZoSNFHn0vSsafQmhPVN2e7Mmc7cldRL7z1HbzPtdcyvH+f7PP1e/pW1z9PcVfRKY3/+4RiwvIhVl2lNn5HU3Q5TL/mTs+VADZOGxAtP5+R7qpNPlQHTvf1bgyMLmfF7OIZy1b42r95C1C37gB1Xe6sNfjNic+s5fER9KaG4lgWOi+QmNI+bjK46BI2L9x5lGxPCbFEHU0jwJwDu85iLQ/0P0Xp/mTlw/qSbueCUDg0u0MQ8jjcD6sqnYEfTl4eYc8zHUkdsy07VOtBp0fzZ8djnj2hXcPrXHL1TO+AISt9s/QYjM3rS/iVRkDU9uXVtDpPo+TsVD5WLrJ+s3GLtthga/Dv7dzAflbh6N8vDlQgDOcVUtWUV9Ri2CPbZvPGM/frCnj32Q/kp/sQdmfbLSoKf0zxn08xbmCnSHXaPfIyy0+HG5KM+vV3v/vsGG/N+15TzC2iwdQ9Xysfkt5mxM3ZbodHXf/pjJYWlIZ0HoKONfQeoq05fptiC+89VSO7n/E2CXrksy2LG9Zfah9ukIsPOOWI8ULbaVIeFWUlheMxeMNF0OSW3OowlG4LrGW6p9V4pj/z7HS0dE9NWe/v/N3Ty3pe8H1pxiunju7D21QdE5qOvfmcIHeUlum7SLdcgwNbhO94bVj0uK2oDevGeK8f0URR+Nm73VZLxU9+FWO6g8LJliVNaoBmPW4RFXRQ3u3ktYl8by0Nx8Dl8JSUvMN1nYaSWKrlg1eTecAjq/rY1XNWixad56c8R+n2BcpKb8m4ZngblbqH0v9i3Svvo+dmyHJfuH+LBDE87J3Z3el7iftCcpVuEyxqsDN6prKL53Equ68q6zK5jKQhU+k4mja+YRwNm8pG1QxXZNBZzStj3Bh2/HE1NkTJJUKhbSquObSBxTWHtnbD309WGoKT4EB0ybKgXwtxHbe4nh31wDZgVcGqwIsCrATaBc5JdYt+RvnuJsNjafEXyhFGsdudmvNHtP2RnfoL/L5Xr1wF3mLatPMNweUt09s3Dw9xEjvn4uwWGEcvOpLLRdWKqcodliPsss5x+7NTIqtmx1yq0n4aohLJ2xyY088x22rxA+1EpnOW9Pzp6Q9vzC6ej3FeX+rM0j5Mm0e+/4kiP7rTuPwQY+dWfkAEh/aQpNicbcnQ8zFLdp7aimfuEQgU8/dR+sPbqm3A11sXo/lenftm2aQ49M326nvkaUBqPDSZFDQXI/30iROeV+bxP0lPI+8roi0bpj0Ie29wE1VwiwoqipajNzpWL0j9G4dn7FFqvMTt5kT+Rb85VWUL5Hvn9iaKfV1xbhel7FsDN5s2LKtlEe6Ex1NJvWbwvK0WOmXzfA9W6OXCLTpEcSqOi60V4TWqXtQ16+gvWdmFf3e6977domN5/xhjSSP8M7kr+zFKd9dPJpNc48zQXwu8aPgYYxysI7fU5KmhSVHz2ptibhfZ7LuoNq7+jB/IEmNtuGr1Nm/ZvqRT434t0O6vuHy0xNeguLMoPpNxTIvkNaC3nX5RELBK9Cji43KW2raD+zla4G3Jq6IVlcTejhyDlrTM8FLy6HGBc4uVPVZv3r3NLDuO/t733KG0Wgf4krBm50+8z6goqFQwztFr2xYhc0nHrewRPGPXsdfgGjtlvd7/mXAWeDNtiAQfcQWqePgktkDoY9quouCnsmNoM4omoU+R22VYPZiHNhlbW2jbBTt/9H/u2AZlcn5Nf1W0rLzMmj5hiKd9KCvhhFFraMZe/6+w6XxIYvQPURn52e1scoWTF7VeMGo2J8ATGLSeOcEko3dtYgVWdF0buZ6YtWdBc5920vu4K9pZmvyed21xp+vChMP7KInSPh6U1cSvCH65yOfazu+ZCL2tsnHc4dsECTVXnWfVJ8mvv1tOOAbc59H2R5zuPZ6TknuBYec65LAT+TqNbVP2eWI3xvjV9o617Jx0+lq9xalLejhq4zeyg+9YGExpylOeVumr0DqC4Hh49+apVSefVvnYCLtFUZGll1leA8nPwqCBIyILco0NCBumuhc8jB6MDcIPOuYKjR4eqsWWH3Znu0OcCu7ZOed/YbyIj+d9c7nLYzC0ZuQi1XmUr2d4dXelN1HezEec9snqXSy7E2O6rMspMY0A95PiyQeHiQ9pwr1TwV0X5VWuiZCRoX0MrYvzgdwbozU4QeiKgCx8rN76eo75acnyuaJvergPnlfFffR88YwoTW3YYCbnuDn3Wf7ieEFHnYPXbpPbQx5tcjqFXMwUPsAL5wJWkxmN09xi5OZZhA3JZuOa3nJ8+e3pBjs121NePfRr7v2v6ABqqLp51+I5eL9CP8vzSlbrNWDl/GWboNrBOTVPxeYG5XEmvsGhNzWpzjUIN6cDTN2kG6PNJaDXXKZipHEZR4nlORZbevbPUojF8U+fsv0auiGlLjCH1mhejsGyI2jrvnCeI16rX9eA2fq9a5PARjpx/t1i4qycjdsEOb1IK2IDL6zd/ivxU1jNT+gJJRYpx7L9hhT9379cFV/Jdfx//eWyqtMqeGSWl4KLP9LTGS3uVQIxry9GjjopMP6wSIER0E3rPdbfIyBpLNv3/25zPvaA8hzlIXh73SbGoN7zF3r0+wMpDnYIx22Hj2V5hOMeDByzqfm+FvAzL7NsLmaBvelIFmtwsrS+Xh8xTT6tg5LCGC8QndMAjDEfvMnJ04huGFkBat2Pty/z7GCPUba0YwIfezHosm4RsgOl//GY26u4RZHjgVazMfL+pQ7k5IlZ90R37m8kugCOa3AR1rHe/YSc97AHZ7oRqH8havuIdqcE3UfFjzVAQTmH/LvFrPcM9yT5IstqwZClF8/HCn/A5crhvNeYfb1bZLGAIOBhVwr/4mlLRKP2L3gQuT2lm6Z41y9/txnoyux4nV7kuZv7b0QCnbWDRhbr4iqnEzmBsJWlY0D+dZE6S4NZ+Jfld0vLnEgOXA8XiYlANGy89cqr4mO82yGnCHH4z33lNW5QvqVmIhb3NVtOOjteNtreZj+/oZx2b3n286n9ReD66/iHlefwtYOerCe1d62922qp5hHk1kq7grnNEpuLOUxppynbFZ7zJSHmW1/zN7TNBW13yWnvnamXC4AWMfr1w9VV7RtvV3X71wcQsQpNpkfPiMGMrSbiw4bcYEYetvCZgIxu+zTGW3PaIL5/RAf0LcrjitUaEEwUNoGeVp5dQ6+qwxOEz/DA0ZhrOCQRzmtAUJAhVHVSajUsF4nxfQmAS3WpUnlKbAHwj3RSaW9LoU/ZPruJt1VY/nk8ZPhYHpL32Y4aNN2edWVpiYHbRmP4gsqfWf7Dd9vc5PEhyl9I12mzONo8jYO4OD7TIywvnrGW6R6ROP6u8sHMDMTUfy7ScH9zk2Qr12JZK85QyvI2v3MfMkdp40s+ZRUDA6Po3yG6zPIDbjIqz4In9tZvqIC+e/qexMXjFK9OvJ4NAqO3z+wZH7IqY8YFyU3of3wkWQ+7iB6FhwkC4fjldPhQ9xqna/+9dOQdgS/peo4fUJod4jQq+z1n/9kS2SpvT32f9z3f/hyRN/hrGE/mvgcWbGHF5I9dQ0tr5jr3c09YEvjXPUyMozfThsg/TuiEdiTc/llZRtvHtbx+oxQ3n/4zhd1uemG1oj2q5nc0kOwixPApOJ0H/cvY5hioLuVyck3HWca6fMcDb/5itexVOgCb1ySf8fQMvr7hyhjt4qgBhbnd2dIBooZSwF+Dk8DL/Cx3DE5T3U31ft3pPvPO8hYdkxczvlqLTC5fqyvH99utb5Y6921tvGe1e+dn787nmvMO94b7PHZ8Zo2ZeAkJWM89tpaZ/tjSbkMxSnefo/QUJclLgJkWLekafCeYwokdH/9uvtnWbOKoB3WWt+6pgnd5b7JcdP1Lb3+owK1gqKzmbLEolK93bRjXa5W7ItHvQ6DaH9BDdEpK7PkIAqntIZ9RmKLDMYr3q3hbBvUZy0sF8HBpx0xrjBx/YzZ8BLDmSuc9OmCPto5LQEHWYsuZC3/OdogEyvP+UqG6Dl1zz5G6k2vOsusZS/3+RiGyxQv22c+5fVwjlExux/JHvia6PJ9+w/B3YJvIzNg9rz888vq/hry0ffcX9LP4hCp3uLYX6rDmXt+qC1YOWg95fhYJJ1y9SNw05yzsGxbHya67h/C7sgeOknyb7Q9/l1o+o6g45ajNZbOCzqOa/9iESwobjOm2aorQ11tCRQJtAPYB96W0WIuDfsPYqBi7OhyzvIpB/hCv4x58EIBdZokgi7w7a9wuFUsfF0B88HG/V/0jPrqIcB/9MLhWBGzOkTtr16nb6gGj+DJGya76y/9ltbbRz7P0Id6f8gi6pmG1LLx4LvPIWxrg8yw5HdLuMpYHjreoOCXlVfrAbV/YJVeqo1Ng6ar4FBb54QblQ1yOvntJt2u/z6bTFL2ZNu9fai59c9Cx6O4xMGxO0LJTvkW2j+lY8WpecvHYeIBe7/S5mvN3Xl5dVYGizmMgefvxXIbR9Q97XYGi7nNVUsXHqJDfhvp/lvflr5ziOdQ87srcl7uvGb7PMtF5m5aTx20UONrVxXM1Tf6Ajkm2mgj7zaLAKjPYg/QK3XQTdR8X6/3OUHpMuc/OtY7bbV7F6Zy2W/DVeFxp/rqyLI/3eZQWh5jEvXK3KsTR6Uod7hv1joTyzquFTe9O3+vVqG/GBudpNu1G2GtFjrQV3cc5VtUT4if0mXqHZ3lBxOiKieE49badM8p2js1zmarzV/9qNPfe+7VfjRiPAXn2FGOrhHyXclU0frHFr8PRtYeNqun2DkDcYLz4mnbjPys/62tTqcVGvV3lbXfJ80zr+uGhQE7XGcktBhcG76Ny+3gX/9Np/nCDO2Edc2QONzuqaHokOq3Z/MDuCvn/j49nmJ3roXWCorSPw+Rx+H0fbX9cpbh1tj/WdrPCQxT1m2lTUU+eZGCMsN3VzamHiISJyd/ubrpGOovRT9M13BwOfdscym8AWCkAGof51v4rbf9blBCr1jBYQ/N3U4TfvcxQ/nDhEtq751lR3KEkeWveEZpX3+eivNlAruM308GmbVphyG/DNwtYjq4XKKNuSrBiwwk7z8LL1L3TyW7+3hQPcsnjMSaHyHVwl/Nq3Z2/2DQ7y0i70esKrdobqNFwI2zIwDHDbq+KYTN3JcPEOKxB1KbMtGjbloVuqzZVWbUqU5ddd3FtSVp8Mwn6kiFaEo/PZGOsXSbZNGXLQ7uDNnVZtSVTmZklqaJuvZKW3xBOfdGQrUnvfIzRonR9S21VTgczMQbFQ7ZuO9cdoWGbqpbaprT4VtusgVqy4d5uTUR7q0mxsG1k5Lgyu5nwuKYUCtDKbyVDXTjkjMh1pWM6LXJe4cxgbjT/Fc1NliTfsirycpOPaJqdBA1juHYyrOhZWvy0ORmiy4ZohOrt63l2IFdSX6v9G/3u45KPIW75aqVmqJNOWCtGI26EKqi03dPlumQIcLxPsv1awOGrLSub3WSFxSF0XzLg7OgWPcXo50eUHB9OSWq5zbCIhmUUtp7ctMWdRPkrKhqLB7gywgj62ltzqqsd/saOup28uZvq7Xh/p8/qdnqNzP9EBYm17YHVl8yQkwjrZ0WRbWPSsk0NdSKQ+v3QLSrIU6dNm6xwAP6LdPdLNX3tsxm2Et2h5OFv/Y+fT0kZH5N4i0X4t19//3XYZa7TOn/6L2fkxmO1V1Vsox1vDqzGTigDIDkrD0jAyva/uCpxN0Z5HSDwPEuLMo+wufk+H6fb+BglQ3sMCDXdQ6Vpx3L45QM6orQ6hZPprVMvnYKSr7+rZtACKnu8+40ClQbW4n+SU24i24KARovNo4z9+jogxui0CHxxZ5j0mrvY3AFdhWrmYWmmlfmPo0BPekItk48lDAJIziQjAFP/xF5Qv94R/QzAeh/lezRcJPbAEAJB1vArBKkxQKYGqGKndCxwZkmyjMG5kpQFGflh8UMwUWMZo263obrhhTZtqYAoqWXkJGh/DjNG6raiB7Q0imiNgph8MrwM7h7hJQxeymxL1FzBitUzNZYD06LDT6OgC75JJharowiCuYEJRkCfxk06QcU6V+dmgknVhAxodUE7rwKFJiiYDH7iO5hj4a6MSnRTvdVNt2gD30GdFcZoeVlssV8WjylGHQMsTQal9gKmRxD9n7/97Xeu5XpO7bVamlP329IBAN4ZnnnTS0Dr3odnBwbzLjoiJBjhJgNGd/WpCwKgmlq3JcA9pZEGmeG1fUiUwH6mVXgEVEkfKQiqlF+/mwhZigmygYN4tbgyaeMJYCV+vDI2qt7HSRWlB364Y4UpxfBl5PeWiwatuljjT48GEms2LTewEjMdvBqhQVG6b69m8Go1Mhm8JsNVey1wGQccrbSMDP2Piz/o6FTRqWv6w46YvJ1iEqML4cMQ0c3HfjAaqBoB2GOM9rcgYBCrGgYQrTY6VQ2z20+DieaZVAMNUH67Fgx1GsYIzAoy+BTmVMyghV3BxOqjU2NDOxs4gc8oucaEWnEtcIIsNB849fesp5lFt2+5Pfol1WKqfaXPTH67H5fuU+AgBKLmn9ibdG+Uz47Y5FW+10Z8jXP2tizTjv2PozgXLvIDJEtgbHUqjwAueaQLQZ3y9/KTw0x1dG7iLl4xzIyafAqYSSKijAQzJnzFeAMaE6iEWcgxH5Y+sInjsQjqm9/gRquwqPFNCDGY4NWMc8aYm+NYx6JOMdxZOZMVoM8YCVMhUBFGajIUNivNRbk9aPeC+/ZqnJ3JTsUc/VyHMIWLm35Panp8jbgrZQOvG1lYrlHR1f7jFv33Kc5RFTtEfOI/J99FCQyKw3x/NT6M1srEj029n95ejL1+uM7jfZyOcEHWwA0u74KsibMZmH5yKGAnED+h/OW+SuMi7OY0EdO/mQ8zR4RY1elhQcs2NSben9JdgqrIX2dlmcffTyWqE7xt+i+q+Q5FCTQw/XXMczmhZnIhOeKQsySRjYNiVKyrjgx96flAt8HqYo6MbTvM8qbodjhnmnNGMPMKMOVo+YYSeaUzwUd3OiQI0j+vA7+B0BCyXtFx31AjrUl7QzwbXC1mWJsQVOM7K7PbMPNwVcwOviQLxTwPbQAlIKS9wiMbSDOdaukCs8LeYlzaDMA2vmszvw8xD/d2d0Tb+CHekk/d2nY5YIPlh+QSUb4SAArUWwIUYdGvSfL1jY5edMQCNR70gBAqKopEVw0pW8IwURLsIeQaQkWmrE79MIOZOld9bUMDZ5W+2RlskztsmQZTY55KTKUOozyXWUQvM/gkifr6SmYLlEoGM4QJg8wC4NK7WCRoU6Ax1wAx3QafEmVwPr9xgfYtyuMoLTvfep4dvscpIZz8RoBENghaUvLXdI9ApqiOFHO6YiDD32LW5rMH6vjjritGp16qa8DzH6eI5BP6msZijDJENBbYD6/SPYoNNGvo0WLPDX/L9Yk6kHy13m9JLm+wzC7uULcaUe9KcoRAq4+9EynWSyodTRYSlqPuP0qUNIDobPYdh3vouuqFRMeoKNaQcST0GiIpCI6tMNwLPjWaRYP9ot3u/OYBUzhclwnBbLztUIlvUXLqQCrXMAwuxkUuUVdHzIYwJIat8BQGyrW2BngeMpga1pMv5Ed8+TTRknw56+4P2c80yaLddMFMWwnYzfTux1cRzrRTR6euOcUz3dxFh2OCYPltG3F2XsKoeUb0EKzxJ8PCfYxyrF+VnEqYydQ1LS3BRJDZTSc9Iwv166vIOdvro1MZLd0MYDX/Td5pQDTiRq4Zfqbeuv2CfhbkEeIiove30jIy9D8uPnp/p4pOXZNH7+/yy7C51heUE0s5wI6YU30MfIFqmQxz06evGYBNFQzXZga1ItCNNalyBl5VYDrwXTyXKE+j5OxUPlYc62vFt2ib5btlJFKSacDIJSdcvAeUqmcCyMmweJnlpwNJuOQbeOKNhK5OhhP16+Jx0euyHBDcZ8d4OzYKSKU8DJqfXwcOamWWA4QN+e+feXY6ClFAkXCN1/w8ykBEKuRFCAQdkWECgkerol6uObgQQG7zFguJlwmcjn5bjutxCPkMJh8isV2aLiSERp+9GLbrqPMXItZkILrOd0GSEsvmLqROhknzy9JzEddq6FTEGnzi1l/Eonlc0Iw5zdVHzeQz3Dbi9dci2qOPMZYmf9nAobpnGtmclhyUhyV4NbHNGbV06p08uDmINaCvmPuIFUBM36dMhS8i4bQDHzlrnTOiOil5CQIeso6GnV6RJQFm/tc6poHNiNc6zIAz9bWOP+OH8jzKd5ubU759jAq0+ysuHwU62Dej4v5hKwXDrP8xnCcZK/R9p4sWJsCmmBwizFwHVsi2SQP5GkhyUJ4RJj1GCPCENuN5T1twXlj7SneFmc6G5gS10eZI1jhjWnTaadOXrETzn2dXUvIS1L8uG0O9IvOfZ9+inxjtNxlmULTOaRH7k4DgjDjg98XvXUJaLWInE8KZ10FQMR2fD1xGc0O2WGGaZbp7h3eP8bFKDTnrkawVkg2x2/24bAB1esx/GGtFJTtGsNy2rRYYOdyGA/shTIhjk4b1BCLtbYmuwLSHtJUYR79n9Ioh6u2Y/teB0SecrZSnPK3yE6MAd40DzYcpkQdTG+bLK5gD0/osYvbbPeAZ36sYYXN5vsUIeAPrTzm3zYgg51FeUtlWQ2YGVsBkKNFgSjL8+Hoy+HK66dQ5g4y9HIQWMUrNAWZjjlVW6Jp8uOKwNf8z+DkAa8QTeStcTX0wf/6Itj+y0zAUHvez2INxlIwr47+O87YZVEsuWshgdwp7hgGkQEMtdzcsOuGyb3vKscb7m+iFbD1epXHF19MOpGx3mq14sH4bflz2tiKnj1adVEvMBh/tQYZcI1/tHGzjAFRKKlrYExI7gPgFpcmBybDsZPisYPCEW+JTtt9Q/64aUrzTMKBjdhyG30YBJFWrSJpQ+xYym4XBHa2UTnUDEWcBtUWsPKdD1ZjrTVM4Tb7U9I+fcDEgh9B5HZBZDFTIM9+70/dim8d14oqRw3/QdfPPqdmvi4cFr9MiQIL1e4pK9BkV1QXOzWWeHcZDCVv5YDeM/bR4fAwU0qmRboy5AOQ+e4PHTODRN8V0s9qHhzjBv6DNSKEZugpZRv2vSz+f7VXRWt1MfO/jbJsMAhtWWig9AyGaOMJmJ/pgdZOMEr6QN1MgOHX6mMxGqrLTHaaVWV4FJ48PUf5y8bx9jNI9usU94vyU4yq2LxJ4NQQstNof9b0MEYE9Eat/CYQJSK8weKj10KlI0gDzgAb54w0TE2CCsfxkYPjHCZ3Q7uIQxclZWUbbR3ICdRlLxh/zfClBhh5Q8kG2J5Bi8WlYYL201kLxhEMSDLXJcjfNCz9jZ3Wyx9AsUjxR4m9qJbby6FQMERN9m/kwyqSZEl6Et0AoE5sqOMq0qqPlmwO2KD8lVMWtYVcxOJrCYDLfRpWcDH5Xh2OWl1i2BzxYb+62j2h3StB9VPwQvyyjiZjJNfNBf5rOyMBwHHwJ81BMqHMYvLA66VTYSBin+0rGCZNkTA8VRoZBXovXBxVWp8VBBVeUZPUlQ1AH93YNlkyFFR8QiP4YZvJk3PZewEbppbe7QOSbDGjvo+2PqxSvD7Y/gp6aB4GZQHhGJCHN4k/PRJrpVD35IZoId/N/RDI/0I34pMQFc1O/LLnJkuRbVuKhvTm+qxqsZ0qugXR+D/9eVuPufbYZllO6xKYsnLqu/WZwI2BYP4N97qPGrpqTa2s1GAFtcssb1TkFvqofztLip2QUpUiGrdr+PIpTc8KYLzcmMNeMsNWLOGk+4vPsQBYFmg6MKjK276KrHqYk7n5/RR5LaGqj6kaGEZxU2TardMDM1oZI8uSXjHJOj4yfVrbp5vFJtjd0R1SRsd0RXTUzX6d/f0XuSGhqo+pGhlH1bz5R16AVuWxl/Y/jrALNkeTJHcHmmQd+WtkmvLhN1pi36ClGPz+i5PhwStIqio/uWk9QfvQ1n0gOYN8DIHpFLkyvRYzqnhKHzAfVLldDJWzzyRDld+MKMsYMwcQUngeyrNzapL5MH8yvx2styFUtYP/dEk7L22s3x9C4O+wXuEz5UnUqXALl7clStkOXcV6UH6Iy+h4V/JF1VeoOld2DLpLnuP6Zaszm9+o8/hD926+77xlu6+h70hfhHM6AcfR8HpVoT0KQ8Ozpr2AlNIGiKvyvaiMRqKb7AlXRfVSw/5RtoyT+J9q1LQ5UBNBAVQJkqsqjdH8i13X5OrtPYFXdVx310F2Zk73YIjvlW7A2kEyoJEepkOIG5Ye4KDDG201uTgKeBKqdp1LUzL4D42plP0M1shQqPbMkgXQjP4P6kC8aXNsDC5B3+1FUQ/td01bdNERoro5CZrGOSLNaSX3yipQ1dM9CuQq6LxD/7qNKgeqKLugIuy+g+O1HlQNsYuZ+RuVjBnWdIQHoDgc0qjpL7J6xG3vCwynUbwbfwRpZEkWF/Q4TV1f/Caqm/6rqRe1Eiu9C7Rew/7QfFez77M4c//4TVEH/VQUz8YArH221h9qbeFuecqi9uy+gidqPCvbsAxWuDvYzVBFLodfeEp0GBJLW3zREm88R6UVqVaP09BCRMpBfYz+DqjIUmtirIrLHOTrAzhukkiGSIVSJgJL4CeUv9/EBsjX7GayUodBrWzrWtqh5aRpJC9NkppV3YTMv46SEB2llES3RuFJ6kkocB0ch6wQtlXYvaAoqOgNIJZODpjSV5e6ItvFDvCULLipOrUgqEb1MPriMtqRw8evmkhs/FEvJwZFZWsJKOm25TCTSbdP7CFod0h8lrUW+69XzLcrjKO2j5J5nh+9xGgnaRaeQRC5pOYW8/zhF5JevaQwNBOxnSAaWws46+iZRDL31/8370bCgWCA9SYxxOehaBSbQkIEm1pGGpreSS1smE3lsUdOE5daFTkNu0I+aEqrpTPeEn5/KdJ/AaUz3VbWDFqP8Jo/B1RX1Ddw96z8rKumvEXF19J+gKqqvSu4Xz3gSkkbJ2al8rPY4a/ct3OGRk0NSyEsopCPh8wRLSuobVC/5XGy0lpWEVrTPSn+UVKS350qIRZVI+TcUOvz/zLPTUVRJ81FSU0OhqKmJzs5V0vwO8W8+aS6E2HzYwpUQSyZbCrGUCingrNycFDAZJAVMqSmFpGZ5bXrNKHAv1Ddhc2qttqic1HAl9TdhJfVnRSVg7lmuOpAKqhgk1NjlEyzc+0+inT2tJTqb1lJYi7jRWAqlSZmcd4Axme+wGRkSpXrDFDOAikMSWM0hlWpjjk97wu/Q8TTgVh1PZlq5aAYlItQSQ2/WJExSALS+gBLGgYDYUBwNOfQEUB840CHN+UMH+it48EAT6FdFIuZLq6spFFXWRMpzRUgzoUY6mgAxnOFpCEsjmY2whMoFCRsAFliCsASymeOAVNWGfbRRvvX6b2C79Z9V/pEJasD7RuYz6BcZCiUsE+FCgPoGQzHRnOJ/zcWVUN+gSqjPqnkUShFebMmcO08Czqs4KtUSEfNAZP36HTy8HnwHl4osifLULwNPSprf4VO+TOPkpw+PyA9L3Sf4iLf9qiN6tzMEa9B9FiqivbVUjwPCE9nBd3B7giVR7uiBEYqArT2QDt7jA0n1BZFXr6xUvb3ARCPjtxOYz+D2AUOhPF09HKN4Dw06/Sf4dLX9qjz+JKPBPTocE9jXcxTwIeiASGMP6BMqS5QrxlYRoWh/CKJVmiAqTjn6C8X7R6hNB99h9RkSvQo/xBjcBaw2TyKplqJS1DwIDsVVO/gO1TkgUXnAl3QrcYD0V9D/0QTKjb9hkBlgs29IAm/wDam0ahZbdfBdXKeuVYXBJriqhZTgrRURscG9ApkjAclU9wy0XUp7ECyRgCeR3UzRrrk9tBNXzFHIzv50q71FFdlOfIVoSACvIlkalZHzrCgw80RcK08CGpmjMrytqbhDKSfXucW5qUj1z8eZq2ziy3cCOvXtuFtU4Ck6uUWvcfmv0VZ8x5CjkN1lbIiQuuJ2a1pyu4Mnke1xb86OxyRGu/usoY8NpFDc8YDJ9KShy+gLJO41HIWeGA25WoL2XpnG5QTNOwqbnk67l3S3cPUuQOtfhGYSc+gfxnNhTnibcCSi+8wslcb8t3s0DE56u6+imW5HoHGJVVwV81V0lVW3KvFjQhHWAFIJ5ABqE4lUYmjUzVVIvZSRv2/Y9K8jqDLdFqWswPBhD5V6cPBUA4sgfobBlpQ8wai56Dyp+I1VX9c09HMUtV1g6mBGgZ7d9BaRvqQxNwc36aD9WLG5a8zOW0avoFhV0eMXoqnqQYuUE+TbQa7SRyzeTXkf5fvqNpKxKZuCYgMIFZYpOE8T4mFM2h9ZghBdkH6sVKsMPUayU61+cbSpOcLK0SSOQnIl2NdSXTnBOyhzFdn1wqZdMKBmlRFL/Yl+YbGS8Jsgoqj8pY+Ey2AlBTATvVgKYkCxF9EvLPEkgLIC9eZiMvo91abjC5gHJAxgCuiNWG0C6dMvc9WbRbFM6SGJf3UHC3xSTvRC0F7FgenEirKE3ltpRNW7hWW39yFxnmJi9awBnDFIFRds7jAMghpC7ATFxF7acQ5meB8nVa6AjrPECAPScCbQwpCD0m1cmZ61WGuONmQXGDwKZxiIXnqbm6F9zSudGvNEIabHwxfJpKTwtbHFNLl9Uso8agXmyhCdWGzokS0RXfZ4lp1Asy9m69mz4ImvhdrMq9tNxxhQHKb0IjhbDnwpXBcffPKufrsHrVYfjo3DqwHJPxv1uyekknbnaPy3+PDMpPZmomfy5mrKjlQkkxudYmKlhhEIiFKi6AJgSdAs/Mew5hFPeXSKeWrzSc3DvG+WdBSQzn9ngY726hFFFqPAByqYkz+jfgOWDNl1hEaCCYIby6QXgSU942GmRmsPdY3ANSwUElfQoM59C2kYEyANC/mfq0xkFiAiCrgelRcIiRMg9AvDRBbLxXoW2+62XT9c5/E+TiXTWI7U/wadAaYcVGbj1Ij1Zegk7QfEzakbThIPZ3y1hTFoNnTgHKE1tIorVeTD/dDKiiP4yHkJovfArFUReDwYuDGJemUoLBJsiWjbAF7MYmSQV2iKblncrzSEluBpQ6yLBOGlaEN4XBUNVVPjgSMNhoZxTTCIf0VPkYW2EJcJOdEHKodM5HmaD6msRgtIHgwx05hEEJlMbRxFwWBmkkcbo7lphhCzOHeXREsThHoDT+Ut2EhOddV20TOIBlc2SpSEtyDyk2+0yis3hbCU28immmkP4G+hS69LiqnDnV1wd+2Z4wvqaxBjSC48iqmVSgm0AdSYh0kkEQt1V83aLEZYUGtEe6QtahKvMYiJ1YO4TulgXXRp5mRDYFrZlGEhNhAUrZOYQhaFc6kmtoDp+NjUsZY/EwGRPLmgpxJzaRVXmkCqu97sUSyPlKckfqkvk3L2oOvUNayMyYhG0eCuwTl8B9eBrrrsiLid1gmAYWo3wmq1TSrnM4pRdPgyQetkzOFAdPbTeM1hfvSxPNBxThueWHnrECaU7BU6XzschlyuJ+2icMr2LX4XHY4J6hmL23xA6Un0EVu7CxS9YV+v8SoLKCWnBs4v8bgA2KS4OLa1i/qSSSdP5H+aGV7VNpq39N40TyQW2f7e9DDmOCnZ/+jveYDu+115ATXEHe7MqzqJ50elAk01HpGM/g53KhPJQspLH13oFQyBJp0w+4SbUbh8c9N1QfOldgKoQhiFi+5PilK/+lGXROZX6zsgC6Ywk2eg1xjOH2Cp8oYO9S/Ql6ZRiMwkFOhFBhMF8KX5gn5btmEpbdaaxlnQaVqT9ESarazHUnQeRR+535Ko/xpvB0E6seC2DweZXBekFJzKwlZRmW8aUIRwS+HUg1JvbPo7seLLt3AByerN+fItlOiD4SJN3OHJNI3dNe1SUzu36yzM0SUTEdtgSOJXcS4RSl/O06Knl1+ylOWJ/C9lw6vapp/Z3OD12WNUoN1fcflI1cArririTR2m7DDBDikqTJ5jbwim3/b8xWaAC3hSBCwp7PFamYs8meYr3e7a9mFL+fULU5mnS3ykcIgUSQCHSCdt6suB+ZjMVQRSLEmnQlL6EBMjSUopwkQnR5Qfsyj7hbqQX3RMY5o2e5W4Twwo/Co9zO9Figlzd9mrR8b/nq9YS5bQk+BgSW58lSYSs10ItXG3lEu+IeGS1nxMSjSFwwMpw7g6INVb05NlGdwcNrg1GltI67+9QwcHGmakUzy6k5ErVwReHtmJcvE1vV+RX8+DgWQ9Q0wconNMbgrJglFI63/dOLYZ4AyGGyC/IuA5dMtK8CJKCVkDR5XlUc6Lvyyjma/RZrxhMx9urtK4jKNEMpeUFfA9j4TzOzZDjyJno7sx2rkyX5XaLsKygdSVcoIn4XpJNy1CXfapJjdc2knecjJyyVAOp8Wsh3R5rksRHxEPzyaRDV0gXYhRaxy1VeoGV3Oonh+1uCSl6gNkmDrYOTKUyqo/iJNmpLK5eEcnQ91c5tlBZg8ZeQiDwHlfm4mNNI2rsynuMwNDUMRLN0OXtHYjWb/xRP4Xblzi3bqkMKeujWdPgFQHsG8HKdVtbX8Ji0vS2/h4UWpcm6DlGYldGR+i/OXiefsYpXt0i03bJ3YFliXKQjKjsJlmG4PAWWTZFQqd/LZeloC5bR2NQP7Q1p6lXpbaYILZzWUMw19CLTlxtr54LU2oe1ff2NfJj+vLLKr7+BqlxMq6X86fhbk2bEJfqZFYWrFiUJJhoo8sebDINCKDBEMNU4cuaOhCXtWaCWKYtLkbNms9byMJtcRpMkxp38l8kJgHTA9MWMiz/trceDYxh4Q6pDnAvL51P5Sm67U1R59yeNOxFhkDoPWuCMCDT69MsZGkTLYIug8nI5auWpVlQqxRFMmYCSPdzMr+zCTZYFcV8b/PPq2Jhsk5N9X+2nmWFmUexdV0Di/mO4i06Svusw2f1BNYDvvirUamRa4LtvEEyU7rVlQlMPVgdjrZmYYlKXK5UkZ51SY1CZW2VRcoTKZX3mqOHMOjDkh0e9e+vROllXUzbP+qT26ujk4uvParwQnUpnLz6rY+k84XGBvcOIbHE5DNuB4+JGmK3QzbZ9eRm6ujkwuvnb1nArWbsZxLs6w9qgnKS96z+69rhJFUqCY/L1KnrPbYTMwHI6OzJTVVF+u7HPNZwU1pWjuuIyNXv/mcjayOrzUg9L8G8a/6u99qJpXhcSujvPv27rc6kXzzA/6z3s38nO1QUpBf3/12e8KlD6j+6wMq4n3P4h3mmaJtVWfPtKW5Sh+qpBYkBflAopak/dzm7kFltIvK6Cwv4yp+L/68xX0JT21//YVcyql2Fr+j3VV6fSqPpxKrjA7fE2aP/t1v8vrf/cbJ/K4JGOVDBSxmjFVA1+n7U5zsOrkvo6QYbFyIWJxj6/+J8O91W+KuWaL9S8fpS5ZqMmrM9wEdUbrDXe4eHY4JZlZcp3fRE7KR7WuBPqF9tH25qVKfkjtGIibqhmDN/u5DHO3z6FA0PPry+E+M4d3h+d//B4DvA7P7VAkA + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/201712290151517_AddressFormat.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201712290151517_AddressFormat.Designer.cs new file mode 100644 index 0000000000..d3db49312f --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201712290151517_AddressFormat.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 AddressFormat : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AddressFormat)); + + string IMigrationMetadata.Id + { + get { return "201712290151517_AddressFormat"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201712290151517_AddressFormat.cs b/src/Libraries/SmartStore.Data/Migrations/201712290151517_AddressFormat.cs new file mode 100644 index 0000000000..038f51ae45 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201712290151517_AddressFormat.cs @@ -0,0 +1,39 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + using SmartStore.Data.Setup; + using SmartStore.Data.Utilities; + + public partial class AddressFormat : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.Country", "AddressFormat", c => c.String()); + } + + public override void Down() + { + DropColumn("dbo.Country", "AddressFormat"); + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + DataMigrator.ImportAddressFormats(context); + + context.SaveChanges(); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.Configuration.Countries.Fields.AddressFormat", + "Address format", + "Adressenformat", + "The address format according to the countries mailing address format rules.", + "Das Addressenformat gem�� der Landes-Richtlinien."); + } + + public bool RollbackOnFailure => false; + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201712290151517_AddressFormat.resx b/src/Libraries/SmartStore.Data/Migrations/201712290151517_AddressFormat.resx new file mode 100644 index 0000000000..460884a86c --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201712290151517_AddressFormat.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/201802081830029_ShippingMethodMultistore.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201802081830029_ShippingMethodMultistore.Designer.cs new file mode 100644 index 0000000000..2323322d13 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201802081830029_ShippingMethodMultistore.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 ShippingMethodMultistore : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(ShippingMethodMultistore)); + + string IMigrationMetadata.Id + { + get { return "201802081830029_ShippingMethodMultistore"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201802081830029_ShippingMethodMultistore.cs b/src/Libraries/SmartStore.Data/Migrations/201802081830029_ShippingMethodMultistore.cs new file mode 100644 index 0000000000..92e74e27ac --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201802081830029_ShippingMethodMultistore.cs @@ -0,0 +1,38 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class ShippingMethodMultistore : DbMigration + { + public override void Up() + { + DropForeignKey("dbo.ShippingMethodRestrictions", "ShippingMethod_Id", "dbo.ShippingMethod"); + DropForeignKey("dbo.ShippingMethodRestrictions", "Country_Id", "dbo.Country"); + DropIndex("dbo.ShippingMethodRestrictions", new[] { "ShippingMethod_Id" }); + DropIndex("dbo.ShippingMethodRestrictions", new[] { "Country_Id" }); + AddColumn("dbo.ShippingMethod", "LimitedToStores", c => c.Boolean(nullable: false)); + AddColumn("dbo.PaymentMethod", "LimitedToStores", c => c.Boolean(nullable: false)); + DropTable("dbo.ShippingMethodRestrictions"); + } + + public override void Down() + { + CreateTable( + "dbo.ShippingMethodRestrictions", + c => new + { + ShippingMethod_Id = c.Int(nullable: false), + Country_Id = c.Int(nullable: false), + }) + .PrimaryKey(t => new { t.ShippingMethod_Id, t.Country_Id }); + + DropColumn("dbo.PaymentMethod", "LimitedToStores"); + DropColumn("dbo.ShippingMethod", "LimitedToStores"); + CreateIndex("dbo.ShippingMethodRestrictions", "Country_Id"); + CreateIndex("dbo.ShippingMethodRestrictions", "ShippingMethod_Id"); + AddForeignKey("dbo.ShippingMethodRestrictions", "Country_Id", "dbo.Country", "Id", cascadeDelete: true); + AddForeignKey("dbo.ShippingMethodRestrictions", "ShippingMethod_Id", "dbo.ShippingMethod", "Id", cascadeDelete: true); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201802081830029_ShippingMethodMultistore.resx b/src/Libraries/SmartStore.Data/Migrations/201802081830029_ShippingMethodMultistore.resx new file mode 100644 index 0000000000..c7e857c781 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201802081830029_ShippingMethodMultistore.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/201802270844034_ExportAttributeMappings.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201802270844034_ExportAttributeMappings.Designer.cs new file mode 100644 index 0000000000..665cb9c0b9 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201802270844034_ExportAttributeMappings.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 ExportAttributeMappings : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(ExportAttributeMappings)); + + string IMigrationMetadata.Id + { + get { return "201802270844034_ExportAttributeMappings"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201802270844034_ExportAttributeMappings.cs b/src/Libraries/SmartStore.Data/Migrations/201802270844034_ExportAttributeMappings.cs new file mode 100644 index 0000000000..d99106df13 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201802270844034_ExportAttributeMappings.cs @@ -0,0 +1,18 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class ExportAttributeMappings : DbMigration + { + public override void Up() + { + AddColumn("dbo.ProductAttribute", "ExportMappings", c => c.String()); + } + + public override void Down() + { + DropColumn("dbo.ProductAttribute", "ExportMappings"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201802270844034_ExportAttributeMappings.resx b/src/Libraries/SmartStore.Data/Migrations/201802270844034_ExportAttributeMappings.resx new file mode 100644 index 0000000000..cf1434b570 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201802270844034_ExportAttributeMappings.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 e3b75eb55e..d2c6fbfce5 100644 --- a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs +++ b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs @@ -3,7 +3,12 @@ namespace SmartStore.Data.Migrations using System; using System.Data.Entity; using System.Data.Entity.Migrations; + using System.Linq; using Setup; + using SmartStore.Utilities; + using SmartStore.Core.Domain.Media; + using Core.Domain.Configuration; + using SmartStore.Core.Domain.Customers; public sealed class MigrationsConfiguration : DbMigrationsConfiguration { @@ -23,10 +28,47 @@ protected override void Seed(SmartObjectContext context) { context.MigrateLocaleResources(MigrateLocaleResources); MigrateSettings(context); + + context.SaveChanges(); } public void MigrateSettings(SmartObjectContext context) { + // Change MediaSettings.MaximumImageSize to 2048 + var name = TypeHelper.NameOf(y => y.MaximumImageSize, true); + var setting = context.Set().FirstOrDefault(x => x.Name == name); + if (setting != null && setting.Value.Convert() < 2048) + { + setting.Value = "2048"; + } + + // Change MediaSettings.AvatarPictureSize to 250 + name = TypeHelper.NameOf(y => y.AvatarPictureSize, true); + setting = context.Set().FirstOrDefault(x => x.Name == name); + if (setting != null && setting.Value.Convert() < 250) + { + setting.Value = "250"; + } + + // Change MediaSettings.AvatarMaximumSizeBytes to 512000 (500 KB) + name = TypeHelper.NameOf(y => y.AvatarMaximumSizeBytes, true); + setting = context.Set().FirstOrDefault(x => x.Name == name); + if (setting != null && setting.Value.Convert() < 512000) + { + setting.Value = "512000"; + } + + // Delete MessageTemplatesSettings + var settings = context.Set(); + var caseInvariantReplacementSetting = settings.FirstOrDefault(x => x.Name == "MessageTemplatesSettings.CaseInvariantReplacement"); + var color1Setting = settings.FirstOrDefault(x => x.Name == "MessageTemplatesSettings.Color1"); + var color2Setting = settings.FirstOrDefault(x => x.Name == "MessageTemplatesSettings.Color2"); + var color3Setting = settings.FirstOrDefault(x => x.Name == "MessageTemplatesSettings.Color3"); + + if (caseInvariantReplacementSetting != null) settings.Remove(caseInvariantReplacementSetting); + if (color1Setting != null) settings.Remove(color1Setting); + if (color2Setting != null) settings.Remove(color2Setting); + if (color3Setting != null) settings.Remove(color3Setting); } public void MigrateLocaleResources(LocaleResourcesBuilder builder) @@ -37,6 +79,301 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.AddOrUpdate("Admin.Order.ViaShippingMethod", "via {0}", "via {0}"); builder.AddOrUpdate("Admin.Order.WithPaymentMethod", "with {0}", "per {0}"); builder.AddOrUpdate("Admin.Order.FromStore", "from {0}", "von {0}"); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.MaxItemsToDisplayInCatalogMenu", + "Max items to display in catalog menu", + "Maximale Anzahl von Elementen im Katalogmen�", + "Defines the maximum number of top level items to be displayed in the main catalog menu. All menu items which are exceeding this limit will be placed in a new dropdown menu item.", + "Legt die maximale Anzahl von Menu-Eintr�gen der obersten Hierarchie fest, die im Katalogmen� angezeigt werden. Alle weiteren Menu-Eintr�ge werden innerhalb eines neuen Dropdownmenus ausgegeben."); + + builder.AddOrUpdate("CatalogMenu.MoreLink", "More", "Mehr"); + + builder.AddOrUpdate("Admin.CatalogSettings.Homepage", "Homepage", "Homepage"); + builder.AddOrUpdate("Admin.CatalogSettings.ProductDisplay", "Product display", "Produktdarstellung"); + builder.AddOrUpdate("Admin.CatalogSettings.Prices", "Prices", "Preise"); + builder.AddOrUpdate("Admin.CatalogSettings.CompareProducts", "Compare products", "Produktvergleich"); + + builder.AddOrUpdate("Footer.Service.Mobile", "Service", "Service, Versand & Zahlung"); + builder.AddOrUpdate("Footer.Company.Mobile", "Company", "Firma, Impressum & Datenschutz"); + + builder.AddOrUpdate("Enums.SmartStore.Core.Search.Facets.FacetSorting.LabelAsc", + "Displayed Name: A to Z", + "Angezeigter Name: A bis Z"); + + builder.AddOrUpdate("Admin.Catalog.Products.Copy.NumberOfCopies", + "Number of copies", + "Anzahl an Kopien", + "Defines the number of copies to be created.", + "Legt die Anzahl der anzulegenden Kopien fest."); + + builder.AddOrUpdate("Admin.Configuration.Languages.OfType", + "of type \"{0}\"", + "vom Typ \"{0}\""); + + builder.AddOrUpdate("Admin.Configuration.Languages.CheckAvailableLanguagesFailed", + "An error occurred while checking for other available languages.", + "Bei der Suche nach weiteren verf�gbaren Sprachen trat ein Fehler auf."); + + builder.AddOrUpdate("Admin.Configuration.Languages.NoAvailableLanguagesFound", + "There were no other available languages found for version {0}. On translate.smartstore.com you will find more details about available resources.", + "Es wurden keine weiteren verf�gbaren Sprachen f�r Version {0} gefunden. Auf translate.smartstore.com finden Sie weitere Details zu verf�gbaren Ressourcen."); + + builder.AddOrUpdate("Admin.Configuration.Languages.InstalledLanguages", + "Installed Languages", + "Installierte Sprachen"); + builder.AddOrUpdate("Admin.Configuration.Languages.AvailableLanguages", + "Available Languages", + "Verf�gbare Sprachen"); + + 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.Translated", + "Translated", + "�bersetzt"); + builder.AddOrUpdate("Admin.Configuration.Languages.TranslatedPercentage", + "{0}% translated", + "{0}% �bersetzt"); + builder.AddOrUpdate("Admin.Configuration.Languages.TranslatedPercentageAtLastImport", + "{0}% at the last import", + "{0}% beim letzten Import"); + + builder.AddOrUpdate("Admin.Configuration.Languages.NumberOfTranslatedResources", + "{0} of {1}", + "{0} von {1}"); + + builder.AddOrUpdate("Admin.Configuration.Languages.DownloadingResources", + "Loading ressources", + "Lade Ressourcen"); + builder.AddOrUpdate("Admin.Configuration.Languages.ImportResources", + "Import resources", + "Importiere Ressourcen"); + + builder.AddOrUpdate("Admin.Configuration.Languages.OnePublishedLanguageRequired", + "At least one published language is required.", + "Mindestens eine ver�ffentlichte Sprache ist erforderlich."); + + builder.AddOrUpdate("Admin.Configuration.Languages.Fields.AvailableLanguageSetId", + "Available Languages", + "Verf�gbare Sprachen", + "Specifies the available language whose localized resources are to be imported.", + "Legt die verf�gbare Sprache fest, deren lokalisierte Ressourcen importiert werden sollen."); + + builder.AddOrUpdate("Admin.Configuration.Languages.UploadFileOrSelectLanguage", + "Please upload an import file or select an available language whose resources are to be imported.", + "Bitte laden Sie eine Importdatei hoch oder w�hlen Sie eine verf�gbare Sprache, deren Ressourcen importiert werden sollen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Shipping.ChargeOnlyHighestProductShippingSurcharge", + "Charge the highest shipping surcharge only", + "Nur den h�chsten Transportzuschlag berechnen", + "Specifies whether to charge only the highest additional shipping surcharge of products.", + "Bestimmt ob bei der Berechnung der Versandkosten nur der h�chste Transportzuschlag von Produkten ber�cksichtigt wird."); + + builder.AddOrUpdate("Order.OrderDetails") + .Value("en", "Order Details"); + + builder.AddOrUpdate("Admin.Configuration.Settings.Media.AutoGenerateAbsoluteUrls", + "Generate absolute URLs", + "Absolute URLs erzeugen", + "Generates absolute URLs for media files by prepending the current host name (e.g. http://myshop.com/media/image/1.jpg instead of /media/image/1.jpg). Has no effect if a CDN URL has been applied to the store.", + "Erzeugt absolute URLs f�r Mediendateien, indem der aktuelle Hostname vorangestellt wird (z.B. http://meinshop.de/media/image/1.jpg statt /media/image/1.jpg). Hat keine Auswirkung, wenn f�r den Store eine CDN-URL eingerichtet wurde."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Search.SearchFieldsNote", + "The Name, SKU and Short Description fields can be searched in the standard search. Other fields require a search plugin such as the MegaSearch plugin from Premium Edition.", + "In der Standardsuche k�nnen die Felder Name, SKU und Kurzbeschreibung durchsucht werden. F�r weitere Felder ist ein Such-Plugin wie etwa das MegaSearch-Plugin aus der Premium Edition notwendig."); + + builder.AddOrUpdate("Admin.DataExchange.Import.FolderName", "Folder path", "Ordnerpfad"); + + builder.AddOrUpdate("Admin.MessageTemplate.Preview.From", "From", "Von"); + builder.AddOrUpdate("Admin.MessageTemplate.Preview.To", "To", "An"); + builder.AddOrUpdate("Admin.MessageTemplate.Preview.ReplyTo", "Reply To", "Antwort an"); + builder.AddOrUpdate("Admin.MessageTemplate.Preview.SendTestMail", "Test-E-mail to...", "Test E-Mail an..."); + builder.AddOrUpdate("Admin.MessageTemplate.Preview.TestMailSent", "E-mail has been sent.", "E-Mail gesendet."); + builder.AddOrUpdate("Admin.MessageTemplate.Preview.NoBody", + "The generated preview file seems to have expired. Please reload the page.", + "Die generierte Vorschaudatei scheint abgelaufen zu sein. Laden Sie die Seite bitte neu."); + + builder.AddOrUpdate("Admin.ContentManagement.MessageTemplates.Preview.SuccessfullySent", + "The email has been sent successfully.", + "Die E-Mail wurde erfolgreich versendet."); + + builder.AddOrUpdate("Admin.ContentManagement.MessageTemplates.SuccessfullyCopied", + "The message template has been copied successfully.", + "Die Nachrichtenvorlage wurde erfolgreich kopiert."); + + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportEntityType.ShoppingCartItem", "Shopping Cart", "Warenkorb"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Orders.ShoppingCartType.ShoppingCart", "Shopping Cart", "Warenkorb"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Orders.ShoppingCartType.Wishlist", "Wishlist", "Wunschliste"); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.NoBundleProducts", + "Do not export bundled products", + "Keine Produkt-Bundle exportieren", + "Specifies whether to export bundled products. If this option is activated, then the associated bundle items will be exported.", + "Legt fest, ob Produkt-Bundle exportiert werden sollen. Ist diese Option aktiviert, so werden die zum Bundle geh�renden Produkte (Bundle-Bestandteile) exportiert."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.ShoppingCartTypeId", + "Shopping cart type", + "Warenkorbtyp", + "Filter by shopping cart type.", + "Nach Warenkorbtyp filtern."); + + builder.AddOrUpdate("Common.CustomerId", "Customer ID", "Kunden ID"); + + builder.AddOrUpdate("Account.AccountActivation.InvalidEmailOrToken", + "Unknown email or token. Please register again.", + "Unbekannte E-Mail oder Token. Bitte f�hren Sie die Registrierung erneut durch."); + + builder.AddOrUpdate("Account.PasswordRecoveryConfirm.InvalidEmailOrToken", + "Unknown email or token. Please click \"Forgot password\" again, if you want to renew your password.", + "Unbekannte E-Mail oder Token. Klicken Sie bitte erneut \"Passwort vergessen\", falls Sie Ihr Passwort erneuern m�chten."); + + builder.Delete("Account.PasswordRecoveryConfirm.InvalidEmail"); + builder.Delete("Account.PasswordRecoveryConfirm.InvalidToken"); + + builder.AddOrUpdate("Admin.Common.Acl.SubjectTo", + "Restrict access", + "Zugriff einschr�nken", + "Determines whether this entity is subject to access restrictions (no = no restriction, yes = accessible only for selected customer groups)", + "Legt fest, ob dieser Datensatz Zugriffsbeschr�nkungen unterliegt (Nein = keine Beschr�nkung, Ja = zug�nglich nur f�r gew�hlte Kundengruppen)"); + + builder.AddOrUpdate("Admin.Common.Acl.AvailableFor", + "Customer roles", + "Kundengruppen", + "Select customer roles who can access the entity. For all inactive roles, this record is hidden.", + "W�hlen Sie Kundengruppen, die auf den Datensatz zugreifen k�nnen. Bei allen nicht aktivierten Gruppen wird dieser Datensatz ausgeblendet."); + + builder.Delete( + "Admin.Catalog.Categories.Fields.SubjectToAcl", + "Admin.Catalog.Categories.Fields.AclCustomerRoles", + "Admin.Catalog.Products.Fields.SubjectToAcl", + "Admin.Catalog.Products.Fields.AclCustomerRoles", + "Common.Options.Count"); + + builder.AddOrUpdate("Admin.Common.ApplyFilter", "Apply filter", "Filter anwenden"); + builder.AddOrUpdate("Time.Milliseconds", "Milliseconds", "Millisekunden"); + builder.AddOrUpdate("Common.Pixel", "Pixel", "Pixel"); + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.ShowPlaceholder", "Show placeholder", "Zeige Platzhalter"); + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.HidePlaceholder", "Hide placeholder", "Verberge Platzhalter"); + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.UpdateExampleFileName", "Update example", "Aktualisiere Beispiel"); + + builder.AddOrUpdate("Admin.Configuration.Themes.AvailableDesktopThemes", "Installed themes", "Installierte Themes"); + + builder.AddOrUpdate("Admin.Catalog.Products.List.GoDirectlyToSku", "Find by SKU", "Nach SKU suchen"); + builder.AddOrUpdate("Admin.Orders.List.GoDirectlyToNumber", "Find by order id", "Nach Auftragsnummer suchen"); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.StoreLastIpAddress", + "Store IP address", + "IP-Adresse speichern", + "Specifies whether to store the IP address in the customer data set.", + "Legt fest, ob die IP-Adresse im Kundendatensatz gespeichert werden soll."); + + builder.AddOrUpdate("Admin.Orders.Info", "General", "Allgemein"); + builder.AddOrUpdate("Admin.Orders.BillingAndShipment", "Billing & Shipping", "Rechnung & Versand"); + builder.AddOrUpdate("Admin.Orders.Fields.ShippingAddress.ViewOnGoogleMaps", "View on Google Maps", "Auf Google Maps ansehen"); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.SocialSettings.InstagramLink", + "Instagram Link", + "Instagram Link", + "Leave this field empty if the Instagram link should not be shown", + "Lassen Sie dieses Feld leer, wenn der Instagram Link nicht angezeigt werden soll"); + + builder.AddOrUpdate("Common.License", "License", "Lizenz"); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Payments.CapturePaymentReason.OrderShipped", + "The order has been marked as shipped", + "Der Auftrag wurde als versendet markiert"); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Payments.CapturePaymentReason.OrderDelivered", + "The order has been marked as delivered", + "Der Auftrag wurde als ausgeliefert markiert"); + + builder.AddOrUpdate("Admin.Configuration.Settings.Payment.CapturePaymentReason", + "Capture payment amount when�", + "Zahlungsbetrag einziehen, wenn�", + "Specifies the event when the payment amount is automatically captured. The selected payment method must support capturing for this.", + "Legt das Ereignis fest, zu dem der Zahlunsgbetrag automatisch eingezogen wird. Die gew�hlte Zahlart muss hierf�r Buchungen unterst�tzen."); + + + #region taken from V22Final, because they were never added yet + + builder.AddOrUpdate("Common.Next", + "Next", + "Weiter"); + builder.AddOrUpdate("Admin.Common.BackToConfiguration", + "Back to configuration", + "Zur�ck zur Konfiguration"); + builder.AddOrUpdate("Admin.Common.UploadFileSucceeded", + "The file has been successfully uploaded.", + "Die Datei wurde erfolgreich hochgeladen."); + builder.AddOrUpdate("Admin.Common.UploadFileFailed", + "The upload has failed.", + "Der Upload ist leider fehlgeschlagen."); + builder.AddOrUpdate("Admin.Common.ImportAll", + "Import all", + "Alle importieren"); + builder.AddOrUpdate("Admin.Common.ImportSelected", + "Import selected", + "Ausgew�hlte importieren"); + builder.AddOrUpdate("Admin.Common.UnknownError", + "An unknown error has occurred.", + "Es ist ein unbekannter Fehler aufgetreten."); + builder.AddOrUpdate("Plugins.Feed.FreeShippingThreshold", + "Free shipping threshold", + "Kostenloser Versand ab", + "Amount as from shipping is free.", + "Betrag, ab dem keine Versandkosten anfallen."); + + #endregion + + builder.AddOrUpdate("Admin.Product.Picture.Added", + "The picture has successfully been added", + "Das Bild wurde erfolgreich zugef�gt"); + + builder.AddOrUpdate("HtmlEditor.ClickToEdit", "Click to edit HTML...", "Hier klicken, um HTML zu editieren..."); + + builder.AddOrUpdate("Admin.Catalog.Attributes.ProductAttributes.Fields.ExportMappings.Note", + "Define mappings of attribute values to export fields according to the pattern <Format prefix>:<Export field name>. Example: gmc:color exports the attribute values for colors to the field color during the Google Merchant Center Export. The mappings are only effective when exporting attribute combinations.", + "Legen Sie Zuordnungen von Attributwerten zu Exportfeldern nach dem Muster <Formatpr�fix>:<Export-Feldname> fest. Beispiel: gmc:color exportiert beim Google Merchant Center Export die Attributwerte f�r Farben in das Feld color. Die Zuordnungen sind nur beim Export von Attributkombinationen wirksam."); + + builder.AddOrUpdate("Admin.Catalog.Attributes.ProductAttributes.Fields.ExportMappings", + "Mappings to export fields", + "Zuordnungen zu Exportfeldern", + "Allows to map attribute values to export fields. Each entry has to be entered in a new line.", + "Erm�glicht die Zuordnung von Attributwerten zu Exportfeldern. Jeder Eintrag muss in einer neuen Zeile erfolgen."); + + builder.AddOrUpdate("Admin.Configuration.Payment.Methods.AdditionalFee", + "Additional fee", + "Zus�tzliche Geb�hr", + "Specifies an additional fee to be charged to the customer for using the payment method.", + "Legt eine zus�tzliche Geb�hr fest, die dem Kunden f�r die Inanspruchnahme der Zahlart berechnet wird."); + + builder.AddOrUpdate("Admin.Configuration.Payment.Methods.AdditionalFeePercentage", + "Additional fee percentage", + "Zus�tzliche Geb�hr prozentual", + "Specifies whether the additional fee should be calculated as a percentage. A fixed value is used if this option is disabled.", + "Legt fest, ob die zus�tzliche Geb�hr prozentual berechnet werden soll. Es wird ein fester Wert verwendet, falls diese Option deaktiviert ist."); + + builder.Delete("Common.Buttons.Default"); + builder.AddOrUpdate("Common.Buttons.Secondary", "Secondary", "Secondary"); + builder.AddOrUpdate("Common.Buttons.Light", "Light", "Light"); + builder.AddOrUpdate("Common.Buttons.Dark", "Dark", "Dark"); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.AddressFormFields.CountryRequired", + "'Country' required", + "Die Eingabe eines Landes ist erforderlich", + "Check the box if 'Country' is required.", + "Legt fest, ob die Eingabe eines Landes erforderlich ist."); + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.AddressFormFields.StateProvinceRequired", + "'State/province' required", + "Die Eingabe eines Bundeslandes ist erforderlich", + "Check the box if 'State/province' is required.", + "Legt fest, ob die Eingabe eines Bundeslandes erforderlich ist."); + + builder.AddOrUpdate("Address.Fields.StateProvince.Required", "State is required.", "Bundesland wird ben�tigt"); + + builder.AddOrUpdate("Common.Columns", "Columns", "Spalten"); } } } diff --git a/src/Libraries/SmartStore.Data/ObjectContextBase.SaveChanges.cs b/src/Libraries/SmartStore.Data/ObjectContextBase.SaveChanges.cs index ec2e7fed39..b870ca862c 100644 --- a/src/Libraries/SmartStore.Data/ObjectContextBase.SaveChanges.cs +++ b/src/Libraries/SmartStore.Data/ObjectContextBase.SaveChanges.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Data.Entity; using System.Data.Entity.Infrastructure; @@ -7,20 +8,22 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Web.Hosting; using SmartStore.Core; +using SmartStore.Core.Data; using SmartStore.Core.Data.Hooks; using SmartStore.Core.Infrastructure; using SmartStore.Utilities; +using EfState = System.Data.Entity.EntityState; namespace SmartStore.Data { public abstract partial class ObjectContextBase { - private IDbHookHandler _dbHookHandler; private SaveChangesOperation _currentSaveOperation; - enum SaveStage + private readonly static ConcurrentDictionary _hookableEntities = new ConcurrentDictionary(); + + private enum SaveStage { PreSave, PostSave @@ -28,53 +31,7 @@ enum SaveStage private IEnumerable GetChangedEntries() { - return ChangeTracker.Entries().Where(IsChangedEntry); - } - - private bool IsChangedEntry(DbEntityEntry entry) - { - var state = entry.State; - - if (state == EntityState.Added || state == EntityState.Deleted) - { - return true; - } - else if (state == EntityState.Modified) - { - var mergeable = entry.Entity as IMergedData; - - if (mergeable?.MergedDataValues == null) - { - return true; - } - else - { - if (mergeable.MergedDataValues.Count == 0) - return true; - - // At this point it's certain that 'MergeWithCombination()' has been called recently - // (to merge variant with product attributes), and at least one attribute has been - // overwritten on variant level. - - // This should return an empty dict if no other property has been changed. - // GetModifiedProperties() can handle/ignore merged properties very well. - var modProps = GetModifiedProperties(entry); - - if (modProps.Count == 0) - { - try - { - // Set state to unchanged: no 'real' prop changed! - entry.State = EntityState.Unchanged; - } - catch { } - } - - return modProps.Count > 0; - } - } - - return false; + return ChangeTracker.Entries().Where(x => x.State > EfState.Unchanged); } public IDbHookHandler DbHookHandler @@ -91,23 +48,31 @@ public override int SaveChanges() { if (op.Stage == SaveStage.PreSave) { - // // This was called from within a PRE action hook. We must get out:... - // // 1.) to prevent cyclic calls - // // 2.) we want new entities in the state tracker (added by pre hooks) to be committed atomically in the core SaveChanges() call later on. + // This was called from within a PRE action hook. We must get out:... + // 1.) to prevent cyclic calls + // 2.) we want new entities in the state tracker (added by pre hooks) to be committed atomically in the core SaveChanges() call later on. return 0; } else if (op.Stage == SaveStage.PostSave) { - // // This was called from within a POST action hook. Core SaveChanges() has already been called, - // // but new entities could have been added to the state tracker by hooks. - // // Therefore we need to commit them and get outta here, otherwise: cyclic nightmare! + // This was called from within a POST action hook. Core SaveChanges() has already been called, + // but new entities could have been added to the state tracker by hooks. + // Therefore we need to commit them and get outta here, otherwise: cyclic nightmare! + // DetectChanges() here is important, 'cause we turned it off for the save process. + base.ChangeTracker.DetectChanges(); return SaveChangesCore(); } } _currentSaveOperation = new SaveChangesOperation(this, this.DbHookHandler); - using (new ActionDisposable(() => _currentSaveOperation = null)) + Action endExecute = () => + { + _currentSaveOperation?.Dispose(); + _currentSaveOperation = null; + }; + + using (new ActionDisposable(endExecute)) { return _currentSaveOperation.Execute(); } @@ -121,26 +86,33 @@ public override Task SaveChangesAsync(CancellationToken cancellationToken) { if (op.Stage == SaveStage.PreSave) { - // // This was called from within a PRE action hook. We must get out:... - // // 1.) to prevent cyclic calls - // // 2.) we want new entities in the state tracker (added by pre hooks) to be committed atomically in the core SaveChanges() call later on. + // This was called from within a PRE action hook. We must get out:... + // 1.) to prevent cyclic calls + // 2.) we want new entities in the state tracker (added by pre hooks) to be committed atomically in the core SaveChanges() call later on. return Task.FromResult(0); } else if (op.Stage == SaveStage.PostSave) { - // // This was called from within a POST action hook. Core SaveChanges() has already been called, - // // but new entities could have been added to the state tracker by hooks. - // // Therefore we need to commit them and get outta here, otherwise: cyclic nightmare! + // This was called from within a POST action hook. Core SaveChanges() has already been called, + // but new entities could have been added to the state tracker by hooks. + // Therefore we need to commit them and get outta here, otherwise: cyclic nightmare! + // DetectChanges() here is important, 'cause we turned it off for the save process. + base.ChangeTracker.DetectChanges(); return SaveChangesCoreAsync(cancellationToken); } } _currentSaveOperation = new SaveChangesOperation(this, this.DbHookHandler); - using (new ActionDisposable(() => _currentSaveOperation = null)) + var result = _currentSaveOperation.ExecuteAsync(cancellationToken); + + result.ContinueWith(t => { - return _currentSaveOperation.ExecuteAsync(cancellationToken); - } + _currentSaveOperation?.Dispose(); + _currentSaveOperation = null; + }); + + return result; } /// @@ -149,13 +121,7 @@ public override Task SaveChangesAsync(CancellationToken cancellationToken) /// The number of affected records protected internal int SaveChangesCore() { - var changedEntries = _currentSaveOperation?.ChangedEntries ?? GetChangedEntries().ToList(); - - using (new ActionDisposable(() => IgnoreMergedData(changedEntries, false))) - { - IgnoreMergedData(changedEntries, true); - return base.SaveChanges(); - } + return base.SaveChanges(); } /// @@ -164,27 +130,54 @@ protected internal int SaveChangesCore() /// The number of affected records protected internal Task SaveChangesCoreAsync(CancellationToken cancellationToken) { - var changedEntries = _currentSaveOperation?.ChangedEntries ?? GetChangedEntries().ToList(); + return base.SaveChangesAsync(cancellationToken); + } - using (new ActionDisposable(() => IgnoreMergedData(changedEntries, false))) + private void IgnoreMergedData(IEnumerable entries, bool ignore) + { + foreach (var entry in entries) { - IgnoreMergedData(changedEntries, true); - return base.SaveChangesAsync(cancellationToken); + entry.MergedDataIgnore = ignore; } } - private void IgnoreMergedData(IEnumerable entries, bool ignore) + internal bool IsInSaveOperation { - foreach (var entry in entries.Select(x => x.Entity).OfType()) + get { return _currentSaveOperation != null; } + } + + private bool IsHookableEntry(IHookedEntity entry) + { + var entity = entry.Entity; + if (entity == null) { - entry.MergedDataIgnore = ignore; + return false; } + + return IsHookableEntityType(entry.EntityType); + } + + private bool IsHookableEntityType(Type entityType) + { + var isHookable = _hookableEntities.GetOrAdd(entityType, t => + { + var attr = t.GetAttribute(true); + if (attr != null) + { + return attr.IsHookable; + } + + // Entities are hookable by default + return true; + }); + + return isHookable; } class SaveChangesOperation : IDisposable { private SaveStage _stage; - private IList _changedEntries; + private IEnumerable _changedEntries; private ObjectContextBase _ctx; private IDbHookHandler _hookHandler; @@ -192,7 +185,6 @@ public SaveChangesOperation(ObjectContextBase ctx, IDbHookHandler hookHandler) { _ctx = ctx; _hookHandler = hookHandler; - _changedEntries = ctx.GetChangedEntries().ToList(); } public IEnumerable ChangedEntries @@ -207,23 +199,80 @@ public SaveStage Stage public int Execute() { - // pre - HookedEntity[] changedHookEntries; - PreExecute(out changedHookEntries); + var autoDetectChanges = _ctx.Configuration.AutoDetectChangesEnabled; + IEnumerable mergeableEntities = null; + + Action endExecute = () => + { + _ctx.Configuration.AutoDetectChangesEnabled = autoDetectChanges; + _ctx.IgnoreMergedData(mergeableEntities, false); + }; - // save - var result = _ctx.SaveChangesCore(); + using (new ActionDisposable(endExecute)) + { + // Suppress implicit DetectChanges() calls by EF, + // e.g. called by SaveChanges(), ChangeTracker.Entries() etc. + _ctx.Configuration.AutoDetectChangesEnabled = false; - // post - PostExecute(changedHookEntries); + // Get all attached entries implementing IMergedData, + // we need to ignore merge on them. Otherwise + // EF's change detection may think that properties has changed + // where they actually didn't. + mergeableEntities = _ctx.GetMergeableEntitiesFromChangeTracker().ToArray(); - return result; + // Now ignore merged data, otherwise merged data will be saved to database + _ctx.IgnoreMergedData(mergeableEntities, true); + + // We must detect changes earlier in the process + // before hooks are executed. Therefore we suppressed the + // implicit DetectChanges() call by EF and call it here explicitly. + _ctx.ChangeTracker.DetectChanges(); + + // Now get changed entries + _changedEntries = _ctx.GetChangedEntries(); + + // pre + IEnumerable changedHookEntries; + PreExecute(out changedHookEntries); + + // save + var result = _ctx.SaveChangesCore(); + + // post + PostExecute(changedHookEntries); + + return result; + } } public Task ExecuteAsync(CancellationToken cancellationToken) { + var autoDetectChanges = _ctx.Configuration.AutoDetectChangesEnabled; + IEnumerable mergeableEntities = null; + + // Suppress implicit DetectChanges() calls by EF, + // e.g. called by SaveChanges(), ChangeTracker.Entries() etc. + _ctx.Configuration.AutoDetectChangesEnabled = false; + + // Get all attached entries implementing IMergedData, + // we need to ignore merge on them. Otherwise + // EF's change detection may think that properties has changed + // where they actually didn't. + mergeableEntities = _ctx.GetMergeableEntitiesFromChangeTracker().ToArray(); + + // Now ignore merged data, otherwise merged data will be saved to database + _ctx.IgnoreMergedData(mergeableEntities, true); + + // We must detect changes earlier in the process + // before hooks are executed. Therefore we suppressed the + // implicit DetectChanges() calls by EF and call it here explicitly. + _ctx.ChangeTracker.DetectChanges(); + + // Now get changed entries + _changedEntries = _ctx.GetChangedEntries(); + // pre - HookedEntity[] changedHookEntries; + IEnumerable changedHookEntries; PreExecute(out changedHookEntries); // save @@ -236,16 +285,20 @@ public Task ExecuteAsync(CancellationToken cancellationToken) { PostExecute(changedHookEntries); } + + _ctx.Configuration.AutoDetectChangesEnabled = autoDetectChanges; + _ctx.IgnoreMergedData(mergeableEntities, false); }); - + return result; } - private void PreExecute(out HookedEntity[] changedHookEntries) + private IEnumerable PreExecute(out IEnumerable changedHookEntries) { bool enableHooks = false; bool importantHooksOnly = false; bool anyStateChanged = false; + IEnumerable processedHooks = null; changedHookEntries = null; @@ -265,26 +318,35 @@ private void PreExecute(out HookedEntity[] changedHookEntries) { changedHookEntries = _changedEntries .Select(x => new HookedEntity(x)) + .Where(x => _ctx.IsHookableEntityType(x.EntityType)) .ToArray(); // Regardless of validation (possible fixing validation errors too) - anyStateChanged = _hookHandler.TriggerPreSaveHooks(changedHookEntries, importantHooksOnly); + processedHooks = _hookHandler.TriggerPreSaveHooks(changedHookEntries, importantHooksOnly, out anyStateChanged); + + if (processedHooks.Any() && changedHookEntries.Any(x => x.State == SmartStore.Core.Data.EntityState.Modified)) + { + // Because at least one pre action hook has been processed, + // we must assume that entity properties has been changed. + // We need to call DetectChanges() again. + _ctx.ChangeTracker.DetectChanges(); + } } if (anyStateChanged) { // because the state of at least one entity has been changed during pre hooking // we have to further reduce the set of hookable entities (for the POST hooks) - changedHookEntries = changedHookEntries - .Where(x => x.InitialState > SmartStore.Core.Data.EntityState.Unchanged) - .ToArray(); + changedHookEntries = changedHookEntries.Where(x => x.InitialState > SmartStore.Core.Data.EntityState.Unchanged); } + + return processedHooks ?? Enumerable.Empty(); } - private void PostExecute(HookedEntity[] changedHookEntries) + private IEnumerable PostExecute(IEnumerable changedHookEntries) { - if (changedHookEntries == null || changedHookEntries.Length == 0) - return; + if (changedHookEntries == null || !changedHookEntries.Any()) + return Enumerable.Empty(); // the existence of hook entries actually implies that hooking is enabled. @@ -292,7 +354,7 @@ private void PostExecute(HookedEntity[] changedHookEntries) var importantHooksOnly = !_ctx.HooksEnabled && _hookHandler.HasImportantSaveHooks(); - _hookHandler.TriggerPostSaveHooks(changedHookEntries, importantHooksOnly); + return _hookHandler.TriggerPostSaveHooks(changedHookEntries, importantHooksOnly); } private string FormatValidationExceptionMessage(IEnumerable results) diff --git a/src/Libraries/SmartStore.Data/ObjectContextBase.cs b/src/Libraries/SmartStore.Data/ObjectContextBase.cs index 7c63ffd34c..dbc79bead9 100644 --- a/src/Libraries/SmartStore.Data/ObjectContextBase.cs +++ b/src/Libraries/SmartStore.Data/ObjectContextBase.cs @@ -5,25 +5,24 @@ using System.Data.Entity; using System.Data.Entity.Core.Objects; using System.Data.Entity.Infrastructure; -using System.Data.SqlClient; using System.Linq; -using System.Text.RegularExpressions; -using Microsoft.SqlServer.Management.Common; -using Microsoft.SqlServer.Management.Smo; +using SmartStore.ComponentModel; using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Data.Hooks; -using SmartStore.Core.Events; +using SmartStore.Data.Setup; +using EfState = System.Data.Entity.EntityState; namespace SmartStore.Data { - /// - /// Object context - /// [DbConfigurationType(typeof(SmartDbConfiguration))] public abstract partial class ObjectContextBase : DbContext, IDbContext { private static bool? s_isSqlServer2012OrHigher = null; + + // Instance of the internal ObjectStateManager.TransactionManager + // required for detecting if EF performs change detection + private object _transactionManager; /// /// Parameterless constructor for tooling support, e.g. EF Migrations. @@ -41,9 +40,9 @@ protected ObjectContextBase(string nameOrConnectionString, string alias = null) this.Alias = null; this.DbHookHandler = NullDbHookHandler.Instance; - if (DataSettings.DatabaseIsInstalled()) + if (DataSettings.DatabaseIsInstalled() && !DbSeedingMigrator.IsMigrating) { - // listen to 'ObjectMaterialized' for load hooking + //// listen to 'ObjectMaterialized' for load hooking ((IObjectContextAdapter)this).ObjectContext.ObjectMaterialized += ObjectMaterialized; } } @@ -57,7 +56,10 @@ private void ObjectMaterialized(object sender, ObjectMaterializedEventArgs e) var hookHandler = this.DbHookHandler; var importantHooksOnly = !this.HooksEnabled && hookHandler.HasImportantLoadHooks(); - hookHandler.TriggerLoadHooks(entity, importantHooksOnly); + if (IsHookableEntityType(entity.GetUnproxiedType())) + { + hookHandler.TriggerLoadHooks(entity, importantHooksOnly); + } } public bool HooksEnabled @@ -206,135 +208,80 @@ public int ExecuteSqlCommand(string sql, bool doNotEnsureTransaction = false, in if (timeout.HasValue) { - //Set previous timeout back + // Set previous timeout back ((IObjectContextAdapter)this).ObjectContext.CommandTimeout = previousTimeout; } return result; } - /// Executes sql by using SQL-Server Management Objects which supports GO statements. - public int ExecuteSqlThroughSmo(string sql) + /// + /// Checks whether the underlying ORM mapper is currently in the process of detecting changes. + /// + /// + public virtual bool IsDetectingChanges() { - Guard.NotEmpty(sql, "sql"); - - int result = 0; - - try + if (_transactionManager == null && DataSettings.DatabaseIsInstalled()) { - bool isSqlServer = DataSettings.Current.IsSqlServer; - - if (!isSqlServer) + var stateManager = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager; + if (stateManager != null) { - result = ExecuteSqlCommand(sql); + // Get the internal TransactionManager property instance from ObjectStateManager + _transactionManager = FastProperty.GetProperty(stateManager.GetType(), "TransactionManager")?.GetValue(stateManager); } - else - { - using (var sqlConnection = new SqlConnection(GetConnectionString())) - { - var serverConnection = new ServerConnection(sqlConnection); - var server = new Server(serverConnection); + } - result = server.ConnectionContext.ExecuteNonQuery(sql); - } + if (_transactionManager != null) + { + // Get the "IsDetectChanges" property of the internal TransactionManager + var prop = FastProperty.GetProperty(_transactionManager.GetType(), "IsDetectChanges"); + if (prop != null) + { + return (bool)prop.GetValue(_transactionManager); } } - catch (Exception) - { - // remove the GO statements - sql = Regex.Replace(sql, @"\r{0,1}\n[Gg][Oo]\r{0,1}\n", "\n"); - result = ExecuteSqlCommand(sql); - } - return result; + return false; + } + + public void DetectChanges() + { + base.ChangeTracker.DetectChanges(); } public bool HasChanges { get { - return this.ChangeTracker.Entries() - .Where(x => x.State > System.Data.Entity.EntityState.Unchanged) - .Any(); + return GetChangedEntries().Any(); } } - public IDictionary GetModifiedProperties(BaseEntity entity) - { - return GetModifiedProperties(this.Entry(entity)); - } - - private IDictionary GetModifiedProperties(DbEntityEntry entry) + public virtual bool IsModified(BaseEntity entity) { - var props = new Dictionary(); - - // be aware of the entity state. you cannot get modified properties for detached entities. - if (entry.State == System.Data.Entity.EntityState.Modified) - { - var detectChangesEnabled = this.Configuration.AutoDetectChangesEnabled; - var mergeable = entry.Entity as IMergedData; - var mergedProps = mergeable?.MergedDataValues; - var ignoreMerge = false; - - if (mergeable != null) - { - ignoreMerge = mergeable.MergedDataIgnore; - mergeable.MergedDataIgnore = true; - } - - try - { - var modifiedProperties = from p in entry.CurrentValues.PropertyNames - let prop = entry.Property(p) - // prop.IsModified seems to return true even if values are equal - where PropIsModified(prop, detectChangesEnabled, mergedProps) - select prop; + Guard.NotNull(entity, nameof(entity)); - foreach (var prop in modifiedProperties) - { - props.Add(prop.Name, prop.OriginalValue); - } - } - finally - { - if (mergeable != null) - mergeable.MergedDataIgnore = ignoreMerge; - } - } - - //System.Diagnostics.Debug.WriteLine("GetModifiedProperties: " + String.Join(", ", props.Select(x => x.Key))); - - return props; + var entry = this.Entry((object)entity); + return entry.HasChanges(this); } - private static bool PropIsModified(DbPropertyEntry prop, bool detectChangesEnabled, IDictionary mergedProps) + public bool TryGetModifiedProperty(BaseEntity entity, string propertyName, out object originalValue) { - var propIsModified = prop.IsModified; + Guard.NotNull(entity, nameof(entity)); - if (detectChangesEnabled && !propIsModified) - return false; // Perf - - if (propIsModified && mergedProps != null && mergedProps.ContainsKey(prop.Name)) + if (entity.IsTransientRecord()) { - // EF "thinks" that prop has changed because merged value differs + originalValue = null; return false; - } + } - // INFO: "CurrentValue" cannot be used for entities in the Deleted state. - // INFO: "OriginalValues" cannot be used for entities in the Added state. - return detectChangesEnabled - ? propIsModified - : !AreEqual(prop.CurrentValue, prop.OriginalValue); + var entry = this.Entry((object)entity); + return entry.TryGetModifiedProperty(this, propertyName, out originalValue); } - private static bool AreEqual(object cur, object orig) + public IDictionary GetModifiedProperties(BaseEntity entity) { - if (cur == null && orig == null) - return true; - - return orig != null - ? orig.Equals(cur) - : cur.Equals(orig); + return this.Entry((object)entity).GetModifiedProperties(this); } // required for UoW implementation @@ -405,6 +352,14 @@ public void UseTransaction(DbTransaction transaction) this.Database.UseTransaction(transaction); } + private IEnumerable GetMergeableEntitiesFromChangeTracker() + { + return base.ChangeTracker.Entries() + .Where(x => x.State > EfState.Detached) + .Select(x => x.Entity) + .OfType(); + } + #endregion #region Utils @@ -472,7 +427,7 @@ public bool IsAttached(TEntity entity) where TEntity : BaseEntity public void DetachEntity(TEntity entity) where TEntity : BaseEntity { - this.Entry(entity).State = System.Data.Entity.EntityState.Detached; + this.Entry(entity).State = EfState.Detached; } public int DetachEntities(bool unchangedEntitiesOnly = true) where TEntity : class @@ -486,10 +441,10 @@ public int DetachEntities(Func predicate, bool unchangedEntitiesOn Func predicate2 = x => { - if (x.State > System.Data.Entity.EntityState.Detached && predicate(x.Entity)) + if (x.State > EfState.Detached && predicate(x.Entity)) { return unchangedEntitiesOnly - ? x.State == System.Data.Entity.EntityState.Unchanged + ? x.State == EfState.Unchanged : true; } @@ -497,35 +452,29 @@ public int DetachEntities(Func predicate, bool unchangedEntitiesOn }; var attachedEntities = this.ChangeTracker.Entries().Where(predicate2).ToList(); - attachedEntities.Each(entry => entry.State = System.Data.Entity.EntityState.Detached); + attachedEntities.Each(entry => entry.State = EfState.Detached); return attachedEntities.Count; } - public void ChangeState(TEntity entity, System.Data.Entity.EntityState newState) where TEntity : BaseEntity + public void ChangeState(TEntity entity, EfState requestedState) where TEntity : BaseEntity { //Console.WriteLine("ChangeState ORIGINAL"); - this.Entry(entity).State = newState; - } - - public void ReloadEntity(TEntity entity) where TEntity : BaseEntity - { var entry = this.Entry(entity); - try - { - entry.Reload(); - } - catch + if (entry.State != requestedState) { - // Can occur when entity has been detached in the meantime (for whatever fucking reasons) - if (entry.State == System.Data.Entity.EntityState.Detached) - { - entry.State = System.Data.Entity.EntityState.Unchanged; - entry.Reload(); - } + // Only change state when requested state differs, + // because EF internally sets all properties to modified + // if necessary, even when requested state equals current state. + entry.State = requestedState; } } + public void ReloadEntity(TEntity entity) where TEntity : BaseEntity + { + this.Entry((object)entity).ReloadEntity(); + } + #endregion #region Nested classes diff --git a/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesBuilder.cs b/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesBuilder.cs index 9b88ce6bc7..6305f33373 100644 --- a/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesBuilder.cs +++ b/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesBuilder.cs @@ -133,7 +133,7 @@ internal void Reset() internal IEnumerable Build() { - return _entries.OrderByDescending(x => x.Important).ThenBy(x => x.Lang); + return _entries.OrderByDescending(x => x.Important).ThenBy(x => x.Lang).ToList(); } #region Nested builder for AddOrUpdate diff --git a/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesMigrator.cs b/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesMigrator.cs index c12d5dd465..c0b2227841 100644 --- a/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesMigrator.cs +++ b/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesMigrator.cs @@ -50,7 +50,8 @@ public void Migrate(IEnumerable entries, bool updateTouched foreach (var lang in langMap) { - foreach (var entry in entries.Where(x => x.Lang == null || langMap[x.Lang.ToLower()].Id == lang.Value.Id)) + var validEntries = entries.Where(x => x.Lang == null || langMap[x.Lang.ToLower()].Id == lang.Value.Id); + foreach (var entry in validEntries) { bool isLocal; var db = GetResource(entry.Key, lang.Value.Id, toAdd, out isLocal); @@ -98,9 +99,6 @@ public void Migrate(IEnumerable entries, bool updateTouched // remove deleted resources _resources.RemoveRange(toDelete); - // update modified resources - toUpdate.Each(x => _ctx.Entry(x).State = System.Data.Entity.EntityState.Modified); - // save now int affectedRows = _ctx.SaveChanges(); } diff --git a/src/Libraries/SmartStore.Data/Setup/DbSeedingMigrator.cs b/src/Libraries/SmartStore.Data/Setup/DbSeedingMigrator.cs index 0d041837e2..ab071e861c 100644 --- a/src/Libraries/SmartStore.Data/Setup/DbSeedingMigrator.cs +++ b/src/Libraries/SmartStore.Data/Setup/DbSeedingMigrator.cs @@ -20,6 +20,7 @@ public class DbSeedingMigrator : DbMigrator where TContext : DbContext { private ILogger _logger; private static bool _isMigrating; + private static Exception _lastSeedException; /// /// Initializes a new instance of the DbMigrator class with the default (core db) configuration. @@ -72,6 +73,12 @@ public static bool IsMigrating /// The number of applied migrations public int RunPendingMigrations(TContext context) { + if (_lastSeedException != null) + { + // This can happen when a previous migration attempt failed with a rollback. + throw _lastSeedException; + } + var pendingMigrations = GetPendingMigrations().ToList(); if (!pendingMigrations.Any()) return 0; @@ -79,8 +86,9 @@ public int RunPendingMigrations(TContext context) var coreSeeders = new List(); var externalSeeders = new List(); var isCoreMigration = context is SmartObjectContext; - var initialMigration = this.GetDatabaseMigrations().LastOrDefault() ?? "[Initial]"; - var lastSuccessfulMigration = initialMigration; + var databaseMigrations = this.GetDatabaseMigrations().ToArray(); + var initialMigration = databaseMigrations.LastOrDefault() ?? "[Initial]"; + var lastSuccessfulMigration = databaseMigrations.FirstOrDefault(); IDataSeeder coreSeeder = null; IDataSeeder externalSeeder = null; @@ -189,7 +197,8 @@ private void RunSeeders(IEnumerable seederEntries, T ctx) where { Update(seederEntry.PreviousMigrationId); _isMigrating = false; - throw new DbMigrationException(seederEntry.PreviousMigrationId, seederEntry.MigrationId, ex.InnerException ?? ex, true); + _lastSeedException = new DbMigrationException(seederEntry.PreviousMigrationId, seederEntry.MigrationId, ex.InnerException ?? ex, true); + throw _lastSeedException; } Logger.WarnFormat(ex, "Seed error in migration '{0}'. The error was ignored because no rollback was requested.", seederEntry.MigrationId); diff --git a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs index 1b0e06e5c3..e0264eec68 100644 --- a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs +++ b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs @@ -49,8 +49,8 @@ public void Initialize(SmartObjectContext context) { this._ctx = context; - this._sampleImagesPath = CommonHelper.MapPath("~/content/samples/"); - this._sampleDownloadsPath = CommonHelper.MapPath("~/content/samples/"); + this._sampleImagesPath = CommonHelper.MapPath("~/App_Data/Samples/"); + this._sampleDownloadsPath = CommonHelper.MapPath("~/App_Data/Samples/"); } #region Mandatory data creators @@ -3862,254 +3862,6 @@ public IList EmailAccounts() return entities; } - public IList MessageTemplates() - { - var eaGeneral = this.EmailAccounts().FirstOrDefault(x => x.Email != null); - - string cssString = @""; - string templateHeader = cssString + "
"; - string templateFooter = "
"; - - var entities = new List() - { - new MessageTemplate - { - Name = "Blog.BlogComment", - Subject = "%Store.Name%. New blog comment.", - Body = templateHeader + "

%Store.Name%



A new blog comment has been created for blog post \"%BlogComment.BlogPostTitle%\".

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "Customer.BackInStock", - Subject = "%Store.Name%. Back in stock notification", - Body = templateHeader + "

%Store.Name%



Hello %Customer.FullName%,
Product \"%BackInStockSubscription.ProductName%\" is in stock.

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "Customer.EmailValidationMessage", - Subject = "%Store.Name%. Email validation", - Body = templateHeader + "

%Store.Name%



To activate your account click here.

%Store.Name%" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "Customer.NewPM", - Subject = "%Store.Name%. You have received a new private message", - Body = templateHeader + "

%Store.Name%



You have received a new private message.

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "Customer.PasswordRecovery", - Subject = "%Store.Name%. Password recovery", - Body = templateHeader + "

%Store.Name%



To change your password click here.

%Store.Name%" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "Customer.WelcomeMessage", - Subject = "Welcome to %Store.Name%", - Body = templateHeader + "We welcome you to %Store.Name%.

You can now take part in the various services we have to offer you. Some of these services include:

Permanent Cart - Any products added to your online cart remain there until you remove them, or check them out.
Address Book - We can now deliver your products to another address other than yours! This is perfect to send birthday gifts direct to the birthday-person themselves.
Order History - View your history of purchases that you have made with us.
Products Reviews - Share your opinions on products with our other customers.

For help with any of our online services, please email the store-owner: %Store.Email%.

Note: This email address was provided on our registration page. If you own the email and did not register on our site, please send an email to %Store.Email%." + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "Forums.NewForumPost", - Subject = "%Store.Name%. New Post Notification.", - Body = templateHeader + "

%Store.Name%

A new post has been created in the topic \"%Forums.TopicName%\" at \"%Forums.ForumName%\" forum.

Click here for more info.

Post author: %Forums.PostAuthor%
Post body: %Forums.PostBody%

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "Forums.NewForumTopic", - Subject = "%Store.Name%. New Topic Notification.", - Body = templateHeader + "

%Store.Name%



A new topic \"%Forums.TopicName%\" has been created at \"%Forums.ForumName%\" forum.

Click here for more info.

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "GiftCard.Notification", - Subject = "%GiftCard.SenderName% has sent you a gift card for %Store.Name%", - Body = templateHeader + "

You have received a gift card for %Store.Name%

Dear %GiftCard.RecipientName%,

%GiftCard.SenderName% (%GiftCard.SenderEmail%) has sent you a %GiftCard.Amount% gift cart for %Store.Name%

You gift card code is %GiftCard.CouponCode%

%GiftCard.Message%

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "NewCustomer.Notification", - Subject = "%Store.Name%. New customer registration", - Body = templateHeader + "

%Store.Name%



A new customer registered with your store. Below are the customer's details:
Full name: %Customer.FullName%
Email: %Customer.Email%

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "NewReturnRequest.StoreOwnerNotification", - Subject = "%Store.Name%. New return request.", - Body = templateHeader + "

%Store.Name%



%Customer.FullName% has just submitted a new return request. Details are below:
Request ID: %ReturnRequest.ID%
Product: %ReturnRequest.Product.Quantity% x Product: %ReturnRequest.Product.Name%
Reason for return: %ReturnRequest.Reason%
Requested action: %ReturnRequest.RequestedAction%
Customer comments:
%ReturnRequest.CustomerComment%

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "News.NewsComment", - Subject = "%Store.Name%. New news comment.", - Body = templateHeader + "

%Store.Name%



A new news comment has been created for news \"%NewsComment.NewsTitle%\".

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "NewsLetterSubscription.ActivationMessage", - Subject = "%Store.Name%. Subscription activation message.", - Body = templateHeader + "

Click here to confirm your subscription to our list.

If you received this email by mistake, simply delete it.

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "NewsLetterSubscription.DeactivationMessage", - Subject = "%Store.Name%. Subscription deactivation message.", - Body = templateHeader + "

Click here to unsubscribe from our newsletter list.

If you received this email by mistake, simply delete it.

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "NewVATSubmitted.StoreOwnerNotification", - Subject = "%Store.Name%. New VAT number is submitted.", - Body = templateHeader + "

%Store.Name%



%Customer.FullName% (%Customer.Email%) has just submitted a new VAT number. Details are below:
VAT number: %Customer.VatNumber%
VAT number status: %Customer.VatNumberStatus%
Received name: %VatValidationResult.Name%
Received address: %VatValidationResult.Address%

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "OrderCancelled.CustomerNotification", - Subject = "%Store.Name%. Your order cancelled", - Body = templateHeader + "

%Store.Name%



Hello %Order.CustomerFullName%,
Your order has been cancelled. Below is the summary of the order.

Order Number: %Order.OrderNumber%
Order Details: %Order.OrderURLForCustomer%
Date Ordered: %Order.CreatedOn%



Billing Address
%Order.BillingFirstName% %Order.BillingLastName%
%Order.BillingAddress1%
%Order.BillingCity% %Order.BillingZipPostalCode%
%Order.BillingStateProvince% %Order.BillingCountry%



Shipping Address
%Order.ShippingFirstName% %Order.ShippingLastName%
%Order.ShippingAddress1%
%Order.ShippingCity% %Order.ShippingZipPostalCode%
%Order.ShippingStateProvince% %Order.ShippingCountry%

Shipping Method: %Order.ShippingMethod%
Payment Method: %Order.PaymentMethod%

%Order.Product(s)%

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "OrderCompleted.CustomerNotification", - Subject = "%Store.Name%. Your order completed", - Body = templateHeader + "

%Store.Name%



Hello %Order.CustomerFullName%,
Your order has been completed. Below is the summary of the order.

Order Number: %Order.OrderNumber%
Order Details: %Order.OrderURLForCustomer%
Date Ordered: %Order.CreatedOn%



Billing Address
%Order.BillingFirstName% %Order.BillingLastName%
%Order.BillingAddress1%
%Order.BillingCity% %Order.BillingZipPostalCode%
%Order.BillingStateProvince% %Order.BillingCountry%



Shipping Address
%Order.ShippingFirstName% %Order.ShippingLastName%
%Order.ShippingAddress1%
%Order.ShippingCity% %Order.ShippingZipPostalCode%
%Order.ShippingStateProvince% %Order.ShippingCountry%

Shipping Method: %Order.ShippingMethod%
Payment Method: %Order.PaymentMethod%

%Order.Product(s)%

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "ShipmentDelivered.CustomerNotification", - Subject = "Your order from %Store.Name% has been delivered.", - Body = templateHeader + "

%Store.Name%



Hello %Order.CustomerFullName%,
Good news! You order has been delivered.
Order Number: %Order.OrderNumber%
Order Details: %Order.OrderURLForCustomer%
Date Ordered: %Order.CreatedOn%



Billing Address
%Order.BillingFirstName% %Order.BillingLastName%
%Order.BillingAddress1%
%Order.BillingCity% %Order.BillingZipPostalCode%
%Order.BillingStateProvince% %Order.BillingCountry%



Shipping Address
%Order.ShippingFirstName% %Order.ShippingLastName%
%Order.ShippingAddress1%
%Order.ShippingCity% %Order.ShippingZipPostalCode%
%Order.ShippingStateProvince% %Order.ShippingCountry%

Shipping Method: %Order.ShippingMethod%
Payment Method: %Order.PaymentMethod%

Delivered Products:

%Shipment.Product(s)%

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - - new MessageTemplate - { - Name = "OrderPlaced.CustomerNotification", - Subject = "Order receipt from %Store.Name%.", - Body = templateHeader + "

%Store.Name%



Hello %Order.CustomerFullName%,
Thanks for buying from %Store.Name%. Below is the summary of the order.

Order Number: %Order.OrderNumber%
Order Details: %Order.OrderURLForCustomer%
Date Ordered: %Order.CreatedOn%



Billing Address
%Order.BillingFirstName% %Order.BillingLastName%
%Order.BillingAddress1%
%Order.BillingCity% %Order.BillingZipPostalCode%
%Order.BillingStateProvince% %Order.BillingCountry%



Shipping Address
%Order.ShippingFirstName% %Order.ShippingLastName%
%Order.ShippingAddress1%
%Order.ShippingCity% %Order.ShippingZipPostalCode%
%Order.ShippingStateProvince% %Order.ShippingCountry%

Shipping Method: %Order.ShippingMethod%
Payment Method: %Order.PaymentMethod%

%Order.Product(s)%

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "OrderPlaced.StoreOwnerNotification", - Subject = "%Store.Name%. Purchase Receipt for Order #%Order.OrderNumber%", - Body = templateHeader + "

%Store.Name%



%Order.CustomerFullName% (%Order.CustomerEmail%) has just placed an order from your store. Below is the summary of the order.

Order Number: %Order.OrderNumber%
Date Ordered: %Order.CreatedOn%



Billing Address
%Order.BillingFirstName% %Order.BillingLastName%
%Order.BillingAddress1%
%Order.BillingCity% %Order.BillingZipPostalCode%
%Order.BillingStateProvince% %Order.BillingCountry%



Shipping Address
%Order.ShippingFirstName% %Order.ShippingLastName%
%Order.ShippingAddress1%
%Order.ShippingCity% %Order.ShippingZipPostalCode%
%Order.ShippingStateProvince% %Order.ShippingCountry%

Shipping Method: %Order.ShippingMethod%
Payment Method: %Order.PaymentMethod%

%Order.Product(s)%

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "ShipmentSent.CustomerNotification", - Subject = "Your order from %Store.Name% has been shipped.", - Body = templateHeader + "

%Store.Name%



Hello %Order.CustomerFullName%!,
Good news! You order has been shipped.
Order Number: %Order.OrderNumber%
Order Details: %Order.OrderURLForCustomer%
Date Ordered: %Order.CreatedOn%



Billing Address
%Order.BillingFirstName% %Order.BillingLastName%
%Order.BillingAddress1%
%Order.BillingCity% %Order.BillingZipPostalCode%
%Order.BillingStateProvince% %Order.BillingCountry%



Shipping Address
%Order.ShippingFirstName% %Order.ShippingLastName%
%Order.ShippingAddress1%
%Order.ShippingCity% %Order.ShippingZipPostalCode%
%Order.ShippingStateProvince% %Order.ShippingCountry%

Shipping Method: %Order.ShippingMethod%
Payment Method: %Order.PaymentMethod%

Shipped Products:

%Shipment.Product(s)%

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "Product.ProductReview", - Subject = "%Store.Name%. New product review.", - Body = templateHeader + "

%Store.Name%



A new product review has been written for product \"%ProductReview.ProductName%\".

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "QuantityBelow.StoreOwnerNotification", - Subject = "%Store.Name%. Quantity below notification. %Product.Name%", - Body = templateHeader + "

%Store.Name%



%Product.Name% (ID: %Product.ID%, SKU: %Product.Sku%) low quantity.

Quantity: %Product.StockQuantity%

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "ReturnRequestStatusChanged.CustomerNotification", - Subject = "%Store.Name%. Return request status was changed.", - Body = templateHeader + "

%Store.Name%



Hello %Customer.FullName%,
Your return request #%ReturnRequest.ID% status has been changed: %ReturnRequest.Status%

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "Service.EmailAFriend", - Subject = "%Store.Name%. Referred Item", - Body = templateHeader + "

%Store.Name%



%EmailAFriend.Email% was shopping on %Store.Name% and wanted to share the following item with you.

%Product.Name%
%Product.ShortDescription%

For more info click here


%EmailAFriend.PersonalMessage%

%Store.Name%

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "Wishlist.EmailAFriend", - Subject = "%Store.Name%. Wishlist", - Body = templateHeader + "

%Store.Name%



%Wishlist.Email% was shopping on %Store.Name% and wanted to share a wishlist with you.


For more info click here


%Wishlist.PersonalMessage%

%Store.Name%

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "Customer.NewOrderNote", - Subject = "%Store.Name%. New order note has been added", - Body = templateHeader + "

%Store.Name%



Hello %Customer.FullName%,
New order note has been added to your account:
\"%Order.NewNoteText%\".
%Order.OrderURLForCustomer%

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "RecurringPaymentCancelled.StoreOwnerNotification", - Subject = "%Store.Name%. Recurring payment cancelled", - Body = templateHeader + "

%Store.Name%



%Customer.FullName% (%Customer.Email%) has just cancelled a recurring payment ID=%RecurringPayment.ID%.

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - new MessageTemplate - { - Name = "Product.AskQuestion", - Subject = "%Store.Name% - Question concerning '%Product.Name%' from %ProductQuestion.SenderName%", - Body = templateHeader + "

%ProductQuestion.Message%

SKU: %Product.Sku%
Email: %ProductQuestion.SenderEmail%
Name: %ProductQuestion.SenderName%
Phone: %ProductQuestion.SenderPhone%

" + templateFooter, - IsActive = true, - EmailAccountId = eaGeneral.Id, - }, - }; - this.Alter(entities); - return entities; - } - public IList Topics() { var entities = new List() @@ -4266,9 +4018,6 @@ public IList Settings() BaseDimensionId = _ctx.Set().Where(m => m.SystemKeyword == "inch").Single().Id, BaseWeightId = _ctx.Set().Where(m => m.SystemKeyword == "lb").Single().Id, }, - new MessageTemplatesSettings() - { - }, new ShoppingCartSettings() { }, @@ -14581,8 +14330,8 @@ public void AssignGroupedProducts(IList savedProducts) { x.ParentGroupedProductId = productGamingAccessoriesId; - _ctx.Set().Attach(x); - _ctx.Entry(x).State = System.Data.Entity.EntityState.Modified; + //_ctx.Set().Attach(x); + //_ctx.Entry(x).State = System.Data.Entity.EntityState.Modified; }); _ctx.SaveChanges(); @@ -14646,6 +14395,7 @@ public IList Forums() #endregion Forums #region Discounts + public IList Discounts() { var sampleDiscountWithCouponCode = new Discount() @@ -14656,7 +14406,7 @@ public IList Discounts() UsePercentage = false, DiscountAmount = 10, RequiresCouponCode = true, - CouponCode = "123", + CouponCode = "123" }; var sampleDiscounTwentyPercentTotal = new Discount() { @@ -14668,7 +14418,7 @@ public IList Discounts() StartDateUtc = new DateTime(2013, 1, 1), EndDateUtc = new DateTime(2020, 1, 1), RequiresCouponCode = true, - CouponCode = "456", + CouponCode = "456" }; var entities = new List @@ -14679,6 +14429,7 @@ public IList Discounts() this.Alter(entities); return entities; } + #endregion Discounts #region Deliverytimes diff --git a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj index 457d828d5d..f3d6bb6a2a 100644 --- a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj +++ b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj @@ -10,7 +10,7 @@ Properties SmartStore.Data SmartStore.Data - v4.5.2 + v4.6.1 512 @@ -67,16 +67,16 @@ True - False - ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll + ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll + True - False - ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll + ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll + True - False - ..\..\packages\EntityFramework.SqlServerCompact.6.1.3\lib\net45\EntityFramework.SqlServerCompact.dll + ..\..\packages\EntityFramework.SqlServerCompact.6.2.0\lib\net45\EntityFramework.SqlServerCompact.dll + True True @@ -102,6 +102,9 @@ ..\..\packages\Microsoft.SqlServer.Compact.4.0.8876.1\lib\net40\System.Data.SqlServerCe.dll + + ..\..\packages\System.ValueTuple.4.4.0\lib\net461\System.ValueTuple.dll + @@ -130,6 +133,7 @@ + @@ -521,6 +525,42 @@ 201709251538312_UpdateTrustedShopsTask.cs + + + 201710102038287_CurrencyRounding.cs + + + + 201710252016556_IndexOptionNames.cs + + + + 201711112331162_ProductMainPictureId.cs + + + + 201711222311112_MoveFsMedia.cs + + + + 201711291017168_SyncStringResources.cs + + + + 201712081631552_Liquid.cs + + + + 201712290151517_AddressFormat.cs + + + + 201802081830029_ShippingMethodMultistore.cs + + + + 201802270844034_ExportAttributeMappings.cs + @@ -528,7 +568,6 @@ - @@ -649,6 +688,8 @@ + + @@ -948,6 +989,33 @@ 201709251538312_UpdateTrustedShopsTask.cs + + 201710102038287_CurrencyRounding.cs + + + 201710252016556_IndexOptionNames.cs + + + 201711112331162_ProductMainPictureId.cs + + + 201711222311112_MoveFsMedia.cs + + + 201711291017168_SyncStringResources.cs + + + 201712081631552_Liquid.cs + + + 201712290151517_AddressFormat.cs + + + 201802081830029_ShippingMethodMultistore.cs + + + 201802270844034_ExportAttributeMappings.cs + diff --git a/src/Libraries/SmartStore.Data/Utilities/DataMigrator.cs b/src/Libraries/SmartStore.Data/Utilities/DataMigrator.cs new file mode 100644 index 0000000000..3cb6a0781c --- /dev/null +++ b/src/Libraries/SmartStore.Data/Utilities/DataMigrator.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Data.Entity; +using EfState = System.Data.Entity.EntityState; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core; +using SmartStore.Core.Domain.Directory; +using SmartStore.Utilities; +using System.IO; +using System.Xml.Linq; + +namespace SmartStore.Data.Utilities +{ + public static class DataMigrator + { + #region Product.MainPicture + + /// + /// Fixes 'MainPictureId' property of a single product entity + /// + /// Database context (must be ) + /// When null, Product.ProductPictures gets called. + /// Product to fix + /// true when value was fixed + public static bool FixProductMainPictureId(IDbContext context, Product product, IEnumerable entities = null) + { + Guard.NotNull(product, nameof(product)); + + // INFO: this method must be able to handle pre-save state also. + + var ctx = context as SmartObjectContext; + if (ctx == null) + throw new ArgumentException("Passed context must be an instance of type '{0}'.".FormatInvariant(typeof(SmartObjectContext)), nameof(context)); + + entities = entities ?? product.ProductPictures; + if (entities == null) + return false; + + var transientEntities = entities.Where(x => x.Id == 0); + + var sortedEntities = entities + // Remove transient entities + .Except(transientEntities) + .OrderBy(x => x.DisplayOrder) + .ThenBy(x => x.Id) + .Select(x => ctx.Entry(x)) + // Remove deleted and detached entities + .Where(x => x.State != EfState.Deleted && x.State != EfState.Detached) + .Select(x => x.Entity) + // Added/transient entities must be appended + .Concat(transientEntities.OrderBy(x => x.DisplayOrder)); + + var newMainPictureId = sortedEntities.FirstOrDefault()?.PictureId; + + if (newMainPictureId != product.MainPictureId) + { + product.MainPictureId = newMainPictureId; + return true; + } + + return false; + } + + /// + /// Traverses all products and fixes 'MainPictureId' property values if it is out of sync. + /// + /// Database context (must be ) + /// Minimum modified or created date of products to process. Pass null to fix all products. + /// The total count of fixed and updated product entities + public static int FixProductMainPictureIds(IDbContext context, DateTime? ifModifiedSinceUtc = null) + { + return FixProductMainPictureIds(context, false); + } + + /// + /// Called from migration seeder and only processes product entities without MainPictureId value. + /// + /// The total count of fixed and updated product entities + internal static int FixProductMainPictureIds(IDbContext context, bool initial, DateTime? ifModifiedSinceUtc = null) + { + var ctx = context as SmartObjectContext; + if (ctx == null) + throw new ArgumentException("Passed context must be an instance of type '{0}'.".FormatInvariant(typeof(SmartObjectContext)), nameof(context)); + + var query = from p in ctx.Set().AsNoTracking() + where (!initial || p.MainPictureId == null) && (ifModifiedSinceUtc == null || p.UpdatedOnUtc >= ifModifiedSinceUtc.Value) + orderby p.Id + select new { p.Id, p.MainPictureId }; + + // Key = ProductId, Value = MainPictureId + var toUpate = new Dictionary(); + + // 1st pass + int pageIndex = -1; + while (true) + { + var products = PagedList.Create(query, ++pageIndex, 1000); + var map = GetPoductPictureMap(ctx, products.Select(x => x.Id).ToArray()); + + foreach (var p in products) + { + int? fixedPictureId = null; + if (map.ContainsKey(p.Id)) + { + // Product has still a pic. + fixedPictureId = map[p.Id]; + } + + // Update only if fixed PictureId differs from current + if (fixedPictureId != p.MainPictureId) + { + toUpate.Add(p.Id, fixedPictureId); + } + } + + if (!products.HasNextPage) + break; + } + + // 2nd pass + foreach (var chunk in toUpate.Slice(1000)) + { + using (var tx = ctx.Database.BeginTransaction()) + { + foreach (var kvp in chunk) + { + context.ExecuteSqlCommand("Update [Product] Set [MainPictureId] = {0} WHERE [Id] = {1}", false, null, kvp.Value, kvp.Key); + } + + context.SaveChanges(); + tx.Commit(); + } + } + + return toUpate.Count; + } + + private static IDictionary GetPoductPictureMap(SmartObjectContext context, IEnumerable productIds) + { + var map = new Dictionary(); + + var query = from pp in context.Set().AsNoTracking() + where productIds.Contains(pp.ProductId) + group pp by pp.ProductId into g + select new + { + ProductId = g.Key, + PictureIds = g.OrderBy(x => x.DisplayOrder) + .Take(1) + .Select(x => x.PictureId) + }; + + map = query.ToList().ToDictionary(x => x.ProductId, x => x.PictureIds.First()); + + return map; + } + + #endregion + + #region Address Formats + + public static int ImportAddressFormats(IDbContext context) + { + var ctx = context as SmartObjectContext; + if (ctx == null) + throw new ArgumentException("Passed context must be an instance of type '{0}'.".FormatInvariant(typeof(SmartObjectContext)), nameof(context)); + + var filePath = CommonHelper.MapPath("~/App_Data/AddressFormats.xml"); + + if (!File.Exists(filePath)) + { + return 0; + } + + var countries = ctx.Set() + .Where(x => x.AddressFormat == null) + .ToList() + .ToDictionarySafe(x => x.TwoLetterIsoCode, StringComparer.OrdinalIgnoreCase); + + var doc = XDocument.Load(filePath); + + foreach (var node in doc.Root.Nodes().OfType()) + { + var code = node.Attribute("code")?.Value?.Trim(); + var format = node.Value.Trim(); + + if (code.HasValue() && countries.TryGetValue(code, out var country)) + { + country.AddressFormat = format; + } + } + + return ctx.SaveChanges(); + } + + #endregion + } +} diff --git a/src/Libraries/SmartStore.Data/Utilities/MessageTemplateConverter.cs b/src/Libraries/SmartStore.Data/Utilities/MessageTemplateConverter.cs new file mode 100644 index 0000000000..40071eced8 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Utilities/MessageTemplateConverter.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Messages; +using SmartStore.Utilities; +using System.Xml; +using System.Xml.Linq; + +namespace SmartStore.Data.Utilities +{ + public sealed class MessageTemplateConverter + { + private readonly SmartObjectContext _ctx; + private readonly EmailAccount _defaultEmailAccount; + + public MessageTemplateConverter(IDbContext context) + { + _ctx = context as SmartObjectContext; + if (_ctx == null) + throw new ArgumentException("Passed context must be an instance of type '{0}'.".FormatInvariant(typeof(SmartObjectContext)), nameof(context)); + + _defaultEmailAccount = _ctx.Set().FirstOrDefault(x => x.Email != null); + } + + /// + /// Loads a single message template from file (~/App_Data/EmailTemplates/) + /// and deserializes its xml content. + /// + /// Name of template without extension, e.g. 'GiftCard.Notification' + /// Language + /// Deserialized template xml + public MessageTemplate Load(string templateName, Language language) + { + Guard.NotEmpty(templateName, nameof(templateName)); + Guard.NotNull(language, nameof(language)); + + var dir = ResolveTemplateDirectory(language); + var fullPath = Path.Combine(dir.FullName, templateName + ".xml"); + + if (!File.Exists(fullPath)) + { + throw new FileNotFoundException($"File '{fullPath}' does not exist."); + } + + return DeserializeTemplate(fullPath); + } + + /// + /// Loads all message templates from disk (~/App_Data/EmailTemplates/) + /// + /// Language + /// List of deserialized template xml + public IEnumerable LoadAll(Language language) + { + Guard.NotNull(language, nameof(language)); + + var dir = ResolveTemplateDirectory(language); + var files = dir.EnumerateFiles("*.xml", SearchOption.TopDirectoryOnly); + + foreach (var file in files) + { + var template = DeserializeTemplate(file.FullName); + template.Name = Path.GetFileNameWithoutExtension(file.Name); + yield return template; + } + } + + public MessageTemplate Deserialize(string xml, string templateName) + { + Guard.NotEmpty(xml, nameof(xml)); + Guard.NotEmpty(templateName, nameof(templateName)); + + var template = DeserializeDocument(XDocument.Parse(xml)); + template.Name = templateName; + return template; + } + + public XmlDocument Save(MessageTemplate template, Language language) + { + Guard.NotNull(template, nameof(template)); + Guard.NotNull(language, nameof(language)); + + var doc = new XmlDocument(); + doc.LoadXml(""); + + var root = doc.DocumentElement; + root.AppendChild(doc.CreateElement("To")).InnerText = template.To; + if (template.ReplyTo.HasValue()) + root.AppendChild(doc.CreateElement("ReplyTo")).InnerText = template.ReplyTo; + root.AppendChild(doc.CreateElement("Subject")).InnerText = template.Subject; + root.AppendChild(doc.CreateElement("ModelTypes")).InnerText = template.ModelTypes; + root.AppendChild(doc.CreateElement("Body")).AppendChild(doc.CreateCDataSection(template.Body)); + + var path = Path.Combine(CommonHelper.MapPath("~/App_Data/EmailTemplates"), language.GetTwoLetterISOLanguageName()); + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + // File path + path = Path.Combine(path, template.Name + ".xml"); + + var xml = Prettifier.PrettifyXML(doc.OuterXml); + File.WriteAllText(path, xml); + + return doc; + } + + /// + /// Imports all template xml files to MessageTemplate table + /// + public void ImportAll(Language language) + { + var table = _ctx.Set(); + + var sourceTemplates = LoadAll(language); + var dbTemplatesMap = table + .ToList() + .ToDictionarySafe(x => x.Name, StringComparer.OrdinalIgnoreCase); + + foreach (var source in sourceTemplates) + { + if (dbTemplatesMap.TryGetValue(source.Name, out var target)) + { + if (source.To.HasValue()) target.To = source.To; + if (source.ReplyTo.HasValue()) target.ReplyTo = source.ReplyTo; + if (source.Subject.HasValue()) target.Subject = source.Subject; + if (source.ModelTypes.HasValue()) target.ModelTypes = source.ModelTypes; + if (source.Body.HasValue()) target.Body = source.Body; + } + else + { + target = new MessageTemplate + { + Name = source.Name, + To = source.To, + ReplyTo = source.ReplyTo, + Subject = source.Subject, + ModelTypes = source.ModelTypes, + Body = source.Body, + IsActive = true, + EmailAccountId = (_defaultEmailAccount?.Id).GetValueOrDefault(), + }; + + table.Add(target); + } + } + + _ctx.SaveChanges(); + } + + private DirectoryInfo ResolveTemplateDirectory(Language language) + { + var rootPath = CommonHelper.MapPath("~/App_Data/EmailTemplates/"); + var testPaths = new[] + { + language.LanguageCulture, + language.GetTwoLetterISOLanguageName(), + "en" + }; + + foreach (var path in testPaths.Select(x => Path.Combine(rootPath, x))) + { + if (Directory.Exists(path)) + { + return new DirectoryInfo(path); + } + } + + throw new DirectoryNotFoundException($"Could not obtain an email templates path for language {language.LanguageCulture}. Fallback to 'en' failed, because directory does not exist."); + } + + private MessageTemplate DeserializeTemplate(string fullPath) + { + return DeserializeDocument(XDocument.Load(fullPath)); + } + + private MessageTemplate DeserializeDocument(XDocument doc) + { + var root = doc.Root; + var result = new MessageTemplate(); + + foreach (var node in root.Nodes().OfType()) + { + var value = node.Value.Trim(); + + switch (node.Name.LocalName) + { + case "To": + result.To = value; + break; + case "ReplyTo": + result.ReplyTo = value; + break; + case "Subject": + result.Subject = value; + break; + case "ModelTypes": + result.ModelTypes = value; + break; + case "Body": + result.Body = value; + break; + } + } + + return result; + } + } +} diff --git a/src/Libraries/SmartStore.Data/app.config b/src/Libraries/SmartStore.Data/app.config index bb268217d0..9e763e6753 100644 --- a/src/Libraries/SmartStore.Data/app.config +++ b/src/Libraries/SmartStore.Data/app.config @@ -10,6 +10,10 @@ + + + + - + diff --git a/src/Libraries/SmartStore.Data/packages.config b/src/Libraries/SmartStore.Data/packages.config index 3f03f7df14..fe0a5053cd 100644 --- a/src/Libraries/SmartStore.Data/packages.config +++ b/src/Libraries/SmartStore.Data/packages.config @@ -1,9 +1,10 @@  - - + + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Affiliates/AffiliateService.cs b/src/Libraries/SmartStore.Services/Affiliates/AffiliateService.cs index 37e67c5239..68475199cf 100644 --- a/src/Libraries/SmartStore.Services/Affiliates/AffiliateService.cs +++ b/src/Libraries/SmartStore.Services/Affiliates/AffiliateService.cs @@ -89,9 +89,6 @@ public virtual void InsertAffiliate(Affiliate affiliate) throw new ArgumentNullException("affiliate"); _affiliateRepository.Insert(affiliate); - - //event notification - _eventPublisher.EntityInserted(affiliate); } /// @@ -104,9 +101,6 @@ public virtual void UpdateAffiliate(Affiliate affiliate) throw new ArgumentNullException("affiliate"); _affiliateRepository.Update(affiliate); - - //event notification - _eventPublisher.EntityUpdated(affiliate); } #endregion diff --git a/src/Libraries/SmartStore.Services/Authentication/External/ExternalAuthorizer.cs b/src/Libraries/SmartStore.Services/Authentication/External/ExternalAuthorizer.cs index 93202955fb..fe7fd42bfb 100644 --- a/src/Libraries/SmartStore.Services/Authentication/External/ExternalAuthorizer.cs +++ b/src/Libraries/SmartStore.Services/Authentication/External/ExternalAuthorizer.cs @@ -1,5 +1,3 @@ -//Contributor: Nicholas Mayne - using System; using SmartStore.Core; using SmartStore.Core.Domain.Customers; @@ -28,34 +26,38 @@ public partial class ExternalAuthorizer : IExternalAuthorizer private readonly CustomerSettings _customerSettings; private readonly ExternalAuthenticationSettings _externalAuthenticationSettings; private readonly IShoppingCartService _shoppingCartService; - private readonly IWorkflowMessageService _workflowMessageService; - private readonly LocalizationSettings _localizationSettings; + private readonly IMessageFactory _messageFactory; + private readonly LocalizationSettings _localizationSettings; #endregion #region Ctor - public ExternalAuthorizer(IAuthenticationService authenticationService, + public ExternalAuthorizer( + IAuthenticationService authenticationService, IOpenAuthenticationService openAuthenticationService, IGenericAttributeService genericAttributeService, ICustomerRegistrationService customerRegistrationService, - ICustomerActivityService customerActivityService, ILocalizationService localizationService, - IWorkContext workContext, CustomerSettings customerSettings, + ICustomerActivityService customerActivityService, + ILocalizationService localizationService, + IWorkContext workContext, + CustomerSettings customerSettings, ExternalAuthenticationSettings externalAuthenticationSettings, IShoppingCartService shoppingCartService, - IWorkflowMessageService workflowMessageService, LocalizationSettings localizationSettings) + IMessageFactory messageFactory, + LocalizationSettings localizationSettings) { - this._authenticationService = authenticationService; - this._openAuthenticationService = openAuthenticationService; - this._genericAttributeService = genericAttributeService; - this._customerRegistrationService = customerRegistrationService; - this._customerActivityService = customerActivityService; - this._localizationService = localizationService; - this._workContext = workContext; - this._customerSettings = customerSettings; - this._externalAuthenticationSettings = externalAuthenticationSettings; - this._shoppingCartService = shoppingCartService; - this._workflowMessageService = workflowMessageService; - this._localizationSettings = localizationSettings; + _authenticationService = authenticationService; + _openAuthenticationService = openAuthenticationService; + _genericAttributeService = genericAttributeService; + _customerRegistrationService = customerRegistrationService; + _customerActivityService = customerActivityService; + _localizationService = localizationService; + _workContext = workContext; + _customerSettings = customerSettings; + _externalAuthenticationSettings = externalAuthenticationSettings; + _shoppingCartService = shoppingCartService; + _messageFactory = messageFactory; + _localizationSettings = localizationSettings; } #endregion @@ -144,31 +146,31 @@ public virtual AuthorizationResult Authorize(OpenAuthenticationParameters parame //authenticate if (isApproved) _authenticationService.SignIn(userFound ?? userLoggedIn, false); - + //notifications if (_customerSettings.NotifyNewCustomerRegistration) - _workflowMessageService.SendCustomerRegisteredNotificationMessage(currentCustomer, _localizationSettings.DefaultAdminLanguageId); + _messageFactory.SendCustomerRegisteredNotificationMessage(currentCustomer, _localizationSettings.DefaultAdminLanguageId); switch (_customerSettings.UserRegistrationType) { case UserRegistrationType.EmailValidation: { - //email validation message + // email validation message _genericAttributeService.SaveAttribute(currentCustomer, SystemCustomerAttributeNames.AccountActivationToken, Guid.NewGuid().ToString()); - _workflowMessageService.SendCustomerEmailValidationMessage(currentCustomer, _workContext.WorkingLanguage.Id); + _messageFactory.SendCustomerEmailValidationMessage(currentCustomer, _workContext.WorkingLanguage.Id); - //result + // result return new AuthorizationResult(OpenAuthenticationStatus.AutoRegisteredEmailValidation); } case UserRegistrationType.AdminApproval: { - //result + // result return new AuthorizationResult(OpenAuthenticationStatus.AutoRegisteredAdminApproval); } case UserRegistrationType.Standard: { - //send customer welcome message - _workflowMessageService.SendCustomerWelcomeMessage(currentCustomer, _workContext.WorkingLanguage.Id); + // send customer welcome message + _messageFactory.SendCustomerWelcomeMessage(currentCustomer, _workContext.WorkingLanguage.Id); //result return new AuthorizationResult(OpenAuthenticationStatus.AutoRegisteredStandard); diff --git a/src/Libraries/SmartStore.Services/Blogs/BlogMessageFactoryExtensions.cs b/src/Libraries/SmartStore.Services/Blogs/BlogMessageFactoryExtensions.cs new file mode 100644 index 0000000000..7ef5c96706 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Blogs/BlogMessageFactoryExtensions.cs @@ -0,0 +1,19 @@ +using System; +using SmartStore.Core.Domain.Blogs; +using SmartStore.Core.Domain.Messages; +using SmartStore.Services.Messages; + +namespace SmartStore.Services.Blogs +{ + public static class BlogMessageFactoryExtensions + { + /// + /// Sends a blog comment notification message to a store owner + /// + public static CreateMessageResult SendBlogCommentNotificationMessage(this IMessageFactory factory, BlogComment blogComment, int languageId = 0) + { + Guard.NotNull(blogComment, nameof(blogComment)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.BlogCommentStoreOwner, languageId, customer: blogComment.Customer), true, blogComment); + } + } +} diff --git a/src/Libraries/SmartStore.Services/Blogs/BlogService.cs b/src/Libraries/SmartStore.Services/Blogs/BlogService.cs index 9bd71977dc..ea9c74c547 100644 --- a/src/Libraries/SmartStore.Services/Blogs/BlogService.cs +++ b/src/Libraries/SmartStore.Services/Blogs/BlogService.cs @@ -63,9 +63,6 @@ public virtual void DeleteBlogPost(BlogPost blogPost) throw new ArgumentNullException("blogPost"); _blogPostRepository.Delete(blogPost); - - //event notification - _services.EventPublisher.EntityDeleted(blogPost); } /// @@ -226,9 +223,6 @@ public virtual void InsertBlogPost(BlogPost blogPost) throw new ArgumentNullException("blogPost"); _blogPostRepository.Insert(blogPost); - - //event notification - _services.EventPublisher.EntityInserted(blogPost); } /// @@ -241,9 +235,6 @@ public virtual void UpdateBlogPost(BlogPost blogPost) throw new ArgumentNullException("blogPost"); _blogPostRepository.Update(blogPost); - - //event notification - _services.EventPublisher.EntityUpdated(blogPost); } /// diff --git a/src/Libraries/SmartStore.Services/Catalog/BackInStockSubscriptionService.cs b/src/Libraries/SmartStore.Services/Catalog/BackInStockSubscriptionService.cs index c81a8e13cd..38a945b869 100644 --- a/src/Libraries/SmartStore.Services/Catalog/BackInStockSubscriptionService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/BackInStockSubscriptionService.cs @@ -18,7 +18,7 @@ public partial class BackInStockSubscriptionService : IBackInStockSubscriptionSe #region Fields private readonly IRepository _backInStockSubscriptionRepository; - private readonly IWorkflowMessageService _workflowMessageService; + private readonly IMessageFactory _messageFactory; private readonly IWorkContext _workContext; private readonly IEventPublisher _eventPublisher; @@ -26,22 +26,16 @@ public partial class BackInStockSubscriptionService : IBackInStockSubscriptionSe #region Ctor - /// - /// Ctor - /// - /// Back in stock subscription repository - /// Workflow message service - /// Work context - /// Event publisher - public BackInStockSubscriptionService(IRepository backInStockSubscriptionRepository, - IWorkflowMessageService workflowMessageService, + public BackInStockSubscriptionService( + IRepository backInStockSubscriptionRepository, + IMessageFactory messageFactory, IWorkContext workContext, IEventPublisher eventPublisher) { - this._backInStockSubscriptionRepository = backInStockSubscriptionRepository; - this._workflowMessageService = workflowMessageService; - this._workContext = workContext; - this._eventPublisher = eventPublisher; + _backInStockSubscriptionRepository = backInStockSubscriptionRepository; + _messageFactory = messageFactory; + _workContext = workContext; + _eventPublisher = eventPublisher; } #endregion @@ -58,9 +52,6 @@ public virtual void DeleteSubscription(BackInStockSubscription subscription) throw new ArgumentNullException("subscription"); _backInStockSubscriptionRepository.Delete(subscription); - - //event notification - _eventPublisher.EntityDeleted(subscription); } /// @@ -152,9 +143,6 @@ public virtual void InsertSubscription(BackInStockSubscription subscription) throw new ArgumentNullException("subscription"); _backInStockSubscriptionRepository.Insert(subscription); - - //event notification - _eventPublisher.EntityInserted(subscription); } /// @@ -167,9 +155,6 @@ public virtual void UpdateSubscription(BackInStockSubscription subscription) throw new ArgumentNullException("subscription"); _backInStockSubscriptionRepository.Update(subscription); - - //event notification - _eventPublisher.EntityUpdated(subscription); } /// @@ -186,12 +171,10 @@ public virtual int SendNotificationsToSubscribers(Product product) var subscriptions = GetAllSubscriptionsByProductId(product.Id, 0, 0, int.MaxValue); foreach (var subscription in subscriptions) { - //ensure that customer is registered (simple and fast way) + // Ensure that customer is registered (simple and fast way) if (subscription.Customer.Email.IsEmail()) { - var customer = subscription.Customer; - var customerLanguageId = customer.GetAttribute(SystemCustomerAttributeNames.LanguageId, subscription.StoreId); - _workflowMessageService.SendBackInStockNotification(subscription, customerLanguageId); + _messageFactory.SendBackInStockNotification(subscription); result++; } } diff --git a/src/Libraries/SmartStore.Services/Catalog/CatalogMessageFactoryExtensions.cs b/src/Libraries/SmartStore.Services/Catalog/CatalogMessageFactoryExtensions.cs new file mode 100644 index 0000000000..41361d97fa --- /dev/null +++ b/src/Libraries/SmartStore.Services/Catalog/CatalogMessageFactoryExtensions.cs @@ -0,0 +1,79 @@ +using System; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Messages; +using SmartStore.Services.Messages; +using SmartStore.Services.Common; + +namespace SmartStore.Services.Catalog +{ + public static class CatalogMessageFactoryExtensions + { + /// + /// Sends "email a friend" message + /// + public static CreateMessageResult SendShareProductMessage(this IMessageFactory factory, Customer customer, Product product, + string fromEmail, string toEmail, string personalMessage, int languageId = 0) + { + Guard.NotNull(customer, nameof(customer)); + Guard.NotNull(product, nameof(product)); + + var model = new NamedModelPart("Message") + { + ["Body"] = personalMessage.NullEmpty(), + ["From"] = fromEmail.NullEmpty(), + ["To"] = toEmail.NullEmpty() + }; + + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.ShareProduct, languageId, customer: customer), true, product, model); + } + + public static CreateMessageResult SendProductQuestionMessage(this IMessageFactory factory, Customer customer, Product product, + string senderEmail, string senderName, string senderPhone, string question, int languageId = 0) + { + Guard.NotNull(customer, nameof(customer)); + Guard.NotNull(product, nameof(product)); + + var model = new NamedModelPart("Message") + { + ["Message"] = question.NullEmpty(), + ["SenderEmail"] = senderEmail.NullEmpty(), + ["SenderName"] = senderName.NullEmpty(), + ["SenderPhone"] = senderPhone.NullEmpty() + }; + + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.ProductQuestion, languageId, customer: customer), true, product, model); + } + + /// + /// Sends a product review notification message to a store owner + /// + public static CreateMessageResult SendProductReviewNotificationMessage(this IMessageFactory factory, ProductReview productReview, int languageId = 0) + { + Guard.NotNull(productReview, nameof(productReview)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.ProductReviewStoreOwner, languageId, customer: productReview.Customer), true, productReview, productReview.Product); + } + + /// + /// Sends a "quantity below" notification to a store owner + /// + public static CreateMessageResult SendQuantityBelowStoreOwnerNotification(this IMessageFactory factory, Product product, int languageId = 0) + { + Guard.NotNull(product, nameof(product)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.QuantityBelowStoreOwner, languageId), true, product); + } + + /// + /// Sends a 'Back in stock' notification message to a customer + /// + public static CreateMessageResult SendBackInStockNotification(this IMessageFactory factory, BackInStockSubscription subscription) + { + Guard.NotNull(subscription, nameof(subscription)); + + var customer = subscription.Customer; + var languageId = customer.GetAttribute(SystemCustomerAttributeNames.LanguageId); + + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.BackInStockCustomer, languageId, subscription.StoreId, customer), true, subscription.Product); + } + } +} diff --git a/src/Libraries/SmartStore.Services/Catalog/CategoryExtensions.cs b/src/Libraries/SmartStore.Services/Catalog/CategoryExtensions.cs index 39825192a6..f115704eed 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CategoryExtensions.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CategoryExtensions.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; +using SmartStore.Collections; using SmartStore.Core.Domain.Catalog; +using SmartStore.Services.Localization; namespace SmartStore.Services.Catalog { @@ -17,26 +20,33 @@ public static class CategoryExtensions /// Parent category identifier /// A value indicating whether categories without parent category in provided category list (source) should be ignored /// Sorted categories - public static IList SortCategoriesForTree(this IList source, int parentId = 0, bool ignoreCategoriesWithoutExistingParent = false) + public static IList SortCategoryNodesForTree(this IEnumerable source, int parentId = 0, bool ignoreCategoriesWithoutExistingParent = false) + where T : ICategoryNode { - if (source == null) - throw new ArgumentNullException("source"); + Guard.NotNull(source, nameof(source)); - var result = new List(); + var result = new List(); + var sourceCount = source.Count(); - var categories = source.ToList().FindAll(c => c.ParentCategoryId == parentId); - foreach (var cat in categories) + var childNodes = source.Where(c => c.ParentCategoryId == parentId).ToArray(); + foreach (var node in childNodes) { - result.Add(cat); - result.AddRange(SortCategoriesForTree(source, cat.Id, true)); + result.Add(node); + result.AddRange(SortCategoryNodesForTree(source, node.Id, true)); } - if (!ignoreCategoriesWithoutExistingParent && result.Count != source.Count) + + if (!ignoreCategoriesWithoutExistingParent && result.Count != sourceCount) { - // find categories without parent in provided category source and insert them into result + // Find categories without parent in provided category source and insert them into result foreach (var cat in source) - if (result.Where(x => x.Id == cat.Id).FirstOrDefault() == null) - result.Add(cat); + { + if (result.Where(x => x.Id == cat.Id).FirstOrDefault() == null) + { + result.Add(cat); + } + } } + return result; } @@ -54,77 +64,65 @@ public static ProductCategory FindProductCategory(this IList so if (productCategory.ProductId == productId && productCategory.CategoryId == categoryId) return productCategory; } + return null; } - public static string GetCategoryNameWithAlias(this Category category) + public static string GetCategoryNameIndented(this TreeNode treeNode, + string indentWith = "--", + int? languageId = null, + bool withAlias = true) { - if (category != null) + Guard.NotNull(treeNode, nameof(treeNode)); + + var sb = new StringBuilder(); + var indentSize = treeNode.Depth - 1; + for (int i = 0; i < indentSize; i++) { - if (category.Alias.HasValue()) - return "{0} ({1})".FormatWith(category.Name, category.Alias); - else - return category.Name; + sb.Append(indentWith); } - return null; - } - public static string GetCategoryNameWithPrefix(this Category category, ICategoryService categoryService, IDictionary mappedCategories = null) - { - string result = string.Empty; + var cat = treeNode.Value; - while (category != null) - { - if (String.IsNullOrEmpty(result)) - { - result = category.GetCategoryNameWithAlias(); - } - else - { - result = "--" + result; - } - - int parentId = category.ParentCategoryId; - if (mappedCategories == null) - { - category = categoryService.GetCategoryById(parentId); - } - else - { - category = mappedCategories.ContainsKey(parentId) ? mappedCategories[parentId] : categoryService.GetCategoryById(parentId); - } - } - return result; - } + var name = languageId.HasValue + ? cat.GetLocalized(n => n.Name, languageId.Value) + : cat.Name; - public static string GetCategoryBreadCrumb(this Category category, ICategoryService categoryService, IDictionary mappedCategories = null) - { - string result = string.Empty; + sb.Append(name); - while (category != null && !category.Deleted) - { - if (String.IsNullOrEmpty(result)) - { - result = category.GetCategoryNameWithAlias(); - } - else - { - result = category.GetCategoryNameWithAlias() + " >> " + result; - } - - int parentId = category.ParentCategoryId; - if (mappedCategories == null) - { - category = categoryService.GetCategoryById(parentId); - } - else - { - category = mappedCategories.ContainsKey(parentId) ? mappedCategories[parentId] : categoryService.GetCategoryById(parentId); - } - } + if (withAlias && cat.Alias.HasValue()) + { + sb.Append(" ("); + sb.Append(cat.Alias); + sb.Append(")"); + } - return result; - } + return sb.ToString(); + } - } + /// + /// Builds a category breadcrumb (path) for a particular category node + /// + /// The category node + /// The id of language. Pass null to skip localization. + /// true appends the category alias - if specified - to the name + /// The separator string + /// Category breadcrumb path + public static string GetCategoryPath(this ICategoryNode categoryNode, + ICategoryService categoryService, + int? languageId = null, + bool withAlias = false, + string separator = " � ") + { + Guard.NotNull(categoryNode, nameof(categoryNode)); + + var treeNode = categoryService.GetCategoryTree(categoryNode.Id, true); + if (treeNode != null) + { + return categoryService.GetCategoryPath(treeNode, languageId, withAlias, separator); + } + + return string.Empty; + } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs b/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs index 0ad8b81cab..60216e9614 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Caching; @@ -18,16 +19,19 @@ namespace SmartStore.Services.Catalog { - /// - /// Category service - /// public partial class CategoryService : ICategoryService { - private const string CATEGORIES_BY_PARENT_CATEGORY_ID_KEY = "SmartStore.category.byparent-{0}-{1}-{2}-{3}"; - private const string PRODUCTCATEGORIES_ALLBYCATEGORYID_KEY = "SmartStore.productcategory.allbycategoryid-{0}-{1}-{2}-{3}-{4}-{5}"; - private const string PRODUCTCATEGORIES_ALLBYPRODUCTID_KEY = "SmartStore.productcategory.allbyproductid-{0}-{1}-{2}-{3}"; - private const string CATEGORIES_PATTERN_KEY = "SmartStore.category."; - private const string PRODUCTCATEGORIES_PATTERN_KEY = "SmartStore.productcategory."; + internal static TimeSpan CategoryTreeCacheDuration = TimeSpan.FromHours(6); + + // {0} = IncludeHidden, {1} = CustomerRoleIds, {2} = StoreId + internal const string CATEGORY_TREE_KEY = "category:tree-{0}-{1}-{2}"; + internal const string CATEGORY_TREE_PATTERN_KEY = "category:tree-*"; + + private const string CATEGORIES_BY_PARENT_CATEGORY_ID_KEY = "category.byparent-{0}-{1}-{2}-{3}"; + private const string PRODUCTCATEGORIES_ALLBYCATEGORYID_KEY = "productcategory.allbycategoryid-{0}-{1}-{2}-{3}-{4}-{5}"; + private const string PRODUCTCATEGORIES_ALLBYPRODUCTID_KEY = "productcategory.allbyproductid-{0}-{1}-{2}-{3}"; + private const string CATEGORIES_PATTERN_KEY = "category.*"; + private const string PRODUCTCATEGORIES_PATTERN_KEY = "productcategory.*"; private readonly IRepository _categoryRepository; private readonly IRepository _productCategoryRepository; @@ -38,6 +42,7 @@ public partial class CategoryService : ICategoryService private readonly IStoreContext _storeContext; private readonly IEventPublisher _eventPublisher; private readonly IRequestCache _requestCache; + private readonly ICacheManager _cache; private readonly IStoreMappingService _storeMappingService; private readonly IAclService _aclService; private readonly ICustomerService _customerService; @@ -45,7 +50,8 @@ public partial class CategoryService : ICategoryService private readonly ICatalogSearchService _catalogSearchService; public CategoryService(IRequestCache requestCache, - IRepository categoryRepository, + ICacheManager cache, + IRepository categoryRepository, IRepository productCategoryRepository, IRepository productRepository, IRepository aclRepository, @@ -60,6 +66,7 @@ public CategoryService(IRequestCache requestCache, ICatalogSearchService catalogSearchService) { _requestCache = requestCache; + _cache = cache; _categoryRepository = categoryRepository; _productCategoryRepository = productCategoryRepository; _productRepository = productRepository; @@ -82,17 +89,16 @@ public CategoryService(IRequestCache requestCache, private void DeleteAllCategories(IList categories, bool delete) { foreach (var category in categories) - { + { if (delete) { category.Deleted = true; - _eventPublisher.EntityDeleted(category); } else { category.ParentCategoryId = 0; } - + UpdateCategory(category); var childCategories = GetAllCategoriesByParentCategoryId(category.Id, true); @@ -101,7 +107,7 @@ private void DeleteAllCategories(IList categories, bool delete) } public virtual void InheritAclIntoChildren( - int categoryId, + int categoryId, bool touchProductsWithMultipleCategories = false, bool touchExistingAcls = false, bool categoriesOnly = false) @@ -153,7 +159,7 @@ public virtual void InheritAclIntoChildren( } } } - + _aclRepository.Context.SaveChanges(); foreach (var product in products) @@ -191,7 +197,7 @@ public virtual void InheritAclIntoChildren( } public virtual void InheritStoresIntoChildren( - int categoryId, + int categoryId, bool touchProductsWithMultipleCategories = false, bool touchExistingAcls = false, bool categoriesOnly = false) @@ -287,17 +293,14 @@ public virtual void DeleteCategory(Category category, bool deleteChilds = false) category.Deleted = true; UpdateCategory(category); - _eventPublisher.EntityDeleted(category); - var childCategories = GetAllCategoriesByParentCategoryId(category.Id, true); DeleteAllCategories(childCategories, deleteChilds); } - public virtual IQueryable GetCategories( + public virtual IQueryable BuildCategoriesQuery( string categoryName = "", bool showHidden = false, string alias = null, - bool applyNavigationFilters = true, int storeId = 0) { var query = _categoryRepository.Table; @@ -330,25 +333,24 @@ orderby cGroup.Key } else { - query = ApplyHiddenCategoriesFilter(query, applyNavigationFilters, storeId); + query = ApplyHiddenCategoriesFilter(query, storeId); } query = query.Where(c => !c.Deleted); return query; } - + public virtual IPagedList GetAllCategories( - string categoryName = "", - int pageIndex = 0, - int pageSize = int.MaxValue, - bool showHidden = false, + string categoryName = "", + int pageIndex = 0, + int pageSize = int.MaxValue, + bool showHidden = false, string alias = null, - bool applyNavigationFilters = true, - bool ignoreCategoriesWithoutExistingParent = true, + bool ignoreCategoriesWithoutExistingParent = true, int storeId = 0) { - var query = GetCategories(categoryName, showHidden, alias, applyNavigationFilters, storeId); + var query = BuildCategoriesQuery(categoryName, showHidden, alias, storeId); query = query .OrderBy(x => x.ParentCategoryId) @@ -357,10 +359,10 @@ public virtual IPagedList GetAllCategories( var unsortedCategories = query.ToList(); - // sort categories - var sortedCategories = unsortedCategories.SortCategoriesForTree(ignoreCategoriesWithoutExistingParent: ignoreCategoriesWithoutExistingParent); + // Sort categories + var sortedCategories = unsortedCategories.SortCategoryNodesForTree(ignoreCategoriesWithoutExistingParent: ignoreCategoriesWithoutExistingParent); - // paging + // Paging return new PagedList(sortedCategories, pageIndex, pageSize); } @@ -381,7 +383,7 @@ public IList GetAllCategoriesByParentCategoryId(int parentCategoryId, if (!showHidden) { - query = ApplyHiddenCategoriesFilter(query, false, storeId); + query = ApplyHiddenCategoriesFilter(query, storeId); query = query.OrderBy(c => c.DisplayOrder); } @@ -390,7 +392,7 @@ public IList GetAllCategoriesByParentCategoryId(int parentCategoryId, }); } - protected virtual IQueryable ApplyHiddenCategoriesFilter(IQueryable query, bool applyNavigationFilters, int storeId = 0) + protected virtual IQueryable ApplyHiddenCategoriesFilter(IQueryable query, int storeId = 0) { // ACL (access control list) if (!QuerySettings.IgnoreAcl) @@ -424,20 +426,20 @@ orderby cGroup.Key return query; } - + public virtual IList GetAllCategoriesDisplayedOnHomePage() { var query = from c in _categoryRepository.Table orderby c.DisplayOrder where c.Published && - !c.Deleted && + !c.Deleted && c.ShowOnHomePage select c; var categories = query.ToList(); return categories; } - + public virtual Category GetCategoryById(int categoryId) { if (categoryId == 0) @@ -454,8 +456,6 @@ public virtual void InsertCategory(Category category) _requestCache.RemoveByPattern(CATEGORIES_PATTERN_KEY); _requestCache.RemoveByPattern(PRODUCTCATEGORIES_PATTERN_KEY); - - _eventPublisher.EntityInserted(category); } public virtual void UpdateCategory(Category category) @@ -478,10 +478,8 @@ public virtual void UpdateCategory(Category category) _requestCache.RemoveByPattern(CATEGORIES_PATTERN_KEY); _requestCache.RemoveByPattern(PRODUCTCATEGORIES_PATTERN_KEY); - - _eventPublisher.EntityUpdated(category); } - + public virtual void UpdateHasDiscountsApplied(Category category) { Guard.NotNull(category, nameof(category)); @@ -499,9 +497,6 @@ public virtual void DeleteProductCategory(ProductCategory productCategory) //cache _requestCache.RemoveByPattern(CATEGORIES_PATTERN_KEY); _requestCache.RemoveByPattern(PRODUCTCATEGORIES_PATTERN_KEY); - - //event notification - _eventPublisher.EntityDeleted(productCategory); } public virtual IPagedList GetProductCategoriesByCategoryId(int categoryId, int pageIndex, int pageSize, bool showHidden = false) @@ -575,7 +570,7 @@ public virtual Multimap GetProductCategoriesByProductIds(i { Guard.NotNull(productIds, nameof(productIds)); - var query = + var query = from pc in _productCategoryRepository.TableUntracked.Expand(x => x.Category).Expand(x => x.Category.Picture) join c in _categoryRepository.Table on pc.CategoryId equals c.Id where productIds.Contains(pc.ProductId) && !c.Deleted && (showHidden || c.Published) @@ -595,7 +590,7 @@ orderby pc.DisplayOrder } var map = list.ToMultimap(x => x.ProductId, x => x); - + return map; } @@ -670,15 +665,11 @@ public virtual void InsertProductCategory(ProductCategory productCategory) { if (productCategory == null) throw new ArgumentNullException("productCategory"); - + _productCategoryRepository.Insert(productCategory); - //cache _requestCache.RemoveByPattern(CATEGORIES_PATTERN_KEY); _requestCache.RemoveByPattern(PRODUCTCATEGORIES_PATTERN_KEY); - - //event notification - _eventPublisher.EntityInserted(productCategory); } public virtual void UpdateProductCategory(ProductCategory productCategory) @@ -688,79 +679,175 @@ public virtual void UpdateProductCategory(ProductCategory productCategory) _productCategoryRepository.Update(productCategory); - //cache _requestCache.RemoveByPattern(CATEGORIES_PATTERN_KEY); _requestCache.RemoveByPattern(PRODUCTCATEGORIES_PATTERN_KEY); - - //event notification - _eventPublisher.EntityUpdated(productCategory); } - public virtual ICollection GetCategoryTrail(Category category) + public virtual IEnumerable GetCategoryTrail(ICategoryNode node) { - Guard.NotNull(category, nameof(category)); + Guard.NotNull(node, nameof(node)); - var trail = new List(10); + var treeNode = GetCategoryTree(node.Id, true); - do + if (treeNode == null) { - trail.Add(category); - category = GetCategoryById(category.ParentCategoryId); + return Enumerable.Empty(); } - while (category != null && !category.Deleted && category.Published); - trail.Reverse(); - return trail; + return treeNode.Trail + .Where(x => !x.IsRoot) + //.TakeWhile(x => x.Value.Published) // TBD: (mc) do we need this? + .Select(x => x.Value); } public virtual string GetCategoryPath( - Product product, - int? languageId, - Func pathLookup, - Action addPathToCache, - Func categoryLookup, - ProductCategory prodCategory = null) + TreeNode treeNode, + int? languageId = null, + bool withAlias = false, + string separator = " � ") { - if (product == null) - return string.Empty; + Guard.NotNull(treeNode, nameof(treeNode)); - pathLookup = pathLookup ?? ((i) => { return string.Empty; }); - categoryLookup = categoryLookup ?? ((i) => { return GetCategoryById(i); }); - addPathToCache = addPathToCache ?? ((i, val) => { }); + var lookupKey = "Path.{0}.{1}.{2}".FormatInvariant(separator, languageId ?? 0, withAlias); + var cachedPath = treeNode.GetMetadata(lookupKey, false); - var alreadyProcessedCategoryIds = new List(); - var path = new List(); + if (cachedPath != null) + { + return cachedPath; + } - var productCategory = prodCategory ?? GetProductCategoriesByProductId(product.Id).FirstOrDefault(); + var trail = treeNode.Trail; + var sb = new StringBuilder(string.Empty, (trail.Count()) * 16); - if (productCategory != null && productCategory.Category != null) + foreach (var node in trail) { - string cached = pathLookup(productCategory.CategoryId); - if (cached.HasValue()) + //if (!node.Value.Published) + //{ + // // If any parent is unpublished, + // // this category is not visible: so, no path. + // sb.Clear(); + // break; + //} + + if (!node.IsRoot) { - return cached; + var cat = node.Value; + + var name = languageId.HasValue + ? cat.GetLocalized(n => n.Name, languageId.Value) + : cat.Name; + + sb.Append(name); + + if (withAlias && cat.Alias.HasValue()) + { + sb.Append(" ("); + sb.Append(cat.Alias); + sb.Append(")"); + } + + if (node != treeNode) + { + // Is not self (trail end) + sb.Append(separator); + } } + } - var category = productCategory.Category; + var path = sb.ToString(); + treeNode.SetThreadMetadata(lookupKey, path); + return path; + } - path.Add(languageId.HasValue ? category.GetLocalized(x => x.Name, languageId.Value) : category.Name); - alreadyProcessedCategoryIds.Add(category.Id); + public TreeNode GetCategoryTree(int rootCategoryId = 0, bool includeHidden = false, int storeId = 0) + { + var storeToken = QuerySettings.IgnoreMultiStore ? "0" : storeId.ToString(); + var rolesToken = QuerySettings.IgnoreAcl || includeHidden ? "0" : _workContext.CurrentCustomer.GetRolesIdent(); + var cacheKey = CATEGORY_TREE_KEY.FormatInvariant(includeHidden.ToString().ToLower(), rolesToken, storeToken); - category = categoryLookup(category.ParentCategoryId); - while (category != null && !category.Deleted && category.Published && !alreadyProcessedCategoryIds.Contains(category.Id)) + var root = _cache.Get(cacheKey, () => + { + // (Perf) don't fetch every field from db + var query = from x in BuildCategoriesQuery(showHidden: includeHidden, storeId: storeId) + orderby x.ParentCategoryId, x.DisplayOrder, x.Name + select new + { + x.Id, + x.ParentCategoryId, + x.Name, + x.Alias, + x.PictureId, + x.Published, + x.DisplayOrder, + x.UpdatedOnUtc, + x.BadgeText, + x.BadgeStyle, + x.LimitedToStores, + x.SubjectToAcl + }; + + var unsortedNodes = query.ToList().Select(x => new CategoryNode { - path.Add(languageId.HasValue ? category.GetLocalized(x => x.Name, languageId.Value) : category.Name); - alreadyProcessedCategoryIds.Add(category.Id); - category = categoryLookup(category.ParentCategoryId); - } + Id = x.Id, + ParentCategoryId = x.ParentCategoryId, + Name = x.Name, + Alias = x.Alias, + PictureId = x.PictureId, + Published = x.Published, + DisplayOrder = x.DisplayOrder, + UpdatedOnUtc = x.UpdatedOnUtc, + BadgeText = x.BadgeText, + BadgeStyle = x.BadgeStyle, + LimitedToStores = x.LimitedToStores, + SubjectToAcl = x.SubjectToAcl + }); + + var nodes = unsortedNodes.SortCategoryNodesForTree(0, true); + var curParent = new TreeNode(new CategoryNode { Name = "Home" }); + CategoryNode prevNode = null; + + foreach (var node in nodes) + { + // Determine parent + if (prevNode != null) + { + if (node.ParentCategoryId != curParent.Value.Id) + { + if (node.ParentCategoryId == prevNode.Id) + { + // level +1 + curParent = curParent.LastChild; + } + else + { + // level -x + while (!curParent.IsRoot) + { + if (curParent.Value.Id == node.ParentCategoryId) + { + break; + } + curParent = curParent.Parent; + } + } + } + } - path.Reverse(); - string result = String.Join(" > ", path); - addPathToCache(productCategory.CategoryId, result); - return result; + // add to parent + curParent.Append(node, node.Id); + + prevNode = node; + } + + return curParent.Root; + }, CategoryTreeCacheDuration); + + if (rootCategoryId > 0) + { + root = root.SelectNodeById(rootCategoryId); } - return string.Empty; + return root; } - } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/CategoryTemplateService.cs b/src/Libraries/SmartStore.Services/Catalog/CategoryTemplateService.cs index d5211c66b3..c455ddc0c6 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CategoryTemplateService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CategoryTemplateService.cs @@ -24,9 +24,6 @@ public virtual void DeleteCategoryTemplate(CategoryTemplate categoryTemplate) throw new ArgumentNullException("categoryTemplate"); _categoryTemplateRepository.Delete(categoryTemplate); - - //event notification - _eventPublisher.EntityDeleted(categoryTemplate); } public virtual IList GetAllCategoryTemplates() @@ -53,9 +50,6 @@ public virtual void InsertCategoryTemplate(CategoryTemplate categoryTemplate) throw new ArgumentNullException("categoryTemplate"); _categoryTemplateRepository.Insert(categoryTemplate); - - //event notification - _eventPublisher.EntityInserted(categoryTemplate); } public virtual void UpdateCategoryTemplate(CategoryTemplate categoryTemplate) @@ -64,9 +58,6 @@ public virtual void UpdateCategoryTemplate(CategoryTemplate categoryTemplate) throw new ArgumentNullException("categoryTemplate"); _categoryTemplateRepository.Update(categoryTemplate); - - //event notification - _eventPublisher.EntityUpdated(categoryTemplate); } } } diff --git a/src/Libraries/SmartStore.Services/Catalog/CategoryTreeChangeHandler.cs b/src/Libraries/SmartStore.Services/Catalog/CategoryTreeChangeHandler.cs new file mode 100644 index 0000000000..22b31b1c5c --- /dev/null +++ b/src/Libraries/SmartStore.Services/Catalog/CategoryTreeChangeHandler.cs @@ -0,0 +1,308 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using SmartStore.Collections; +using SmartStore.Core.Data; +using SmartStore.Core.Data.Hooks; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Configuration; +using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Security; +using SmartStore.Core.Domain.Stores; +using SmartStore.Data; + +namespace SmartStore.Services.Catalog +{ + public enum CategoryTreeChangeReason + { + ElementCounts, + Data, + Localization, + StoreMapping, + Acl, + Hierarchy + } + + public class CategoryTreeChangedEvent + { + public CategoryTreeChangedEvent(CategoryTreeChangeReason reason) + { + Reason = reason; + } + + public CategoryTreeChangeReason Reason { get; private set; } + } + + public class CategoryTreeChangeHook : IDbSaveHook + { + private readonly ICommonServices _services; + private readonly ICategoryService _categoryService; + + private readonly bool[] _handledReasons = new bool[(int)CategoryTreeChangeReason.Hierarchy + 1]; + private bool _invalidated; + + private static readonly HashSet _countAffectingProductProps = new HashSet(); + + // Hierarchy affecting category prop names + private static readonly string[] _h = new string[] { "ParentCategoryId", "Published", "Deleted", "DisplayOrder" }; + // Visibility affecting category prop names + private static readonly string[] _a = new string[] { "LimitedToStores", "SubjectToAcl" }; + // Data affecting category prop names + private static readonly string[] _d = new string[] { "Name", "Alias", "PictureId", "BadgeText", "BadgeStyle" }; + + private static readonly HashSet _candidateTypes = new HashSet + { + typeof(Product), + typeof(Category), + typeof(ProductCategory), + typeof(Setting), + typeof(Language), + typeof(LocalizedProperty), + typeof(StoreMapping), + typeof(AclRecord) + }; + + static CategoryTreeChangeHook() + { + AddPropsToSet(_countAffectingProductProps, + x => x.AvailableEndDateTimeUtc, + x => x.AvailableStartDateTimeUtc, + x => x.Deleted, + x => x.LowStockActivityId, + x => x.LimitedToStores, + x => x.ManageInventoryMethodId, + x => x.MinStockQuantity, + x => x.Published, + x => x.SubjectToAcl, + x => x.VisibleIndividually); + } + + static void AddPropsToSet(HashSet props, params Expression>[] lambdas) + { + foreach (var lambda in lambdas) + { + props.Add(lambda.ExtractPropertyInfo().Name); + } + } + + public CategoryTreeChangeHook(ICommonServices services, ICategoryService categoryService) + { + _services = services; + _categoryService = categoryService; + } + + public void OnBeforeSave(IHookedEntity entry) + { + if (_invalidated) + return; + + if (entry.InitialState != EntityState.Modified) + throw new NotSupportedException(); + + var cache = _services.Cache; + var entity = entry.Entity; + + if (entity is Product) + { + var modProps = _services.DbContext.GetModifiedProperties(entity); + if (modProps.Keys.Any(x => _countAffectingProductProps.Contains(x))) + { + // No eviction, just notification + PublishEvent(CategoryTreeChangeReason.ElementCounts); + } + } + else if (entity is ProductCategory) + { + var modProps = _services.DbContext.GetModifiedProperties(entity); + if (modProps.ContainsKey("CategoryId")) + { + // No eviction, just notification + PublishEvent(CategoryTreeChangeReason.ElementCounts); + } + } + else if (entity is Category) + { + var category = entity as Category; + + var modProps = _services.DbContext.GetModifiedProperties(entity); + + if (modProps.Keys.Any(x => _h.Contains(x))) + { + // Hierarchy affecting properties has changed. Nuke every tree. + cache.RemoveByPattern(CategoryService.CATEGORY_TREE_PATTERN_KEY); + PublishEvent(CategoryTreeChangeReason.Hierarchy); + _invalidated = true; + } + else if (modProps.Keys.Any(x => _a.Contains(x))) + { + if (modProps.ContainsKey("LimitedToStores")) + { + // Don't nuke store agnostic trees + cache.RemoveByPattern(BuildCacheKeyPattern("*", "*", "[^0]*")); + PublishEvent(CategoryTreeChangeReason.StoreMapping); + } + if (modProps.ContainsKey("SubjectToAcl")) + { + // Don't nuke ACL agnostic trees + cache.RemoveByPattern(BuildCacheKeyPattern("*", "[^0]*", "*")); + PublishEvent(CategoryTreeChangeReason.Acl); + } + } + else if (modProps.Keys.Any(x => _d.Contains(x))) + { + // Only data has changed. Don't nuke trees, update corresponding cache entries instead. + var keys = cache.Keys(CategoryService.CATEGORY_TREE_PATTERN_KEY).ToArray(); + foreach (var key in keys) + { + var tree = cache.Get>(key); + if (tree != null) + { + var node = tree.SelectNodeById(entity.Id); + if (node != null) + { + var value = node.Value as CategoryNode; + if (value == null) + { + // Cannot update. Nuke tree. + cache.Remove(key); + } + else + { + value.Name = category.Name; + value.Alias = category.Alias; + value.PictureId = category.PictureId; + value.BadgeText = category.BadgeText; + value.BadgeStyle = category.BadgeStyle; + + // Persist to cache store + cache.Put(key, tree, CategoryService.CategoryTreeCacheDuration); + } + } + } + } + + // Publish event only once + PublishEvent(CategoryTreeChangeReason.Data); + } + } + else + { + throw new NotSupportedException(); + } + } + + public void OnAfterSave(IHookedEntity entry) + { + if (_invalidated) + return; + + if (!_candidateTypes.Contains(entry.EntityType)) + throw new NotSupportedException(); + + // INFO: Acl & StoreMapping affect element counts + + var cache = _services.Cache; + var isNewOrDeleted = entry.InitialState == EntityState.Added || entry.InitialState == EntityState.Deleted; + var entity = entry.Entity; + + if (entity is Product) + { + // INFO: 'Modified' case already handled in 'OnBeforeSave()' + if (entry.InitialState == EntityState.Deleted || (entry.InitialState == EntityState.Added && ((Product)entity).Published)) + { + // No eviction, just notification, but for PUBLISHED products only + PublishEvent(CategoryTreeChangeReason.ElementCounts); + } + } + else if (entity is ProductCategory && isNewOrDeleted) + { + // INFO: 'Modified' case already handled in 'OnBeforeSave()' + // New or deleted product category mappings affect counts + PublishEvent(CategoryTreeChangeReason.ElementCounts); + } + else if (entity is Category && isNewOrDeleted) + { + // INFO: 'Modified' case already handled in 'OnBeforeSave()' + // Hierarchy affecting change, nuke all. + cache.RemoveByPattern(CategoryService.CATEGORY_TREE_PATTERN_KEY); + _invalidated = true; + } + else if (entity is Setting) + { + var name = (entity as Setting).Name.ToLowerInvariant(); + if (name == "catalogsettings.showcategoryproductnumber" || name == "catalogsettings.showcategoryproductnumberincludingsubcategories") + { + PublishEvent(CategoryTreeChangeReason.ElementCounts); + } + } + else if (entity is Language && entry.InitialState == EntityState.Deleted) + { + PublishEvent(CategoryTreeChangeReason.Localization); + } + else if (entity is LocalizedProperty) + { + var lp = entity as LocalizedProperty; + var key = lp.LocaleKey; + if (lp.LocaleKeyGroup == "Category" && (key == "Name" || key == "BadgeText")) + { + PublishEvent(CategoryTreeChangeReason.Localization); + } + } + else if (entity is StoreMapping) + { + var stm = entity as StoreMapping; + if (stm.EntityName == "Product") + { + PublishEvent(CategoryTreeChangeReason.ElementCounts); + } + else if (stm.EntityName == "Category") + { + // Don't nuke store agnostic trees + cache.RemoveByPattern(BuildCacheKeyPattern("*", "*", "[^0]*")); + PublishEvent(CategoryTreeChangeReason.StoreMapping); + } + } + else if (entity is AclRecord) + { + var acl = entity as AclRecord; + if (!acl.IsIdle) + { + if (acl.EntityName == "Product") + { + PublishEvent(CategoryTreeChangeReason.ElementCounts); + } + else if (acl.EntityName == "Category") + { + // Don't nuke ACL agnostic trees + cache.RemoveByPattern(BuildCacheKeyPattern("*", "[^0]*", "*")); + PublishEvent(CategoryTreeChangeReason.Acl); + } + } + } + } + + private void PublishEvent(CategoryTreeChangeReason reason) + { + if (_handledReasons[(int)reason] == false) + { + _services.EventPublisher.Publish(new CategoryTreeChangedEvent(reason)); + _handledReasons[(int)reason] = true; + } + } + + private string BuildCacheKeyPattern(string includeHiddenToken = "*", string rolesToken = "*", string storeToken = "*") + { + return CategoryService.CATEGORY_TREE_KEY.FormatInvariant(includeHiddenToken, rolesToken, storeToken); + } + + public void OnBeforeSaveCompleted() + { + } + + public void OnAfterSaveCompleted() + { + } + } +} diff --git a/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs b/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs index 3bd350940f..c3d587f42c 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs @@ -1,9 +1,14 @@ using System; using System.Collections.Generic; +using System.Data.Entity; +using System.Globalization; using System.Linq; +using SmartStore.Core.Logging; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Media; +using SmartStore.Core.Localization; using SmartStore.Services.Localization; using SmartStore.Services.Media; using SmartStore.Services.Search; @@ -12,454 +17,591 @@ namespace SmartStore.Services.Catalog { - /// - /// Copy Product service - /// public partial class CopyProductService : ICopyProductService { - #region Fields - - private readonly IProductService _productService; + private readonly IRepository _productRepository; + private readonly IRepository _relatedProductRepository; + private readonly IRepository _crossSellProductRepository; + private readonly ICommonServices _services; + private readonly IProductService _productService; private readonly IProductAttributeService _productAttributeService; private readonly ILanguageService _languageService; private readonly ILocalizedEntityService _localizedEntityService; private readonly IPictureService _pictureService; - private readonly ICategoryService _categoryService; - private readonly IManufacturerService _manufacturerService; - private readonly ISpecificationAttributeService _specificationAttributeService; private readonly IDownloadService _downloadService; private readonly IProductAttributeParser _productAttributeParser; private readonly IUrlRecordService _urlRecordService; private readonly IStoreMappingService _storeMappingService; - private readonly ILocalizationService _localizationService; private readonly ICatalogSearchService _catalogSearchService; - #endregion - - #region Ctor - public CopyProductService( + IRepository productRepository, + IRepository relatedProductRepository, + IRepository crossSellProductRepository, + ICommonServices services, IProductService productService, IProductAttributeService productAttributeService, ILanguageService languageService, ILocalizedEntityService localizedEntityService, IPictureService pictureService, - ICategoryService categoryService, - IManufacturerService manufacturerService, - ISpecificationAttributeService specificationAttributeService, IDownloadService downloadService, IProductAttributeParser productAttributeParser, IUrlRecordService urlRecordService, IStoreMappingService storeMappingService, - ILocalizationService localizationService, ICatalogSearchService catalogSearchService) { - _productService = productService; + _productRepository = productRepository; + _relatedProductRepository = relatedProductRepository; + _crossSellProductRepository = crossSellProductRepository; + _services = services; + _productService = productService; _productAttributeService = productAttributeService; _languageService = languageService; _localizedEntityService = localizedEntityService; _pictureService = pictureService; - _categoryService = categoryService; - _manufacturerService = manufacturerService; - _specificationAttributeService = specificationAttributeService; _downloadService = downloadService; _productAttributeParser = productAttributeParser; _urlRecordService = urlRecordService; _storeMappingService = storeMappingService; - _localizationService = localizationService; _catalogSearchService = catalogSearchService; - } - #endregion + T = NullLocalizer.Instance; + } - #region Methods + public Localizer T { get; set; } - /// - /// Create a copy of product with all depended data - /// - /// The product - /// The name of product duplicate - /// A value indicating whether the product duplicate should be published - /// A value indicating whether the product images should be copied - /// A value indicating whether the copy associated products - /// Product entity - public virtual Product CopyProduct(Product product, string newName, bool isPublished, bool copyImages, bool copyAssociatedProducts = true) + public virtual Product CopyProduct( + Product product, + string newName, + bool isPublished, + bool copyImages, + bool copyAssociatedProducts = true) { - if (product == null) - throw new ArgumentNullException("product"); + Guard.NotNull(product, nameof(product)); + Guard.NotEmpty(newName, nameof(newName)); - if (String.IsNullOrEmpty(newName)) - throw new ArgumentException("Product name is required"); + using (_services.Chronometer.Step("Copy product " + product.Id)) + { + Product clone = null; + var utcNow = DateTime.UtcNow; + var languages = _languageService.GetAllLanguages(true); + + // Media stuff + int? downloadId = null; + int? sampleDownloadId = null; + var clonedPictures = new Dictionary(); // Key = former ID, Value = cloned picture + + using (var scope = new DbContextScope(ctx: _productRepository.Context, + autoCommit: false, + autoDetectChanges: false, + proxyCreation: true, + validateOnSave: false, + forceNoTracking: true, + hooksEnabled: false)) + { + if (product.IsDownload) + { + downloadId = CopyDownload(product.DownloadId)?.Id; + } - Product productCopy = null; - var utcNow = DateTime.UtcNow; + if (product.HasSampleDownload) + { + sampleDownloadId = CopyDownload(product.SampleDownloadId.GetValueOrDefault())?.Id; + } - // product download & sample download - int downloadId = 0; - int? sampleDownloadId = null; + if (copyImages) + { + clonedPictures = CopyPictures(product, newName); + } - if (product.IsDownload) - { - var download = _downloadService.GetDownloadById(product.DownloadId); - if (download != null) - { - var downloadCopy = new Download + // Product + clone = new Product { - DownloadGuid = Guid.NewGuid(), - UseDownloadUrl = download.UseDownloadUrl, - DownloadUrl = download.DownloadUrl, - ContentType = download.ContentType, - Filename = download.Filename, - Extension = download.Extension, - IsNew = download.IsNew, + ProductTypeId = product.ProductTypeId, + ParentGroupedProductId = product.ParentGroupedProductId, + VisibleIndividually = product.VisibleIndividually, + Name = newName, + ShortDescription = product.ShortDescription, + FullDescription = product.FullDescription, + ProductTemplateId = product.ProductTemplateId, + AdminComment = product.AdminComment, + ShowOnHomePage = product.ShowOnHomePage, + HomePageDisplayOrder = product.HomePageDisplayOrder, + MetaKeywords = product.MetaKeywords, + MetaDescription = product.MetaDescription, + MetaTitle = product.MetaTitle, + AllowCustomerReviews = product.AllowCustomerReviews, + LimitedToStores = product.LimitedToStores, + Sku = product.Sku, + ManufacturerPartNumber = product.ManufacturerPartNumber, + Gtin = product.Gtin, + IsGiftCard = product.IsGiftCard, + GiftCardType = product.GiftCardType, + RequireOtherProducts = product.RequireOtherProducts, + RequiredProductIds = product.RequiredProductIds, + AutomaticallyAddRequiredProducts = product.AutomaticallyAddRequiredProducts, + IsDownload = product.IsDownload, + DownloadId = downloadId ?? 0, + UnlimitedDownloads = product.UnlimitedDownloads, + MaxNumberOfDownloads = product.MaxNumberOfDownloads, + DownloadExpirationDays = product.DownloadExpirationDays, + DownloadActivationType = product.DownloadActivationType, + HasSampleDownload = product.HasSampleDownload, + SampleDownloadId = sampleDownloadId, + HasUserAgreement = product.HasUserAgreement, + UserAgreementText = product.UserAgreementText, + IsRecurring = product.IsRecurring, + RecurringCycleLength = product.RecurringCycleLength, + RecurringCyclePeriod = product.RecurringCyclePeriod, + RecurringTotalCycles = product.RecurringTotalCycles, + IsShipEnabled = product.IsShipEnabled, + IsFreeShipping = product.IsFreeShipping, + AdditionalShippingCharge = product.AdditionalShippingCharge, + IsEsd = product.IsEsd, + IsTaxExempt = product.IsTaxExempt, + TaxCategoryId = product.TaxCategoryId, + ManageInventoryMethod = product.ManageInventoryMethod, + StockQuantity = product.StockQuantity, + DisplayStockAvailability = product.DisplayStockAvailability, + DisplayStockQuantity = product.DisplayStockQuantity, + MinStockQuantity = product.MinStockQuantity, + LowStockActivityId = product.LowStockActivityId, + NotifyAdminForQuantityBelow = product.NotifyAdminForQuantityBelow, + BackorderMode = product.BackorderMode, + AllowBackInStockSubscriptions = product.AllowBackInStockSubscriptions, + OrderMinimumQuantity = product.OrderMinimumQuantity, + OrderMaximumQuantity = product.OrderMaximumQuantity, + HideQuantityControl = product.HideQuantityControl, + AllowedQuantities = product.AllowedQuantities, + DisableBuyButton = product.DisableBuyButton, + DisableWishlistButton = product.DisableWishlistButton, + AvailableForPreOrder = product.AvailableForPreOrder, + CallForPrice = product.CallForPrice, + Price = product.Price, + OldPrice = product.OldPrice, + ProductCost = product.ProductCost, + SpecialPrice = product.SpecialPrice, + SpecialPriceStartDateTimeUtc = product.SpecialPriceStartDateTimeUtc, + SpecialPriceEndDateTimeUtc = product.SpecialPriceEndDateTimeUtc, + CustomerEntersPrice = product.CustomerEntersPrice, + MinimumCustomerEnteredPrice = product.MinimumCustomerEnteredPrice, + MaximumCustomerEnteredPrice = product.MaximumCustomerEnteredPrice, + LowestAttributeCombinationPrice = product.LowestAttributeCombinationPrice, + Weight = product.Weight, + Length = product.Length, + Width = product.Width, + Height = product.Height, + AvailableStartDateTimeUtc = product.AvailableStartDateTimeUtc, + AvailableEndDateTimeUtc = product.AvailableEndDateTimeUtc, + DisplayOrder = product.DisplayOrder, + Published = isPublished, + Deleted = product.Deleted, + DeliveryTimeId = product.DeliveryTimeId, + QuantityUnitId = product.QuantityUnitId, + BasePriceEnabled = product.BasePriceEnabled, + BasePriceMeasureUnit = product.BasePriceMeasureUnit, + BasePriceAmount = product.BasePriceAmount, + BasePriceBaseAmount = product.BasePriceBaseAmount, + BundleTitleText = product.BundleTitleText, + BundlePerItemShipping = product.BundlePerItemShipping, + BundlePerItemPricing = product.BundlePerItemPricing, + BundlePerItemShoppingCart = product.BundlePerItemShoppingCart, + CustomsTariffNumber = product.CustomsTariffNumber, + CountryOfOriginId = product.CountryOfOriginId, + CreatedOnUtc = utcNow, UpdatedOnUtc = utcNow }; - if ((download.MediaStorageId ?? 0) != 0 && download.MediaStorage != null) - _downloadService.InsertDownload(downloadCopy, download.MediaStorage.Data); - else - _downloadService.InsertDownload(downloadCopy, null); + // Category mappings + foreach (var pc in product.ProductCategories) + { + clone.ProductCategories.Add(new ProductCategory + { + CategoryId = pc.CategoryId, + IsFeaturedProduct = pc.IsFeaturedProduct, + DisplayOrder = pc.DisplayOrder + }); + } - downloadId = downloadCopy.Id; - } + // Manufacturer mappings + foreach (var pm in product.ProductManufacturers) + { + clone.ProductManufacturers.Add(new ProductManufacturer + { + ManufacturerId = pm.ManufacturerId, + IsFeaturedProduct = pm.IsFeaturedProduct, + DisplayOrder = pm.DisplayOrder + }); + } - if (product.HasSampleDownload) - { - var sampleDownload = _downloadService.GetDownloadById(product.SampleDownloadId.GetValueOrDefault()); - if (sampleDownload != null) + // Picture mappings + if (copyImages) { - var sampleDownloadCopy = new Download + foreach (var pp in product.ProductPictures) { - DownloadGuid = Guid.NewGuid(), - UseDownloadUrl = sampleDownload.UseDownloadUrl, - DownloadUrl = sampleDownload.DownloadUrl, - ContentType = sampleDownload.ContentType, - Filename = sampleDownload.Filename, - Extension = sampleDownload.Extension, - IsNew = sampleDownload.IsNew, - UpdatedOnUtc = utcNow - }; - - if ((sampleDownload.MediaStorageId ?? 0) != 0 && sampleDownload.MediaStorage != null) - _downloadService.InsertDownload(sampleDownloadCopy, sampleDownload.MediaStorage.Data); - else - _downloadService.InsertDownload(sampleDownloadCopy, null); + var pictureClone = clonedPictures.Get(pp.PictureId); + if (pictureClone != null) + { + clone.ProductPictures.Add(new ProductPicture + { + PictureId = pictureClone.Id, + DisplayOrder = pp.DisplayOrder + }); + } + } + } + + // Product specifications + foreach (var psa in product.ProductSpecificationAttributes) + { + clone.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + SpecificationAttributeOptionId = psa.SpecificationAttributeOptionId, + AllowFiltering = psa.AllowFiltering, + ShowOnProductPage = psa.ShowOnProductPage, + DisplayOrder = psa.DisplayOrder + }); + } + + // Tier prices + foreach (var tp in product.TierPrices) + { + clone.TierPrices.Add(new TierPrice + { + StoreId = tp.StoreId, + CustomerRoleId = tp.CustomerRoleId, + Quantity = tp.Quantity, + Price = tp.Price, + CalculationMethod = tp.CalculationMethod + }); + } + + // Discount mapping + foreach (var discount in product.AppliedDiscounts) + { + clone.AppliedDiscounts.Add(discount); + } + + // Tags + foreach (var tag in product.ProductTags) + { + clone.ProductTags.Add(tag); + } + + // >>>>>>> Put clone to db (from here on we need the product clone's ID) + _productRepository.Insert(clone); + Commit(); - sampleDownloadId = sampleDownloadCopy.Id; + // Related products mapping + foreach (var rp in _productService.GetRelatedProductsByProductId1(product.Id, true)) + { + _relatedProductRepository.Insert(new RelatedProduct + { + ProductId1 = clone.Id, + ProductId2 = rp.ProductId2, + DisplayOrder = rp.DisplayOrder + }); + } + + // CrossSell products mapping + foreach (var csp in _productService.GetCrossSellProductsByProductId1(product.Id, true)) + { + _crossSellProductRepository.Insert(new CrossSellProduct + { + ProductId1 = clone.Id, + ProductId2 = csp.ProductId2 + }); + } + + // Store mapping + var selectedStoreIds = _storeMappingService.GetStoresIdsWithAccess(product); + foreach (var id in selectedStoreIds) + { + _storeMappingService.InsertStoreMapping(clone, id); + } + + // SEO + ProcessSlug(clone); + + // Localization + ProcessLocalization(product, clone, languages); + + // Attr stuff ... + ProcessAttributes(product, clone, newName, copyImages, clonedPictures, languages); + + // update computed properties + clone.HasTierPrices = clone.TierPrices.Count > 0; + clone.HasDiscountsApplied = clone.AppliedDiscounts.Count > 0; + clone.LowestAttributeCombinationPrice = _productAttributeService.GetLowestCombinationPrice(clone.Id); + clone.MainPictureId = clone.ProductPictures.OrderBy(x => x.DisplayOrder).Select(x => x.PictureId).FirstOrDefault(); + + // Associated products + if (copyAssociatedProducts && product.ProductType != ProductType.BundledProduct) + { + ProcessAssociatedProducts(product, clone, isPublished, copyImages); } + + // Bundle items + ProcessBundleItems(product, clone); + + // >>>>>>> Our final commit + Commit(); + } + + return clone; + } + } + + private void Commit() + { + _services.DbContext.SaveChanges(); + } + + private Download CopyDownload(int downloadId) + { + var download = _downloadService.GetDownloadById(downloadId); + + if (download == null) + { + return null; + } + + var clone = new Download + { + DownloadGuid = Guid.NewGuid(), + UseDownloadUrl = download.UseDownloadUrl, + DownloadUrl = download.DownloadUrl, + ContentType = download.ContentType, + Filename = download.Filename, + Extension = download.Extension, + IsNew = download.IsNew, + UpdatedOnUtc = DateTime.UtcNow + }; + + using (var scope = new DbContextScope(ctx: _productRepository.Context, autoCommit: true)) + { + _downloadService.InsertDownload(clone, download.MediaStorage?.Data); + } + + return clone; + } + + private Dictionary CopyPictures(Product product, string newProductName) + { + var clonedPictures = new Dictionary(); + var seoFilename = _pictureService.GetPictureSeName(newProductName); + + foreach (var pp in product.ProductPictures) + { + var clone = CopyPicture(pp.Picture, seoFilename); + clonedPictures[pp.PictureId] = clone; + } + + return clonedPictures; + } + + private Picture CopyPicture(Picture picture, string seoFilename) + { + using (var scope = new DbContextScope(ctx: _productRepository.Context, autoCommit: true)) + { + var buffer = _pictureService.LoadPictureBinary(picture); + + var clone = _pictureService.InsertPicture( + buffer, + picture.MimeType, + seoFilename, + true, + picture.Width ?? 0, + picture.Height ?? 0, + false); + + return clone; + } + } + + private void ProcessSlug(Product clone) + { + using (var scope = new DbContextScope(ctx: _productRepository.Context, autoCommit: true)) + { + _urlRecordService.SaveSlug(clone, clone.ValidateSeName("", clone.Name, true), 0); + } + } + + private void ProcessLocalization(Product product, Product clone, IEnumerable languages) + { + using (var scope = new DbContextScope(ctx: _productRepository.Context, autoCommit: true)) + { + foreach (var lang in languages) + { + var name = product.GetLocalized(x => x.Name, lang.Id, false, false); + if (!String.IsNullOrEmpty(name)) + _localizedEntityService.SaveLocalizedValue(clone, x => x.Name, name, lang.Id); + + var shortDescription = product.GetLocalized(x => x.ShortDescription, lang.Id, false, false); + if (!String.IsNullOrEmpty(shortDescription)) + _localizedEntityService.SaveLocalizedValue(clone, x => x.ShortDescription, shortDescription, lang.Id); + + var fullDescription = product.GetLocalized(x => x.FullDescription, lang.Id, false, false); + if (!String.IsNullOrEmpty(fullDescription)) + _localizedEntityService.SaveLocalizedValue(clone, x => x.FullDescription, fullDescription, lang.Id); + + var metaKeywords = product.GetLocalized(x => x.MetaKeywords, lang.Id, false, false); + if (!String.IsNullOrEmpty(metaKeywords)) + _localizedEntityService.SaveLocalizedValue(clone, x => x.MetaKeywords, metaKeywords, lang.Id); + + var metaDescription = product.GetLocalized(x => x.MetaDescription, lang.Id, false, false); + if (!String.IsNullOrEmpty(metaDescription)) + _localizedEntityService.SaveLocalizedValue(clone, x => x.MetaDescription, metaDescription, lang.Id); + + var metaTitle = product.GetLocalized(x => x.MetaTitle, lang.Id, false, false); + if (!String.IsNullOrEmpty(metaTitle)) + _localizedEntityService.SaveLocalizedValue(clone, x => x.MetaTitle, metaTitle, lang.Id); + + var bundleTitleText = product.GetLocalized(x => x.BundleTitleText, lang.Id, false, false); + if (!String.IsNullOrEmpty(bundleTitleText)) + _localizedEntityService.SaveLocalizedValue(clone, x => x.BundleTitleText, bundleTitleText, lang.Id); + + // search engine name + _urlRecordService.SaveSlug(clone, clone.ValidateSeName("", name, false), lang.Id); } } + } + + private void ProcessAssociatedProducts(Product product, Product clone, bool isPublished, bool copyImages) + { + var copyOf = T("Admin.Common.CopyOf"); + var searchQuery = new CatalogSearchQuery().HasParentGroupedProduct(product.Id); + + var query = _catalogSearchService.PrepareQuery(searchQuery); + var associatedProducts = query.OrderBy(p => p.DisplayOrder).ToList(); + + foreach (var associatedProduct in associatedProducts) + { + var associatedProductCopy = CopyProduct(associatedProduct, $"{copyOf} {associatedProduct.Name}", isPublished, copyImages, false); + associatedProductCopy.ParentGroupedProductId = clone.Id; + } + } - // product - productCopy = new Product - { - ProductTypeId = product.ProductTypeId, - ParentGroupedProductId = product.ParentGroupedProductId, - VisibleIndividually = product.VisibleIndividually, - Name = newName, - ShortDescription = product.ShortDescription, - FullDescription = product.FullDescription, - ProductTemplateId = product.ProductTemplateId, - AdminComment = product.AdminComment, - ShowOnHomePage = product.ShowOnHomePage, - HomePageDisplayOrder = product.HomePageDisplayOrder, - MetaKeywords = product.MetaKeywords, - MetaDescription = product.MetaDescription, - MetaTitle = product.MetaTitle, - AllowCustomerReviews = product.AllowCustomerReviews, - LimitedToStores = product.LimitedToStores, - Sku = product.Sku, - ManufacturerPartNumber = product.ManufacturerPartNumber, - Gtin = product.Gtin, - IsGiftCard = product.IsGiftCard, - GiftCardType = product.GiftCardType, - RequireOtherProducts = product.RequireOtherProducts, - RequiredProductIds = product.RequiredProductIds, - AutomaticallyAddRequiredProducts = product.AutomaticallyAddRequiredProducts, - IsDownload = product.IsDownload, - DownloadId = downloadId, - UnlimitedDownloads = product.UnlimitedDownloads, - MaxNumberOfDownloads = product.MaxNumberOfDownloads, - DownloadExpirationDays = product.DownloadExpirationDays, - DownloadActivationType = product.DownloadActivationType, - HasSampleDownload = product.HasSampleDownload, - SampleDownloadId = sampleDownloadId, - HasUserAgreement = product.HasUserAgreement, - UserAgreementText = product.UserAgreementText, - IsRecurring = product.IsRecurring, - RecurringCycleLength = product.RecurringCycleLength, - RecurringCyclePeriod = product.RecurringCyclePeriod, - RecurringTotalCycles = product.RecurringTotalCycles, - IsShipEnabled = product.IsShipEnabled, - IsFreeShipping = product.IsFreeShipping, - AdditionalShippingCharge = product.AdditionalShippingCharge, - IsEsd = product.IsEsd, - IsTaxExempt = product.IsTaxExempt, - TaxCategoryId = product.TaxCategoryId, - ManageInventoryMethod = product.ManageInventoryMethod, - StockQuantity = product.StockQuantity, - DisplayStockAvailability = product.DisplayStockAvailability, - DisplayStockQuantity = product.DisplayStockQuantity, - MinStockQuantity = product.MinStockQuantity, - LowStockActivityId = product.LowStockActivityId, - NotifyAdminForQuantityBelow = product.NotifyAdminForQuantityBelow, - BackorderMode = product.BackorderMode, - AllowBackInStockSubscriptions = product.AllowBackInStockSubscriptions, - OrderMinimumQuantity = product.OrderMinimumQuantity, - OrderMaximumQuantity = product.OrderMaximumQuantity, - HideQuantityControl = product.HideQuantityControl, - AllowedQuantities = product.AllowedQuantities, - DisableBuyButton = product.DisableBuyButton, - DisableWishlistButton = product.DisableWishlistButton, - AvailableForPreOrder = product.AvailableForPreOrder, - CallForPrice = product.CallForPrice, - Price = product.Price, - OldPrice = product.OldPrice, - ProductCost = product.ProductCost, - SpecialPrice = product.SpecialPrice, - SpecialPriceStartDateTimeUtc = product.SpecialPriceStartDateTimeUtc, - SpecialPriceEndDateTimeUtc = product.SpecialPriceEndDateTimeUtc, - CustomerEntersPrice = product.CustomerEntersPrice, - MinimumCustomerEnteredPrice = product.MinimumCustomerEnteredPrice, - MaximumCustomerEnteredPrice = product.MaximumCustomerEnteredPrice, - LowestAttributeCombinationPrice = product.LowestAttributeCombinationPrice, - Weight = product.Weight, - Length = product.Length, - Width = product.Width, - Height = product.Height, - AvailableStartDateTimeUtc = product.AvailableStartDateTimeUtc, - AvailableEndDateTimeUtc = product.AvailableEndDateTimeUtc, - DisplayOrder = product.DisplayOrder, - Published = isPublished, - Deleted = product.Deleted, - DeliveryTimeId = product.DeliveryTimeId, - QuantityUnitId = product.QuantityUnitId, - BasePriceEnabled = product.BasePriceEnabled, - BasePriceMeasureUnit = product.BasePriceMeasureUnit, - BasePriceAmount = product.BasePriceAmount, - BasePriceBaseAmount = product.BasePriceBaseAmount, - BundleTitleText = product.BundleTitleText, - BundlePerItemShipping = product.BundlePerItemShipping, - BundlePerItemPricing = product.BundlePerItemPricing, - BundlePerItemShoppingCart = product.BundlePerItemShoppingCart, - CustomsTariffNumber = product.CustomsTariffNumber, - CountryOfOriginId = product.CountryOfOriginId - }; - - _productService.InsertProduct(productCopy); - - //search engine name - _urlRecordService.SaveSlug(productCopy, productCopy.ValidateSeName("", productCopy.Name, true), 0); - - var languages = _languageService.GetAllLanguages(true); - - //localization - foreach (var lang in languages) - { - var name = product.GetLocalized(x => x.Name, lang.Id, false, false); - if (!String.IsNullOrEmpty(name)) - _localizedEntityService.SaveLocalizedValue(productCopy, x => x.Name, name, lang.Id); - - var shortDescription = product.GetLocalized(x => x.ShortDescription, lang.Id, false, false); - if (!String.IsNullOrEmpty(shortDescription)) - _localizedEntityService.SaveLocalizedValue(productCopy, x => x.ShortDescription, shortDescription, lang.Id); - - var fullDescription = product.GetLocalized(x => x.FullDescription, lang.Id, false, false); - if (!String.IsNullOrEmpty(fullDescription)) - _localizedEntityService.SaveLocalizedValue(productCopy, x => x.FullDescription, fullDescription, lang.Id); - - var metaKeywords = product.GetLocalized(x => x.MetaKeywords, lang.Id, false, false); - if (!String.IsNullOrEmpty(metaKeywords)) - _localizedEntityService.SaveLocalizedValue(productCopy, x => x.MetaKeywords, metaKeywords, lang.Id); - - var metaDescription = product.GetLocalized(x => x.MetaDescription, lang.Id, false, false); - if (!String.IsNullOrEmpty(metaDescription)) - _localizedEntityService.SaveLocalizedValue(productCopy, x => x.MetaDescription, metaDescription, lang.Id); - - var metaTitle = product.GetLocalized(x => x.MetaTitle, lang.Id, false, false); - if (!String.IsNullOrEmpty(metaTitle)) - _localizedEntityService.SaveLocalizedValue(productCopy, x => x.MetaTitle, metaTitle, lang.Id); - - var bundleTitleText = product.GetLocalized(x => x.BundleTitleText, lang.Id, false, false); - if (!String.IsNullOrEmpty(bundleTitleText)) - _localizedEntityService.SaveLocalizedValue(productCopy, x => x.BundleTitleText, bundleTitleText, lang.Id); - - //search engine name - _urlRecordService.SaveSlug(productCopy, productCopy.ValidateSeName("", name, false), lang.Id); - - } - - // product pictures - var newPictureIds = new Dictionary(); - - if (copyImages) - { - foreach (var productPicture in product.ProductPictures) - { - var picture = productPicture.Picture; - var pictureCopy = _pictureService.InsertPicture( - _pictureService.LoadPictureBinary(picture), - picture.MimeType, - _pictureService.GetPictureSeName(newName), - true, - false, - false); - - _productService.InsertProductPicture(new ProductPicture - { - ProductId = productCopy.Id, - PictureId = pictureCopy.Id, - DisplayOrder = productPicture.DisplayOrder - }); - - newPictureIds.Add(productPicture.PictureId, pictureCopy.Id.ToString()); - } - } - - // product <-> categories mappings - foreach (var productCategory in product.ProductCategories) - { - var productCategoryCopy = new ProductCategory - { - ProductId = productCopy.Id, - CategoryId = productCategory.CategoryId, - IsFeaturedProduct = productCategory.IsFeaturedProduct, - DisplayOrder = productCategory.DisplayOrder - }; - - _categoryService.InsertProductCategory(productCategoryCopy); - } - - // product <-> manufacturers mappings - foreach (var productManufacturers in product.ProductManufacturers) - { - var productManufacturerCopy = new ProductManufacturer - { - ProductId = productCopy.Id, - ManufacturerId = productManufacturers.ManufacturerId, - IsFeaturedProduct = productManufacturers.IsFeaturedProduct, - DisplayOrder = productManufacturers.DisplayOrder - }; - - _manufacturerService.InsertProductManufacturer(productManufacturerCopy); - } - - // product <-> releated products mappings - foreach (var relatedProduct in _productService.GetRelatedProductsByProductId1(product.Id, true)) - { - _productService.InsertRelatedProduct(new RelatedProduct - { - ProductId1 = productCopy.Id, - ProductId2 = relatedProduct.ProductId2, - DisplayOrder = relatedProduct.DisplayOrder - }); - } - - // product <-> cross sells mappings - foreach (var csProduct in _productService.GetCrossSellProductsByProductId1(product.Id, true)) - { - _productService.InsertCrossSellProduct(new CrossSellProduct - { - ProductId1 = productCopy.Id, - ProductId2 = csProduct.ProductId2, - }); - } - - // product specifications - foreach (var productSpecificationAttribute in product.ProductSpecificationAttributes) - { - var psaCopy = new ProductSpecificationAttribute - { - ProductId = productCopy.Id, - SpecificationAttributeOptionId = productSpecificationAttribute.SpecificationAttributeOptionId, - AllowFiltering = productSpecificationAttribute.AllowFiltering, - ShowOnProductPage = productSpecificationAttribute.ShowOnProductPage, - DisplayOrder = productSpecificationAttribute.DisplayOrder - }; - - _specificationAttributeService.InsertProductSpecificationAttribute(psaCopy); - } - - //store mapping - var selectedStoreIds = _storeMappingService.GetStoresIdsWithAccess(product); - foreach (var id in selectedStoreIds) + private void ProcessBundleItems(Product product, Product clone) + { + var bundledItems = _productService.GetBundleItems(product.Id, true); + + foreach (var bundleItem in bundledItems) { - _storeMappingService.InsertStoreMapping(productCopy, id); + var newBundleItem = bundleItem.Item.Clone(); + newBundleItem.BundleProductId = clone.Id; + + _productService.InsertBundleItem(newBundleItem); + + foreach (var itemFilter in bundleItem.Item.AttributeFilters) + { + var newItemFilter = itemFilter.Clone(); + newItemFilter.BundleItemId = newBundleItem.Id; + + _productAttributeService.InsertProductBundleItemAttributeFilter(newItemFilter); + } } + } - // product <-> attributes mappings - var associatedAttributes = new Dictionary(); - var associatedAttributeValues = new Dictionary(); + private void ProcessAttributes( + Product product, + Product clone, + string newName, + bool copyImages, + Dictionary clonedPictures, + IEnumerable languages) + { + // Former attribute id > clone + var pvaMap = new Dictionary(); - foreach (var productVariantAttribute in _productAttributeService.GetProductVariantAttributesByProductId(product.Id)) + // Former attribute value id > clone + var pvavMap = new Dictionary(); + + var pictureSeName = _pictureService.GetPictureSeName(newName); + + foreach (var pva in product.ProductVariantAttributes) { - var productVariantAttributeCopy = new ProductVariantAttribute + var pvaClone = new ProductVariantAttribute { - ProductId = productCopy.Id, - ProductAttributeId = productVariantAttribute.ProductAttributeId, - TextPrompt = productVariantAttribute.TextPrompt, - IsRequired = productVariantAttribute.IsRequired, - AttributeControlTypeId = productVariantAttribute.AttributeControlTypeId, - DisplayOrder = productVariantAttribute.DisplayOrder + ProductAttributeId = pva.ProductAttributeId, + TextPrompt = pva.TextPrompt, + IsRequired = pva.IsRequired, + AttributeControlTypeId = pva.AttributeControlTypeId, + DisplayOrder = pva.DisplayOrder }; - _productAttributeService.InsertProductVariantAttribute(productVariantAttributeCopy); - //save associated value (used for combinations copying) - associatedAttributes.Add(productVariantAttribute.Id, productVariantAttributeCopy.Id); + clone.ProductVariantAttributes.Add(pvaClone); - // product variant attribute values - var productVariantAttributeValues = _productAttributeService.GetProductVariantAttributeValues(productVariantAttribute.Id); + // Save associated value (used for combinations copying) + pvaMap[pva.Id] = pvaClone; - foreach (var productVariantAttributeValue in productVariantAttributeValues) + // Product variant attribute values + foreach (var pvav in pva.ProductVariantAttributeValues) { + var pvavClone = new ProductVariantAttributeValue + { + Name = pvav.Name, + Color = pvav.Color, + PriceAdjustment = pvav.PriceAdjustment, + WeightAdjustment = pvav.WeightAdjustment, + IsPreSelected = pvav.IsPreSelected, + DisplayOrder = pvav.DisplayOrder, + ValueTypeId = pvav.ValueTypeId, + LinkedProductId = pvav.LinkedProductId, + Quantity = pvav.Quantity, + PictureId = copyImages ? pvav.PictureId : 0 // we'll clone this later + }; - var newPictureId = 0; + pvaClone.ProductVariantAttributeValues.Add(pvavClone); - if (copyImages) - { - var picture = _pictureService.GetPictureById(productVariantAttributeValue.PictureId); - if (picture != null) - { - var pictureCopy = _pictureService.InsertPicture( - _pictureService.LoadPictureBinary(picture), - picture.MimeType, - _pictureService.GetPictureSeName(newName), - true, false, false); + // Save associated value (used for combinations copying) + pvavMap.Add(pvav.Id, pvavClone); + } + } - newPictureId = pictureCopy.Id; - } - } + // >>>>>> Commit + Commit(); - var pvavCopy = new ProductVariantAttributeValue + // Attribute value localization + foreach (var pvav in product.ProductVariantAttributes.SelectMany(x => x.ProductVariantAttributeValues).ToArray()) + { + foreach (var lang in languages) + { + var name = pvav.GetLocalized(x => x.Name, lang.Id, false, false); + if (!String.IsNullOrEmpty(name)) { - ProductVariantAttributeId = productVariantAttributeCopy.Id, - Name = productVariantAttributeValue.Name, - Color = productVariantAttributeValue.Color, - PriceAdjustment = productVariantAttributeValue.PriceAdjustment, - WeightAdjustment = productVariantAttributeValue.WeightAdjustment, - IsPreSelected = productVariantAttributeValue.IsPreSelected, - DisplayOrder = productVariantAttributeValue.DisplayOrder, - ValueTypeId = productVariantAttributeValue.ValueTypeId, - LinkedProductId = productVariantAttributeValue.LinkedProductId, - Quantity = productVariantAttributeValue.Quantity, - PictureId = newPictureId - }; - - _productAttributeService.InsertProductVariantAttributeValue(pvavCopy); - - //save associated value (used for combinations copying) - associatedAttributeValues.Add(productVariantAttributeValue.Id, pvavCopy.Id); - - //localization - foreach (var lang in languages) + var pvavClone = pvavMap.Get(pvav.Id); + if (pvavClone != null) + { + _localizedEntityService.SaveLocalizedValue(pvavClone, x => x.Name, name, lang.Id); + } + } + } + } + + // Clone attribute value images + if (copyImages) + { + // Reduce value set to those with assigned pictures + var allValueClonesWithPictures = pvavMap.Values.Where(x => x.PictureId > 0).ToArray(); + // Get those pictures for cloning + var allPictures = _pictureService.GetPicturesByIds(allValueClonesWithPictures.Select(x => x.PictureId).ToArray(), true); + + foreach (var pvavClone in allValueClonesWithPictures) + { + var picture = allPictures.FirstOrDefault(x => x.Id == pvavClone.PictureId); + if (picture != null) { - var name = productVariantAttributeValue.GetLocalized(x => x.Name, lang.Id, false, false); - if (!String.IsNullOrEmpty(name)) - _localizedEntityService.SaveLocalizedValue(pvavCopy, x => x.Name, name, lang.Id); + var pictureClone = CopyPicture(picture, pictureSeName); + clonedPictures[pvavClone.PictureId] = pictureClone; + pvavClone.PictureId = pictureClone.Id; } } } + // >>>>>> Commit attributes & values + Commit(); + // attribute combinations using (var scope = new DbContextScope(lazyLoading: false, forceNoTracking: false)) { @@ -468,61 +610,58 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli foreach (var combination in product.ProductVariantAttributeCombinations) { - //generate new AttributesXml according to new value IDs + // Generate new AttributesXml according to new value IDs string newAttributesXml = ""; var parsedProductVariantAttributes = _productAttributeParser.ParseProductVariantAttributes(combination.AttributesXml); foreach (var oldPva in parsedProductVariantAttributes) { - if (associatedAttributes.ContainsKey(oldPva.Id)) + if (!pvaMap.ContainsKey(oldPva.Id)) + continue; + + var newPva = pvaMap.Get(oldPva.Id); + + if (newPva == null) + continue; + + var oldPvaValuesStr = _productAttributeParser.ParseValues(combination.AttributesXml, oldPva.Id); + foreach (var oldPvaValueStr in oldPvaValuesStr) { - int newPvaId = associatedAttributes[oldPva.Id]; - var newPva = _productAttributeService.GetProductVariantAttributeById(newPvaId); - if (newPva != null) + if (newPva.ShouldHaveValues()) { - var oldPvaValuesStr = _productAttributeParser.ParseValues(combination.AttributesXml, oldPva.Id); - foreach (var oldPvaValueStr in oldPvaValuesStr) + // attribute values + int oldPvaValue = oldPvaValueStr.Convert(); + if (pvavMap.ContainsKey(oldPvaValue)) { - if (newPva.ShouldHaveValues()) + var newPvav = pvavMap.Get(oldPvaValue); + if (newPvav != null) { - //attribute values - int oldPvaValue = int.Parse(oldPvaValueStr); - if (associatedAttributeValues.ContainsKey(oldPvaValue)) - { - int newPvavId = associatedAttributeValues[oldPvaValue]; - var newPvav = _productAttributeService.GetProductVariantAttributeValueById(newPvavId); - if (newPvav != null) - { - newAttributesXml = _productAttributeParser.AddProductAttribute(newAttributesXml, newPva, newPvav.Id.ToString()); - } - } - } - else - { - //just a text - newAttributesXml = _productAttributeParser.AddProductAttribute(newAttributesXml, newPva, oldPvaValueStr); + newAttributesXml = _productAttributeParser.AddProductAttribute(newAttributesXml, newPva, newPvav.Id.ToString()); } } } + else + { + // just a text + newAttributesXml = _productAttributeParser.AddProductAttribute(newAttributesXml, newPva, oldPvaValueStr); + } } } - var newAssignedPictureIds = new List(); + var newAssignedPictureIds = new HashSet(); + foreach (var strPicId in combination.AssignedPictureIds.EmptyNull().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) + { + var newPic = clonedPictures.Get(strPicId.Convert()); + if (newPic != null) + { + newAssignedPictureIds.Add(newPic.Id.ToString(CultureInfo.InvariantCulture)); + } + } - if(!String.IsNullOrEmpty(combination.AssignedPictureIds)) - { - combination.AssignedPictureIds.Split(',').Each(x => { - newAssignedPictureIds.Add(newPictureIds[Convert.ToInt32(x)]); - }); - } - - var combinationCopy = new ProductVariantAttributeCombination + var combinationClone = new ProductVariantAttributeCombination { - ProductId = productCopy.Id, AttributesXml = newAttributesXml, StockQuantity = combination.StockQuantity, AllowOutOfStockOrders = combination.AllowOutOfStockOrders, - - // SmartStore extension Sku = combination.Sku, Gtin = combination.Gtin, ManufacturerPartNumber = combination.ManufacturerPartNumber, @@ -538,77 +677,12 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli IsActive = combination.IsActive //IsDefaultCombination = combination.IsDefaultCombination }; - _productAttributeService.InsertProductVariantAttributeCombination(combinationCopy); - } - - // tier prices - foreach (var tierPrice in product.TierPrices) - { - _productService.InsertTierPrice( - new TierPrice() - { - ProductId = productCopy.Id, - StoreId = tierPrice.StoreId, - CustomerRoleId = tierPrice.CustomerRoleId, - Quantity = tierPrice.Quantity, - Price = tierPrice.Price, - CalculationMethod = tierPrice.CalculationMethod - }); - } - - // product <-> discounts mapping - foreach (var discount in product.AppliedDiscounts) - { - productCopy.AppliedDiscounts.Add(discount); - _productService.UpdateProduct(productCopy); - } - - // update "HasTierPrices" and "HasDiscountsApplied" properties - _productService.UpdateHasTierPricesProperty(productCopy); - _productService.UpdateLowestAttributeCombinationPriceProperty(productCopy); - _productService.UpdateHasDiscountsApplied(productCopy); - - // associated products - if (copyAssociatedProducts && product.ProductType != ProductType.BundledProduct) - { - var copyOf = _localizationService.GetResource("Admin.Common.CopyOf"); - var searchQuery = new CatalogSearchQuery() - .HasParentGroupedProduct(product.Id); - - var query = _catalogSearchService.PrepareQuery(searchQuery); - var associatedProducts = query.OrderBy(p => p.DisplayOrder).ToList(); - foreach (var associatedProduct in associatedProducts) - { - var associatedProductCopy = CopyProduct(associatedProduct, $"{copyOf} {associatedProduct.Name}", isPublished, copyImages, false); - associatedProductCopy.ParentGroupedProductId = productCopy.Id; - - _productService.UpdateProduct(productCopy); - } - } - - // bundled products - var bundledItems = _productService.GetBundleItems(product.Id, true); - - foreach (var bundleItem in bundledItems) - { - var newBundleItem = bundleItem.Item.Clone(); - newBundleItem.BundleProductId = productCopy.Id; - - _productService.InsertBundleItem(newBundleItem); - - foreach (var itemFilter in bundleItem.Item.AttributeFilters) - { - var newItemFilter = itemFilter.Clone(); - newItemFilter.BundleItemId = newBundleItem.Id; - - _productAttributeService.InsertProductBundleItemAttributeFilter(newItemFilter); - } + clone.ProductVariantAttributeCombinations.Add(combinationClone); } - return productCopy; - } - - #endregion - } + // >>>>>> Commit combinations + Commit(); + } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/Extensions/ProductUrlHelper.cs b/src/Libraries/SmartStore.Services/Catalog/Extensions/ProductUrlHelper.cs index f8f22b8725..ce2b90d9d2 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Extensions/ProductUrlHelper.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Extensions/ProductUrlHelper.cs @@ -1,11 +1,9 @@ using System; +using System.Collections.Generic; using System.Globalization; -using System.Linq; -using System.Text; using System.Web; using System.Web.Mvc; using System.Web.Routing; -using SmartStore; using SmartStore.Collections; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Localization; @@ -17,289 +15,278 @@ namespace SmartStore.Services.Catalog.Extensions { public class ProductUrlHelper - { - private readonly HttpRequestBase _httpRequest; - private readonly ICommonServices _services; - private readonly IProductAttributeParser _productAttributeParser; - private readonly IProductAttributeService _productAttributeService; - private readonly Lazy _languageService; - private readonly Lazy _catalogSearchQueryAliasMapper; - private readonly Lazy _localizationSettings; - - private readonly int _languageId; - - public ProductUrlHelper( - HttpRequestBase httpRequest, - ICommonServices services, - IProductAttributeParser productAttributeParser, - IProductAttributeService productAttributeService, - Lazy languageService, - Lazy catalogSearchQueryAliasMapper, - Lazy localizationSettings) - { - _httpRequest = httpRequest; - _services = services; - _productAttributeParser = productAttributeParser; - _productAttributeService = productAttributeService; - _languageService = languageService; - _catalogSearchQueryAliasMapper = catalogSearchQueryAliasMapper; - _localizationSettings = localizationSettings; - - _languageId = _services.WorkContext.WorkingLanguage.Id; - } - - /// - /// URL of the product page used to create the new product URL. Created from route if null. - /// - public string Url { get; set; } - - /// - /// Initial query string used to create the new query string. Usually null. - /// - public QueryString InitialQuery { get; set; } - - protected virtual string ToQueryString(ProductVariantQuery query) - { - var qs = InitialQuery != null - ? new QueryString(InitialQuery) - : new QueryString(); - - // Checkout Attributes - foreach (var item in query.CheckoutAttributes) - { - var name = item.ToString(); - - if (item.Date.HasValue) - { - qs.Add(name + "-date", string.Join("-", item.Date.Value.Year, item.Date.Value.Month, item.Date.Value.Day)); - } - else - { - qs.Add(name, item.Value); - } - } - - // Gift cards - foreach (var item in query.GiftCards) - { - qs.Add(item.ToString(), item.Value); - } - - // Variants - foreach (var item in query.Variants) - { - if (item.Alias.IsEmpty()) - { - item.Alias = _catalogSearchQueryAliasMapper.Value.GetVariantAliasById(item.AttributeId, _languageId); - } - - var name = item.Alias.HasValue() - ? $"{item.Alias}-{item.ProductId}-{item.BundleItemId}-{item.VariantAttributeId}" - : item.ToString(); - - if (item.Date.HasValue) - { - qs.Add(name + "-date", string.Join("-", item.Date.Value.Year, item.Date.Value.Month, item.Date.Value.Day)); - } - else if (item.IsFile) - { - qs.Add(name + "-file", item.Value); - } - else if (item.IsText) - { - qs.Add(name + "-text", item.Value); - } - else - { - if (item.ValueAlias.IsEmpty()) - { - item.ValueAlias = _catalogSearchQueryAliasMapper.Value.GetVariantOptionAliasById(item.Value.ToInt(), _languageId); - } - - var value = item.ValueAlias.HasValue() - ? $"{item.ValueAlias}-{item.Value}" - : item.Value; - - qs.Add(name, value); - } - } - - return qs.ToString(false); - } - - /// - /// Deserializes attributes XML into a product variant query - /// - /// Product variant query - /// Product identifier - /// Bundle item identifier - /// XML formatted attributes - public virtual void DeserializeQuery(ProductVariantQuery query, int productId, string attributesXml, int bundleItemId = 0) - { - Guard.NotNull(query, nameof(query)); - Guard.NotZero(productId, nameof(productId)); - - if (attributesXml.IsEmpty() || productId == 0) - return; - - ProductVariantAttributeValue attributeValue = null; - var attributeMap = _productAttributeParser.DeserializeProductVariantAttributes(attributesXml); - var attributes = _productAttributeService.GetProductVariantAttributesByIds(attributeMap.Keys); - var attributeValues = _productAttributeParser.ParseProductVariantAttributeValues(attributesXml).ToDictionarySafe(x => x.Id); - - foreach (var attribute in attributes) - { - var values = attributeMap[attribute.Id]; - - foreach (var value in values) - { - string newValue = null; - string valueAlias = null; - DateTime? date = null; - - switch (attribute.AttributeControlType) - { - case AttributeControlType.Datepicker: - date = value.ToDateTime(new string[] { "D" }, CultureInfo.CurrentCulture, DateTimeStyles.None, null); - if (date != null) - { - newValue = string.Join("-", date.Value.Year, date.Value.Month, date.Value.Day); - } - break; - case AttributeControlType.FileUpload: - case AttributeControlType.TextBox: - case AttributeControlType.MultilineTextbox: - newValue = value; - break; - default: - newValue = value; - if (attributeValues.TryGetValue(value.ToInt(), out attributeValue)) - { - valueAlias = attributeValue.Alias; - } - break; - } - - if (newValue.HasValue()) - { - query.AddVariant(new ProductVariantQueryItem(newValue) - { - ProductId = productId, - BundleItemId = bundleItemId, - AttributeId = attribute.ProductAttributeId, - VariantAttributeId = attribute.Id, - Alias = attribute.ProductAttribute.Alias, - ValueAlias = valueAlias, - Date = date, - IsFile = attribute.AttributeControlType == AttributeControlType.FileUpload, - IsText = attribute.AttributeControlType == AttributeControlType.TextBox || attribute.AttributeControlType == AttributeControlType.MultilineTextbox - }); - } - } - } - } - - /// - /// Creates a product URL including variant query string. - /// - /// Product variant query - /// Product SEO name - /// Product URL - public virtual string GetProductUrl(ProductVariantQuery query, string productSeName) - { - if (productSeName.IsEmpty()) - return null; - - var url = Url ?? UrlHelper.GenerateUrl( - "Product", - null, - null, - new RouteValueDictionary(new { SeName = productSeName }), - RouteTable.Routes, - _httpRequest.RequestContext, - false); - - return url + ToQueryString(query); - } - - /// - /// Creates a product URL including variant query string. - /// - /// Product identifier - /// Product SEO name - /// XML formatted attributes - /// Product URL - public virtual string GetProductUrl(int productId, string productSeName, string attributesXml) - { - var query = new ProductVariantQuery(); - DeserializeQuery(query, productId, attributesXml); - - return GetProductUrl(query, productSeName); - } - - /// - /// Creates an absolute product URL. - /// - /// Product identifier - /// Product SEO name - /// XML formatted attributes - /// Store entity - /// Language entity - /// Absolute product URL - public virtual string GetAbsoluteProductUrl( - int productId, - string productSeName, - string attributesXml, - Store store = null, - Language language = null) - { - if (_httpRequest == null || productSeName.IsEmpty()) - return null; - - var url = Url; - - if (url.IsEmpty()) - { - // No given URL. Create SEO friendly URL. - var urlHelper = new LocalizedUrlHelper(_httpRequest.ApplicationPath, productSeName, false); - - store = store ?? _services.StoreContext.CurrentStore; - language = language ?? _services.WorkContext.WorkingLanguage; - - if (_localizationSettings.Value.SeoFriendlyUrlsForLanguagesEnabled) - { - var defaultSeoCode = _languageService.Value.GetDefaultLanguageSeoCode(store.Id); - - if (language.UniqueSeoCode == defaultSeoCode && _localizationSettings.Value.DefaultLanguageRedirectBehaviour > 0) - { - urlHelper.StripSeoCode(); - } - else - { - urlHelper.PrependSeoCode(language.UniqueSeoCode, true); - } - } - - var storeUrl = store.Url.TrimEnd('/'); - - // Prevent duplicate occurrence of application path. - if (urlHelper.ApplicationPath.HasValue() && storeUrl.EndsWith(urlHelper.ApplicationPath, StringComparison.OrdinalIgnoreCase)) - { - storeUrl = storeUrl.Substring(0, storeUrl.Length - urlHelper.ApplicationPath.Length).TrimEnd('/'); - } - - url = storeUrl + urlHelper.GetAbsolutePath(); - } - - if (attributesXml.HasValue()) - { - var query = new ProductVariantQuery(); - DeserializeQuery(query, productId, attributesXml); - - url = url + ToQueryString(query); - } - - return url; - } - } + { + private readonly HttpRequestBase _httpRequest; + private readonly ICommonServices _services; + private readonly IProductAttributeParser _productAttributeParser; + private readonly IProductAttributeService _productAttributeService; + private readonly Lazy _languageService; + private readonly Lazy _catalogSearchQueryAliasMapper; + private readonly Lazy _localizationSettings; + + private readonly int _languageId; + + public ProductUrlHelper( + HttpRequestBase httpRequest, + ICommonServices services, + IProductAttributeParser productAttributeParser, + IProductAttributeService productAttributeService, + Lazy languageService, + Lazy catalogSearchQueryAliasMapper, + Lazy localizationSettings) + { + _httpRequest = httpRequest; + _services = services; + _productAttributeParser = productAttributeParser; + _productAttributeService = productAttributeService; + _languageService = languageService; + _catalogSearchQueryAliasMapper = catalogSearchQueryAliasMapper; + _localizationSettings = localizationSettings; + + _languageId = _services.WorkContext.WorkingLanguage.Id; + } + + /// + /// URL of the product page used to create the new product URL. Created from route if null. + /// + public string Url { get; set; } + + /// + /// Initial query string used to create the new query string. Usually null. + /// + public QueryString InitialQuery { get; set; } + + /// + /// Converts a query object into a URL query string + /// + /// Product variant query + /// URL query string + public virtual string ToQueryString(ProductVariantQuery query) + { + var qs = InitialQuery != null + ? new QueryString(InitialQuery) + : new QueryString(); + + // Checkout Attributes + foreach (var item in query.CheckoutAttributes) + { + if (item.Date.HasValue) + { + qs.Add(item.ToString(), string.Join("-", item.Date.Value.Year, item.Date.Value.Month, item.Date.Value.Day)); + } + else + { + qs.Add(item.ToString(), item.Value); + } + } + + // Gift cards + foreach (var item in query.GiftCards) + { + qs.Add(item.ToString(), item.Value); + } + + // Variants + foreach (var item in query.Variants) + { + if (item.Alias.IsEmpty()) + { + item.Alias = _catalogSearchQueryAliasMapper.Value.GetVariantAliasById(item.AttributeId, _languageId); + } + + if (item.Date.HasValue) + { + qs.Add(item.ToString(), string.Join("-", item.Date.Value.Year, item.Date.Value.Month, item.Date.Value.Day)); + } + else if (item.IsFile || item.IsText) + { + qs.Add(item.ToString(), item.Value); + } + else + { + if (item.ValueAlias.IsEmpty()) + { + item.ValueAlias = _catalogSearchQueryAliasMapper.Value.GetVariantOptionAliasById(item.Value.ToInt(), _languageId); + } + + var value = item.ValueAlias.HasValue() + ? $"{item.ValueAlias}-{item.Value}" + : item.Value; + + qs.Add(item.ToString(), value); + } + } + + return qs.ToString(false); + } + + /// + /// Deserializes attributes XML into a product variant query + /// + /// Product variant query + /// Product identifier + /// XML formatted attributes + /// Bundle item identifier + /// Product variant attributes + public virtual void DeserializeQuery( + ProductVariantQuery query, + int productId, + string attributesXml, + int bundleItemId = 0, + ICollection attributes = null) + { + Guard.NotNull(query, nameof(query)); + + if (productId == 0 || attributesXml.IsEmpty()) + return; + + var attributeMap = _productAttributeParser.DeserializeProductVariantAttributes(attributesXml); + + if (attributes == null) + { + attributes = _productAttributeService.GetProductVariantAttributesByIds(attributeMap.Keys); + } + + foreach (var attribute in attributes) + { + if (attributeMap.ContainsKey(attribute.Id)) + { + foreach (var originalValue in attributeMap[attribute.Id]) + { + var value = originalValue; + DateTime? date = null; + + if (attribute.AttributeControlType == AttributeControlType.Datepicker) + { + date = originalValue.ToDateTime(new string[] { "D" }, CultureInfo.CurrentCulture, DateTimeStyles.None, null); + if (date == null) + continue; + + value = string.Join("-", date.Value.Year, date.Value.Month, date.Value.Day); + } + + var queryItem = new ProductVariantQueryItem(value); + queryItem.ProductId = productId; + queryItem.BundleItemId = bundleItemId; + queryItem.AttributeId = attribute.ProductAttributeId; + queryItem.VariantAttributeId = attribute.Id; + queryItem.Alias = _catalogSearchQueryAliasMapper.Value.GetVariantAliasById(attribute.ProductAttributeId, _languageId); + queryItem.Date = date; + queryItem.IsFile = attribute.AttributeControlType == AttributeControlType.FileUpload; + queryItem.IsText = attribute.AttributeControlType == AttributeControlType.TextBox || attribute.AttributeControlType == AttributeControlType.MultilineTextbox; + + if (attribute.ShouldHaveValues()) + { + queryItem.ValueAlias = _catalogSearchQueryAliasMapper.Value.GetVariantOptionAliasById(value.ToInt(), _languageId); + } + + query.AddVariant(queryItem); + } + } + } + } + + /// + /// Creates a product URL including variant query string. + /// + /// Product variant query + /// Product SEO name + /// Product URL + public virtual string GetProductUrl(ProductVariantQuery query, string productSeName) + { + if (productSeName.IsEmpty()) + return null; + + var url = Url ?? UrlHelper.GenerateUrl( + "Product", + null, + null, + new RouteValueDictionary(new { SeName = productSeName }), + RouteTable.Routes, + _httpRequest.RequestContext, + false); + + return url + ToQueryString(query); + } + + /// + /// Creates a product URL including variant query string. + /// + /// Product identifier + /// Product SEO name + /// XML formatted attributes + /// Product URL + public virtual string GetProductUrl(int productId, string productSeName, string attributesXml) + { + var query = new ProductVariantQuery(); + DeserializeQuery(query, productId, attributesXml); + + return GetProductUrl(query, productSeName); + } + + /// + /// Creates an absolute product URL. + /// + /// Product identifier + /// Product SEO name + /// XML formatted attributes + /// Store entity + /// Language entity + /// Absolute product URL + public virtual string GetAbsoluteProductUrl( + int productId, + string productSeName, + string attributesXml, + Store store = null, + Language language = null) + { + if (_httpRequest == null || productSeName.IsEmpty()) + return null; + + var url = Url; + + if (url.IsEmpty()) + { + // No given URL. Create SEO friendly URL. + var urlHelper = new LocalizedUrlHelper(_httpRequest.ApplicationPath, productSeName, false); + + store = store ?? _services.StoreContext.CurrentStore; + language = language ?? _services.WorkContext.WorkingLanguage; + + if (_localizationSettings.Value.SeoFriendlyUrlsForLanguagesEnabled) + { + var defaultSeoCode = _languageService.Value.GetDefaultLanguageSeoCode(store.Id); + + if (language.UniqueSeoCode == defaultSeoCode && _localizationSettings.Value.DefaultLanguageRedirectBehaviour > 0) + { + urlHelper.StripSeoCode(); + } + else + { + urlHelper.PrependSeoCode(language.UniqueSeoCode, true); + } + } + + var storeUrl = store.Url.TrimEnd('/'); + + // Prevent duplicate occurrence of application path. + if (urlHelper.ApplicationPath.HasValue() && storeUrl.EndsWith(urlHelper.ApplicationPath, StringComparison.OrdinalIgnoreCase)) + { + storeUrl = storeUrl.Substring(0, storeUrl.Length - urlHelper.ApplicationPath.Length).TrimEnd('/'); + } + + url = storeUrl + urlHelper.GetAbsolutePath(); + } + + if (attributesXml.HasValue()) + { + var query = new ProductVariantQuery(); + DeserializeQuery(query, productId, attributesXml); + + url = url + ToQueryString(query); + } + + return url; + } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/ICategoryService.cs b/src/Libraries/SmartStore.Services/Catalog/ICategoryService.cs index cc249f73de..711d3053b3 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ICategoryService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ICategoryService.cs @@ -4,6 +4,7 @@ using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Infrastructure; namespace SmartStore.Services.Catalog { @@ -12,7 +13,6 @@ namespace SmartStore.Services.Catalog /// public partial interface ICategoryService { - /// /// Assign acl to sub-categories and products /// @@ -45,29 +45,27 @@ void InheritStoresIntoChildren(int categoryId, void DeleteCategory(Category category, bool deleteChilds = false); /// - /// Gets categories + /// Builds LINQ query for categories /// - /// Category name + /// Category name filter /// A value indicating whether to show hidden records - /// Alias to be filtered - /// (Obsolete) Whether to apply instances to the actual categories query. Never applied when is true + /// Alias filter /// Store identifier; 0 to load all records /// Category query - IQueryable GetCategories( + IQueryable BuildCategoriesQuery( string categoryName = "", bool showHidden = false, string alias = null, - bool applyNavigationFilters = true, int storeId = 0); /// /// Gets all categories /// - /// Category name + /// Category name filter /// Page index /// Page size /// A value indicating whether to show hidden records - /// Alias to be filtered + /// Alias filter /// (Obsolete) Whether to apply instances to the actual categories query. Never applied when is true /// A value indicating whether categories without parent category in provided category list (source) should be ignored /// Store identifier; 0 to load all records @@ -78,7 +76,6 @@ IPagedList GetAllCategories( int pageSize = int.MaxValue, bool showHidden = false, string alias = null, - bool applyNavigationFilters = true, bool ignoreCategoriesWithoutExistingParent = true, int storeId = 0); @@ -135,8 +132,11 @@ IPagedList GetAllCategories( /// Page size /// A value indicating whether to show hidden records /// Product a category mapping collection - IPagedList GetProductCategoriesByCategoryId(int categoryId, - int pageIndex, int pageSize, bool showHidden = false); + IPagedList GetProductCategoriesByCategoryId( + int categoryId, + int pageIndex, + int pageSize, + bool showHidden = false); /// /// Gets a product category mapping collection @@ -184,23 +184,43 @@ IPagedList GetProductCategoriesByCategoryId(int categoryId, /// /// Gets the category trail /// - /// Category + /// The category node /// Trail - ICollection GetCategoryTrail(Category category); + IEnumerable GetCategoryTrail(ICategoryNode node); + /// - /// Builds a category breadcrumb (path) for a particular product + /// Builds a category breadcrumb (path) for a particular category node /// - /// The product - /// The id of language - /// A delegate for fast (cached) path lookup - /// A callback that saves the resolved path to a cache (when pathLookup returned null) - /// A delegate for fast (cached) category lookup - /// First product category of product - /// Category breadcrumb for product - string GetCategoryPath(Product product, int? languageId, Func pathLookup, Action addPathToCache, Func categoryLookup, - ProductCategory prodCategory = null); - } + /// The category node + /// The id of language. Pass null to skip localization. + /// true appends the category alias - if specified - to the name + /// The separator string + /// Category breadcrumb path + string GetCategoryPath( + TreeNode treeNode, + int? languageId = null, + bool withAlias = false, + string separator = " � "); + + /// + /// Gets the tree representation of categories + /// + /// Specifies which node to return as root + /// false excludes unpublished and ACL-inaccessible categories + /// > 0 = apply store mapping, 0 to bypass store mapping + /// The category tree representation + /// + /// This method puts the tree result into application cache, so subsequent calls are very fast. + /// Localization is up to the caller because the nodes only contain unlocalized data. + /// Subscribe to the CategoryTreeChanged event if you need to evict cache entries which depend + /// on this method's result. + /// + TreeNode GetCategoryTree( + int rootCategoryId = 0, + bool includeHidden = false, + int storeId = 0); + } public static class ICategoryServiceExtensions { @@ -208,10 +228,31 @@ public static class ICategoryServiceExtensions /// Builds a category breadcrumb for a particular product /// /// The product + /// The id of language. Pass null to skip localization. + /// The id of store. Pass null to skip store filtering. + /// The separator string /// Category breadcrumb for product - public static string GetCategoryBreadCrumb(this ICategoryService categoryService, Product product) + public static string GetCategoryPath(this ICategoryService categoryService, + Product product, + int? languageId = null, + int? storeId = null, + string separator = " � ") { - return categoryService.GetCategoryPath(product, null, null, null, null); + Guard.NotNull(product, nameof(product)); + + string result = string.Empty; + + var pc = categoryService.GetProductCategoriesByProductId(product.Id).FirstOrDefault(); + if (pc != null) + { + var node = categoryService.GetCategoryTree(pc.CategoryId, false, storeId ?? 0); + if (node != null) + { + result = categoryService.GetCategoryPath(node, languageId, false, separator); + } + } + + return result; } } } diff --git a/src/Libraries/SmartStore.Services/Catalog/ICopyProductService.cs b/src/Libraries/SmartStore.Services/Catalog/ICopyProductService.cs index 1fffa09df2..287d69836b 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ICopyProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ICopyProductService.cs @@ -9,14 +9,19 @@ namespace SmartStore.Services.Catalog public partial interface ICopyProductService { /// - /// Create a copy of product with all depended data + /// Creates a copy of a product with all it's dependencies /// /// The product to copy - /// The name of product duplicate + /// The name of the product duplicate /// A value indicating whether the product duplicate should be published /// A value indicating whether the product images should be copied /// A value indicating whether the copy associated products /// Product copy - Product CopyProduct(Product product, string newName, bool isPublished, bool copyImages, bool copyAssociatedProducts = true); + Product CopyProduct( + Product product, + string newName, + bool isPublished, + bool copyImages, + bool copyAssociatedProducts = true); } } diff --git a/src/Libraries/SmartStore.Services/Catalog/IPriceCalculationService.cs b/src/Libraries/SmartStore.Services/Catalog/IPriceCalculationService.cs index 52fd6568d0..c0a78cad3c 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IPriceCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IPriceCalculationService.cs @@ -3,6 +3,7 @@ using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Discounts; using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Directory; namespace SmartStore.Services.Catalog { @@ -125,9 +126,10 @@ decimal GetFinalPrice(Product product, /// /// Product /// The customer + /// The currency /// Object with cargo data for better performance /// Preselected price - decimal GetPreselectedPrice(Product product, Customer customer, PriceCalculationContext context); + decimal GetPreselectedPrice(Product product, Customer customer, Currency currency, PriceCalculationContext context); /// /// Gets the product cost diff --git a/src/Libraries/SmartStore.Services/Catalog/IProductAttributeService.cs b/src/Libraries/SmartStore.Services/Catalog/IProductAttributeService.cs index 03ccc886e2..a69241071d 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IProductAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IProductAttributeService.cs @@ -43,6 +43,13 @@ public partial interface IProductAttributeService /// Product attribute void UpdateProductAttribute(ProductAttribute productAttribute); + /// + /// Gets the export mappings for a given field prefix. + /// + /// The export field prefix, e.g. gmc. + /// A multimap with export field names to ProductAttribute.Id mappings. + Multimap GetExportFieldMappings(string fieldPrefix); + #endregion #region Product attribute options diff --git a/src/Libraries/SmartStore.Services/Catalog/IProductService.cs b/src/Libraries/SmartStore.Services/Catalog/IProductService.cs index 5c928da2bd..739b683e7b 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IProductService.cs @@ -8,10 +8,10 @@ namespace SmartStore.Services.Catalog { - /// - /// Product service - /// - public partial interface IProductService + /// + /// Product service + /// + public partial interface IProductService { #region Products @@ -52,7 +52,7 @@ public partial interface IProductService /// Updates the product /// /// Product - void UpdateProduct(Product product, bool publishEvent = true); + void UpdateProduct(Product product); /// /// Update product review totals @@ -153,13 +153,6 @@ public partial interface IProductService /// Map of applied discounts Multimap GetAppliedDiscountsByProductIds(int[] productIds); - /// - /// Get product specification attributes by product identifiers - /// - /// Product identifiers - /// Map of product specification attributes - Multimap GetProductSpecificationAttributesByProductIds(int[] productIds); - #endregion #region Related products diff --git a/src/Libraries/SmartStore.Services/Catalog/ISpecificationAttributeService.cs b/src/Libraries/SmartStore.Services/Catalog/ISpecificationAttributeService.cs index 06dd270095..d4546d153d 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ISpecificationAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ISpecificationAttributeService.cs @@ -1,13 +1,14 @@ using System.Collections.Generic; using System.Linq; +using SmartStore.Collections; using SmartStore.Core.Domain.Catalog; namespace SmartStore.Services.Catalog { - /// - /// Specification attribute service interface - /// - public partial interface ISpecificationAttributeService + /// + /// Specification attribute service interface + /// + public partial interface ISpecificationAttributeService { #region Specification attribute @@ -67,6 +68,13 @@ public partial interface ISpecificationAttributeService /// Specification attribute option IList GetSpecificationAttributeOptionsBySpecificationAttribute(int specificationAttributeId); + /// + /// Gets specification attribute options by specification attribute id + /// + /// Specification attribute identifiers + /// Map of specification attribute options + Multimap GetSpecificationAttributeOptionsBySpecificationAttributeIds(int[] specificationAttributeIds); + /// /// Deletes a specification attribute option /// @@ -119,6 +127,13 @@ IList GetProductSpecificationAttributesByProductI /// Product specification attribute mapping ProductSpecificationAttribute GetProductSpecificationAttributeById(int productSpecificationAttributeId); + /// + /// Get product specification attributes by product identifiers + /// + /// Product identifiers + /// Map of product specification attributes + Multimap GetProductSpecificationAttributesByProductIds(int[] productIds); + /// /// Inserts a product specification attribute mapping /// diff --git a/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs b/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs index 49318ce8b2..504c4d91a2 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs @@ -62,7 +62,7 @@ protected override void Import(ImportExecuteContext context) var templateViewPaths = _categoryTemplateService.GetAllCategoryTemplates().ToDictionarySafe(x => x.ViewPath, x => x.Id); - using (var scope = new DbContextScope(ctx: context.Services.DbContext, autoDetectChanges: false, proxyCreation: false, validateOnSave: false)) + using (var scope = new DbContextScope(ctx: context.Services.DbContext, hooksEnabled: false, autoDetectChanges: false, proxyCreation: false, validateOnSave: false)) { var segmenter = context.DataSegmenter; @@ -223,7 +223,7 @@ protected virtual int ProcessPictures( } var size = Size.Empty; - pictureBinary = _pictureService.ValidatePicture(pictureBinary, out size); + pictureBinary = _pictureService.ValidatePicture(pictureBinary, image.MimeType, out size); pictureBinary = _pictureService.FindEqualPicture(pictureBinary, currentPictures, out equalPictureId); if (pictureBinary != null && pictureBinary.Length > 0) @@ -339,8 +339,6 @@ protected virtual int ProcessCategories( { _categoryRepository.AutoCommitEnabled = true; - Category lastInserted = null; - Category lastUpdated = null; var defaultTemplateId = templateViewPaths["CategoryTemplate.ProductsInGridOrLines"]; foreach (var row in batch) @@ -428,12 +426,10 @@ protected virtual int ProcessCategories( if (row.IsTransient) { _categoryRepository.Insert(category); - lastInserted = category; } else { _categoryRepository.Update(category); - lastUpdated = category; } } @@ -449,17 +445,6 @@ protected virtual int ProcessCategories( srcToDestId[id].DestinationId = row.Entity.Id; } - // Perf: notify only about LAST insertion and update - if (lastInserted != null) - { - _services.EventPublisher.EntityInserted(lastInserted); - } - - if (lastUpdated != null) - { - _services.EventPublisher.EntityUpdated(lastUpdated); - } - return num; } diff --git a/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs b/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs index 6937d95c55..68846923bf 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs @@ -15,6 +15,7 @@ using SmartStore.Core.Domain.Seo; using SmartStore.Core.Domain.Stores; using SmartStore.Core.Events; +using SmartStore.Data.Utilities; using SmartStore.Services.DataExchange.Import; using SmartStore.Services.Localization; using SmartStore.Services.Media; @@ -79,10 +80,10 @@ public ProductImporter( protected override void Import(ImportExecuteContext context) { var srcToDestId = new Dictionary(); - + var importStartTime = DateTime.UtcNow; var templateViewPaths = _productTemplateService.GetAllProductTemplates().ToDictionarySafe(x => x.ViewPath, x => x.Id); - using (var scope = new DbContextScope(ctx: _productRepository.Context, autoDetectChanges: false, proxyCreation: false, validateOnSave: false)) + using (var scope = new DbContextScope(ctx: _productRepository.Context, hooksEnabled: false, autoDetectChanges: false, proxyCreation: false, validateOnSave: false)) { var segmenter = context.DataSegmenter; @@ -242,6 +243,11 @@ x is ProductBundleItem || x is ProductCategory || x is ProductManufacturer || x } } } + + // =========================================================================== + // 9.) PostProcess: normalization + // =========================================================================== + DataMigrator.FixProductMainPictureIds(_productRepository.Context, importStartTime); } } @@ -253,8 +259,6 @@ protected virtual int ProcessProducts( { _productRepository.AutoCommitEnabled = false; - Product lastInserted = null; - Product lastUpdated = null; var defaultTemplateId = templateViewPaths["Product"]; foreach (var row in batch) @@ -434,17 +438,14 @@ protected virtual int ProcessProducts( if (row.IsTransient) { _productRepository.Insert(product); - lastInserted = product; } else { //_productRepository.Update(product); // unnecessary: we use DetectChanges() - lastUpdated = product; } } // commit whole batch at once - DetectChanges(); var num = _productRepository.Context.SaveChanges(); // get new product ids @@ -456,17 +457,6 @@ protected virtual int ProcessProducts( srcToDestId[id].DestinationId = row.Entity.Id; } - // Perf: notify only about LAST insertion and update - if (lastInserted != null) - { - _services.EventPublisher.EntityInserted(lastInserted); - } - - if (lastUpdated != null) - { - _services.EventPublisher.EntityUpdated(lastUpdated); - } - return num; } @@ -497,7 +487,6 @@ protected virtual int ProcessProductMappings( } } - DetectChanges(); var num = _productRepository.Context.SaveChanges(); return num; @@ -508,7 +497,6 @@ protected virtual void ProcessProductPictures(ImportExecuteContext context, IEnu // true, cause pictures must be saved and assigned an id prior adding a mapping. _productPictureRepository.AutoCommitEnabled = true; - ProductPicture lastInserted = null; var equalPictureId = 0; var numberOfPictures = (context.ExtraData.NumberOfPictures ?? int.MaxValue); @@ -572,7 +560,7 @@ protected virtual void ProcessProductPictures(ImportExecuteContext context, IEnu } var size = Size.Empty; - pictureBinary = _pictureService.ValidatePicture(pictureBinary, out size); + pictureBinary = _pictureService.ValidatePicture(pictureBinary, image.MimeType, out size); pictureBinary = _pictureService.FindEqualPicture(pictureBinary, currentPictures, out equalPictureId); if (pictureBinary != null && pictureBinary.Length > 0) @@ -589,7 +577,6 @@ protected virtual void ProcessProductPictures(ImportExecuteContext context, IEnu }; _productPictureRepository.Insert(mapping); - lastInserted = mapping; } } else @@ -609,20 +596,12 @@ protected virtual void ProcessProductPictures(ImportExecuteContext context, IEnu } } } - - // Perf: notify only about LAST insertion and update - if (lastInserted != null) - { - _services.EventPublisher.EntityInserted(lastInserted); - } } protected virtual int ProcessProductManufacturers(ImportExecuteContext context, IEnumerable> batch) { _productManufacturerRepository.AutoCommitEnabled = false; - ProductManufacturer lastInserted = null; - foreach (var row in batch) { var manufacturerIds = row.GetDataValue>("ManufacturerIds"); @@ -646,7 +625,6 @@ protected virtual int ProcessProductManufacturers(ImportExecuteContext context, DisplayOrder = 1 }; _productManufacturerRepository.Insert(productManufacturer); - lastInserted = productManufacturer; } } } @@ -661,10 +639,6 @@ protected virtual int ProcessProductManufacturers(ImportExecuteContext context, // commit whole batch at once var num = _productManufacturerRepository.Context.SaveChanges(); - // Perf: notify only about LAST insertion and update - if (lastInserted != null) - _services.EventPublisher.EntityInserted(lastInserted); - return num; } @@ -672,8 +646,6 @@ protected virtual int ProcessProductCategories(ImportExecuteContext context, IEn { _productCategoryRepository.AutoCommitEnabled = false; - ProductCategory lastInserted = null; - foreach (var row in batch) { var categoryIds = row.GetDataValue>("CategoryIds"); @@ -697,7 +669,6 @@ protected virtual int ProcessProductCategories(ImportExecuteContext context, IEn DisplayOrder = 1 }; _productCategoryRepository.Insert(productCategory); - lastInserted = productCategory; } } } @@ -711,19 +682,9 @@ protected virtual int ProcessProductCategories(ImportExecuteContext context, IEn // commit whole batch at once var num = _productCategoryRepository.Context.SaveChanges(); - - // Perf: notify only about LAST insertion and update - if (lastInserted != null) - _services.EventPublisher.EntityInserted(lastInserted); - return num; } - private void DetectChanges() - { - ((DbContext)_productRepository.Context).ChangeTracker.DetectChanges(); - } - private int? ZeroToNull(object value, CultureInfo culture) { int result; diff --git a/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs b/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs index 56e8a6f39f..8363f604ce 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs @@ -16,8 +16,8 @@ public partial class ManufacturerService : IManufacturerService { private const string PRODUCTMANUFACTURERS_ALLBYMANUFACTURERID_KEY = "SmartStore.productmanufacturer.allbymanufacturerid-{0}-{1}-{2}-{3}-{4}"; private const string PRODUCTMANUFACTURERS_ALLBYPRODUCTID_KEY = "SmartStore.productmanufacturer.allbyproductid-{0}-{1}-{2}"; - private const string MANUFACTURERS_PATTERN_KEY = "SmartStore.manufacturer."; - private const string PRODUCTMANUFACTURERS_PATTERN_KEY = "SmartStore.productmanufacturer."; + private const string MANUFACTURERS_PATTERN_KEY = "SmartStore.manufacturer.*"; + private const string PRODUCTMANUFACTURERS_PATTERN_KEY = "SmartStore.productmanufacturer.*"; private readonly IRepository _manufacturerRepository; private readonly IRepository _productManufacturerRepository; @@ -130,9 +130,6 @@ public virtual void InsertManufacturer(Manufacturer manufacturer) //cache _requestCache.RemoveByPattern(MANUFACTURERS_PATTERN_KEY); _requestCache.RemoveByPattern(PRODUCTMANUFACTURERS_PATTERN_KEY); - - //event notification - _eventPublisher.EntityInserted(manufacturer); } public virtual void UpdateManufacturer(Manufacturer manufacturer) @@ -145,9 +142,6 @@ public virtual void UpdateManufacturer(Manufacturer manufacturer) //cache _requestCache.RemoveByPattern(MANUFACTURERS_PATTERN_KEY); _requestCache.RemoveByPattern(PRODUCTMANUFACTURERS_PATTERN_KEY); - - //event notification - _eventPublisher.EntityUpdated(manufacturer); } public virtual void UpdateHasDiscountsApplied(Manufacturer manufacturer) @@ -165,12 +159,8 @@ public virtual void DeleteProductManufacturer(ProductManufacturer productManufac _productManufacturerRepository.Delete(productManufacturer); - //cache _requestCache.RemoveByPattern(MANUFACTURERS_PATTERN_KEY); _requestCache.RemoveByPattern(PRODUCTMANUFACTURERS_PATTERN_KEY); - - //event notification - _eventPublisher.EntityDeleted(productManufacturer); } public virtual IPagedList GetProductManufacturersByManufacturerId(int manufacturerId, int pageIndex, int pageSize, bool showHidden = false) @@ -313,12 +303,9 @@ public virtual void InsertProductManufacturer(ProductManufacturer productManufac _productManufacturerRepository.Insert(productManufacturer); - //cache _requestCache.RemoveByPattern(MANUFACTURERS_PATTERN_KEY); _requestCache.RemoveByPattern(PRODUCTMANUFACTURERS_PATTERN_KEY); - //event notification - _eventPublisher.EntityInserted(productManufacturer); } public virtual void UpdateProductManufacturer(ProductManufacturer productManufacturer) @@ -328,12 +315,8 @@ public virtual void UpdateProductManufacturer(ProductManufacturer productManufac _productManufacturerRepository.Update(productManufacturer); - //cache _requestCache.RemoveByPattern(MANUFACTURERS_PATTERN_KEY); _requestCache.RemoveByPattern(PRODUCTMANUFACTURERS_PATTERN_KEY); - - //event notification - _eventPublisher.EntityUpdated(productManufacturer); } } } diff --git a/src/Libraries/SmartStore.Services/Catalog/ManufacturerTemplateService.cs b/src/Libraries/SmartStore.Services/Catalog/ManufacturerTemplateService.cs index 70c1543557..eae06162de 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ManufacturerTemplateService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ManufacturerTemplateService.cs @@ -24,9 +24,6 @@ public virtual void DeleteManufacturerTemplate(ManufacturerTemplate manufacturer throw new ArgumentNullException("manufacturerTemplate"); _manufacturerTemplateRepository.Delete(manufacturerTemplate); - - //event notification - _eventPublisher.EntityDeleted(manufacturerTemplate); } public virtual IList GetAllManufacturerTemplates() @@ -53,9 +50,6 @@ public virtual void InsertManufacturerTemplate(ManufacturerTemplate manufacturer throw new ArgumentNullException("manufacturerTemplate"); _manufacturerTemplateRepository.Insert(manufacturerTemplate); - - //event notification - _eventPublisher.EntityInserted(manufacturerTemplate); } public virtual void UpdateManufacturerTemplate(ManufacturerTemplate manufacturerTemplate) @@ -65,8 +59,6 @@ public virtual void UpdateManufacturerTemplate(ManufacturerTemplate manufacturer _manufacturerTemplateRepository.Update(manufacturerTemplate); - //event notification - _eventPublisher.EntityUpdated(manufacturerTemplate); } } } diff --git a/src/Libraries/SmartStore.Services/Catalog/Modelling/CheckoutAttributeQueryItem.cs b/src/Libraries/SmartStore.Services/Catalog/Modelling/CheckoutAttributeQueryItem.cs index b5b6e04475..cc35919d93 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Modelling/CheckoutAttributeQueryItem.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Modelling/CheckoutAttributeQueryItem.cs @@ -10,6 +10,11 @@ public CheckoutAttributeQueryItem(int attributeId, string value) AttributeId = attributeId; } + /// + /// Key used for form names. + /// + /// Checkout attribute identifier + /// Key public static string CreateKey(int attributeId) { return $"cattr{attributeId}"; @@ -18,10 +23,27 @@ public static string CreateKey(int attributeId) public string Value { get; private set; } public int AttributeId { get; private set; } public DateTime? Date { get; set; } + public bool IsFile { get; set; } + public bool IsText { get; set; } public override string ToString() { - return CreateKey(AttributeId); + var key = CreateKey(AttributeId); + + if (Date.HasValue) + { + return key + "-date"; + } + else if (IsFile) + { + return key + "-file"; + } + else if (IsText) + { + return key + "-text"; + } + + return key; } } } diff --git a/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryFactory.cs b/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryFactory.cs index 8e8fb8a3cd..f115f77341 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryFactory.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryFactory.cs @@ -243,23 +243,38 @@ protected virtual void ConvertGiftCard(ProductVariantQuery query, string key, st } } - protected virtual void ConvertCheckoutAttribute(ProductVariantQuery query, string key, string value) + protected virtual void ConvertCheckoutAttribute(ProductVariantQuery query, string key, ICollection values) { - if (key.EndsWith("-day") || key.EndsWith("-month")) + var ids = key.Replace("cattr", "").SplitSafe("-"); + if (ids.Length <= 0) return; - var ids = key.Replace("cattr", "").SplitSafe("-"); - if (ids.Length > 0) + var attributeId = ids[0].ToInt(); + var isDate = key.EndsWith("-date") || key.EndsWith("-year"); + var isFile = key.EndsWith("-file"); + var isText = key.EndsWith("-text"); + + if (isDate || isFile || isText) { - var attribute = new CheckoutAttributeQueryItem(ids[0].ToInt(), value); + var value = isText ? string.Join(",", values) : values.First(); + var attribute = new CheckoutAttributeQueryItem(attributeId, value); + attribute.IsFile = isFile; + attribute.IsText = isText; - if (key.EndsWith("-date") || key.EndsWith("-year")) + if (isDate) { attribute.Date = ConvertToDate(key, value); } query.AddCheckoutAttribute(attribute); } + else + { + foreach (var value in values) + { + query.AddCheckoutAttribute(new CheckoutAttributeQueryItem(attributeId, value)); + } + } } protected virtual void ConvertItems(HttpRequestBase request, ProductVariantQuery query, string key, ICollection values) @@ -295,7 +310,7 @@ public ProductVariantQuery CreateFromQuery() } else if (IsCheckoutAttributeKey.IsMatch(item.Key)) { - item.Value.Each(value => ConvertCheckoutAttribute(query, item.Key, value)); + ConvertCheckoutAttribute(query, item.Key, item.Value); } else if (IsVariantAliasKey.IsMatch(item.Key)) { diff --git a/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryItem.cs b/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryItem.cs index 627f2381a7..a41f3ea64b 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryItem.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryItem.cs @@ -9,6 +9,14 @@ public ProductVariantQueryItem(string value) Value = value ?? string.Empty; } + /// + /// Key used for form names. + /// + /// Product identifier + /// Bundle item identifier. 0 if not a bundle item. + /// Product attribute identifier + /// Product variant attribute identifier + /// Key public static string CreateKey(int productId, int bundleItemId, int attributeId, int variantAttributeId) { return $"pvari{productId}-{bundleItemId}-{attributeId}-{variantAttributeId}"; @@ -29,7 +37,24 @@ public static string CreateKey(int productId, int bundleItemId, int attributeId, public override string ToString() { - return CreateKey(ProductId, BundleItemId, AttributeId, VariantAttributeId); + var key = Alias.HasValue() + ? $"{Alias}-{ProductId}-{BundleItemId}-{VariantAttributeId}" + : CreateKey(ProductId, BundleItemId, AttributeId, VariantAttributeId); + + if (Date.HasValue) + { + return key + "-date"; + } + else if (IsFile) + { + return key + "-file"; + } + else if (IsText) + { + return key + "-text"; + } + + return key; } } } diff --git a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs index ca5feca93b..91b71df230 100644 --- a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs @@ -11,6 +11,8 @@ using SmartStore.Services.Discounts; using SmartStore.Services.Media; using SmartStore.Services.Tax; +using SmartStore.Core.Domain.Directory; +using SmartStore.Core.Domain.Tax; namespace SmartStore.Services.Catalog { @@ -31,8 +33,9 @@ public partial class PriceCalculationService : IPriceCalculationService private readonly ICommonServices _services; private readonly HttpRequestBase _httpRequestBase; private readonly ITaxService _taxService; + private readonly TaxSettings _taxSettings; - public PriceCalculationService( + public PriceCalculationService( IDiscountService discountService, ICategoryService categoryService, IManufacturerService manufacturerService, @@ -44,20 +47,22 @@ public PriceCalculationService( IDownloadService downloadService, ICommonServices services, HttpRequestBase httpRequestBase, - ITaxService taxService) + ITaxService taxService, + TaxSettings taxSettings) { - this._discountService = discountService; - this._categoryService = categoryService; - this._manufacturerService = manufacturerService; - this._productAttributeParser = productAttributeParser; - this._productService = productService; - this._shoppingCartSettings = shoppingCartSettings; - this._catalogSettings = catalogSettings; - this._productAttributeService = productAttributeService; - this._downloadService = downloadService; - this._services = services; - this._httpRequestBase = httpRequestBase; - this._taxService = taxService; + _discountService = discountService; + _categoryService = categoryService; + _manufacturerService = manufacturerService; + _productAttributeParser = productAttributeParser; + _productService = productService; + _shoppingCartSettings = shoppingCartSettings; + _catalogSettings = catalogSettings; + _productAttributeService = productAttributeService; + _downloadService = downloadService; + _services = services; + _httpRequestBase = httpRequestBase; + _taxService = taxService; + _taxSettings = taxSettings; } #region Utilities @@ -289,6 +294,7 @@ protected virtual decimal GetTierPriceAttributeAdjustment(Product product, Custo protected virtual decimal GetPreselectedPrice( Product product, Customer customer, + Currency currency, PriceCalculationContext context, ProductBundleItemData bundleItem, IEnumerable bundleItems) @@ -325,8 +331,10 @@ protected virtual decimal GetPreselectedPrice( if (!isBundlePricing && pvaValue.IsPreSelected) { - decimal attributeValuePriceAdjustment = GetProductVariantAttributeValuePriceAdjustment(pvaValue, product, _services.WorkContext.CurrentCustomer, context, 1); - decimal priceAdjustmentBase = _taxService.GetProductPrice(product, attributeValuePriceAdjustment, out taxRate); + var includingTax = _services.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax; + var attributeValuePriceAdjustment = GetProductVariantAttributeValuePriceAdjustment(pvaValue, product, customer, context, 1); + var priceAdjustmentBase = _taxService.GetProductPrice(product, product.TaxCategoryId, attributeValuePriceAdjustment, + includingTax, customer, currency, _taxSettings.PricesIncludeTax, out taxRate); preSelectedPriceAdjustmentBase = decimal.Add(preSelectedPriceAdjustmentBase, priceAdjustmentBase); } @@ -391,7 +399,7 @@ protected virtual decimal GetPreselectedPrice( { if (selectedAttributeValues.Count > 0) { - selectedAttributeValues.Each(x => attributesTotalPriceBase += GetProductVariantAttributeValuePriceAdjustment(x, product, _services.WorkContext.CurrentCustomer, context, 1)); + selectedAttributeValues.Each(x => attributesTotalPriceBase += GetProductVariantAttributeValuePriceAdjustment(x, product, customer, context, 1)); } else { @@ -473,8 +481,7 @@ public virtual PriceCalculationContext CreatePriceCalculationContext( /// Product /// A value indicating whether include discounts or not for final price computation /// Final price - public virtual decimal GetFinalPrice(Product product, - bool includeDiscounts) + public virtual decimal GetFinalPrice(Product product, bool includeDiscounts) { var customer = _services.WorkContext.CurrentCustomer; return GetFinalPrice(product, customer, includeDiscounts); @@ -487,9 +494,7 @@ public virtual decimal GetFinalPrice(Product product, /// The customer /// A value indicating whether include discounts or not for final price computation /// Final price - public virtual decimal GetFinalPrice(Product product, - Customer customer, - bool includeDiscounts) + public virtual decimal GetFinalPrice(Product product, Customer customer, bool includeDiscounts) { return GetFinalPrice(product, customer, decimal.Zero, includeDiscounts); } @@ -518,8 +523,7 @@ public virtual decimal GetFinalPrice( int quantity, ProductBundleItemData bundleItem = null, PriceCalculationContext context = null, - bool isTierPrice = false - ) + bool isTierPrice = false) { //initial price decimal result = product.Price; @@ -694,7 +698,7 @@ public virtual decimal GetLowestPrice(Product product, Customer customer, PriceC return lowestPrice; } - public virtual decimal GetPreselectedPrice(Product product, Customer customer, PriceCalculationContext context) + public virtual decimal GetPreselectedPrice(Product product, Customer customer, Currency currency, PriceCalculationContext context) { if (product == null) throw new ArgumentNullException("product"); @@ -716,14 +720,14 @@ public virtual decimal GetPreselectedPrice(Product product, Customer customer, P foreach (var bundleItem in bundleItems.Where(x => x.Item.Product.CanBeBundleItem())) { // fetch bundleItems.AdditionalCharge for all bundle items - var unused = GetPreselectedPrice(bundleItem.Item.Product, customer, context, bundleItem, bundleItems); + var unused = GetPreselectedPrice(bundleItem.Item.Product, customer, currency, context, bundleItem, bundleItems); } - result = GetPreselectedPrice(product, customer, context, null, bundleItems); + result = GetPreselectedPrice(product, customer, currency, context, null, bundleItems); } else { - result = GetPreselectedPrice(product, customer, context, null, null); + result = GetPreselectedPrice(product, customer, currency, context, null, null); } return result; @@ -915,7 +919,6 @@ public virtual decimal GetUnitPrice(OrganizedShoppingCartItem shoppingCartItem, product.MergeWithCombination(shoppingCartItem.Item.AttributesXml, _productAttributeParser); var attributesTotalPrice = decimal.Zero; - var pvaValuesEnum = _productAttributeParser.ParseProductVariantAttributeValues(shoppingCartItem.Item.AttributesXml); if (pvaValuesEnum != null) @@ -924,7 +927,7 @@ public virtual decimal GetUnitPrice(OrganizedShoppingCartItem shoppingCartItem, foreach (var pvaValue in pvaValues) { - attributesTotalPrice += GetProductVariantAttributeValuePriceAdjustment(pvaValue, product, _services.WorkContext.CurrentCustomer, null, shoppingCartItem.Item.Quantity); + attributesTotalPrice += GetProductVariantAttributeValuePriceAdjustment(pvaValue, product, customer, null, shoppingCartItem.Item.Quantity); } } @@ -932,9 +935,7 @@ public virtual decimal GetUnitPrice(OrganizedShoppingCartItem shoppingCartItem, } } - if (_shoppingCartSettings.RoundPricesDuringCalculation) - finalPrice = Math.Round(finalPrice, 2); - + finalPrice = finalPrice.RoundIfEnabledFor(_services.WorkContext.WorkingCurrency); return finalPrice; } @@ -959,26 +960,28 @@ public virtual decimal GetDiscountAmount(OrganizedShoppingCartItem shoppingCartI /// Discount amount public virtual decimal GetDiscountAmount(OrganizedShoppingCartItem shoppingCartItem, out Discount appliedDiscount) { - var customer = shoppingCartItem.Item.Customer; - appliedDiscount = null; - decimal totalDiscountAmount = decimal.Zero; + appliedDiscount = null; + + var customer = shoppingCartItem.Item.Customer; + var totalDiscountAmount = decimal.Zero; var product = shoppingCartItem.Item.Product; + var quantity = shoppingCartItem.Item.Quantity; + if (product != null) { - decimal attributesTotalPrice = decimal.Zero; - + var attributesTotalPrice = decimal.Zero; var pvaValues = _productAttributeParser.ParseProductVariantAttributeValues(shoppingCartItem.Item.AttributesXml).ToList(); + foreach (var pvaValue in pvaValues) { - attributesTotalPrice += GetProductVariantAttributeValuePriceAdjustment(pvaValue, product, _services.WorkContext.CurrentCustomer, null, shoppingCartItem.Item.Quantity); + attributesTotalPrice += GetProductVariantAttributeValuePriceAdjustment(pvaValue, product, customer, null, quantity); } - decimal productDiscountAmount = GetDiscountAmount(product, customer, attributesTotalPrice, shoppingCartItem.Item.Quantity, out appliedDiscount); - totalDiscountAmount = productDiscountAmount * shoppingCartItem.Item.Quantity; + var productDiscountAmount = GetDiscountAmount(product, customer, attributesTotalPrice, quantity, out appliedDiscount); + totalDiscountAmount = productDiscountAmount * quantity; } - - if (_shoppingCartSettings.RoundPricesDuringCalculation) - totalDiscountAmount = Math.Round(totalDiscountAmount, 2); + + totalDiscountAmount = totalDiscountAmount.RoundIfEnabledFor(_services.WorkContext.WorkingCurrency); return totalDiscountAmount; } diff --git a/src/Libraries/SmartStore.Services/Catalog/PriceFormatter.cs b/src/Libraries/SmartStore.Services/Catalog/PriceFormatter.cs index b53310d921..f2780df836 100644 --- a/src/Libraries/SmartStore.Services/Catalog/PriceFormatter.cs +++ b/src/Libraries/SmartStore.Services/Catalog/PriceFormatter.cs @@ -166,30 +166,36 @@ public string FormatPrice(decimal price, bool showCurrency, Currency targetCurre public string FormatPrice(decimal price, bool showCurrency, Currency targetCurrency, Language language, bool priceIncludesTax, bool showTax) { - // Round before rendering (also take "BitCoin" into account, where more than 2 decimal places are relevant) - price = targetCurrency.CurrencyCode.IsCaseInsensitiveEqual("btc") ? Math.Round(price, 6) : Math.Round(price, 2); + // Round before rendering (also take "BitCoin" into account, where more than 2 decimal places are relevant) + price = targetCurrency.CurrencyCode.IsCaseInsensitiveEqual("btc") ? Math.Round(price, 6) : Math.Round(price, 2); - string currencyString = GetCurrencyString(price, showCurrency, targetCurrency); - if (showTax) - { - //show tax suffix - string formatStr; - if (priceIncludesTax) - { - formatStr = _localizationService.GetResource("Products.InclTaxSuffix", language.Id, false); - if (String.IsNullOrEmpty(formatStr)) - formatStr = "{0} incl tax"; - } - else - { - formatStr = _localizationService.GetResource("Products.ExclTaxSuffix", language.Id, false); - if (String.IsNullOrEmpty(formatStr)) - formatStr = "{0} excl tax"; - } - return string.Format(formatStr, currencyString); - } - else - return currencyString; + var currencyString = GetCurrencyString(price, showCurrency, targetCurrency); + if (showTax) + { + // Show tax suffix + string formatStr; + if (priceIncludesTax) + { + formatStr = _localizationService.GetResource("Products.InclTaxSuffix", language.Id, false); + if (string.IsNullOrEmpty(formatStr)) + { + formatStr = "{0} incl tax"; + } + } + else + { + formatStr = _localizationService.GetResource("Products.ExclTaxSuffix", language.Id, false); + if (string.IsNullOrEmpty(formatStr)) + { + formatStr = "{0} excl tax"; + } + } + return string.Format(formatStr, currencyString); + } + else + { + return currencyString; + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeExtensions.cs b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeExtensions.cs index 25653e352d..7ce8def364 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeExtensions.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeExtensions.cs @@ -1,10 +1,7 @@ using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Xml; using SmartStore.Core.Domain.Catalog; -using SmartStore.Services.Localization; namespace SmartStore.Services.Catalog { @@ -26,7 +23,7 @@ public static bool ShouldHaveValues(this ProductVariantAttribute productVariantA productVariantAttribute.AttributeControlType == AttributeControlType.FileUpload) return false; - // all other attribute control types support values + // All other attribute control types support values. return true; } @@ -90,39 +87,5 @@ public static string AddProductAttribute(this ProductVariantAttribute pva, strin } return result; } - - /// - /// Searches the alias and returns values for fragments that begins with fieldPrefix - /// - /// Product variant attribute values - /// Field prefix - /// Language identifier - /// Localized value names mapped by field names - public static Dictionary GetMappedValuesFromAlias(this IList attributeValues, string fieldPrefix, int languageId) - { - Guard.NotNull(attributeValues, nameof(attributeValues)); - Guard.NotEmpty(fieldPrefix, nameof(fieldPrefix)); - - var result = new Dictionary(StringComparer.OrdinalIgnoreCase); - - if (!fieldPrefix.EndsWith(":")) - fieldPrefix = fieldPrefix + ":"; - - // TODO: do not use value alias, create a new attribute export field - - //foreach (var value in attributeValues.Where(x => x.Alias.HasValue())) - //{ - // foreach (var item in value.Alias.SplitSafe(null).Where(x => x.EmptyNull().StartsWith(fieldPrefix))) - // { - // var fieldName = item.Substring(fieldPrefix.Length); - // if (fieldName.HasValue() && !result.ContainsKey(fieldName)) - // { - // result.Add(fieldName, value.GetLocalized(x => x.Name, languageId, true, false)); - // } - // } - //} - - return result; - } } } diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeFormatter.cs b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeFormatter.cs index a888d282c3..435a49d283 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeFormatter.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeFormatter.cs @@ -13,10 +13,10 @@ namespace SmartStore.Services.Catalog { - /// - /// Product attribute formatter - /// - public partial class ProductAttributeFormatter : IProductAttributeFormatter + /// + /// Product attribute formatter + /// + public partial class ProductAttributeFormatter : IProductAttributeFormatter { private readonly IWorkContext _workContext; private readonly IProductAttributeService _productAttributeService; @@ -29,8 +29,9 @@ public partial class ProductAttributeFormatter : IProductAttributeFormatter private readonly IDownloadService _downloadService; private readonly IWebHelper _webHelper; private readonly ShoppingCartSettings _shoppingCartSettings; + private readonly CatalogSettings _catalogSettings; - public ProductAttributeFormatter(IWorkContext workContext, + public ProductAttributeFormatter(IWorkContext workContext, IProductAttributeService productAttributeService, IProductAttributeParser productAttributeParser, IPriceCalculationService priceCalculationService, @@ -40,19 +41,21 @@ public ProductAttributeFormatter(IWorkContext workContext, IPriceFormatter priceFormatter, IDownloadService downloadService, IWebHelper webHelper, - ShoppingCartSettings shoppingCartSettings) + ShoppingCartSettings shoppingCartSettings, + CatalogSettings catalogSettings) { - this._workContext = workContext; - this._productAttributeService = productAttributeService; - this._productAttributeParser = productAttributeParser; - this._priceCalculationService = priceCalculationService; - this._currencyService = currencyService; - this._localizationService = localizationService; - this._taxService = taxService; - this._priceFormatter = priceFormatter; - this._downloadService = downloadService; - this._webHelper = webHelper; - this._shoppingCartSettings = shoppingCartSettings; + _workContext = workContext; + _productAttributeService = productAttributeService; + _productAttributeParser = productAttributeParser; + _priceCalculationService = priceCalculationService; + _currencyService = currencyService; + _localizationService = localizationService; + _taxService = taxService; + _priceFormatter = priceFormatter; + _downloadService = downloadService; + _webHelper = webHelper; + _shoppingCartSettings = shoppingCartSettings; + _catalogSettings = catalogSettings; } /// @@ -86,8 +89,9 @@ public string FormatAttributes(Product product, string attributes, bool allowHyperlinks = true) { var result = new StringBuilder(); + var languageId = _workContext.WorkingLanguage.Id; - //attributes + // Attributes if (renderProductAttributes) { var pvaCollection = _productAttributeParser.ParseProductVariantAttributes(attributes); @@ -105,7 +109,7 @@ public string FormatAttributes(Product product, string attributes, if (pva.AttributeControlType == AttributeControlType.MultilineTextbox) { //multiline textbox - var attributeName = pva.ProductAttribute.GetLocalized(a => a.Name, _workContext.WorkingLanguage.Id); + var attributeName = pva.ProductAttribute.GetLocalized(a => a.Name, languageId); //encode (if required) if (htmlEncode) attributeName = HttpUtility.HtmlEncode(attributeName); @@ -139,7 +143,7 @@ public string FormatAttributes(Product product, string attributes, //hyperlinks aren't allowed attributeText = fileName; } - var attributeName = pva.ProductAttribute.GetLocalized(a => a.Name, _workContext.WorkingLanguage.Id); + var attributeName = pva.ProductAttribute.GetLocalized(a => a.Name, languageId); //encode (if required) if (htmlEncode) attributeName = HttpUtility.HtmlEncode(attributeName); @@ -149,7 +153,7 @@ public string FormatAttributes(Product product, string attributes, else { //other attributes (textbox, datepicker) - pvaAttribute = string.Format("{0}: {1}", pva.ProductAttribute.GetLocalized(a => a.Name, _workContext.WorkingLanguage.Id), valueStr); + pvaAttribute = string.Format("{0}: {1}", pva.ProductAttribute.GetLocalized(a => a.Name, languageId), valueStr); //encode (if required) if (htmlEncode) pvaAttribute = HttpUtility.HtmlEncode(pvaAttribute); @@ -157,15 +161,16 @@ public string FormatAttributes(Product product, string attributes, } else { - //attributes with values + // Attributes with values. int pvaId = 0; if (int.TryParse(valueStr, out pvaId)) { var pvaValue = _productAttributeService.GetProductVariantAttributeValueById(pvaId); if (pvaValue != null) { - pvaAttribute = string.Format("{0}: {1}", pva.ProductAttribute.GetLocalized(a => a.Name, _workContext.WorkingLanguage.Id), - pvaValue.GetLocalized(a => a.Name, _workContext.WorkingLanguage.Id)); + pvaAttribute = "{0}: {1}".FormatInvariant( + pva.ProductAttribute.GetLocalized(a => a.Name, languageId), + pvaValue.GetLocalized(a => a.Name, languageId)); if (renderPrices) { @@ -180,21 +185,25 @@ public string FormatAttributes(Product product, string attributes, pvaAttribute += string.Format(" � {0}", pvaValue.Quantity); } - if (priceAdjustmentBase > 0) - { - string priceAdjustmentStr = _priceFormatter.FormatPrice(priceAdjustment, true, false); - pvaAttribute += string.Format(" [+{0}]", priceAdjustmentStr); - } - else if (priceAdjustmentBase < decimal.Zero) - { - string priceAdjustmentStr = _priceFormatter.FormatPrice(-priceAdjustment, true, false); - pvaAttribute += string.Format(" [-{0}]", priceAdjustmentStr); - } + if (_catalogSettings.ShowVariantCombinationPriceAdjustment) + { + if (priceAdjustmentBase > 0) + { + pvaAttribute += " (+{0})".FormatInvariant(_priceFormatter.FormatPrice(priceAdjustment, true, false)); + } + else if (priceAdjustmentBase < decimal.Zero) + { + pvaAttribute += " (-{0})".FormatInvariant(_priceFormatter.FormatPrice(-priceAdjustment, true, false)); + } + } } } - //encode (if required) - if (htmlEncode) - pvaAttribute = HttpUtility.HtmlEncode(pvaAttribute); + + // Encode (if required) + if (htmlEncode) + { + pvaAttribute = HttpUtility.HtmlEncode(pvaAttribute); + } } } diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs index d2a9fb8789..2f9c020ad8 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs @@ -56,20 +56,29 @@ public virtual void PrefetchProductVariantAttributes(IEnumerable attribu foreach (var xml in unfetched) { + var valueIds = new HashSet(); var map = DeserializeProductVariantAttributes(xml); + var attributes = _productAttributeService.GetProductVariantAttributesByIds(map.Keys); - // Get all value ids across all attributes - var valueIds = map.SelectMany(x => x.Value) - .Where(x => x.HasValue()) - .Select(x => x.ToInt()) - .Distinct() - .ToArray(); + foreach (var attribute in attributes) + { + // Only types that have attribute values! Otherwise entered text is misinterpreted as an attribute value id. + if (!attribute.ShouldHaveValues()) + continue; + + var ids = + from id in map[attribute.Id] + where id.HasValue() + select id.ToInt(); + + valueIds.UnionWith(ids); + } var info = new AttributeMapInfo { AttributesXml = xml, DeserializedMap = map, - AllValueIds = valueIds + AllValueIds = valueIds.ToArray() }; infos.Add(info); @@ -87,12 +96,11 @@ public virtual void PrefetchProductVariantAttributes(IEnumerable attribu foreach (var info in infos) { var cachedValues = new List(); - ProductVariantAttributeValue value; // Ensure value id order in cached result list is correct foreach (var id in info.AllValueIds) { - if (attributeValues.TryGetValue(id, out value)) + if (attributeValues.TryGetValue(id, out var value)) { cachedValues.Add(value); } @@ -164,8 +172,7 @@ public virtual Multimap DeserializeProductVariantAttributes(string string sid = node1.Attribute("ID").Value; if (sid.HasValue()) { - int id = 0; - if (int.TryParse(sid, out id)) + if (int.TryParse(sid, out var id)) { // ProductVariantAttributeValue/Value foreach (var node2 in node1.Descendants("Value")) @@ -234,8 +241,7 @@ public virtual IList ParseValues(string attributesXml, int productVarian if (node1.Attributes != null && node1.Attributes["ID"] != null) { string str1 = node1.Attributes["ID"].InnerText.Trim(); - int id = 0; - if (int.TryParse(str1, out id)) + if (int.TryParse(str1, out var id)) { if (id == productVariantAttributeId) { diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeService.cs b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeService.cs index bd959d4d3c..6150113b87 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeService.cs @@ -20,11 +20,11 @@ public partial class ProductAttributeService : IProductAttributeService { // 0 = ProductId, 1 = PageIndex, 2 = PageSize private const string PRODUCTVARIANTATTRIBUTES_COMBINATIONS_BY_ID_KEY = "SmartStore.productvariantattribute.combinations.id-{0}-{1}-{2}"; - private const string PRODUCTVARIANTATTRIBUTES_PATTERN_KEY = "SmartStore.productvariantattribute."; + private const string PRODUCTVARIANTATTRIBUTES_PATTERN_KEY = "SmartStore.productvariantattribute.*"; // 0 = Attribute value ids, e.g. 16-254-1245 private const string PRODUCTVARIANTATTRIBUTEVALUES_BY_IDS_KEY = "SmartStore.productvariantattributevalues.ids-{0}"; - private const string PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY = "SmartStore.productvariantattributevalues"; + private const string PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY = "SmartStore.productvariantattributevalues*"; private readonly IRepository _productAttributeRepository; private readonly IRepository _productAttributeOptionRepository; @@ -104,9 +104,6 @@ public virtual void DeleteProductAttribute(ProductAttribute productAttribute) //cache _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTES_PATTERN_KEY); _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); - - //event notification - _eventPublisher.EntityDeleted(productAttribute); } public virtual IList GetAllProductAttributes() @@ -135,9 +132,6 @@ public virtual void InsertProductAttribute(ProductAttribute productAttribute) _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTES_PATTERN_KEY); _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); - - //event notification - _eventPublisher.EntityInserted(productAttribute); } public virtual void UpdateProductAttribute(ProductAttribute productAttribute) @@ -149,11 +143,46 @@ public virtual void UpdateProductAttribute(ProductAttribute productAttribute) _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTES_PATTERN_KEY); _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); - - //event notification - _eventPublisher.EntityUpdated(productAttribute); } + public virtual Multimap GetExportFieldMappings(string fieldPrefix) + { + Guard.NotEmpty(fieldPrefix, nameof(fieldPrefix)); + + var result = new Multimap(StringComparer.OrdinalIgnoreCase); + + if (!fieldPrefix.EndsWith(":")) + { + fieldPrefix = fieldPrefix + ":"; + } + + var mappings = _productAttributeRepository.TableUntracked + .Where(x => !string.IsNullOrEmpty(x.ExportMappings)) + .Select(x => new + { + x.Id, + x.ExportMappings + }) + .ToList(); + + foreach (var mapping in mappings) + { + var rows = mapping.ExportMappings.SplitSafe(Environment.NewLine) + .Where(x => x.StartsWith(fieldPrefix, StringComparison.InvariantCultureIgnoreCase)); + + foreach (var row in rows) + { + var exportFieldName = row.Substring(fieldPrefix.Length).TrimEnd(); + if (exportFieldName.HasValue()) + { + result.Add(exportFieldName, mapping.Id); + } + } + } + + return result; + } + #endregion #region Product attribute options @@ -199,8 +228,6 @@ public virtual void DeleteProductAttributeOption(ProductAttributeOption productA Guard.NotNull(productAttributeOption, nameof(productAttributeOption)); _productAttributeOptionRepository.Delete(productAttributeOption); - - _eventPublisher.EntityDeleted(productAttributeOption); } public virtual void InsertProductAttributeOption(ProductAttributeOption productAttributeOption) @@ -208,8 +235,6 @@ public virtual void InsertProductAttributeOption(ProductAttributeOption productA Guard.NotNull(productAttributeOption, nameof(productAttributeOption)); _productAttributeOptionRepository.Insert(productAttributeOption); - - _eventPublisher.EntityInserted(productAttributeOption); } public virtual void UpdateProductAttributeOption(ProductAttributeOption productAttributeOption) @@ -217,8 +242,6 @@ public virtual void UpdateProductAttributeOption(ProductAttributeOption productA Guard.NotNull(productAttributeOption, nameof(productAttributeOption)); _productAttributeOptionRepository.Update(productAttributeOption); - - _eventPublisher.EntityUpdated(productAttributeOption); } #endregion @@ -251,8 +274,6 @@ public virtual void DeleteProductAttributeOptionsSet(ProductAttributeOptionsSet Guard.NotNull(productAttributeOptionsSet, nameof(productAttributeOptionsSet)); _productAttributeOptionsSetRepository.Delete(productAttributeOptionsSet); - - _eventPublisher.EntityDeleted(productAttributeOptionsSet); } public virtual void InsertProductAttributeOptionsSet(ProductAttributeOptionsSet productAttributeOptionsSet) @@ -260,8 +281,6 @@ public virtual void InsertProductAttributeOptionsSet(ProductAttributeOptionsSet Guard.NotNull(productAttributeOptionsSet, nameof(productAttributeOptionsSet)); _productAttributeOptionsSetRepository.Insert(productAttributeOptionsSet); - - _eventPublisher.EntityInserted(productAttributeOptionsSet); } public virtual void UpdateProductAttributeOptionsSet(ProductAttributeOptionsSet productAttributeOptionsSet) @@ -269,8 +288,6 @@ public virtual void UpdateProductAttributeOptionsSet(ProductAttributeOptionsSet Guard.NotNull(productAttributeOptionsSet, nameof(productAttributeOptionsSet)); _productAttributeOptionsSetRepository.Update(productAttributeOptionsSet); - - _eventPublisher.EntityUpdated(productAttributeOptionsSet); } #endregion @@ -286,9 +303,6 @@ public virtual void DeleteProductVariantAttribute(ProductVariantAttribute produc _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTES_PATTERN_KEY); _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); - - //event notification - _eventPublisher.EntityDeleted(productVariantAttribute); } public virtual IList GetProductVariantAttributesByProductId(int productId) @@ -405,9 +419,6 @@ public virtual void InsertProductVariantAttribute(ProductVariantAttribute produc _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTES_PATTERN_KEY); _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); - - //event notification - _eventPublisher.EntityInserted(productVariantAttribute); } public virtual void UpdateProductVariantAttribute(ProductVariantAttribute productVariantAttribute) @@ -419,9 +430,6 @@ public virtual void UpdateProductVariantAttribute(ProductVariantAttribute produc _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTES_PATTERN_KEY); _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); - - //event notification - _eventPublisher.EntityUpdated(productVariantAttribute); } public virtual int CopyAttributeOptions(ProductVariantAttribute productVariantAttribute, int productAttributeOptionsSetId, bool deleteExistingValues) @@ -508,11 +516,6 @@ public virtual int CopyAttributeOptions(ProductVariantAttribute productVariantAt } } - if (productVariantAttributeValue != null) - { - _eventPublisher.EntityInserted(productVariantAttributeValue); - } - return result; } @@ -529,9 +532,6 @@ public virtual void DeleteProductVariantAttributeValue(ProductVariantAttributeVa _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTES_PATTERN_KEY); _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); - - //event notification - _eventPublisher.EntityDeleted(productVariantAttributeValue); } public virtual IList GetProductVariantAttributeValues(int productVariantAttributeId) @@ -562,9 +562,6 @@ public virtual void InsertProductVariantAttributeValue(ProductVariantAttributeVa _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTES_PATTERN_KEY); _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); - - //event notification - _eventPublisher.EntityInserted(productVariantAttributeValue); } public virtual void UpdateProductVariantAttributeValue(ProductVariantAttributeValue productVariantAttributeValue) @@ -576,9 +573,6 @@ public virtual void UpdateProductVariantAttributeValue(ProductVariantAttributeVa _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTES_PATTERN_KEY); _requestCache.RemoveByPattern(PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY); - - //event notification - _eventPublisher.EntityUpdated(productVariantAttributeValue); } #endregion @@ -607,9 +601,6 @@ public virtual void DeleteProductVariantAttributeCombination(ProductVariantAttri throw new ArgumentNullException("combination"); _pvacRepository.Delete(combination); - - //event notification - _eventPublisher.EntityDeleted(combination); } public virtual IPagedList GetAllProductVariantAttributeCombinations( @@ -721,15 +712,8 @@ public virtual void InsertProductVariantAttributeCombination(ProductVariantAttri if (combination == null) throw new ArgumentNullException("combination"); - //if (combination.IsDefaultCombination) - //{ - // EnsureSingleDefaultVariant(combination); - //} - _pvacRepository.Insert(combination); - //event notification - _eventPublisher.EntityInserted(combination); } public virtual void UpdateProductVariantAttributeCombination(ProductVariantAttributeCombination combination) @@ -762,9 +746,6 @@ public virtual void UpdateProductVariantAttributeCombination(ProductVariantAttri //} _pvacRepository.Update(combination); - - //event notification - _eventPublisher.EntityUpdated(combination); } public virtual void CreateAllProductVariantAttributeCombinations(Product product) @@ -823,12 +804,6 @@ public virtual void CreateAllProductVariantAttributeCombinations(Product product } scope.Commit(); - - if (combination != null) - { - // Perf: publish event for last one only - _eventPublisher.EntityInserted(combination); - } } } @@ -868,8 +843,6 @@ public virtual void InsertProductBundleItemAttributeFilter(ProductBundleItemAttr if (attributeFilter.AttributeId != 0 && attributeFilter.AttributeValueId != 0) { _productBundleItemAttributeFilterRepository.Insert(attributeFilter); - - _eventPublisher.EntityInserted(attributeFilter); } } @@ -879,8 +852,6 @@ public virtual void UpdateProductBundleItemAttributeFilter(ProductBundleItemAttr throw new ArgumentNullException("attributeFilter"); _productBundleItemAttributeFilterRepository.Update(attributeFilter); - - _eventPublisher.EntityUpdated(attributeFilter); } public virtual void DeleteProductBundleItemAttributeFilter(ProductBundleItemAttributeFilter attributeFilter) @@ -889,8 +860,6 @@ public virtual void DeleteProductBundleItemAttributeFilter(ProductBundleItemAttr throw new ArgumentNullException("attributeFilter"); _productBundleItemAttributeFilterRepository.Delete(attributeFilter); - - _eventPublisher.EntityDeleted(attributeFilter); } public virtual void DeleteProductBundleItemAttributeFilter(ProductBundleItem bundleItem) diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs b/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs index c22098b65c..f84c7379c1 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs @@ -11,6 +11,7 @@ using SmartStore.Services.Media; using SmartStore.Services.Seo; using SmartStore.Services.Tax; +using SmartStore.Core.Domain.Customers; namespace SmartStore.Services.Catalog { @@ -148,21 +149,6 @@ public static CrossSellProduct FindCrossSellProduct(this IList return null; } - /// - /// Get a default picture of a product - /// - /// Source - /// Picture service - /// Product picture - public static Picture GetDefaultProductPicture(this Product source, IPictureService pictureService) - { - Guard.NotNull(source, nameof(source)); - Guard.NotNull(pictureService, nameof(pictureService)); - - var picture = pictureService.GetPicturesByProductId(source.Id, 1).FirstOrDefault(); - return picture; - } - public static bool IsAvailableByStock(this Product product) { Guard.NotNull(product, nameof(product)); @@ -183,13 +169,10 @@ public static bool IsAvailableByStock(this Product product) /// The stock message public static string FormatStockMessage(this Product product, ILocalizationService localizationService) { - if (product == null) - throw new ArgumentNullException("product"); - - if (localizationService == null) - throw new ArgumentNullException("localizationService"); + Guard.NotNull(product, nameof(product)); + Guard.NotNull(localizationService, nameof(localizationService)); - string stockMessage = string.Empty; + string stockMessage = string.Empty; if ((product.ManageInventoryMethod == ManageInventoryMethod.ManageStock || product.ManageInventoryMethod == ManageInventoryMethod.ManageStockByAttributes) && product.DisplayStockAvailability) @@ -246,11 +229,10 @@ public static bool IsNew(this Product product, CatalogSettings catalogSettings) public static bool ProductTagExists(this Product product, int productTagId) { - if (product == null) - throw new ArgumentNullException("product"); + Guard.NotNull(product, nameof(product)); - bool result = product.ProductTags.ToList().Find(pt => pt.Id == productTagId) != null; - return result; + var result = product.ProductTags.Any(x => x.Id == productTagId); + return result; } /// @@ -260,10 +242,9 @@ public static bool ProductTagExists(this Product product, int productTagId) /// Result public static int[] ParseAllowedQuatities(this Product product) { - if (product == null) - throw new ArgumentNullException("product"); + Guard.NotNull(product, nameof(product)); - var result = new List(); + var result = new List(); if (!String.IsNullOrWhiteSpace(product.AllowedQuantities)) { product @@ -285,8 +266,7 @@ public static int[] ParseAllowedQuatities(this Product product) public static int[] ParseRequiredProductIds(this Product product) { - if (product == null) - throw new ArgumentNullException("product"); + Guard.NotNull(product, nameof(product)); if (String.IsNullOrEmpty(product.RequiredProductIds)) return new int[0]; @@ -314,6 +294,7 @@ public static int[] ParseRequiredProductIds(this Product product) /// Currency service /// Tax service /// Price calculation service + /// Customer /// Target currency /// Price adjustment /// Whether the result string should be language insensitive @@ -324,6 +305,7 @@ public static string GetBasePriceInfo(this Product product, ICurrencyService currencyService, ITaxService taxService, IPriceCalculationService priceCalculationService, + Customer customer, Currency currency, decimal priceAdjustment = decimal.Zero, bool languageInsensitive = false) @@ -332,15 +314,14 @@ public static string GetBasePriceInfo(this Product product, Guard.NotNull(currencyService, nameof(currencyService)); Guard.NotNull(taxService, nameof(taxService)); Guard.NotNull(priceCalculationService, nameof(priceCalculationService)); + Guard.NotNull(customer, nameof(customer)); Guard.NotNull(currency, nameof(currency)); - if (product.BasePriceHasValue && product.BasePriceAmount != Decimal.Zero) + if (product.BasePriceHasValue && product.BasePriceAmount != decimal.Zero) { - var workContext = EngineContext.Current.Resolve(); - var taxrate = decimal.Zero; - var currentPrice = priceCalculationService.GetFinalPrice(product, workContext.CurrentCustomer, true); - var price = taxService.GetProductPrice(product, decimal.Add(currentPrice, priceAdjustment), out taxrate); + var currentPrice = priceCalculationService.GetFinalPrice(product, customer, true); + var price = taxService.GetProductPrice(product, decimal.Add(currentPrice, priceAdjustment), customer, currency, out taxrate); price = currencyService.ConvertFromPrimaryStoreCurrency(price, currency); @@ -372,7 +353,7 @@ public static string GetBasePriceInfo(this Product product, Guard.NotNull(priceFormatter, nameof(priceFormatter)); Guard.NotNull(currency, nameof(currency)); - if (product.BasePriceHasValue && product.BasePriceAmount != Decimal.Zero) + if (product.BasePriceHasValue && product.BasePriceAmount != decimal.Zero) { var value = Convert.ToDecimal((productPrice / product.BasePriceAmount) * product.BasePriceBaseAmount); var valueFormatted = priceFormatter.FormatPrice(value, true, currency); diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductService.cs b/src/Libraries/SmartStore.Services/Catalog/ProductService.cs index a631b3d592..81fc1ac527 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductService.cs @@ -17,20 +17,18 @@ namespace SmartStore.Services.Catalog { - public partial class ProductService : IProductService + public partial class ProductService : IProductService { private readonly IRepository _productRepository; private readonly IRepository _relatedProductRepository; private readonly IRepository _crossSellProductRepository; private readonly IRepository _tierPriceRepository; private readonly IRepository _productPictureRepository; - private readonly IRepository _productSpecificationAttributeRepository; private readonly IRepository _productVariantAttributeCombinationRepository; private readonly IRepository _productBundleItemRepository; private readonly IRepository _shoppingCartItemRepository; private readonly IProductAttributeService _productAttributeService; private readonly IProductAttributeParser _productAttributeParser; - private readonly IWorkflowMessageService _workflowMessageService; private readonly IDbContext _dbContext; private readonly LocalizationSettings _localizationSettings; private readonly ICommonServices _services; @@ -41,13 +39,11 @@ public ProductService( IRepository crossSellProductRepository, IRepository tierPriceRepository, IRepository productPictureRepository, - IRepository productSpecificationAttributeRepository, IRepository productVariantAttributeCombinationRepository, IRepository productBundleItemRepository, IRepository shoppingCartItemRepository, IProductAttributeService productAttributeService, IProductAttributeParser productAttributeParser, - IWorkflowMessageService workflowMessageService, IDbContext dbContext, LocalizationSettings localizationSettings, ICommonServices services) @@ -57,13 +53,11 @@ public ProductService( _crossSellProductRepository = crossSellProductRepository; _tierPriceRepository = tierPriceRepository; _productPictureRepository = productPictureRepository; - _productSpecificationAttributeRepository = productSpecificationAttributeRepository; _productVariantAttributeCombinationRepository = productVariantAttributeCombinationRepository; _productBundleItemRepository = productBundleItemRepository; _shoppingCartItemRepository = shoppingCartItemRepository; _productAttributeService = productAttributeService; _productAttributeParser = productAttributeParser; - _workflowMessageService = workflowMessageService; _dbContext = dbContext; _localizationSettings = localizationSettings; _services = services; @@ -152,10 +146,9 @@ join p in _productRepository.Table on rp.ProductId2 equals p.Id public virtual void DeleteProduct(Product product) { - if (product == null) - throw new ArgumentNullException("product"); + Guard.NotNull(product, nameof(product)); - product.Deleted = true; + product.Deleted = true; product.DeliveryTimeId = null; product.QuantityUnitId = null; product.CountryOfOriginId = null; @@ -286,43 +279,23 @@ private IQueryable ApplyLoadFlags(IQueryable query, ProductLoa public virtual void InsertProduct(Product product) { - if (product == null) - throw new ArgumentNullException("product"); + Guard.NotNull(product, nameof(product)); - //insert - _productRepository.Insert(product); - - //event notification - _services.EventPublisher.EntityInserted(product); + _productRepository.Insert(product); } - public virtual void UpdateProduct(Product product, bool publishEvent = true) + public virtual void UpdateProduct(Product product) { - if (product == null) - throw new ArgumentNullException("product"); + Guard.NotNull(product, nameof(product)); - bool modified = false; - if (publishEvent) - { - modified = _productRepository.IsModified(product); - } - - // update _productRepository.Update(product); - - // event notification - if (publishEvent && modified) - { - _services.EventPublisher.EntityUpdated(product); - } } public virtual void UpdateProductReviewTotals(Product product) { - if (product == null) - throw new ArgumentNullException("product"); + Guard.NotNull(product, nameof(product)); - int approvedRatingSum = 0; + int approvedRatingSum = 0; int notApprovedRatingSum = 0; int approvedTotalReviews = 0; int notApprovedTotalReviews = 0; @@ -466,8 +439,7 @@ public virtual AdjustInventoryResult AdjustInventory(OrganizedShoppingCartItem s public virtual AdjustInventoryResult AdjustInventory(OrderItem orderItem, bool decrease, int quantity) { - if (orderItem == null) - throw new ArgumentNullException("orderItem"); + Guard.NotNull(orderItem, nameof(orderItem)); if (orderItem.Product.ProductType == ProductType.BundledProduct && orderItem.Product.BundlePerItemShoppingCart) { @@ -496,8 +468,7 @@ public virtual AdjustInventoryResult AdjustInventory(OrderItem orderItem, bool d public virtual AdjustInventoryResult AdjustInventory(Product product, bool decrease, int quantity, string attributesXml) { - if (product == null) - throw new ArgumentNullException("product"); + Guard.NotNull(product, nameof(product)); var result = new AdjustInventoryResult(); @@ -538,10 +509,10 @@ public virtual AdjustInventoryResult AdjustInventory(Product product, bool decre product.Published = newPublished; UpdateProduct(product); - + //send email notification if (decrease && product.NotifyAdminForQuantityBelow > result.StockQuantityNew) - _workflowMessageService.SendQuantityBelowStoreOwnerNotification(product, _localizationSettings.DefaultAdminLanguageId); + _services.MessageFactory.SendQuantityBelowStoreOwnerNotification(product, _localizationSettings.DefaultAdminLanguageId); } break; case ManageInventoryMethod.ManageStockByAttributes: @@ -581,8 +552,7 @@ public virtual AdjustInventoryResult AdjustInventory(Product product, bool decre public virtual void UpdateHasTierPricesProperty(Product product) { - if (product == null) - throw new ArgumentNullException("product"); + Guard.NotNull(product, nameof(product)); var prevValue = product.HasTierPrices; product.HasTierPrices = product.TierPrices.Count > 0; @@ -592,8 +562,7 @@ public virtual void UpdateHasTierPricesProperty(Product product) public virtual void UpdateLowestAttributeCombinationPriceProperty(Product product) { - if (product == null) - throw new ArgumentNullException("product"); + Guard.NotNull(product, nameof(product)); var prevValue = product.LowestAttributeCombinationPrice; @@ -605,8 +574,7 @@ public virtual void UpdateLowestAttributeCombinationPriceProperty(Product produc public virtual void UpdateHasDiscountsApplied(Product product) { - if (product == null) - throw new ArgumentNullException("product"); + Guard.NotNull(product, nameof(product)); var prevValue = product.HasDiscountsApplied; product.HasDiscountsApplied = product.AppliedDiscounts.Count > 0; @@ -661,36 +629,15 @@ public virtual Multimap GetAppliedDiscountsByProductIds(int[] pro return map; } - public virtual Multimap GetProductSpecificationAttributesByProductIds(int[] productIds) - { - Guard.NotNull(productIds, nameof(productIds)); - - var query = _productSpecificationAttributeRepository.TableUntracked - .Expand(x => x.SpecificationAttributeOption) - .Expand(x => x.SpecificationAttributeOption.SpecificationAttribute) - .Where(x => productIds.Contains(x.ProductId)); - - var map = query - .OrderBy(x => x.DisplayOrder) - .ToList() - .ToMultimap(x => x.ProductId, x => x); - - return map; - } - #endregion #region Related products public virtual void DeleteRelatedProduct(RelatedProduct relatedProduct) { - if (relatedProduct == null) - throw new ArgumentNullException("relatedProduct"); - - _relatedProductRepository.Delete(relatedProduct); + Guard.NotNull(relatedProduct, nameof(relatedProduct)); - //event notification - _services.EventPublisher.EntityDeleted(relatedProduct); + _relatedProductRepository.Delete(relatedProduct); } public virtual IList GetRelatedProductsByProductId1(int productId1, bool showHidden = false) @@ -716,24 +663,16 @@ public virtual RelatedProduct GetRelatedProductById(int relatedProductId) public virtual void InsertRelatedProduct(RelatedProduct relatedProduct) { - if (relatedProduct == null) - throw new ArgumentNullException("relatedProduct"); + Guard.NotNull(relatedProduct, nameof(relatedProduct)); - _relatedProductRepository.Insert(relatedProduct); - - //event notification - _services.EventPublisher.EntityInserted(relatedProduct); + _relatedProductRepository.Insert(relatedProduct); } public virtual void UpdateRelatedProduct(RelatedProduct relatedProduct) { - if (relatedProduct == null) - throw new ArgumentNullException("relatedProduct"); - - _relatedProductRepository.Update(relatedProduct); + Guard.NotNull(relatedProduct, nameof(relatedProduct)); - //event notification - _services.EventPublisher.EntityUpdated(relatedProduct); + _relatedProductRepository.Update(relatedProduct); } public virtual int EnsureMutuallyRelatedProducts(int productId1) @@ -754,13 +693,9 @@ public virtual int EnsureMutuallyRelatedProducts(int productId1) public virtual void DeleteCrossSellProduct(CrossSellProduct crossSellProduct) { - if (crossSellProduct == null) - throw new ArgumentNullException("crossSellProduct"); + Guard.NotNull(crossSellProduct, nameof(crossSellProduct)); - _crossSellProductRepository.Delete(crossSellProduct); - - //event notification - _services.EventPublisher.EntityDeleted(crossSellProduct); + _crossSellProductRepository.Delete(crossSellProduct); } public virtual IList GetCrossSellProductsByProductId1(int productId1, bool showHidden = false) @@ -800,24 +735,16 @@ public virtual CrossSellProduct GetCrossSellProductById(int crossSellProductId) public virtual void InsertCrossSellProduct(CrossSellProduct crossSellProduct) { - if (crossSellProduct == null) - throw new ArgumentNullException("crossSellProduct"); - - _crossSellProductRepository.Insert(crossSellProduct); + Guard.NotNull(crossSellProduct, nameof(crossSellProduct)); - //event notification - _services.EventPublisher.EntityInserted(crossSellProduct); + _crossSellProductRepository.Insert(crossSellProduct); } public virtual void UpdateCrossSellProduct(CrossSellProduct crossSellProduct) { - if (crossSellProduct == null) - throw new ArgumentNullException("crossSellProduct"); + Guard.NotNull(crossSellProduct, nameof(crossSellProduct)); - _crossSellProductRepository.Update(crossSellProduct); - - // event notification - _services.EventPublisher.EntityUpdated(crossSellProduct); + _crossSellProductRepository.Update(crossSellProduct); } public virtual IList GetCrosssellProductsByShoppingCart(IList cart, int numberOfProducts) @@ -860,13 +787,9 @@ public virtual int EnsureMutuallyCrossSellProducts(int productId1) public virtual void DeleteTierPrice(TierPrice tierPrice) { - if (tierPrice == null) - throw new ArgumentNullException("tierPrice"); - - _tierPriceRepository.Delete(tierPrice); + Guard.NotNull(tierPrice, nameof(tierPrice)); - //event notification - _services.EventPublisher.EntityDeleted(tierPrice); + _tierPriceRepository.Delete(tierPrice); } public virtual TierPrice GetTierPriceById(int tierPriceId) @@ -905,24 +828,16 @@ where productIds.Contains(x.ProductId) public virtual void InsertTierPrice(TierPrice tierPrice) { - if (tierPrice == null) - throw new ArgumentNullException("tierPrice"); + Guard.NotNull(tierPrice, nameof(tierPrice)); - _tierPriceRepository.Insert(tierPrice); - - //event notification - _services.EventPublisher.EntityInserted(tierPrice); + _tierPriceRepository.Insert(tierPrice); } public virtual void UpdateTierPrice(TierPrice tierPrice) { - if (tierPrice == null) - throw new ArgumentNullException("tierPrice"); - - _tierPriceRepository.Update(tierPrice); + Guard.NotNull(tierPrice, nameof(tierPrice)); - //event notification - _services.EventPublisher.EntityUpdated(tierPrice); + _tierPriceRepository.Update(tierPrice); } #endregion @@ -931,15 +846,11 @@ public virtual void UpdateTierPrice(TierPrice tierPrice) public virtual void DeleteProductPicture(ProductPicture productPicture) { - if (productPicture == null) - throw new ArgumentNullException("productPicture"); + Guard.NotNull(productPicture, nameof(productPicture)); - UnassignDeletedPictureFromVariantCombinations(productPicture); + UnassignDeletedPictureFromVariantCombinations(productPicture); _productPictureRepository.Delete(productPicture); - - //event notification - _services.EventPublisher.EntityDeleted(productPicture); } private void UnassignDeletedPictureFromVariantCombinations(ProductPicture productPicture) @@ -1020,13 +931,9 @@ public virtual ProductPicture GetProductPictureById(int productPictureId) public virtual void InsertProductPicture(ProductPicture productPicture) { - if (productPicture == null) - throw new ArgumentNullException("productPicture"); - - _productPictureRepository.Insert(productPicture); + Guard.NotNull(productPicture, nameof(productPicture)); - //event notification - _services.EventPublisher.EntityInserted(productPicture); + _productPictureRepository.Insert(productPicture); } /// @@ -1035,13 +942,9 @@ public virtual void InsertProductPicture(ProductPicture productPicture) /// Product picture public virtual void UpdateProductPicture(ProductPicture productPicture) { - if (productPicture == null) - throw new ArgumentNullException("productPicture"); + Guard.NotNull(productPicture, nameof(productPicture)); - _productPictureRepository.Update(productPicture); - - //event notification - _services.EventPublisher.EntityUpdated(productPicture); + _productPictureRepository.Update(productPicture); } #endregion @@ -1050,8 +953,7 @@ public virtual void UpdateProductPicture(ProductPicture productPicture) public virtual void InsertBundleItem(ProductBundleItem bundleItem) { - if (bundleItem == null) - throw new ArgumentNullException("bundleItem"); + Guard.NotNull(bundleItem, nameof(bundleItem)); if (bundleItem.BundleProductId == 0) throw new SmartException("BundleProductId of a bundle item cannot be 0."); @@ -1063,26 +965,18 @@ public virtual void InsertBundleItem(ProductBundleItem bundleItem) throw new SmartException("A bundle item cannot be an element of itself."); _productBundleItemRepository.Insert(bundleItem); - - //event notification - _services.EventPublisher.EntityInserted(bundleItem); } public virtual void UpdateBundleItem(ProductBundleItem bundleItem) { - if (bundleItem == null) - throw new ArgumentNullException("bundleItem"); + Guard.NotNull(bundleItem, nameof(bundleItem)); _productBundleItemRepository.Update(bundleItem); - - //event notification - _services.EventPublisher.EntityUpdated(bundleItem); } public virtual void DeleteBundleItem(ProductBundleItem bundleItem) { - if (bundleItem == null) - throw new ArgumentNullException("bundleItem"); + Guard.NotNull(bundleItem, nameof(bundleItem)); // remove bundles from shopping carts (otherwise bundle item cannot be deleted) var parentCartItemIds = _shoppingCartItemRepository.TableUntracked @@ -1110,9 +1004,6 @@ public virtual void DeleteBundleItem(ProductBundleItem bundleItem) // delete bundle item _productBundleItemRepository.Delete(bundleItem); - - // event notification - _services.EventPublisher.EntityDeleted(bundleItem); } public virtual ProductBundleItem GetBundleItemById(int bundleItemId) diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductTagService.cs b/src/Libraries/SmartStore.Services/Catalog/ProductTagService.cs index 37e0a03cc9..798df87df0 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductTagService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductTagService.cs @@ -29,7 +29,7 @@ public partial class ProductTagService : IProductTagService /// /// Key pattern to clear cache /// - private const string PRODUCTTAG_PATTERN_KEY = "producttag:"; + private const string PRODUCTTAG_PATTERN_KEY = "producttag:*"; #endregion @@ -154,9 +154,6 @@ public virtual void DeleteProductTag(ProductTag productTag) //cache _cacheManager.RemoveByPattern(PRODUCTTAG_PATTERN_KEY); - - //event notification - _eventPublisher.EntityDeleted(productTag); } /// @@ -222,11 +219,7 @@ public virtual void InsertProductTag(ProductTag productTag) _productTagRepository.Insert(productTag); - //cache _cacheManager.RemoveByPattern(PRODUCTTAG_PATTERN_KEY); - - //event notification - _eventPublisher.EntityInserted(productTag); } /// @@ -240,11 +233,7 @@ public virtual void UpdateProductTag(ProductTag productTag) _productTagRepository.Update(productTag); - //cache _cacheManager.RemoveByPattern(PRODUCTTAG_PATTERN_KEY); - - //event notification - _eventPublisher.EntityUpdated(productTag); } /// diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductTemplateService.cs b/src/Libraries/SmartStore.Services/Catalog/ProductTemplateService.cs index 6964de3a78..fbf625225e 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductTemplateService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductTemplateService.cs @@ -49,9 +49,6 @@ public virtual void DeleteProductTemplate(ProductTemplate productTemplate) throw new ArgumentNullException("productTemplate"); _productTemplateRepository.Delete(productTemplate); - - //event notification - _eventPublisher.EntityDeleted(productTemplate); } /// @@ -91,9 +88,6 @@ public virtual void InsertProductTemplate(ProductTemplate productTemplate) throw new ArgumentNullException("productTemplate"); _productTemplateRepository.Insert(productTemplate); - - //event notification - _eventPublisher.EntityInserted(productTemplate); } /// @@ -106,9 +100,6 @@ public virtual void UpdateProductTemplate(ProductTemplate productTemplate) throw new ArgumentNullException("productTemplate"); _productTemplateRepository.Update(productTemplate); - - //event notification - _eventPublisher.EntityUpdated(productTemplate); } #endregion diff --git a/src/Libraries/SmartStore.Services/Catalog/RecentlyViewedProductsService.cs b/src/Libraries/SmartStore.Services/Catalog/RecentlyViewedProductsService.cs index 13b86c2e3d..5ee34a2a5c 100644 --- a/src/Libraries/SmartStore.Services/Catalog/RecentlyViewedProductsService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/RecentlyViewedProductsService.cs @@ -3,19 +3,21 @@ using System.Linq; using System.Web; using SmartStore.Core.Domain.Catalog; +using SmartStore.Services.Security; namespace SmartStore.Services.Catalog { - /// - /// Recently viewed products service - /// - public partial class RecentlyViewedProductsService : IRecentlyViewedProductsService + /// + /// Recently viewed products service + /// + public partial class RecentlyViewedProductsService : IRecentlyViewedProductsService { #region Fields private readonly HttpContextBase _httpContext; private readonly IProductService _productService; - private readonly CatalogSettings _catalogSettings; + private readonly IAclService _aclService; + private readonly CatalogSettings _catalogSettings; #endregion @@ -27,12 +29,16 @@ public partial class RecentlyViewedProductsService : IRecentlyViewedProductsServ /// HTTP context /// Product service /// Catalog settings - public RecentlyViewedProductsService(HttpContextBase httpContext, IProductService productService, - CatalogSettings catalogSettings) + public RecentlyViewedProductsService( + HttpContextBase httpContext, + IProductService productService, + IAclService aclService, + CatalogSettings catalogSettings) { - this._httpContext = httpContext; - this._productService = productService; - this._catalogSettings = catalogSettings; + _httpContext = httpContext; + _productService = productService; + _aclService = aclService; + _catalogSettings = catalogSettings; } #endregion @@ -73,7 +79,6 @@ protected IList GetRecentlyViewedProductsIds(int number) #region Methods - /// /// Gets a "recently viewed products" list /// @@ -81,12 +86,13 @@ protected IList GetRecentlyViewedProductsIds(int number) /// "recently viewed products" list public virtual IList GetRecentlyViewedProducts(int number) { - var products = new List(); var productIds = GetRecentlyViewedProductsIds(number); - var recentlyViewedProducts = _productService.GetProductsByIds(productIds.ToArray()).Where(x => x.Published && !x.Deleted); + var recentlyViewedProducts = _productService + .GetProductsByIds(productIds.ToArray()) + .Where(x => x.Published && !x.Deleted && _aclService.Authorize(x)) + .ToList(); - products.AddRange(recentlyViewedProducts); - return products; + return recentlyViewedProducts; } /// diff --git a/src/Libraries/SmartStore.Services/Catalog/SpecificationAttributeService.cs b/src/Libraries/SmartStore.Services/Catalog/SpecificationAttributeService.cs index 8ceb327cd2..b8f94ad350 100644 --- a/src/Libraries/SmartStore.Services/Catalog/SpecificationAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/SpecificationAttributeService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using SmartStore.Collections; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Events; @@ -72,9 +73,6 @@ public virtual void DeleteSpecificationAttribute(SpecificationAttribute specific throw new ArgumentNullException("specificationAttribute"); _specificationAttributeRepository.Delete(specificationAttribute); - - //event notification - _eventPublisher.EntityDeleted(specificationAttribute); } public virtual void InsertSpecificationAttribute(SpecificationAttribute specificationAttribute) @@ -83,9 +81,6 @@ public virtual void InsertSpecificationAttribute(SpecificationAttribute specific throw new ArgumentNullException("specificationAttribute"); _specificationAttributeRepository.Insert(specificationAttribute); - - //event notification - _eventPublisher.EntityInserted(specificationAttribute); } public virtual void UpdateSpecificationAttribute(SpecificationAttribute specificationAttribute) @@ -94,9 +89,6 @@ public virtual void UpdateSpecificationAttribute(SpecificationAttribute specific throw new ArgumentNullException("specificationAttribute"); _specificationAttributeRepository.Update(specificationAttribute); - - //event notification - _eventPublisher.EntityUpdated(specificationAttribute); } #endregion @@ -121,15 +113,25 @@ orderby sao.DisplayOrder return specificationAttributeOptions; } - public virtual void DeleteSpecificationAttributeOption(SpecificationAttributeOption specificationAttributeOption) + public virtual Multimap GetSpecificationAttributeOptionsBySpecificationAttributeIds(int[] specificationAttributeIds) + { + Guard.NotNull(specificationAttributeIds, nameof(specificationAttributeIds)); + + var options = _specificationAttributeOptionRepository.TableUntracked + .Where(x => specificationAttributeIds.Contains(x.SpecificationAttributeId)) + .OrderBy(x => x.DisplayOrder) + .ToList(); + + var map = options.ToMultimap(x => x.SpecificationAttributeId, x => x); + return map; + } + + public virtual void DeleteSpecificationAttributeOption(SpecificationAttributeOption specificationAttributeOption) { if (specificationAttributeOption == null) throw new ArgumentNullException("specificationAttributeOption"); _specificationAttributeOptionRepository.Delete(specificationAttributeOption); - - //event notification - _eventPublisher.EntityDeleted(specificationAttributeOption); } public virtual void InsertSpecificationAttributeOption(SpecificationAttributeOption specificationAttributeOption) @@ -138,9 +140,6 @@ public virtual void InsertSpecificationAttributeOption(SpecificationAttributeOpt throw new ArgumentNullException("specificationAttributeOption"); _specificationAttributeOptionRepository.Insert(specificationAttributeOption); - - //event notification - _eventPublisher.EntityInserted(specificationAttributeOption); } public virtual void UpdateSpecificationAttributeOption(SpecificationAttributeOption specificationAttributeOption) @@ -149,9 +148,6 @@ public virtual void UpdateSpecificationAttributeOption(SpecificationAttributeOpt throw new ArgumentNullException("specificationAttributeOption"); _specificationAttributeOptionRepository.Update(specificationAttributeOption); - - //event notification - _eventPublisher.EntityUpdated(specificationAttributeOption); } #endregion @@ -164,9 +160,6 @@ public virtual void DeleteProductSpecificationAttribute(ProductSpecificationAttr throw new ArgumentNullException("productSpecificationAttribute"); _productSpecificationAttributeRepository.Delete(productSpecificationAttribute); - - //event notification - _eventPublisher.EntityDeleted(productSpecificationAttribute); } public virtual IList GetProductSpecificationAttributesByProductId(int productId) @@ -223,7 +216,24 @@ join sao in _specificationAttributeOptionRepository.Table.Expand(x => x.Specific } } - public virtual ProductSpecificationAttribute GetProductSpecificationAttributeById(int productSpecificationAttributeId) + public virtual Multimap GetProductSpecificationAttributesByProductIds(int[] productIds) + { + Guard.NotNull(productIds, nameof(productIds)); + + var query = _productSpecificationAttributeRepository.TableUntracked + .Expand(x => x.SpecificationAttributeOption) + .Expand(x => x.SpecificationAttributeOption.SpecificationAttribute) + .Where(x => productIds.Contains(x.ProductId)); + + var map = query + .OrderBy(x => x.DisplayOrder) + .ToList() + .ToMultimap(x => x.ProductId, x => x); + + return map; + } + + public virtual ProductSpecificationAttribute GetProductSpecificationAttributeById(int productSpecificationAttributeId) { if (productSpecificationAttributeId == 0) return null; @@ -238,9 +248,6 @@ public virtual void InsertProductSpecificationAttribute(ProductSpecificationAttr throw new ArgumentNullException("productSpecificationAttribute"); _productSpecificationAttributeRepository.Insert(productSpecificationAttribute); - - //event notification - _eventPublisher.EntityInserted(productSpecificationAttribute); } public virtual void UpdateProductSpecificationAttribute(ProductSpecificationAttribute productSpecificationAttribute) @@ -249,9 +256,6 @@ public virtual void UpdateProductSpecificationAttribute(ProductSpecificationAttr throw new ArgumentNullException("productSpecificationAttribute"); _productSpecificationAttributeRepository.Update(productSpecificationAttribute); - - //event notification - _eventPublisher.EntityUpdated(productSpecificationAttribute); } #endregion diff --git a/src/Libraries/SmartStore.Services/Common/AddressExtentions.cs b/src/Libraries/SmartStore.Services/Common/AddressExtentions.cs index 1055aaa340..100749d906 100644 --- a/src/Libraries/SmartStore.Services/Common/AddressExtentions.cs +++ b/src/Libraries/SmartStore.Services/Common/AddressExtentions.cs @@ -83,22 +83,36 @@ public static Address FindAddress( return source.FirstOrDefault(addressMatcher); } - /// Returns the full name of the address. - public static string GetFullName(this Address address) + /// + /// Returns the full name of the address. + /// + public static string GetFullName(this Address address, bool withCompanyName = true) { - if (address != null) - { - var sb = new StringBuilder(address.FirstName); + if (address == null) + return null; - sb.Grow(address.LastName, " "); + string result = string.Empty; + if (address.FirstName.HasValue() || address.LastName.HasValue()) + { + result = string.Format("{0} {1}", address.FirstName, address.LastName).Trim(); + } - if (address.Company.HasValue()) - { - sb.Grow("({0})".FormatWith(address.Company), " "); - } - return sb.ToString(); + if (withCompanyName && address.Company.HasValue()) + { + result = string.Concat(result, result.HasValue() ? ", " : "", address.Company); } - return null; + + return result; + } + + public static string GetFullSalutaion(this Address address) + { + if (address == null) + return null; + + return string.Format("{0}{1}", + address.Salutation.EmptyNull(), + address.Title.HasValue() ? " " + address.Title : ""); } /// diff --git a/src/Libraries/SmartStore.Services/Common/AddressService.cs b/src/Libraries/SmartStore.Services/Common/AddressService.cs index ff0c8bb97e..de52593d01 100644 --- a/src/Libraries/SmartStore.Services/Common/AddressService.cs +++ b/src/Libraries/SmartStore.Services/Common/AddressService.cs @@ -5,62 +5,47 @@ using SmartStore.Services.Directory; using SmartStore.Core.Events; using System.Collections.Generic; +using SmartStore.Templating; +using SmartStore.Services.Messages; +using SmartStore.Core.Domain.Directory; +using System.Globalization; +using SmartStore.Core.Html; namespace SmartStore.Services.Common { - /// - /// Address service - /// public partial class AddressService : IAddressService { - #region Fields - private readonly IRepository
_addressRepository; private readonly ICountryService _countryService; private readonly IStateProvinceService _stateProvinceService; - private readonly IEventPublisher _eventPublisher; + private readonly ICommonServices _services; private readonly AddressSettings _addressSettings; - - #endregion - - #region Ctor - - /// - /// Ctor - /// - /// Address repository - /// Country service - /// State/province service - /// Event publisher - /// Address settings - public AddressService(IRepository
addressRepository, - ICountryService countryService, IStateProvinceService stateProvinceService, - IEventPublisher eventPublisher, AddressSettings addressSettings) + private readonly ITemplateEngine _templateEngine; + private readonly IMessageModelProvider _messageModelProvider; + + public AddressService( + IRepository
addressRepository, + ICountryService countryService, + IStateProvinceService stateProvinceService, + ICommonServices services, + AddressSettings addressSettings, + ITemplateEngine templateEngine, + IMessageModelProvider messageModelProvider) { - this._addressRepository = addressRepository; - this._countryService = countryService; - this._stateProvinceService = stateProvinceService; - this._eventPublisher = eventPublisher; - this._addressSettings = addressSettings; + _addressRepository = addressRepository; + _countryService = countryService; + _stateProvinceService = stateProvinceService; + _services = services; + _addressSettings = addressSettings; + _templateEngine = templateEngine; + _messageModelProvider = messageModelProvider; } - #endregion - - #region Methods - - /// - /// Deletes an address - /// - /// Address public virtual void DeleteAddress(Address address) { - if (address == null) - throw new ArgumentNullException("address"); + Guard.NotNull(address, nameof(address)); _addressRepository.Delete(address); - - //event notification - _eventPublisher.EntityDeleted(address); } public virtual void DeleteAddress(int id) @@ -70,11 +55,6 @@ public virtual void DeleteAddress(int id) DeleteAddress(address); } - /// - /// Gets total number of addresses by country identifier - /// - /// Country identifier - /// Number of addresses public virtual int GetAddressTotalByCountryId(int countryId) { if (countryId == 0) @@ -86,11 +66,6 @@ public virtual int GetAddressTotalByCountryId(int countryId) return query.Count(); } - /// - /// Gets total number of addresses by state/province identifier - /// - /// State/province identifier - /// Number of addresses public virtual int GetAddressTotalByStateProvinceId(int stateProvinceId) { if (stateProvinceId == 0) @@ -102,11 +77,6 @@ public virtual int GetAddressTotalByStateProvinceId(int stateProvinceId) return query.Count(); } - /// - /// Gets an address by address identifier - /// - /// Address identifier - /// Address public virtual Address GetAddressById(int addressId) { if (addressId == 0) @@ -128,16 +98,11 @@ where addressIds.Contains(x.Id) return query.ToList(); } - /// - /// Inserts an address - /// - /// Address public virtual void InsertAddress(Address address) { - if (address == null) - throw new ArgumentNullException("address"); + Guard.NotNull(address, nameof(address)); - address.CreatedOnUtc = DateTime.UtcNow; + address.CreatedOnUtc = DateTime.UtcNow; //some validation if (address.CountryId == 0) @@ -146,43 +111,26 @@ public virtual void InsertAddress(Address address) address.StateProvinceId = null; _addressRepository.Insert(address); - - //event notification - _eventPublisher.EntityInserted(address); } - /// - /// Updates the address - /// - /// Address public virtual void UpdateAddress(Address address) { - if (address == null) - throw new ArgumentNullException("address"); + Guard.NotNull(address, nameof(address)); - //some validation - if (address.CountryId == 0) + //some validation + if (address.CountryId == 0) address.CountryId = null; if (address.StateProvinceId == 0) address.StateProvinceId = null; _addressRepository.Update(address); - - //event notification - _eventPublisher.EntityUpdated(address); } - /// - /// Gets a value indicating whether address is valid (can be saved) - /// - /// Address to validate - /// Result public virtual bool IsAddressValid(Address address) { - if (address == null) - throw new ArgumentNullException("address"); + Guard.NotNull(address, nameof(address)); - if (String.IsNullOrWhiteSpace(address.FirstName)) + if (String.IsNullOrWhiteSpace(address.FirstName)) return false; if (String.IsNullOrWhiteSpace(address.LastName)) @@ -254,6 +202,63 @@ public virtual bool IsAddressValid(Address address) return true; } - #endregion - } + public virtual string FormatAddress(CompanyInformationSettings settings, bool newLineToBr = false) + { + Guard.NotNull(settings, nameof(settings)); + + var address = new Address + { + Address1 = settings.Street, + Address2 = settings.Street2, + City = settings.City, + Company = settings.CompanyName, + FirstName = settings.Firstname, + LastName = settings.Lastname, + Salutation = settings.Salutation, + Title = settings.Title, + ZipPostalCode = settings.ZipCode, + CountryId = settings.CountryId, + Country = _countryService.GetCountryById(settings.CountryId) + }; + + return FormatAddress(address, newLineToBr); + } + + public virtual string FormatAddress(Address address, bool newLineToBr = false) + { + Guard.NotNull(address, nameof(address)); + + var messageContext = new MessageContext + { + Language = _services.WorkContext.WorkingLanguage, + Store = _services.StoreContext.CurrentStore, + Model = new TemplateModel() + }; + + _messageModelProvider.AddModelPart(address, messageContext, "Address"); + var model = messageContext.Model["Address"]; + + var result = FormatAddress(model, address?.Country?.AddressFormat, messageContext.FormatProvider); + + if (newLineToBr) + { + result = HtmlUtils.ConvertPlainTextToHtml(result); + } + + return result; + } + + public virtual string FormatAddress(object address, string template = null, IFormatProvider formatProvider = null) + { + Guard.NotNull(address, nameof(address)); + + template = template.NullEmpty() ?? Address.DefaultAddressFormat; + + var result = _templateEngine + .Render(template, address, formatProvider ?? CultureInfo.CurrentCulture) + .Compact(true); + + return result; + } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Common/GenericAttributeExtentions.cs b/src/Libraries/SmartStore.Services/Common/GenericAttributeExtentions.cs index eb905b7e7b..4c0e1c05d0 100644 --- a/src/Libraries/SmartStore.Services/Common/GenericAttributeExtentions.cs +++ b/src/Libraries/SmartStore.Services/Common/GenericAttributeExtentions.cs @@ -30,8 +30,7 @@ public static TPropType GetAttribute(this BaseEntity entity, string k /// GenericAttributeService /// Load a value specific for a certain store; pass 0 to load a value shared for all stores /// Attribute - public static TPropType GetAttribute(this BaseEntity entity, - string key, IGenericAttributeService genericAttributeService, int storeId = 0) + public static TPropType GetAttribute(this BaseEntity entity, string key, IGenericAttributeService genericAttributeService, int storeId = 0) { if (entity == null) throw new ArgumentNullException("entity"); @@ -39,7 +38,7 @@ public static TPropType GetAttribute(this BaseEntity entity, if (genericAttributeService == null) genericAttributeService = EngineContext.Current.Resolve(); - string keyGroup = entity.GetUnproxiedEntityType().Name; + string keyGroup = entity.GetUnproxiedType().Name; return genericAttributeService.GetAttribute(keyGroup, entity.Id, key, storeId); diff --git a/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs b/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs index ef9546c671..5c6a353ddb 100644 --- a/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs @@ -39,9 +39,6 @@ public virtual void DeleteAttribute(GenericAttribute attribute) _genericAttributeRepository.Delete(attribute); - //event notifications - _eventPublisher.EntityDeleted(attribute); - if (keyGroup.IsCaseInsensitiveEqual("Order") && entityId != 0) { var order = _orderRepository.GetById(entityId); @@ -65,9 +62,6 @@ public virtual void InsertAttribute(GenericAttribute attribute) _genericAttributeRepository.Insert(attribute); - //event notifications - _eventPublisher.EntityInserted(attribute); - if (attribute.KeyGroup.IsCaseInsensitiveEqual("Order") && attribute.EntityId != 0) { var order = _orderRepository.GetById(attribute.EntityId); @@ -82,9 +76,6 @@ public virtual void UpdateAttribute(GenericAttribute attribute) _genericAttributeRepository.Update(attribute); - //event notifications - _eventPublisher.EntityUpdated(attribute); - if (attribute.KeyGroup.IsCaseInsensitiveEqual("Order") && attribute.EntityId != 0) { var order = _orderRepository.GetById(attribute.EntityId); @@ -131,7 +122,7 @@ public virtual void SaveAttribute(BaseEntity entity, string key, TPro { Guard.NotNull(entity, nameof(entity)); - SaveAttribute(entity.Id, key, entity.GetUnproxiedEntityType().Name, value, storeId); + SaveAttribute(entity.Id, key, entity.GetUnproxiedType().Name, value, storeId); } public virtual void SaveAttribute(int entityId, string key, string keyGroup, TPropType value, int storeId = 0) diff --git a/src/Libraries/SmartStore.Services/Common/IAddressService.cs b/src/Libraries/SmartStore.Services/Common/IAddressService.cs index 37faa65bea..5ece5f704d 100644 --- a/src/Libraries/SmartStore.Services/Common/IAddressService.cs +++ b/src/Libraries/SmartStore.Services/Common/IAddressService.cs @@ -1,7 +1,7 @@ - - +using System; using System.Collections.Generic; using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Directory; namespace SmartStore.Services.Common { @@ -64,5 +64,29 @@ public partial interface IAddressService /// Address to validate /// Result bool IsAddressValid(Address address); - } + + /// + /// Formats the address according to the countries address formatting template + /// + /// Address to format + /// Whether new lines should be replaced with html BR tags + /// The formatted address + string FormatAddress(CompanyInformationSettings settings, bool newLineToBr = false); + + /// + /// Formats the address according to the countries address formatting template + /// + /// Address to format + /// Whether new lines should be replaced with html BR tags + /// The formatted address + string FormatAddress(Address address, bool newLineToBr = false); + + /// + /// Formats the address according to the countries address formatting template + /// + /// Address to format. Usually passed by the template engine as a dictionary + /// The (liquid) formatting template. If null, the system global template will be used. + /// The formatted address + string FormatAddress(object address, string template = null, IFormatProvider formatProvider = null); + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Common/IUserAgent.cs b/src/Libraries/SmartStore.Services/Common/IUserAgent.cs index 9099c513c7..ff20f7b696 100644 --- a/src/Libraries/SmartStore.Services/Common/IUserAgent.cs +++ b/src/Libraries/SmartStore.Services/Common/IUserAgent.cs @@ -72,6 +72,7 @@ public sealed class UserAgentInfo }; private bool? _isBot; + private bool? _supportsWebP; public UserAgentInfo(string family, string major, string minor, string patch) { @@ -100,6 +101,41 @@ public bool IsBot return _isBot.Value; } } + public bool SupportsWebP + { + get + { + if (_supportsWebP == null) + { + if (Family == "Chrome") + { + _supportsWebP = Major.ToInt() >= 49; + } + else if (Family.StartsWith("Chrome Mobile")) + { + _supportsWebP = Major.ToInt() >= 61; + } + else if (Family == "Opera") + { + _supportsWebP = Major.ToInt() >= 48; + } + else if (Family == "Opera Mini") + { + _supportsWebP = true; + } + else if (Family == "Android") + { + _supportsWebP = Major.ToInt() >= 5 || (Major.ToInt() == 4 && Minor.ToInt() >= 4); + } + else + { + _supportsWebP = false; + } + } + + return _supportsWebP.Value; + } + } } internal static class VersionString diff --git a/src/Libraries/SmartStore.Services/CommonServices.cs b/src/Libraries/SmartStore.Services/CommonServices.cs index 1542718a37..28b7c20ff2 100644 --- a/src/Libraries/SmartStore.Services/CommonServices.cs +++ b/src/Libraries/SmartStore.Services/CommonServices.cs @@ -12,6 +12,8 @@ using SmartStore.Services.Stores; using Autofac; using SmartStore.Services.Helpers; +using SmartStore.Services.Media; +using SmartStore.Services.Messages; namespace SmartStore.Services { @@ -28,6 +30,7 @@ public class CommonServices : ICommonServices private readonly Lazy _eventPublisher; private readonly Lazy _localization; private readonly Lazy _customerActivity; + private readonly Lazy _pictureService; private readonly Lazy _notifier; private readonly Lazy _permissions; private readonly Lazy _settings; @@ -35,6 +38,7 @@ public class CommonServices : ICommonServices private readonly Lazy _dateTimeHelper; private readonly Lazy _displayControl; private readonly Lazy _chronometer; + private readonly Lazy _messageFactory; public CommonServices( IComponentContext container, @@ -48,13 +52,15 @@ public CommonServices( Lazy eventPublisher, Lazy localization, Lazy customerActivity, + Lazy pictureService, Lazy notifier, Lazy permissions, Lazy settings, Lazy storeService, Lazy dateTimeHelper, Lazy displayControl, - Lazy chronometer) + Lazy chronometer, + Lazy messageFactory) { this._container = container; this._env = env; @@ -67,6 +73,7 @@ public CommonServices( this._eventPublisher = eventPublisher; this._localization = localization; this._customerActivity = customerActivity; + this._pictureService = pictureService; this._notifier = notifier; this._permissions = permissions; this._settings = settings; @@ -74,151 +81,28 @@ public CommonServices( this._dateTimeHelper = dateTimeHelper; this._displayControl = displayControl; this._chronometer = chronometer; - } - - public IComponentContext Container - { - get - { - return _container; - } - } - - public IApplicationEnvironment ApplicationEnvironment - { - get - { - return _env.Value; - } - } - - public ICacheManager Cache - { - get - { - return _cacheManager.Value; - } - } - - public IRequestCache RequestCache - { - get - { - return _requestCache.Value; - } - } - - public IDbContext DbContext - { - get - { - return _dbContext.Value; - } - } - - public IStoreContext StoreContext - { - get - { - return _storeContext.Value; - } - } - - public IWebHelper WebHelper - { - get - { - return _webHelper.Value; - } - } - - public IWorkContext WorkContext - { - get - { - return _workContext.Value; - } - } - - public IEventPublisher EventPublisher - { - get - { - return _eventPublisher.Value; - } - } - - public ILocalizationService Localization - { - get - { - return _localization.Value; - } - } - - public ICustomerActivityService CustomerActivity - { - get - { - return _customerActivity.Value; - } - } - - public INotifier Notifier - { - get - { - return _notifier.Value; - } - } - - public IPermissionService Permissions - { - get - { - return _permissions.Value; - } - } - - public ISettingService Settings - { - get - { - return _settings.Value; - } - } - - - public IStoreService StoreService - { - get - { - return _storeService.Value; - } - } - - public IDateTimeHelper DateTimeHelper - { - get - { - return _dateTimeHelper.Value; - } - } - - public IDisplayControl DisplayControl - { - get - { - return _displayControl.Value; - } - } - - public IChronometer Chronometer - { - get - { - return _chronometer.Value; - } - } + this._messageFactory = messageFactory; + } + + public IComponentContext Container => _container; + public IApplicationEnvironment ApplicationEnvironment => _env.Value; + public ICacheManager Cache => _cacheManager.Value; + public IRequestCache RequestCache => _requestCache.Value; + public IDbContext DbContext => _dbContext.Value; + public IStoreContext StoreContext => _storeContext.Value; + public IWebHelper WebHelper => _webHelper.Value; + public IWorkContext WorkContext => _workContext.Value; + public IEventPublisher EventPublisher => _eventPublisher.Value; + public ILocalizationService Localization => _localization.Value; + public ICustomerActivityService CustomerActivity => _customerActivity.Value; + public IPictureService PictureService => _pictureService.Value; + public INotifier Notifier => _notifier.Value; + public IPermissionService Permissions => _permissions.Value; + public ISettingService Settings => _settings.Value; + public IStoreService StoreService => _storeService.Value; + public IDateTimeHelper DateTimeHelper => _dateTimeHelper.Value; + public IDisplayControl DisplayControl => _displayControl.Value; + public IChronometer Chronometer => _chronometer.Value; + public IMessageFactory MessageFactory => _messageFactory.Value; } } diff --git a/src/Libraries/SmartStore.Services/Configuration/SettingService.cs b/src/Libraries/SmartStore.Services/Configuration/SettingService.cs index df20550636..f91205f0ba 100644 --- a/src/Libraries/SmartStore.Services/Configuration/SettingService.cs +++ b/src/Libraries/SmartStore.Services/Configuration/SettingService.cs @@ -1,33 +1,29 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; +using System.Reflection; using Newtonsoft.Json; +using SmartStore.ComponentModel; using SmartStore.Core.Caching; using SmartStore.Core.Configuration; using SmartStore.Core.Data; using SmartStore.Core.Domain.Configuration; -using SmartStore.Core.Events; -using System.Linq.Expressions; -using System.Reflection; -using SmartStore.ComponentModel; -using System.Collections; -using SmartStore.Utilities; using SmartStore.Core.Logging; namespace SmartStore.Services.Configuration { - public partial class SettingService : ScopedServiceBase, ISettingService + public partial class SettingService : ScopedServiceBase, ISettingService { private const string SETTINGS_ALL_KEY = "setting:all"; private readonly IRepository _settingRepository; - private readonly IEventPublisher _eventPublisher; private readonly ICacheManager _cacheManager; - public SettingService(ICacheManager cacheManager, IEventPublisher eventPublisher, IRepository settingRepository) + public SettingService(ICacheManager cacheManager, IRepository settingRepository) { _cacheManager = cacheManager; - _eventPublisher = eventPublisher; _settingRepository = settingRepository; Logger = NullLogger.Instance; @@ -37,8 +33,7 @@ public SettingService(ICacheManager cacheManager, IEventPublisher eventPublisher protected virtual IDictionary GetAllCachedSettings() { - string key = string.Format(SETTINGS_ALL_KEY); - return _cacheManager.Get(key, () => + return _cacheManager.Get(SETTINGS_ALL_KEY, () => { var query = from s in _settingRepository.TableUntracked orderby s.Name, s.StoreId @@ -65,6 +60,23 @@ protected virtual IDictionary GetAllCachedSettings() }); } + protected virtual PropertyInfo GetPropertyInfo(Expression> keySelector) + { + var member = keySelector.Body as MemberExpression; + if (member == null) + { + throw new ArgumentException($"Expression '{keySelector}' refers to a method, not a property."); + } + + var propInfo = member.Member as PropertyInfo; + if (propInfo == null) + { + throw new ArgumentException($"Expression '{keySelector}' refers to a field, not a property."); + } + + return propInfo; + } + public virtual void InsertSetting(Setting setting, bool clearCache = true) { Guard.NotNull(setting, nameof(setting)); @@ -75,8 +87,6 @@ public virtual void InsertSetting(Setting setting, bool clearCache = true) if (clearCache) ClearCache(); - - _eventPublisher.EntityInserted(setting); } public virtual void UpdateSetting(Setting setting, bool clearCache = true) @@ -89,16 +99,13 @@ public virtual void UpdateSetting(Setting setting, bool clearCache = true) if (clearCache) ClearCache(); - - _eventPublisher.EntityUpdated(setting); } - private T LoadSettingsJson(int storeId = 0) + private ISettings LoadSettingsJson(Type settingType, int storeId = 0) { - Type t = typeof(T); - string key = t.Namespace + "." + t.Name; + string key = settingType.Namespace + "." + settingType.Name; - T settings = Activator.CreateInstance(); + var settings = (ISettings)Activator.CreateInstance(settingType); var rawSetting = GetSettingByKey(key, storeId: storeId, loadSharedValueIfNotFound: true); if (rawSetting.HasValue()) @@ -109,9 +116,9 @@ private T LoadSettingsJson(int storeId = 0) return settings; } - private void SaveSettingsJson(T settings) + private void SaveSettingsJson(ISettings settings) { - Type t = typeof(T); + Type t = settings.GetType(); string key = t.Namespace + "." + t.Name; var storeId = 0; @@ -190,40 +197,43 @@ public virtual bool SettingExists( int storeId = 0) where T : ISettings, new() { - var member = keySelector.Body as MemberExpression; - if (member == null) - { - throw new ArgumentException(string.Format( - "Expression '{0}' refers to a method, not a property.", - keySelector)); - } + var propInfo = GetPropertyInfo(keySelector); + var key = string.Concat(typeof(T).Name, ".", propInfo.Name); - var propInfo = member.Member as PropertyInfo; - if (propInfo == null) + string setting = GetSettingByKey(key, storeId: storeId); + return setting != null; + } + + public T LoadSetting(int storeId = 0) where T : ISettings, new() + { + return (T)LoadSettingCore(typeof(T), storeId); + } + + public ISettings LoadSetting(Type settingType, int storeId = 0) + { + Guard.NotNull(settingType, nameof(settingType)); + Guard.HasDefaultConstructor(settingType); + + if (!typeof(ISettings).IsAssignableFrom(settingType)) { - throw new ArgumentException(string.Format( - "Expression '{0}' refers to a field, not a property.", - keySelector)); + throw new ArgumentException($"The type to load settings for must be a subclass of the '{typeof(ISettings).FullName}' interface", nameof(settingType)); } - string key = typeof(T).Name + "." + propInfo.Name; - - string setting = GetSettingByKey(key, storeId: storeId); - return setting != null; + return LoadSettingCore(settingType, storeId); } - public virtual T LoadSetting(int storeId = 0) where T : ISettings, new() + protected virtual ISettings LoadSettingCore(Type settingType, int storeId = 0) { - if (typeof(T).HasAttribute(true)) + if (settingType.HasAttribute(true)) { - return LoadSettingsJson(storeId); + return LoadSettingsJson(settingType, storeId); } - var settings = Activator.CreateInstance(); + var settings = (ISettings)Activator.CreateInstance(settingType); - var prefix = typeof(T).Name; + var prefix = settingType.Name; - foreach (var fastProp in FastProperty.GetProperties(typeof(T)).Values) + foreach (var fastProp in FastProperty.GetProperties(settingType).Values) { var prop = fastProp.Property; @@ -238,45 +248,45 @@ public virtual bool SettingExists( if (setting == null) { if (fastProp.IsSequenceType) - { + { if ((fastProp.GetValue(settings) as IEnumerable) != null) - { - // Instance of IEnumerable<> was already created, most likely in the constructor of the settings concrete class. - // In this case we shouldn't let the EnumerableConverter create a new instance but keep this one. - continue; - } - } - else - { - #region Obsolete ('EnumerableConverter' can handle this case now) - //if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>)) - //{ - // // convenience: don't return null for simple list types - // var listArg = prop.PropertyType.GetGenericArguments()[0]; - // object list = null; - - // if (listArg == typeof(int)) - // list = new List(); - // else if (listArg == typeof(decimal)) - // list = new List(); - // else if (listArg == typeof(string)) - // list = new List(); - - // if (list != null) - // { - // fastProp.SetValue(settings, list); - // } - //} - #endregion - - continue; - } + { + // Instance of IEnumerable<> was already created, most likely in the constructor of the settings concrete class. + // In this case we shouldn't let the EnumerableConverter create a new instance but keep this one. + continue; + } + } + else + { + #region Obsolete ('EnumerableConverter' can handle this case now) + //if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>)) + //{ + // // convenience: don't return null for simple list types + // var listArg = prop.PropertyType.GetGenericArguments()[0]; + // object list = null; + + // if (listArg == typeof(int)) + // list = new List(); + // else if (listArg == typeof(decimal)) + // list = new List(); + // else if (listArg == typeof(string)) + // list = new List(); + + // if (list != null) + // { + // fastProp.SetValue(settings, list); + // } + //} + #endregion + + continue; + } } var converter = TypeConverterFactory.GetConverter(prop.PropertyType); - if (converter == null || !converter.CanConvertFrom(typeof(string))) + if (converter == null || !converter.CanConvertFrom(typeof(string))) continue; try @@ -341,20 +351,35 @@ public virtual void SetSetting(string key, T value, int storeId = 0, bool cle } } - public virtual void SaveSetting(T settings, int storeId = 0) where T : ISettings, new() + public void SaveSetting(T settings, int storeId = 0) where T : ISettings, new() { + SaveSettingCore(settings, storeId); + } + + public void SaveSetting(ISettings settings, int storeId = 0) + { + SaveSettingCore(settings, storeId); + } + + protected virtual void SaveSettingCore(ISettings settings, int storeId = 0) + { + Guard.NotNull(settings, nameof(settings)); + using (BeginScope()) { - if (typeof(T).HasAttribute(true)) + var settingType = settings.GetType(); + var prefix = settingType.Name; + + if (settingType.HasAttribute(true)) { - SaveSettingsJson(settings); - return; + //SaveSettingsJson(settings); + //return; } /* We do not clear cache after each setting update. * This behavior can increase performance because cached settings will not be cleared * and loaded from database after each update */ - foreach (var prop in FastProperty.GetProperties(typeof(T)).Values) + foreach (var prop in FastProperty.GetProperties(settingType).Values) { // get properties we can read and write to if (!prop.IsPublicSettable) @@ -364,14 +389,14 @@ public virtual void SetSetting(string key, T value, int storeId = 0, bool cle if (converter == null || !converter.CanConvertFrom(typeof(string))) continue; - string key = typeof(T).Name + "." + prop.Name; + string key = prefix + "." + prop.Name; // Duck typing is not supported in C#. That's why we're using dynamic type dynamic value = prop.GetValue(settings); SetSetting(key, value ?? "", storeId, false); } } - } + } public virtual void SaveSetting( T settings, @@ -379,24 +404,10 @@ public virtual void SaveSetting( int storeId = 0, bool clearCache = true) where T : ISettings, new() { - var member = keySelector.Body as MemberExpression; - if (member == null) - { - throw new ArgumentException(string.Format( - "Expression '{0}' refers to a method, not a property.", - keySelector)); - } + var propInfo = GetPropertyInfo(keySelector); + var key = string.Concat(typeof(T).Name, ".", propInfo.Name); - var propInfo = member.Member as PropertyInfo; - if (propInfo == null) - { - throw new ArgumentException(string.Format( - "Expression '{0}' refers to a field, not a property.", - keySelector)); - } - - string key = typeof(T).Name + "." + propInfo.Name; - // Duck typing is not supported in C#. That's why we're using dynamic type + // Duck typing is not supported in C#. That's why we're using dynamic type. var fastProp = FastProperty.GetProperty(propInfo, PropertyCachingStrategy.EagerCached); dynamic value = fastProp.GetValue(settings); @@ -410,9 +421,13 @@ public virtual void UpdateSetting( int storeId = 0) where T : ISettings, new() { if (overrideForStore || storeId == 0) - SaveSetting(settings, keySelector, storeId, true); + { + SaveSetting(settings, keySelector, storeId, false); + } else if (storeId > 0) + { DeleteSetting(settings, keySelector, storeId); + } } public virtual void DeleteSetting(Setting setting) @@ -425,8 +440,6 @@ public virtual void DeleteSetting(Setting setting) HasChanges = true; ClearCache(); - - _eventPublisher.EntityDeleted(setting); } public virtual void DeleteSetting() where T : ISettings, new() @@ -459,23 +472,8 @@ public virtual void DeleteSetting( Expression> keySelector, int storeId = 0) where T : ISettings, new() { - var member = keySelector.Body as MemberExpression; - if (member == null) - { - throw new ArgumentException(string.Format( - "Expression '{0}' refers to a method, not a property.", - keySelector)); - } - - var propInfo = member.Member as PropertyInfo; - if (propInfo == null) - { - throw new ArgumentException(string.Format( - "Expression '{0}' refers to a field, not a property.", - keySelector)); - } - - string key = typeof(T).Name + "." + propInfo.Name; + var propInfo = GetPropertyInfo(keySelector); + var key = string.Concat(typeof(T).Name, ".", propInfo.Name); DeleteSetting(key, storeId); } @@ -522,7 +520,7 @@ public virtual int DeleteSettings(string rootKey) { protected override void OnClearCache() { - _cacheManager.RemoveByPattern(SETTINGS_ALL_KEY); + _cacheManager.Remove(SETTINGS_ALL_KEY); } protected string CreateCacheKey(string name, int storeId) @@ -531,21 +529,6 @@ protected string CreateCacheKey(string name, int storeId) } } - //[Serializable] - //public class SettingKey : ComparableObject - //{ - // [ObjectSignature] - // public string Name { get; set; } - - // [ObjectSignature] - // public int StoreId { get; set; } - - // public override string ToString() - // { - // return Name + "@__!__@" + StoreId; - // } - //} - [Serializable] public class CachedSetting { diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerContentService.cs b/src/Libraries/SmartStore.Services/Customers/CustomerContentService.cs index 9f0d86f957..570baeba76 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerContentService.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerContentService.cs @@ -24,9 +24,6 @@ public virtual void DeleteCustomerContent(CustomerContent content) throw new ArgumentNullException("content"); _contentRepository.Delete(content); - - //event notification - _eventPublisher.EntityDeleted(content); } public virtual IList GetAllCustomerContent(int customerId, bool? approved) @@ -72,9 +69,6 @@ public virtual void InsertCustomerContent(CustomerContent content) throw new ArgumentNullException("content"); _contentRepository.Insert(content); - - //event notification - _eventPublisher.EntityInserted(content); } public virtual void UpdateCustomerContent(CustomerContent content) @@ -83,9 +77,6 @@ public virtual void UpdateCustomerContent(CustomerContent content) throw new ArgumentNullException("content"); _contentRepository.Update(content); - - //event notification - _eventPublisher.EntityUpdated(content); } } } diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerExtensions.cs b/src/Libraries/SmartStore.Services/Customers/CustomerExtensions.cs index b082901b1f..34a7cc9aa0 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerExtensions.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerExtensions.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Linq; using System.Xml; +using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Infrastructure; @@ -139,33 +140,37 @@ public static bool IsGuest(this Customer customer, bool onlyActiveCustomerRoles public static string GetFullName(this Customer customer) { - Guard.NotNull(customer, nameof(customer)); + if (customer == null) + return string.Empty; - var firstName = customer.GetAttribute(SystemCustomerAttributeNames.FirstName); - var lastName = customer.GetAttribute(SystemCustomerAttributeNames.LastName); + var firstName = customer.GetAttribute(SystemCustomerAttributeNames.FirstName).NullEmpty(); + var lastName = customer.GetAttribute(SystemCustomerAttributeNames.LastName).NullEmpty(); - string fullName = ""; - if (!String.IsNullOrWhiteSpace(firstName) && !String.IsNullOrWhiteSpace(lastName)) - { - fullName = string.Format("{0} {1}", firstName, lastName); - } - else - { - if (!String.IsNullOrWhiteSpace(firstName)) - fullName = firstName; + if (firstName != null && lastName != null) + { + return firstName + " " + lastName; + } + else if (firstName != null) + { + return firstName; + } + else if (lastName != null) + { + return lastName; + } - if (!String.IsNullOrWhiteSpace(lastName)) - fullName = lastName; + string name = customer.BillingAddress?.GetFullName(); + if (name.IsEmpty()) + { + name = customer.ShippingAddress?.GetFullName(); + } + if (name.IsEmpty()) + { + name = customer.Addresses.FirstOrDefault()?.GetFullName(); + } - if (String.IsNullOrWhiteSpace(firstName) && String.IsNullOrWhiteSpace(lastName)) - { - var address = customer.Addresses.FirstOrDefault(); - if (address != null) - fullName = string.Format("{0} {1}", address.FirstName, address.LastName); - } - } - return fullName; - } + return name.TrimSafe(); + } /// /// Formats the customer name @@ -261,15 +266,11 @@ public static string FindEmail(this Customer customer) { if (customer != null) { - if (customer.Email.HasValue()) - return customer.Email; - - if (customer.BillingAddress != null && customer.BillingAddress.Email.HasValue()) - return customer.BillingAddress.Email; - - if (customer.ShippingAddress != null && customer.ShippingAddress.Email.HasValue()) - return customer.ShippingAddress.Email; + return customer.Email.NullEmpty() + ?? customer.BillingAddress?.Email?.NullEmpty() + ?? customer.ShippingAddress?.Email?.NullEmpty(); } + return null; } diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerMessageFactoryExtensions.cs b/src/Libraries/SmartStore.Services/Customers/CustomerMessageFactoryExtensions.cs new file mode 100644 index 0000000000..398e45d2a8 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Customers/CustomerMessageFactoryExtensions.cs @@ -0,0 +1,80 @@ +using System; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Messages; +using SmartStore.Services.Messages; + +namespace SmartStore.Services.Customers +{ + public static class CustomerMessageFactoryExtensions + { + /// + /// Sends 'New customer' notification message to a store owner + /// + public static CreateMessageResult SendCustomerRegisteredNotificationMessage(this IMessageFactory factory, Customer customer, int languageId = 0) + { + Guard.NotNull(customer, nameof(customer)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.CustomerRegistered, languageId, customer: customer), true); + } + + /// + /// Sends a welcome message to a customer + /// + public static CreateMessageResult SendCustomerWelcomeMessage(this IMessageFactory factory, Customer customer, int languageId = 0) + { + Guard.NotNull(customer, nameof(customer)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.CustomerWelcome, languageId, customer: customer), true); + } + + /// + /// Sends an email validation message to a customer + /// + public static CreateMessageResult SendCustomerEmailValidationMessage(this IMessageFactory factory, Customer customer, int languageId = 0) + { + Guard.NotNull(customer, nameof(customer)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.CustomerEmailValidation, languageId, customer: customer), true); + } + + /// + /// Sends password recovery message to a customer + /// + public static CreateMessageResult SendCustomerPasswordRecoveryMessage(this IMessageFactory factory, Customer customer, int languageId = 0) + { + Guard.NotNull(customer, nameof(customer)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.CustomerPasswordRecovery, languageId, customer: customer), true); + } + + /// + /// Sends wishlist "email a friend" message + /// + public static CreateMessageResult SendShareWishlistMessage(this IMessageFactory factory, Customer customer, + string fromEmail, string toEmail, string personalMessage, int languageId = 0) + { + Guard.NotNull(customer, nameof(customer)); + + var model = new NamedModelPart("Wishlist") + { + ["PersonalMessage"] = personalMessage, + ["From"] = fromEmail, + ["To"] = toEmail + }; + + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.ShareWishlist, languageId, customer: customer), true, model); + } + + /// + /// Sends a "new VAT sumitted" notification to a store owner + /// + public static CreateMessageResult SendNewVatSubmittedStoreOwnerNotification(this IMessageFactory factory, Customer customer, string vatName, string vatAddress, int languageId = 0) + { + Guard.NotNull(customer, nameof(customer)); + + var model = new NamedModelPart("VatValidationResult") + { + ["Name"] = vatName, + ["Address"] = vatAddress + }; + + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewVatSubmittedStoreOwner, languageId, customer: customer), true, model); + } + } +} diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerService.cs b/src/Libraries/SmartStore.Services/Customers/CustomerService.cs index d4c9729163..aec3a43344 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerService.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerService.cs @@ -440,8 +440,6 @@ public virtual void InsertCustomer(Customer customer) throw new ArgumentNullException("customer"); _customerRepository.Insert(customer); - - _services.EventPublisher.EntityInserted(customer); } public virtual void UpdateCustomer(Customer customer) @@ -450,8 +448,6 @@ public virtual void UpdateCustomer(Customer customer) throw new ArgumentNullException("customer"); _customerRepository.Update(customer); - - _services.EventPublisher.EntityUpdated(customer); } public virtual void ResetCheckoutData(Customer customer, int storeId, @@ -632,8 +628,6 @@ public virtual void DeleteCustomerRole(CustomerRole customerRole) throw new SmartException("System role could not be deleted"); _customerRoleRepository.Delete(customerRole); - - _services.EventPublisher.EntityDeleted(customerRole); } public virtual CustomerRole GetCustomerRoleById(int customerRoleId) @@ -675,8 +669,6 @@ public virtual void InsertCustomerRole(CustomerRole customerRole) throw new ArgumentNullException("customerRole"); _customerRoleRepository.Insert(customerRole); - - _services.EventPublisher.EntityInserted(customerRole); } public virtual void UpdateCustomerRole(CustomerRole customerRole) @@ -685,8 +677,6 @@ public virtual void UpdateCustomerRole(CustomerRole customerRole) throw new ArgumentNullException("customerRole"); _customerRoleRepository.Update(customerRole); - - _services.EventPublisher.EntityUpdated(customerRole); } #endregion diff --git a/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs b/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs index 633f36b97d..31a36989a4 100644 --- a/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs +++ b/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs @@ -96,9 +96,11 @@ protected override void Import(ImportExecuteContext context) _genericAttributeService.GetAttributes(SystemCustomerAttributeNames.CustomerNumber, _attributeKeyGroup).Select(x => x.Value), StringComparer.OrdinalIgnoreCase); - var allCustomerRoles = _customerRoleRepository.Table.ToDictionarySafe(x => x.SystemName, StringComparer.OrdinalIgnoreCase); + var allCustomerRoles = _customerRoleRepository.Table + .Where(x => !string.IsNullOrEmpty(x.SystemName)) + .ToDictionarySafe(x => x.SystemName, StringComparer.OrdinalIgnoreCase); - using (var scope = new DbContextScope(ctx: _services.DbContext, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) + using (var scope = new DbContextScope(ctx: _services.DbContext, hooksEnabled: false, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) { var segmenter = context.DataSegmenter; @@ -204,8 +206,6 @@ protected virtual int ProcessCustomers( { _customerRepository.AutoCommitEnabled = true; - Customer lastInserted = null; - Customer lastUpdated = null; var currentCustomer = _services.WorkContext.CurrentCustomer; var customerQuery = _customerRepository.Table.Expand(x => x.Addresses); var hasCustomerRoleSystemNames = context.DataSegmenter.HasColumn("CustomerRoleSystemNames"); @@ -307,27 +307,14 @@ protected virtual int ProcessCustomers( if (row.IsTransient) { _customerRepository.Insert(customer); - lastInserted = customer; } else { _customerRepository.Update(customer); - lastUpdated = customer; } } var num = _customerRepository.Context.SaveChanges(); - - if (lastInserted != null) - { - _services.EventPublisher.EntityInserted(lastInserted); - } - - if (lastUpdated != null) - { - _services.EventPublisher.EntityUpdated(lastUpdated); - } - return num; } @@ -619,7 +606,7 @@ protected virtual int ProcessAvatars( } var size = Size.Empty; - pictureBinary = _pictureService.ValidatePicture(pictureBinary, out size); + pictureBinary = _pictureService.ValidatePicture(pictureBinary, image.MimeType, out size); pictureBinary = _pictureService.FindEqualPicture(pictureBinary, currentPictures, out equalPictureId); if (pictureBinary != null && pictureBinary.Length > 0) diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportTask.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportTask.cs index f32f06c625..e6dcd55405 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportTask.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportTask.cs @@ -4,7 +4,7 @@ namespace SmartStore.Services.DataExchange.Export { - // note: namespace persisted in ScheduleTask.Type + // Note, namespace persisted in ScheduleTask.Type! public partial class DataExportTask : ITask { private readonly IDataExporter _exporter; @@ -25,12 +25,12 @@ public void Execute(TaskExecutionContext ctx) var profileId = ctx.ScheduleTask.Alias.ToInt(); var profile = _exportProfileService.GetExportProfileById(profileId); - // load provider + // Load provider. var provider = _exportProfileService.LoadProvider(profile.ProviderSystemName); if (provider == null) throw new SmartException(T("Admin.Common.ProviderNotLoaded", profile.ProviderSystemName.NaIfEmpty())); - // build export request + // Build export request. var request = new DataExportRequest(profile, provider); request.ProgressValueSetter = delegate (int val, int max, string msg) @@ -46,7 +46,12 @@ public void Execute(TaskExecutionContext ctx) .ToList(); } - // process! + if (ctx.Parameters.ContainsKey("ActionOrigin")) + { + request.ActionOrigin = ctx.Parameters["ActionOrigin"]; + } + + // Process! _exporter.Export(request, ctx.CancellationToken); } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs index 7f8a144956..3b5d1bf471 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs @@ -19,6 +19,7 @@ using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Domain.Tax; using SmartStore.Core.Email; using SmartStore.Core.Localization; using SmartStore.Core.Logging; @@ -64,7 +65,8 @@ public partial class DataExporter : IDataExporter private readonly Lazy _categoryService; private readonly Lazy _productAttributeParser; private readonly Lazy _productAttributeService; - private readonly Lazy _productTemplateService; + private readonly Lazy _specificationAttributeService; + private readonly Lazy _productTemplateService; private readonly Lazy _categoryTemplateService; private readonly Lazy _productService; private readonly Lazy _orderService; @@ -85,12 +87,14 @@ public partial class DataExporter : IDataExporter private readonly Lazy>_customerRepository; private readonly Lazy> _subscriptionRepository; private readonly Lazy> _orderRepository; + private readonly Lazy> _shoppingCartItemRepository; private readonly Lazy _mediaSettings; private readonly Lazy _contactDataSettings; private readonly Lazy _customerSettings; private readonly Lazy _catalogSettings; private readonly Lazy _localizationSettings; + private readonly Lazy _taxSettings; public DataExporter( ICommonServices services, @@ -108,7 +112,8 @@ public DataExporter( Lazy categoryService, Lazy productAttributeParser, Lazy productAttributeService, - Lazy productTemplateService, + Lazy specificationAttributeService, + Lazy productTemplateService, Lazy categoryTemplateService, Lazy productService, Lazy orderService, @@ -128,11 +133,13 @@ public DataExporter( Lazy> customerRepository, Lazy> subscriptionRepository, Lazy> orderRepository, + Lazy> shoppingCartItemRepository, Lazy mediaSettings, Lazy contactDataSettings, Lazy customerSettings, Lazy catalogSettings, - Lazy localizationSettings) + Lazy localizationSettings, + Lazy taxSettings) { _services = services; _dbContext = dbContext; @@ -149,6 +156,7 @@ public DataExporter( _categoryService = categoryService; _productAttributeParser = productAttributeParser; _productAttributeService = productAttributeService; + _specificationAttributeService = specificationAttributeService; _productTemplateService = productTemplateService; _categoryTemplateService = categoryTemplateService; _productService = productService; @@ -170,12 +178,14 @@ public DataExporter( _customerRepository = customerRepository; _subscriptionRepository = subscriptionRepository; _orderRepository = orderRepository; + _shoppingCartItemRepository = shoppingCartItemRepository; _mediaSettings = mediaSettings; _contactDataSettings = contactDataSettings; _customerSettings = customerSettings; _catalogSettings = catalogSettings; _localizationSettings = localizationSettings; + _taxSettings = taxSettings; T = NullLocalizer.Instance; } @@ -220,26 +230,34 @@ private void SetProgress(DataExporterContext ctx, string message) private bool HasPermission(DataExporterContext ctx) { if (ctx.Request.HasPermission) + { return true; + } var customer = _services.WorkContext.CurrentCustomer; if (customer.SystemName == SystemCustomerNames.BackgroundTask) + { return true; + } - if (ctx.Request.Provider.Value.EntityType == ExportEntityType.Product || - ctx.Request.Provider.Value.EntityType == ExportEntityType.Category || - ctx.Request.Provider.Value.EntityType == ExportEntityType.Manufacturer) - return _services.Permissions.Authorize(StandardPermissionProvider.ManageCatalog, customer); + switch (ctx.Request.Provider.Value.EntityType) + { + case ExportEntityType.Product: + case ExportEntityType.Category: + case ExportEntityType.Manufacturer: + return _services.Permissions.Authorize(StandardPermissionProvider.ManageCatalog, customer); - if (ctx.Request.Provider.Value.EntityType == ExportEntityType.Customer) - return _services.Permissions.Authorize(StandardPermissionProvider.ManageCustomers, customer); + case ExportEntityType.Customer: + return _services.Permissions.Authorize(StandardPermissionProvider.ManageCustomers, customer); - if (ctx.Request.Provider.Value.EntityType == ExportEntityType.Order) - return _services.Permissions.Authorize(StandardPermissionProvider.ManageOrders, customer); + case ExportEntityType.Order: + case ExportEntityType.ShoppingCartItem: + return _services.Permissions.Authorize(StandardPermissionProvider.ManageOrders, customer); - if (ctx.Request.Provider.Value.EntityType == ExportEntityType.NewsLetterSubscription) - return _services.Permissions.Authorize(StandardPermissionProvider.ManageNewsletterSubscribers, customer); + case ExportEntityType.NewsLetterSubscription: + return _services.Permissions.Authorize(StandardPermissionProvider.ManageNewsletterSubscribers, customer); + } return true; } @@ -300,6 +318,14 @@ x is Picture || x is ProductBundleItem || x is ProductCategory || x is ProductMa ctx.CustomerExportContext.Clear(); } + + if (ctx.Request.Provider.Value.EntityType == ExportEntityType.ShoppingCartItem) + { + _dbContext.DetachEntities(x => + { + return x is ShoppingCartItem || x is Customer || x is Product; + }); + } } catch (Exception ex) { @@ -310,7 +336,7 @@ x is Picture || x is ProductBundleItem || x is ProductCategory || x is ProductMa private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, int pageIndex = 0) { var offset = Math.Max(ctx.Request.Profile.Offset, 0) + (pageIndex * PageSize); - var limit = (ctx.IsPreview ? PageSize : Math.Max(ctx.Request.Profile.Limit, 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; @@ -407,6 +433,16 @@ private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, in ); break; + case ExportEntityType.ShoppingCartItem: + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter + ( + skip => GetShoppingCartItems(ctx, skip), + null, + entity => Convert(ctx, entity), + offset, PageSize, limit, recordsPerSegment, totalCount + ); + break; + default: ctx.ExecuteContext.DataSegmenter = null; break; @@ -631,7 +667,8 @@ public virtual ProductExportContext CreateProductExportContext( IEnumerable products = null, Customer customer = null, int? storeId = null, - int? maxPicturesPerProduct = null) + int? maxPicturesPerProduct = null, + bool showHidden = true) { if (customer == null) customer = _services.WorkContext.CurrentCustomer; @@ -642,12 +679,12 @@ public virtual ProductExportContext CreateProductExportContext( var context = new ProductExportContext(products, x => _productAttributeService.Value.GetProductVariantAttributesByProductIds(x, null), x => _productAttributeService.Value.GetProductVariantAttributeCombinations(x), - x => _productService.Value.GetProductSpecificationAttributesByProductIds(x), + x => _specificationAttributeService.Value.GetProductSpecificationAttributesByProductIds(x), x => _productService.Value.GetTierPricesByProductIds(x, customer, storeId.GetValueOrDefault()), - x => _categoryService.Value.GetProductCategoriesByProductIds(x, null, true), + x => _categoryService.Value.GetProductCategoriesByProductIds(x, null, showHidden), x => _manufacturerService.Value.GetProductManufacturersByProductIds(x), x => _productService.Value.GetAppliedDiscountsByProductIds(x), - x => _productService.Value.GetBundleItemsByProductIds(x, true), + x => _productService.Value.GetBundleItemsByProductIds(x, showHidden), x => _pictureService.Value.GetPicturesByProductIds(x, maxPicturesPerProduct, true), x => _productService.Value.GetProductPicturesByProductIds(x), x => _productService.Value.GetProductTagsByProductIds(x) @@ -736,7 +773,7 @@ private List GetProducts(DataExporterContext ctx, int skip) } else if (product.ProductType == ProductType.GroupedProduct) { - if (ctx.Projection.NoGroupedProducts && !ctx.IsPreview) + if (ctx.Projection.NoGroupedProducts) { var searchQuery = new CatalogSearchQuery() .HasParentGroupedProduct(product.Id) @@ -849,7 +886,7 @@ private List GetManufacturers(DataExporterContext ctx, int skip) private IQueryable GetCategoryQuery(DataExporterContext ctx, int skip, int take) { var storeId = ctx.Request.Profile.PerStore ? ctx.Store.Id : 0; - var query = _categoryService.Value.GetCategories(null, true, null, true, storeId); + var query = _categoryService.Value.BuildCategoriesQuery(null, true, null, storeId); if (ctx.Request.EntitiesToExport.Any()) query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); @@ -1013,6 +1050,100 @@ private List GetNewsLetterSubscriptions(DataExporterCont return subscriptions; } + private IQueryable GetShoppingCartItemQuery(DataExporterContext ctx, int skip, int take) + { + 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 + + if (storeId > 0) + query = query.Where(x => x.StoreId == storeId); + + if (ctx.Request.ActionOrigin.IsCaseInsensitiveEqual("CurrentCarts")) + { + query = query.Where(x => x.ShoppingCartTypeId == (int)ShoppingCartType.ShoppingCart); + } + else if (ctx.Request.ActionOrigin.IsCaseInsensitiveEqual("CurrentWishlists")) + { + query = query.Where(x => x.ShoppingCartTypeId == (int)ShoppingCartType.Wishlist); + } + else if (ctx.Filter.ShoppingCartTypeId.HasValue) + { + 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.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.LastActivityFrom.HasValue) + { + var activityFrom = _services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.LastActivityFrom.Value, _services.DateTimeHelper.CurrentTimeZone); + query = query.Where(x => activityFrom <= x.Customer.LastActivityDateUtc); + } + + if (ctx.Filter.LastActivityTo.HasValue) + { + var activityTo = _services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.LastActivityTo.Value, _services.DateTimeHelper.CurrentTimeZone); + query = query.Where(x => activityTo >= x.Customer.LastActivityDateUtc); + } + + if (ctx.Filter.CreatedFrom.HasValue) + { + var createdFrom = _services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.CreatedFrom.Value, _services.DateTimeHelper.CurrentTimeZone); + query = query.Where(x => createdFrom <= x.CreatedOnUtc); + } + + if (ctx.Filter.CreatedTo.HasValue) + { + var createdTo = _services.DateTimeHelper.ConvertToUtcTime(ctx.Filter.CreatedTo.Value, _services.DateTimeHelper.CurrentTimeZone); + query = query.Where(x => createdTo >= x.CreatedOnUtc); + } + + if (ctx.Projection.NoBundleProducts) + { + query = query.Where(x => x.Product.ProductTypeId != (int)ProductType.BundledProduct); + } + else + { + query = query.Where(x => x.BundleItemId == null); + } + + 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); + + if (skip > 0) + query = query.Skip(skip); + + if (take != int.MaxValue) + query = query.Take(take); + + return query; + } + + private List GetShoppingCartItems(DataExporterContext ctx, int skip) + { + var shoppingCartItems = GetShoppingCartItemQuery(ctx, skip, PageSize).ToList(); + + SetProgress(ctx, shoppingCartItems.Count); + + return shoppingCartItems; + } + #endregion private List Init(DataExporterContext ctx, int? totalRecords = null) @@ -1084,6 +1215,9 @@ private List Init(DataExporterContext ctx, int? totalRecords = null) 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; } } @@ -1278,12 +1412,6 @@ private void ExportCoreOuter(DataExporterContext ctx) 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) - { - var allCategories = _categoryService.Value.GetAllCategories(showHidden: true, applyNavigationFilters: false); - ctx.Categories = allCategories.ToDictionary(x => x.Id); - } - if (ctx.Request.Provider.Value.EntityType == ExportEntityType.Product || ctx.Request.Provider.Value.EntityType == ExportEntityType.Order) { @@ -1306,6 +1434,7 @@ private void ExportCoreOuter(DataExporterContext ctx) ctx.ExecuteContext.Language = ToDynamic(ctx, ctx.ContextLanguage); ctx.ExecuteContext.Customer = ToDynamic(ctx, ctx.ContextCustomer); ctx.ExecuteContext.Currency = ToDynamic(ctx, ctx.ContextCurrency); + ctx.ExecuteContext.Profile = ToDynamic(ctx, ctx.Request.Profile); stores.ForEach(x => ExportCoreInner(ctx, x)); } @@ -1372,8 +1501,6 @@ private void ExportCoreOuter(DataExporterContext ctx) ctx.Languages.Clear(); ctx.QuantityUnits.Clear(); ctx.DeliveryTimes.Clear(); - ctx.CategoryPathes.Clear(); - ctx.Categories.Clear(); ctx.Stores.Clear(); ctx.Request.CustomData.Clear(); @@ -1392,7 +1519,7 @@ private void ExportCoreOuter(DataExporterContext ctx) if (ctx.IsPreview || ctx.ExecuteContext.Abort == DataExchangeAbortion.Hard) return; - // post process order entities + // Post process order entities. if (ctx.EntityIdsLoaded.Any() && ctx.Request.Provider.Value.EntityType == ExportEntityType.Order && ctx.Projection.OrderStatusChange != ExportOrderStatusChange.None) { using (var logger = new TraceLogger(logPath)) @@ -1408,7 +1535,7 @@ private void ExportCoreOuter(DataExporterContext ctx) using (var scope = new DbContextScope(_dbContext, false, null, false, false, false, false)) { - foreach (var chunk in ctx.EntityIdsLoaded.Chunk()) + foreach (var chunk in ctx.EntityIdsLoaded.Slice(128)) { var entities = _orderRepository.Value.Table.Where(x => chunk.Contains(x.Id)).ToList(); @@ -1432,15 +1559,9 @@ private void ExportCoreOuter(DataExporterContext ctx) /// /// The name of the public export folder /// - public static string PublicFolder - { - get { return "Exchange"; } - } + public static string PublicFolder => "Exchange"; - public static int PageSize - { - get { return 100; } - } + public static int PageSize => 100; public DataExportResult Export(DataExportRequest request, CancellationToken cancellationToken) { @@ -1455,54 +1576,74 @@ public DataExportResult Export(DataExportRequest request, CancellationToken canc public IList Preview(DataExportRequest request, int pageIndex, int? totalRecords = null) { - var resultData = new List(); + var result = new List(); 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); if (!HasPermission(ctx)) + { throw new SmartException(T("Admin.AccessDenied")); + } - using (var segmenter = CreateSegmenter(ctx, pageIndex)) + switch (request.Provider.Value.EntityType) { - if (segmenter == null) - { - throw new SmartException(T("Admin.Common.UnsupportedEntityType", ctx.Request.Provider.Value.EntityType.ToString())); - } - - while (segmenter.HasData) - { - segmenter.RecordPerSegmentCount = 0; - - while (segmenter.ReadNextSegment()) + case ExportEntityType.Product: { - resultData.AddRange(segmenter.CurrentSegment); + var items = GetProductQuery(ctx, offset, PageSize).ToList(); + items.Each(x => result.Add(ToDynamic(ctx, x))); } - } - - DetachAllEntitiesAndClear(ctx); - } - - if (ctx.Result.LastError.HasValue()) - { - _services.Notifier.Error(ctx.Result.LastError); + break; + case ExportEntityType.Order: + { + var items = GetOrderQuery(ctx, offset, PageSize).ToList(); + items.Each(x => result.Add(ToDynamic(ctx, x))); + } + break; + case ExportEntityType.Category: + { + var items = GetCategoryQuery(ctx, offset, PageSize).ToList(); + items.Each(x => result.Add(ToDynamic(ctx, x))); + } + break; + case ExportEntityType.Manufacturer: + { + var items = GetManufacturerQuery(ctx, offset, PageSize).ToList(); + items.Each(x => result.Add(ToDynamic(ctx, x))); + } + break; + case ExportEntityType.Customer: + { + var items = GetCustomerQuery(ctx, offset, PageSize).ToList(); + items.Each(x => result.Add(ToDynamic(ctx, x))); + } + break; + case ExportEntityType.NewsLetterSubscription: + { + var items = GetNewsLetterSubscriptionQuery(ctx, offset, PageSize).ToList(); + items.Each(x => result.Add(ToDynamic(ctx, x))); + } + break; + case ExportEntityType.ShoppingCartItem: + { + var items = GetShoppingCartItemQuery(ctx, offset, PageSize).ToList(); + items.Each(x => result.Add(ToDynamic(ctx, x))); + } + break; } - return resultData; + 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; } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/EmailFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/EmailFilePublisher.cs index 99c450679b..abeb1514d0 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/EmailFilePublisher.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/EmailFilePublisher.cs @@ -34,8 +34,7 @@ public virtual void Publish(ExportDeploymentContext context, ExportDeployment de { var queuedEmail = new QueuedEmail { - From = emailAccount.Email, - FromName = emailAccount.DisplayName, + From = emailAccount.ToEmailAddress(), SendManually = false, To = email, Subject = deployment.EmailSubject.NaIfEmpty(), diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs index 251a75c33b..24ce121d28 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs @@ -5,8 +5,10 @@ using System.Linq.Expressions; using System.Reflection; using System.Web; +using SmartStore.Collections; using SmartStore.ComponentModel; using SmartStore.Core; +using SmartStore.Core.Domain; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; @@ -26,6 +28,7 @@ using SmartStore.Services.DataExchange.Export.Events; using SmartStore.Services.DataExchange.Export.Internal; using SmartStore.Services.Localization; +using SmartStore.Services.Media; using SmartStore.Services.Seo; namespace SmartStore.Services.DataExchange.Export @@ -137,7 +140,8 @@ private void PrepareProductDescription(DataExporterContext ctx, dynamic dynObjec if (ctx.Projection.ConvertNetToGrossPrices) { decimal taxRate; - price = _taxService.Value.GetProductPrice(product, price.Value, true, ctx.ContextCustomer, out taxRate); + price = _taxService.Value.GetProductPrice(product, product.TaxCategoryId, price.Value, true, ctx.ContextCustomer, ctx.ContextCurrency, + _taxSettings.Value.PricesIncludeTax, out taxRate); } if (price != decimal.Zero) @@ -145,6 +149,7 @@ private void PrepareProductDescription(DataExporterContext ctx, dynamic dynObjec price = _currencyService.Value.ConvertFromPrimaryStoreCurrency(price.Value, ctx.ContextCurrency, ctx.Store); } } + return price; } @@ -179,7 +184,7 @@ private decimal CalculatePrice( } else if (ctx.Projection.PriceType.Value == PriceDisplayType.PreSelectedPrice) { - price = _priceCalculationService.Value.GetPreselectedPrice(product, ctx.ContextCustomer, priceCalculationContext); + price = _priceCalculationService.Value.GetPreselectedPrice(product, ctx.ContextCustomer, ctx.ContextCurrency, priceCalculationContext); } else if (ctx.Projection.PriceType.Value == PriceDisplayType.PriceWithoutDiscountsAndAttributes) { @@ -243,6 +248,15 @@ private List GetLocalized(DataExporterContext ctx, T entity, params return (localized.Count == 0 ? null : localized); } + private dynamic ToDynamic(DataExporterContext ctx, ExportProfile profile) + { + if (profile == null) + return null; + + dynamic result = new DynamicEntity(profile); + return result; + } + private dynamic ToDynamic(DataExporterContext ctx, Currency currency) { if (currency == null) @@ -324,7 +338,6 @@ private dynamic ToDynamic(DataExporterContext ctx, Customer customer) result.BillingAddress = null; result.ShippingAddress = null; result.Addresses = null; - result.CustomerRoles = null; result.RewardPointsHistory = null; result._RewardPointsBalance = 0; @@ -335,6 +348,14 @@ private dynamic ToDynamic(DataExporterContext ctx, Customer customer) result._FullName = null; result._AvatarPictureUrl = null; + result.CustomerRoles = customer.CustomerRoles + .Select(x => + { + dynamic dyn = new DynamicEntity(x); + return dyn; + }) + .ToList(); + return result; } @@ -408,16 +429,22 @@ private dynamic ToDynamic(DataExporterContext ctx, Picture picture, int thumbPic if (picture == null) return null; + // TODO: (mc) Refactor > GetPictureInfo + dynamic result = new DynamicEntity(picture); - var relativeUrl = _pictureService.Value.GetPictureUrl(picture, 0, false); + var pictureInfo = _pictureService.Value.GetPictureInfo(picture); + var host = _services.StoreService.GetHost(ctx.Store); - result._FileName = relativeUrl.Substring(relativeUrl.LastIndexOf("/") + 1); - result._RelativeUrl = relativeUrl; - result._ThumbImageUrl = _pictureService.Value.GetPictureUrl(picture, thumbPictureSize, false, ctx.Store.Url); - result._ImageUrl = _pictureService.Value.GetPictureUrl(picture, detailsPictureSize, false, ctx.Store.Url); - result._FullSizeImageUrl = _pictureService.Value.GetPictureUrl(picture, 0, false, ctx.Store.Url); + if (pictureInfo != null) + { + result._FileName = System.IO.Path.GetFileName(pictureInfo.Path); + result._RelativeUrl = _pictureService.Value.GetUrl(pictureInfo, 0, FallbackPictureType.NoFallback); + 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); + //result._ThumbLocalPath = _pictureService.Value.GetThumbLocalPath(picture); + } return result; } @@ -476,21 +503,24 @@ private dynamic ToDynamic(DataExporterContext ctx, Manufacturer manufacturer) dynamic result = new DynamicEntity(manufacturer); - result.Name = manufacturer.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - 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.Picture = null; + result.Name = manufacturer.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - result._Localized = GetLocalized(ctx, manufacturer, - x => x.Name, - x => x.Description, - x => x.MetaKeywords, - x => x.MetaDescription, - x => x.MetaTitle); + 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._Localized = GetLocalized(ctx, manufacturer, + x => x.Name, + x => x.Description, + x => x.MetaKeywords, + x => x.MetaDescription, + x => x.MetaTitle); + } return result; } @@ -502,50 +532,43 @@ private dynamic ToDynamic(DataExporterContext ctx, Category category) dynamic result = new DynamicEntity(category); + result.Picture = null; result.Name = category.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - result.SeName = category.GetSeName(ctx.Projection.LanguageId ?? 0, true, false); result.FullName = category.GetLocalized(x => x.FullName, 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.Picture = null; - if (ctx.CategoryTemplates.ContainsKey(category.CategoryTemplateId)) - result._CategoryTemplateViewPath = ctx.CategoryTemplates[category.CategoryTemplateId]; - else - result._CategoryTemplateViewPath = ""; - - result._Localized = GetLocalized(ctx, category, - x => x.Name, - x => x.FullName, - x => x.Description, - x => x.BottomDescription, - x => x.MetaKeywords, - x => x.MetaDescription, - x => x.MetaTitle); + 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._CategoryTemplateViewPath = ctx.CategoryTemplates.ContainsKey(category.CategoryTemplateId) + ? ctx.CategoryTemplates[category.CategoryTemplateId] + : ""; + + result._Localized = GetLocalized(ctx, category, + x => x.Name, + x => x.FullName, + x => x.Description, + x => x.BottomDescription, + x => x.MetaKeywords, + x => x.MetaDescription, + x => x.MetaTitle); + } return result; } - private dynamic ToDynamic(DataExporterContext ctx, Product product) + private dynamic ToDynamic(DataExporterContext ctx, Product product, string seName = null) { if (product == null) return null; dynamic result = new DynamicEntity(product); - result.Name = product.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - result.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); - 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.AppliedDiscounts = null; result.TierPrices = null; result.ProductAttributes = null; @@ -557,119 +580,139 @@ private dynamic ToDynamic(DataExporterContext ctx, Product product) result.ProductSpecificationAttributes = null; result.ProductBundleItems = null; - result._Localized = GetLocalized(ctx, product, - x => x.Name, - x => x.ShortDescription, - x => x.FullDescription, - x => x.MetaKeywords, - x => x.MetaDescription, - x => x.MetaTitle, - x => x.BundleTitleText); + result.Name = product.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); + + 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._ProductTemplateViewPath = ctx.ProductTemplates.ContainsKey(product.ProductTemplateId) + ? ctx.ProductTemplates[product.ProductTemplateId] + : ""; + + result._BasePriceInfo = product.GetBasePriceInfo(_services.Localization, _priceFormatter.Value, _currencyService.Value, _taxService.Value, + _priceCalculationService.Value, ctx.ContextCustomer, ctx.ContextCurrency, decimal.Zero, true); + + ToDeliveryTime(ctx, result, product.DeliveryTimeId); + ToQuantityUnit(ctx, result, product.QuantityUnitId); + + result._Localized = GetLocalized(ctx, product, + x => x.Name, + x => x.ShortDescription, + x => x.FullDescription, + x => x.MetaKeywords, + x => x.MetaDescription, + x => x.MetaTitle, + x => x.BundleTitleText); + } return result; } - private dynamic ToDynamic( - DataExporterContext ctx, - Product product, - ICollection combinations, - ProductVariantAttributeCombination combination, - bool isParent) + private dynamic ToDynamic(DataExporterContext ctx, Product product, bool isParent, DynamicProductContext productContext) { - product.MergeWithCombination(combination); + product.MergeWithCombination(productContext.Combination); - var languageId = (ctx.Projection.LanguageId ?? 0); - var numberOfPictures = (ctx.Projection.NumberOfPictures ?? int.MaxValue); - int[] pictureIds = (combination == null ? new int[0] : combination.GetAssignedPictureIds()); + 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; - var perfLoadId = (ctx.IsPreview ? 0 : product.Id); // perf preview (it's a compromise) - IEnumerable productPictures = ctx.ProductExportContext.ProductPictures.GetOrLoad(perfLoadId); - var productManufacturers = ctx.ProductExportContext.ProductManufacturers.GetOrLoad(perfLoadId); + IEnumerable productPictures = ctx.ProductExportContext.ProductPictures.GetOrLoad(product.Id); + var productManufacturers = ctx.ProductExportContext.ProductManufacturers.GetOrLoad(product.Id); var productCategories = ctx.ProductExportContext.ProductCategories.GetOrLoad(product.Id); var productAttributes = ctx.ProductExportContext.Attributes.GetOrLoad(product.Id); var productTags = ctx.ProductExportContext.ProductTags.GetOrLoad(product.Id); var specificationAttributes = ctx.ProductExportContext.SpecificationAttributes.GetOrLoad(product.Id); - - var variantAttributes = (combination != null ? _productAttributeParser.Value.DeserializeProductVariantAttributes(combination.AttributesXml) : null); - var variantAttributeValues = (combination != null ? _productAttributeParser.Value.ParseProductVariantAttributeValues(variantAttributes, productAttributes) : null); - - if (pictureIds.Length > 0) - productPictures = productPictures.Where(x => pictureIds.Contains(x.PictureId)); - - productPictures = productPictures.Take(numberOfPictures); - - dynamic dynObject = ToDynamic(ctx, product); - - #region gerneral data - - dynObject._IsParent = isParent; - dynObject._CategoryName = null; - dynObject._CategoryPath = null; - dynObject._AttributeCombination = null; - dynObject._AttributeCombinationValues = null; - dynObject._AttributeCombinationId = (combination == null ? 0 : combination.Id); - dynObject._DetailUrl = _productUrlHelper.Value.GetAbsoluteProductUrl( - product.Id, - (string)dynObject.SeName, - combination != null ? combination.AttributesXml : null, - ctx.Store, - ctx.ContextLanguage); - - if (combination == null) - dynObject._UniqueId = product.Id.ToString(); - else - dynObject._UniqueId = string.Concat(product.Id, "-", combination.Id); - - dynObject.Price = CalculatePrice(ctx, product, combination, variantAttributeValues); - - dynObject._BasePriceInfo = product.GetBasePriceInfo(_services.Localization, _priceFormatter.Value, _currencyService.Value, _taxService.Value, - _priceCalculationService.Value, ctx.ContextCurrency, decimal.Zero, true); - - if (ctx.ProductTemplates.ContainsKey(product.ProductTemplateId)) - dynObject._ProductTemplateViewPath = ctx.ProductTemplates[product.ProductTemplateId]; - else - dynObject._ProductTemplateViewPath = ""; - - if (combination != null) + Multimap variantAttributes = null; + ICollection variantAttributeValues = null; + string attributesXml = null; + + dynamic dynObject = ToDynamic(ctx, product, productContext.SeName); + dynObject._IsParent = isParent; + dynObject._CategoryName = null; + dynObject._CategoryPath = null; + dynObject._AttributeCombination = null; + dynObject._AttributeCombinationValues = null; + dynObject._AttributeCombinationId = 0; + + if (productContext.Combination != null) + { + var pictureIds = productContext.Combination.GetAssignedPictureIds(); + productPictures = productPictures.Where(x => pictureIds.Contains(x.PictureId)); + + attributesXml = productContext.Combination.AttributesXml; + variantAttributes = _productAttributeParser.Value.DeserializeProductVariantAttributes(attributesXml); + variantAttributeValues = _productAttributeParser.Value.ParseProductVariantAttributeValues(variantAttributes, productAttributes); + + dynObject._AttributeCombinationId = productContext.Combination.Id; + dynObject._UniqueId = string.Concat(product.Id, "-", productContext.Combination.Id); + + if (ctx.Supports(ExportFeatures.UsesAttributeCombination)) + { + dynObject._AttributeCombination = variantAttributes; + dynObject._AttributeCombinationValues = variantAttributeValues; + } + + if (ctx.Projection.AttributeCombinationValueMerging == ExportAttributeValueMerging.AppendAllValuesToName) + { + var valueNames = variantAttributeValues + .Select(x => x.GetLocalized(y => y.Name, languageId, true, false)) + .ToList(); + + dynObject.Name = ((string)dynObject.Name).Grow(string.Join(", ", valueNames), " "); + } + } + else + { + dynObject._UniqueId = product.Id.ToString(); + } + + productPictures = productPictures.Take(numberOfPictures); + + #region Gerneral data + + if (attributesXml.HasValue()) + { + var query = new ProductVariantQuery(); + _productUrlHelper.Value.DeserializeQuery(query, product.Id, attributesXml, 0, productAttributes); + + dynObject._DetailUrl = productContext.AbsoluteProductUrl + _productUrlHelper.Value.ToQueryString(query); + } + else + { + dynObject._DetailUrl = productContext.AbsoluteProductUrl; + } + + dynObject.Price = CalculatePrice(ctx, product, productContext.Combination, variantAttributeValues); + + // Category path { - if (ctx.Supports(ExportFeatures.UsesAttributeCombination)) - { - dynObject._AttributeCombination = variantAttributes; - dynObject._AttributeCombinationValues = variantAttributeValues; - } + var categoryPath = string.Empty; + var pc = productCategories.OrderBy(x => x.DisplayOrder).FirstOrDefault(); - if (ctx.Projection.AttributeCombinationValueMerging == ExportAttributeValueMerging.AppendAllValuesToName) + if (pc != null) { - var valueNames = variantAttributeValues - .Select(x => x.GetLocalized(y => y.Name, languageId, true, false)) - .ToList(); - - dynObject.Name = ((string)dynObject.Name).Grow(string.Join(", ", valueNames), " "); + var node = _categoryService.Value.GetCategoryTree(pc.CategoryId, true, ctx.Store.Id); + if (node != null) + { + categoryPath = _categoryService.Value.GetCategoryPath(node, ctx.Projection.LanguageId, false, " > "); + } } - } - if (ctx.Categories.Count > 0) - { - dynObject._CategoryPath = _categoryService.Value.GetCategoryPath( - product, - null, - x => ctx.CategoryPathes.ContainsKey(x) ? ctx.CategoryPathes[x] : null, - (id, value) => ctx.CategoryPathes[id] = value, - x => ctx.Categories.ContainsKey(x) ? ctx.Categories[x] : _categoryService.Value.GetCategoryById(x), - productCategories.OrderBy(x => x.DisplayOrder).FirstOrDefault() - ); + dynObject._CategoryPath = categoryPath; } - ToDeliveryTime(ctx, dynObject, product.DeliveryTimeId); - ToQuantityUnit(ctx, dynObject, product.QuantityUnitId); - if (ctx.Countries != null) { - if (product.CountryOfOriginId.HasValue && ctx.Countries.ContainsKey(product.CountryOfOriginId.Value)) - dynObject.CountryOfOrigin = ToDynamic(ctx, ctx.Countries[product.CountryOfOriginId.Value]); - else - dynObject.CountryOfOrigin = null; + dynObject.CountryOfOrigin = product.CountryOfOriginId.HasValue && ctx.Countries.ContainsKey(product.CountryOfOriginId.Value) + ? ToDynamic(ctx, ctx.Countries[product.CountryOfOriginId.Value]) + : null; } dynObject.ProductPictures = productPictures @@ -692,10 +735,9 @@ private dynamic ToDynamic( dyn.Manufacturer = ToDynamic(ctx, x.Manufacturer); - if (x.Manufacturer != null && x.Manufacturer.PictureId.HasValue) - dyn.Manufacturer.Picture = ToDynamic(ctx, x.Manufacturer.Picture, _mediaSettings.Value.ManufacturerThumbPictureSize, _mediaSettings.Value.ManufacturerThumbPictureSize); - else - dyn.Manufacturer.Picture = null; + dyn.Manufacturer.Picture = x.Manufacturer != null && x.Manufacturer.PictureId.HasValue + ? ToDynamic(ctx, x.Manufacturer.Picture, _mediaSettings.Value.ManufacturerThumbPictureSize, _mediaSettings.Value.ManufacturerThumbPictureSize) + : null; return dyn; }) @@ -724,26 +766,34 @@ private dynamic ToDynamic( .Select(x => ToDynamic(ctx, x)) .ToList(); - dynObject.ProductAttributeCombinations = (combinations ?? Enumerable.Empty()) - .Select(x => - { - dynamic dyn = ToDynamic(ctx, x); - var assignedPictures = new List(); - - foreach (int pictureId in x.GetAssignedPictureIds().Take(numberOfPictures)) + // Do not export combinations if a combination is exported as a product. + if (productContext.Combinations != null && productContext.Combination == null) + { + dynObject.ProductAttributeCombinations = productContext.Combinations + .Select(x => { - var assignedPicture = productPictures.FirstOrDefault(y => y.PictureId == pictureId); - if (assignedPicture != null && assignedPicture.Picture != null) + dynamic dyn = ToDynamic(ctx, x); + var assignedPictures = new List(); + + foreach (int pictureId in x.GetAssignedPictureIds().Take(numberOfPictures)) { - assignedPictures.Add(ToDynamic(ctx, assignedPicture.Picture, _mediaSettings.Value.ProductThumbPictureSize, productDetailsPictureSize)); + var assignedPicture = productPictures.FirstOrDefault(y => y.PictureId == pictureId); + if (assignedPicture != null && assignedPicture.Picture != null) + { + assignedPictures.Add(ToDynamic(ctx, assignedPicture.Picture, _mediaSettings.Value.ProductThumbPictureSize, productDetailsPictureSize)); + } } - } - dyn.Pictures = assignedPictures; + dyn.Pictures = assignedPictures; - return dyn; - }) - .ToList(); + return dyn; + }) + .ToList(); + } + else + { + dynObject.ProductAttributeCombinations = Enumerable.Empty(); + } if (product.HasTierPrices) { @@ -788,7 +838,7 @@ private dynamic ToDynamic( if (product.ProductType == ProductType.BundledProduct) { - var bundleItems = ctx.ProductExportContext.ProductBundleItems.GetOrLoad(perfLoadId); + var bundleItems = ctx.ProductExportContext.ProductBundleItems.GetOrLoad(product.Id); dynObject.ProductBundleItems = bundleItems .Select(x => @@ -806,7 +856,7 @@ private dynamic ToDynamic( #endregion - #region more attribute controlled data + #region More data based on export features if (ctx.Supports(ExportFeatures.CanProjectDescription)) { @@ -816,7 +866,7 @@ private dynamic ToDynamic( if (ctx.Supports(ExportFeatures.OffersBrandFallback)) { string brand = null; - var productManus = ctx.ProductExportContext.ProductManufacturers.GetOrLoad(perfLoadId); + var productManus = ctx.ProductExportContext.ProductManufacturers.GetOrLoad(product.Id); if (productManus != null && productManus.Any()) brand = productManus.First().Manufacturer.GetLocalized(x => x.Name, languageId, true, false); @@ -826,19 +876,19 @@ private dynamic ToDynamic( dynObject._Brand = brand; } - + if (ctx.Supports(ExportFeatures.CanIncludeMainPicture)) { if (productPictures != null && productPictures.Any()) { var firstPicture = productPictures.First().Picture; - dynObject._MainPictureUrl = _pictureService.Value.GetPictureUrl(firstPicture, ctx.Projection.PictureSize, storeLocation: ctx.Store.Url); - dynObject._MainPictureRelativeUrl = _pictureService.Value.GetPictureUrl(firstPicture, ctx.Projection.PictureSize); + dynObject._MainPictureUrl = _pictureService.Value.GetUrl(firstPicture, ctx.Projection.PictureSize, host: _services.StoreService.GetHost(ctx.Store)); + dynObject._MainPictureRelativeUrl = _pictureService.Value.GetUrl(firstPicture, ctx.Projection.PictureSize); } else if (!_catalogSettings.Value.HideProductDefaultPictures) { - dynObject._MainPictureUrl = _pictureService.Value.GetDefaultPictureUrl(ctx.Projection.PictureSize, storeLocation: ctx.Store.Url); - dynObject._MainPictureRelativeUrl = _pictureService.Value.GetDefaultPictureUrl(ctx.Projection.PictureSize); + dynObject._MainPictureUrl = _pictureService.Value.GetFallbackUrl(ctx.Projection.PictureSize, host: _services.StoreService.GetHost(ctx.Store)); + dynObject._MainPictureRelativeUrl = _pictureService.Value.GetFallbackUrl(ctx.Projection.PictureSize); } else { @@ -875,7 +925,8 @@ private dynamic ToDynamic( if (ctx.Projection.ConvertNetToGrossPrices) { decimal taxRate; - dynObject._OldPrice = _taxService.Value.GetProductPrice(product, product.OldPrice, true, ctx.ContextCustomer, out taxRate); + dynObject._OldPrice = _taxService.Value.GetProductPrice(product, product.TaxCategoryId, product.OldPrice, true, ctx.ContextCustomer, + ctx.ContextCurrency, _taxSettings.Value.PricesIncludeTax, out taxRate); } else { @@ -914,7 +965,7 @@ private dynamic ToDynamic( decimal tmpSpecialPrice = product.SpecialPrice.Value; product.SpecialPrice = null; - dynObject._RegularPrice = CalculatePrice(ctx, product, combination, variantAttributeValues); + dynObject._RegularPrice = CalculatePrice(ctx, product, productContext.Combination, variantAttributeValues); product.SpecialPrice = tmpSpecialPrice; } @@ -941,10 +992,16 @@ private dynamic ToDynamic(DataExporterContext ctx, Order order) result.Customer = null; result.BillingAddress = null; result.ShippingAddress = null; - result.Store = null; result.Shipments = null; - result.RedeemedRewardPointsEntry = ToDynamic(ctx, order.RedeemedRewardPointsEntry); + result.Store = ctx.Stores.ContainsKey(order.StoreId) + ? ToDynamic(ctx, ctx.Stores[order.StoreId]) + : null; + + if (!ctx.IsPreview) + { + result.RedeemedRewardPointsEntry = ToDynamic(ctx, order.RedeemedRewardPointsEntry); + } return result; } @@ -1041,26 +1098,58 @@ private dynamic ToDynamic(DataExporterContext ctx, NewsLetterSubscription subscr dynamic result = new DynamicEntity(subscription); + result.Store = ctx.Stores.ContainsKey(subscription.StoreId) + ? ToDynamic(ctx, ctx.Stores[subscription.StoreId]) + : null; + + return result; + } + + private dynamic ToDynamic(DataExporterContext ctx, ShoppingCartItem shoppingCartItem) + { + if (shoppingCartItem == null) + return null; + + dynamic result = new DynamicEntity(shoppingCartItem); + + shoppingCartItem.Product.MergeWithCombination(shoppingCartItem.AttributesXml, _productAttributeParser.Value); + + result.Store = ctx.Stores.ContainsKey(shoppingCartItem.StoreId) + ? ToDynamic(ctx, ctx.Stores[shoppingCartItem.StoreId]) + : null; + + result.Customer = ToDynamic(ctx, shoppingCartItem.Customer); + result.Product = ToDynamic(ctx, shoppingCartItem.Product); + return result; } private List Convert(DataExporterContext ctx, Product product) { - var result = new List(); - var combinations = ctx.ProductExportContext.AttributeCombinations.GetOrLoad(product.Id); - - if (!ctx.IsPreview && ctx.Projection.AttributeCombinationAsProduct && combinations.Where(x => x.IsActive).Count() > 0) + var result = new List(); + var productContext = new DynamicProductContext(); + productContext.SeName = product.GetSeName(ctx.Projection.LanguageId ?? 0, true, false); + productContext.Combinations = ctx.ProductExportContext.AttributeCombinations.GetOrLoad(product.Id); + + productContext.AbsoluteProductUrl = _productUrlHelper.Value.GetAbsoluteProductUrl( + product.Id, + productContext.SeName, + null, + ctx.Store, + ctx.ContextLanguage); + + if (ctx.Projection.AttributeCombinationAsProduct && productContext.Combinations.Where(x => x.IsActive).Any()) { if (ctx.Supports(ExportFeatures.UsesAttributeCombinationParent)) { - var dynObject = ToDynamic(ctx, product, combinations, null, true); + var dynObject = ToDynamic(ctx, product, true, productContext); result.Add(dynObject); } var dbContext = _dbContext as DbContext; - foreach (var combination in combinations.Where(x => x.IsActive)) + foreach (var combination in productContext.Combinations.Where(x => x.IsActive)) { product = _dbContext.Attach(product); var entry = dbContext.Entry(product); @@ -1071,13 +1160,15 @@ private List Convert(DataExporterContext ctx, Product product) var productClone = entry.CurrentValues.ToObject() as Product; _dbContext.DetachEntity(product); - var dynObject = ToDynamic(ctx, productClone, combinations, combination, false); + productContext.Combination = combination; + + var dynObject = ToDynamic(ctx, productClone, false, productContext); result.Add(dynObject); } } else { - var dynObject = ToDynamic(ctx, product, combinations, null, false); + var dynObject = ToDynamic(ctx, product, false, productContext); result.Add(dynObject); } @@ -1099,29 +1190,20 @@ private List Convert(DataExporterContext ctx, Order order) { var result = new List(); - if (!ctx.IsPreview) - { - ctx.OrderExportContext.Addresses.Collect(order.ShippingAddressId.HasValue ? order.ShippingAddressId.Value : 0); - ctx.OrderExportContext.Addresses.GetOrLoad(order.BillingAddressId); - } + ctx.OrderExportContext.Addresses.Collect(order.ShippingAddressId.HasValue ? order.ShippingAddressId.Value : 0); + ctx.OrderExportContext.Addresses.GetOrLoad(order.BillingAddressId); - var perfLoadId = (ctx.IsPreview ? 0 : order.Id); var customers = ctx.OrderExportContext.Customers.GetOrLoad(order.CustomerId); - var genericAttributes = ctx.OrderExportContext.CustomerGenericAttributes.GetOrLoad(ctx.IsPreview ? 0 : order.CustomerId); - var rewardPointsHistories = ctx.OrderExportContext.RewardPointsHistories.GetOrLoad(ctx.IsPreview ? 0 : order.CustomerId); - var orderItems = ctx.OrderExportContext.OrderItems.GetOrLoad(perfLoadId); - var shipments = ctx.OrderExportContext.Shipments.GetOrLoad(perfLoadId); + var genericAttributes = ctx.OrderExportContext.CustomerGenericAttributes.GetOrLoad(order.CustomerId); + var rewardPointsHistories = ctx.OrderExportContext.RewardPointsHistories.GetOrLoad(order.CustomerId); + var orderItems = ctx.OrderExportContext.OrderItems.GetOrLoad(order.Id); + var shipments = ctx.OrderExportContext.Shipments.GetOrLoad(order.Id); dynamic dynObject = ToDynamic(ctx, order); - if (ctx.Stores.ContainsKey(order.StoreId)) - { - dynObject.Store = ToDynamic(ctx, ctx.Stores[order.StoreId]); - } - dynObject.Customer = ToDynamic(ctx, customers.FirstOrDefault(x => x.Id == order.CustomerId)); - // we do not export all customer generic attributes because otherwise the export file gets too large + // We do not export all customer generic attributes because otherwise the export file gets too large. dynObject.Customer._GenericAttributes = genericAttributes .Where(x => x.Value.HasValue() && _orderCustomerAttributes.Contains(x.Key)) .Select(x => ToDynamic(ctx, x)) @@ -1151,23 +1233,7 @@ private List Convert(DataExporterContext ctx, Order order) } dynObject.OrderItems = orderItems - .Select(e => - { - dynamic dyn = ToDynamic(ctx, e); - - if (ctx.ProductTemplates.ContainsKey(e.Product.ProductTemplateId)) - dyn.Product._ProductTemplateViewPath = ctx.ProductTemplates[e.Product.ProductTemplateId]; - else - dyn.Product._ProductTemplateViewPath = ""; - - dyn.Product._BasePriceInfo = e.Product.GetBasePriceInfo(_services.Localization, _priceFormatter.Value, _currencyService.Value, _taxService.Value, - _priceCalculationService.Value, ctx.ContextCurrency, decimal.Zero, true); - - ToDeliveryTime(ctx, dyn.Product, e.Product.DeliveryTimeId); - ToQuantityUnit(ctx, dyn.Product, e.Product.QuantityUnitId); - - return dyn; - }) + .Select(x => ToDynamic(ctx, x)) .ToList(); dynObject.Shipments = shipments @@ -1195,7 +1261,7 @@ private List Convert(DataExporterContext ctx, Manufacturer manufacturer dynamic dynObject = ToDynamic(ctx, manufacturer); - if (!ctx.IsPreview && manufacturer.PictureId.HasValue) + if (manufacturer.PictureId.HasValue) { var numberOfPictures = (ctx.Projection.NumberOfPictures ?? int.MaxValue); var pictures = ctx.ManufacturerExportContext.Pictures.GetOrLoad(manufacturer.PictureId.Value).Take(numberOfPictures); @@ -1235,7 +1301,7 @@ private List Convert(DataExporterContext ctx, Category category) dynamic dynObject = ToDynamic(ctx, category); - if (!ctx.IsPreview && category.PictureId.HasValue) + if (category.PictureId.HasValue) { var numberOfPictures = (ctx.Projection.NumberOfPictures ?? int.MaxValue); var pictures = ctx.CategoryExportContext.Pictures.GetOrLoad(category.PictureId.Value).Take(numberOfPictures); @@ -1271,8 +1337,7 @@ private List Convert(DataExporterContext ctx, Customer customer) { var result = new List(); - var perfLoadId = (ctx.IsPreview ? 0 : customer.Id); - var genericAttributes = ctx.CustomerExportContext.GenericAttributes.GetOrLoad(perfLoadId); + var genericAttributes = ctx.CustomerExportContext.GenericAttributes.GetOrLoad(customer.Id); dynamic dynObject = ToDynamic(ctx, customer); @@ -1283,15 +1348,6 @@ private List Convert(DataExporterContext ctx, Customer customer) .Select(x => ToDynamic(ctx, x)) .ToList(); - dynObject.CustomerRoles = customer.CustomerRoles - .Select(x => - { - dynamic dyn = new DynamicEntity(x); - - return dyn; - }) - .ToList(); - dynObject._GenericAttributes = genericAttributes .Select(x => ToDynamic(ctx, x)) .ToList(); @@ -1322,7 +1378,7 @@ private List Convert(DataExporterContext ctx, Customer customer) if (pictureId != null) { // reduce traffic and do not export default avatar - dynObject._AvatarPictureUrl = _pictureService.Value.GetPictureUrl(pictureId.Value.ToInt(), _mediaSettings.Value.AvatarPictureSize, false, ctx.Store.Url); + dynObject._AvatarPictureUrl = _pictureService.Value.GetUrl(pictureId.Value.ToInt(), _mediaSettings.Value.AvatarPictureSize, false, _services.StoreService.GetHost(ctx.Store)); } } @@ -1353,8 +1409,34 @@ private List Convert(DataExporterContext ctx, NewsLetterSubscription su ExecuteContext = ctx.ExecuteContext }); + return result; + } + + private List Convert(DataExporterContext ctx, ShoppingCartItem shoppingCartItem) + { + var result = new List(); + dynamic dynObject = ToDynamic(ctx, shoppingCartItem); + + result.Add(dynObject); + + _services.EventPublisher.Publish(new RowExportingEvent + { + Row = dynObject, + EntityType = ExportEntityType.ShoppingCartItem, + ExportRequest = ctx.Request, + ExecuteContext = ctx.ExecuteContext + }); return result; } } + + + internal class DynamicProductContext + { + public string SeName { get; set; } + public string AbsoluteProductUrl { get; set; } + public ICollection Combinations { get; set; } + public ProductVariantAttributeCombination Combination { get; set; } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExecuteContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExecuteContext.cs index 4e1423b528..27c5e3aa9b 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExecuteContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExecuteContext.cs @@ -3,7 +3,9 @@ using System.IO; using System.Threading; using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Localization; using SmartStore.Core.Logging; +using SmartStore.Utilities; namespace SmartStore.Services.DataExchange.Export { @@ -20,12 +22,17 @@ internal ExportExecuteContext(DataExportResult result, CancellationToken cancell Folder = folder; ExtraDataUnits = new List(); CustomProperties = new Dictionary(); - } + } + + /// + /// Identifier of the export profile + /// + public int ProfileId { get; internal set; } /// - /// Identifier of the export profile + /// The export profile /// - public int ProfileId { get; internal set; } + public dynamic Profile { get; internal set; } /// /// Provides the data to be exported @@ -158,18 +165,37 @@ public bool IsMaxFailures /// /// Processes an exception that occurred while exporting a record /// + /// Identifier of the current entity /// Exception public void RecordException(Exception exception, int entityId) { ++RecordsFailed; - Log.ErrorFormat(exception, "Error while processing record with id {0}", entityId); + Log.ErrorFormat("Error while processing record with id {0}. {1}".FormatInvariant(entityId, exception.ToString())); if (IsMaxFailures) _result.LastError = exception.ToString(); } - public ProgressValueSetter ProgressValueSetter { get; internal set; } + /// + /// Processes an out-of-memory exception and hard aborts the export + /// + /// Out-of-memory exception + /// Identifier of the current entity + /// Localizer + public void RecordOutOfMemoryException(OutOfMemoryException exception, int entityId, Localizer localizer) + { + Abort = DataExchangeAbortion.Hard; + + var fileLength = Prettifier.BytesToString(DataStream.Length); + var batchSizeString = localizer("Admin.DataExchange.Export.BatchSize").Text; + + Log.Fatal($"No more memory could be allocated. Probably the export file is getting too large ({fileLength}). Please use profile setting \"{batchSizeString}\" to split the export into smaller files."); + + RecordException(exception, entityId); + } + + public ProgressValueSetter ProgressValueSetter { get; internal set; } /// /// Allows to set a progress message diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs index 56889a7026..ed71ce4a8b 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs @@ -6,6 +6,7 @@ using SmartStore.Core.Domain; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Tasks; using SmartStore.Core.Events; using SmartStore.Core.Plugins; @@ -56,15 +57,17 @@ public virtual ExportProfile InsertExportProfile( { Guard.NotEmpty(providerSystemName, nameof(providerSystemName)); - var profileCount = _exportProfileRepository.Table.Count(x => x.ProviderSystemName == providerSystemName); - if (name.IsEmpty()) + { name = providerSystemName; + } if (!isSystemProfile) - name = string.Concat(_localizationService.GetResource("Common.My"), " ", name); + { + var profileCount = _exportProfileRepository.Table.Count(x => x.ProviderSystemName == providerSystemName); - name = string.Concat(name, " ", profileCount + 1); + name = string.Concat(_localizationService.GetResource("Common.My"), " ", name, " ", profileCount + 1); + } var cloneProfile = GetExportProfileById(cloneFromProfileId); @@ -122,7 +125,8 @@ public virtual ExportProfile InsertExportProfile( var filter = new ExportFilter { - IsPublished = true + IsPublished = true, + ShoppingCartTypeId = (int)ShoppingCartType.ShoppingCart }; profile.Projection = XmlHelper.Serialize(projection); @@ -152,10 +156,9 @@ public virtual ExportProfile InsertExportProfile( var path = DataSettings.Current.TenantPath + "/ExportProfiles"; profile.FolderName = path + "/" + FileSystemHelper.CreateNonExistingDirectoryName(CommonHelper.MapPath(path), folderName); - if (profileSystemName.IsEmpty() && isSystemProfile) - profile.SystemName = cleanedSystemName; - else - profile.SystemName = profileSystemName; + profile.SystemName = profileSystemName.IsEmpty() && isSystemProfile + ? cleanedSystemName + : profileSystemName; _exportProfileRepository.Insert(profile); @@ -194,8 +197,6 @@ public virtual ExportProfile InsertExportProfile( } } - _eventPublisher.EntityInserted(profile); - return profile; } @@ -232,8 +233,6 @@ public virtual void UpdateExportProfile(ExportProfile profile) } _exportProfileRepository.Update(profile); - - _eventPublisher.EntityUpdated(profile); } public virtual void DeleteExportProfile(ExportProfile profile, bool force = false) @@ -252,8 +251,6 @@ public virtual void DeleteExportProfile(ExportProfile profile, bool force = fals var scheduleTask = _scheduleTaskService.GetTaskById(scheduleTaskId); _scheduleTaskService.DeleteTask(scheduleTask); - _eventPublisher.EntityDeleted(profile); - if (System.IO.Directory.Exists(folder)) { FileSystemHelper.ClearDirectory(folder, true); @@ -360,8 +357,6 @@ public virtual void UpdateExportDeployment(ExportDeployment deployment) } _exportDeploymentRepository.Update(deployment); - - _eventPublisher.EntityUpdated(deployment); } public virtual void DeleteExportDeployment(ExportDeployment deployment) @@ -370,8 +365,6 @@ public virtual void DeleteExportDeployment(ExportDeployment deployment) throw new ArgumentNullException("deployment"); _exportDeploymentRepository.Delete(deployment); - - _eventPublisher.EntityDeleted(deployment); } } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs index a1fbd7cd3b..93f41a6d5a 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs @@ -9,14 +9,15 @@ using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Discounts; using SmartStore.Core.Domain.Media; +using SmartStore.Core.Domain.Orders; namespace SmartStore.Services.DataExchange.Export { public class ExportXmlHelper : IDisposable { - private XmlWriter _writer; - private CultureInfo _culture; - private bool _doNotDispose; + protected XmlWriter _writer; + protected CultureInfo _culture; + protected bool _doNotDispose; public ExportXmlHelper(XmlWriter writer, bool doNotDispose = false, CultureInfo culture = null) { @@ -51,10 +52,7 @@ public static XmlWriterSettings DefaultSettings public ExportXmlExclude Exclude { get; set; } - public XmlWriter Writer - { - get { return _writer; } - } + public XmlWriter Writer => _writer; public void Dispose() { @@ -202,8 +200,13 @@ public void WriteCurrency(dynamic currency, string node) _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); _writer.Write("UpdatedOnUtc", entity.UpdatedOnUtc.ToString(_culture)); _writer.Write("DomainEndings", entity.DomainEndings); + _writer.Write("RoundOrderItemsEnabled", entity.RoundOrderItemsEnabled.ToString()); + _writer.Write("RoundNumDecimals", entity.RoundNumDecimals.ToString()); + _writer.Write("RoundOrderTotalEnabled", entity.RoundOrderTotalEnabled.ToString()); + _writer.Write("RoundOrderTotalDenominator", entity.RoundOrderTotalDenominator.ToString(_culture)); + _writer.Write("RoundOrderTotalRule", ((int)entity.RoundOrderTotalRule).ToString()); - WriteLocalized(currency); + WriteLocalized(currency); if (node.HasValue()) { @@ -544,6 +547,7 @@ public void WriteProduct(dynamic product, string node) _writer.Write("MaximumCustomerEnteredPrice", entity.MaximumCustomerEnteredPrice.ToString(_culture)); _writer.Write("HasTierPrices", entity.HasTierPrices.ToString()); _writer.Write("HasDiscountsApplied", entity.HasDiscountsApplied.ToString()); + _writer.Write("MainPictureId", entity.MainPictureId.HasValue ? entity.MainPictureId.Value.ToString() : ""); _writer.Write("Weight", ((decimal)product.Weight).ToString(_culture)); _writer.Write("Length", ((decimal)product.Length).ToString(_culture)); _writer.Write("Width", ((decimal)product.Width).ToString(_culture)); @@ -613,7 +617,8 @@ public void WriteProduct(dynamic product, string node) _writer.Write("CustomerRoleId", entityTierPrice.CustomerRoleId.HasValue ? entityTierPrice.CustomerRoleId.Value.ToString() : ""); _writer.Write("Quantity", entityTierPrice.Quantity.ToString()); _writer.Write("Price", entityTierPrice.Price.ToString(_culture)); - _writer.WriteEndElement(); // TierPrice + _writer.Write("CalculationMethod", ((int)entityTierPrice.CalculationMethod).ToString()); + _writer.WriteEndElement(); // TierPrice } _writer.WriteEndElement(); // TierPrices } @@ -641,8 +646,9 @@ public void WriteProduct(dynamic product, string node) foreach (dynamic pva in product.ProductAttributes) { ProductVariantAttribute entityPva = pva.Entity; + ProductAttribute entityPa = pva.Attribute.Entity; - _writer.WriteStartElement("ProductAttribute"); + _writer.WriteStartElement("ProductAttribute"); _writer.Write("Id", entityPva.Id.ToString()); _writer.Write("TextPrompt", (string)pva.TextPrompt); _writer.Write("IsRequired", entityPva.IsRequired.ToString()); @@ -650,12 +656,16 @@ public void WriteProduct(dynamic product, string node) _writer.Write("DisplayOrder", entityPva.DisplayOrder.ToString()); _writer.WriteStartElement("Attribute"); - _writer.Write("Id", ((int)pva.Attribute.Id).ToString()); - _writer.Write("Alias", (string)pva.Attribute.Alias); - _writer.Write("Name", (string)pva.Attribute.Name); - _writer.Write("Description", (string)pva.Attribute.Description); + _writer.Write("Id", entityPa.Id.ToString()); + _writer.Write("Alias", entityPa.Alias); + _writer.Write("Name", entityPa.Name); + _writer.Write("Description", entityPa.Description); + _writer.Write("AllowFiltering", entityPa.AllowFiltering.ToString()); + _writer.Write("DisplayOrder", entityPa.DisplayOrder.ToString()); + _writer.Write("FacetTemplateHint", ((int)entityPa.FacetTemplateHint).ToString()); + _writer.Write("IndexOptionNames", entityPa.IndexOptionNames.ToString()); - WriteLocalized(pva.Attribute); + WriteLocalized(pva.Attribute); _writer.WriteEndElement(); // Attribute @@ -824,6 +834,7 @@ public void WriteProduct(dynamic product, string node) _writer.Write("ShowOnProductPage", entitySa.ShowOnProductPage.ToString()); _writer.Write("FacetSorting", ((int)entitySa.FacetSorting).ToString()); _writer.Write("FacetTemplateHint", ((int)entitySa.FacetTemplateHint).ToString()); + _writer.Write("IndexOptionNames", entitySa.IndexOptionNames.ToString()); WriteLocalized(option.SpecificationAttribute); @@ -943,6 +954,40 @@ public void WriteCustomer(dynamic customer, string node) _writer.WriteEndElement(); } } + + public void WriteShoppingCartItem(dynamic shoppingCartItem, string node) + { + if (shoppingCartItem == null) + return; + + ShoppingCartItem entity = shoppingCartItem.Entity; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + _writer.Write("Id", entity.Id.ToString()); + _writer.Write("StoreId", entity.StoreId.ToString()); + _writer.Write("ParentItemId", entity.ParentItemId.HasValue ? entity.ParentItemId.Value.ToString() : ""); + _writer.Write("BundleItemId", entity.BundleItemId.HasValue ? entity.BundleItemId.Value.ToString() : ""); + _writer.Write("ShoppingCartTypeId", entity.ShoppingCartTypeId.ToString()); + _writer.Write("CustomerId", entity.CustomerId.ToString()); + _writer.Write("ProductId", entity.ProductId.ToString()); + _writer.Write("AttributesXml", entity.AttributesXml, null, true); + _writer.Write("CustomerEnteredPrice", entity.CustomerEnteredPrice.ToString(_culture)); + _writer.Write("Quantity", entity.Quantity.ToString()); + _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); + _writer.Write("UpdatedOnUtc", entity.UpdatedOnUtc.ToString(_culture)); + + WriteCustomer(shoppingCartItem.Customer, "Customer"); + WriteProduct(shoppingCartItem.Product, "Product"); + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/IDataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/IDataExporter.cs index c3e08f1458..3bea8233e3 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/IDataExporter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/IDataExporter.cs @@ -25,12 +25,14 @@ public interface IDataExporter /// Customer, null to use current customer. /// Store identifier, null to use current store. /// Pictures per product, null to load all pictures per product. + /// A value indicating whether to show hidden records /// Product export context ProductExportContext CreateProductExportContext( IEnumerable products = null, Customer customer = null, int? storeId = null, - int? maxPicturesPerProduct = null); + int? maxPicturesPerProduct = null, + bool showHidden = true); } @@ -61,6 +63,8 @@ public DataExportRequest(ExportProfile profile, Provider provid public IList EntitiesToExport { get; set; } + public string ActionOrigin { get; set; } + public IDictionary CustomData { get; private set; } public IQueryable ProductQuery { get; set; } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs index 95c8b0d9d4..c0e56b6175 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs @@ -32,8 +32,6 @@ public DataExporterContext( FolderContent = request.Profile.GetExportFolder(true, true); - Categories = new Dictionary(); - CategoryPathes = new Dictionary(); DeliveryTimes = new Dictionary(); QuantityUnits = new Dictionary(); Stores = new Dictionary(); @@ -110,8 +108,6 @@ public bool IsFileBasedExport } // data loaded once per export - public Dictionary Categories { get; set; } - public Dictionary CategoryPathes { get; set; } public Dictionary DeliveryTimes { get; set; } public Dictionary QuantityUnits { get; set; } public Dictionary Stores { get; set; } diff --git a/src/Libraries/SmartStore.Services/DataExchange/ISyncMappingService.cs b/src/Libraries/SmartStore.Services/DataExchange/ISyncMappingService.cs index d350a904ae..9d08368ae3 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/ISyncMappingService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/ISyncMappingService.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using SmartStore.Core; using SmartStore.Core.Domain.DataExchange; @@ -25,8 +24,9 @@ public partial interface ISyncMappingService /// /// The context (external application) name. Leave null to load all records regardless of context. /// The entity name. Leave null to load all records regardless of entity name. + /// Array of entity identifiers /// SyncMappings - IList GetAllSyncMappings(string contextName = null, string entityName = null); + IList GetAllSyncMappings(string contextName = null, string entityName = null, int[] entityIds = null); /// /// Gets a sync mapping record by (target) entity id, name and context name. diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs index 61c9179d0e..3ad203cc4a 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs @@ -100,40 +100,35 @@ public FileDownloadManagerItem CreateDownloadImage(string urlOrPath, string seoN item.MimeType = MediaTypeNames.Image.Jpeg; } - var extension = MimeTypes.MapMimeTypeToExtension(item.MimeType); - - if (extension.HasValue()) + if (urlOrPath.IsWebUrl()) { - if (urlOrPath.IsWebUrl()) - { - item.Url = urlOrPath; - item.FileName = "{0}-{1}".FormatInvariant(seoName, item.Id).ToValidFileName(); + item.Url = urlOrPath; + item.FileName = "{0}-{1}".FormatInvariant(seoName, item.Id).ToValidFileName(); - if (DownloadedItems.ContainsKey(urlOrPath)) - { - item.Path = Path.Combine(ImageDownloadFolder, DownloadedItems[urlOrPath]); - item.Success = true; - } - else - { - item.Path = Path.Combine(ImageDownloadFolder, item.FileName + extension.EnsureStartsWith(".")); - } - } - else if (Path.IsPathRooted(urlOrPath)) + if (DownloadedItems.ContainsKey(urlOrPath)) { - item.Path = urlOrPath; + item.Path = Path.Combine(ImageDownloadFolder, DownloadedItems[urlOrPath]); item.Success = true; } else { - item.Path = Path.Combine(ImageFolder, urlOrPath); - item.Success = true; - } + var extension = MimeTypes.MapMimeTypeToExtension(item.MimeType).NullEmpty() ?? ".jpg"; - return item; + item.Path = Path.Combine(ImageDownloadFolder, item.FileName + extension.EnsureStartsWith(".")); + } + } + else if (Path.IsPathRooted(urlOrPath)) + { + item.Path = urlOrPath; + item.Success = true; + } + else + { + item.Path = Path.Combine(ImageFolder, urlOrPath); + item.Success = true; } - return null; + return item; } public void Succeeded(FileDownloadManagerItem item) diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExtensions.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExtensions.cs index 2e14cb2500..09201325f1 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExtensions.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExtensions.cs @@ -14,13 +14,27 @@ public static class ImportExtensions /// /// Import profile /// Folder path - public static string GetImportFolder(this ImportProfile profile, bool content = false, bool create = false) + public static string GetImportFolder( + this ImportProfile profile, + bool content = false, + bool create = false, + bool absolutePath = true) { - var basePath = DataSettings.Current.TenantPath + "/ImportProfiles/"; - var path = CommonHelper.MapPath(string.Concat(basePath, profile.FolderName, content ? "/Content" : "")); + var path = string.Concat( + DataSettings.Current.TenantPath, + "/ImportProfiles/", + profile.FolderName, + content ? "/Content" : ""); - if (create && !System.IO.Directory.Exists(path)) - System.IO.Directory.CreateDirectory(path); + if (absolutePath) + { + path = CommonHelper.MapPath(path); + + if (create && !System.IO.Directory.Exists(path)) + { + System.IO.Directory.CreateDirectory(path); + } + } return path; } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs index 1a50bdb78a..fbded791c7 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs @@ -235,8 +235,6 @@ public virtual ImportProfile InsertImportProfile(string fileName, string name, I task.Alias = profile.Id.ToString(); _scheduleTaskService.UpdateTask(task); - _eventPublisher.EntityInserted(profile); - return profile; } @@ -246,8 +244,6 @@ public virtual void UpdateImportProfile(ImportProfile profile) throw new ArgumentNullException("profile"); _importProfileRepository.Update(profile); - - _eventPublisher.EntityUpdated(profile); } public virtual void DeleteImportProfile(ImportProfile profile) @@ -263,8 +259,6 @@ public virtual void DeleteImportProfile(ImportProfile profile) var scheduleTask = _scheduleTaskService.GetTaskById(scheduleTaskId); _scheduleTaskService.DeleteTask(scheduleTask); - _eventPublisher.EntityDeleted(profile); - if (System.IO.Directory.Exists(folder)) { FileSystemHelper.ClearDirectory(folder, true); diff --git a/src/Libraries/SmartStore.Services/DataExchange/SyncMappingService.cs b/src/Libraries/SmartStore.Services/DataExchange/SyncMappingService.cs index f18bdb87a2..395a8f208a 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/SyncMappingService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/SyncMappingService.cs @@ -1,16 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; -using System.Text; -using System.Threading.Tasks; using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Domain.DataExchange; namespace SmartStore.Services.DataExchange { - + public partial class SyncMappingService : ISyncMappingService { private readonly IRepository _syncMappingsRepository; @@ -34,7 +31,7 @@ public void InsertSyncMappings(IEnumerable mappings) _syncMappingsRepository.InsertRange(mappings); } - public IList GetAllSyncMappings(string contextName = null, string entityName = null) + public IList GetAllSyncMappings(string contextName = null, string entityName = null, int[] entityIds = null) { var query = _syncMappingsRepository.Table; @@ -48,6 +45,11 @@ public IList GetAllSyncMappings(string contextName = null, string e query = query.Where(x => x.ContextName == contextName); } + if (entityIds != null && entityIds.Any()) + { + query = query.Where(x => entityIds.Contains(x.EntityId)); + } + return query.ToList(); } diff --git a/src/Libraries/SmartStore.Services/Directory/CountryService.cs b/src/Libraries/SmartStore.Services/Directory/CountryService.cs index 384fd75e3c..13f6b21646 100644 --- a/src/Libraries/SmartStore.Services/Directory/CountryService.cs +++ b/src/Libraries/SmartStore.Services/Directory/CountryService.cs @@ -1,84 +1,48 @@ using System; using System.Collections.Generic; using System.Linq; -using SmartStore.Core; -using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Stores; -using SmartStore.Core.Events; namespace SmartStore.Services.Directory { - /// - /// Country service - /// public partial class CountryService : ICountryService { - #region Constants private const string COUNTRIES_ALL_KEY = "SmartStore.country.all-{0}"; private const string COUNTRIES_BILLING_KEY = "SmartStore.country.billing-{0}"; private const string COUNTRIES_SHIPPING_KEY = "SmartStore.country.shipping-{0}"; - private const string COUNTRIES_PATTERN_KEY = "SmartStore.country."; - #endregion - - #region Fields - - private readonly IRepository _countryRepository; - private readonly IEventPublisher _eventPublisher; - private readonly IRequestCache _requestCache; - private readonly IStoreContext _storeContext; - private readonly IRepository _storeMappingRepository; - - #endregion + private const string COUNTRIES_PATTERN_KEY = "SmartStore.country.*"; - #region Ctor + private readonly ICommonServices _services; + private readonly IRepository _countryRepository; + private readonly IRepository _storeMappingRepository; - public CountryService(IRequestCache requestCache, + public CountryService( + ICommonServices services, IRepository countryRepository, - IEventPublisher eventPublisher, - IStoreContext storeContext, IRepository storeMappingRepository) { - _requestCache = requestCache; _countryRepository = countryRepository; - _eventPublisher = eventPublisher; - _storeContext = storeContext; + _services = services; _storeMappingRepository = storeMappingRepository; } public DbQuerySettings QuerySettings { get; set; } - #endregion - - #region Methods - - /// - /// Deletes a country - /// - /// Country public virtual void DeleteCountry(Country country) { - if (country == null) - throw new ArgumentNullException("country"); + Guard.NotNull(country, nameof(country)); - _countryRepository.Delete(country); - - _requestCache.RemoveByPattern(COUNTRIES_PATTERN_KEY); - - //event notification - _eventPublisher.EntityDeleted(country); + _countryRepository.Delete(country); + + _services.RequestCache.RemoveByPattern(COUNTRIES_PATTERN_KEY); } - /// - /// Gets all countries - /// - /// A value indicating whether to show hidden records - /// Country collection public virtual IList GetAllCountries(bool showHidden = false) { string key = string.Format(COUNTRIES_ALL_KEY, showHidden); - return _requestCache.Get(key, () => + return _services.RequestCache.Get(key, () => { var query = _countryRepository.Table; @@ -89,7 +53,7 @@ public virtual IList GetAllCountries(bool showHidden = false) if (!showHidden && !QuerySettings.IgnoreMultiStore) { - var currentStoreId = _storeContext.CurrentStore.Id; + var currentStoreId = _services.StoreContext.CurrentStore.Id; query = from c in query join sc in _storeMappingRepository.Table on new { c1 = c.Id, c2 = "Country" } equals new { c1 = sc.EntityId, c2 = sc.EntityName } into c_sm @@ -110,15 +74,10 @@ orderby cGroup.Key }); } - /// - /// Gets all countries that allow billing - /// - /// A value indicating whether to show hidden records - /// Country collection public virtual IList GetAllCountriesForBilling(bool showHidden = false) { string key = string.Format(COUNTRIES_BILLING_KEY, showHidden); - return _requestCache.Get(key, () => + return _services.RequestCache.Get(key, () => { var allCountries = GetAllCountries(showHidden); @@ -127,15 +86,10 @@ public virtual IList GetAllCountriesForBilling(bool showHidden = false) }); } - /// - /// Gets all countries that allow shipping - /// - /// A value indicating whether to show hidden records - /// Country collection public virtual IList GetAllCountriesForShipping(bool showHidden = false) { string key = string.Format(COUNTRIES_SHIPPING_KEY, showHidden); - return _requestCache.Get(key, () => + return _services.RequestCache.Get(key, () => { var allCountries = GetAllCountries(showHidden); @@ -144,11 +98,6 @@ public virtual IList GetAllCountriesForShipping(bool showHidden = false }); } - /// - /// Gets a country - /// - /// Country identifier - /// Country public virtual Country GetCountryById(int countryId) { if (countryId == 0) @@ -157,11 +106,6 @@ public virtual Country GetCountryById(int countryId) return _countryRepository.GetById(countryId); } - /// - /// Gets a country by two or three letter ISO code - /// - /// Country two or three letter ISO code - /// Country public virtual Country GetCountryByTwoOrThreeLetterIsoCode(string letterIsoCode) { if (letterIsoCode.HasValue()) @@ -174,11 +118,6 @@ public virtual Country GetCountryByTwoOrThreeLetterIsoCode(string letterIsoCode) return null; } - /// - /// Gets a country by two letter ISO code - /// - /// Country two letter ISO code - /// Country public virtual Country GetCountryByTwoLetterIsoCode(string twoLetterIsoCode) { if (twoLetterIsoCode.IsEmpty()) @@ -192,11 +131,6 @@ public virtual Country GetCountryByTwoLetterIsoCode(string twoLetterIsoCode) return country; } - /// - /// Gets a country by three letter ISO code - /// - /// Country three letter ISO code - /// Country public virtual Country GetCountryByThreeLetterIsoCode(string threeLetterIsoCode) { if (threeLetterIsoCode.IsEmpty()) @@ -210,40 +144,22 @@ public virtual Country GetCountryByThreeLetterIsoCode(string threeLetterIsoCode) return country; } - /// - /// Inserts a country - /// - /// Country public virtual void InsertCountry(Country country) { - if (country == null) - throw new ArgumentNullException("country"); - - _countryRepository.Insert(country); + Guard.NotNull(country, nameof(country)); - _requestCache.RemoveByPattern(COUNTRIES_PATTERN_KEY); + _countryRepository.Insert(country); - //event notification - _eventPublisher.EntityInserted(country); + _services.RequestCache.RemoveByPattern(COUNTRIES_PATTERN_KEY); } - /// - /// Updates the country - /// - /// Country public virtual void UpdateCountry(Country country) { - if (country == null) - throw new ArgumentNullException("country"); + Guard.NotNull(country, nameof(country)); _countryRepository.Update(country); - _requestCache.RemoveByPattern(COUNTRIES_PATTERN_KEY); - - //event notification - _eventPublisher.EntityUpdated(country); + _services.RequestCache.RemoveByPattern(COUNTRIES_PATTERN_KEY); } - - #endregion - } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Directory/CurrencyService.cs b/src/Libraries/SmartStore.Services/Directory/CurrencyService.cs index 2427378dcd..0085240ff5 100644 --- a/src/Libraries/SmartStore.Services/Directory/CurrencyService.cs +++ b/src/Libraries/SmartStore.Services/Directory/CurrencyService.cs @@ -56,9 +56,6 @@ public virtual void DeleteCurrency(Currency currency) throw new ArgumentNullException("currency"); _currencyRepository.Delete(currency); - - //event notification - _eventPublisher.EntityDeleted(currency); } public virtual Currency GetCurrencyById(int currencyId) @@ -103,8 +100,6 @@ public virtual void InsertCurrency(Currency currency) throw new ArgumentNullException("currency"); _currencyRepository.Insert(currency); - - _eventPublisher.EntityInserted(currency); } public virtual void UpdateCurrency(Currency currency) @@ -113,8 +108,6 @@ public virtual void UpdateCurrency(Currency currency) throw new ArgumentNullException("currency"); _currencyRepository.Update(currency); - - _eventPublisher.EntityUpdated(currency); } public virtual decimal ConvertCurrency(decimal amount, decimal exchangeRate) diff --git a/src/Libraries/SmartStore.Services/Directory/DeliveryTimeService.cs b/src/Libraries/SmartStore.Services/Directory/DeliveryTimeService.cs index 4ddb543c44..3923083fdb 100644 --- a/src/Libraries/SmartStore.Services/Directory/DeliveryTimeService.cs +++ b/src/Libraries/SmartStore.Services/Directory/DeliveryTimeService.cs @@ -53,9 +53,6 @@ public virtual void DeleteDeliveryTime(DeliveryTime deliveryTime) throw new SmartException(T("Admin.Configuration.DeliveryTimes.CannotDeleteAssignedProducts")); _deliveryTimeRepository.Delete(deliveryTime); - - //event notification - _eventPublisher.EntityDeleted(deliveryTime); } public virtual bool IsAssociated(int deliveryTimeId) @@ -115,9 +112,6 @@ public virtual void InsertDeliveryTime(DeliveryTime deliveryTime) throw new ArgumentNullException("deliveryTime"); _deliveryTimeRepository.Insert(deliveryTime); - - //event notification - _eventPublisher.EntityInserted(deliveryTime); } public virtual void UpdateDeliveryTime(DeliveryTime deliveryTime) @@ -126,9 +120,6 @@ public virtual void UpdateDeliveryTime(DeliveryTime deliveryTime) throw new ArgumentNullException("deliveryTime"); _deliveryTimeRepository.Update(deliveryTime); - - //event notification - _eventPublisher.EntityUpdated(deliveryTime); } public virtual void SetToDefault(DeliveryTime deliveryTime) @@ -143,9 +134,6 @@ public virtual void SetToDefault(DeliveryTime deliveryTime) time.IsDefault = time.Equals(deliveryTime) ? true : false; _deliveryTimeRepository.Update(time); } - - //event notification - _eventPublisher.EntityUpdated(deliveryTime); } public virtual DeliveryTime GetDefaultDeliveryTime() diff --git a/src/Libraries/SmartStore.Services/Directory/ICountryService.cs b/src/Libraries/SmartStore.Services/Directory/ICountryService.cs index d91346a4ff..001d2d55fd 100644 --- a/src/Libraries/SmartStore.Services/Directory/ICountryService.cs +++ b/src/Libraries/SmartStore.Services/Directory/ICountryService.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using SmartStore.Core.Domain.Directory; @@ -74,5 +75,5 @@ public partial interface ICountryService /// /// Country void UpdateCountry(Country country); - } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Directory/MeasureService.cs b/src/Libraries/SmartStore.Services/Directory/MeasureService.cs index d4f3d44df0..1e8c052807 100644 --- a/src/Libraries/SmartStore.Services/Directory/MeasureService.cs +++ b/src/Libraries/SmartStore.Services/Directory/MeasureService.cs @@ -37,9 +37,6 @@ public virtual void DeleteMeasureDimension(MeasureDimension measureDimension) throw new ArgumentNullException("measureDimension"); _measureDimensionRepository.Delete(measureDimension); - - //event notification - _eventPublisher.EntityDeleted(measureDimension); } public virtual MeasureDimension GetMeasureDimensionById(int measureDimensionId) @@ -77,9 +74,6 @@ public virtual void InsertMeasureDimension(MeasureDimension measure) throw new ArgumentNullException("measure"); _measureDimensionRepository.Insert(measure); - - //event notification - _eventPublisher.EntityInserted(measure); } public virtual void UpdateMeasureDimension(MeasureDimension measure) @@ -88,9 +82,6 @@ public virtual void UpdateMeasureDimension(MeasureDimension measure) throw new ArgumentNullException("measure"); _measureDimensionRepository.Update(measure); - - //event notification - _eventPublisher.EntityUpdated(measure); } public virtual decimal ConvertDimension(decimal quantity, @@ -147,9 +138,6 @@ public virtual void DeleteMeasureWeight(MeasureWeight measureWeight) throw new ArgumentNullException("measureWeight"); _measureWeightRepository.Delete(measureWeight); - - //event notification - _eventPublisher.EntityDeleted(measureWeight); } public virtual MeasureWeight GetMeasureWeightById(int measureWeightId) @@ -188,9 +176,6 @@ public virtual void InsertMeasureWeight(MeasureWeight measure) throw new ArgumentNullException("measure"); _measureWeightRepository.Insert(measure); - - //event notification - _eventPublisher.EntityInserted(measure); } public virtual void UpdateMeasureWeight(MeasureWeight measure) @@ -199,9 +184,6 @@ public virtual void UpdateMeasureWeight(MeasureWeight measure) throw new ArgumentNullException("measure"); _measureWeightRepository.Update(measure); - - //event notification - _eventPublisher.EntityUpdated(measure); } public virtual decimal ConvertWeight(decimal quantity, diff --git a/src/Libraries/SmartStore.Services/Directory/QuantityUnitService.cs b/src/Libraries/SmartStore.Services/Directory/QuantityUnitService.cs index f738eab12e..1048718152 100644 --- a/src/Libraries/SmartStore.Services/Directory/QuantityUnitService.cs +++ b/src/Libraries/SmartStore.Services/Directory/QuantityUnitService.cs @@ -40,9 +40,6 @@ public virtual void DeleteQuantityUnit(QuantityUnit quantityUnit) throw new SmartException("The quantity unit cannot be deleted. It has associated product variants"); _quantityUnitRepository.Delete(quantityUnit); - - //event notification - _eventPublisher.EntityDeleted(quantityUnit); } public virtual bool IsAssociated(int quantityUnitId) @@ -97,9 +94,6 @@ public virtual void InsertQuantityUnit(QuantityUnit quantityUnit) throw new ArgumentNullException("quantityUnit"); _quantityUnitRepository.Insert(quantityUnit); - - //event notification - _eventPublisher.EntityInserted(quantityUnit); } public virtual void UpdateQuantityUnit(QuantityUnit quantityUnit) @@ -124,9 +118,6 @@ public virtual void UpdateQuantityUnit(QuantityUnit quantityUnit) } _quantityUnitRepository.Update(quantityUnit); - - //event notification - _eventPublisher.EntityUpdated(quantityUnit); } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Directory/StateProvinceService.cs b/src/Libraries/SmartStore.Services/Directory/StateProvinceService.cs index bf57322862..db65144d52 100644 --- a/src/Libraries/SmartStore.Services/Directory/StateProvinceService.cs +++ b/src/Libraries/SmartStore.Services/Directory/StateProvinceService.cs @@ -27,9 +27,6 @@ public virtual void DeleteStateProvince(StateProvince stateProvince) throw new ArgumentNullException("stateProvince"); _stateProvinceRepository.Delete(stateProvince); - - //event notification - _eventPublisher.EntityDeleted(stateProvince); } public virtual IQueryable GetAllStateProvinces(bool showHidden = false) @@ -77,9 +74,6 @@ public virtual void InsertStateProvince(StateProvince stateProvince) throw new ArgumentNullException("stateProvince"); _stateProvinceRepository.Insert(stateProvince); - - //event notification - _eventPublisher.EntityInserted(stateProvince); } public virtual void UpdateStateProvince(StateProvince stateProvince) @@ -88,9 +82,6 @@ public virtual void UpdateStateProvince(StateProvince stateProvince) throw new ArgumentNullException("stateProvince"); _stateProvinceRepository.Update(stateProvince); - - //event notification - _eventPublisher.EntityUpdated(stateProvince); } } } diff --git a/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs b/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs index 14770e49fc..abb9af5b2d 100644 --- a/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs +++ b/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs @@ -19,7 +19,7 @@ namespace SmartStore.Services.Discounts public partial class DiscountService : IDiscountService { private const string DISCOUNTS_ALL_KEY = "SmartStore.discount.all-{0}-{1}"; - private const string DISCOUNTS_PATTERN_KEY = "SmartStore.discount."; + private const string DISCOUNTS_PATTERN_KEY = "SmartStore.discount.*"; private readonly IRepository _discountRepository; private readonly IRepository _discountRequirementRepository; @@ -104,9 +104,6 @@ public virtual void DeleteDiscount(Discount discount) _discountRepository.Delete(discount); _requestCache.RemoveByPattern(DISCOUNTS_PATTERN_KEY); - - //event notification - _eventPublisher.EntityDeleted(discount); } public virtual Discount GetDiscountById(int discountId) @@ -172,9 +169,6 @@ public virtual void InsertDiscount(Discount discount) _discountRepository.Insert(discount); _requestCache.RemoveByPattern(DISCOUNTS_PATTERN_KEY); - - // event notification - _eventPublisher.EntityInserted(discount); } public virtual void UpdateDiscount(Discount discount) @@ -185,9 +179,6 @@ public virtual void UpdateDiscount(Discount discount) _discountRepository.Update(discount); _requestCache.RemoveByPattern(DISCOUNTS_PATTERN_KEY); - - //event notification - _eventPublisher.EntityUpdated(discount); } public virtual void DeleteDiscountRequirement(DiscountRequirement discountRequirement) @@ -198,9 +189,6 @@ public virtual void DeleteDiscountRequirement(DiscountRequirement discountRequir _discountRequirementRepository.Delete(discountRequirement); _requestCache.RemoveByPattern(DISCOUNTS_PATTERN_KEY); - - //event notification - _eventPublisher.EntityDeleted(discountRequirement); } public virtual Provider LoadDiscountRequirementRuleBySystemName(string systemName, int storeId = 0) @@ -332,9 +320,6 @@ public virtual void InsertDiscountUsageHistory(DiscountUsageHistory discountUsag _discountUsageHistoryRepository.Insert(discountUsageHistory); _requestCache.RemoveByPattern(DISCOUNTS_PATTERN_KEY); - - //event notification - _eventPublisher.EntityInserted(discountUsageHistory); } public virtual void UpdateDiscountUsageHistory(DiscountUsageHistory discountUsageHistory) @@ -345,9 +330,6 @@ public virtual void UpdateDiscountUsageHistory(DiscountUsageHistory discountUsag _discountUsageHistoryRepository.Update(discountUsageHistory); _requestCache.RemoveByPattern(DISCOUNTS_PATTERN_KEY); - - //event notification - _eventPublisher.EntityUpdated(discountUsageHistory); } public virtual void DeleteDiscountUsageHistory(DiscountUsageHistory discountUsageHistory) @@ -358,9 +340,6 @@ public virtual void DeleteDiscountUsageHistory(DiscountUsageHistory discountUsag _discountUsageHistoryRepository.Delete(discountUsageHistory); _requestCache.RemoveByPattern(DISCOUNTS_PATTERN_KEY); - - //event notification - _eventPublisher.EntityDeleted(discountUsageHistory); } } } diff --git a/src/Libraries/SmartStore.Services/Events/DefaultConsumerFactory.cs b/src/Libraries/SmartStore.Services/Events/DefaultConsumerFactory.cs index c09d2f01ed..72382fd1c7 100644 --- a/src/Libraries/SmartStore.Services/Events/DefaultConsumerFactory.cs +++ b/src/Libraries/SmartStore.Services/Events/DefaultConsumerFactory.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using System.Collections.Generic; -using SmartStore.Core.Infrastructure; using SmartStore.Core.Events; using SmartStore.Core.Plugins; @@ -16,8 +15,8 @@ public DefaultConsumerFactory( IEnumerable, EventConsumerMetadata>> consumers, ICommonServices services) { - this._consumers = consumers; - this._services = services; + _consumers = consumers; + _services = services; } public IEnumerable> GetConsumers(bool? resolveAsyncs = null) diff --git a/src/Libraries/SmartStore.Services/Events/EventPublisher.cs b/src/Libraries/SmartStore.Services/Events/EventPublisher.cs index 5451306e09..eddb7b5a9e 100644 --- a/src/Libraries/SmartStore.Services/Events/EventPublisher.cs +++ b/src/Libraries/SmartStore.Services/Events/EventPublisher.cs @@ -3,15 +3,11 @@ using System.Collections.Generic; using System.Collections.Concurrent; using System.Threading; -using System.Threading.Tasks; using SmartStore.Core.Infrastructure; -using SmartStore.Core.Plugins; using SmartStore.Core.Logging; using SmartStore.Core.Async; -using SmartStore.Collections; -using Autofac; -using System.Diagnostics; using SmartStore.Core.Events; +using Autofac; namespace SmartStore.Services.Events { @@ -54,7 +50,7 @@ public void Publish(T eventMessage) { // for wiring up dependencies correctly var newFactory = c.Resolve>(); - consumers = newFactory.GetConsumers(true); + consumers = newFactory.GetConsumers(true).ToArray(); foreach (var consumer in consumers) { consumer.HandleEvent(eventMessage); @@ -73,7 +69,7 @@ public void Publish(T eventMessage) } // now execute all sync consumers - consumers = consumerFactory.GetConsumers(false); + consumers = consumerFactory.GetConsumers(false).ToArray(); foreach (var consumer in consumers) { PublishEvent(consumer, eventMessage); diff --git a/src/Libraries/SmartStore.Services/Forums/ForumMessageFactoryExtensions.cs b/src/Libraries/SmartStore.Services/Forums/ForumMessageFactoryExtensions.cs new file mode 100644 index 0000000000..0e8a1140be --- /dev/null +++ b/src/Libraries/SmartStore.Services/Forums/ForumMessageFactoryExtensions.cs @@ -0,0 +1,50 @@ +using System; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Forums; +using SmartStore.Core.Domain.Messages; +using SmartStore.Services.Messages; + +namespace SmartStore.Services.Forums +{ + public static class ForumMessageFactoryExtensions + { + /// + /// Sends a forum subscription message to a customer + /// + public static CreateMessageResult SendNewForumTopicMessage(this IMessageFactory factory, Customer customer, ForumTopic forumTopic, int languageId = 0) + { + Guard.NotNull(customer, nameof(customer)); + Guard.NotNull(forumTopic, nameof(forumTopic)); + + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewForumTopic, languageId, customer: customer), true, forumTopic, forumTopic.Forum); + } + + /// + /// Sends a forum subscription message to a customer + /// + /// Friendly forum topic page to use for URL generation (1-based) + public static CreateMessageResult SendNewForumPostMessage(this IMessageFactory factory, Customer customer, ForumPost forumPost, int topicPageIndex, int languageId = 0) + { + Guard.NotNull(customer, nameof(customer)); + Guard.NotNull(forumPost, nameof(forumPost)); + + var bag = new ModelPart + { + ["TopicPageIndex"] = topicPageIndex + }; + + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewForumPost, languageId, customer: customer), true, bag, forumPost, forumPost.ForumTopic, forumPost.ForumTopic.Forum); + } + + /// + /// Sends a private message notification + /// + public static CreateMessageResult SendPrivateMessageNotification(this IMessageFactory factory, Customer customer, PrivateMessage privateMessage, int languageId = 0) + { + Guard.NotNull(customer, nameof(customer)); + Guard.NotNull(privateMessage, nameof(privateMessage)); + + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewPrivateMessage, languageId, privateMessage.StoreId, customer), true, privateMessage); + } + } +} diff --git a/src/Libraries/SmartStore.Services/Forums/ForumService.cs b/src/Libraries/SmartStore.Services/Forums/ForumService.cs index 03df1fb38a..2fe46d227c 100644 --- a/src/Libraries/SmartStore.Services/Forums/ForumService.cs +++ b/src/Libraries/SmartStore.Services/Forums/ForumService.cs @@ -26,7 +26,6 @@ public partial class ForumService : IForumService private readonly IRepository _customerRepository; private readonly IGenericAttributeService _genericAttributeService; private readonly ICustomerService _customerService; - private readonly IWorkflowMessageService _workflowMessageService; private readonly IRepository _storeMappingRepository; private readonly ICommonServices _services; @@ -41,7 +40,6 @@ public ForumService( IRepository customerRepository, IGenericAttributeService genericAttributeService, ICustomerService customerService, - IWorkflowMessageService workflowMessageService, IRepository storeMappingRepository, ICommonServices services) { @@ -55,7 +53,6 @@ public ForumService( _customerRepository = customerRepository; _genericAttributeService = genericAttributeService; _customerService = customerService; - _workflowMessageService = workflowMessageService; _storeMappingRepository = storeMappingRepository; _services = services; } @@ -208,9 +205,6 @@ public virtual void DeleteForumGroup(ForumGroup forumGroup) } _forumGroupRepository.Delete(forumGroup); - - //event notification - _services.EventPublisher.EntityDeleted(forumGroup); } public virtual ForumGroup GetForumGroupById(int forumGroupId) @@ -256,9 +250,6 @@ public virtual void InsertForumGroup(ForumGroup forumGroup) } _forumGroupRepository.Insert(forumGroup); - - //event notification - _services.EventPublisher.EntityInserted(forumGroup); } public virtual void UpdateForumGroup(ForumGroup forumGroup) @@ -269,9 +260,6 @@ public virtual void UpdateForumGroup(ForumGroup forumGroup) } _forumGroupRepository.Update(forumGroup); - - //event notification - _services.EventPublisher.EntityUpdated(forumGroup); } public virtual void DeleteForum(Forum forum) @@ -291,8 +279,6 @@ where queryTopicIds.Contains(fs.TopicId) foreach (var fs in queryFs1.ToList()) { _forumSubscriptionRepository.Delete(fs); - //event notification - _services.EventPublisher.EntityDeleted(fs); } //delete forum subscriptions (forum) @@ -302,15 +288,10 @@ where queryTopicIds.Contains(fs.TopicId) foreach (var fs2 in queryFs2.ToList()) { _forumSubscriptionRepository.Delete(fs2); - //event notification - _services.EventPublisher.EntityDeleted(fs2); } //delete forum _forumRepository.Delete(forum); - - //event notification - _services.EventPublisher.EntityDeleted(forum); } public virtual Forum GetForumById(int forumId) @@ -340,9 +321,6 @@ public virtual void InsertForum(Forum forum) } _forumRepository.Insert(forum); - - //event notification - _services.EventPublisher.EntityInserted(forum); } public virtual void UpdateForum(Forum forum) @@ -353,9 +331,6 @@ public virtual void UpdateForum(Forum forum) } _forumRepository.Update(forum); - - //event notification - _services.EventPublisher.EntityUpdated(forum); } public virtual void DeleteTopic(ForumTopic forumTopic) @@ -379,16 +354,11 @@ public virtual void DeleteTopic(ForumTopic forumTopic) foreach (var fs in forumSubscriptions) { _forumSubscriptionRepository.Delete(fs); - //event notification - _services.EventPublisher.EntityDeleted(fs); } //update stats UpdateForumStats(forumId); UpdateCustomerStats(customerId); - - //event notification - _services.EventPublisher.EntityDeleted(forumTopic); } public virtual ForumTopic GetTopicById(int forumTopicId) @@ -498,9 +468,6 @@ public virtual void InsertTopic(ForumTopic forumTopic, bool sendNotifications) //update stats UpdateForumStats(forumTopic.ForumId); - //event notification - _services.EventPublisher.EntityInserted(forumTopic); - //send notifications if (sendNotifications) { @@ -517,7 +484,7 @@ public virtual void InsertTopic(ForumTopic forumTopic, bool sendNotifications) if (!String.IsNullOrEmpty(subscription.Customer.Email)) { - _workflowMessageService.SendNewForumTopicMessage(subscription.Customer, forumTopic, forum, languageId); + _services.MessageFactory.SendNewForumTopicMessage(subscription.Customer, forumTopic, languageId); } } } @@ -531,9 +498,6 @@ public virtual void UpdateTopic(ForumTopic forumTopic) } _forumTopicRepository.Update(forumTopic); - - //event notification - _services.EventPublisher.EntityUpdated(forumTopic); } public virtual ForumTopic MoveTopic(int forumTopicId, int newForumId) @@ -600,9 +564,6 @@ public virtual void DeletePost(ForumPost forumPost) UpdateForumStats(forumId); UpdateCustomerStats(customerId); - //event notification - _services.EventPublisher.EntityDeleted(forumPost); - } public virtual ForumPost GetPostById(int forumPostId) @@ -671,9 +632,6 @@ public virtual void InsertPost(ForumPost forumPost, bool sendNotifications) UpdateForumStats(forumId); UpdateCustomerStats(customerId); - //event notification - _services.EventPublisher.EntityInserted(forumPost); - //notifications if (sendNotifications) { @@ -693,7 +651,7 @@ public virtual void InsertPost(ForumPost forumPost, bool sendNotifications) if (!String.IsNullOrEmpty(subscription.Customer.Email)) { - _workflowMessageService.SendNewForumPostMessage(subscription.Customer, forumPost, forumTopic, forum, friendlyTopicPageIndex, languageId); + _services.MessageFactory.SendNewForumPostMessage(subscription.Customer, forumPost, friendlyTopicPageIndex, languageId); } } } @@ -708,9 +666,6 @@ public virtual void UpdatePost(ForumPost forumPost) } _forumPostRepository.Update(forumPost); - - //event notification - _services.EventPublisher.EntityUpdated(forumPost); } public virtual void DeletePrivateMessage(PrivateMessage privateMessage) @@ -721,9 +676,6 @@ public virtual void DeletePrivateMessage(PrivateMessage privateMessage) } _forumPrivateMessageRepository.Delete(privateMessage); - - //event notification - _services.EventPublisher.EntityDeleted(privateMessage); } public virtual PrivateMessage GetPrivateMessageById(int privateMessageId) @@ -789,9 +741,6 @@ public virtual void InsertPrivateMessage(PrivateMessage privateMessage) _forumPrivateMessageRepository.Insert(privateMessage); - //event notification - _services.EventPublisher.EntityInserted(privateMessage); - var customerTo = _customerService.GetCustomerById(privateMessage.ToCustomerId); if (customerTo == null) { @@ -804,7 +753,7 @@ public virtual void InsertPrivateMessage(PrivateMessage privateMessage) //Email notification if (_forumSettings.NotifyAboutPrivateMessages) { - _workflowMessageService.SendPrivateMessageNotification(customerTo, privateMessage, _services.WorkContext.WorkingLanguage.Id); + _services.MessageFactory.SendPrivateMessageNotification(customerTo, privateMessage, _services.WorkContext.WorkingLanguage.Id); } } @@ -816,14 +765,10 @@ public virtual void UpdatePrivateMessage(PrivateMessage privateMessage) if (privateMessage.IsDeletedByAuthor && privateMessage.IsDeletedByRecipient) { _forumPrivateMessageRepository.Delete(privateMessage); - //event notification - _services.EventPublisher.EntityDeleted(privateMessage); } else { _forumPrivateMessageRepository.Update(privateMessage); - //event notification - _services.EventPublisher.EntityUpdated(privateMessage); } } @@ -835,9 +780,6 @@ public virtual void DeleteSubscription(ForumSubscription forumSubscription) } _forumSubscriptionRepository.Delete(forumSubscription); - - //event notification - _services.EventPublisher.EntityDeleted(forumSubscription); } public virtual ForumSubscription GetSubscriptionById(int forumSubscriptionId) @@ -885,9 +827,6 @@ public virtual void InsertSubscription(ForumSubscription forumSubscription) } _forumSubscriptionRepository.Insert(forumSubscription); - - //event notification - _services.EventPublisher.EntityInserted(forumSubscription); } public virtual void UpdateSubscription(ForumSubscription forumSubscription) @@ -898,9 +837,6 @@ public virtual void UpdateSubscription(ForumSubscription forumSubscription) } _forumSubscriptionRepository.Update(forumSubscription); - - //event notification - _services.EventPublisher.EntityUpdated(forumSubscription); } public virtual bool IsCustomerAllowedToCreateTopic(Customer customer, Forum forum) diff --git a/src/Libraries/SmartStore.Services/Hooks/AclEntityHook.cs b/src/Libraries/SmartStore.Services/Hooks/AclEntityHook.cs index f49cf68fd9..b1518ba862 100644 --- a/src/Libraries/SmartStore.Services/Hooks/AclEntityHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/AclEntityHook.cs @@ -17,9 +17,9 @@ public AclEntityHook(Lazy aclService) _aclService = aclService; } - protected override void OnDeleted(IAclSupported entity, HookedEntity entry) + protected override void OnDeleted(IAclSupported entity, IHookedEntity entry) { - var entityType = entry.Entity.GetUnproxiedType(); + var entityType = entry.EntityType; var records = _aclService.Value.GetAclRecordsFor(entityType.Name, entry.Entity.Id); _toDelete.AddRange(records); diff --git a/src/Libraries/SmartStore.Services/Hooks/AuditableHook.cs b/src/Libraries/SmartStore.Services/Hooks/AuditableHook.cs index 589328ba83..cb22b6a757 100644 --- a/src/Libraries/SmartStore.Services/Hooks/AuditableHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/AuditableHook.cs @@ -7,7 +7,7 @@ namespace SmartStore.Services.Hooks [Important] public class AuditableHook : DbSaveHook { - protected override void OnInserting(IAuditable entity, HookedEntity entry) + protected override void OnInserting(IAuditable entity, IHookedEntity entry) { var now = DateTime.UtcNow; @@ -18,7 +18,7 @@ protected override void OnInserting(IAuditable entity, HookedEntity entry) entity.UpdatedOnUtc = now; } - protected override void OnUpdating(IAuditable entity, HookedEntity entry) + protected override void OnUpdating(IAuditable entity, IHookedEntity entry) { entity.UpdatedOnUtc = DateTime.UtcNow; } diff --git a/src/Libraries/SmartStore.Services/Hooks/FixProductMainPictureHook.cs b/src/Libraries/SmartStore.Services/Hooks/FixProductMainPictureHook.cs new file mode 100644 index 0000000000..91c9121160 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Hooks/FixProductMainPictureHook.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using SmartStore.Core.Data; +using SmartStore.Core.Data.Hooks; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Data.Utilities; + +namespace SmartStore.Services.Common +{ + public class FixProductMainPictureHook : DbSaveHook + { + private readonly IRepository _rsProduct; + private readonly HashSet _products = new HashSet(); + + public FixProductMainPictureHook(IRepository rsProduct) + { + _rsProduct = rsProduct; + } + + protected override void OnDeleting(ProductPicture entity, IHookedEntity entry) + { + Fix(entity); + } + + protected override void OnInserting(ProductPicture entity, IHookedEntity entry) + { + Fix(entity); + } + + protected override void OnUpdating(ProductPicture entity, IHookedEntity entry) + { + Fix(entity); + } + + private void Fix(ProductPicture entity) + { + var product = entity.Product ?? _rsProduct.GetById(entity.ProductId); + if (product != null) + { + _products.Add(product); + } + } + + public override void OnBeforeSaveCompleted() + { + foreach (var product in _products) + { + DataMigrator.FixProductMainPictureId(_rsProduct.Context, product); + } + + _products.Clear(); + } + } +} diff --git a/src/Libraries/SmartStore.Services/Hooks/LocalizedEntityHook.cs b/src/Libraries/SmartStore.Services/Hooks/LocalizedEntityHook.cs index bc96ec8dfb..c0f0942078 100644 --- a/src/Libraries/SmartStore.Services/Hooks/LocalizedEntityHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/LocalizedEntityHook.cs @@ -17,9 +17,9 @@ public LocalizedEntityHook(Lazy localizedEntityService) _localizedEntityService = localizedEntityService; } - protected override void OnDeleted(ILocalizedEntity entity, HookedEntity entry) + protected override void OnDeleted(ILocalizedEntity entity, IHookedEntity entry) { - var entityType = entry.Entity.GetUnproxiedType(); + var entityType = entry.EntityType; var localizedEntities = _localizedEntityService.Value.GetLocalizedProperties(entry.Entity.Id, entityType.Name); _toDelete.AddRange(localizedEntities); } diff --git a/src/Libraries/SmartStore.Services/Hooks/LocalizedEntityRelationHook.cs b/src/Libraries/SmartStore.Services/Hooks/LocalizedEntityRelationHook.cs index 74f55f1978..61555fb8eb 100644 --- a/src/Libraries/SmartStore.Services/Hooks/LocalizedEntityRelationHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/LocalizedEntityRelationHook.cs @@ -44,12 +44,12 @@ public LocalizedEntityRelationHook( _specificationAttributeService = specificationAttributeService; } - protected override void OnDeleting(BaseEntity entity, HookedEntity entry) + protected override void OnDeleting(BaseEntity entity, IHookedEntity entry) { - var type = entry.Entity.GetUnproxiedType(); + var type = entry.EntityType; if (!_candidateTypes.Contains(type)) - return; + throw new NotSupportedException(); if (type == typeof(SpecificationAttribute)) { diff --git a/src/Libraries/SmartStore.Services/Hooks/PictureHook.cs b/src/Libraries/SmartStore.Services/Hooks/PictureHook.cs index 6efa0f278b..81f434a3f5 100644 --- a/src/Libraries/SmartStore.Services/Hooks/PictureHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/PictureHook.cs @@ -37,12 +37,12 @@ public PictureHook( _productAttributeService = productAttributeService; } - protected override void OnDeleting(BaseEntity entity, HookedEntity entry) + protected override void OnDeleting(BaseEntity entity, IHookedEntity entry) { - var type = entry.Entity.GetUnproxiedType(); + var type = entry.EntityType; if (!_candidateTypes.Contains(type)) - return; + throw new NotSupportedException(); if (type == typeof(ProductAttributeOption)) { diff --git a/src/Libraries/SmartStore.Services/Hooks/ProductVariantAttributeHook.cs b/src/Libraries/SmartStore.Services/Hooks/ProductVariantAttributeHook.cs index 89873cbba6..3a31edcde0 100644 --- a/src/Libraries/SmartStore.Services/Hooks/ProductVariantAttributeHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/ProductVariantAttributeHook.cs @@ -17,7 +17,7 @@ public ProductVariantAttributeHook(Lazy productAttribu _productAttributeService = productAttributeService; } - protected override void OnDeleted(ProductVariantAttribute entity, HookedEntity entry) + protected override void OnDeleted(ProductVariantAttribute entity, IHookedEntity entry) { _toDelete.Add(entity.Id); } diff --git a/src/Libraries/SmartStore.Services/Hooks/ProductVariantAttributeValueHook.cs b/src/Libraries/SmartStore.Services/Hooks/ProductVariantAttributeValueHook.cs index 5b2d1adc6f..a1d4d15db5 100644 --- a/src/Libraries/SmartStore.Services/Hooks/ProductVariantAttributeValueHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/ProductVariantAttributeValueHook.cs @@ -17,7 +17,7 @@ public ProductVariantAttributeValueHook(Lazy productAt _productAttributeService = productAttributeService; } - protected override void OnDeleted(ProductVariantAttributeValue entity, HookedEntity entry) + protected override void OnDeleted(ProductVariantAttributeValue entity, IHookedEntity entry) { _toDelete.Add(entity); } diff --git a/src/Libraries/SmartStore.Services/Hooks/SearchQueryAliasHook.cs b/src/Libraries/SmartStore.Services/Hooks/SearchQueryAliasHook.cs index 66f0b91995..3efd100e04 100644 --- a/src/Libraries/SmartStore.Services/Hooks/SearchQueryAliasHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/SearchQueryAliasHook.cs @@ -51,7 +51,7 @@ public SearchQueryAliasHook( #region Utilities - private bool IsPropertyModified(HookedEntity entry, string propertyName) + private bool IsPropertyModified(IHookedEntity entry, string propertyName) { var result = false; @@ -82,24 +82,24 @@ private bool IsPropertyModified(HookedEntity entry, string propertyName) return result; } - private void RevertChanges(HookedEntity entry, string errorMessage) + private void RevertChanges(IHookedEntity entry, string errorMessage) { // throw exception in OnBeforeSaveCompleted _errorMessage = errorMessage; // revert changes - if (entry.Entry.State == System.Data.Entity.EntityState.Modified) + if (entry.State == EntityState.Modified) { - entry.Entry.State = System.Data.Entity.EntityState.Unchanged; + entry.State = EntityState.Unchanged; } - else if (entry.Entry.State == System.Data.Entity.EntityState.Added) + else if (entry.State == EntityState.Added) { - entry.Entry.State = System.Data.Entity.EntityState.Detached; + entry.State = EntityState.Detached; } } private bool HasAliasDuplicate( - HookedEntity entry, + IHookedEntity entry, BaseEntity baseEntity, Func, TEntity, bool> hasDuplicate = null) where TEntity : BaseEntity @@ -135,7 +135,7 @@ private bool HasAliasDuplicate( return false; } - private bool HasAliasDuplicate(HookedEntity entry, BaseEntity baseEntity) where T1 : BaseEntity where T2 : BaseEntity + private bool HasAliasDuplicate(IHookedEntity entry, BaseEntity baseEntity) where T1 : BaseEntity where T2 : BaseEntity { if (entry.InitialState == EntityState.Added || entry.InitialState == EntityState.Modified) { @@ -153,7 +153,7 @@ private bool HasAliasDuplicate(HookedEntity entry, BaseEntity baseEntity if (duplicate1 != null || duplicate2 != null) { - var type = baseEntity.GetUnproxiedType(); + var type = entry.EntityType; if (duplicate1 != null && duplicate1.Id == entity.Id && type == typeof(T1)) return false; @@ -258,7 +258,7 @@ private bool HasAliasDuplicate(LocalizedProperty property) } private bool HasEntityDuplicate( - HookedEntity entry, + IHookedEntity entry, BaseEntity baseEntity, Func getName, Expression> getDuplicate) where TEntity : BaseEntity @@ -281,17 +281,17 @@ private bool HasEntityDuplicate( #endregion - protected override void OnDeleting(BaseEntity entity, HookedEntity entry) + protected override void OnDeleting(BaseEntity entity, IHookedEntity entry) { HookObject(entity, entry); } - protected override void OnInserting(BaseEntity entity, HookedEntity entry) + protected override void OnInserting(BaseEntity entity, IHookedEntity entry) { HookObject(entity, entry); } - protected override void OnUpdating(BaseEntity entity, HookedEntity entry) + protected override void OnUpdating(BaseEntity entity, IHookedEntity entry) { HookObject(entity, entry); } @@ -307,12 +307,12 @@ public override void OnBeforeSaveCompleted() } } - private void HookObject(BaseEntity baseEntity, HookedEntity entry) + private void HookObject(BaseEntity baseEntity, IHookedEntity entry) { - var type = baseEntity.GetUnproxiedType(); + var type = entry.EntityType; if (!_candidateTypes.Contains(type)) - return; + throw new NotSupportedException(); if (type == typeof(SpecificationAttribute)) { diff --git a/src/Libraries/SmartStore.Services/Hooks/SlugSupportedHook.cs b/src/Libraries/SmartStore.Services/Hooks/SlugSupportedHook.cs index 381b5b1f6e..e744e41f2d 100644 --- a/src/Libraries/SmartStore.Services/Hooks/SlugSupportedHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/SlugSupportedHook.cs @@ -17,9 +17,9 @@ public SlugSupportedHook(Lazy urlRecordService) _urlRecordService = urlRecordService; } - protected override void OnDeleted(ISlugSupported entity, HookedEntity entry) + protected override void OnDeleted(ISlugSupported entity, IHookedEntity entry) { - var entityType = entry.Entity.GetUnproxiedType(); + var entityType = entry.EntityType; var records = _urlRecordService.Value.GetUrlRecordsFor(entityType.Name, entry.Entity.Id); _toDelete.AddRange(records); } diff --git a/src/Libraries/SmartStore.Services/Hooks/SoftDeletableHook.cs b/src/Libraries/SmartStore.Services/Hooks/SoftDeletableHook.cs index de191f4e53..a7fed44987 100644 --- a/src/Libraries/SmartStore.Services/Hooks/SoftDeletableHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/SoftDeletableHook.cs @@ -6,6 +6,7 @@ using SmartStore.Core.Domain.Seo; using SmartStore.Services.Security; using SmartStore.Services.Seo; +using SmartStore.Data; namespace SmartStore.Services.Hooks { @@ -18,7 +19,7 @@ public SoftDeletablePreUpdateHook(IComponentContext ctx) _ctx = ctx; } - protected override void OnUpdating(ISoftDeletable entity, HookedEntity entry) + protected override void OnUpdating(ISoftDeletable entity, IHookedEntity entry) { var baseEntity = entry.Entity; @@ -27,7 +28,7 @@ protected override void OnUpdating(ISoftDeletable entity, HookedEntity entry) if (!deletedModified) return; - var entityType = baseEntity.GetUnproxiedType(); + var entityType = entry.EntityType; // mark orphaned ACL records as idle var aclSupported = baseEntity as IAclSupported; diff --git a/src/Libraries/SmartStore.Services/Hooks/StoreMappingEntityHook.cs b/src/Libraries/SmartStore.Services/Hooks/StoreMappingEntityHook.cs index 47a812ee04..422ba4c281 100644 --- a/src/Libraries/SmartStore.Services/Hooks/StoreMappingEntityHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/StoreMappingEntityHook.cs @@ -18,9 +18,9 @@ public StoreMappingEntityHook(Lazy storeMappingService) _storeMappingService = storeMappingService; } - protected override void OnDeleted(IStoreMappingSupported entity, HookedEntity entry) + protected override void OnDeleted(IStoreMappingSupported entity, IHookedEntity entry) { - var entityType = entry.Entity.GetUnproxiedType(); + var entityType = entry.EntityType; var records = _storeMappingService.Value .GetStoreMappingsFor(entityType.Name, entry.Entity.Id) diff --git a/src/Libraries/SmartStore.Services/Hooks/StoreSaveHook.cs b/src/Libraries/SmartStore.Services/Hooks/StoreSaveHook.cs new file mode 100644 index 0000000000..12b02a4cc1 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Hooks/StoreSaveHook.cs @@ -0,0 +1,59 @@ +using System; +using SmartStore.Services.Media; +using SmartStore.Core.Data.Hooks; +using SmartStore.Core.Domain.Stores; +using SmartStore.Services.Tasks; +using SmartStore.Services.Stores; +using System.Web; +using SmartStore.Utilities; + +namespace SmartStore.Services.Hooks +{ + public class StoreSaveHook : DbSaveHook + { + private readonly IPictureService _pictureService; + private readonly ITaskScheduler _taskScheduler; + private readonly IStoreService _storeService; + private readonly HttpContextBase _httpContext; + + public StoreSaveHook(IPictureService pictureService, ITaskScheduler taskScheduler, IStoreService storeService, HttpContextBase httpContext) + { + _pictureService = pictureService; + _taskScheduler = taskScheduler; + _storeService = storeService; + _httpContext = httpContext; + } + + protected override void OnUpdating(Store entity, IHookedEntity entry) + { + if (entry.IsPropertyModified(nameof(entity.ContentDeliveryNetwork))) + { + _pictureService.ClearUrlCache(); + } + } + + protected override void OnInserted(Store entity, IHookedEntity entry) + { + TryChangeSchedulerBaseUrl(); + } + + protected override void OnUpdated(Store entity, IHookedEntity entry) + { + TryChangeSchedulerBaseUrl(); + } + + protected override void OnDeleted(Store entity, IHookedEntity entry) + { + _pictureService.ClearUrlCache(); + TryChangeSchedulerBaseUrl(); + } + + private void TryChangeSchedulerBaseUrl() + { + if (CommonHelper.GetAppSetting("sm:TaskSchedulerBaseUrl").IsWebUrl() == false) + { + _taskScheduler.SetBaseUrl(_storeService, _httpContext); + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/ICommonServices.cs b/src/Libraries/SmartStore.Services/ICommonServices.cs index 9379c2ee76..0f2030395f 100644 --- a/src/Libraries/SmartStore.Services/ICommonServices.cs +++ b/src/Libraries/SmartStore.Services/ICommonServices.cs @@ -11,100 +11,33 @@ using SmartStore.Services.Stores; using SmartStore.Services.Helpers; using Autofac; +using SmartStore.Services.Media; +using SmartStore.Services.Messages; namespace SmartStore.Services { public interface ICommonServices { - IComponentContext Container - { - get; - } - - IApplicationEnvironment ApplicationEnvironment - { - get; - } - - ICacheManager Cache - { - get; - } - - IRequestCache RequestCache - { - get; - } - - IDisplayControl DisplayControl - { - get; - } - - IDbContext DbContext - { - get; - } - - IStoreContext StoreContext - { - get; - } - - IWebHelper WebHelper - { - get; - } - - IWorkContext WorkContext - { - get; - } - - IEventPublisher EventPublisher - { - get; - } - - ILocalizationService Localization - { - get; - } - - ICustomerActivityService CustomerActivity - { - get; - } - - INotifier Notifier - { - get; - } - - IPermissionService Permissions - { - get; - } - - ISettingService Settings - { - get; - } - - IStoreService StoreService - { - get; - } - - IDateTimeHelper DateTimeHelper - { - get; - } - - IChronometer Chronometer - { - get; - } + IComponentContext Container { get; } + IApplicationEnvironment ApplicationEnvironment { get; } + ICacheManager Cache { get; } + IRequestCache RequestCache { get; } + IDisplayControl DisplayControl { get; } + IDbContext DbContext { get; } + IStoreContext StoreContext { get; } + IWebHelper WebHelper { get; } + IWorkContext WorkContext { get; } + IEventPublisher EventPublisher { get; } + ILocalizationService Localization { get; } + ICustomerActivityService CustomerActivity { get; } + IPictureService PictureService { get; } + INotifier Notifier { get; } + IPermissionService Permissions { get; } + ISettingService Settings { get; } + IStoreService StoreService { get; } + IDateTimeHelper DateTimeHelper { get; } + IChronometer Chronometer { get; } + IMessageFactory MessageFactory { get; } } public static class ICommonServicesExtensions diff --git a/src/Libraries/SmartStore.Services/Localization/LanguageService.cs b/src/Libraries/SmartStore.Services/Localization/LanguageService.cs index 973a7afb94..3bcb3646e3 100644 --- a/src/Libraries/SmartStore.Services/Localization/LanguageService.cs +++ b/src/Libraries/SmartStore.Services/Localization/LanguageService.cs @@ -16,7 +16,7 @@ namespace SmartStore.Services.Localization public partial class LanguageService : ILanguageService { private const string LANGUAGES_COUNT = "SmartStore.language.count-{0}"; - private const string LANGUAGES_PATTERN_KEY = "SmartStore.language."; + private const string LANGUAGES_PATTERN_KEY = "SmartStore.language.*"; private readonly IRepository _languageRepository; private readonly IStoreMappingService _storeMappingService; @@ -39,23 +39,22 @@ public LanguageService( IStoreService storeService, IStoreContext storeContext) { - this._requestCache = requestCache; - this._cache = cache; - this._languageRepository = languageRepository; - this._settingService = settingService; - this._localizationSettings = localizationSettings; - this._eventPublisher = eventPublisher; - this._storeMappingService = storeMappingService; - this._storeService = storeService; - this._storeContext = storeContext; + _requestCache = requestCache; + _cache = cache; + _languageRepository = languageRepository; + _settingService = settingService; + _localizationSettings = localizationSettings; + _eventPublisher = eventPublisher; + _storeMappingService = storeMappingService; + _storeService = storeService; + _storeContext = storeContext; } public virtual void DeleteLanguage(Language language) { - if (language == null) - throw new ArgumentNullException("language"); - - //update default admin area language (if required) + Guard.NotNull(language, nameof(language)); + + // Update default admin area language (if required) if (_localizationSettings.DefaultAdminLanguageId == language.Id) { foreach (var activeLanguage in GetAllLanguages()) @@ -72,11 +71,7 @@ public virtual void DeleteLanguage(Language language) _languageRepository.Delete(language); // cache - _requestCache.RemoveByPattern(LANGUAGES_PATTERN_KEY); - _cache.RemoveByPattern(ServiceCacheConsumer.STORE_LANGUAGE_MAP_KEY); - - //event notification - _eventPublisher.EntityDeleted(language); + _requestCache.RemoveByPattern(LANGUAGES_PATTERN_KEY); } public virtual IList GetAllLanguages(bool showHidden = false, int storeId = 0) @@ -152,10 +147,6 @@ public virtual void InsertLanguage(Language language) // cache _requestCache.RemoveByPattern(LANGUAGES_PATTERN_KEY); - _cache.RemoveByPattern(ServiceCacheConsumer.STORE_LANGUAGE_MAP_KEY); - - //event notification - _eventPublisher.EntityInserted(language); } public virtual void UpdateLanguage(Language language) @@ -168,10 +159,6 @@ public virtual void UpdateLanguage(Language language) //cache _requestCache.RemoveByPattern(LANGUAGES_PATTERN_KEY); - _cache.RemoveByPattern(ServiceCacheConsumer.STORE_LANGUAGE_MAP_KEY); - - //event notification - _eventPublisher.EntityUpdated(language); } public virtual bool IsPublishedLanguage(string seoCode, int storeId = 0) @@ -239,7 +226,7 @@ public virtual int GetDefaultLanguageId(int storeId = 0) /// A map of store languages where key is the store id and values are tuples of language ids and seo codes protected virtual Multimap GetStoreLanguageMap() { - var result = _cache.Get(ServiceCacheConsumer.STORE_LANGUAGE_MAP_KEY, () => + var result = _cache.Get(ServiceCacheBuster.STORE_LANGUAGE_MAP_KEY, () => { var map = new Multimap(); diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizationExtensions.cs b/src/Libraries/SmartStore.Services/Localization/LocalizationExtensions.cs new file mode 100644 index 0000000000..d3f3094655 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Localization/LocalizationExtensions.cs @@ -0,0 +1,530 @@ +using System; +using System.Globalization; +using System.Linq.Expressions; +using System.Reflection; +using System.Xml; +using SmartStore.ComponentModel; +using SmartStore.Core; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Infrastructure; +using SmartStore.Core.Plugins; +using SmartStore.Utilities; + +namespace SmartStore.Services.Localization +{ + public static class LocalizationExtensions + { + /// + /// Get localized property of an entity + /// + /// Entity type + /// Entity + /// Key selector + /// When true, additionally checks whether the localized value contains empty HTML only and falls back to the default value if so. + /// Localized property + public static string GetLocalized(this T entity, Expression> keySelector, bool detectEmptyHtml = false) + where T : BaseEntity, ILocalizedEntity + { + Guard.NotNull(entity, nameof(entity)); + + return GetLocalized( + entity, + typeof(T).Name, + entity.Id, + keySelector, + EngineContext.Current.Resolve().WorkingLanguage.Id, + detectEmptyHtml: detectEmptyHtml); + } + + /// + /// Get localized property of an entity + /// + /// Entity type + /// Entity + /// Key selector + /// Language identifier + /// A value indicating whether to return default value (if localized is not found) + /// A value indicating whether to ensure that we have at least two published languages; otherwise, load only default value + /// When true, additionally checks whether the localized value contains empty HTML only and falls back to the default value if so. + /// Localized property + public static string GetLocalized(this T entity, + Expression> keySelector, + int languageId, + bool returnDefaultValue = true, + bool ensureTwoPublishedLanguages = true, + bool detectEmptyHtml = false) + where T : BaseEntity, ILocalizedEntity + { + Guard.NotNull(entity, nameof(entity)); + + return GetLocalized( + entity, + typeof(T).Name, + entity.Id, + keySelector, + languageId, + returnDefaultValue, + ensureTwoPublishedLanguages, + detectEmptyHtml); + } + + /// + /// Get localized property of an entity + /// + /// Entity type + /// Property type + /// Entity + /// Key selector + /// Language identifier + /// A value indicating whether to return default value (if localized is not found) + /// A value indicating whether to ensure that we have at least two published languages; otherwise, load only default value + /// When true, additionally checks whether the localized value contains empty HTML only and falls back to the default value if so. + /// Localized property + public static TPropType GetLocalized(this T entity, + Expression> keySelector, + int languageId, + bool returnDefaultValue = true, + bool ensureTwoPublishedLanguages = true, + bool detectEmptyHtml = false) + where T : BaseEntity, ILocalizedEntity + { + Guard.NotNull(entity, nameof(entity)); + + return GetLocalized( + entity, + typeof(T).Name, + entity.Id, + keySelector, + languageId, + returnDefaultValue, + ensureTwoPublishedLanguages, + detectEmptyHtml); + } + + /// + /// Get localized property of an instance + /// + /// Node + /// Key selector + /// Localized property + public static string GetLocalized(this ICategoryNode node, Expression> keySelector) + { + Guard.NotNull(node, nameof(node)); + + return GetLocalized( + node, + "Category", + node.Id, + keySelector, + EngineContext.Current.Resolve().WorkingLanguage.Id); + } + + /// + /// Get localized property of an instance + /// + /// Node + /// Key selector + /// /// Language identifier + /// Localized property + public static string GetLocalized(this ICategoryNode node, Expression> keySelector, int languageId) + { + Guard.NotNull(node, nameof(node)); + + return GetLocalized( + node, + "Category", + node.Id, + keySelector, + languageId); + } + + internal static TPropType GetLocalized(T entity, + string localeKeyGroup, + int entityId, + Expression> keySelector, + int languageId, + bool returnDefaultValue = true, + bool ensureTwoPublishedLanguages = true, + bool detectEmptyHtml = false) + where T : ILocalizedEntity + { + TPropType result = default(TPropType); + string resultStr = string.Empty; + + var member = keySelector.Body as MemberExpression; + if (member == null) + { + throw new ArgumentException($"Expression '{keySelector}' refers to a method, not a property."); + } + + var propInfo = member.Member as PropertyInfo; + if (propInfo == null) + { + throw new ArgumentException($"Expression '{keySelector}' refers to a field, not a property."); + } + + // Load localized value + string localeKey = propInfo.Name; + + if (languageId > 0) + { + // Ensure that we have at least two published languages + bool loadLocalizedValue = true; + if (ensureTwoPublishedLanguages) + { + var lService = EngineContext.Current.Resolve(); + var totalPublishedLanguages = lService.GetLanguagesCount(false); + loadLocalizedValue = totalPublishedLanguages >= 2; + } + + // Localized value + if (loadLocalizedValue) + { + var leService = EngineContext.Current.Resolve(); + resultStr = leService.GetLocalizedValue(languageId, entityId, localeKeyGroup, localeKey); + + if (detectEmptyHtml && resultStr.HasValue() && resultStr.RemoveHtml().IsEmpty()) + { + resultStr = string.Empty; + } + + if (resultStr.HasValue()) + { + result = (TPropType)resultStr.Convert(typeof(TPropType), CultureInfo.InvariantCulture); + } + } + } + + // Set default value if required + if (returnDefaultValue && resultStr.IsEmpty()) + { + var localizer = keySelector.Compile(); + result = localizer(entity); + } + + return result; + } + + + /// + /// Get localized value of enum + /// + /// Enum + /// Enum value + /// Localization service + /// Work context + /// Localized value + public static string GetLocalizedEnum(this T enumValue, ILocalizationService localizationService, IWorkContext workContext) + where T : struct + { + Guard.NotNull(workContext, nameof(workContext)); + + return GetLocalizedEnum(enumValue, localizationService, workContext.WorkingLanguage.Id); + } + + /// + /// Get localized value of enum + /// + /// Enum + /// Enum value + /// Localization service + /// Language identifier + /// Localized value + public static string GetLocalizedEnum(this T enumValue, ILocalizationService localizationService, int languageId = 0) + where T : struct + { + Guard.NotNull(localizationService, nameof(localizationService)); + + if (!typeof(T).IsEnum) throw new ArgumentException("T must be an enumerated type"); + + //localized value + string resourceName = string.Format("Enums.{0}.{1}", + typeof(T).ToString(), + //Convert.ToInt32(enumValue) + enumValue.ToString()); + + string result = localizationService.GetResource(resourceName, languageId, false, "", true); + + // Set default value if required + if (String.IsNullOrEmpty(result)) + result = Inflector.Titleize(enumValue.ToString()); + + return result; + } + + /// + /// Delete a locale resource + /// + /// Plugin + /// Resource name + public static void DeletePluginLocaleResource(this BasePlugin plugin, + string resourceName) + { + var localizationService = EngineContext.Current.Resolve(); + var languageService = EngineContext.Current.Resolve(); + DeletePluginLocaleResource(plugin, localizationService, + languageService, resourceName); + } + + /// + /// Delete a locale resource + /// + /// Plugin + /// Localization service + /// Language service + /// Resource name + public static void DeletePluginLocaleResource(this BasePlugin plugin, + ILocalizationService localizationService, ILanguageService languageService, + string resourceName) + { + //actually plugin instance is not required + if (plugin == null) + throw new ArgumentNullException("plugin"); + if (localizationService == null) + throw new ArgumentNullException("localizationService"); + if (languageService == null) + throw new ArgumentNullException("languageService"); + + foreach (var lang in languageService.GetAllLanguages(true)) + { + var lsr = localizationService.GetLocaleStringResourceByName(resourceName, lang.Id, false); + if (lsr != null) + localizationService.DeleteLocaleStringResource(lsr); + } + } + + /// + /// Add a locale resource (if new) or update an existing one + /// + /// Plugin + /// Resource name + /// Resource value + public static void AddOrUpdatePluginLocaleResource(this BasePlugin plugin, + string resourceName, string resourceValue) + { + var localizationService = EngineContext.Current.Resolve(); + var languageService = EngineContext.Current.Resolve(); + AddOrUpdatePluginLocaleResource(plugin, localizationService, + languageService, resourceName, resourceValue); + } + + /// + /// Add a locale resource (if new) or update an existing one + /// + /// Plugin + /// Localization service + /// Language service + /// Resource name + /// Resource value + public static void AddOrUpdatePluginLocaleResource(this BasePlugin plugin, + ILocalizationService localizationService, ILanguageService languageService, + string resourceName, string resourceValue) + { + //actually plugin instance is not required + if (plugin == null) + throw new ArgumentNullException("plugin"); + if (localizationService == null) + throw new ArgumentNullException("localizationService"); + if (languageService == null) + throw new ArgumentNullException("languageService"); + + foreach (var lang in languageService.GetAllLanguages(true)) + { + var lsr = localizationService.GetLocaleStringResourceByName(resourceName, lang.Id, false); + if (lsr == null) + { + lsr = new LocaleStringResource() + { + LanguageId = lang.Id, + ResourceName = resourceName, + ResourceValue = resourceValue, + IsFromPlugin = true + }; + localizationService.InsertLocaleStringResource(lsr); + } + else + { + lsr.ResourceValue = resourceValue; + localizationService.UpdateLocaleStringResource(lsr); + } + } + } + + public static void AddPluginLocaleResource(this BasePlugin plugin, ILocalizationService localizationService, string resourceName, string value, int languageID) + { + if (languageID != 0) + { + localizationService.InsertLocaleStringResource(new LocaleStringResource + { + LanguageId = languageID, + ResourceName = resourceName, + ResourceValue = value, + IsFromPlugin = true + }); + } + } + + /// + /// Get localized property value of a plugin + /// + /// Plugin + /// Plugin + /// Localization service + /// Name of the property + /// Language identifier + /// A value indicating whether to return default value (if localized is not found) + /// Localized value + public static string GetLocalizedValue(this T plugin, ILocalizationService localizationService, string propertyName, int languageId = 0, bool returnDefaultValue = true) + where T : IPlugin + { + if (plugin == null) + throw new ArgumentNullException("plugin"); + + if (plugin.PluginDescriptor == null) + throw new ArgumentNullException("PluginDescriptor cannot be loaded"); + + return plugin.PluginDescriptor.GetLocalizedValue(localizationService, propertyName, languageId, returnDefaultValue); + } + + /// + /// Get localized property value of a plugin + /// + /// Plugin descriptor + /// Localization service + /// Name of the property + /// Language identifier + /// A value indicating whether to return default value (if localized is not found) + /// Localized value + public static string GetLocalizedValue(this PluginDescriptor descriptor, ILocalizationService localizationService, string propertyName, int languageId = 0, bool returnDefaultValue = true) + { + if (localizationService == null) + throw new ArgumentNullException("localizationService"); + + if (descriptor == null) + throw new ArgumentNullException("descriptor"); + + if (propertyName == null) + throw new ArgumentNullException("name"); + + string systemName = descriptor.SystemName; + string resourceName = string.Format("Plugins.{0}.{1}", propertyName, systemName); + string result = localizationService.GetResource(resourceName, languageId, false, "", true); + + if (String.IsNullOrEmpty(result) && returnDefaultValue) + { + var fastProp = FastProperty.GetProperty(descriptor.GetType(), propertyName); + if (fastProp != null) + { + result = fastProp.GetValue(descriptor) as string; + } + } + + return result; + } + + /// + /// Save localized plugin descriptor value + /// + /// Plugin + /// Plugin + /// Localization service + /// Language identifier + /// Name of the property + /// Localized value + public static void SaveLocalizedValue(this T plugin, ILocalizationService localizationService, int languageId, + string propertyName, string value) where T : IPlugin + { + if (plugin == null) + throw new ArgumentNullException("plugin"); + + if (plugin.PluginDescriptor == null) + throw new ArgumentNullException("PluginDescriptor cannot be loaded"); + + plugin.PluginDescriptor.SaveLocalizedValue(localizationService, languageId, propertyName, value); + } + + /// + /// Save localized plugin descriptor value + /// + /// Plugin + /// Localization service + /// Language identifier + /// Name of the property + /// Localized value + public static void SaveLocalizedValue(this PluginDescriptor descriptor, ILocalizationService localizationService, int languageId, + string propertyName, string value) + { + if (localizationService == null) + throw new ArgumentNullException("localizationService"); + + if (languageId == 0) + throw new ArgumentOutOfRangeException("languageId", "Language ID should not be 0"); + + if (descriptor == null) + throw new ArgumentNullException("descriptor"); + + if (propertyName == null) + throw new ArgumentNullException("name"); + + string systemName = descriptor.SystemName; + string resourceName = string.Format("Plugins.{0}.{1}", propertyName, systemName); + var resource = localizationService.GetLocaleStringResourceByName(resourceName, languageId, false); + + if (resource != null) + { + if (string.IsNullOrWhiteSpace(value)) + { + //delete + localizationService.DeleteLocaleStringResource(resource); + } + else + { + //update + resource.ResourceValue = value; + localizationService.UpdateLocaleStringResource(resource); + } + } + else + { + if (!string.IsNullOrWhiteSpace(value)) + { + //insert + resource = new LocaleStringResource() + { + LanguageId = languageId, + ResourceName = resourceName, + ResourceValue = value, + }; + localizationService.InsertLocaleStringResource(resource); + } + } + } + + /// + /// Import language resources from XML file + /// + /// Language + /// XML + public static void ImportResourcesFromXml(this ILocalizationService service, + Language language, + string xml, + string rootKey = null, + bool sourceIsPlugin = false, + ImportModeFlags mode = ImportModeFlags.Insert | ImportModeFlags.Update, + bool updateTouchedResources = false) + { + if (language == null) + throw new ArgumentNullException("language"); + + if (String.IsNullOrEmpty(xml)) + return; + + var xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(xml); + + service.ImportResourcesFromXml(language, xmlDoc, rootKey, sourceIsPlugin, mode, updateTouchedResources); + } + } +} diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizationExtentions.cs b/src/Libraries/SmartStore.Services/Localization/LocalizationExtentions.cs deleted file mode 100644 index 5c0ae71618..0000000000 --- a/src/Libraries/SmartStore.Services/Localization/LocalizationExtentions.cs +++ /dev/null @@ -1,437 +0,0 @@ -using System; -using System.Linq.Expressions; -using System.Reflection; -using System.Xml; -using SmartStore.ComponentModel; -using SmartStore.Core; -using SmartStore.Core.Domain.DataExchange; -using SmartStore.Core.Domain.Localization; -using SmartStore.Core.Infrastructure; -using SmartStore.Core.Plugins; -using SmartStore.Utilities; - -namespace SmartStore.Services.Localization -{ - public static class LocalizationExtentions - { - /// - /// Get localized property of an entity - /// - /// Entity type - /// Entity - /// Key selector - /// Localized property - public static string GetLocalized(this T entity, Expression> keySelector) - where T : BaseEntity, ILocalizedEntity - { - var workContext = EngineContext.Current.Resolve(); - return GetLocalized(entity, keySelector, workContext.WorkingLanguage.Id); - } - /// - /// Get localized property of an entity - /// - /// Entity type - /// Entity - /// Key selector - /// Language identifier - /// A value indicating whether to return default value (if localized is not found) - /// A value indicating whether to ensure that we have at least two published languages; otherwise, load only default value - /// Localized property - public static string GetLocalized(this T entity, - Expression> keySelector, int languageId, - bool returnDefaultValue = true, bool ensureTwoPublishedLanguages = true) - where T : BaseEntity, ILocalizedEntity - { - return GetLocalized(entity, keySelector, languageId, returnDefaultValue, ensureTwoPublishedLanguages); - } - /// - /// Get localized property of an entity - /// - /// Entity type - /// Property type - /// Entity - /// Key selector - /// Language identifier - /// A value indicating whether to return default value (if localized is not found) - /// A value indicating whether to ensure that we have at least two published languages; otherwise, load only default value - /// Localized property - public static TPropType GetLocalized(this T entity, - Expression> keySelector, - int languageId, - bool returnDefaultValue = true, - bool ensureTwoPublishedLanguages = true) - where T : BaseEntity, ILocalizedEntity - { - Guard.NotNull(entity, nameof(entity)); - - var member = keySelector.Body as MemberExpression; - if (member == null) - { - throw new ArgumentException(string.Format( - "Expression '{0}' refers to a method, not a property.", - keySelector)); - } - - var propInfo = member.Member as PropertyInfo; - if (propInfo == null) - { - throw new ArgumentException(string.Format( - "Expression '{0}' refers to a field, not a property.", - keySelector)); - } - - TPropType result = default(TPropType); - string resultStr = string.Empty; - - // load localized value - string localeKeyGroup = typeof(T).Name; - string localeKey = propInfo.Name; - - if (languageId > 0) - { - // ensure that we have at least two published languages - bool loadLocalizedValue = true; - if (ensureTwoPublishedLanguages) - { - var lService = EngineContext.Current.Resolve(); - var totalPublishedLanguages = lService.GetLanguagesCount(false); - loadLocalizedValue = totalPublishedLanguages >= 2; - } - - // localized value - if (loadLocalizedValue) - { - var leService = EngineContext.Current.Resolve(); - resultStr = leService.GetLocalizedValue(languageId, entity.Id, localeKeyGroup, localeKey); - if (!String.IsNullOrEmpty(resultStr)) - result = resultStr.Convert(); - } - } - - //set default value if required - if (String.IsNullOrEmpty(resultStr) && returnDefaultValue) - { - //var localizer = (Func)_compiledExpressions.GetOrAdd(keySelector, exp => exp.Compile()); // --> MEM LEAK - var localizer = keySelector.Compile(); - result = localizer(entity); - } - - return result; - } - - /// - /// Get localized value of enum - /// - /// Enum - /// Enum value - /// Localization service - /// Work context - /// Localized value - public static string GetLocalizedEnum(this T enumValue, ILocalizationService localizationService, IWorkContext workContext) - where T : struct - { - Guard.NotNull(workContext, nameof(workContext)); - - return GetLocalizedEnum(enumValue, localizationService, workContext.WorkingLanguage.Id); - } - /// - /// Get localized value of enum - /// - /// Enum - /// Enum value - /// Localization service - /// Language identifier - /// Localized value - public static string GetLocalizedEnum(this T enumValue, ILocalizationService localizationService, int languageId = 0) - where T : struct - { - Guard.NotNull(localizationService, nameof(localizationService)); - - if (!typeof(T).IsEnum) throw new ArgumentException("T must be an enumerated type"); - - //localized value - string resourceName = string.Format("Enums.{0}.{1}", - typeof(T).ToString(), - //Convert.ToInt32(enumValue) - enumValue.ToString()); - - string result = localizationService.GetResource(resourceName, languageId, false, "", true); - - //set default value if required - if (String.IsNullOrEmpty(result)) - result = Inflector.Titleize(enumValue.ToString()); - - return result; - } - - /// - /// Delete a locale resource - /// - /// Plugin - /// Resource name - public static void DeletePluginLocaleResource(this BasePlugin plugin, - string resourceName) - { - var localizationService = EngineContext.Current.Resolve(); - var languageService = EngineContext.Current.Resolve(); - DeletePluginLocaleResource(plugin, localizationService, - languageService, resourceName); - } - /// - /// Delete a locale resource - /// - /// Plugin - /// Localization service - /// Language service - /// Resource name - public static void DeletePluginLocaleResource(this BasePlugin plugin, - ILocalizationService localizationService, ILanguageService languageService, - string resourceName) - { - //actually plugin instance is not required - if (plugin == null) - throw new ArgumentNullException("plugin"); - if (localizationService == null) - throw new ArgumentNullException("localizationService"); - if (languageService == null) - throw new ArgumentNullException("languageService"); - - foreach (var lang in languageService.GetAllLanguages(true)) - { - var lsr = localizationService.GetLocaleStringResourceByName(resourceName, lang.Id, false); - if (lsr != null) - localizationService.DeleteLocaleStringResource(lsr); - } - } - /// - /// Add a locale resource (if new) or update an existing one - /// - /// Plugin - /// Resource name - /// Resource value - public static void AddOrUpdatePluginLocaleResource(this BasePlugin plugin, - string resourceName, string resourceValue) - { - var localizationService = EngineContext.Current.Resolve(); - var languageService = EngineContext.Current.Resolve(); - AddOrUpdatePluginLocaleResource(plugin, localizationService, - languageService, resourceName, resourceValue); - } - /// - /// Add a locale resource (if new) or update an existing one - /// - /// Plugin - /// Localization service - /// Language service - /// Resource name - /// Resource value - public static void AddOrUpdatePluginLocaleResource(this BasePlugin plugin, - ILocalizationService localizationService, ILanguageService languageService, - string resourceName, string resourceValue) - { - //actually plugin instance is not required - if (plugin == null) - throw new ArgumentNullException("plugin"); - if (localizationService == null) - throw new ArgumentNullException("localizationService"); - if (languageService == null) - throw new ArgumentNullException("languageService"); - - foreach (var lang in languageService.GetAllLanguages(true)) - { - var lsr = localizationService.GetLocaleStringResourceByName(resourceName, lang.Id, false); - if (lsr == null) - { - lsr = new LocaleStringResource() - { - LanguageId = lang.Id, - ResourceName = resourceName, - ResourceValue = resourceValue, - IsFromPlugin = true - }; - localizationService.InsertLocaleStringResource(lsr); - } - else - { - lsr.ResourceValue = resourceValue; - localizationService.UpdateLocaleStringResource(lsr); - } - } - } - - public static void AddPluginLocaleResource(this BasePlugin plugin, ILocalizationService localizationService, string resourceName, string value, int languageID) - { - if (languageID != 0) - { - localizationService.InsertLocaleStringResource(new LocaleStringResource - { - LanguageId = languageID, - ResourceName = resourceName, - ResourceValue = value, - IsFromPlugin = true - }); - } - } - - /// - /// Get localized property value of a plugin - /// - /// Plugin - /// Plugin - /// Localization service - /// Name of the property - /// Language identifier - /// A value indicating whether to return default value (if localized is not found) - /// Localized value - public static string GetLocalizedValue(this T plugin, ILocalizationService localizationService, string propertyName, int languageId = 0, bool returnDefaultValue = true) - where T : IPlugin - { - if (plugin == null) - throw new ArgumentNullException("plugin"); - - if (plugin.PluginDescriptor == null) - throw new ArgumentNullException("PluginDescriptor cannot be loaded"); - - return plugin.PluginDescriptor.GetLocalizedValue(localizationService, propertyName, languageId, returnDefaultValue); - } - - /// - /// Get localized property value of a plugin - /// - /// Plugin descriptor - /// Localization service - /// Name of the property - /// Language identifier - /// A value indicating whether to return default value (if localized is not found) - /// Localized value - public static string GetLocalizedValue(this PluginDescriptor descriptor, ILocalizationService localizationService, string propertyName, int languageId = 0, bool returnDefaultValue = true) - { - if (localizationService == null) - throw new ArgumentNullException("localizationService"); - - if (descriptor == null) - throw new ArgumentNullException("descriptor"); - - if (propertyName == null) - throw new ArgumentNullException("name"); - - string systemName = descriptor.SystemName; - string resourceName = string.Format("Plugins.{0}.{1}", propertyName, systemName); - string result = localizationService.GetResource(resourceName, languageId, false, "", true); - - if (String.IsNullOrEmpty(result) && returnDefaultValue) - { - var fastProp = FastProperty.GetProperty(descriptor.GetType(), propertyName); - if (fastProp != null) - { - result = fastProp.GetValue(descriptor) as string; - } - } - - return result; - } - - /// - /// Save localized plugin descriptor value - /// - /// Plugin - /// Plugin - /// Localization service - /// Language identifier - /// Name of the property - /// Localized value - public static void SaveLocalizedValue(this T plugin, ILocalizationService localizationService, int languageId, - string propertyName, string value) where T : IPlugin - { - if (plugin == null) - throw new ArgumentNullException("plugin"); - - if (plugin.PluginDescriptor == null) - throw new ArgumentNullException("PluginDescriptor cannot be loaded"); - - plugin.PluginDescriptor.SaveLocalizedValue(localizationService, languageId, propertyName, value); - } - - /// - /// Save localized plugin descriptor value - /// - /// Plugin - /// Localization service - /// Language identifier - /// Name of the property - /// Localized value - public static void SaveLocalizedValue(this PluginDescriptor descriptor, ILocalizationService localizationService, int languageId, - string propertyName, string value) - { - if (localizationService == null) - throw new ArgumentNullException("localizationService"); - - if (languageId == 0) - throw new ArgumentOutOfRangeException("languageId", "Language ID should not be 0"); - - if (descriptor == null) - throw new ArgumentNullException("descriptor"); - - if (propertyName == null) - throw new ArgumentNullException("name"); - - string systemName = descriptor.SystemName; - string resourceName = string.Format("Plugins.{0}.{1}", propertyName, systemName); - var resource = localizationService.GetLocaleStringResourceByName(resourceName, languageId, false); - - if (resource != null) - { - if (string.IsNullOrWhiteSpace(value)) - { - //delete - localizationService.DeleteLocaleStringResource(resource); - } - else - { - //update - resource.ResourceValue = value; - localizationService.UpdateLocaleStringResource(resource); - } - } - else - { - if (!string.IsNullOrWhiteSpace(value)) - { - //insert - resource = new LocaleStringResource() - { - LanguageId = languageId, - ResourceName = resourceName, - ResourceValue = value, - }; - localizationService.InsertLocaleStringResource(resource); - } - } - } - - /// - /// Import language resources from XML file - /// - /// Language - /// XML - public static void ImportResourcesFromXml(this ILocalizationService service, - Language language, - string xml, - string rootKey = null, - bool sourceIsPlugin = false, - ImportModeFlags mode = ImportModeFlags.Insert | ImportModeFlags.Update, - bool updateTouchedResources = false) - { - if (language == null) - throw new ArgumentNullException("language"); - - if (String.IsNullOrEmpty(xml)) - return; - - var xmlDoc = new XmlDocument(); - xmlDoc.LoadXml(xml); - - service.ImportResourcesFromXml(language, xmlDoc, rootKey, sourceIsPlugin, mode, updateTouchedResources); - } - } -} diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs b/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs index dfaf9b6f41..bac41c3830 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs @@ -26,7 +26,7 @@ public partial class LocalizationService : ILocalizationService /// 0 = segment (first 3 chars of key), 1 = language id /// const string LOCALESTRINGRESOURCES_SEGMENT_KEY = "localization:{0}-lang-{1}"; - const string LOCALESTRINGRESOURCES_SEGMENT_PATTERN = "localization:{0}"; + const string LOCALESTRINGRESOURCES_SEGMENT_PATTERN = "localization:{0}*"; private readonly IRepository _lsrRepository; private readonly IWorkContext _workContext; @@ -65,9 +65,6 @@ public virtual void DeleteLocaleStringResource(LocaleStringResource resource) // db _lsrRepository.Delete(resource); - - // event notification - _eventPublisher.EntityDeleted(resource); } public virtual int DeleteLocaleStringResources(string key, bool keyIsRootKey = true) { @@ -152,30 +149,21 @@ public virtual void InsertLocaleStringResource(LocaleStringResource resource) // cache ClearCachedResourceSegment(resource.ResourceName, resource.LanguageId); - - // event notification - _eventPublisher.EntityInserted(resource); } public virtual void UpdateLocaleStringResource(LocaleStringResource resource) { Guard.NotNull(resource, nameof(resource)); - var modProps = _lsrRepository.GetModifiedProperties(resource); - - _lsrRepository.Update(resource); - // cache object origKey = null; - if (modProps.TryGetValue("ResourceName", out origKey)) - { + if (_dbContext.TryGetModifiedProperty(resource, "ResourceName", out origKey)) + { ClearCachedResourceSegment((string)origKey, resource.LanguageId); } ClearCachedResourceSegment(resource.ResourceName, resource.LanguageId); - - // event notification - _eventPublisher.EntityUpdated(resource); + _lsrRepository.Update(resource); } protected virtual IDictionary GetCachedResourceSegment(string forKey, int languageId) diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs b/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs index 6d0281dfc1..ca58d76a85 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs @@ -20,7 +20,7 @@ public partial class LocalizedEntityService : ScopedServiceBase, ILocalizedEntit ///
const string LOCALIZEDPROPERTY_SEGMENT_KEY = "localizedproperty:{0}-lang-{1}"; const string LOCALIZEDPROPERTY_SEGMENT_PATTERN = "localizedproperty:{0}"; - const string LOCALIZEDPROPERTY_ALLSEGMENTS_PATTERN = "localizedproperty:"; + const string LOCALIZEDPROPERTY_ALLSEGMENTS_PATTERN = "localizedproperty:*"; private readonly IRepository _localizedPropertyRepository; private readonly ICacheManager _cacheManager; @@ -43,10 +43,7 @@ protected virtual IDictionary GetCachedPropertySegment(string local Guard.NotEmpty(localeKeyGroup, nameof(localeKeyGroup)); Guard.NotEmpty(localeKey, nameof(localeKey)); - int minEntityId = 0; - int maxEntityId = 0; - - var segmentKey = GetSegmentKey(localeKeyGroup, localeKey, entityId, out minEntityId, out maxEntityId); + var segmentKey = GetSegmentKey(localeKeyGroup, localeKey, entityId, out var minEntityId, out var maxEntityId); var cacheKey = BuildCacheSegmentKey(segmentKey, languageId); // TODO: (MC) skip caching product.fulldescription (?), OR @@ -101,9 +98,7 @@ public virtual string GetLocalizedValue(int languageId, int entityId, string loc var props = GetCachedPropertySegment(localeKeyGroup, localeKey, entityId, languageId); - string val = null; - - if (!props.TryGetValue(entityId, out val)) + if (!props.TryGetValue(entityId, out var val)) { return string.Empty; } @@ -289,10 +284,7 @@ private string BuildCacheSegmentKey(string segment, int languageId) private string GetSegmentKey(string localeKeyGroup, string localeKey, int entityId) { - int minId = 0; - int maxId = 0; - - return GetSegmentKey(localeKeyGroup, localeKey, entityId, out minId, out maxId); + return GetSegmentKey(localeKeyGroup, localeKey, entityId, out var minId, out var maxId); } private string GetSegmentKey(string localeKeyGroup, string localeKey, int entityId, out int minId, out int maxId) diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizedUrlHelper.cs b/src/Libraries/SmartStore.Services/Localization/LocalizedUrlHelper.cs index 33861ceb44..0bf0cdf389 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizedUrlHelper.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizedUrlHelper.cs @@ -100,7 +100,7 @@ public string PrependSeoCode(string seoCode, bool safe = false) } } - this.RelativePath = "{0}/{1}".FormatCurrent(seoCode, this.RelativePath); + this.RelativePath = "{0}/{1}".FormatCurrent(seoCode, this.RelativePath).TrimEnd('/'); return this.RelativePath; } diff --git a/src/Libraries/SmartStore.Services/Media/CachedImageResult.cs b/src/Libraries/SmartStore.Services/Media/CachedImageResult.cs index f0c9d8dd75..613192513a 100644 --- a/src/Libraries/SmartStore.Services/Media/CachedImageResult.cs +++ b/src/Libraries/SmartStore.Services/Media/CachedImageResult.cs @@ -1,4 +1,5 @@ using System; +using SmartStore.Core.IO; namespace SmartStore.Services.Media { @@ -7,33 +8,74 @@ namespace SmartStore.Services.Media ///
/// /// An instance of this object is always returned, even when - /// the requested image does not physically exists in the repository. + /// the requested image does not physically exists in the storage. /// public class CachedImageResult { - /// - /// true when the image exists in the cache, false otherwise. - /// - public bool Exists { get; set; } + private bool? _exists; - /// - /// The name of the file - /// - public string FileName { get; set; } + public CachedImageResult(IFile file) + { + Guard.NotNull(file, nameof(file)); + + File = file; + } + + /// + /// The abstracted file object + /// + public IFile File { get; internal set; } + + /// + /// true when the image exists in the cache, false otherwise. + /// + public bool Exists + { + get + { + return _exists ?? (_exists = File.Exists).Value; + } + // For internal use + set + { + _exists = value; + } + } /// - /// The file extension (without 'dot') + /// The name of the file (without path) /// - public string Extension { get; set; } + public string FileName + { + get { return System.IO.Path.GetFileName(this.Path); } + } + + public long FileSize + { + get { return !Exists ? 0 : File.Size; } + } + + /// + /// The file extension (without 'dot') + /// + public string Extension { get; set; } /// /// The path relative to the cache root folder /// public string Path { get; set; } - ///// - ///// The local (physical) full path - ///// - //public string LocalPath { get; set; } + /// + /// The last modified date or null if the file does not exist + /// + public DateTime? LastModifiedUtc + { + get { return Exists ? File.LastUpdated : (DateTime?)null; } + } + + /// + /// Checks whether the file is remote (outside the application's physical root) + /// + public bool IsRemote { get; set; } } } diff --git a/src/Libraries/SmartStore.Services/Media/DefaultImageProcessor.cs b/src/Libraries/SmartStore.Services/Media/DefaultImageProcessor.cs new file mode 100644 index 0000000000..01f8d58440 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/DefaultImageProcessor.cs @@ -0,0 +1,218 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Drawing; +using ImageProcessor; +using ImageProcessor.Imaging; +using ImageProcessor.Imaging.Formats; +using SmartStore.Core.Logging; +using ImageProcessor.Configuration; +using SmartStore.Core.Events; + +namespace SmartStore.Services.Media +{ + public partial class DefaultImageProcessor : IImageProcessor + { + private static long _totalProcessingTime; + + private readonly IEventPublisher _eventPublisher; + + public DefaultImageProcessor(IEventPublisher eventPublisher) + { + _eventPublisher = eventPublisher; + + Logger = NullLogger.Instance; + } + + public ILogger Logger { get; set; } + + public bool IsSupportedImage(string fileName) + { + var ext = Path.GetExtension(fileName); + if (ext != null) + { + var extension = ext.Trim('.').ToLower(); + return ImageProcessorBootstrapper.Instance.SupportedImageFormats + .SelectMany(x => x.FileExtensions) + .Any(x => x == extension); + } + + return false; + } + + public ProcessImageResult ProcessImage(ProcessImageQuery query) + { + Guard.NotNull(query, nameof(query)); + + ValidateQuery(query); + + var watch = new Stopwatch(); + + try + { + watch.Start(); + + using (var processor = new ImageFactory(preserveExifData: false, fixGamma: false)) + { + var source = query.Source; + + // Load source + if (source is byte[]) + { + processor.Load((byte[])source); + } + else if (source is Stream) + { + processor.Load((Stream)source); + } + else if (source is Image) + { + processor.Load((Image)source); + } + else if (source is string) + { + // TODO: (mc) map virtual pathes + processor.Load((string)source); + } + else + { + throw new ProcessImageException("Invalid source type '{0}' in query.".FormatInvariant(query.Source.GetType().FullName), query); + } + + // Pre-process event + _eventPublisher.Publish(new ImageProcessingEvent(query, processor)); + + var result = new ProcessImageResult + { + Query = query, + SourceWidth = processor.Image.Width, + SourceHeight = processor.Image.Height + }; + + // Core processing + ProcessImageCore(query, processor); + + // Create & prepare result + var outStream = new MemoryStream(); + processor.Save(outStream); + + result.Width = processor.Image.Width; + result.Height = processor.Image.Height; + result.FileExtension = processor.CurrentImageFormat.DefaultExtension; + result.MimeType = processor.CurrentImageFormat.MimeType; + result.OutputStream = outStream; + + // Post-process event + _eventPublisher.Publish(new ImageProcessedEvent(query, processor, result)); + + result.ProcessTimeMs = watch.ElapsedMilliseconds; + + return result; + } + } + catch (Exception ex) + { + var pex = new ProcessImageException(query, ex); + Logger.Error(pex); + throw pex; + } + finally + { + if (query.DisposeSource && query.Source is IDisposable) + { + ((IDisposable)query.Source).Dispose(); + } + + watch.Stop(); + _totalProcessingTime += watch.ElapsedMilliseconds; + } + } + + /// + /// Processes the loaded image. Inheritors should NOT save the image, this is done by the main method. + /// + /// Query + /// Processor instance + protected virtual void ProcessImageCore(ProcessImageQuery query, ImageFactory processor) + { + // Resize + var size = query.MaxWidth != null || query.MaxHeight != null + ? new Size(query.MaxWidth ?? 0, query.MaxHeight ?? 0) + : Size.Empty; + + if (!size.IsEmpty) + { + var scaleMode = ConvertScaleMode(query.ScaleMode); + processor.Resize(new ResizeLayer(size, resizeMode: scaleMode, upscale: false)); + } + + // Format + if (query.Format != null) + { + var format = query.Format as ISupportedImageFormat; + + if (format == null && query.Format is string) + { + var requestedFormat = ((string)query.Format).ToLowerInvariant(); + switch (requestedFormat) + { + case "jpg": + case "jpeg": + format = new JpegFormat(); + break; + case "png": + format = new PngFormat(); + break; + case "gif": + format = new GifFormat(); + break; + } + } + + if (format != null) + { + processor.Format(format); + } + } + + // Set Quality + if (query.Quality.HasValue) + { + processor.Quality(query.Quality.Value); + } + } + + private void ValidateQuery(ProcessImageQuery query) + { + if (query.Source == null) + { + throw new ArgumentException("During image processing 'ProcessImageQuery.Source' must not be null.", nameof(query)); + } + } + + public long TotalProcessingTimeMs + { + get { return _totalProcessingTime; } + } + + private ResizeMode ConvertScaleMode(string mode) + { + switch (mode.EmptyNull().ToLower()) + { + case "boxpad": + return ResizeMode.BoxPad; + case "crop": + return ResizeMode.Crop; + case "min": + return ResizeMode.Min; + case "pad": + return ResizeMode.Pad; + case "stretch": + return ResizeMode.Stretch; + default: + return ResizeMode.Max; + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/DownloadService.cs b/src/Libraries/SmartStore.Services/Media/DownloadService.cs index 741b4f2441..1f36eb41af 100644 --- a/src/Libraries/SmartStore.Services/Media/DownloadService.cs +++ b/src/Libraries/SmartStore.Services/Media/DownloadService.cs @@ -44,9 +44,6 @@ private void UpdateDownloadCore(Download download, byte[] downloadBinary, bool u // save to storage _storageProvider.Value.Save(download.ToMedia(), downloadBinary); } - - // event notification - _eventPubisher.EntityUpdated(download); } public virtual Download GetDownloadById(int downloadId) @@ -94,9 +91,6 @@ public virtual void DeleteDownload(Download download) // delete entity _downloadRepository.Delete(download); - - // event notification - _eventPubisher.EntityDeleted(download); } public virtual void InsertDownload(Download download, byte[] downloadBinary) @@ -107,9 +101,6 @@ public virtual void InsertDownload(Download download, byte[] downloadBinary) // save to storage _storageProvider.Value.Save(download.ToMedia(), downloadBinary); - - // event notification - _eventPubisher.EntityInserted(download); } public virtual void UpdateDownload(Download download) diff --git a/src/Libraries/SmartStore.Services/Media/Events.cs b/src/Libraries/SmartStore.Services/Media/Events.cs new file mode 100644 index 0000000000..0ab3361917 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/Events.cs @@ -0,0 +1,99 @@ +using System; +using System.Drawing; +using System.Web; +using ImageProcessor; + +namespace SmartStore.Services.Media +{ + /// + /// Published for every uploaded image which does NOT exceed maximum + /// allowed size. This gives subscribers the chance to still process the image, + /// e.g. to achive better compression before saving image data to storage. + /// This event does NOT get published when the uploaded image is about to be processed anyway. + /// + /// + /// A subscriber should NOT resize the image. But if you do - and you shouldn't :-) - , don't forget to set . + /// + public class ImageUploadValidatedEvent + { + public ImageUploadValidatedEvent(ProcessImageQuery query, Size size) + { + Query = query; + Size = size; + } + + /// + /// Contains the source (as byte[]), max size, format and default image quality instructions. + /// + public ProcessImageQuery Query { get; private set; } + + /// + /// The original size of the uploaded image. May be empty. + /// + public Size Size { get; private set; } + + /// + /// The processing result. If null, the original data + /// from Query.Source will be put to storage. + /// + public byte[] ResultBuffer { get; set; } + + /// + /// Size of the result image. + /// + public Size ResultSize { get; set; } + } + + /// + /// Published after image query has been created and initialized + /// by the media middleware controller with data from HttpContent.Request.QueryString. + /// This event implies that a thumbnail is about to be created. + /// + public class ImageQueryCreatedEvent + { + public ImageQueryCreatedEvent(ProcessImageQuery query, HttpContextBase httpContext, string mimeType, string extension) + { + Query = query; + HttpContext = httpContext; + MimeType = mimeType; + Extension = extension; + } + + public ProcessImageQuery Query { get; private set; } + public HttpContextBase HttpContext { get; private set; } + public string MimeType { get; private set; } + public string Extension { get; private set; } + } + + /// + /// Published before processing begins, but after the source has been loaded. + /// + public class ImageProcessingEvent + { + public ImageProcessingEvent(ProcessImageQuery query, ImageFactory processor) + { + Query = query; + Processor = processor; + } + + public ProcessImageQuery Query { get; private set; } + public ImageFactory Processor { get; private set; } + } + + /// + /// Published after processing finishes and the result is saved to a stream (ProcessImageResult.Result) + /// + public class ImageProcessedEvent + { + public ImageProcessedEvent(ProcessImageQuery query, ImageFactory processor, ProcessImageResult result) + { + Query = query; + Processor = processor; + Result = result; + } + + public ProcessImageQuery Query { get; private set; } + public ImageFactory Processor { get; private set; } + public ProcessImageResult Result { get; private set; } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/IImageCache.cs b/src/Libraries/SmartStore.Services/Media/IImageCache.cs index 1b8c5dbd81..97b08e2cf7 100644 --- a/src/Libraries/SmartStore.Services/Media/IImageCache.cs +++ b/src/Libraries/SmartStore.Services/Media/IImageCache.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using SmartStore.Core.Domain.Media; +using SmartStore.Core.IO; namespace SmartStore.Services.Media { @@ -13,45 +14,21 @@ namespace SmartStore.Services.Media ///
public interface IImageCache { - /// - /// Resolves an http url to the image - /// - /// The path of the image relative to the cache root path - /// Store location URL; null to use determine the current store location automatically - /// The image http url - string GetImageUrl(string imagePath, string storeLocation = null); - - /// - /// Processes (resizes) and adds an image to the cache. - /// - /// An instance of the object, which is returned by the GetCachedImage() method. - /// The image binary data. - /// The max size of the target image. - /// The binary buffer of the resized image - byte[] ProcessAndAddImageToCache(CachedImageResult cachedImage, byte[] source, int targetSize); - - /// - /// Processes (resizes) and adds an image to the cache asynchronously. - /// - /// An instance of the object, which is returned by the GetCachedImage() method. - /// The image binary data. - /// The max size of the target image. - /// The binary buffer of the resized image - Task ProcessAndAddImageToCacheAsync(CachedImageResult cachedImage, byte[] source, int targetSize); - /// /// Adds an image to the cache. /// - /// An instance of the object, which is returned by the GetCachedImage() method. - /// The image binary data. - void AddImageToCache(CachedImageResult cachedImage, byte[] buffer); + /// An instance of the object, which is returned by the Get() method. + /// The image binary buffer. + /// true when the operation succeded, false otherwise + void Put(CachedImageResult cachedImage, byte[] buffer); /// /// Asynchronously adds an image to the cache. /// - /// An instance of the object, which is returned by the GetCachedImage() method. - /// The image binary data. - Task AddImageToCacheAsync(CachedImageResult cachedImage, byte[] buffer); + /// An instance of the object, which is returned by the Get() method. + /// The image binary buffer. + /// true when the operation succeded, false otherwise + Task PutAsync(CachedImageResult cachedImage, byte[] buffer); /// /// Gets an instance of the object, which contains information about a cached image. @@ -59,34 +36,62 @@ public interface IImageCache /// The picture id of the image to be resolved. /// The seo friendly picture name of the image to be resolved. /// The extension of the image to be resolved. - /// The image processing settings. + /// The image processing query. + /// An instance of the object + /// If the requested image does not exist in the cache, the value of the Exists property will be false. + CachedImageResult Get(int? pictureId, string seoFileName, string extension, ProcessImageQuery query = null); + + /// + /// Gets an instance of the object, which contains information about a cached image. + /// Use this overload to get thumbnail info about uploaded media manager asset files. + /// + /// The file to get info about. + /// The image processing query. /// An instance of the object /// If the requested image does not exist in the cache, the value of the Exists property will be false. - CachedImageResult GetCachedImage(int? pictureId, string seoFileName, string extension, object settings = null); + CachedImageResult Get(IFile file, ProcessImageQuery query); /// /// Opens a readonly file stream to the cached image /// /// An instance of the object, which is returned by the GetCachedImage() method. /// File stream - Stream OpenCachedImage(CachedImageResult cachedImage); + Stream Open(CachedImageResult cachedImage); /// /// Deletes all cached images for the given /// /// The for which to delete cached images - void DeleteCachedImages(Picture picture); + void Delete(Picture picture); + + /// + /// Deletes all cached images for the given + /// + /// The for which to delete cached images + void Delete(IFile file); - /// - /// Deletes all cached images (nukes all files in the cache folder) - /// - void DeleteCachedImages(); + /// + /// Deletes all cached images (nukes all files in the cache folder) + /// + void Clear(); - /// - /// Calculates statistics about the image cache data. - /// - /// The total count of files in the cache. - /// The total size of files in the cache (in bytes) - void CacheStatistics(out long fileCount, out long totalSize); - } + /// + /// Refreshes the file info. + /// + void RefreshInfo(CachedImageResult cachedImage); + + /// + /// Calculates statistics about the image cache data. + /// + /// The total count of files in the cache. + /// The total size of files in the cache (in bytes) + void CacheStatistics(out long fileCount, out long totalSize); + + /// + /// Resolves a publicly accessible http url to the image + /// + /// The path of the image relative to the cache root path + /// The image http url + string GetPublicUrl(string imagePath); + } } diff --git a/src/Libraries/SmartStore.Services/Media/IImageProcessor.cs b/src/Libraries/SmartStore.Services/Media/IImageProcessor.cs new file mode 100644 index 0000000000..3ff8fe25f9 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/IImageProcessor.cs @@ -0,0 +1,29 @@ +using System; + +namespace SmartStore.Services.Media +{ + /// + /// A service interface responsible for resizing/processing images. + /// + public interface IImageProcessor + { + /// + /// Determines whether the given file name is processable by the image resizer + /// + /// The name of the file (without path but including extension) + /// A value indicating whether processing is possible + bool IsSupportedImage(string fileName); + + /// + /// Processes an image + /// + /// Resize request + /// The resizing result encapsulated in type + ProcessImageResult ProcessImage(ProcessImageQuery query); + + /// + /// Gets the cumulative total processing time since app start in miliseconds + /// + long TotalProcessingTimeMs { get; } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/IImageResizerService.cs b/src/Libraries/SmartStore.Services/Media/IImageResizerService.cs deleted file mode 100644 index 438db1a049..0000000000 --- a/src/Libraries/SmartStore.Services/Media/IImageResizerService.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.IO; -using System.Collections.Generic; - -namespace SmartStore.Services.Media -{ - - /// - /// A service interface responsible for resizing images. - /// - public interface IImageResizerService - { - /// - /// Determines whether the given file name is processable by the image resizer - /// - /// The name of the file (without path but including extension) - /// A value indicating whether processing is possible - bool IsSupportedImage(string fileName); - - /// - /// Resizes an image - /// - /// The source stream - /// The max width of the destination image - /// The max height of the destination image - /// The output quality - /// The resize mode - /// A provider specific settings object. - /// The result image as a MemoryStream object - MemoryStream ResizeImage(Stream source, int? maxWidth = null, int? maxHeight = null, int? quality = 0, object settings = null); - } -} diff --git a/src/Libraries/SmartStore.Services/Media/IPictureService.cs b/src/Libraries/SmartStore.Services/Media/IPictureService.cs index cc983690c8..84c6e2013c 100644 --- a/src/Libraries/SmartStore.Services/Media/IPictureService.cs +++ b/src/Libraries/SmartStore.Services/Media/IPictureService.cs @@ -1,9 +1,13 @@ using System.Collections.Generic; using System.Drawing; +using System.IO; +using System.Linq; using System.Threading.Tasks; using SmartStore.Collections; using SmartStore.Core; +using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Media; +using SmartStore.Core.IO; namespace SmartStore.Services.Media { @@ -15,7 +19,7 @@ public partial interface IPictureService /// Picture binary /// MIME type /// Picture binary or throws an exception - byte[] ValidatePicture(byte[] pictureBinary); + byte[] ValidatePicture(byte[] pictureBinary, string mimeType); /// /// Validates input picture dimensions and prevents that the image size exceeds global max size @@ -24,7 +28,7 @@ public partial interface IPictureService /// MIME type /// The size of the original input OR the resized picture /// Picture binary or throws an exception - byte[] ValidatePicture(byte[] pictureBinary, out Size size); + byte[] ValidatePicture(byte[] pictureBinary, string mimeType, out Size size); /// /// Finds an equal picture by comparing the binary buffer @@ -50,6 +54,13 @@ public partial interface IPictureService /// Picture Picture SetSeoFilename(int pictureId, string seoFilename); + /// + /// Opens the picture stream from the underlying storage provider for reading + /// + /// Picture + /// Picture stream + Stream OpenPictureStream(Picture picture); + /// /// Loads the picture binary from the underlying storage provider /// @@ -68,83 +79,83 @@ public partial interface IPictureService /// Gets the size of a picture /// /// The buffer + /// Passing MIME type can slightly speed things up /// Size - Size GetPictureSize(byte[] pictureBinary); + Size GetPictureSize(byte[] pictureBinary, string mimeType = null); /// - /// Gets a picture URL + /// TODO: (mc) /// - /// Picture identifier - /// The target picture size (longest side) - /// A value indicating whether the default picture is shown - /// Store location URL; null to use determine the current store location automatically - /// Default picture type - /// Picture URL - string GetPictureUrl( - int pictureId, - int targetSize = 0, - bool showDefaultPicture = true, - string storeLocation = null, - PictureType defaultPictureType = PictureType.Entity); + /// + /// + /// + /// + /// + /// + IDictionary GetPictureInfos(IEnumerable pictureIds); + + /// + /// TODO: (mc) + /// + /// + /// + PictureInfo GetPictureInfo(int? pictureId); /// - /// Gets a picture URL asynchronously + /// TODO: (mc) + /// + /// + /// + /// + /// + /// + /// + PictureInfo GetPictureInfo(Picture picture); + + /// + /// Builds a url for a given . /// /// Picture identifier /// The target picture size (longest side) - /// A value indicating whether the default picture is shown - /// Store location URL; null to use determine the current store location automatically - /// Default picture type + /// Store location URL; null to use determine the current store location automatically + /// Specifies the kind of fallback url to return if the argument is 0 or a picture with the passed id does not exist in the storage. /// Picture URL - Task GetPictureUrlAsync( - int pictureId, - int targetSize = 0, - bool showDefaultPicture = true, - string storeLocation = null, - PictureType defaultPictureType = PictureType.Entity); + string GetUrl(int pictureId, int targetSize = 0, FallbackPictureType fallbackType = FallbackPictureType.Entity, string host = null); /// - /// Gets a picture URL + /// Builds a url for a given instance. /// /// Picture instance /// The target picture size (longest side) - /// A value indicating whether the default picture is shown - /// Store location URL; null to use determine the current store location automatically - /// Default picture type + /// Store location URL; null to use determine the current store location automatically + /// Specifies the kind of fallback url to return if the argument is null. /// Picture URL - string GetPictureUrl( - Picture picture, - int targetSize = 0, - bool showDefaultPicture = true, - string storeLocation = null, - PictureType defaultPictureType = PictureType.Entity); + string GetUrl(Picture picture, int targetSize = 0, FallbackPictureType fallbackType = FallbackPictureType.Entity, string host = null); /// - /// Gets a picture URL asynchronously + /// Builds a url for a given instance. /// - /// Picture instance - /// The target picture size (longest side) - /// A value indicating whether the default picture is shown - /// Store location URL; null to use determine the current store location automatically - /// Default picture type - /// Picture URL - Task GetPictureUrlAsync( - Picture picture, - int targetSize = 0, - bool showDefaultPicture = true, - string storeLocation = null, - PictureType defaultPictureType = PictureType.Entity); + /// The PictureInfo instance to build a url for + /// The maximum size of the picture. If greather than null, a query is appended to the generated url. + /// The host (including scheme) to prepend to the url. + /// Specifies the kind of fallback url to return if the argument is null. + /// Generated url which can be processed by the media middleware controller + string GetUrl(PictureInfo info, int targetSize = 0, FallbackPictureType fallbackType = FallbackPictureType.Entity, string host = null); /// - /// Gets the default picture URL + /// Gets the fallback picture URL /// /// The target picture size (longest side) - /// Default picture type - /// Store location URL; null to use determine the current store location automatically + /// Store location URL; null to use determine the current store location automatically + /// Default picture type /// Picture URL - string GetDefaultPictureUrl(int targetSize = 0, - PictureType defaultPictureType = PictureType.Entity, - string storeLocation = null); + string GetFallbackUrl(int targetSize = 0, FallbackPictureType fallbackType = FallbackPictureType.Entity, string host = null); + + /// + /// Clears the url cache completely or for a particular store + /// + /// The total count of removed cache entries + int ClearUrlCache(); /// /// Gets a picture @@ -258,8 +269,61 @@ public static Picture UpdatePicture(this IPictureService pictureService, public static Size GetPictureSize(this IPictureService pictureService, Picture picture) { - var pictureBinary = pictureService.LoadPictureBinary(picture); - return pictureService.GetPictureSize(pictureBinary); + return ImageHeader.GetDimensions(pictureService.OpenPictureStream(picture), picture.MimeType, false); + } + + /// + /// TODO: (mc) + /// + /// + /// + public static IDictionary GetPictureInfos(this IPictureService pictureService, IEnumerable products) + { + Guard.NotNull(products, nameof(products)); + + return pictureService.GetPictureInfos(products.Select(x => x.MainPictureId.GetValueOrDefault())); + } + + /// + /// Builds a picture url + /// + /// The picture id to build a url for + /// The maximum size of the picture. If greather than null, a query is appended to the generated url. + /// The host (including scheme) to prepend to the url. + /// Specifies whether to return a fallback url if the picture does not exist in the storage (default: true). + /// Generated url which can be processed by the media middleware controller + public static string GetUrl(this IPictureService pictureService, int pictureId, int targetSize, bool fallback, string host = null) + { + var fallbackType = fallback ? FallbackPictureType.Entity : FallbackPictureType.NoFallback; + return pictureService.GetUrl(pictureId, targetSize, fallbackType, host); + } + + /// + /// Builds a picture url + /// + /// The picture to build a url for + /// The maximum size of the picture. If greather than null, a query is appended to the generated url. + /// The host (including scheme) to prepend to the url. + /// Specifies whether to return a fallback url if the picture does not exist in the storage (default: true). + /// Generated url which can be processed by the media middleware controller + public static string GetUrl(this IPictureService pictureService, Picture picture, int targetSize, bool fallback, string host = null) + { + var fallbackType = fallback ? FallbackPictureType.Entity : FallbackPictureType.NoFallback; + return pictureService.GetUrl(picture, targetSize, fallbackType, host); + } + + /// + /// Builds a picture url + /// + /// The picture info to build a url for + /// The maximum size of the picture. If greather than null, a query is appended to the generated url. + /// The host (including scheme) to prepend to the url. + /// Specifies whether to return a fallback url if the picture does not exist in the storage (default: true). + /// Generated url which can be processed by the media middleware controller + public static string GetUrl(this IPictureService pictureService, PictureInfo info, int targetSize, bool fallback, string host = null) + { + var fallbackType = fallback ? FallbackPictureType.Entity : FallbackPictureType.NoFallback; + return pictureService.GetUrl(info, targetSize, fallbackType, host); } } } diff --git a/src/Libraries/SmartStore.Services/Media/ImageCache.cs b/src/Libraries/SmartStore.Services/Media/ImageCache.cs index 2f1859218e..1d93b47845 100644 --- a/src/Libraries/SmartStore.Services/Media/ImageCache.cs +++ b/src/Libraries/SmartStore.Services/Media/ImageCache.cs @@ -1,11 +1,10 @@ using System; +using System.Collections.Specialized; using System.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; using System.Web; using System.Web.Hosting; -using ImageResizer; using SmartStore.Core; using SmartStore.Core.Domain.Media; using SmartStore.Core.IO; @@ -15,27 +14,28 @@ namespace SmartStore.Services.Media { public class ImageCache : IImageCache { - private const int MULTIPLE_THUMB_DIRECTORIES_LENGTH = 4; + public const string IdFormatString = "0000000"; + internal const int MaxDirLength = 4; - private readonly MediaSettings _mediaSettings; + private readonly MediaSettings _mediaSettings; private readonly string _thumbsRootDir; private readonly IStoreContext _storeContext; private readonly HttpContextBase _httpContext; private readonly IMediaFileSystem _fileSystem; - private readonly IImageResizerService _imageResizerService; + private readonly IImageProcessor _imageProcessor; public ImageCache( MediaSettings mediaSettings, IStoreContext storeContext, HttpContextBase httpContext, IMediaFileSystem fileSystem, - IImageResizerService imageResizerService) + IImageProcessor imageProcessor) { _mediaSettings = mediaSettings; _storeContext = storeContext; _httpContext = httpContext; _fileSystem = fileSystem; - _imageResizerService = imageResizerService; + _imageProcessor = imageProcessor; _thumbsRootDir = "Thumbs/"; @@ -48,75 +48,41 @@ public ILogger Logger set; } - public byte[] ProcessAndAddImageToCache(CachedImageResult cachedImage, byte[] source, int targetSize) + public void Put(CachedImageResult cachedImage, byte[] buffer) { - byte[] result; - - if (targetSize == 0) - { - AddImageToCache(cachedImage, source); - result = source; - } - else + if (PreparePut(cachedImage, buffer)) { - var sourceStream = new MemoryStream(source); - using (var resultStream = _imageResizerService.ResizeImage(sourceStream, targetSize, targetSize, _mediaSettings.DefaultImageQuality)) - { - result = resultStream.GetBuffer(); - AddImageToCache(cachedImage, result); - } - } + var path = BuildPath(cachedImage.Path); - cachedImage.Exists = true; + _fileSystem.WriteAllBytes(path, buffer); - return result; + cachedImage.Exists = true; + cachedImage.File = _fileSystem.GetFile(path); + } } - public async Task ProcessAndAddImageToCacheAsync(CachedImageResult cachedImage, byte[] source, int targetSize) + public Task PutAsync(CachedImageResult cachedImage, byte[] buffer) { - byte[] result; - - if (targetSize == 0) + if (PreparePut(cachedImage, buffer)) { - await AddImageToCacheAsync(cachedImage, source); - result = source; - } - else - { - var sourceStream = new MemoryStream(source); - using (var resultStream = _imageResizerService.ResizeImage(sourceStream, targetSize, targetSize, _mediaSettings.DefaultImageQuality)) - { - result = resultStream.GetBuffer(); - await AddImageToCacheAsync(cachedImage, result); - } - } - - cachedImage.Exists = true; - - return result; - } + var path = BuildPath(cachedImage.Path); - public void AddImageToCache(CachedImageResult cachedImage, byte[] buffer) - { - if (PrepareAddImageToCache(cachedImage, buffer)) - { // save file - _fileSystem.WriteAllBytes(BuildPath(cachedImage.Path), buffer); - } - } + var t = _fileSystem.WriteAllBytesAsync(path, buffer); + t.ContinueWith(x => + { + // Refresh info + cachedImage.Exists = true; + cachedImage.File = _fileSystem.GetFile(path); + }); - public Task AddImageToCacheAsync(CachedImageResult cachedImage, byte[] buffer) - { - if (PrepareAddImageToCache(cachedImage, buffer)) - { - // save file - return _fileSystem.WriteAllBytesAsync(BuildPath(cachedImage.Path), buffer); + return t; } return Task.FromResult(false); } - private bool PrepareAddImageToCache(CachedImageResult cachedImage, byte[] buffer) + private bool PreparePut(CachedImageResult cachedImage, byte[] buffer) { Guard.NotNull(cachedImage, nameof(cachedImage)); @@ -140,73 +106,70 @@ private bool PrepareAddImageToCache(CachedImageResult cachedImage, byte[] buffer return true; } - public virtual CachedImageResult GetCachedImage(int? pictureId, string seoFileName, string extension, object settings = null) + public virtual CachedImageResult Get(int? pictureId, string seoFileName, string extension, ProcessImageQuery query = null) { - var imagePath = this.GetCachedImagePath(pictureId, seoFileName, extension, ImageResizerUtil.CreateResizeSettings(settings)); + Guard.NotEmpty(extension, nameof(extension)); - var result = new CachedImageResult - { - Path = imagePath, //"Media/Thumbs/" + imagePath, - FileName = System.IO.Path.GetFileName(imagePath), - Extension = GetCleanFileExtension(imagePath), - Exists = _fileSystem.FileExists(BuildPath(imagePath)) - }; + extension = query?.GetResultExtension() ?? extension.TrimStart('.').ToLower(); + var imagePath = GetCachedImagePath(pictureId, seoFileName, extension, query); + + var file = _fileSystem.GetFile(BuildPath(imagePath)); + var result = new CachedImageResult(file) + { + Path = imagePath, + Extension = extension, + IsRemote = _fileSystem.IsCloudStorage + }; + return result; } - public virtual Stream OpenCachedImage(CachedImageResult cachedImage) + public virtual CachedImageResult Get(IFile file, ProcessImageQuery query) + { + Guard.NotNull(file, nameof(file)); + Guard.NotNull(query, nameof(query)); + + var imagePath = GetCachedImagePath(file, query); + var thumbFile = _fileSystem.GetFile(BuildPath(imagePath)); + + var result = new CachedImageResult(thumbFile) + { + Path = imagePath, + Extension = file.Extension.TrimStart('.'), + IsRemote = _fileSystem.IsCloudStorage + }; + + return result; + } + + public virtual Stream Open(CachedImageResult cachedImage) { Guard.NotNull(cachedImage, nameof(cachedImage)); return _fileSystem.GetFile(BuildPath(cachedImage.Path)).OpenRead(); } - public virtual string GetImageUrl(string imagePath, string storeLocation = null) + public virtual string GetPublicUrl(string imagePath) { if (imagePath.IsEmpty()) return null; - var publicUrl = _fileSystem.GetPublicUrl(BuildPath(imagePath)).EmptyNull(); - if (publicUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || publicUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) - { - // absolute url - return publicUrl; - } - - var root = storeLocation; - - if (root.IsEmpty()) - { - var cdnUrl = _storeContext.CurrentStore.ContentDeliveryNetwork; - if (cdnUrl.HasValue() && !_httpContext.IsDebuggingEnabled && !_httpContext.Request.IsLocal) - { - root = cdnUrl; - } - } - - if (root.IsEmpty()) - { - // relative url must start with a slash - return publicUrl.EnsureStartsWith("/"); - } - - if (HostingEnvironment.IsHosted) - { - // strip out app path from public url if needed but do not strip away leading slash from publicUrl - var appPath = HostingEnvironment.ApplicationVirtualPath.EmptyNull(); - if (appPath.Length > 0 && appPath != "/") - { - publicUrl = publicUrl.Substring(appPath.Length + 1); - } - } + return _fileSystem.GetPublicUrl(BuildPath(imagePath), true).EmptyNull(); + } - return root.TrimEnd('/', '\\') + publicUrl.EnsureStartsWith("/"); + public virtual void RefreshInfo(CachedImageResult cachedImage) + { + Guard.NotNull(cachedImage, nameof(cachedImage)); + + var file = _fileSystem.GetFile(cachedImage.File.Path); + cachedImage.File = file; + cachedImage.Exists = file.Exists; } - public virtual void DeleteCachedImages(Picture picture) + public virtual void Delete(Picture picture) { - var filter = string.Format("{0}*.*", picture.Id.ToString("0000000")); + var filter = string.Format("{0}*.*", picture.Id.ToString(IdFormatString)); var files = _fileSystem.SearchFiles(_thumbsRootDir, filter); foreach (var file in files) @@ -215,7 +178,19 @@ public virtual void DeleteCachedImages(Picture picture) } } - public virtual void DeleteCachedImages() + public virtual void Delete(IFile file) + { + // TODO: (mc) this could lead to more thumbs getting deleted as desired. But who cares? :-) + var filter = string.Format("{0}*.*", file.Title); + + var files = _fileSystem.SearchFiles(BuildPath(file.Directory), filter); + foreach (var f in files) + { + _fileSystem.DeleteFile(f); + } + } + + public virtual void Clear() { for (int i = 0; i < 10; i++) { @@ -256,24 +231,24 @@ public void CacheStatistics(out long fileCount, out long totalSize) totalSize = _fileSystem.ListFolders(_thumbsRootDir).Sum(x => x.Size); } - #region Utils - - /// - /// Returns the file name with the subfolder (when multidirs are enabled) - /// - /// - /// - /// - internal string GetCachedImagePath(int? pictureId, string seoFileName, string extension, ResizeSettings settings = null) + #region Utils + + /// + /// Returns the file name with the subfolder (when multidirs are enabled) + /// + /// + /// File name without extension + /// Dot-less file extension + /// + /// + private string GetCachedImagePath(int? pictureId, string seoFileName, string extension, ProcessImageQuery query = null) { - Guard.NotEmpty(extension, nameof(extension)); - string imageFileName = null; string firstPart = ""; if (pictureId.GetValueOrDefault() > 0) { - firstPart = pictureId.Value.ToString("0000000") + (seoFileName.IsEmpty() ? "" : "-"); + firstPart = pictureId.Value.ToString(IdFormatString) + (seoFileName.IsEmpty() ? "" : "-"); } if (firstPart.IsEmpty() && seoFileName.IsEmpty()) @@ -283,65 +258,56 @@ internal string GetCachedImagePath(int? pictureId, string seoFileName, string ex } seoFileName = seoFileName.EmptyNull(); - extension = extension.TrimStart('.'); - if (!NeedsProcessing(settings)) + if (query == null || !query.NeedsProcessing()) { - imageFileName = "{0}{1}.{2}".FormatInvariant(firstPart, seoFileName, extension); + imageFileName = String.Concat(firstPart, seoFileName); } else { - string hashedProps = CreateSettingsHash(settings); - imageFileName = "{0}{1}-{2}.{3}".FormatInvariant(firstPart, seoFileName, hashedProps, extension); - } + imageFileName = String.Concat(firstPart, seoFileName, query.CreateHash()); + } - if (_mediaSettings.MultipleThumbDirectories) + if (_mediaSettings.MultipleThumbDirectories && imageFileName != null && imageFileName.Length > MaxDirLength) { - // get the first four letters of the file name - var fileNameWithoutExtension = System.IO.Path.GetFileNameWithoutExtension(imageFileName); - if (fileNameWithoutExtension != null && fileNameWithoutExtension.Length > MULTIPLE_THUMB_DIRECTORIES_LENGTH) - { - var subDirectoryName = fileNameWithoutExtension.Substring(0, MULTIPLE_THUMB_DIRECTORIES_LENGTH); - imageFileName = subDirectoryName + "/" + imageFileName; - } + // Get the first four letters of the file name + var subDirectoryName = imageFileName.Substring(0, MaxDirLength); + imageFileName = String.Concat(subDirectoryName, "/", imageFileName); } - return imageFileName; + return String.Concat(imageFileName, ".", extension); } - private string CreateSettingsHash(ResizeSettings settings) - { - if (settings.Count == 2 && settings.MaxWidth > 0 && settings.MaxWidth == settings.MaxHeight) - { - return settings.MaxWidth.ToString(); - } - return settings.ToString().Hash(Encoding.ASCII); - } + /// + /// Returns the images thumb path as is plus query (required for uploaded images) + /// + /// Image file to get thumbnail for + /// + /// + private string GetCachedImagePath(IFile file, ProcessImageQuery query) + { + if (!_imageProcessor.IsSupportedImage(file.Name)) + { + throw new InvalidOperationException("Thumbnails for '{0}' files are not supported".FormatInvariant(file.Extension)); + } + + // TODO: (mc) prevent creating thumbs for thumbs AND check equality of source and target - private bool NeedsProcessing(ResizeSettings settings) - { - return settings != null && settings.Count > 0; - } + var imageFileName = String.Concat(file.Title, query.CreateHash()); + var extension = (query.GetResultExtension() ?? file.Extension).EnsureStartsWith(".").ToLower(); + var path = _fileSystem.Combine(file.Directory, imageFileName + extension); + + return path.TrimStart('/', '\\'); + } private string BuildPath(string imagePath) { if (imagePath.IsEmpty()) return null; - return _thumbsRootDir + imagePath; + return String.Concat(_thumbsRootDir, imagePath); } - private static string GetCleanFileExtension(string url) - { - var extension = System.IO.Path.GetExtension(url); - if (extension != null) - { - return extension.Replace(".", "").ToLower(); - } - - return string.Empty; - } - #endregion } diff --git a/src/Libraries/SmartStore.Services/Media/ImageCacheExtensions.cs b/src/Libraries/SmartStore.Services/Media/ImageCacheExtensions.cs index afb2f332c1..98934a6561 100644 --- a/src/Libraries/SmartStore.Services/Media/ImageCacheExtensions.cs +++ b/src/Libraries/SmartStore.Services/Media/ImageCacheExtensions.cs @@ -6,46 +6,44 @@ namespace SmartStore.Services.Media { public static class ImageCacheExtensions { - - /// - /// Gets an instance of the object, which contains information about a cached image. - /// - /// The picture object for which to resolve a cached image. - /// The image processing settings. - /// An instance of the object - /// If the requested image does not exist in the cache, the value of the Exists property will be false. - public static CachedImageResult GetCachedImage(this IImageCache imageCache, Picture picture, object settings = null) + /// + /// Gets an instance of the object, which contains information about a cached image. + /// + /// The picture object for which to resolve a cached image. + /// The image processing query. + /// An instance of the object + /// If the requested image does not exist in the cache, the value of the Exists property will be false. + public static CachedImageResult Get(this IImageCache imageCache, Picture picture, ProcessImageQuery query = null) { Guard.NotNull(picture, nameof(picture)); - return imageCache.GetCachedImage(picture.Id, picture.SeoFilename, MimeTypes.MapMimeTypeToExtension(picture.MimeType), settings); + return imageCache.Get(picture.Id, picture.SeoFilename, MimeTypes.MapMimeTypeToExtension(picture.MimeType), query); } - /// - /// Adds an image to the cache. - /// - /// The picture id, which will be part of the resulting file name. - /// The seo friendly picture name, which will be part of the resulting file name. - /// The extension of the resulting file - /// The image binary data. - /// The image processing settings.This object, if not null, is hashed and appended to the resulting file name. - public static void AddImageToCache(this IImageCache imageCache, int? pictureId, string seoFileName, string extension, byte[] buffer, object settings = null) + /// + /// Adds an image to the cache. + /// + /// The picture id, which will be part of the resulting file name. + /// The seo friendly picture name, which will be part of the resulting file name. + /// The extension of the resulting file + /// The image binary data. + /// The image processing query. This object, if not null, is hashed and appended to the resulting file name. + public static void Put(this IImageCache imageCache, int? pictureId, string seoFileName, string extension, byte[] buffer, ProcessImageQuery query = null) { - var cachedImage = imageCache.GetCachedImage(pictureId, seoFileName, extension, settings); - imageCache.AddImageToCache(cachedImage, buffer); + var cachedImage = imageCache.Get(pictureId, seoFileName, extension, query); + imageCache.Put(cachedImage, buffer); } - /// - /// Adds an image to the cache. - /// - /// The picture object needed for building the resulting file name. - /// The image binary data. - /// The image processing settings. This object, if not null, is hashed and appended to the resulting file name. - public static void AddImageToCache(this IImageCache imageCache, Picture picture, byte[] buffer, object settings = null) + /// + /// Adds an image to the cache. + /// + /// The picture object needed for building the resulting file name. + /// The image binary data. + /// The image processing query. This object, if not null, is hashed and appended to the resulting file name. + public static void Put(this IImageCache imageCache, Picture picture, byte[] buffer, ProcessImageQuery query = null) { Guard.NotNull(picture, nameof(picture)); - imageCache.AddImageToCache(picture.Id, picture.SeoFilename, MimeTypes.MapMimeTypeToExtension(picture.MimeType), buffer, settings); + imageCache.Put(picture.Id, picture.SeoFilename, MimeTypes.MapMimeTypeToExtension(picture.MimeType), buffer, query); } - } } diff --git a/src/Libraries/SmartStore.Services/Media/ImageHeader.cs b/src/Libraries/SmartStore.Services/Media/ImageHeader.cs deleted file mode 100644 index 1385f6d55a..0000000000 --- a/src/Libraries/SmartStore.Services/Media/ImageHeader.cs +++ /dev/null @@ -1,191 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.IO; -using System.Linq; - -namespace SmartStore.Services.Media -{ - /// - /// Taken from http://stackoverflow.com/questions/111345/getting-image-dimensions-without-reading-the-entire-file/111349 - /// Minor improvements including supporting unsigned 16-bit integers when decoding Jfif and added logic - /// to load the image using new Bitmap if reading the headers fails - /// - public static class ImageHeader - { - internal class UnknownImageFormatException : ArgumentException - { - public UnknownImageFormatException(string paramName = "", Exception e = null) - : base("Could not recognise image format.", paramName, e) - { - } - } - - private static Dictionary> _imageFormatDecoders = new Dictionary>() - { - { new byte[] { 0x42, 0x4D }, DecodeBitmap }, - { new byte[] { 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 }, DecodeGif }, - { new byte[] { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 }, DecodeGif }, - { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }, DecodePng }, - { new byte[] { 0xff, 0xd8 }, DecodeJfif }, - }; - - private static int _maxMagicBytesLength = 0; - - static ImageHeader() - { - _maxMagicBytesLength = _imageFormatDecoders.Keys.OrderByDescending(x => x.Length).First().Length; - } - - /// - /// Gets the dimensions of an image. - /// - /// The path of the image to get the dimensions of. - /// The dimensions of the specified image. - /// The image was of an unrecognised format. - public static Size GetDimensions(string path) - { - try - { - using (BinaryReader binaryReader = new BinaryReader(File.OpenRead(path))) - { - try - { - return GetDimensions(binaryReader); - } - catch (ArgumentException e) - { - throw new UnknownImageFormatException("path", e); - } - } - } - catch (ArgumentException) - { - using (var b = new Bitmap(path)) - { - return b.Size; - } - } - } - - /// - /// Gets the dimensions of an image. - /// - /// The path of the image to get the dimensions of. - /// The dimensions of the specified image. - /// The image was of an unrecognised format. - public static Size GetDimensions(BinaryReader binaryReader) - { - byte[] magicBytes = new byte[_maxMagicBytesLength]; - for (int i = 0; i < _maxMagicBytesLength; i += 1) - { - magicBytes[i] = binaryReader.ReadByte(); - foreach (var kvPair in _imageFormatDecoders) - { - if (StartsWith(magicBytes, kvPair.Key)) - { - return kvPair.Value(binaryReader); - } - } - } - - throw new UnknownImageFormatException("binaryReader"); - } - - private static bool StartsWith(byte[] thisBytes, byte[] thatBytes) - { - for (int i = 0; i < thatBytes.Length; i += 1) - { - if (thisBytes[i] != thatBytes[i]) - { - return false; - } - } - - return true; - } - - private static short ReadLittleEndianInt16(BinaryReader binaryReader) - { - byte[] bytes = new byte[sizeof(short)]; - - for (int i = 0; i < sizeof(short); i += 1) - { - bytes[sizeof(short) - 1 - i] = binaryReader.ReadByte(); - } - return BitConverter.ToInt16(bytes, 0); - } - - private static ushort ReadLittleEndianUInt16(BinaryReader binaryReader) - { - byte[] bytes = new byte[sizeof(ushort)]; - - for (int i = 0; i < sizeof(ushort); i += 1) - { - bytes[sizeof(ushort) - 1 - i] = binaryReader.ReadByte(); - } - return BitConverter.ToUInt16(bytes, 0); - } - - private static int ReadLittleEndianInt32(BinaryReader binaryReader) - { - byte[] bytes = new byte[sizeof(int)]; - for (int i = 0; i < sizeof(int); i += 1) - { - bytes[sizeof(int) - 1 - i] = binaryReader.ReadByte(); - } - return BitConverter.ToInt32(bytes, 0); - } - - private static Size DecodeBitmap(BinaryReader binaryReader) - { - binaryReader.ReadBytes(16); - int width = binaryReader.ReadInt32(); - int height = binaryReader.ReadInt32(); - return new Size(width, height); - } - - private static Size DecodeGif(BinaryReader binaryReader) - { - int width = binaryReader.ReadInt16(); - int height = binaryReader.ReadInt16(); - return new Size(width, height); - } - - private static Size DecodePng(BinaryReader binaryReader) - { - binaryReader.ReadBytes(8); - int width = ReadLittleEndianInt32(binaryReader); - int height = ReadLittleEndianInt32(binaryReader); - return new Size(width, height); - } - - private static Size DecodeJfif(BinaryReader binaryReader) - { - while (binaryReader.ReadByte() == 0xff) - { - byte marker = binaryReader.ReadByte(); - short chunkLength = ReadLittleEndianInt16(binaryReader); - if (marker == 0xc0) - { - binaryReader.ReadByte(); - int height = ReadLittleEndianInt16(binaryReader); - int width = ReadLittleEndianInt16(binaryReader); - return new Size(width, height); - } - - if (chunkLength < 0) - { - ushort uchunkLength = (ushort)chunkLength; - binaryReader.ReadBytes(uchunkLength - 2); - } - else - { - binaryReader.ReadBytes(chunkLength - 2); - } - } - - throw new UnknownImageFormatException(); - } - } -} diff --git a/src/Libraries/SmartStore.Services/Media/ImageResizerService.cs b/src/Libraries/SmartStore.Services/Media/ImageResizerService.cs deleted file mode 100644 index 68810bf71d..0000000000 --- a/src/Libraries/SmartStore.Services/Media/ImageResizerService.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.IO; -using System.Linq; -using ImageResizer; -using ImageResizer.Configuration; -using ImageResizer.Plugins.PrettyGifs; - -namespace SmartStore.Services.Media -{ - - public class ImageResizerService : IImageResizerService - { - static ImageResizerService() - { - new PrettyGifs().Install(Config.Current); - } - - public bool IsSupportedImage(string fileName) - { - var ext = Path.GetExtension(fileName); - if (ext != null) - { - var extension = ext.Trim('.'); - return ImageBuilder.Current.GetSupportedFileExtensions().Any(x => x == extension); - } - - return false; - } - - public MemoryStream ResizeImage(Stream source, int? maxWidth = null, int? maxHeight = null, int? quality = 0, object settings = null) - { - Guard.NotNull(source, nameof(source)); - - var resultStream = new MemoryStream(); - var resizeSettings = ImageResizerUtil.CreateResizeSettings(settings); - - if (source.Length != 0) - { - if (quality.HasValue) - resizeSettings.Quality = quality.Value; - if (maxHeight.HasValue) - resizeSettings.MaxHeight = maxHeight.Value; - if (maxWidth.HasValue) - resizeSettings.MaxWidth = maxWidth.Value; - - ImageBuilder.Current.Build(source, resultStream, resizeSettings); - } - - return resultStream; - } - - } - -} diff --git a/src/Libraries/SmartStore.Services/Media/ImageResizerUtil.cs b/src/Libraries/SmartStore.Services/Media/ImageResizerUtil.cs deleted file mode 100644 index d7787dc889..0000000000 --- a/src/Libraries/SmartStore.Services/Media/ImageResizerUtil.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Specialized; -using ImageResizer; -using ImageResizer.Configuration; - -namespace SmartStore.Services.Media -{ - internal static class ImageResizerUtil - { - public static ResizeSettings CreateResizeSettings(object settings) - { - ResizeSettings resizeSettings; - - if (settings is string) - { - resizeSettings = new ResizeSettings((string)settings); - } - else if (settings is NameValueCollection) - { - resizeSettings = new ResizeSettings((NameValueCollection)settings); - } - else if (settings is ResizeSettings) - { - resizeSettings = (ResizeSettings)settings; - } - else - { - resizeSettings = new ResizeSettings(); - } - - return resizeSettings; - } - } -} diff --git a/src/Libraries/SmartStore.Services/Media/MediaExtensions.cs b/src/Libraries/SmartStore.Services/Media/MediaExtensions.cs index 9ba32e0422..40d85dd39e 100644 --- a/src/Libraries/SmartStore.Services/Media/MediaExtensions.cs +++ b/src/Libraries/SmartStore.Services/Media/MediaExtensions.cs @@ -102,12 +102,9 @@ internal static void UpdateTransientStateForEntityInternal( Action deleteAction, bool save) where TEntity : BaseEntity where TMedia : BaseEntity { - bool editMode = !entity.IsTransientRecord(); - var modifiedProperties = editMode ? rs.Context.GetModifiedProperties(entity) : new Dictionary(); - object obj = null; int prevMediaId = 0; - if (modifiedProperties.TryGetValue(propName, out obj)) + if (rs.Context.TryGetModifiedProperty(entity, propName, out obj)) { prevMediaId = ((int?)obj).GetValueOrDefault(); } diff --git a/src/Libraries/SmartStore.Services/Media/MediaFileSystem.cs b/src/Libraries/SmartStore.Services/Media/MediaFileSystem.cs index 75fcc59fdc..6317105fde 100644 --- a/src/Libraries/SmartStore.Services/Media/MediaFileSystem.cs +++ b/src/Libraries/SmartStore.Services/Media/MediaFileSystem.cs @@ -11,23 +11,45 @@ public interface IMediaFileSystem : IFileSystem public class MediaFileSystem : LocalFileSystem, IMediaFileSystem { + private static string _mediaPublicPath; + public MediaFileSystem() - : base(GetMediaBasePath(), CommonHelper.GetAppSetting("sm:MediaPublicPath")) + : base(GetMediaBasePath(), "~/" + GetMediaPublicPath()) { + this.TryCreateFolder("Storage"); this.TryCreateFolder("Thumbs"); this.TryCreateFolder("Uploaded"); this.TryCreateFolder("QueuedEmailAttachment"); this.TryCreateFolder("Downloads"); } - private static string GetMediaBasePath() + public static string GetMediaBasePath() { var path = CommonHelper.GetAppSetting("sm:MediaStoragePath")?.Trim().NullEmpty(); if (path == null) { - path = "/Media/" + DataSettings.Current.TenantName; + path = "/App_Data/Tenants/" + DataSettings.Current.TenantName + "/Media"; } + return path; } + + public static string GetMediaPublicPath() + { + if (_mediaPublicPath == null) + { + var path = CommonHelper.GetAppSetting("sm:MediaPublicPath")?.Trim().NullEmpty() ?? "media"; + + if (path.IsWebUrl()) + { + throw new NotSupportedException("Fully qualified URLs are not supported for the 'sm:MediaPublicPath' setting."); + } + + _mediaPublicPath = path.TrimStart('~', '/').Replace('\\', '/').ToLower().EnsureEndsWith("/"); + } + + + return _mediaPublicPath; + } } } diff --git a/src/Libraries/SmartStore.Services/Media/PictureService.cs b/src/Libraries/SmartStore.Services/Media/PictureService.cs index fa88ec704e..4e1b1b6f53 100644 --- a/src/Libraries/SmartStore.Services/Media/PictureService.cs +++ b/src/Libraries/SmartStore.Services/Media/PictureService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Data.Entity; using System.Drawing; @@ -6,9 +7,11 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using ImageResizer; +using System.Web; +using System.Web.Hosting; using SmartStore.Collections; using SmartStore.Core; +using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Media; @@ -22,65 +25,125 @@ namespace SmartStore.Services.Media { + [Serializable] + public class PictureInfo + { + public int Id { get; set; } + /// + /// The virtual path to the image file to be processed by the media middleware controller, e.g. "/media/image/1234/image.jpg" + /// + public string Path { get; set; } + public int? Width { get; set; } + public int? Height { get; set; } + public string MimeType { get; set; } + public string Extension { get; set; } + } + public partial class PictureService : IPictureService { - private const int MULTIPLE_THUMB_DIRECTORIES_LENGTH = 4; - private const string STATIC_IMAGE_PATH = "~/Content/Images"; + // 0 = Id + private const string MEDIACACHE_LOOKUP_KEY = "media:info-{0}"; + private const string MEDIACACHE_LOOKUP_KEY_PATTERN = "media:info-*"; private readonly IRepository _pictureRepository; private readonly IRepository _productPictureRepository; private readonly ISettingService _settingService; - private readonly ILogger _logger; private readonly IEventPublisher _eventPublisher; private readonly MediaSettings _mediaSettings; - private readonly IImageResizerService _imageResizerService; + private readonly IImageProcessor _imageProcessor; private readonly IImageCache _imageCache; private readonly Provider _storageProvider; + private readonly IStoreContext _storeContext; + private readonly HttpContextBase _httpContext; + private readonly ICacheManager _cacheManager; + + private readonly string _host; + private readonly string _appPath; - private string _staticImagePath; + private static readonly string _processedImagesRootPath; + private static readonly string _fallbackImagesRootPath; + + static PictureService() + { + _processedImagesRootPath = MediaFileSystem.GetMediaPublicPath() + "image/"; + _fallbackImagesRootPath = "content/images/"; + } - public PictureService( + public PictureService( IRepository pictureRepository, IRepository productPictureRepository, ISettingService settingService, - ILogger logger, IEventPublisher eventPublisher, MediaSettings mediaSettings, - IImageResizerService imageResizerService, + IImageProcessor imageProcessor, IImageCache imageCache, - IProviderManager providerManager) + IProviderManager providerManager, + IStoreContext storeContext, + HttpContextBase httpContext, + ICacheManager cacheManager) { _pictureRepository = pictureRepository; _productPictureRepository = productPictureRepository; _settingService = settingService; - _logger = logger; _eventPublisher = eventPublisher; _mediaSettings = mediaSettings; - _imageResizerService = imageResizerService; + _imageProcessor = imageProcessor; _imageCache = imageCache; + _storeContext = storeContext; + _httpContext = httpContext; + _cacheManager = cacheManager; var systemName = settingService.GetSettingByKey("Media.Storage.Provider", DatabaseMediaStorageProvider.SystemName); - _storageProvider = providerManager.GetProvider(systemName); - } + + Logger = NullLogger.Instance; + + string appPath = "/"; + + if (HostingEnvironment.IsHosted) + { + appPath = HostingEnvironment.ApplicationVirtualPath.EmptyNull(); + + var cdn = storeContext.CurrentStore.ContentDeliveryNetwork; + if (cdn.HasValue() && !_httpContext.IsDebuggingEnabled && !_httpContext.Request.IsLocal) + { + _host = cdn; + } + else if (mediaSettings.AutoGenerateAbsoluteUrls) + { + var uri = httpContext.Request.Url; + _host = "{0}://{1}{2}".FormatInvariant(uri.Scheme, uri.Authority, appPath); + } + else + { + _host = appPath; + } + } + + _host = _host.EmptyNull().EnsureEndsWith("/"); + _appPath = appPath.EnsureEndsWith("/"); + + } + + public ILogger Logger { get; set; } #region Utilities - protected virtual string StaticImagePath + public static string FallbackImagesRootPath { - get { return _staticImagePath ?? (_staticImagePath = CommonHelper.MapPath(STATIC_IMAGE_PATH, false)); } + get { return "~/" + _fallbackImagesRootPath; } } - protected virtual string GetDefaultImageFileName(PictureType defaultPictureType = PictureType.Entity) + protected virtual string GetFallbackImageFileName(FallbackPictureType defaultPictureType = FallbackPictureType.Entity) { string defaultImageFileName; switch (defaultPictureType) { - case PictureType.Entity: + case FallbackPictureType.Entity: defaultImageFileName = _settingService.GetSettingByKey("Media.DefaultImageName", "default-image.png"); break; - case PictureType.Avatar: + case FallbackPictureType.Avatar: defaultImageFileName = _settingService.GetSettingByKey("Media.Customer.DefaultAvatarImageName", "default-avatar.jpg"); break; default: @@ -91,172 +154,59 @@ protected virtual string GetDefaultImageFileName(PictureType defaultPictureType return defaultImageFileName; } - protected internal virtual string GetProcessedImageUrl( - object source, // byte[], string or Picture - string seoFileName, - string extension, - int targetSize = 0, - string storeLocation = null) - { - var resizeSettings = new ResizeSettings(); - if (targetSize > 0) - { - resizeSettings.MaxWidth = targetSize; - resizeSettings.MaxHeight = targetSize; - } - - var picture = source as Picture; - - var cachedImage = _imageCache.GetCachedImage( - picture?.Id, - seoFileName, - extension, - resizeSettings); - - if (!cachedImage.Exists) - { - lock (String.Intern(cachedImage.Path)) - { - byte[] buffer = null; - - try - { - if (source is string) - { - // static default image - buffer = File.ReadAllBytes((string)source); - } - else if (source is Picture) - { - buffer = LoadPictureBinary((Picture)source); - } - else if (source is byte[]) - { - buffer = (byte[])source; - } - - if (buffer == null || buffer.LongLength == 0) - { - return string.Empty; - } - } - catch (Exception exception) - { - _logger.ErrorFormat(exception, "Error reading media file '{0}'.", source); - return string.Empty; - } + #endregion - try - { - _imageCache.ProcessAndAddImageToCache(cachedImage, buffer, targetSize); - } - catch (Exception exception) - { - _logger.ErrorFormat(exception, "Error processing/writing media file '{0}'.", cachedImage.Path); - return string.Empty; - } - } - } + #region Imaging - var url = _imageCache.GetImageUrl(cachedImage.Path, storeLocation); - return url; + public virtual byte[] ValidatePicture(byte[] pictureBinary, string mimeType) + { + var size = Size.Empty; + return ValidatePicture(pictureBinary, mimeType, out size); } - protected internal virtual async Task GetProcessedImageUrlAsync( - object source, // byte[], string or Picture - string seoFileName, - string extension, - int targetSize = 0, - string storeLocation = null) + public virtual byte[] ValidatePicture(byte[] pictureBinary, string mimeType, out Size size) { - var resizeSettings = new ResizeSettings(); - if (targetSize > 0) - { - resizeSettings.MaxWidth = targetSize; - resizeSettings.MaxHeight = targetSize; - } + Guard.NotNull(pictureBinary, nameof(pictureBinary)); + Guard.NotEmpty(mimeType, nameof(mimeType)); - var picture = source as Picture; + size = Size.Empty; - var cachedImage = _imageCache.GetCachedImage( - picture?.Id, - seoFileName, - extension, - resizeSettings); + var originalSize = ImageHeader.GetDimensions(pictureBinary, mimeType); + var maxSize = _mediaSettings.MaximumImageSize; - if (!cachedImage.Exists) + var query = new ProcessImageQuery(pictureBinary) { - byte[] buffer = null; - - try - { - if (source is string) - { - // static default image - buffer = File.ReadAllBytes((string)source); - } - else if (source is Picture) - { - buffer = await LoadPictureBinaryAsync((Picture)source); - } - else if (source is byte[]) - { - buffer = (byte[])source; - } + Quality = _mediaSettings.DefaultImageQuality, + Format = MimeTypes.MapMimeTypeToExtension(mimeType), + IsValidationMode = true + }; - if (buffer == null || buffer.Length == 0) - { - return string.Empty; - } - } - catch (Exception exception) - { - _logger.ErrorFormat(exception, "Error reading media file '{0}'.", source); - return string.Empty; - } + if (originalSize.IsEmpty || (originalSize.Height <= maxSize && originalSize.Width <= maxSize)) + { + // Give subscribers the chance to (pre)-process + var evt = new ImageUploadValidatedEvent(query, originalSize); + _eventPublisher.Publish(evt); - try + if (evt.ResultBuffer != null) { - await _imageCache.ProcessAndAddImageToCacheAsync(cachedImage, buffer, targetSize); + // Maybe subscriber forgot to set this, so check + size = evt.ResultSize.IsEmpty ? originalSize : evt.ResultSize; + return evt.ResultBuffer; } - catch (Exception exception) + else { - _logger.ErrorFormat(exception, "Error processing/writing media file '{0}'.", cachedImage.Path); - return string.Empty; + size = originalSize; + return pictureBinary; } } - var url = _imageCache.GetImageUrl(cachedImage.Path, storeLocation); - return url; - } - - #endregion - - #region Methods - - public virtual byte[] ValidatePicture(byte[] pictureBinary) - { - var size = Size.Empty; - return ValidatePicture(pictureBinary, out size); - } - - public virtual byte[] ValidatePicture(byte[] pictureBinary, out Size size) - { - size = Size.Empty; - - var originalSize = GetPictureSize(pictureBinary); - var maxSize = _mediaSettings.MaximumImageSize; - - if (originalSize.IsEmpty || (originalSize.Height <= maxSize && originalSize.Width <= maxSize)) - { - size = originalSize; - return pictureBinary; - } + query.MaxWidth = maxSize; + query.MaxHeight = maxSize; - using (var resultStream = _imageResizerService.ResizeImage(new MemoryStream(pictureBinary), maxSize, maxSize, _mediaSettings.DefaultImageQuality)) + using (var result = _imageProcessor.ProcessImage(query)) { - var buffer = resultStream.GetBuffer(); - size = GetPictureSize(buffer); + size = new Size(result.Width, result.Height); + var buffer = result.OutputStream.GetBuffer(); return buffer; } } @@ -289,22 +239,11 @@ public virtual byte[] FindEqualPicture(byte[] pictureBinary, IEnumerable LoadPictureBinaryAsync(Picture picture) return _storageProvider.Value.LoadAsync(picture.ToMedia()); } - public virtual Size GetPictureSize(byte[] pictureBinary) + public virtual Size GetPictureSize(byte[] pictureBinary, string mimeType = null) { - if (pictureBinary == null || pictureBinary.Length == 0) - { - return Size.Empty; - } - - return GetPictureSize(new MemoryStream(pictureBinary), false); + return ImageHeader.GetDimensions(pictureBinary, mimeType); } - protected virtual Size GetPictureSize(Stream input, bool leaveOpen = true) + public IDictionary GetPictureInfos(IEnumerable pictureIds) { - Guard.NotNull(input, nameof(input)); + Guard.NotNull(pictureIds, nameof(pictureIds)); - var size = Size.Empty; + var allRequestedInfos = (from id in pictureIds.Distinct().Where(x => x > 0) + let cacheKey = MEDIACACHE_LOOKUP_KEY.FormatInvariant(id) + select new + { + PictureId = id, + CacheKey = cacheKey, + Info = _cacheManager.Contains(cacheKey) ? _cacheManager.Get(cacheKey) : (PictureInfo)null + }).ToList(); - if (!input.CanSeek || input.Length == 0) + var result = new Dictionary(allRequestedInfos.Count); + var uncachedPictureIds = allRequestedInfos.Where(x => x.Info == null).Select(x => x.PictureId).ToArray(); + var uncachedPictures = new Dictionary(); + + if (uncachedPictureIds.Length > 0) { - return size; + uncachedPictures = GetPicturesByIds(uncachedPictureIds, false).ToDictionary(x => x.Id); } - try + foreach (var info in allRequestedInfos) { - using (var reader = new BinaryReader(input, Encoding.UTF8, true)) + if (info.Info != null) { - size = ImageHeader.GetDimensions(reader); + result.Add(info.PictureId, info.Info); } - } - catch (Exception) - { - // something went wrong with fast image access, - // so get original size the classic way - try + else { - input.Seek(0, SeekOrigin.Begin); - using (var b = new Bitmap(input)) - { - size = new Size(b.Width, b.Height); - } + // TBD: (mc) Does this need a locking strategy? Apparently yes. But it is hard to accomplish for a random sequence + // without locking the whole thing and loosing performance. Better no lock (?) + var newInfo = CreatePictureInfo(uncachedPictures.Get(info.PictureId)); + result.Add(info.PictureId, newInfo); + _cacheManager.Put(info.CacheKey, newInfo); } - catch { } } - finally + + return result; + } + + public PictureInfo GetPictureInfo(int? pictureId) + { + if (pictureId.GetValueOrDefault() < 1) + return null; + + var cacheKey = MEDIACACHE_LOOKUP_KEY.FormatInvariant(pictureId.GetValueOrDefault()); + var info = _cacheManager.Get(cacheKey, () => { - if (!leaveOpen) - { - input.Dispose(); - } - } + return CreatePictureInfo(GetPictureById(pictureId.GetValueOrDefault())); + }); - return size; + return info; } - public virtual string GetPictureUrl( - int pictureId, - int targetSize = 0, - bool showDefaultPicture = true, - string storeLocation = null, - PictureType defaultPictureType = PictureType.Entity) + public PictureInfo GetPictureInfo(Picture picture) + { + if (picture == null) + return null; + + var cacheKey = MEDIACACHE_LOOKUP_KEY.FormatInvariant(picture.Id); + var info = _cacheManager.Get(cacheKey, () => + { + return CreatePictureInfo(picture); + }); + + return info; + } + + public virtual string GetUrl(int pictureId, int targetSize = 0, FallbackPictureType fallbackType = FallbackPictureType.Entity, string host = null) { - return GetPictureUrl(GetPictureById(pictureId), targetSize, showDefaultPicture, storeLocation, defaultPictureType); - } + return GetUrl(GetPictureInfo(pictureId), targetSize, fallbackType, host); + } - public virtual Task GetPictureUrlAsync( - int pictureId, - int targetSize = 0, - bool showDefaultPicture = true, - string storeLocation = null, - PictureType defaultPictureType = PictureType.Entity) + public virtual string GetUrl(Picture picture, int targetSize = 0, FallbackPictureType fallbackType = FallbackPictureType.Entity, string host = null) { - return GetPictureUrlAsync(GetPictureById(pictureId), targetSize, showDefaultPicture, storeLocation, defaultPictureType); + return GetUrl(GetPictureInfo(picture), targetSize, fallbackType, host); } - public virtual string GetPictureUrl( - Picture picture, - int targetSize = 0, - bool showDefaultPicture = true, - string storeLocation = null, - PictureType defaultPictureType = PictureType.Entity) - { - var url = PrepareGetPictureUrl(picture, targetSize, showDefaultPicture, storeLocation, defaultPictureType); + public string GetUrl(PictureInfo info, int targetSize = 0, FallbackPictureType fallbackType = FallbackPictureType.Entity, string host = null) + { + string path = null; + string query = null; - if (url.IsEmpty() && picture != null) + if (info?.Path != null) { - url = GetProcessedImageUrl( - picture, - picture.SeoFilename, - MimeTypes.MapMimeTypeToExtension(picture.MimeType), - targetSize, - storeLocation); + path = info.Path; + } + else if (fallbackType > FallbackPictureType.NoFallback) + { + path = String.Concat(_processedImagesRootPath, "0/", GetFallbackImageFileName(fallbackType)); } - return url; - } - - public virtual Task GetPictureUrlAsync( - Picture picture, - int targetSize = 0, - bool showDefaultPicture = true, - string storeLocation = null, - PictureType defaultPictureType = PictureType.Entity) - { - var url = PrepareGetPictureUrl(picture, targetSize, showDefaultPicture, storeLocation, defaultPictureType); - - if (url.IsEmpty() && picture != null) + if (path != null) { - return GetProcessedImageUrlAsync( - picture, - picture.SeoFilename, - MimeTypes.MapMimeTypeToExtension(picture.MimeType), - targetSize, - storeLocation); + if (targetSize > 0) + { + // TBD: (mc) let pass query string as NameValueCollection (?) + query = "?size=" + targetSize; + } + + path = BuildUrlCore(path, query, host); } - return Task.FromResult(url); + return path; } - private string PrepareGetPictureUrl( - Picture picture, - int targetSize = 0, - bool showDefaultPicture = true, - string storeLocation = null, - PictureType defaultPictureType = PictureType.Entity) + public virtual string GetFallbackUrl(int targetSize = 0, FallbackPictureType fallbackType = FallbackPictureType.Entity, string host = null) + { + return GetUrl((PictureInfo)null, targetSize, fallbackType, host); + } + + /// + /// Creates a cacheable counterpart for a Picture object instance + /// + /// + /// + protected virtual PictureInfo CreatePictureInfo(Picture picture) { if (picture == null) - { - if (showDefaultPicture) - { - return GetDefaultPictureUrl(targetSize, defaultPictureType, storeLocation); - } - else - { - return string.Empty; - } - } + return null; + + var extension = MimeTypes.MapMimeTypeToExtension(picture.MimeType); + // Build virtual path with pattern "media/image/{id}/{SeoFileName}.{extension}" + var path = "{0}{1}/{2}.{3}".FormatInvariant( + _processedImagesRootPath, + picture.Id, + picture.SeoFilename.NullEmpty() ?? picture.Id.ToString(ImageCache.IdFormatString), + extension); + + // Do some maintenance stuff EnsurePictureSizeResolved(picture, true); if (picture.IsNew) { - _imageCache.DeleteCachedImages(picture); + _imageCache.Delete(picture); - // we do not validate picture binary here to ensure that no exception ("Parameter is not valid") will be thrown + // We do not validate picture binary here to ensure that no exception ("Parameter is not valid") will be thrown UpdatePicture( picture, - LoadPictureBinary(picture), + null, picture.MimeType, picture.SeoFilename, false, false); } + + return new PictureInfo + { + Id = picture.Id, + MimeType = picture.MimeType, + Extension = extension, + Path = path, + Width = picture?.Width, + Height = picture?.Height, + }; + } + + protected virtual string BuildUrlCore(string virtualPath, string query, string host) + { + // TBD: (mc) No arg check because of performance?! - return string.Empty; + if (host == null) + { + host = _host; + } + else if (host == string.Empty) + { + host = _appPath; + } + else + { + host = host.EnsureEndsWith("/"); + } + + var sb = new StringBuilder(host, 100); + + // Strip leading "/", the host/apppath has this already + if (virtualPath[0] == '/') + { + virtualPath = virtualPath.Substring(1); + } + + // Append media path + sb.Append(virtualPath); + + // Append query + if (query != null && query.Length > 0) + { + if (query[0] != '?') sb.Append("?"); + sb.Append(query); + } + + return sb.ToString(); } private void EnsurePictureSizeResolved(Picture picture, bool saveOnResolve) @@ -487,7 +473,7 @@ private void EnsurePictureSizeResolved(Picture picture, bool saveOnResolve) { try { - var size = GetPictureSize(stream, true); + var size = ImageHeader.GetDimensions(stream, picture.MimeType, true); picture.Width = size.Width; picture.Height = size.Height; picture.UpdatedOnUtc = DateTime.UtcNow; @@ -516,60 +502,69 @@ private void EnsurePictureSizeResolved(Picture picture, bool saveOnResolve) } } - public virtual string GetDefaultPictureUrl( - int targetSize = 0, - PictureType defaultPictureType = PictureType.Entity, - string storeLocation = null) + public int ClearUrlCache() { - var defaultImageFileName = GetDefaultImageFileName(defaultPictureType); - var filePath = Path.Combine(StaticImagePath, defaultImageFileName); + return _cacheManager.RemoveByPattern(MEDIACACHE_LOOKUP_KEY_PATTERN); + } + + #endregion - var url = GetProcessedImageUrl( - filePath, - Path.GetFileNameWithoutExtension(filePath), - Path.GetExtension(filePath), - targetSize, - storeLocation); + #region Metadata Storage - return url; + public virtual string GetPictureSeName(string name) + { + return SeoHelper.GetSeName(name, true, false); } - public virtual Picture GetPictureById(int pictureId) - { - if (pictureId == 0) - return null; + public virtual Picture SetSeoFilename(int pictureId, string seoFilename) + { + var picture = GetPictureById(pictureId); - var picture = _pictureRepository.GetById(pictureId); - return picture; - } + // update if it has been changed + if (picture != null && seoFilename != picture.SeoFilename) + { + UpdatePicture(picture, null, picture.MimeType, seoFilename, true, false); + } - public virtual IPagedList GetPictures(int pageIndex, int pageSize) - { - var query = from p in _pictureRepository.Table - orderby p.Id descending - select p; + return picture; + } - var pics = new PagedList(query, pageIndex, pageSize); - return pics; - } + public virtual Picture GetPictureById(int pictureId) + { + if (pictureId == 0) + return null; - public virtual IList GetPicturesByProductId(int productId, int recordsToReturn = 0) - { - if (productId == 0) - return new List(); + var picture = _pictureRepository.GetById(pictureId); + return picture; + } + + public virtual IPagedList GetPictures(int pageIndex, int pageSize) + { + var query = from p in _pictureRepository.Table + orderby p.Id descending + select p; - var query = from p in _pictureRepository.Table + var pics = new PagedList(query, pageIndex, pageSize); + return pics; + } + + public virtual IList GetPicturesByProductId(int productId, int recordsToReturn = 0) + { + if (productId == 0) + return new List(); + + var query = from p in _pictureRepository.Table join pp in _productPictureRepository.Table on p.Id equals pp.PictureId - orderby pp.DisplayOrder - where pp.ProductId == productId - select p; + orderby pp.DisplayOrder + where pp.ProductId == productId + select p; - if (recordsToReturn > 0) - query = query.Take(recordsToReturn); + if (recordsToReturn > 0) + query = query.Take(recordsToReturn); - var pics = query.ToList(); - return pics; - } + var pics = query.ToList(); + return pics; + } public virtual Multimap GetPicturesByProductIds(int[] productIds, int? maxPicturesPerProduct = null, bool withBlobs = false) { @@ -638,16 +633,16 @@ public virtual void DeletePicture(Picture picture) Guard.NotNull(picture, nameof(picture)); // delete thumbs - _imageCache.DeleteCachedImages(picture); + _imageCache.Delete(picture); + + // delete from url cache + _cacheManager.Remove(MEDIACACHE_LOOKUP_KEY.FormatInvariant(picture.Id)); // delete from storage _storageProvider.Value.Remove(picture.ToMedia()); // delete entity _pictureRepository.Delete(picture); - - // event notification - _eventPublisher.EntityDeleted(picture); } public virtual Picture InsertPicture( @@ -677,9 +672,6 @@ public virtual Picture InsertPicture( // Save to storage. _storageProvider.Value.Save(picture.ToMedia(), pictureBinary); - // Event notification. - _eventPublisher.EntityInserted(picture); - return picture; } @@ -690,25 +682,25 @@ public virtual Picture InsertPicture( bool isNew, bool isTransient = true, bool validateBinary = true) - { + { var size = Size.Empty; - if (validateBinary) - { - pictureBinary = ValidatePicture(pictureBinary, out size); - } + if (validateBinary) + { + pictureBinary = ValidatePicture(pictureBinary, mimeType, out size); + } return InsertPicture(pictureBinary, mimeType, seoFilename, isNew, size.Width, size.Height, isTransient); - } + } - public virtual void UpdatePicture( + public virtual void UpdatePicture( Picture picture, byte[] pictureBinary, string mimeType, string seoFilename, bool isNew, bool validateBinary = true) - { + { if (picture == null) return; @@ -717,20 +709,23 @@ public virtual void UpdatePicture( var size = Size.Empty; - if (validateBinary) - { - pictureBinary = ValidatePicture(pictureBinary, out size); - } + if (validateBinary && pictureBinary != null) + { + pictureBinary = ValidatePicture(pictureBinary, mimeType, out size); + } - // delete old thumbs if a picture has been changed - if (seoFilename != picture.SeoFilename) - { - _imageCache.DeleteCachedImages(picture); - } + // delete old thumbs if a picture has been changed + if (seoFilename != picture.SeoFilename) + { + _imageCache.Delete(picture); + + // delete from url cache + _cacheManager.Remove(MEDIACACHE_LOOKUP_KEY.FormatInvariant(picture.Id)); + } - picture.MimeType = mimeType; - picture.SeoFilename = seoFilename; - picture.IsNew = isNew; + picture.MimeType = mimeType; + picture.SeoFilename = seoFilename; + picture.IsNew = isNew; picture.UpdatedOnUtc = DateTime.UtcNow; if (!size.IsEmpty) @@ -739,15 +734,15 @@ public virtual void UpdatePicture( picture.Height = size.Height; } - _pictureRepository.Update(picture); + _pictureRepository.Update(picture); // save to storage - _storageProvider.Value.Save(picture.ToMedia(), pictureBinary); - - // event notification - _eventPublisher.EntityUpdated(picture); - } + if (pictureBinary != null) + { + _storageProvider.Value.Save(picture.ToMedia(), pictureBinary); + } + } - #endregion - } + #endregion + } } diff --git a/src/Libraries/SmartStore.Services/Media/ProcessImageException.cs b/src/Libraries/SmartStore.Services/Media/ProcessImageException.cs new file mode 100644 index 0000000000..1cb2038497 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/ProcessImageException.cs @@ -0,0 +1,45 @@ +using System; + +namespace SmartStore.Services.Media +{ + public sealed class ProcessImageException : Exception + { + public ProcessImageException() + : this((string)null, null) + { + } + + public ProcessImageException(ProcessImageQuery query) + : this(query, null) + { + } + + public ProcessImageException(string message, ProcessImageQuery query) + : base(message) + { + Query = query; + } + + public ProcessImageException(ProcessImageQuery query, Exception innerException) + : base(CreateMessage(query), innerException) + { + Query = query; + } + + private static string CreateMessage(ProcessImageQuery query) + { + var fileName = query?.FileName; + + if (fileName.HasValue()) + { + return "Error while processing image '{0}'.".FormatCurrent(fileName); + } + else + { + return "Error while processing image."; + } + } + + public ProcessImageQuery Query { get; private set; } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/ProcessImageQuery.cs b/src/Libraries/SmartStore.Services/Media/ProcessImageQuery.cs new file mode 100644 index 0000000000..7e316381ef --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/ProcessImageQuery.cs @@ -0,0 +1,201 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.IO; +using System.Collections.Specialized; +using System.Text; +using SmartStore.Collections; +using System.Drawing; +using ImageProcessor.Imaging.Formats; + +namespace SmartStore.Services.Media +{ + public class ProcessImageQuery : QueryString + { + private readonly static HashSet _supportedTokens = new HashSet { "w", "h", "q", "m", "size" }; + + public ProcessImageQuery() + : this(null, new NameValueCollection()) + { + } + + public ProcessImageQuery(byte[] source) + : this(source, new NameValueCollection()) + { + } + + public ProcessImageQuery(Stream source) + : this(source, new NameValueCollection()) + { + } + + public ProcessImageQuery(Image source) + : this(source, new NameValueCollection()) + { + } + + public ProcessImageQuery(string source) + : this(source, new NameValueCollection()) + { + } + + public ProcessImageQuery(object source, NameValueCollection query) + : base(SanitizeCollection(query)) + { + Guard.NotNull(query, nameof(query)); + + Source = source; + DisposeSource = true; + Notify = true; + } + + public ProcessImageQuery(ProcessImageQuery query) + : base(SanitizeCollection(query)) + { + Guard.NotNull(query, nameof(query)); + + Source = query.Source; + Format = query.Format; + DisposeSource = query.DisposeSource; + } + + private static NameValueCollection SanitizeCollection(NameValueCollection query) + { + // We just need the supported flags + var sanitizable = query.AllKeys.Any(x => !_supportedTokens.Contains(x)); + if (sanitizable) + { + var copy = new NameValueCollection(query); + foreach (var key in copy.AllKeys) + { + if (!_supportedTokens.Contains(key)) + { + copy.Remove(key); + } + } + + return copy; + } + + return query; + } + + /// + /// The source image's physical path, app-relative virtual path, or a Stream, byte array or Image instance. + /// + public object Source { get; set; } + + public string FileName { get; set; } + + /// + /// Whether to dispose the source stream after resizing completes + /// + public bool DisposeSource { get; set; } + + /// + /// Whether to execute an applicable post processor which + /// can reduce the resulting file size drastically, but also + /// can slow down processing time. + /// + public bool ExecutePostProcessor { get; set; } + + public int? MaxWidth + { + get { return Get("w"); } + set { Set("w", value); } + } + + public int? MaxHeight + { + get { return Get("h"); } + set { Set("h", value); } + } + + public int? Quality + { + get { return Get("q"); } + set { Set("q", value); } + } + + // TODO: (mc) make Enum + public string ScaleMode + { + get { return Get("m"); } + set { Set("m", value); } + } + + /// + /// Gets or sets the output file format either as a string ("png", "jpg", and "gif"), + /// or as a format object instance. + /// When format is not specified, the original format of the source image is used (unless it is not a web safe format - jpeg is the fallback in that scenario). + /// + public object Format { get; set; } + + public bool IsValidationMode { get; set; } + + public bool Notify { get; set; } + + private T Get(string name) + { + return base[name].Convert(); + } + + private void Set(string name, T val) + { + if (val == null) + base.Remove(name); + else + base.Add(name, val.Convert(), true); + } + + + public bool NeedsProcessing(bool ignoreQualityFlag = false) + { + if (base.Count == 0) + return false; + + if (ignoreQualityFlag && base.Count == 1 && base["q"] != null) + { + // Return false if ignoreQualityFlag is true and "q" is the only flag. + return false; + } + + return true; + } + + public string CreateHash() + { + var sb = new StringBuilder(); + + foreach (var key in base.AllKeys) + { + if (key == "m" && base["m"] == "max") + continue; // Mode 'max' is default and can be omitted + + sb.Append("-"); + sb.Append(key); + sb.Append(base[key]); + } + + return sb.ToString(); + } + + public string GetResultExtension() + { + if (Format == null) + { + return null; + } + else if (Format is ISupportedImageFormat) + { + return ((ISupportedImageFormat)Format).DefaultExtension; + } + else if (Format is string) + { + return (string)Format; + } + + return null; + } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/ProcessImageResult.cs b/src/Libraries/SmartStore.Services/Media/ProcessImageResult.cs new file mode 100644 index 0000000000..e520fcfefd --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/ProcessImageResult.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; + +namespace SmartStore.Services.Media +{ + public class ProcessImageResult : DisposableObject + { + public ProcessImageQuery Query { get; set; } + + public MemoryStream OutputStream { get; set; } + + public int? SourceWidth { get; set; } + public int? SourceHeight { get; set; } + + public string FileExtension { get; set; } + public string MimeType { get; set; } + public int Width { get; set; } + public int Height { get; set; } + + public long ProcessTimeMs { get; set; } + + protected override void OnDispose(bool disposing) + { + if (disposing && OutputStream != null) + { + OutputStream.Dispose(); + OutputStream = null; + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/Media/Storage/DatabaseMediaStorageProvider.cs b/src/Libraries/SmartStore.Services/Media/Storage/DatabaseMediaStorageProvider.cs index 5dede8663c..32c4cae971 100644 --- a/src/Libraries/SmartStore.Services/Media/Storage/DatabaseMediaStorageProvider.cs +++ b/src/Libraries/SmartStore.Services/Media/Storage/DatabaseMediaStorageProvider.cs @@ -76,8 +76,8 @@ public void Save(MediaItem media, byte[] data) media.Entity.MediaStorageId = newStorage.Id; - // Required because during import the ChangeTracker doesn't treat media.Entity as changed entry. - _dbContext.ChangeState((BaseEntity)media.Entity, System.Data.Entity.EntityState.Modified); + //// Required because during import the ChangeTracker doesn't treat media.Entity as changed entry. + //_dbContext.ChangeState((BaseEntity)media.Entity, System.Data.Entity.EntityState.Modified); _dbContext.SaveChanges(); } diff --git a/src/Libraries/SmartStore.Services/Media/Storage/FileSystemMediaStorageProvider.cs b/src/Libraries/SmartStore.Services/Media/Storage/FileSystemMediaStorageProvider.cs index c5d0707f04..f7f3acc322 100644 --- a/src/Libraries/SmartStore.Services/Media/Storage/FileSystemMediaStorageProvider.cs +++ b/src/Libraries/SmartStore.Services/Media/Storage/FileSystemMediaStorageProvider.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using SmartStore.Core.Domain.Media; using SmartStore.Core.IO; using SmartStore.Core.Plugins; @@ -25,14 +26,23 @@ public static string SystemName get { return "MediaStorage.SmartStoreFileSystem"; } } - protected string GetPicturePath(MediaItem media) + protected string GetPath(MediaItem media) { - return _fileSystem.Combine(media.Path, media.GetFileName()); + var fileName = media.GetFileName(); + + var picture = media.Entity as Picture; + if (picture != null) + { + var subfolder = fileName.Substring(0, ImageCache.MaxDirLength); + fileName = _fileSystem.Combine(subfolder, fileName); + } + + return _fileSystem.Combine(media.Path, fileName); } public Stream OpenRead(MediaItem media) { - var file = _fileSystem.GetFile(GetPicturePath(media)); + var file = _fileSystem.GetFile(GetPath(media)); return file.Exists ? file.OpenRead() : null; } @@ -41,7 +51,7 @@ public byte[] Load(MediaItem media) { Guard.NotNull(media, nameof(media)); - var filePath = GetPicturePath(media); + var filePath = GetPath(media); return _fileSystem.ReadAllBytes(filePath) ?? new byte[0]; } @@ -49,7 +59,7 @@ public async Task LoadAsync(MediaItem media) { Guard.NotNull(media, nameof(media)); - var filePath = GetPicturePath(media); + var filePath = GetPath(media); return (await _fileSystem.ReadAllBytesAsync(filePath)) ?? new byte[0]; } @@ -59,7 +69,7 @@ public void Save(MediaItem media, byte[] data) // TODO: (?) if the new file extension differs from the old one then the old file never gets deleted - var filePath = GetPicturePath(media); + var filePath = GetPath(media); if (data != null && data.LongLength != 0) { @@ -77,7 +87,7 @@ public async Task SaveAsync(MediaItem media, byte[] data) // TODO: (?) if the new file extension differs from the old one then the old file never gets deleted - var filePath = GetPicturePath(media); + var filePath = GetPath(media); if (data != null && data.LongLength != 0) { @@ -93,7 +103,7 @@ public void Remove(params MediaItem[] medias) { foreach (var media in medias) { - var filePath = GetPicturePath(media); + var filePath = GetPath(media); _fileSystem.DeleteFile(filePath); } } @@ -105,7 +115,7 @@ public void MoveTo(ISupportsMediaMoving target, MediaMoverContext context, Media Guard.NotNull(context, nameof(context)); Guard.NotNull(media, nameof(media)); - var filePath = GetPicturePath(media); + var filePath = GetPath(media); try { @@ -132,7 +142,7 @@ public void Receive(MediaMoverContext context, MediaItem media, byte[] data) // store data into file if (data != null && data.LongLength != 0) { - var filePath = GetPicturePath(media); + var filePath = GetPath(media); if (!_fileSystem.FileExists(filePath)) { @@ -155,7 +165,7 @@ public async Task ReceiveAsync(MediaMoverContext context, MediaItem media, byte[ // store data into file if (data != null && data.LongLength != 0) { - var filePath = GetPicturePath(media); + var filePath = GetPath(media); await _fileSystem.WriteAllBytesAsync(filePath, data); diff --git a/src/Libraries/SmartStore.Services/Media/Storage/MediaStorageExtensions.cs b/src/Libraries/SmartStore.Services/Media/Storage/MediaStorageExtensions.cs index 608221b589..eaca77dd54 100644 --- a/src/Libraries/SmartStore.Services/Media/Storage/MediaStorageExtensions.cs +++ b/src/Libraries/SmartStore.Services/Media/Storage/MediaStorageExtensions.cs @@ -20,7 +20,7 @@ public static MediaItem ToMedia(this Picture picture) var media = new MediaItem { Entity = picture, - Path = "", + Path = "Storage", MimeType = picture.MimeType }; @@ -83,13 +83,13 @@ public static string GetFileName(this MediaItem media) var baseEntity = media.Entity as BaseEntity; - var fileName = string.Format("{0}-0{1}", - baseEntity.Id.ToString("0000000"), - extension.EmptyNull().EnsureStartsWith(".") - ); + var fileName = string.Concat( + baseEntity.Id.ToString(ImageCache.IdFormatString), + extension.EmptyNull().EnsureStartsWith(".")); return fileName; } + return null; } } diff --git a/src/Libraries/SmartStore.Services/Messages/CampaignService.cs b/src/Libraries/SmartStore.Services/Messages/CampaignService.cs index 0db4388dc7..ec574a44dc 100644 --- a/src/Libraries/SmartStore.Services/Messages/CampaignService.cs +++ b/src/Libraries/SmartStore.Services/Messages/CampaignService.cs @@ -3,95 +3,67 @@ using System.Linq; using SmartStore.Core.Data; using SmartStore.Core.Domain.Messages; -using SmartStore.Core.Events; using SmartStore.Services.Customers; -using SmartStore.Core; using SmartStore.Core.Email; using SmartStore.Services.Stores; +using SmartStore.Templating; +using SmartStore.Core.Localization; namespace SmartStore.Services.Messages { public partial class CampaignService : ICampaignService { - private readonly IRepository _campaignRepository; - private readonly IEmailSender _emailSender; - private readonly IMessageTokenProvider _messageTokenProvider; - private readonly ITokenizer _tokenizer; + private readonly ICommonServices _services; + private readonly IRepository _campaignRepository; + private readonly IMessageTemplateService _messageTemplateService; + private readonly IEmailSender _emailSender; private readonly IQueuedEmailService _queuedEmailService; private readonly ICustomerService _customerService; - private readonly IStoreContext _storeContext; private readonly IStoreMappingService _storeMappingService; - private readonly IEventPublisher _eventPublisher; - public CampaignService(IRepository campaignRepository, - IEmailSender emailSender, IMessageTokenProvider messageTokenProvider, - ITokenizer tokenizer, IQueuedEmailService queuedEmailService, + public CampaignService( + ICommonServices services, + IRepository campaignRepository, + IMessageTemplateService messageTemplateService, + IEmailSender emailSender, + IQueuedEmailService queuedEmailService, ICustomerService customerService, - IStoreContext storeContext, - IStoreMappingService storeMappingService, - IEventPublisher eventPublisher) + IStoreMappingService storeMappingService) { - this._campaignRepository = campaignRepository; - this._emailSender = emailSender; - this._messageTokenProvider = messageTokenProvider; - this._tokenizer = tokenizer; - this._queuedEmailService = queuedEmailService; - this._customerService = customerService; - this._storeContext = storeContext; - this._storeMappingService = storeMappingService; - this._eventPublisher = eventPublisher; + _services = services; + _campaignRepository = campaignRepository; + _messageTemplateService = messageTemplateService; + _emailSender = emailSender; + _queuedEmailService = queuedEmailService; + _customerService = customerService; + _storeMappingService = storeMappingService; + + T = NullLocalizer.Instance; } - /// - /// Inserts a campaign - /// - /// Campaign + public Localizer T { get; set; } + public virtual void InsertCampaign(Campaign campaign) { - if (campaign == null) - throw new ArgumentNullException("campaign"); + Guard.NotNull(campaign, nameof(campaign)); _campaignRepository.Insert(campaign); - - //event notification - _eventPublisher.EntityInserted(campaign); } - /// - /// Updates a campaign - /// - /// Campaign public virtual void UpdateCampaign(Campaign campaign) { - if (campaign == null) - throw new ArgumentNullException("campaign"); - - _campaignRepository.Update(campaign); + Guard.NotNull(campaign, nameof(campaign)); - //event notification - _eventPublisher.EntityUpdated(campaign); + _campaignRepository.Update(campaign); } - /// - /// Deleted a queued email - /// - /// Campaign public virtual void DeleteCampaign(Campaign campaign) { - if (campaign == null) - throw new ArgumentNullException("campaign"); + Guard.NotNull(campaign, nameof(campaign)); - _campaignRepository.Delete(campaign); - - //event notification - _eventPublisher.EntityDeleted(campaign); + _campaignRepository.Delete(campaign); } - /// - /// Gets a campaign by identifier - /// - /// Campaign identifier - /// Campaign public virtual Campaign GetCampaignById(int campaignId) { if (campaignId == 0) @@ -102,10 +74,6 @@ public virtual Campaign GetCampaignById(int campaignId) } - /// - /// Gets all campaigns - /// - /// Campaign collection public virtual IList GetAllCampaigns() { var query = from c in _campaignRepository.Table @@ -116,26 +84,15 @@ orderby c.CreatedOnUtc return campaigns; } - - /// - /// Sends a campaign to specified emails - /// - /// Campaign - /// Email account - /// Subscriptions - /// Total emails sent - public virtual int SendCampaign(Campaign campaign, EmailAccount emailAccount, IEnumerable subscriptions) - { - if (campaign == null) - throw new ArgumentNullException("campaign"); - if (emailAccount == null) - throw new ArgumentNullException("emailAccount"); + public virtual int SendCampaign(Campaign campaign, IEnumerable subscriptions) + { + Guard.NotNull(campaign, nameof(campaign)); if (subscriptions == null || subscriptions.Count() <= 0) return 0; - int totalEmailsSent = 0; + int totalEmailsQueued = 0; var subscriptionData = subscriptions .Where(x => _storeMappingService.Authorize(campaign, x.StoreId)) @@ -143,70 +100,49 @@ public virtual int SendCampaign(Campaign campaign, EmailAccount emailAccount, IE foreach (var group in subscriptionData) { - var subscription = group.First(); // only one email per email address - - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, _storeContext.CurrentStore); - _messageTokenProvider.AddNewsLetterSubscriptionTokens(tokens, subscription); - - var customer = _customerService.GetCustomerByEmail(subscription.Email); - if (customer != null) - _messageTokenProvider.AddCustomerTokens(tokens, customer); - - string subject = _tokenizer.Replace(campaign.Subject, tokens, false); - string body = _tokenizer.Replace(campaign.Body, tokens, true); - - var email = new QueuedEmail() - { - Priority = 3, - From = emailAccount.Email, - FromName = emailAccount.DisplayName, - To = subscription.Email, - Subject = subject, - Body = body, - CreatedOnUtc = DateTime.UtcNow, - EmailAccountId = emailAccount.Id - }; - - _queuedEmailService.InsertQueuedEmail(email); - totalEmailsSent++; - } - return totalEmailsSent; + var subscription = group.First(); // only one email per email address + var customer = _customerService.GetCustomerByEmail(subscription.Email); + var messageContext = new MessageContext + { + MessageTemplate = GetCampaignTemplate(), + Customer = customer + }; + + var msg = _services.MessageFactory.CreateMessage(messageContext, true, subscription, campaign); + + if (msg.Email?.Id != null) + { + totalEmailsQueued++; + } + } + + return totalEmailsQueued; } - /// - /// Sends a campaign to specified email - /// - /// Campaign - /// Email account - /// Email - public virtual void SendCampaign(Campaign campaign, EmailAccount emailAccount, string email) - { - if (campaign == null) - throw new ArgumentNullException("campaign"); + public virtual CreateMessageResult Preview(Campaign campaign) + { + Guard.NotNull(campaign, nameof(campaign)); - if (emailAccount == null) - throw new ArgumentNullException("emailAccount"); + var messageContext = new MessageContext + { + MessageTemplate = GetCampaignTemplate(), + TestMode = true + }; - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, _storeContext.CurrentStore); - _messageTokenProvider.AddNewsLetterSubscriptionTokens(tokens, new NewsLetterSubscription() { - Email = email - }); + var subscription =_services.MessageFactory.GetTestModels(messageContext).OfType().FirstOrDefault(); - var customer = _customerService.GetCustomerByEmail(email); - if (customer != null) - _messageTokenProvider.AddCustomerTokens(tokens, customer); - - string subject = _tokenizer.Replace(campaign.Subject, tokens, false); - string body = _tokenizer.Replace(campaign.Body, tokens, true); + var result = _services.MessageFactory.CreateMessage(messageContext, false /* do NOT queue */, subscription, campaign); - var to = new EmailAddress(email); - var from = new EmailAddress(emailAccount.Email, emailAccount.DisplayName); + return result; + } - var msg = new EmailMessage(to, subject, body, from); + private MessageTemplate GetCampaignTemplate() + { + var messageTemplate = _messageTemplateService.GetMessageTemplateByName(MessageTemplateNames.SystemCampaign, _services.StoreContext.CurrentStore.Id); + if (messageTemplate == null) + throw new SmartException(T("Common.Error.NoMessageTemplate", MessageTemplateNames.SystemCampaign)); - _emailSender.SendEmail(new SmtpContext(emailAccount), msg); - } - } + return messageTemplate; + } + } } diff --git a/src/Libraries/SmartStore.Services/Messages/CreateAttachmentsConsumer.cs b/src/Libraries/SmartStore.Services/Messages/CreateAttachmentsConsumer.cs new file mode 100644 index 0000000000..ac9ffb04ab --- /dev/null +++ b/src/Libraries/SmartStore.Services/Messages/CreateAttachmentsConsumer.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Mvc; +using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.Domain.Messages; +using SmartStore.Core.Events; +using SmartStore.Core.Localization; +using SmartStore.Core.Logging; +using SmartStore.Utilities; + +namespace SmartStore.Services.Messages +{ + public class CreateAttachmentsConsumer : IConsumer + { + private readonly PdfSettings _pdfSettings; + private readonly HttpRequestBase _httpRequest; + private readonly Lazy _fileDownloadManager; + + public CreateAttachmentsConsumer( + PdfSettings pdfSettings, + HttpRequestBase httpRequest, + Lazy fileDownloadManager) + { + this._pdfSettings = pdfSettings; + this._httpRequest = httpRequest; + this._fileDownloadManager = fileDownloadManager; + + Logger = NullLogger.Instance; + T = NullLocalizer.Instance; + } + + public ILogger Logger { get; set; } + public Localizer T { get; set; } + + public void HandleEvent(MessageQueuingEvent message) + { + var qe = message.QueuedEmail; + var ctx = message.MessageContext; + var model = message.MessageModel; + + var handledTemplates = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "OrderPlaced.CustomerNotification", _pdfSettings.AttachOrderPdfToOrderPlacedEmail }, + { "OrderCompleted.CustomerNotification", _pdfSettings.AttachOrderPdfToOrderCompletedEmail } + }; + + if (handledTemplates.TryGetValue(ctx.MessageTemplate.Name, out var shouldHandle) && shouldHandle) + { + if (model.Get("Order") is IDictionary order && order.Get("ID") is int orderId) + { + try + { + var qea = CreatePdfInvoiceAttachment(orderId); + qe.Attachments.Add(qea); + } + catch (Exception ex) + { + Logger.Error(ex, T("Admin.System.QueuedEmails.ErrorCreatingAttachment")); + } + } + } + } + + private QueuedEmailAttachment CreatePdfInvoiceAttachment(int orderId) + { + var urlHelper = new UrlHelper(_httpRequest.RequestContext); + var path = urlHelper.Action("Print", "Order", new { id = orderId, pdf = true, area = "" }); + + var fileResponse = _fileDownloadManager.Value.DownloadFile(path, true, 5000); + + if (fileResponse == null) + { + throw new InvalidOperationException(T("Admin.System.QueuedEmails.ErrorEmptyAttachmentResult", path)); + } + + if (!fileResponse.ContentType.IsCaseInsensitiveEqual("application/pdf")) + { + throw new InvalidOperationException(T("Admin.System.QueuedEmails.ErrorNoPdfAttachment")); + } + + return new QueuedEmailAttachment + { + StorageLocation = EmailAttachmentStorageLocation.Blob, + MediaStorage = new MediaStorage { Data = fileResponse.Data }, + MimeType = fileResponse.ContentType, + Name = fileResponse.FileName + }; + } + + } +} diff --git a/src/Libraries/SmartStore.Services/Messages/EmailAccountService.cs b/src/Libraries/SmartStore.Services/Messages/EmailAccountService.cs index 0c1c39b0b3..8fafb3b117 100644 --- a/src/Libraries/SmartStore.Services/Messages/EmailAccountService.cs +++ b/src/Libraries/SmartStore.Services/Messages/EmailAccountService.cs @@ -52,8 +52,6 @@ public virtual void InsertEmailAccount(EmailAccount emailAccount) _emailAccountRepository.Insert(emailAccount); _defaultEmailAccount = null; - - _eventPublisher.EntityInserted(emailAccount); } public virtual void UpdateEmailAccount(EmailAccount emailAccount) @@ -82,8 +80,6 @@ public virtual void UpdateEmailAccount(EmailAccount emailAccount) _emailAccountRepository.Update(emailAccount); _defaultEmailAccount = null; - - _eventPublisher.EntityUpdated(emailAccount); } public virtual void DeleteEmailAccount(EmailAccount emailAccount) @@ -97,8 +93,6 @@ public virtual void DeleteEmailAccount(EmailAccount emailAccount) _emailAccountRepository.Delete(emailAccount); _defaultEmailAccount = null; - - _eventPublisher.EntityDeleted(emailAccount); } public virtual EmailAccount GetEmailAccountById(int emailAccountId) diff --git a/src/Libraries/SmartStore.Services/Messages/EventPublisherExtensions.cs b/src/Libraries/SmartStore.Services/Messages/EventPublisherExtensions.cs deleted file mode 100644 index 681cb588b6..0000000000 --- a/src/Libraries/SmartStore.Services/Messages/EventPublisherExtensions.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; -using SmartStore.Core; -using SmartStore.Core.Domain.Messages; -using SmartStore.Core.Events; - -namespace SmartStore.Services.Messages -{ - public static class EventPublisherExtensions - { - /// - /// Publishes the newsletter subscribe event. - /// - /// The event publisher. - /// The email. - public static void PublishNewsletterSubscribe(this IEventPublisher eventPublisher, string email) - { - eventPublisher.Publish(new EmailSubscribedEvent(email)); - } - - /// - /// Publishes the newsletter unsubscribe event. - /// - /// The event publisher. - /// The email. - public static void PublishNewsletterUnsubscribe(this IEventPublisher eventPublisher, string email) - { - eventPublisher.Publish(new EmailUnsubscribedEvent(email)); - } - - public static void EntityTokensAdded(this IEventPublisher eventPublisher, T entity, IList tokens) where T : BaseEntity - { - eventPublisher.Publish(new EntityTokensAddedEvent(entity, tokens)); - } - - public static void MessageTokensAdded(this IEventPublisher eventPublisher, MessageTemplate message, IList tokens) - { - eventPublisher.Publish(new MessageTokensAddedEvent(message, tokens)); - } - } -} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Messages/Events.cs b/src/Libraries/SmartStore.Services/Messages/Events.cs new file mode 100644 index 0000000000..02bbf52c66 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Messages/Events.cs @@ -0,0 +1,87 @@ +using System; +using SmartStore.Core.Domain.Messages; + +namespace SmartStore.Services.Messages +{ + /// + /// Published after the creation of a single message model part has been completed. + /// + /// Type of source entity + public class MessageModelPartCreatedEvent where T : class + { + public MessageModelPartCreatedEvent(T source, dynamic part) + { + Source = source; + Part = part; + } + + /// + /// The source object for which the model part has been created, e.g. a Product entity. + /// + public T Source { get; private set; } + + /// + /// The resulting model part. + /// + public dynamic Part { get; private set; } + } + + /// + /// Published when a system mapper is missing for a particular model type (e.g. a custom entity in a plugin). + /// Implementors should subscribe to this event in order to provide a corresponding dynamic model part. + /// The result model should be assigned to the property. If this property + /// is still null, the source is used as model part instead. + /// + public class MessageModelPartMappingEvent + { + public MessageModelPartMappingEvent(object source) + { + Source = source; + } + + /// + /// The source object for which a model part should be created. + /// + public object Source { get; private set; } + + /// + /// The resulting model part. + /// + public dynamic Result { get; set; } + + /// + /// The name of the model part. If null the source's type name is used. + /// + public string ModelPartName { get; set; } + } + + /// + /// Published after the message model has been completely created. + /// + public class MessageModelCreatedEvent + { + public MessageModelCreatedEvent(MessageContext messageContext, TemplateModel model) + { + MessageContext = messageContext; + Model = model; + } + + public MessageContext MessageContext{ get; private set; } + + /// + /// The result message model. + /// + public TemplateModel Model { get; private set; } + } + + /// + /// An event message which gets published just before a new instance + /// of is persisted to the database + /// + public class MessageQueuingEvent + { + public QueuedEmail QueuedEmail { get; set; } + public MessageContext MessageContext { get; set; } + public TemplateModel MessageModel { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Services/Messages/GenericMessageContext.cs b/src/Libraries/SmartStore.Services/Messages/GenericMessageContext.cs deleted file mode 100644 index 29018db637..0000000000 --- a/src/Libraries/SmartStore.Services/Messages/GenericMessageContext.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using SmartStore.Core.Domain.Customers; - -namespace SmartStore.Services.Messages -{ - public class GenericMessageContext - { - public GenericMessageContext() - { - this.Tokens = new List(); - } - public int? LanguageId { get; set; } - public int? StoreId { get; set; } - public IList Tokens { get; internal set; } - public IMessageTokenProvider MessagenTokenProvider { get; internal set; } - public Customer Customer { get; set; } - public string ToEmail { get; set; } - public string ToName { get; set; } - - /// - /// Gets or sets a value specifying whether customer's email should be used as reply address - /// - /// Value is ignored, if Customer property is null - public bool ReplyToCustomer { get; set; } - - /// - /// Gets or sets the reply email address - /// - /// Value is ignored, if ReplyToCustomer is true AND Customer property is not null - public string ReplyToEmail { get; set; } - - /// - /// Gets or sets the reply to name - /// - /// Value is ignored, if ReplyToCustomer is true AND Customer property is not null - public string ReplyToName { get; set; } - } -} diff --git a/src/Libraries/SmartStore.Services/Messages/ICampaignService.cs b/src/Libraries/SmartStore.Services/Messages/ICampaignService.cs index 3bd2ab3e2a..0954ec1eb7 100644 --- a/src/Libraries/SmartStore.Services/Messages/ICampaignService.cs +++ b/src/Libraries/SmartStore.Services/Messages/ICampaignService.cs @@ -40,17 +40,15 @@ public partial interface ICampaignService /// Sends a campaign to specified emails /// /// Campaign - /// Email account /// Subscriptions /// Total emails sent - int SendCampaign(Campaign campaign, EmailAccount emailAccount, IEnumerable subscriptions); + int SendCampaign(Campaign campaign, IEnumerable subscriptions); - /// - /// Sends a campaign to specified email - /// - /// Campaign - /// Email account - /// Email - void SendCampaign(Campaign campaign, EmailAccount emailAccount, string email); - } + /// + /// Creates a campaign email without sending it for previewing and testing purposes. + /// + /// The campaign to preview + /// The preview result + CreateMessageResult Preview(Campaign campaign); + } } diff --git a/src/Libraries/SmartStore.Services/Messages/IMessageFactory.cs b/src/Libraries/SmartStore.Services/Messages/IMessageFactory.cs new file mode 100644 index 0000000000..0f8906b886 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Messages/IMessageFactory.cs @@ -0,0 +1,109 @@ +using System; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Messages; +using SmartStore.Core.Email; +using SmartStore.Templating; + +namespace SmartStore.Services.Messages +{ + /// + /// Contains the result data of a call + /// + public class CreateMessageResult + { + /// + /// The queued email instance which can be saved to the database + /// + public QueuedEmail Email { get; set; } + + /// + /// The final model which contains all global and template specific model parts. + /// + public TemplateModel Model { get; set; } + + /// + /// The message context used to create the message. + /// + public MessageContext MessageContext { get; set; } + } + + /// + /// Creates and optionally queues email messages + /// + public interface IMessageFactory + { + /// + /// Creates an email message + /// + /// Contains all data required for creating a message + /// If true, the created email message will automatically be queued for sending (saved in database as a ) + /// + /// All model objects that are necessary to render the template (in no particular order). + /// The passed object instances will be converted to special types which the underlying can handle. + /// is responsible for the conversion. See also . + /// + /// Contains the message creation result. + CreateMessageResult CreateMessage(MessageContext messageContext, bool queue, params object[] modelParts); + + /// + /// Queues a message created by + /// + /// The message context used to create the message. + /// The instance of to queue, e.g. obtained from + void QueueMessage(MessageContext messageContext, QueuedEmail queuedEmail); + + /// + /// Gets an array of suitable test model parts during preview mode. The message template defines + /// which model part types are required (a comma-separated type list in ). + /// The framework tries to load a random entity for each defined type from the database. If the table does not contain any records, + /// gets called internally to obtain a test model wrapper with sample data. + /// + /// The message context used to create the message. + /// An array of model parts which can be passed to + object[] GetTestModels(MessageContext messageContext); + } + + public static class IMessageFactoryExtensions + { + /// + /// Sends the "ContactUs" message to the store owner + /// + public static CreateMessageResult SendContactUsMessage(this IMessageFactory factory, Customer customer, + string senderEmail, string senderName, string subject, string message, EmailAddress senderEmailAddress, int languageId = 0) + { + var model = new NamedModelPart("Message") + { + ["Subject"] = subject.NullEmpty(), + ["Message"] = message.NullEmpty(), + ["SenderEmail"] = senderEmail.NullEmpty(), + ["SenderName"] = senderName.NullEmpty() + }; + + var messageContext = MessageContext.Create(MessageTemplateNames.SystemContactUs, languageId, customer: customer); + if (senderEmailAddress != null) + { + messageContext.SenderEmailAddress = senderEmailAddress; + } + + return factory.CreateMessage(messageContext, true, model); + } + + /// + /// Sends a newsletter subscription activation message + /// + public static CreateMessageResult SendNewsLetterSubscriptionActivationMessage(this IMessageFactory factory, NewsLetterSubscription subscription, int languageId = 0) + { + Guard.NotNull(subscription, nameof(subscription)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewsLetterSubscriptionActivation, languageId), true, subscription); + } + + /// + /// Sends a newsletter subscription deactivation message + /// + public static CreateMessageResult SendNewsLetterSubscriptionDeactivationMessage(this IMessageFactory factory, NewsLetterSubscription subscription, int languageId = 0) + { + Guard.NotNull(subscription, nameof(subscription)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewsLetterSubscriptionDeactivation, languageId), true, subscription); + } + } +} diff --git a/src/Libraries/SmartStore.Services/Messages/IMessageModelProvider.cs b/src/Libraries/SmartStore.Services/Messages/IMessageModelProvider.cs new file mode 100644 index 0000000000..e584fa5863 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Messages/IMessageModelProvider.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using SmartStore.Collections; +using SmartStore.Core.Domain.Messages; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Shipping; +using SmartStore.Core.Domain.Blogs; +using SmartStore.Core.Domain.News; +using SmartStore.Core.Domain.Forums; +using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Domain.Common; + +namespace SmartStore.Services.Messages +{ + /// + /// Responsible for building the message template model + /// + public interface IMessageModelProvider + { + /// + /// Creates and adds all global model parts to the template model (): + /// + /// + /// Context (contains meta infos like template name, language etc.) + /// Customer (obtained from property) + /// Store (obtained from property) + /// Email (the ) + /// Theme (some theming variables, mostly colors) + /// + /// + /// + /// Contains all data required for building a model part and creating a message + void AddGlobalModelParts(MessageContext messageContext); + + /// + /// Adds a template specific model part to the template model. + /// The passed object instance () will be converted to a special type which the underlying can handle. + /// + /// Supported types are: , , , , , + /// , , , , , + /// , , , , , + /// , + /// + /// + /// Furthermore, any object implementing or can also be passed as model part. + /// The first merges all entries within the passed object with the special Bag entry, the latter creates a whole + /// new entry using the name provided by its property. + /// + /// + /// If an unsupported object is passed, the framework will publish the event, giving + /// a subscriber the chance to provide a converted model object and a part name. + /// + /// + /// The model part instance to convert and add to the final model. + /// Contains all data required for building a model part and creating a message + /// + /// The name to use for the model part in the final model. If null, the framework tries to infer the name. + /// See also + /// + void AddModelPart(object part, MessageContext messageContext, string name = null); + + /// + /// Tries to infer the model part name by type: + /// + /// When is a plain object: type name + /// When is : ModelPartName property + /// + /// + /// The model part instance to resolve a name for. + /// The inferred name or null + string ResolveModelName(object model); + + /// + /// Build a model metadata tree for a final template model. Model trees are used + /// on the client to provide autocomplete information. + /// + /// The final template model to build a model tree for. + /// A hierarchy of instances. + TreeNode BuildModelTree(TemplateModel model); + + /// + /// Gets the last known model metadata tree for a particular template. + /// See also + /// + /// Name of the template to get metadata for. + /// A hierarchy of instances. + TreeNode GetLastModelTree(string messageTemplateName); + + /// + /// Gets the last known model metadata tree for a particular template. + /// See also + /// + /// The template to get metadata for. + /// A hierarchy of instances. + TreeNode GetLastModelTree(MessageTemplate template); + } +} diff --git a/src/Libraries/SmartStore.Services/Messages/IMessageTokenProvider.cs b/src/Libraries/SmartStore.Services/Messages/IMessageTokenProvider.cs deleted file mode 100644 index d1f336646d..0000000000 --- a/src/Libraries/SmartStore.Services/Messages/IMessageTokenProvider.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Collections.Generic; -using SmartStore.Core.Domain.Blogs; -using SmartStore.Core.Domain.Catalog; -using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Forums; -using SmartStore.Core.Domain.Localization; -using SmartStore.Core.Domain.Messages; -using SmartStore.Core.Domain.News; -using SmartStore.Core.Domain.Orders; -using SmartStore.Core.Domain.Shipping; -using SmartStore.Core.Domain.Stores; -using SmartStore.Collections; - -namespace SmartStore.Services.Messages -{ - public partial interface IMessageTokenProvider - { - void AddStoreTokens(IList tokens, Store store); - - void AddOrderTokens(IList tokens, Order order, Language language); - - void AddShipmentTokens(IList tokens, Shipment shipment, Language language); - - void AddOrderNoteTokens(IList tokens, OrderNote orderNote); - - void AddRecurringPaymentTokens(IList tokens, RecurringPayment recurringPayment); - - void AddReturnRequestTokens(IList tokens, ReturnRequest returnRequest, OrderItem orderItem); - - void AddGiftCardTokens(IList tokens, GiftCard giftCard); - - void AddCustomerTokens(IList tokens, Customer customer); - - void AddNewsLetterSubscriptionTokens(IList tokens, NewsLetterSubscription subscription); - - void AddProductReviewTokens(IList tokens, ProductReview productReview); - - void AddBlogCommentTokens(IList tokens, BlogComment blogComment); - - void AddNewsCommentTokens(IList tokens, NewsComment newsComment); - - void AddProductTokens(IList tokens, Product product, Language language); - - void AddForumTokens(IList tokens, Forum forum, Language language); - - void AddForumTopicTokens(IList tokens, ForumTopic forumTopic, - int? friendlyForumTopicPageIndex = null, int? appendedPostIdentifierAnchor = null); - - void AddForumPostTokens(IList tokens, ForumPost forumPost); - - void AddPrivateMessageTokens(IList tokens, PrivateMessage privateMessage); - - void AddBackInStockTokens(IList tokens, BackInStockSubscription subscription); - - string[] GetListOfCampaignAllowedTokens(); - - string[] GetListOfAllowedTokens(); - - TreeNode GetTreeOfCampaignAllowedTokens(); - - TreeNode GetTreeOfAllowedTokens(); - - void AddBankConnectionTokens(IList tokens); - - void AddCompanyTokens(IList tokens); - - void AddContactDataTokens(IList tokens); - } -} diff --git a/src/Libraries/SmartStore.Services/Messages/IModelPart.cs b/src/Libraries/SmartStore.Services/Messages/IModelPart.cs new file mode 100644 index 0000000000..16ed798135 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Messages/IModelPart.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using SmartStore.ComponentModel; + +namespace SmartStore.Services.Messages +{ + /// + /// Used to transfer miscellaneous data to the template engine + /// which is merged with the generic "Bag" part. + /// + public interface IModelPart : IDictionary + { + } + + public interface INamedModelPart : IDictionary + { + string ModelPartName { get; } + } + + + #region Impl + + public class ModelPart : HybridExpando, IModelPart + { + } + + public class NamedModelPart : HybridExpando, INamedModelPart + { + public NamedModelPart(string modelPartName) + : base(true) + { + Guard.NotEmpty(modelPartName, nameof(modelPartName)); + ModelPartName = modelPartName; + } + + public string ModelPartName + { + get; + private set; + } + } + + #endregion +} diff --git a/src/Libraries/SmartStore.Services/Messages/ITokenizer.cs b/src/Libraries/SmartStore.Services/Messages/ITokenizer.cs deleted file mode 100644 index 81fc75982b..0000000000 --- a/src/Libraries/SmartStore.Services/Messages/ITokenizer.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; - -namespace SmartStore.Services.Messages -{ - public partial interface ITokenizer - { - /// - /// Replace all of the token key occurences inside the specified template text with corresponded token values - /// - /// The template with token keys inside - /// The sequence of tokens to use - /// The value indicating whether tokens should be HTML encoded - /// Text with all token keys replaces by token value - string Replace(string template, IEnumerable tokens, bool htmlEncode); - } -} diff --git a/src/Libraries/SmartStore.Services/Messages/IWorkflowMessageService.cs b/src/Libraries/SmartStore.Services/Messages/IWorkflowMessageService.cs deleted file mode 100644 index 5dbb25c83a..0000000000 --- a/src/Libraries/SmartStore.Services/Messages/IWorkflowMessageService.cs +++ /dev/null @@ -1,308 +0,0 @@ -using System; -using SmartStore.Core.Domain.Blogs; -using SmartStore.Core.Domain.Catalog; -using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Forums; -using SmartStore.Core.Domain.Messages; -using SmartStore.Core.Domain.News; -using SmartStore.Core.Domain.Orders; -using SmartStore.Core.Domain.Shipping; - -namespace SmartStore.Services.Messages -{ - public partial interface IWorkflowMessageService - { - #region Customer workflow - - /// - /// Sends 'New customer' notification message to a store owner - /// - /// Customer instance - /// Message language identifier - /// Queued email identifier - int SendCustomerRegisteredNotificationMessage(Customer customer, int languageId); - - /// - /// Sends a welcome message to a customer - /// - /// Customer instance - /// Message language identifier - /// Queued email identifier - int SendCustomerWelcomeMessage(Customer customer, int languageId); - - /// - /// Sends an email validation message to a customer - /// - /// Customer instance - /// Message language identifier - /// Queued email identifier - int SendCustomerEmailValidationMessage(Customer customer, int languageId); - - /// - /// Sends password recovery message to a customer - /// - /// Customer instance - /// Message language identifier - /// Queued email identifier - int SendCustomerPasswordRecoveryMessage(Customer customer, int languageId); - - #endregion - - #region Order workflow - - /// - /// Sends an order placed notification to a store owner - /// - /// Order instance - /// Message language identifier - /// Queued email identifier - int SendOrderPlacedStoreOwnerNotification(Order order, int languageId); - - /// - /// Sends an order placed notification to a customer - /// - /// Order instance - /// Message language identifier - /// Queued email identifier - int SendOrderPlacedCustomerNotification(Order order, int languageId); - - /// - /// Sends a shipment sent notification to a customer - /// - /// Shipment - /// Message language identifier - /// Queued email identifier - int SendShipmentSentCustomerNotification(Shipment shipment, int languageId); - - /// - /// Sends a shipment delivered notification to a customer - /// - /// Shipment - /// Message language identifier - /// Queued email identifier - int SendShipmentDeliveredCustomerNotification(Shipment shipment, int languageId); - - /// - /// Sends an order completed notification to a customer - /// - /// Order instance - /// Message language identifier - /// Queued email identifier - int SendOrderCompletedCustomerNotification(Order order, int languageId); - - /// - /// Sends an order cancelled notification to a customer - /// - /// Order instance - /// Message language identifier - /// Queued email identifier - int SendOrderCancelledCustomerNotification(Order order, int languageId); - - /// - /// Sends a new order note added notification to a customer - /// - /// Order note - /// Message language identifier - /// Queued email identifier - int SendNewOrderNoteAddedCustomerNotification(OrderNote orderNote, int languageId); - - /// - /// Sends a "Recurring payment cancelled" notification to a store owner - /// - /// Recurring payment - /// Message language identifier - /// Queued email identifier - int SendRecurringPaymentCancelledStoreOwnerNotification(RecurringPayment recurringPayment, int languageId); - - #endregion - - #region Newsletter workflow - - /// - /// Sends a newsletter subscription activation message - /// - /// Newsletter subscription - /// Language identifier - /// Queued email identifier - int SendNewsLetterSubscriptionActivationMessage(NewsLetterSubscription subscription, - int languageId); - - /// - /// Sends a newsletter subscription deactivation message - /// - /// Newsletter subscription - /// Language identifier - /// Queued email identifier - int SendNewsLetterSubscriptionDeactivationMessage(NewsLetterSubscription subscription, - int languageId); - - #endregion - - #region Send a message to a friend - - /// - /// Sends "email a friend" message - /// - /// Customer instance - /// Message language identifier - /// Product instance - /// Customer's email - /// Friend's email - /// Personal message - /// Queued email identifier - int SendProductEmailAFriendMessage(Customer customer, int languageId, - Product product, string customerEmail, string friendsEmail, string personalMessage); - - int SendProductQuestionMessage(Customer customer, int languageId, Product product, - string senderEmail, string senderName, string senderPhone, string question); - - /// - /// Sends wishlist "email a friend" message - /// - /// Customer - /// Message language identifier - /// Customer's email - /// Friend's email - /// Personal message - /// Queued email identifier - int SendWishlistEmailAFriendMessage(Customer customer, int languageId, - string customerEmail, string friendsEmail, string personalMessage); - - #endregion - - #region Return requests - - /// - /// Sends 'New Return Request' message to a store owner - /// - /// Return request - /// Order item - /// Message language identifier - /// Queued email identifier - int SendNewReturnRequestStoreOwnerNotification(ReturnRequest returnRequest, OrderItem orderItem, int languageId); - - - /// - /// Sends 'Return Request status changed' message to a customer - /// - /// Return request - /// Order item - /// Message language identifier - /// Queued email identifier - int SendReturnRequestStatusChangedCustomerNotification(ReturnRequest returnRequest, OrderItem orderItem, int languageId); - - #endregion - - #region Forum Notifications - - /// - /// Sends a forum subscription message to a customer - /// - /// Customer instance - /// Forum Topic - /// Forum - /// Message language identifier - /// Queued email identifier - int SendNewForumTopicMessage(Customer customer, - ForumTopic forumTopic, Forum forum, int languageId); - - /// - /// Sends a forum subscription message to a customer - /// - /// Customer instance - /// Forum post - /// Forum Topic - /// Forum - /// Friendly (starts with 1) forum topic page to use for URL generation - /// Message language identifier - /// Queued email identifier - int SendNewForumPostMessage(Customer customer, - ForumPost forumPost, ForumTopic forumTopic, - Forum forum, int friendlyForumTopicPageIndex, - int languageId); - - /// - /// Sends a private message notification - /// - /// Private message - /// Message language identifier - /// Queued email identifier - int SendPrivateMessageNotification(Customer customer, PrivateMessage privateMessage, int languageId); - - #endregion - - #region Misc - - /// - /// Sends a product review notification message to a store owner - /// - /// Product review - /// Message language identifier - /// Queued email identifier - int SendProductReviewNotificationMessage(ProductReview productReview, - int languageId); - - /// - /// Sends a gift card notification - /// - /// Gift card - /// Message language identifier - /// Queued email identifier - int SendGiftCardNotification(GiftCard giftCard, int languageId); - - - /// - /// Sends a "quantity below" notification to a store owner - /// - /// Product - /// Message language identifier - /// Queued email identifier - int SendQuantityBelowStoreOwnerNotification(Product product, int languageId); - - - /// - /// Sends a "new VAT sumitted" notification to a store owner - /// - /// Customer - /// Received VAT name - /// Received VAT address - /// Message language identifier - /// Queued email identifier - int SendNewVatSubmittedStoreOwnerNotification(Customer customer, - string vatName, string vatAddress, int languageId); - - /// - /// Sends a blog comment notification message to a store owner - /// - /// Blog comment - /// Message language identifier - /// Queued email identifier - int SendBlogCommentNotificationMessage(BlogComment blogComment, int languageId); - - /// - /// Sends a news comment notification message to a store owner - /// - /// News comment - /// Message language identifier - /// Queued email identifier - int SendNewsCommentNotificationMessage(NewsComment newsComment, int languageId); - - /// - /// Sends a 'Back in stock' notification message to a customer - /// - /// Subscription - /// Message language identifier - /// Queued email identifier - int SendBackInStockNotification(BackInStockSubscription subscription, int languageId); - - /// - /// Sends a generic message - /// - /// The name of the message template - /// Configurator action for the message - /// Queued email identifier - int SendGenericMessage(string messageTemplateName, Action cfg); - - #endregion - } -} diff --git a/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs b/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs index 979051d1d6..3b81f017f6 100644 --- a/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs +++ b/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs @@ -41,7 +41,7 @@ public void Execute(ImportExecuteContext context) var utcNow = DateTime.UtcNow; var currentStoreId = _services.StoreContext.CurrentStore.Id; - using (var scope = new DbContextScope(ctx: _services.DbContext, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) + using (var scope = new DbContextScope(ctx: _services.DbContext, hooksEnabled: false, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) { var segmenter = context.DataSegmenter; diff --git a/src/Libraries/SmartStore.Services/Messages/MessageContext.cs b/src/Libraries/SmartStore.Services/Messages/MessageContext.cs new file mode 100644 index 0000000000..9d3014b713 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Messages/MessageContext.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Messages; +using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Email; +using SmartStore.Core.Localization; + +namespace SmartStore.Services.Messages +{ + /// + /// A context object which contains all required and optional information + /// for the creation of message templates. + /// + public class MessageContext + { + private IFormatProvider _formatProvider; + + /// + /// The source message template. Required if is empty. + /// + public MessageTemplate MessageTemplate { get; set; } + + /// + /// The source message template name. Required if is null. + /// + public string MessageTemplateName { get; set; } + + /// + /// If null, the email account specifies the sender. + /// + public EmailAddress SenderEmailAddress { get; set; } + + /// + /// If null, obtained from WorkContext.CurrentCustomer. + /// + public Customer Customer { get; set; } + + /// + /// If null, obtained from WorkContext.WorkingLanguage. + /// + public int? LanguageId { get; set; } + + /// + /// If null, obtained from StoreContext.CurrentStore. + /// + public int? StoreId { get; set; } + + internal Language Language { get; set; } + internal Store Store { get; set; } + public EmailAccount EmailAccount { get; internal set; } + + public bool TestMode { get; set; } + + /// + /// If null, obtained from . + /// + public Uri BaseUri { get; set; } + + /// + /// The final template model containing all global and template specific model parts. + /// + public TemplateModel Model { get; set; } + + /// + /// If null, inferred from . + /// + public IFormatProvider FormatProvider + { + get + { + if (_formatProvider == null) + { + var culture = this.Language?.LanguageCulture; + if (culture != null && LocalizationHelper.IsValidCultureCode(culture)) + { + _formatProvider = CultureInfo.GetCultureInfo(culture); + } + } + + return _formatProvider ?? CultureInfo.CurrentCulture; + } + set + { + _formatProvider = value; + } + } + + private IFormatProvider GetFormatProvider(MessageContext messageContext) + { + var culture = messageContext.Language.LanguageCulture; + + if (LocalizationHelper.IsValidCultureCode(culture)) + { + return CultureInfo.GetCultureInfo(culture); + } + + return CultureInfo.CurrentCulture; + } + + public static MessageContext Create(string messageTemplateName, int languageId, int? storeId = null, Customer customer = null) + { + return new MessageContext + { + MessageTemplateName = messageTemplateName, + LanguageId = languageId, + StoreId = storeId, + Customer = customer + }; + } + } +} diff --git a/src/Libraries/SmartStore.Services/Messages/MessageFactory.cs b/src/Libraries/SmartStore.Services/Messages/MessageFactory.cs new file mode 100644 index 0000000000..d04fb30d29 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Messages/MessageFactory.cs @@ -0,0 +1,644 @@ +using System; +using System.Collections.Generic; +using System.Data.Entity; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using Newtonsoft.Json; +using SmartStore.ComponentModel; +using SmartStore.Core; +using SmartStore.Core.Domain.Blogs; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Forums; +using SmartStore.Core.Domain.Messages; +using SmartStore.Core.Domain.News; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Shipping; +using SmartStore.Core.Email; +using SmartStore.Core.Localization; +using SmartStore.Core.Logging; +using SmartStore.Services.Localization; +using SmartStore.Services.Media; +using SmartStore.Templating; + +namespace SmartStore.Services.Messages +{ + public partial class MessageFactory : IMessageFactory + { + const string LoremIpsum = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua."; + + private readonly ICommonServices _services; + private readonly ITemplateEngine _templateEngine; + private readonly ITemplateManager _templateManager; + private readonly IMessageModelProvider _modelProvider; + private readonly IMessageTemplateService _messageTemplateService; + private readonly IQueuedEmailService _queuedEmailService; + private readonly ILanguageService _languageService; + private readonly IEmailAccountService _emailAccountService; + private readonly EmailAccountSettings _emailAccountSettings; + private readonly IDownloadService _downloadService; + + public MessageFactory( + ICommonServices services, + ITemplateEngine templateEngine, + ITemplateManager templateManager, + IMessageModelProvider modelProvider, + IMessageTemplateService messageTemplateService, + IQueuedEmailService queuedEmailService, + ILanguageService languageService, + IEmailAccountService emailAccountService, + EmailAccountSettings emailAccountSettings, + IDownloadService downloadService) + { + _services = services; + _templateEngine = templateEngine; + _templateManager = templateManager; + _modelProvider = modelProvider; + _messageTemplateService = messageTemplateService; + _queuedEmailService = queuedEmailService; + _languageService = languageService; + _emailAccountService = emailAccountService; + _emailAccountSettings = emailAccountSettings; + _downloadService = downloadService; + + T = NullLocalizer.Instance; + Logger = NullLogger.Instance; + } + + public Localizer T { get; set; } + public ILogger Logger { get; set; } + + public virtual CreateMessageResult CreateMessage(MessageContext messageContext, bool queue, params object[] modelParts) + { + Guard.NotNull(messageContext, nameof(messageContext)); + + modelParts = modelParts ?? new object[0]; + + // Handle TestMode + if (messageContext.TestMode && modelParts.Length == 0) + { + modelParts = GetTestModels(messageContext); + } + + ValidateMessageContext(messageContext, ref modelParts); + + // Create and assign model + var model = messageContext.Model = new TemplateModel(); + + // Add all global template model parts + _modelProvider.AddGlobalModelParts(messageContext); + + // Add specific template models for passed parts + foreach (var part in modelParts) + { + if (model != null) + { + _modelProvider.AddModelPart(part, messageContext); + } + } + + // Give implementors the chance to customize the final template model + _services.EventPublisher.Publish(new MessageModelCreatedEvent(messageContext, model)); + + var messageTemplate = messageContext.MessageTemplate; + var languageId = messageContext.Language.Id; + + // Render templates + var to = RenderEmailAddress(messageTemplate.To, messageContext); + var replyTo = RenderEmailAddress(messageTemplate.ReplyTo, messageContext, false); + var bcc = RenderTemplate(messageTemplate.GetLocalized((x) => x.BccEmailAddresses, languageId), messageContext, false); + + var subject = RenderTemplate(messageTemplate.GetLocalized((x) => x.Subject, languageId), messageContext); + ((dynamic)model).Email.Subject = subject; + + var body = RenderBodyTemplate(messageContext); + + // CSS inliner + body = InlineCss(body, model); + + // Model tree + var modelTree = _modelProvider.BuildModelTree(model); + var modelTreeJson = JsonConvert.SerializeObject(modelTree, Formatting.None); + if (modelTreeJson != messageTemplate.LastModelTree) + { + messageContext.MessageTemplate.LastModelTree = modelTreeJson; + if (!messageTemplate.IsTransientRecord()) + { + _messageTemplateService.UpdateMessageTemplate(messageContext.MessageTemplate); + } + } + + // Create queued email from template + var qe = new QueuedEmail + { + Priority = 5, + From = messageContext.SenderEmailAddress ?? messageContext.EmailAccount.ToEmailAddress(), + To = to.ToString(), + Bcc = bcc, + ReplyTo = replyTo?.ToString(), + Subject = subject, + Body = body, + CreatedOnUtc = DateTime.UtcNow, + EmailAccountId = messageContext.EmailAccount.Id, + SendManually = messageTemplate.SendManually + }; + + // Create and add attachments (if any) + CreateAttachments(qe, messageContext); + + if (queue) + { + // Put to queue + QueueMessage(messageContext, qe); + } + + return new CreateMessageResult { Email = qe, Model = model, MessageContext = messageContext }; + } + + public virtual void QueueMessage(MessageContext messageContext, QueuedEmail queuedEmail) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(queuedEmail, nameof(queuedEmail)); + + // Publish event so that integrators can add attachments, alter the email etc. + _services.EventPublisher.Publish(new MessageQueuingEvent + { + QueuedEmail = queuedEmail, + MessageContext = messageContext, + MessageModel = messageContext.Model + }); + + _queuedEmailService.InsertQueuedEmail(queuedEmail); + } + + private EmailAddress RenderEmailAddress(string email, MessageContext ctx, bool required = true) + { + string parsed = null; + + try + { + parsed = RenderTemplate(email, ctx, required); + + if (required || parsed != null) + { + return parsed.Convert(); + } + else + { + return null; + } + } + catch (Exception ex) + { + if (ctx.TestMode) + { + return new EmailAddress("john@doe.com", "John Doe"); + } + + var ex2 = new SmartException($"Failed to parse email address for variable '{email}'. Value was '{parsed.EmptyNull()}': {ex.Message}", ex); + _services.Notifier.Error(ex2.Message); + throw ex2; + } + } + + private string RenderTemplate(string template, MessageContext ctx, bool required = true) + { + if (!required && template.IsEmpty()) + { + return null; + } + + return _templateEngine.Render(template, ctx.Model, ctx.FormatProvider); + } + + private string RenderBodyTemplate(MessageContext ctx) + { + var key = BuildTemplateKey(ctx); + var source = ctx.MessageTemplate.GetLocalized((x) => x.Body, ctx.Language.Id); + var fromCache = true; + var template = _templateManager.GetOrAdd(key, GetBodyTemplate); + + if (fromCache && template.Source != source) + { + // The template was resolved from template cache, but it has expired + // because the source text has changed. + template = _templateEngine.Compile(source); + _templateManager.Put(key, template); + } + + return template.Render(ctx.Model, ctx.FormatProvider); + + string GetBodyTemplate() + { + fromCache = false; + return source; + } + } + + private string BuildTemplateKey(MessageContext messageContext) + { + var prefix = messageContext.MessageTemplate.IsTransientRecord() ? "TransientTemplate/" : "MessageTemplate/"; + return prefix + messageContext.MessageTemplate.Name + "/" + messageContext.Language.Id + "/Body"; + } + + private string InlineCss(string html, dynamic model) + { + Uri baseUri = null; + + try + { + // 'Store' is a global model part, so we pretty can be sure it exists + baseUri = new Uri((string)model.Store.Url); + } + catch { } + + var pm = new PreMailer.Net.PreMailer(html, baseUri); + var result = pm.MoveCssInline(true, "#ignore"); + return result.Html; + } + + protected virtual void CreateAttachments(QueuedEmail queuedEmail, MessageContext messageContext) + { + var messageTemplate = messageContext.MessageTemplate; + var languageId = messageContext.Language.Id; + + // create attachments if any + var fileIds = (new int?[] + { + messageTemplate.GetLocalized(x => x.Attachment1FileId, languageId), + messageTemplate.GetLocalized(x => x.Attachment2FileId, languageId), + messageTemplate.GetLocalized(x => x.Attachment3FileId, languageId) + }) + .Where(x => x.HasValue) + .Select(x => x.Value) + .ToArray(); + + if (fileIds.Any()) + { + var files = _downloadService.GetDownloadsByIds(fileIds); + foreach (var file in files) + { + queuedEmail.Attachments.Add(new QueuedEmailAttachment + { + StorageLocation = EmailAttachmentStorageLocation.FileReference, + FileId = file.Id, + Name = (file.Filename.NullEmpty() ?? file.Id.ToString()) + file.Extension.EmptyNull(), + MimeType = file.ContentType.NullEmpty() ?? "application/octet-stream" + }); + } + } + } + + + private void ValidateMessageContext(MessageContext ctx, ref object[] modelParts) + { + var t = ctx.MessageTemplate; + if (t != null) + { + if (t.To.IsEmpty() || t.Subject.IsEmpty() || t.Name.IsEmpty()) + { + throw new InvalidOperationException("Message template validation failed, because at least one of the following properties has not been set: Name, To, Subject."); + } + } + + if (ctx.StoreId.GetValueOrDefault() == 0) + { + ctx.Store = _services.StoreContext.CurrentStore; + ctx.StoreId = ctx.Store.Id; + } + else + { + ctx.Store = _services.StoreService.GetStoreById(ctx.StoreId.Value); + } + + if (ctx.BaseUri == null) + { + ctx.BaseUri = new Uri(_services.StoreService.GetHost(ctx.Store)); + } + + if (ctx.LanguageId.GetValueOrDefault() == 0) + { + ctx.Language = _services.WorkContext.WorkingLanguage; + ctx.LanguageId = ctx.Language.Id; + } + else + { + ctx.Language = _languageService.GetLanguageById(ctx.LanguageId.Value); + } + + EnsureLanguageIsActive(ctx); + + var parts = modelParts?.AsEnumerable() ?? Enumerable.Empty(); + + if (ctx.Customer == null) + { + // Try to move Customer from parts to MessageContext + var customer = parts.OfType().FirstOrDefault(); + if (customer != null) + { + // Exclude the found customer from parts list + parts = parts.Where(x => !object.ReferenceEquals(x, customer)); + } + + ctx.Customer = customer ?? _services.WorkContext.CurrentCustomer; + } + + if (ctx.Customer.IsSystemAccount) + { + throw new ArgumentException("Cannot create messages for system customer accounts.", nameof(ctx)); + } + + if (ctx.MessageTemplate == null) + { + if (ctx.MessageTemplateName.IsEmpty()) + { + throw new ArgumentException("'MessageTemplateName' must not be empty if 'MessageTemplate' is null.", nameof(ctx)); + } + + ctx.MessageTemplate = GetActiveMessageTemplate(ctx.MessageTemplateName, ctx.Store.Id); + if (ctx.MessageTemplate == null) + { + throw new FileNotFoundException("The message template '{0}' does not exist.".FormatInvariant(ctx.MessageTemplateName)); + } + } + + if (ctx.EmailAccount == null) + { + ctx.EmailAccount = GetEmailAccountOfMessageTemplate(ctx.MessageTemplate, ctx.Language.Id); + } + + // Sort parts: "IModelPart" instances must come first + var bagParts = parts.OfType(); + if (bagParts.Any()) + { + parts = bagParts.Concat(parts.Except(bagParts)); + } + + modelParts = parts.ToArray(); + } + + protected MessageTemplate GetActiveMessageTemplate(string messageTemplateName, int storeId) + { + var messageTemplate = _messageTemplateService.GetMessageTemplateByName(messageTemplateName, storeId); + if (messageTemplate == null || !messageTemplate.IsActive) + return null; + + return messageTemplate; + } + + protected EmailAccount GetEmailAccountOfMessageTemplate(MessageTemplate messageTemplate, int languageId) + { + var accountId = messageTemplate.GetLocalized(x => x.EmailAccountId, languageId); + var account = _emailAccountService.GetEmailAccountById(accountId); + + if (account == null) + { + account = _emailAccountService.GetDefaultEmailAccount(); + } + + if (account == null) + { + throw new SmartException(T("Common.Error.NoEmailAccount")); + } + + return account; + } + + private void EnsureLanguageIsActive(MessageContext ctx) + { + var language = ctx.Language; + + if (language == null || !language.Published) + { + // Load any language from the specified store + language = _languageService.GetLanguageById(_languageService.GetDefaultLanguageId(ctx.StoreId.Value)); + } + + if (language == null || !language.Published) + { + // Load any language + language = _languageService.GetAllLanguages().FirstOrDefault(); + } + + ctx.Language = language ?? throw new SmartException(T("Common.Error.NoActiveLanguage")); + } + + #region TestModels + + public virtual object[] GetTestModels(MessageContext messageContext) + { + var templateName = (messageContext.MessageTemplate?.Name ?? messageContext.MessageTemplateName); + + var factories = new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + { "BlogComment", () => GetRandomEntity(x => true) }, + { "Product", () => GetRandomEntity(x => !x.Deleted && x.VisibleIndividually && x.Published) }, + { "Customer", () => GetRandomEntity(x => !x.Deleted && !x.IsSystemAccount && !string.IsNullOrEmpty(x.Email)) }, + { "Order", () => GetRandomEntity(x => !x.Deleted) }, + { "Shipment", () => GetRandomEntity(x => !x.Order.Deleted) }, + { "OrderNote", () => GetRandomEntity(x => !x.Order.Deleted) }, + { "RecurringPayment", () => GetRandomEntity(x => !x.Deleted) }, + { "NewsLetterSubscription", () => GetRandomEntity(x => true) }, + { "Campaign", () => GetRandomEntity(x => true) }, + { "ReturnRequest", () => GetRandomEntity(x => true) }, + { "OrderItem", () => GetRandomEntity(x => !x.Order.Deleted) }, + { "ForumTopic", () => GetRandomEntity(x => true) }, + { "ForumPost", () => GetRandomEntity(x => true) }, + { "PrivateMessage", () => GetRandomEntity(x => true) }, + { "GiftCard", () => GetRandomEntity(x => true) }, + { "ProductReview", () => GetRandomEntity(x => !x.Product.Deleted && x.Product.VisibleIndividually && x.Product.Published) }, + { "NewsComment", () => GetRandomEntity(x => x.NewsItem.Published) } + }; + + var modelNames = messageContext.MessageTemplate.ModelTypes + .SplitSafe(",") + .Select(x => x.Trim()) + .Distinct() + .ToArray(); + + var models = new Dictionary(); + var result = new List(); + + foreach (var modelName in modelNames) + { + var model = GetModelFromExpression(modelName, models, factories); + if (model != null) + { + result.Add(model); + } + } + + // Some models are special + var isTransientTemplate = messageContext.MessageTemplate != null && messageContext.MessageTemplate.IsTransientRecord(); + + if (!isTransientTemplate) + { + switch (templateName) + { + case MessageTemplateNames.SystemContactUs: + result.Add(new NamedModelPart("Message") + { + ["Subject"] = "Test subject", + ["Message"] = LoremIpsum, + ["SenderEmail"] = "jane@doe.com", + ["SenderName"] = "Jane Doe" + }); + break; + case MessageTemplateNames.ProductQuestion: + result.Add(new NamedModelPart("Message") + { + ["Message"] = LoremIpsum, + ["SenderEmail"] = "jane@doe.com", + ["SenderName"] = "Jane Doe", + ["SenderPhone"] = "123456789" + }); + break; + case MessageTemplateNames.ShareProduct: + result.Add(new NamedModelPart("Message") + { + ["Body"] = LoremIpsum, + ["From"] = "jane@doe.com", + ["To"] = "john@doe.com", + }); + break; + case MessageTemplateNames.ShareWishlist: + result.Add(new NamedModelPart("Wishlist") + { + ["PersonalMessage"] = LoremIpsum, + ["From"] = "jane@doe.com", + ["To"] = "john@doe.com", + }); + break; + case MessageTemplateNames.NewVatSubmittedStoreOwner: + result.Add(new NamedModelPart("VatValidationResult") + { + ["Name"] = "VatName", + ["Address"] = "VatAddress" + }); + break; + } + } + + return result.ToArray(); + } + + private object GetModelFromExpression(string expression, IDictionary models, IDictionary> factories) + { + object currentModel = null; + int dotIndex = 0; + int len = expression.Length; + bool bof = true; + string token = null; + + for (var i = 0; i < len; i++) + { + if (expression[i] == '.') + { + bof = false; + token = expression.Substring(0, i); + } + else if (i == len - 1) + { + // End reached + token = expression; + } + else + { + continue; + } + + if (!models.TryGetValue(token, out currentModel)) + { + if (bof) + { + // It's a simple dot-less expression where the token + // is actually the model name + currentModel = factories.Get(token)?.Invoke(); + } + else + { + // Sub-token, e.g. "Order.Customer" + // Get "Customer" part, this is our property name, NOT the model name + var propName = token.Substring(dotIndex + 1); + // Get parent model "Order" + var parentModel = models.Get(token.Substring(0, dotIndex)); + if (parentModel == null) + break; + + if (parentModel is ITestModel) + { + // When the parent model is a test model, we need to create a random instance + // instead of using the property value (which is null/void in this case) + currentModel = factories.Get(propName)?.Invoke(); + } + else + { + // Get "Customer" property of Order + var fastProp = FastProperty.GetProperty(parentModel.GetType(), propName, PropertyCachingStrategy.Uncached); + if (fastProp != null) + { + // Get "Customer" value + var propValue = fastProp.GetValue(parentModel); + if (propValue != null) + { + currentModel = propValue; + //// Resolve logical model name... + //var modelName = _modelProvider.ResolveModelName(propValue); + //if (modelName != null) + //{ + // // ...and create the value + // currentModel = factories.Get(modelName)?.Invoke(); + //} + } + } + } + } + + if (currentModel == null) + break; + + // Put it in dict as e.g. "Order.Customer" + models[token] = currentModel; + } + + if (!bof) + { + dotIndex = i; + } + } + + return currentModel; + } + + private object GetRandomEntity(Expression> predicate) where T : BaseEntity, new() + { + var dbSet = _services.DbContext.Set(); + + var query = dbSet.Where(predicate); + + // Determine how many entities match the given predicate + var count = query.Count(); + + object result; + + if (count > 0) + { + // Fetch a random one + var skip = new Random().Next(count - 1); + result = query.OrderBy(x => x.Id).Skip(skip).FirstOrDefault(); + } + else + { + // No entity macthes the predicate. Provide a fallback test entity + var entity = Activator.CreateInstance(); + result = _templateEngine.CreateTestModelFor(entity, entity.GetUnproxiedType().Name); + } + + return result; + } + + #endregion + } +} diff --git a/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.OrderParts.cs b/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.OrderParts.cs new file mode 100644 index 0000000000..f939392935 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.OrderParts.cs @@ -0,0 +1,406 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using SmartStore.ComponentModel; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Shipping; +using SmartStore.Core.Domain.Tax; +using SmartStore.Core.Html; +using SmartStore.Core.Plugins; +using SmartStore.Services.Catalog; +using SmartStore.Services.Common; +using SmartStore.Services.Customers; +using SmartStore.Services.Directory; +using SmartStore.Services.Localization; +using SmartStore.Services.Media; +using SmartStore.Services.Orders; +using SmartStore.Services.Payments; + +namespace SmartStore.Services.Messages +{ + public partial class MessageModelProvider + { + protected virtual object CreateModelPart(Order part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var allow = new HashSet + { + nameof(part.Id), + nameof(part.OrderNumber), + nameof(part.OrderGuid), + nameof(part.StoreId), + nameof(part.OrderStatus), + nameof(part.PaymentStatus), + nameof(part.ShippingStatus), + nameof(part.CustomerTaxDisplayType), + nameof(part.TaxRatesDictionary), + nameof(part.VatNumber), + nameof(part.AffiliateId), + nameof(part.CustomerIp), + nameof(part.CardType), + nameof(part.CardName), + nameof(part.MaskedCreditCardNumber), + nameof(part.DirectDebitAccountHolder), + nameof(part.DirectDebitBankCode), // TODO: (mc) Liquid > Bank data (?) + nameof(part.PurchaseOrderNumber), + nameof(part.ShippingMethod), + nameof(part.PaymentMethodSystemName), + nameof(part.ShippingRateComputationMethodSystemName) + // TODO: (mc) Liquid > More whitelisting? + }; + + var m = new HybridExpando(part, allow, MemberOptMethod.Allow); + var d = m as dynamic; + + d.ID = part.Id; + d.Billing = CreateModelPart(part.BillingAddress, messageContext); + d.Shipping = part.ShippingAddress?.IsPostalDataEqual(part.BillingAddress) == true ? null : CreateModelPart(part.ShippingAddress, messageContext); + d.CustomerEmail = part.BillingAddress.Email.NullEmpty(); + d.CustomerComment = part.CustomerOrderComment.NullEmpty(); + d.Disclaimer = GetTopic("Disclaimer", messageContext); + d.ConditionsOfUse = GetTopic("ConditionsOfUse", messageContext); + d.Status = part.OrderStatus.GetLocalizedEnum(_services.Localization, messageContext.Language.Id); + d.CreatedOn = ToUserDate(part.CreatedOnUtc, messageContext); + d.PaidOn = ToUserDate(part.PaidDateUtc, messageContext); + + // Payment method + var paymentMethodName = part.PaymentMethodSystemName; + var paymentMethod = _services.Resolve().GetProvider(part.PaymentMethodSystemName); + if (paymentMethod != null) + { + paymentMethodName = GetLocalizedValue(messageContext, paymentMethod.Metadata, nameof(paymentMethod.Metadata.FriendlyName), x => x.FriendlyName); + } + d.PaymentMethod = paymentMethodName.NullEmpty(); + + d.Url = part.Customer != null && !part.Customer.IsGuest() + ? BuildActionUrl("Details", "Order", new { id = part.Id, area = "" }, messageContext) + : null; + + // Overrides + m.Properties["OrderNumber"] = part.GetOrderNumber().NullEmpty(); + m.Properties["AcceptThirdPartyEmailHandOver"] = GetBoolResource(part.AcceptThirdPartyEmailHandOver, messageContext); + + // Items, Totals & Co. + d.Items = part.OrderItems.Where(x => x.Product != null).Select(x => CreateModelPart(x, messageContext)).ToList(); + d.Totals = CreateOrderTotalsPart(part, messageContext); + + // Checkout Attributes + if (part.CheckoutAttributeDescription.HasValue()) + { + d.CheckoutAttributes = HtmlUtils.ConvertPlainTextToTable(HtmlUtils.ConvertHtmlToPlainText(part.CheckoutAttributeDescription)).NullEmpty(); + } + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateOrderTotalsPart(Order order, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(order, nameof(order)); + + var language = messageContext.Language; + var currencyService = _services.Resolve(); + var paymentService = _services.Resolve(); + var priceFormatter = _services.Resolve(); + var taxSettings = _services.Settings.LoadSetting(messageContext.Store.Id); + + var taxRates = new SortedDictionary(); + string cusTaxTotal = string.Empty; + string cusDiscount = string.Empty; + string cusRounding = string.Empty; + string cusTotal = string.Empty; + + var subTotals = GetSubTotals(order, messageContext); + + // Shipping + bool dislayShipping = order.ShippingStatus != ShippingStatus.ShippingNotRequired; + + // Payment method fee + bool displayPaymentMethodFee = true; + if (order.PaymentMethodAdditionalFeeExclTax == decimal.Zero) + { + displayPaymentMethodFee = false; + } + + // Tax + bool displayTax = true; + bool displayTaxRates = true; + if (taxSettings.HideTaxInOrderSummary && order.CustomerTaxDisplayType == TaxDisplayType.IncludingTax) + { + displayTax = false; + displayTaxRates = false; + } + else + { + if (order.OrderTax == 0 && taxSettings.HideZeroTax) + { + displayTax = false; + displayTaxRates = false; + } + else + { + taxRates = new SortedDictionary(); + foreach (var tr in order.TaxRatesDictionary) + { + taxRates.Add(tr.Key, currencyService.ConvertCurrency(tr.Value, order.CurrencyRate)); + } + + displayTaxRates = taxSettings.DisplayTaxRates && taxRates.Count > 0; + displayTax = !displayTaxRates; + + var orderTaxInCustomerCurrency = currencyService.ConvertCurrency(order.OrderTax, order.CurrencyRate); + string taxStr = priceFormatter.FormatPrice(orderTaxInCustomerCurrency, true, order.CustomerCurrencyCode, false, language); + cusTaxTotal = taxStr; + } + } + + // Discount + bool dislayDiscount = false; + if (order.OrderDiscount > decimal.Zero) + { + var orderDiscountInCustomerCurrency = currencyService.ConvertCurrency(order.OrderDiscount, order.CurrencyRate); + cusDiscount = priceFormatter.FormatPrice(-orderDiscountInCustomerCurrency, true, order.CustomerCurrencyCode, false, language); + dislayDiscount = true; + } + + // Total + var roundingAmount = decimal.Zero; + var orderTotal = order.GetOrderTotalInCustomerCurrency(currencyService, paymentService, out roundingAmount); + cusTotal = priceFormatter.FormatPrice(orderTotal, true, order.CustomerCurrencyCode, false, language); + + // Rounding + if (roundingAmount != decimal.Zero) + { + cusRounding = priceFormatter.FormatPrice(roundingAmount, true, order.CustomerCurrencyCode, false, language); + } + + // Model + dynamic m = new ExpandoObject(); + + m.SubTotal = subTotals.SubTotal.NullEmpty(); + m.SubTotalDiscount = subTotals.DisplaySubTotalDiscount ? subTotals.SubTotalDiscount : null; + m.Shipping = dislayShipping ? subTotals.ShippingTotal : null; + m.Payment = displayPaymentMethodFee ? subTotals.PaymentFee : null; + m.Tax = displayTax ? cusTaxTotal : null; + m.Discount = dislayDiscount ? cusDiscount : null; + m.RoundingDiff = cusRounding.NullEmpty(); + m.Total = cusTotal; + + // TaxRates + m.TaxRates = !displayTaxRates ? (object[])null : taxRates.Select(x => + { + return new + { + Rate = T("Order.TaxRateLine", language.Id, priceFormatter.FormatTaxRate(x.Key)).Text, + Value = FormatPrice(x.Value, order, messageContext) + }; + }).ToArray(); + + + // Gift Cards + m.GiftCardUsage = order.GiftCardUsageHistory.Count == 0 ? (object[])null : order.GiftCardUsageHistory.Select(x => + { + return new + { + GiftCard = T("Order.GiftCardInfo", language.Id, x.GiftCard.GiftCardCouponCode).Text, + UsedAmount = FormatPrice(-x.UsedValue, order, messageContext), + RemainingAmount = FormatPrice(x.GiftCard.GetGiftCardRemainingAmount(), order, messageContext) + }; + }).ToArray(); + + // Reward Points + m.RedeemedRewardPoints = order.RedeemedRewardPointsEntry == null ? null : new + { + Title = T("Order.RewardPoints", language.Id, -order.RedeemedRewardPointsEntry.Points).Text, + Amount = FormatPrice(-order.RedeemedRewardPointsEntry.UsedAmount, order, messageContext) + }; + + return m; + } + + private (string SubTotal, string SubTotalDiscount, string ShippingTotal, string PaymentFee, bool DisplaySubTotalDiscount) GetSubTotals(Order order, MessageContext messageContext) + { + var language = messageContext.Language; + var currencyService = _services.Resolve(); + var priceFormatter = _services.Resolve(); + + string cusSubTotal = string.Empty; + string cusSubTotalDiscount = string.Empty; + string cusShipTotal = string.Empty; + string cusPaymentMethodFee = string.Empty; + bool dislaySubTotalDiscount = false; + + var subTotal = order.CustomerTaxDisplayType == TaxDisplayType.ExcludingTax ? order.OrderSubtotalExclTax : order.OrderSubtotalInclTax; + var subTotalDiscount = order.CustomerTaxDisplayType == TaxDisplayType.ExcludingTax ? order.OrderSubTotalDiscountExclTax : order.OrderSubTotalDiscountInclTax; + var shipping = order.CustomerTaxDisplayType == TaxDisplayType.ExcludingTax ? order.OrderShippingExclTax : order.OrderShippingInclTax; + var payment = order.CustomerTaxDisplayType == TaxDisplayType.ExcludingTax ? order.PaymentMethodAdditionalFeeExclTax : order.PaymentMethodAdditionalFeeInclTax; + + // Subtotal + cusSubTotal = FormatPrice(subTotal, order, messageContext); + + // Discount (applied to order subtotal) + var orderSubTotalDiscount = currencyService.ConvertCurrency(subTotalDiscount, order.CurrencyRate); + if (orderSubTotalDiscount > decimal.Zero) + { + cusSubTotalDiscount = priceFormatter.FormatPrice(-orderSubTotalDiscount, true, order.CustomerCurrencyCode, language, false, false); + dislaySubTotalDiscount = true; + } + + // Shipping + var orderShipping = currencyService.ConvertCurrency(shipping, order.CurrencyRate); + cusShipTotal = priceFormatter.FormatShippingPrice(orderShipping, true, order.CustomerCurrencyCode, language, false, false); + + // Payment method additional fee + var paymentMethodAdditionalFee = currencyService.ConvertCurrency(payment, order.CurrencyRate); + cusPaymentMethodFee = priceFormatter.FormatPaymentMethodAdditionalFee(paymentMethodAdditionalFee, true, order.CustomerCurrencyCode, language, false, false); + + return (cusSubTotal, cusSubTotalDiscount, cusShipTotal, cusPaymentMethodFee, dislaySubTotalDiscount); + } + + protected virtual object CreateModelPart(OrderItem part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var productAttributeParser = _services.Resolve(); + var downloadService = _services.Resolve(); + var order = part.Order; + var isNet = order.CustomerTaxDisplayType == TaxDisplayType.ExcludingTax; + var product = part.Product; + product.MergeWithCombination(part.AttributesXml, productAttributeParser); + + var m = new Dictionary + { + { "DownloadUrl", !downloadService.IsDownloadAllowed(part) ? null : BuildActionUrl("GetDownload", "Download", new { id = part.OrderItemGuid, area = "" }, messageContext) }, + { "AttributeDescription", part.AttributeDescription.NullEmpty() }, + { "Weight", part.ItemWeight }, + { "TaxRate", part.TaxRate }, + { "Qty", part.Quantity }, + { "UnitPrice", FormatPrice(isNet ? part.UnitPriceExclTax : part.UnitPriceInclTax, part.Order, messageContext) }, + { "LineTotal", FormatPrice(isNet ? part.PriceExclTax : part.PriceInclTax, part.Order, messageContext) }, + { "Product", CreateModelPart(product, messageContext, part.AttributesXml) } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(OrderNote part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "Id", part.Id }, + { "CreatedOn", ToUserDate(part.CreatedOnUtc, messageContext) }, + { "Text", part.FormatOrderNoteText().NullEmpty() } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(ShoppingCartItem part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "Id", part.Id }, + { "Quantity", part.Quantity }, + { "Product", CreateModelPart(part.Product, messageContext, part.AttributesXml) }, + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(Shipment part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var orderItems = new List(); + var orderItemIds = part.ShipmentItems.Select(x => x.OrderItemId).ToArray(); + foreach (var orderItemId in orderItemIds) + { + var orderItem = _services.Resolve().GetOrderItemById(orderItemId); + if (orderItem != null) + { + orderItems.Add(orderItem); + } + } + + var m = new Dictionary + { + { "Id", part.Id }, + { "TrackingNumber", part.TrackingNumber.NullEmpty() }, + { "TotalWeight", part.TotalWeight }, + { "CreatedOn", ToUserDate(part.CreatedOnUtc, messageContext) }, + { "DeliveredOn", ToUserDate(part.DeliveryDateUtc, messageContext) }, + { "ShippedOn", ToUserDate(part.ShippedDateUtc, messageContext) }, + { "Url", BuildActionUrl("ShipmentDetails", "Order", new { id = part.Id, area = "" }, messageContext)}, + { "Items", orderItems.Where(x => x.Product != null).Select(x => CreateModelPart(x, messageContext)).ToList() }, + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(RecurringPayment part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "Id", part.Id }, + { "CreatedOn", ToUserDate(part.CreatedOnUtc, messageContext) }, + { "StartedOn", ToUserDate(part.StartDateUtc, messageContext) }, + { "NextOn", ToUserDate(part.NextPaymentDate, messageContext) }, + { "CycleLength", part.CycleLength }, + { "CyclePeriod", part.CyclePeriod.GetLocalizedEnum(_services.Localization, messageContext.Language.Id) }, + { "CyclesRemaining", part.CyclesRemaining }, + { "TotalCycles", part.TotalCycles }, + { "Url", BuildActionUrl("Edit", "RecurringPayment", new { id = part.Id, area = "admin" }, messageContext) } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(ReturnRequest part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "Id", part.Id }, + { "Reason", part.ReasonForReturn.NullEmpty() }, + { "Status", part.ReturnRequestStatus.GetLocalizedEnum(_services.Localization, messageContext.Language.Id) }, + { "RequestedAction", part.RequestedAction.NullEmpty() }, + { "CustomerComments", HtmlUtils.FormatText(part.CustomerComments, true, false, false, false, false, false).NullEmpty() }, + { "StaffNotes", HtmlUtils.FormatText(part.StaffNotes, true, false, false, false, false, false).NullEmpty() }, + { "Quantity", part.Quantity }, + { "Url", BuildActionUrl("Edit", "ReturnRequest", new { id = part.Id, area = "admin" }, messageContext) } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + } +} diff --git a/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.Utils.cs b/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.Utils.cs new file mode 100644 index 0000000000..5872aff5ff --- /dev/null +++ b/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.Utils.cs @@ -0,0 +1,164 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Plugins; +using SmartStore.Services.Catalog; +using SmartStore.Services.Common; +using SmartStore.Services.Customers; +using SmartStore.Services.Topics; +using SmartStore.Services.Media; +using SmartStore.Services.Directory; +using SmartStore.Services.Localization; +using SmartStore.Core.Domain.Orders; +using System.Text; +using SmartStore.Core.Domain.Common; +using SmartStore.Core.Html; +using SmartStore.Utilities; + +namespace SmartStore.Services.Messages +{ + public partial class MessageModelProvider + { + private string BuildUrl(string url, MessageContext ctx) + { + return ctx.BaseUri.ToString().TrimEnd('/') + url; + } + + private string BuildRouteUrl(object routeValues, MessageContext ctx) + { + return ctx.BaseUri.ToString().TrimEnd('/') + _urlHelper.RouteUrl(routeValues); + } + + private string BuildRouteUrl(string routeName, object routeValues, MessageContext ctx) + { + return ctx.BaseUri.ToString().TrimEnd('/') + _urlHelper.RouteUrl(routeName, routeValues); + } + + private string BuildActionUrl(string action, string controller, object routeValues, MessageContext ctx) + { + return ctx.BaseUri.ToString().TrimEnd('/') + _urlHelper.Action(action, controller, routeValues); + } + + private void PublishModelPartCreatedEvent(T source, dynamic part) where T : class + { + _services.EventPublisher.Publish(new MessageModelPartCreatedEvent(source, part)); + } + + private string GetLocalizedValue(MessageContext messageContext, ProviderMetadata metadata, string propertyName, Expression> fallback) + { + // TODO: (mc) this actually belongs to PluginMediator, but we simply cannot add a dependency to framework from here. Refactor later! + + Guard.NotNull(metadata, nameof(metadata)); + + string systemName = metadata.SystemName; + var resourceName = metadata.ResourceKeyPattern.FormatInvariant(metadata.SystemName, propertyName); + string result = _services.Localization.GetResource(resourceName, messageContext.Language.Id, false, "", true); + + if (result.IsEmpty()) + result = fallback.Compile()(metadata); + + return result; + } + + private object GetTopic(string topicSystemName, MessageContext ctx) + { + var topicService = _services.Resolve(); + + // Load by store + var topic = topicService.GetTopicBySystemName(topicSystemName, ctx.Store.Id); + if (topic == null) + { + // Not found. Let's find topic assigned to all stores + topic = topicService.GetTopicBySystemName(topicSystemName, 0); + } + + var body = topic?.GetLocalized(x => x.Body, ctx.Language.Id); + if (body.HasValue()) + { + body = HtmlUtils.RelativizeFontSizes(body); + } + + return new + { + Title = topic?.GetLocalized(x => x.Title, ctx.Language.Id).NullEmpty(), + Body = body.NullEmpty() + }; + } + + private string GetDisplayNameForCustomer(Customer customer) + { + return customer.GetFullName().NullEmpty() ?? customer.Username ?? customer.FindEmail(); + } + + private string GetBoolResource(bool value, MessageContext ctx) + { + return _services.Localization.GetResource(value ? "Common.Yes" : "Common.No", ctx.Language.Id); + } + + private DateTime? ToUserDate(DateTime? utcDate, MessageContext messageContext) + { + if (utcDate == null) + return null; + + return _services.DateTimeHelper.ConvertToUserTime( + utcDate.Value, + TimeZoneInfo.Utc, + _services.DateTimeHelper.GetCustomerTimeZone(messageContext.Customer)); + } + + private string FormatPrice(decimal price, Order order, MessageContext messageContext) + { + return FormatPrice(price, order.CurrencyRate, order.CustomerCurrencyCode, messageContext); + } + + private string FormatPrice(decimal price, decimal currencyRate, string customerCurrencyCode, MessageContext messageContext) + { + var language = messageContext.Language; + var currencyService = _services.Resolve(); + var priceFormatter = _services.Resolve(); + + return priceFormatter.FormatPrice(currencyService.ConvertCurrency(price, currencyRate), true, customerCurrencyCode, false, language); + } + + private PictureInfo GetPictureFor(Product product, string attributesXml) + { + var pictureService = _services.PictureService; + var attrParser = _services.Resolve(); + + PictureInfo pictureInfo = null; + + if (attributesXml.HasValue()) + { + var combination = attrParser.FindProductVariantAttributeCombination(product.Id, attributesXml); + + if (combination != null) + { + var picturesIds = combination.GetAssignedPictureIds(); + if (picturesIds != null && picturesIds.Length > 0) + { + pictureInfo = pictureService.GetPictureInfo(picturesIds[0]); + } + } + } + + if (pictureInfo == null) + { + pictureInfo = pictureService.GetPictureInfo(product.MainPictureId); + } + + if (pictureInfo == null && !product.VisibleIndividually && product.ParentGroupedProductId > 0) + { + pictureInfo = pictureService.GetPictureInfo(pictureService.GetPicturesByProductId(product.ParentGroupedProductId, 1).FirstOrDefault()); + } + + return pictureInfo; + } + + private object[] Concat(params object[] values) + { + return values.Where(x => CommonHelper.IsTruthy(x)).ToArray(); + } + } +} diff --git a/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.cs b/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.cs new file mode 100644 index 0000000000..d1f79a9a4d --- /dev/null +++ b/src/Libraries/SmartStore.Services/Messages/MessageModelProvider.cs @@ -0,0 +1,967 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Dynamic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Web; +using System.Web.Mvc; +using SmartStore.Collections; +using SmartStore.ComponentModel; +using SmartStore.Core; +using SmartStore.Core.Domain.Blogs; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Directory; +using SmartStore.Core.Domain.Forums; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.Domain.Messages; +using SmartStore.Core.Domain.News; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Shipping; +using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Domain.Tax; +using SmartStore.Core.Html; +using SmartStore.Core.Localization; +using SmartStore.Core.Logging; +using SmartStore.Services.Catalog; +using SmartStore.Services.Catalog.Extensions; +using SmartStore.Services.Common; +using SmartStore.Services.Customers; +using SmartStore.Services.Directory; +using SmartStore.Services.Forums; +using SmartStore.Services.Localization; +using SmartStore.Services.Media; +using SmartStore.Services.Orders; +using SmartStore.Services.Seo; +using SmartStore.Templating; +using SmartStore.Utilities; + +namespace SmartStore.Services.Messages +{ + public enum ModelTreeMemberKind + { + Primitive, + Complex, + Collection, + Root + } + + public class ModelTreeMember + { + public string Name { get; set; } + public ModelTreeMemberKind Kind { get; set; } + } + + public partial class MessageModelProvider : IMessageModelProvider + { + private readonly ICommonServices _services; + private readonly ITemplateEngine _templateEngine; + private readonly IMessageTemplateService _messageTemplateService; + private readonly IEmailAccountService _emailAccountService; + private readonly UrlHelper _urlHelper; + + public MessageModelProvider( + ICommonServices services, + ITemplateEngine templateEngine, + IMessageTemplateService messageTemplateService, + IEmailAccountService emailAccountService, + UrlHelper urlHelper) + { + _services = services; + _templateEngine = templateEngine; + _messageTemplateService = messageTemplateService; + _emailAccountService = emailAccountService; + _urlHelper = urlHelper; + + T = NullLocalizer.InstanceEx; + Logger = NullLogger.Instance; + } + + public LocalizerEx T { get; set; } + public ILogger Logger { get; set; } + + public virtual void AddGlobalModelParts(MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + + var model = messageContext.Model; + + model["Context"] = new Dictionary + { + { "TemplateName", messageContext.MessageTemplate.Name }, + { "LanguageId", messageContext.Language.Id }, + { "LanguageCulture", messageContext.Language.LanguageCulture }, + { "BaseUrl", messageContext.BaseUri.ToString() } + }; + + dynamic email = new ExpandoObject(); + email.Email = messageContext.EmailAccount.Email; + email.SenderName = messageContext.EmailAccount.DisplayName; + email.DisplayName = messageContext.EmailAccount.DisplayName; // Alias + model["Email"] = email; + + model["Theme"] = CreateThemeModelPart(messageContext); + model["Customer"] = CreateModelPart(messageContext.Customer, messageContext); + model["Store"] = CreateModelPart(messageContext.Store, messageContext); + } + + public virtual void AddModelPart(object part, MessageContext messageContext, string name = null) + { + Guard.NotNull(part, nameof(part)); + Guard.NotNull(messageContext, nameof(messageContext)); + + var model = messageContext.Model; + + name = name.NullEmpty() ?? ResolveModelName(part); + + object modelPart = null; + + switch (part) + { + case INamedModelPart x: + modelPart = x; + break; + case IModelPart x: + MergeModelBag(x, model, messageContext); + break; + case Order x: + modelPart = CreateModelPart(x, messageContext); + break; + case Product x: + modelPart = CreateModelPart(x, messageContext); + break; + case Customer x: + //modelPart = CreateModelPart(x, messageContext); + break; + case Address x: + modelPart = CreateModelPart(x, messageContext); + break; + case Shipment x: + modelPart = CreateModelPart(x, messageContext); + break; + case OrderNote x: + modelPart = CreateModelPart(x, messageContext); + break; + case RecurringPayment x: + modelPart = CreateModelPart(x, messageContext); + break; + case ReturnRequest x: + modelPart = CreateModelPart(x, messageContext); + break; + case GiftCard x: + modelPart = CreateModelPart(x, messageContext); + break; + case NewsLetterSubscription x: + modelPart = CreateModelPart(x, messageContext); + break; + case Campaign x: + modelPart = CreateModelPart(x, messageContext); + break; + case ProductReview x: + modelPart = CreateModelPart(x, messageContext); + break; + case BlogComment x: + modelPart = CreateModelPart(x, messageContext); + break; + case NewsComment x: + modelPart = CreateModelPart(x, messageContext); + break; + case ForumTopic x: + modelPart = CreateModelPart(x, messageContext); + break; + case ForumPost x: + modelPart = CreateModelPart(x, messageContext); + break; + case Forum x: + modelPart = CreateModelPart(x, messageContext); + break; + case PrivateMessage x: + modelPart = CreateModelPart(x, messageContext); + break; + //case BackInStockSubscription x: + // modelPart = CreateModelPart(x, messageContext); + // break; + default: + var partType = part.GetType(); + modelPart = part; + + if (!messageContext.TestMode && partType.IsPlainObjectType() && !partType.IsAnonymous()) + { + var evt = new MessageModelPartMappingEvent(part); + _services.EventPublisher.Publish(evt); + + if (evt.Result != null && !object.ReferenceEquals(evt.Result, part)) + { + modelPart = evt.Result; + name = evt.ModelPartName.NullEmpty() ?? ResolveModelName(evt.Result) ?? name; + } + else + { + modelPart = part; + } + + modelPart = evt.Result ?? part; + name = evt.ModelPartName.NullEmpty() ?? name; + } + + break; + } + + if (modelPart != null) + { + if (name.IsEmpty()) + { + throw new SmartException($"Could not resolve a model key for part '{modelPart.GetType().Name}'. Use an instance of 'NamedModelPart' class to pass model with name."); + } + + if (model.TryGetValue(name, out var existing)) + { + // A model part with the same name exists in model already... + if (existing is IDictionary x) + { + // but it's a dictionary which we can easily merge with + x.Merge(FastProperty.ObjectToDictionary(modelPart), true); + } + else + { + // Wrap in HybridExpando and merge + var he = new HybridExpando(existing, true); + he.Merge(FastProperty.ObjectToDictionary(modelPart), true); + model[name] = he; + } + } + else + { + // Put part to model as new property + model[name] = modelPart; + } + } + } + + public string ResolveModelName(object model) + { + Guard.NotNull(model, nameof(model)); + + string name = null; + var type = model.GetType(); + + try + { + if (model is BaseEntity be) + { + name = be.GetUnproxiedType().Name; + } + else if (model is ITestModel te) + { + name = te.ModelName; + } + else if (model is INamedModelPart mp) + { + name = mp.ModelPartName; + } + else if (type.IsPlainObjectType()) + { + name = type.Name; + } + } + catch { } + + return name; + } + + #region Global model part handlers + + protected virtual object CreateThemeModelPart(MessageContext messageContext) + { + var m = new Dictionary + { + { "FontFamily", "-apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif" }, + { "BodyBg", "#f2f4f6" }, + { "BodyColor", "#555" }, + { "TitleColor", "#2f3133" }, + { "ContentBg", "#fff" }, + { "ShadeColor", "#e2e2e2" }, + { "LinkColor", "#0066c0" }, + { "BrandPrimary", "#3f51b5" }, + { "BrandSuccess", "#4caf50" }, + { "BrandWarning", "#ff9800" }, + { "BrandDanger", "#f44336" }, + { "MutedColor", "#a5a5a5" }, + }; + + return m; + } + + protected virtual object CreateCompanyModelPart(MessageContext messageContext) + { + var settings = _services.Settings.LoadSetting(messageContext.Store.Id); + dynamic m = new HybridExpando(settings, true); + + m.NameLine = Concat(settings.Salutation, settings.Title, settings.Firstname, settings.Lastname); + m.StreetLine = Concat(settings.Street, settings.Street2); + m.CityLine = Concat(settings.ZipCode, settings.City); + m.CountryLine = Concat(settings.CountryName, settings.Region); + + PublishModelPartCreatedEvent(settings, m); + return m; + } + + protected virtual object CreateBankModelPart(MessageContext messageContext) + { + var settings = _services.Settings.LoadSetting(messageContext.Store.Id); + var m = new HybridExpando(settings, true); + PublishModelPartCreatedEvent(settings, m); + return m; + } + + protected virtual object CreateContactModelPart(MessageContext messageContext) + { + var settings = _services.Settings.LoadSetting(messageContext.Store.Id); + var contact = new HybridExpando(settings, true) as dynamic; + + // Aliases + contact.Phone = new + { + Company = settings.CompanyTelephoneNumber.NullEmpty(), + Hotline = settings.HotlineTelephoneNumber.NullEmpty(), + Mobile = settings.MobileTelephoneNumber.NullEmpty(), + Fax = settings.CompanyFaxNumber.NullEmpty() + }; + + contact.Email = new + { + Company = settings.CompanyEmailAddress.NullEmpty(), + Webmaster = settings.WebmasterEmailAddress.NullEmpty(), + Support = settings.SupportEmailAddress.NullEmpty(), + Contact = settings.ContactEmailAddress.NullEmpty() + }; + + PublishModelPartCreatedEvent(settings, contact); + + return contact; + } + + #endregion + + #region Generic model part handlers + + protected virtual void MergeModelBag(IModelPart part, IDictionary model, MessageContext messageContext) + { + if (!(model.Get("Bag") is IDictionary bag)) + { + model["Bag"] = bag = new Dictionary(); + } + + var source = part as IDictionary; + bag.Merge(source); + } + + #endregion + + #region Entity specific model part handlers + + protected virtual object CreateModelPart(Store part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var host = messageContext.BaseUri.ToString(); + var logoInfo = _services.PictureService.GetPictureInfo(messageContext.Store.LogoPictureId); + + // Issue: https://github.com/smartstoreag/SmartStoreNET/issues/1321 + + var m = new Dictionary + { + { "Email", messageContext.EmailAccount.Email }, + { "EmailName", messageContext.EmailAccount.DisplayName }, + { "Name", part.Name }, + { "Url", host }, + { "Cdn", part.ContentDeliveryNetwork }, + { "PrimaryStoreCurrency", part.PrimaryStoreCurrency?.CurrencyCode }, + { "PrimaryExchangeRateCurrency", part.PrimaryExchangeRateCurrency?.CurrencyCode }, + { "Logo", CreateModelPart(logoInfo, messageContext, host, null, new Size(400, 75)) }, + { "Company", CreateCompanyModelPart(messageContext) }, + { "Contact", CreateContactModelPart(messageContext) }, + { "Bank", CreateBankModelPart(messageContext) }, + { "Copyright", T("Content.CopyrightNotice", messageContext.Language.Id, DateTime.Now.Year.ToString(), part.Name).Text } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(PictureInfo part, MessageContext messageContext, + string href, + int? targetSize = null, + Size? clientMaxSize = null, + string alt = null) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotEmpty(href, nameof(href)); + + if (part == null) + return null; + + var width = part.Width; + var height = part.Height; + + if (width.HasValue && height.HasValue && (targetSize.HasValue || clientMaxSize.HasValue)) + { + var maxSize = clientMaxSize ?? new Size(targetSize.Value, targetSize.Value); + var size = ImagingHelper.Rescale(new Size(width.Value, height.Value), maxSize); + width = size.Width; + height = size.Height; + } + + var m = new + { + Src = _services.PictureService.GetUrl(part, targetSize.GetValueOrDefault(), FallbackPictureType.NoFallback, messageContext.BaseUri.ToString()), + Href = href, + Width = width, + Height = height, + Alt = alt + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(Product part, MessageContext messageContext, string attributesXml = null) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var mediaSettings = _services.Resolve(); + var shoppingCartSettings = _services.Resolve(); + var catalogSettings = _services.Resolve(); + var deliveryTimeService = _services.Resolve(); + var quantityUnitService = _services.Resolve(); + var productUrlHelper = _services.Resolve(); + + var currency = _services.WorkContext.WorkingCurrency; + var additionalShippingCharge = _services.Resolve().ConvertFromPrimaryStoreCurrency(part.AdditionalShippingCharge, currency); + var additionalShippingChargeFormatted = _services.Resolve().FormatPrice(additionalShippingCharge, false, currency.CurrencyCode, false, messageContext.Language); + var url = productUrlHelper.GetProductUrl(part.Id, part.GetSeName(messageContext.Language.Id), attributesXml); + var pictureInfo = GetPictureFor(part, null); + var name = part.GetLocalized(x => x.Name, messageContext.Language.Id); + var alt = T("Media.Product.ImageAlternateTextFormat", messageContext.Language.Id, name).Text; + + var m = new Dictionary + { + { "Id", part.Id }, + { "Sku", catalogSettings.ShowProductSku ? part.Sku : null }, + { "Name", name }, + { "Description", part.GetLocalized(x => x.ShortDescription, messageContext.Language.Id).NullEmpty() }, + { "StockQuantity", part.StockQuantity }, + { "AdditionalShippingCharge", additionalShippingChargeFormatted.NullEmpty() }, + { "Url", url }, + { "Thumbnail", CreateModelPart(pictureInfo, messageContext, url, mediaSettings.MessageProductThumbPictureSize, new Size(50, 50), alt) }, + { "ThumbnailLg", CreateModelPart(pictureInfo, messageContext, url, mediaSettings.ProductThumbPictureSize, new Size(120, 120), alt) }, + { "DeliveryTime", null }, + { "QtyUnit", null } + }; + + if (shoppingCartSettings.ShowDeliveryTimes && part.IsShipEnabled) + { + if (deliveryTimeService.GetDeliveryTimeById(part.DeliveryTimeId ?? 0) is DeliveryTime dt) + { + m["DeliveryTime"] = new Dictionary + { + { "Color", dt.ColorHexValue }, + { "Name", dt.GetLocalized(x => x.Name, messageContext.Language.Id) }, + }; + } + } + + if (quantityUnitService.GetQuantityUnitById(part.QuantityUnitId) is QuantityUnit qu) + { + m["QtyUnit"] = qu.GetLocalized(x => x.Name, messageContext.Language.Id); + } + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(Customer part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var email = part.FindEmail(); + var pwdRecoveryToken = part.GetAttribute(SystemCustomerAttributeNames.PasswordRecoveryToken).NullEmpty(); + var accountActivationToken = part.GetAttribute(SystemCustomerAttributeNames.AccountActivationToken).NullEmpty(); + + int rewardPointsBalance = part.GetRewardPointsBalance(); + decimal rewardPointsAmountBase = _services.Resolve().ConvertRewardPointsToAmount(rewardPointsBalance); + decimal rewardPointsAmount = _services.Resolve().ConvertFromPrimaryStoreCurrency(rewardPointsAmountBase, _services.WorkContext.WorkingCurrency); + + var m = new Dictionary + { + ["Id"] = part.Id, + ["CustomerGuid"] = part.CustomerGuid, + ["Username"] = part.Username, + ["Email"] = email, + ["IsTaxExempt"] = part.IsTaxExempt, + ["LastIpAddress"] = part.LastIpAddress, + ["CreatedOn"] = ToUserDate(part.CreatedOnUtc, messageContext), + ["LastLoginOn"] = ToUserDate(part.LastLoginDateUtc, messageContext), + ["LastActivityOn"] = ToUserDate(part.LastActivityDateUtc, messageContext), + + ["FullName"] = GetDisplayNameForCustomer(part).NullEmpty(), + ["VatNumber"] = part.GetAttribute(SystemCustomerAttributeNames.VatNumber).NullEmpty(), + ["VatNumberStatus"] = part.GetAttribute(SystemCustomerAttributeNames.VatNumberStatusId).GetLocalizedEnum(_services.Localization, messageContext.Language.Id).NullEmpty(), + ["CustomerNumber"] = part.GetAttribute(SystemCustomerAttributeNames.CustomerNumber).NullEmpty(), + ["IsRegistered"] = part.IsRegistered(), + + // URLs + ["WishlistUrl"] = BuildRouteUrl("Wishlist", new { customerGuid = part.CustomerGuid }, messageContext), + ["EditUrl"] = BuildActionUrl("Edit", "Customer", new { id = part.Id, area = "admin" }, messageContext), + ["PasswordRecoveryURL"] = pwdRecoveryToken == null ? null : BuildActionUrl("passwordrecoveryconfirm", "customer", + new { token = part.GetAttribute(SystemCustomerAttributeNames.PasswordRecoveryToken), email = email, area = "" }, + messageContext), + ["AccountActivationURL"] = accountActivationToken == null ? null : BuildActionUrl("activation", "customer", + new { token = part.GetAttribute(SystemCustomerAttributeNames.AccountActivationToken), email = email, area = "" }, + messageContext), + + // Addresses + ["BillingAddress"] = CreateModelPart(part.BillingAddress ?? new Address(), messageContext), + ["ShippingAddress"] = part.ShippingAddress == null ? null : CreateModelPart(part.ShippingAddress, messageContext), + + // Reward Points + ["RewardPointsAmount"] = rewardPointsAmount, + ["RewardPointsBalance"] = _services.Resolve().FormatPrice(rewardPointsAmount, true, false), + ["RewardPointsHistory"] = part.RewardPointsHistory.Count == 0 ? null : part.RewardPointsHistory.Select(x => CreateModelPart(x, messageContext)).ToList(), + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(GiftCard part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "Id", part.Id }, + { "SenderName", part.SenderName.NullEmpty() }, + { "SenderEmail", part.SenderEmail.NullEmpty() }, + { "RecipientName", part.RecipientName.NullEmpty() }, + { "RecipientEmail", part.RecipientEmail.NullEmpty() }, + { "Amount", _services.Resolve().FormatPrice(part.Amount, true, false) }, + { "CouponCode", part.GiftCardCouponCode.NullEmpty() } + }; + + // Message + var message = (string)null; + if (part.Message.HasValue()) + { + message = HtmlUtils.FormatText(part.Message, true, false, false, false, false, false); + } + m["Message"] = message; + + // RemainingAmount + var remainingAmount = (string)null; + var order = part?.PurchasedWithOrderItem?.Order; + if (order != null) + { + var amount = _services.Resolve().ConvertCurrency(part.GetGiftCardRemainingAmount(), order.CurrencyRate); + remainingAmount = _services.Resolve().FormatPrice(amount, true, false); + } + m["RemainingAmount"] = remainingAmount; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(NewsLetterSubscription part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var gid = part.NewsLetterSubscriptionGuid; + + var m = new Dictionary + { + { "Id", part.Id }, + { "Email", part.Email.NullEmpty() }, + { "ActivationUrl", gid == Guid.Empty ? null : BuildRouteUrl("NewsletterActivation", new { token = part.NewsLetterSubscriptionGuid, active = true }, messageContext) }, + { "DeactivationUrl", gid == Guid.Empty ? null : BuildRouteUrl("NewsletterActivation", new { token = part.NewsLetterSubscriptionGuid, active = false }, messageContext) } + }; + + var customer = messageContext.Customer; + if (customer != null && customer.Email.IsCaseInsensitiveEqual(part.Email.EmptyNull())) + { + // Set FullName only if a customer account exists for the subscriber's email address. + m["FullName"] = customer.GetFullName(); + } + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(Campaign part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var protocol = messageContext.BaseUri.Scheme; + var host = messageContext.BaseUri.Host; + var body = HtmlUtils.RelativizeFontSizes(part.Body.EmptyNull()); + + // We must render the body separately + body = _templateEngine.Render(body, messageContext.Model, messageContext.FormatProvider); + + var m = new Dictionary + { + { "Id", part.Id }, + { "Subject", part.Subject.NullEmpty() }, + { "Body", WebHelper.MakeAllUrlsAbsolute(body, protocol, host).NullEmpty() }, + { "CreatedOn", ToUserDate(part.CreatedOnUtc, messageContext) } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(ProductReview part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "Title", part.Title.NullEmpty() }, + { "Text", HtmlUtils.FormatText(part.ReviewText, true, false, false, false, false, false).NullEmpty() }, + { "Rating", part.Rating } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(PrivateMessage part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "Subject", part.Subject.NullEmpty() }, + { "Text", part.FormatPrivateMessageText().NullEmpty() }, + { "FromEmail", part.FromCustomer?.FindEmail().NullEmpty() }, + { "ToEmail", part.ToCustomer?.FindEmail().NullEmpty() }, + { "FromName", part.FromCustomer?.GetFullName().NullEmpty() }, + { "ToName", part.ToCustomer?.GetFullName().NullEmpty() }, + { "Url", BuildActionUrl("View", "PrivateMessages", new { id = part.Id, area = "" }, messageContext) } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(BlogComment part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "PostTitle", part.BlogPost.Title.NullEmpty() }, + { "PostUrl", BuildRouteUrl("BlogPost", new { SeName = part.BlogPost.GetSeName(part.BlogPost.LanguageId, ensureTwoPublishedLanguages: false) }, messageContext) }, + { "Text", part.CommentText.NullEmpty() } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(NewsComment part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "NewsTitle", part.NewsItem.Title.NullEmpty() }, + { "Title", part.CommentTitle.NullEmpty() }, + { "Text", HtmlUtils.FormatText(part.CommentText, true, false, false, false, false, false).NullEmpty() }, + { "NewsUrl", BuildRouteUrl("NewsItem", new { SeName = part.NewsItem.GetSeName(messageContext.Language.Id) }, messageContext) } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(ForumTopic part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var pageIndex = messageContext.Model.GetFromBag("TopicPageIndex"); + + var url = pageIndex > 0 ? + BuildRouteUrl("TopicSlugPaged", new { id = part.Id, slug = part.GetSeName(), page = pageIndex }, messageContext) : + BuildRouteUrl("TopicSlug", new { id = part.Id, slug = part.GetSeName() }, messageContext); + + var m = new Dictionary + { + { "Subject", part.Subject.NullEmpty() }, + { "NumReplies", part.NumReplies }, + { "NumPosts", part.NumPosts }, + { "NumViews", part.Views }, + { "Body", part.GetFirstPost(_services.Resolve())?.FormatPostText().NullEmpty() }, + { "Url", url }, + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(ForumPost part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "Author", part.Customer.FormatUserName().NullEmpty() }, + { "Body", part.FormatPostText().NullEmpty() } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(Forum part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "Name", part.GetLocalized(x => x.Name, messageContext.Language.Id).NullEmpty() }, + { "GroupName", part.ForumGroup?.GetLocalized(x => x.Name, messageContext.Language.Id).NullEmpty() }, + { "NumPosts", part.NumPosts }, + { "NumTopics", part.NumTopics }, + { "Url", BuildRouteUrl("ForumSlug", new { id = part.Id, slug = part.GetSeName(messageContext.Language.Id) }, messageContext) }, + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + protected virtual object CreateModelPart(Address part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var settings = _services.Resolve(); + var languageId = messageContext.Language?.Id ?? messageContext.LanguageId; + + var salutation = part.Salutation.NullEmpty(); + var title = part.Title.NullEmpty(); + var company = settings.CompanyEnabled ? part.Company : null; + var firstName = part.FirstName.NullEmpty(); + var lastName = part.LastName.NullEmpty(); + var street1 = settings.StreetAddressEnabled ? part.Address1 : null; + var street2 = settings.StreetAddress2Enabled ? part.Address2 : null; + var zip = settings.ZipPostalCodeEnabled ? part.ZipPostalCode : null; + var city = settings.CityEnabled ? part.City : null; + var country = settings.CountryEnabled ? part.Country?.GetLocalized(x => x.Name, languageId ?? 0).NullEmpty() : null; + var state = settings.StateProvinceEnabled ? part.StateProvince?.GetLocalized(x => x.Name, languageId ?? 0).NullEmpty() : null; + + var m = new Dictionary + { + { "Title", title }, + { "Salutation", salutation }, + { "FullSalutation", part.GetFullSalutaion().NullEmpty() }, + { "FullName", part.GetFullName(false).NullEmpty() }, + { "Company", company }, + { "FirstName", firstName }, + { "LastName", lastName }, + { "Street1", street1 }, + { "Street2", street2 }, + { "Country", country }, + { "CountryId", part.Country?.Id }, + { "CountryAbbrev2", settings.CountryEnabled ? part.Country?.TwoLetterIsoCode.NullEmpty() : null }, + { "CountryAbbrev3", settings.CountryEnabled ? part.Country?.ThreeLetterIsoCode.NullEmpty() : null }, + { "State", state }, + { "StateAbbrev", settings.StateProvinceEnabled ? part.StateProvince?.Abbreviation.NullEmpty() : null }, + { "City", city }, + { "ZipCode", zip }, + { "Email", part.Email.NullEmpty() }, + { "Phone", settings.PhoneEnabled ? part.PhoneNumber : null }, + { "Fax", settings.FaxEnabled ? part.FaxNumber : null } + }; + + m["NameLine"] = Concat(salutation, title, firstName, lastName); + m["StreetLine"] = Concat(street1, street2); + m["CityLine"] = Concat(zip, city); + m["CountryLine"] = Concat(country, state); + + PublishModelPartCreatedEvent
(part, m); + + return m; + } + + protected virtual object CreateModelPart(RewardPointsHistory part, MessageContext messageContext) + { + Guard.NotNull(messageContext, nameof(messageContext)); + Guard.NotNull(part, nameof(part)); + + var m = new Dictionary + { + { "Id", part.Id }, + { "CreatedOn", ToUserDate(part.CreatedOnUtc, messageContext) }, + { "Message", part.Message.NullEmpty() }, + { "Points", part.Points }, + { "PointsBalance", part.PointsBalance }, + { "UsedAmount", part.UsedAmount } + }; + + PublishModelPartCreatedEvent(part, m); + + return m; + } + + #endregion + + #region Model Tree + + public TreeNode GetLastModelTree(string messageTemplateName) + { + Guard.NotEmpty(messageTemplateName, nameof(messageTemplateName)); + + var template = _messageTemplateService.GetMessageTemplateByName(messageTemplateName, _services.StoreContext.CurrentStore.Id); + + if (template != null) + { + return GetLastModelTree(template); + } + + return null; + } + + public TreeNode GetLastModelTree(MessageTemplate template) + { + Guard.NotNull(template, nameof(template)); + + if (template.LastModelTree.IsEmpty()) + { + return null; + } + + return Newtonsoft.Json.JsonConvert.DeserializeObject>(template.LastModelTree); + } + + public TreeNode BuildModelTree(TemplateModel model) + { + Guard.NotNull(model, nameof(model)); + + var root = new TreeNode(new ModelTreeMember { Name = "Model", Kind = ModelTreeMemberKind.Root }); + + foreach (var kvp in model) + { + root.Append(BuildModelTreePart(kvp.Key, kvp.Value)); + } + + return root; + } + + private TreeNode BuildModelTreePart(string modelName, object instance) + { + var t = instance?.GetType(); + TreeNode node = null; + + if (t == null || t.IsPredefinedType()) + { + node = new TreeNode(new ModelTreeMember { Name = modelName, Kind = ModelTreeMemberKind.Primitive }); + } + else if (t.IsSequenceType() && !(instance is IDictionary)) + { + node = new TreeNode(new ModelTreeMember { Name = modelName, Kind = ModelTreeMemberKind.Collection }); + } + else + { + node = new TreeNode(new ModelTreeMember { Name = modelName, Kind = ModelTreeMemberKind.Complex }); + + if (instance is IDictionary dict) + { + foreach (var kvp in dict) + { + node.Append(BuildModelTreePart(kvp.Key, kvp.Value)); + } + } + else if (instance is IDynamicMetaObjectProvider dyn) + { + foreach (var name in dyn.GetMetaObject(Expression.Constant(dyn)).GetDynamicMemberNames()) + { + // we don't want to go deeper in "pure" dynamic objects + node.Append(new TreeNode(new ModelTreeMember { Name = name, Kind = ModelTreeMemberKind.Primitive })); + } + } + else + { + node.AppendRange(BuildModelTreePartForClass(instance)); + } + } + + return node; + } + + private IEnumerable> BuildModelTreePartForClass(object instance) + { + var type = instance?.GetType(); + + if (type == null) + { + yield break; + } + + foreach (var prop in FastProperty.GetProperties(type).Values) + { + var pi = prop.Property; + + if (pi.PropertyType.IsPredefinedType()) + { + yield return new TreeNode(new ModelTreeMember { Name = prop.Name, Kind = ModelTreeMemberKind.Primitive }); + } + else if (typeof(IDictionary).IsAssignableFrom(pi.PropertyType)) + { + yield return BuildModelTreePart(prop.Name, prop.GetValue(instance)); + } + else if (pi.PropertyType.IsSequenceType()) + { + yield return new TreeNode(new ModelTreeMember { Name = prop.Name, Kind = ModelTreeMemberKind.Collection }); + } + else + { + var node = new TreeNode(new ModelTreeMember { Name = prop.Name, Kind = ModelTreeMemberKind.Complex }); + node.AppendRange(BuildModelTreePartForClass(prop.GetValue(instance))); + yield return node; + } + } + } + + #endregion + } +} diff --git a/src/Libraries/SmartStore.Services/Messages/MessageTemplateService.cs b/src/Libraries/SmartStore.Services/Messages/MessageTemplateService.cs index 20f9d4c0e3..7d4e91b30f 100644 --- a/src/Libraries/SmartStore.Services/Messages/MessageTemplateService.cs +++ b/src/Libraries/SmartStore.Services/Messages/MessageTemplateService.cs @@ -15,7 +15,7 @@ public partial class MessageTemplateService: IMessageTemplateService { private const string MESSAGETEMPLATES_ALL_KEY = "SmartStore.messagetemplate.all-{0}"; private const string MESSAGETEMPLATES_BY_NAME_KEY = "SmartStore.messagetemplate.name-{0}-{1}"; - private const string MESSAGETEMPLATES_PATTERN_KEY = "SmartStore.messagetemplate."; + private const string MESSAGETEMPLATES_PATTERN_KEY = "SmartStore.messagetemplate.*"; private readonly IRepository _messageTemplateRepository; private readonly IRepository _storeMappingRepository; @@ -55,9 +55,6 @@ public virtual void DeleteMessageTemplate(MessageTemplate messageTemplate) _messageTemplateRepository.Delete(messageTemplate); _requestCache.RemoveByPattern(MESSAGETEMPLATES_PATTERN_KEY); - - //event notification - _eventPublisher.EntityDeleted(messageTemplate); } public virtual void InsertMessageTemplate(MessageTemplate messageTemplate) @@ -68,9 +65,6 @@ public virtual void InsertMessageTemplate(MessageTemplate messageTemplate) _messageTemplateRepository.Insert(messageTemplate); _requestCache.RemoveByPattern(MESSAGETEMPLATES_PATTERN_KEY); - - //event notification - _eventPublisher.EntityInserted(messageTemplate); } public virtual void UpdateMessageTemplate(MessageTemplate messageTemplate) @@ -81,9 +75,6 @@ public virtual void UpdateMessageTemplate(MessageTemplate messageTemplate) _messageTemplateRepository.Update(messageTemplate); _requestCache.RemoveByPattern(MESSAGETEMPLATES_PATTERN_KEY); - - //event notification - _eventPublisher.EntityUpdated(messageTemplate); } public virtual MessageTemplate GetMessageTemplateById(int messageTemplateId) @@ -156,6 +147,8 @@ public virtual MessageTemplate CopyMessageTemplate(MessageTemplate messageTempla var mtCopy = new MessageTemplate { Name = messageTemplate.Name, + To = messageTemplate.To, + ReplyTo = messageTemplate.ReplyTo, BccEmailAddresses = messageTemplate.BccEmailAddresses, Subject = messageTemplate.Subject, Body = messageTemplate.Body, diff --git a/src/Libraries/SmartStore.Services/Messages/MessageTokenProvider.cs b/src/Libraries/SmartStore.Services/Messages/MessageTokenProvider.cs deleted file mode 100644 index 00f3dd54ed..0000000000 --- a/src/Libraries/SmartStore.Services/Messages/MessageTokenProvider.cs +++ /dev/null @@ -1,1370 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Linq.Expressions; -using System.Text; -using System.Web; -using System.Web.Mvc; -using SmartStore.Collections; -using SmartStore.Core.Domain.Blogs; -using SmartStore.Core.Domain.Catalog; -using SmartStore.Core.Domain.Common; -using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Directory; -using SmartStore.Core.Domain.Forums; -using SmartStore.Core.Domain.Localization; -using SmartStore.Core.Domain.Media; -using SmartStore.Core.Domain.Messages; -using SmartStore.Core.Domain.News; -using SmartStore.Core.Domain.Orders; -using SmartStore.Core.Domain.Security; -using SmartStore.Core.Domain.Shipping; -using SmartStore.Core.Domain.Stores; -using SmartStore.Core.Domain.Tax; -using SmartStore.Core.Html; -using SmartStore.Core.Plugins; -using SmartStore.Services.Catalog; -using SmartStore.Services.Catalog.Extensions; -using SmartStore.Services.Common; -using SmartStore.Services.Customers; -using SmartStore.Services.Directory; -using SmartStore.Services.Forums; -using SmartStore.Services.Localization; -using SmartStore.Services.Media; -using SmartStore.Services.Orders; -using SmartStore.Services.Payments; -using SmartStore.Services.Seo; -using SmartStore.Services.Topics; - -namespace SmartStore.Services.Messages -{ - public partial class MessageTokenProvider : IMessageTokenProvider - { - #region Fields - - private readonly UrlHelper _urlHelper; - private readonly IPriceFormatter _priceFormatter; - private readonly ICommonServices _services; - private readonly ILanguageService _languageService; - private readonly IEmailAccountService _emailAccountService; - private readonly ICurrencyService _currencyService; - private readonly IDownloadService _downloadService; - private readonly IOrderService _orderService; - private readonly IProviderManager _providerManager; - private readonly IProductAttributeParser _productAttributeParser; - private readonly ITopicService _topicService; - private readonly IDeliveryTimeService _deliveryTimeService; - private readonly IQuantityUnitService _quantityUnitService; - private readonly IUrlRecordService _urlRecordService; - private readonly IGenericAttributeService _genericAttributeService; - private readonly IPictureService _pictureService; - private readonly ProductUrlHelper _productUrlHelper; - - private readonly MediaSettings _mediaSettings; - private readonly ContactDataSettings _contactDataSettings; - private readonly MessageTemplatesSettings _templatesSettings; - private readonly CatalogSettings _catalogSettings; - private readonly TaxSettings _taxSettings; - private readonly CompanyInformationSettings _companyInfoSettings; - private readonly BankConnectionSettings _bankConnectionSettings; - private readonly ShoppingCartSettings _shoppingCartSettings; - private readonly SecuritySettings _securitySettings; - - #endregion - - #region Ctor - - public MessageTokenProvider( - UrlHelper urlHelper, - IPriceFormatter priceFormatter, - ICommonServices services, - ILanguageService languageService, - IEmailAccountService emailAccountService, - ICurrencyService currencyService, - IDownloadService downloadService, - IOrderService orderService, - IProviderManager providerManager, - IProductAttributeParser productAttributeParser, - ITopicService topicService, - IDeliveryTimeService deliveryTimeService, - IQuantityUnitService quantityUnitService, - IUrlRecordService urlRecordService, - IGenericAttributeService genericAttributeService, - IPictureService pictureService, - ProductUrlHelper productUrlHelper, - MediaSettings mediaSettings, - ContactDataSettings contactDataSettings, - MessageTemplatesSettings templatesSettings, - CatalogSettings catalogSettings, - TaxSettings taxSettings, - CompanyInformationSettings companyInfoSettings, - BankConnectionSettings bankConnectionSettings, - ShoppingCartSettings shoppingCartSettings, - SecuritySettings securitySettings) - { - _urlHelper = urlHelper; - _priceFormatter = priceFormatter; - _services = services; - _languageService = languageService; - _emailAccountService = emailAccountService; - _currencyService = currencyService; - _downloadService = downloadService; - _orderService = orderService; - _providerManager = providerManager; - _productAttributeParser = productAttributeParser; - _topicService = topicService; - _deliveryTimeService = deliveryTimeService; - _quantityUnitService = quantityUnitService; - _urlRecordService = urlRecordService; - _genericAttributeService = genericAttributeService; - _pictureService = pictureService; - _productUrlHelper = productUrlHelper; - - _mediaSettings = mediaSettings; - _contactDataSettings = contactDataSettings; - _templatesSettings = templatesSettings; - _catalogSettings = catalogSettings; - _taxSettings = taxSettings; - _companyInfoSettings = companyInfoSettings; - _bankConnectionSettings = bankConnectionSettings; - _shoppingCartSettings = shoppingCartSettings; - _securitySettings = securitySettings; - } - - #endregion - - #region Utilities - - protected virtual Picture GetPictureFor(Product product, string attributesXml) - { - Picture picture = null; - - if (attributesXml.HasValue()) - { - var combination = _productAttributeParser.FindProductVariantAttributeCombination(product.Id, attributesXml); - - if (combination != null) - { - var picturesIds = combination.GetAssignedPictureIds(); - if (picturesIds != null && picturesIds.Length > 0) - picture = _pictureService.GetPictureById(picturesIds[0]); - } - } - - if (picture == null) - { - picture = _pictureService.GetPicturesByProductId(product.Id, 1).FirstOrDefault(); - } - - if (picture == null && !product.VisibleIndividually && product.ParentGroupedProductId > 0) - { - picture = _pictureService.GetPicturesByProductId(product.ParentGroupedProductId, 1).FirstOrDefault(); - } - - return picture; - } - - protected virtual string ProductPictureToHtml(Picture picture, Language language, string productName, string productUrl, string storeLocation) - { - if (picture != null && _mediaSettings.MessageProductThumbPictureSize > 0) - { - var imageUrl = _pictureService.GetPictureUrl(picture, _mediaSettings.MessageProductThumbPictureSize, false, storeLocation); - if (imageUrl.HasValue()) - { - var title = _services.Localization.GetResource("Media.Product.ImageLinkTitleFormat", language.Id).FormatInvariant(productName); - var alternate = _services.Localization.GetResource("Media.Product.ImageAlternateTextFormat", language.Id).FormatInvariant(productName); - - var polaroid = "padding: 3px; background-color: #fff; border: 1px solid #ccc; border: 1px solid rgba(0,0,0,.2);"; - var style = "max-width: {0}px; max-height: {0}px; {1}".FormatInvariant(_mediaSettings.MessageProductThumbPictureSize, polaroid); - - var image = "\"{1}\"".FormatInvariant(imageUrl, alternate, title, style); - - if (productUrl.IsEmpty()) - return image; - - return "{1}".FormatInvariant(productUrl, image); - } - } - return ""; - } - - /// - /// Convert a collection to a HTML table - /// - /// Order - /// Language identifier - /// HTML table of products - protected virtual string ProductListToHtmlTable(Order order, Language language) - { - var sb = new StringBuilder(); - var storeLocation = _services.WebHelper.GetStoreLocation(false); - - sb.AppendLine(""); - - #region Products - - sb.AppendLine(string.Format("", _templatesSettings.Color1)); - sb.AppendLine(string.Format("", _services.Localization.GetResource("Messages.Order.Product(s).Name", language.Id))); - sb.AppendLine(string.Format("", _services.Localization.GetResource("Messages.Order.Product(s).Price", language.Id))); - sb.AppendLine(string.Format("", _services.Localization.GetResource("Messages.Order.Product(s).Quantity", language.Id))); - sb.AppendLine(string.Format("", _services.Localization.GetResource("Messages.Order.Product(s).Total", language.Id))); - sb.AppendLine(""); - - var table = order.OrderItems.ToList(); - for (int i = 0; i <= table.Count - 1; i++) - { - var orderItem = table[i]; - var product = orderItem.Product; - if (product == null) - continue; - - DeliveryTime deliveryTime = null; - - product.MergeWithCombination(orderItem.AttributesXml, _productAttributeParser); - - if (_shoppingCartSettings.ShowDeliveryTimes && product.IsShipEnabled) - { - deliveryTime = _deliveryTimeService.GetDeliveryTimeById(product.DeliveryTimeId ?? 0); - } - - sb.AppendLine(string.Format("", _templatesSettings.Color2)); - - var productName = product.GetLocalized(x => x.Name, language.Id); - var productUrl = _productUrlHelper.GetProductUrl(product.Id, product.GetSeName(), orderItem.AttributesXml); - - sb.AppendLine(""); - - string unitPriceStr = string.Empty; - switch (order.CustomerTaxDisplayType) - { - case TaxDisplayType.ExcludingTax: - { - var unitPriceExclTaxInCustomerCurrency = _currencyService.ConvertCurrency(orderItem.UnitPriceExclTax, order.CurrencyRate); - unitPriceStr = _priceFormatter.FormatPrice(unitPriceExclTaxInCustomerCurrency, true, order.CustomerCurrencyCode, language, false); - } - break; - case TaxDisplayType.IncludingTax: - { - var unitPriceInclTaxInCustomerCurrency = _currencyService.ConvertCurrency(orderItem.UnitPriceInclTax, order.CurrencyRate); - unitPriceStr = _priceFormatter.FormatPrice(unitPriceInclTaxInCustomerCurrency, true, order.CustomerCurrencyCode, language, true); - } - break; - } - sb.AppendLine(string.Format("", unitPriceStr)); - - var quantityUnit = _quantityUnitService.GetQuantityUnitById(product.QuantityUnitId); - - sb.AppendLine(string.Format("", - orderItem.Quantity, quantityUnit == null ? "" : quantityUnit.GetLocalized(x => x.Name))); - - string priceStr = string.Empty; - switch (order.CustomerTaxDisplayType) - { - case TaxDisplayType.ExcludingTax: - { - var priceExclTaxInCustomerCurrency = _currencyService.ConvertCurrency(orderItem.PriceExclTax, order.CurrencyRate); - priceStr = _priceFormatter.FormatPrice(priceExclTaxInCustomerCurrency, true, order.CustomerCurrencyCode, language, false); - } - break; - case TaxDisplayType.IncludingTax: - { - var priceInclTaxInCustomerCurrency = _currencyService.ConvertCurrency(orderItem.PriceInclTax, order.CurrencyRate); - priceStr = _priceFormatter.FormatPrice(priceInclTaxInCustomerCurrency, true, order.CustomerCurrencyCode, language, true); - } - break; - } - sb.AppendLine(string.Format("", priceStr)); - - sb.AppendLine(""); - } - - #endregion - - #region Checkout Attributes - - if (!String.IsNullOrEmpty(order.CheckoutAttributeDescription)) - { - sb.AppendLine(""); - } - - #endregion - - #region Totals - - string cusSubTotal = string.Empty; - bool dislaySubTotalDiscount = false; - string cusSubTotalDiscount = string.Empty; - string cusShipTotal = string.Empty; - string cusPaymentMethodAdditionalFee = string.Empty; - var taxRates = new SortedDictionary(); - string cusTaxTotal = string.Empty; - string cusDiscount = string.Empty; - string cusTotal = string.Empty; - - //subtotal, shipping, payment method fee - switch (order.CustomerTaxDisplayType) - { - case TaxDisplayType.ExcludingTax: - { - //subtotal - var orderSubtotalExclTaxInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderSubtotalExclTax, order.CurrencyRate); - cusSubTotal = _priceFormatter.FormatPrice(orderSubtotalExclTaxInCustomerCurrency, true, order.CustomerCurrencyCode, language, false); - //discount (applied to order subtotal) - var orderSubTotalDiscountExclTaxInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderSubTotalDiscountExclTax, order.CurrencyRate); - if (orderSubTotalDiscountExclTaxInCustomerCurrency > decimal.Zero) - { - cusSubTotalDiscount = _priceFormatter.FormatPrice(-orderSubTotalDiscountExclTaxInCustomerCurrency, true, order.CustomerCurrencyCode, language, false); - dislaySubTotalDiscount = true; - } - //shipping - var orderShippingExclTaxInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderShippingExclTax, order.CurrencyRate); - cusShipTotal = _priceFormatter.FormatShippingPrice(orderShippingExclTaxInCustomerCurrency, true, order.CustomerCurrencyCode, language, false); - //payment method additional fee - var paymentMethodAdditionalFeeExclTaxInCustomerCurrency = _currencyService.ConvertCurrency(order.PaymentMethodAdditionalFeeExclTax, order.CurrencyRate); - cusPaymentMethodAdditionalFee = _priceFormatter.FormatPaymentMethodAdditionalFee(paymentMethodAdditionalFeeExclTaxInCustomerCurrency, true, order.CustomerCurrencyCode, language, false); - } - break; - case TaxDisplayType.IncludingTax: - { - //subtotal - var orderSubtotalInclTaxInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderSubtotalInclTax, order.CurrencyRate); - cusSubTotal = _priceFormatter.FormatPrice(orderSubtotalInclTaxInCustomerCurrency, true, order.CustomerCurrencyCode, language, true); - //discount (applied to order subtotal) - var orderSubTotalDiscountInclTaxInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderSubTotalDiscountInclTax, order.CurrencyRate); - if (orderSubTotalDiscountInclTaxInCustomerCurrency > decimal.Zero) - { - cusSubTotalDiscount = _priceFormatter.FormatPrice(-orderSubTotalDiscountInclTaxInCustomerCurrency, true, order.CustomerCurrencyCode, language, true); - dislaySubTotalDiscount = true; - } - //shipping - var orderShippingInclTaxInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderShippingInclTax, order.CurrencyRate); - cusShipTotal = _priceFormatter.FormatShippingPrice(orderShippingInclTaxInCustomerCurrency, true, order.CustomerCurrencyCode, language, true); - //payment method additional fee - var paymentMethodAdditionalFeeInclTaxInCustomerCurrency = _currencyService.ConvertCurrency(order.PaymentMethodAdditionalFeeInclTax, order.CurrencyRate); - cusPaymentMethodAdditionalFee = _priceFormatter.FormatPaymentMethodAdditionalFee(paymentMethodAdditionalFeeInclTaxInCustomerCurrency, true, order.CustomerCurrencyCode, language, true); - } - break; - } - - //shipping - bool dislayShipping = order.ShippingStatus != ShippingStatus.ShippingNotRequired; - - //payment method fee - bool displayPaymentMethodFee = true; - if (order.PaymentMethodAdditionalFeeExclTax == decimal.Zero) - { - displayPaymentMethodFee = false; - } - - //tax - bool displayTax = true; - bool displayTaxRates = true; - if (_taxSettings.HideTaxInOrderSummary && order.CustomerTaxDisplayType == TaxDisplayType.IncludingTax) - { - displayTax = false; - displayTaxRates = false; - } - else - { - if (order.OrderTax == 0 && _taxSettings.HideZeroTax) - { - displayTax = false; - displayTaxRates = false; - } - else - { - taxRates = new SortedDictionary(); - foreach (var tr in order.TaxRatesDictionary) - taxRates.Add(tr.Key, _currencyService.ConvertCurrency(tr.Value, order.CurrencyRate)); - - displayTaxRates = _taxSettings.DisplayTaxRates && taxRates.Count > 0; - displayTax = !displayTaxRates; - - var orderTaxInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderTax, order.CurrencyRate); - string taxStr = _priceFormatter.FormatPrice(orderTaxInCustomerCurrency, true, order.CustomerCurrencyCode, false, language); - cusTaxTotal = taxStr; - } - } - - //discount - bool dislayDiscount = false; - if (order.OrderDiscount > decimal.Zero) - { - var orderDiscountInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderDiscount, order.CurrencyRate); - cusDiscount = _priceFormatter.FormatPrice(-orderDiscountInCustomerCurrency, true, order.CustomerCurrencyCode, false, language); - dislayDiscount = true; - } - - //total - var orderTotalInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderTotal, order.CurrencyRate); - cusTotal = _priceFormatter.FormatPrice(orderTotalInCustomerCurrency, true, order.CustomerCurrencyCode, false, language); - - //subtotal - sb.AppendLine(string.Format("", _services.Localization.GetResource("Messages.Order.SubTotal", language.Id), cusSubTotal)); - - //discount (applied to order subtotal) - if (dislaySubTotalDiscount) - { - sb.AppendLine(string.Format("", _services.Localization.GetResource("Messages.Order.SubTotalDiscount", language.Id), cusSubTotalDiscount)); - } - - - //shipping - if (dislayShipping) - { - sb.AppendLine(string.Format("", _services.Localization.GetResource("Messages.Order.Shipping", language.Id), cusShipTotal)); - } - - //payment method fee - if (displayPaymentMethodFee) - { - string paymentMethodFeeTitle = _services.Localization.GetResource("Messages.Order.PaymentMethodAdditionalFee", language.Id); - - sb.AppendLine(string.Format("", paymentMethodFeeTitle, cusPaymentMethodAdditionalFee)); - } - - //tax - if (displayTax) - { - sb.AppendLine(string.Format("", _services.Localization.GetResource("Messages.Order.Tax", language.Id), cusTaxTotal)); - } - if (displayTaxRates) - { - foreach (var item in taxRates) - { - string taxRate = String.Format(_services.Localization.GetResource("Messages.Order.TaxRateLine"), _priceFormatter.FormatTaxRate(item.Key)); - string taxValue = _priceFormatter.FormatPrice(item.Value, true, order.CustomerCurrencyCode, false, language); - - sb.AppendLine(string.Format("", taxRate, taxValue)); - } - } - - //discount - if (dislayDiscount) - { - sb.AppendLine(string.Format("", _services.Localization.GetResource("Messages.Order.TotalDiscount", language.Id), cusDiscount)); - } - - //gift cards - var gcuhC = order.GiftCardUsageHistory; - foreach (var gcuh in gcuhC) - { - string giftCardText = String.Format(_services.Localization.GetResource("Messages.Order.GiftCardInfo", language.Id), HttpUtility.HtmlEncode(gcuh.GiftCard.GiftCardCouponCode)); - string giftCardAmount = _priceFormatter.FormatPrice(-(_currencyService.ConvertCurrency(gcuh.UsedValue, order.CurrencyRate)), true, order.CustomerCurrencyCode, false, language); - - var remaining = _currencyService.ConvertCurrency(gcuh.GiftCard.GetGiftCardRemainingAmount(), order.CurrencyRate); - var remainingFormatted = _priceFormatter.FormatPrice(remaining, true, false); - var remainingText = _services.Localization.GetResource("ShoppingCart.Totals.GiftCardInfo.Remaining", language.Id).FormatInvariant(remainingFormatted); - - sb.AppendLine(string.Format("", - giftCardText, remainingText, giftCardAmount)); - } - - //reward points - if (order.RedeemedRewardPointsEntry != null) - { - string rpTitle = string.Format(_services.Localization.GetResource("Messages.Order.RewardPoints", language.Id), -order.RedeemedRewardPointsEntry.Points); - string rpAmount = _priceFormatter.FormatPrice(-(_currencyService.ConvertCurrency(order.RedeemedRewardPointsEntry.UsedAmount, order.CurrencyRate)), true, order.CustomerCurrencyCode, false, language); - - sb.AppendLine(string.Format("", rpTitle, rpAmount)); - } - - //total - sb.AppendLine(string.Format("", _templatesSettings.Color3, _services.Localization.GetResource("Messages.Order.OrderTotal", language.Id), cusTotal)); - - #endregion - - sb.AppendLine("
{0}{0}{0}{0}
"); - - if (_mediaSettings.MessageProductThumbPictureSize > 0) - { - var pictureHtml = ProductPictureToHtml(GetPictureFor(product, orderItem.AttributesXml), language, productName, productUrl, storeLocation); - if (pictureHtml.HasValue()) - { - sb.AppendLine("
{0}
".FormatInvariant(pictureHtml)); - } - } - - sb.AppendLine("{1}".FormatInvariant(productUrl, HttpUtility.HtmlEncode(productName))); - - //add download link - if (_downloadService.IsDownloadAllowed(orderItem)) - { - //TODO add a method for getting URL (use routing because it handles all SEO friendly URLs) - string downloadUrl = string.Format("{0}download/getdownload/{1}", storeLocation, orderItem.OrderItemGuid); - string downloadLink = string.Format("{1}", downloadUrl, _services.Localization.GetResource("Messages.Order.Product(s).Download", language.Id)); - sb.AppendLine("  ("); - sb.AppendLine(downloadLink); - sb.AppendLine(")"); - } - - //deliverytime - if (deliveryTime != null) - { - string deliveryTimeName = HttpUtility.HtmlEncode(deliveryTime.GetLocalized(x => x.Name)); - - sb.AppendLine("
"); - sb.AppendLine("
"); - sb.AppendLine("" + _services.Localization.GetResource("Products.DeliveryTime", language.Id) + ""); - sb.AppendLine(""); - sb.AppendLine("" + deliveryTimeName + ""); - sb.AppendLine("
"); - } - - //attributes - if (!String.IsNullOrEmpty(orderItem.AttributeDescription)) - { - sb.AppendLine("
"); - sb.AppendLine(orderItem.AttributeDescription); - } - //sku - if (_catalogSettings.ShowProductSku) - { - if (!String.IsNullOrEmpty(product.Sku)) - { - sb.AppendLine("
"); - sb.AppendLine(string.Format(_services.Localization.GetResource("Messages.Order.Product(s).SKU", language.Id), HttpUtility.HtmlEncode(product.Sku))); - } - } - sb.AppendLine("
{0}{0} {1}{0}
 "); - sb.AppendLine(HtmlUtils.ConvertPlainTextToTable(HtmlUtils.ConvertHtmlToPlainText(order.CheckoutAttributeDescription))); - sb.AppendLine("
 {0} {1}
 {0} {1}
 {0} {1}
 {0} {1}
 {0} {1}
 {0} {1}
 {0} {1}
 {0}
{1}
{2}
 {0} {1}
 {1}{2}
"); - - return sb.ToString(); - } - - /// - /// Convert a collection to a HTML table - /// - /// Shipment - /// Language identifier - /// HTML table of products - protected virtual string ProductListToHtmlTable(Shipment shipment, Language language) - { - var sb = new StringBuilder(); - - sb.AppendLine(""); - - #region Products - - sb.AppendLine(string.Format("", _templatesSettings.Color1)); - sb.AppendLine(string.Format("", _services.Localization.GetResource("Messages.Order.Product(s).Name", language.Id))); - sb.AppendLine(string.Format("", _services.Localization.GetResource("Messages.Order.Product(s).Quantity", language.Id))); - sb.AppendLine(""); - - var table = shipment.ShipmentItems.ToList(); - for (int i = 0; i <= table.Count - 1; i++) - { - var si = table[i]; - var orderItem = _orderService.GetOrderItemById(si.OrderItemId); - if (orderItem == null) - continue; - - var product = orderItem.Product; - if (product == null) - continue; - - sb.AppendLine(string.Format("", _templatesSettings.Color2)); - - var productName = product.GetLocalized(x => x.Name, language.Id); - var productUrl = _productUrlHelper.GetProductUrl(product.Id, product.GetSeName(), orderItem.AttributesXml); - - sb.AppendLine(""); - - sb.AppendLine(string.Format("", si.Quantity)); - - sb.AppendLine(""); - } - - #endregion - - sb.AppendLine("
{0}{0}
"); - sb.AppendLine("{1}".FormatInvariant(productUrl, HttpUtility.HtmlEncode(productName))); - - //attributes - if (!String.IsNullOrEmpty(orderItem.AttributeDescription)) - { - sb.AppendLine("
"); - sb.AppendLine(orderItem.AttributeDescription); - } - //sku - if (_catalogSettings.ShowProductSku) - { - product.MergeWithCombination(orderItem.AttributesXml, _productAttributeParser); - - if (!String.IsNullOrEmpty(product.Sku)) - { - sb.AppendLine("
"); - sb.AppendLine(string.Format(_services.Localization.GetResource("Messages.Order.Product(s).SKU", language.Id), HttpUtility.HtmlEncode(product.Sku))); - } - } - sb.AppendLine("
{0}
"); - - return sb.ToString(); - } - - protected virtual string TopicToHtml(string systemName, int languageId) - { - var result = ""; - var sb = new StringBuilder(); - sb.AppendLine(""); - - //load by store - var topic = _topicService.GetTopicBySystemName(systemName, _services.StoreContext.CurrentStore.Id); - if (topic == null) - //not found. let's find topic assigned to all stores - topic = _topicService.GetTopicBySystemName(systemName, 0); - - if (topic == null) - return string.Empty; - - sb.AppendLine(""); - sb.AppendLine("
"); - sb.AppendLine(topic.Title); - sb.AppendLine("
"); - sb.AppendLine(topic.Body); - sb.AppendLine("
"); - result = sb.ToString(); - return result; - } - - protected virtual string GetSupplierIdentification() - { - var result = ""; - var sb = new StringBuilder(); - sb.AppendLine(""); - sb.AppendLine(""); - - sb.AppendLine(""); - sb.AppendLine("
"); - - sb.AppendLine(String.Format("{0}
", _companyInfoSettings.CompanyName )); - - if (!String.IsNullOrEmpty(_companyInfoSettings.Salutation)) - { - sb.AppendLine(_companyInfoSettings.Salutation); - } - if (!String.IsNullOrEmpty(_companyInfoSettings.Title)) - { - sb.AppendLine(_companyInfoSettings.Title); - } - if (!String.IsNullOrEmpty(_companyInfoSettings.Firstname)) - { - sb.AppendLine(String.Format("{0} ", _companyInfoSettings.Firstname)); - } - if (!String.IsNullOrEmpty(_companyInfoSettings.Lastname)) - { - sb.AppendLine(_companyInfoSettings.Lastname); - } - sb.AppendLine("
"); - - if (!String.IsNullOrEmpty(_companyInfoSettings.Street)) - { - sb.AppendLine(String.Format("{0} {1}
", _companyInfoSettings.Street, _companyInfoSettings.Street2)); - } - if (!String.IsNullOrEmpty(_companyInfoSettings.ZipCode) || !String.IsNullOrEmpty(_companyInfoSettings.City)) - { - sb.AppendLine(String.Format("{0} {1}
", _companyInfoSettings.ZipCode, _companyInfoSettings.City)); - } - if (!String.IsNullOrEmpty(_companyInfoSettings.CountryName)) - { - sb.AppendLine(_companyInfoSettings.CountryName); - - if(!String.IsNullOrEmpty(_companyInfoSettings.Region)) - { - sb.AppendLine(String.Format(", {0}", _companyInfoSettings.Region)); - } - sb.AppendLine("
"); - } - - sb.AppendLine("
"); - - sb.AppendLine(""); - - if (!String.IsNullOrEmpty(_services.StoreContext.CurrentStore.Url)) - { - sb.AppendLine(String.Format("Url: {0}
", _services.StoreContext.CurrentStore.Url)); - } - if (!String.IsNullOrEmpty(_contactDataSettings.CompanyEmailAddress)) - { - sb.AppendLine(String.Format("Mail: {0}
", _contactDataSettings.CompanyEmailAddress)); - } - if (!String.IsNullOrEmpty(_contactDataSettings.CompanyTelephoneNumber)) - { - sb.AppendLine(String.Format("Fon: {0}
", _contactDataSettings.CompanyTelephoneNumber)); - } - if (!String.IsNullOrEmpty(_contactDataSettings.CompanyFaxNumber)) - { - sb.AppendLine(String.Format("Fax: {0}
", _contactDataSettings.CompanyFaxNumber)); - } - - sb.AppendLine("
"); - - sb.AppendLine(""); - - if (!String.IsNullOrEmpty(_bankConnectionSettings.Bankname)) - { - sb.AppendLine(String.Format("{0}
", _bankConnectionSettings.Bankname)); - } - if (!String.IsNullOrEmpty(_bankConnectionSettings.Bankcode)) - { - //TODO: caption - sb.AppendLine(String.Format("{0}
", _bankConnectionSettings.Bankcode)); - } - if (!String.IsNullOrEmpty(_bankConnectionSettings.AccountNumber)) - { - //TODO: caption - sb.AppendLine(String.Format("{0}
", _bankConnectionSettings.AccountNumber)); - } - if (!String.IsNullOrEmpty(_bankConnectionSettings.AccountHolder)) - { - //TODO: caption - sb.AppendLine(String.Format("{0}
", _bankConnectionSettings.AccountHolder)); - } - if (!String.IsNullOrEmpty(_bankConnectionSettings.Iban)) - { - //TODO: caption - sb.AppendLine(String.Format("{0}
", _bankConnectionSettings.Iban)); - } - if (!String.IsNullOrEmpty(_bankConnectionSettings.Bic)) - { - //TODO: caption - sb.AppendLine(String.Format("{0}
", _bankConnectionSettings.Bic)); - } - - sb.AppendLine("
"); - - sb.AppendLine("
"); - result = sb.ToString(); - return result; - } - - protected virtual string GetBoolResource(bool value, int languageId) - { - return _services.Localization.GetResource(value ? "Common.Yes" : "Common.No", languageId); - } - - protected virtual string GetRouteUrl(string routeName, object routeValues) - { - Guard.NotEmpty(routeName, nameof(routeName)); - - var protocol = _securitySettings.ForceSslForAllPages ? "https" : "http"; - var url = _urlHelper.RouteUrl(routeName, routeValues, protocol); - return url; - } - - #endregion - - #region Methods - - public virtual void AddStoreTokens(IList tokens, Store store) - { - tokens.Add(new Token("Store.Name", store.Name)); - tokens.Add(new Token("Store.URL", store.Url, true)); - var defaultEmailAccount = _emailAccountService.GetDefaultEmailAccount(); - tokens.Add(new Token("Store.SupplierIdentification", GetSupplierIdentification(), true)); - tokens.Add(new Token("Store.Email", defaultEmailAccount.Email)); - } - - public virtual void AddCompanyTokens(IList tokens) - { - tokens.Add(new Token("Company.CompanyName", _companyInfoSettings.CompanyName)); - tokens.Add(new Token("Company.Salutation", _companyInfoSettings.Salutation)); - tokens.Add(new Token("Company.Title", _companyInfoSettings.Title)); - tokens.Add(new Token("Company.Firstname", _companyInfoSettings.Firstname)); - tokens.Add(new Token("Company.Lastname", _companyInfoSettings.Lastname)); - tokens.Add(new Token("Company.CompanyManagementDescription", _companyInfoSettings.CompanyManagementDescription)); - tokens.Add(new Token("Company.CompanyManagement", _companyInfoSettings.CompanyManagement)); - tokens.Add(new Token("Company.Street", _companyInfoSettings.Street)); - tokens.Add(new Token("Company.Street2", _companyInfoSettings.Street2)); - tokens.Add(new Token("Company.ZipCode", _companyInfoSettings.ZipCode)); - tokens.Add(new Token("Company.City", _companyInfoSettings.City)); - tokens.Add(new Token("Company.CountryName", _companyInfoSettings.CountryName)); - tokens.Add(new Token("Company.Region", _companyInfoSettings.Region)); - tokens.Add(new Token("Company.VatId", _companyInfoSettings.VatId)); - tokens.Add(new Token("Company.CommercialRegister", _companyInfoSettings.CommercialRegister)); - tokens.Add(new Token("Company.TaxNumber", _companyInfoSettings.TaxNumber)); - } - - public virtual void AddBankConnectionTokens(IList tokens) - { - tokens.Add(new Token("Bank.Bankname", _bankConnectionSettings.Bankname)); - tokens.Add(new Token("Bank.Bankcode", _bankConnectionSettings.Bankcode)); - tokens.Add(new Token("Bank.AccountNumber", _bankConnectionSettings.AccountNumber)); - tokens.Add(new Token("Bank.AccountHolder", _bankConnectionSettings.AccountHolder)); - tokens.Add(new Token("Bank.Iban", _bankConnectionSettings.Iban)); - tokens.Add(new Token("Bank.Bic", _bankConnectionSettings.Bic)); - } - - public virtual void AddContactDataTokens(IList tokens) - { - tokens.Add(new Token("Contact.CompanyTelephoneNumber", _contactDataSettings.CompanyTelephoneNumber)); - tokens.Add(new Token("Contact.HotlineTelephoneNumber", _contactDataSettings.HotlineTelephoneNumber)); - tokens.Add(new Token("Contact.MobileTelephoneNumber", _contactDataSettings.MobileTelephoneNumber)); - tokens.Add(new Token("Contact.CompanyFaxNumber", _contactDataSettings.CompanyFaxNumber)); - tokens.Add(new Token("Contact.CompanyEmailAddress", _contactDataSettings.CompanyEmailAddress)); - tokens.Add(new Token("Contact.WebmasterEmailAddress", _contactDataSettings.WebmasterEmailAddress)); - tokens.Add(new Token("Contact.SupportEmailAddress", _contactDataSettings.SupportEmailAddress)); - tokens.Add(new Token("Contact.ContactEmailAddress", _contactDataSettings.ContactEmailAddress)); - } - - public virtual void AddOrderTokens(IList tokens, Order order, Language language) - { - tokens.Add(new Token("Order.ID", order.Id.ToString())); - tokens.Add(new Token("Order.OrderNumber", order.GetOrderNumber())); - - tokens.Add(new Token("Order.CustomerFullName", string.Format("{0} {1}", order.BillingAddress.FirstName, order.BillingAddress.LastName))); - tokens.Add(new Token("Order.CustomerEmail", order.BillingAddress.Email)); - - tokens.Add(new Token("Order.BillingFullSalutation", string.Format("{0}{1}", - order.BillingAddress.Salutation.EmptyNull(), - order.BillingAddress.Title.HasValue() ? " " + order.BillingAddress.Title : ""))); - - tokens.Add(new Token("Order.BillingSalutation", order.BillingAddress.Salutation)); - tokens.Add(new Token("Order.BillingTitle", order.BillingAddress.Title)); - tokens.Add(new Token("Order.BillingFirstName", order.BillingAddress.FirstName)); - tokens.Add(new Token("Order.BillingLastName", order.BillingAddress.LastName)); - tokens.Add(new Token("Order.BillingPhoneNumber", order.BillingAddress.PhoneNumber)); - tokens.Add(new Token("Order.BillingEmail", order.BillingAddress.Email)); - tokens.Add(new Token("Order.BillingFaxNumber", order.BillingAddress.FaxNumber)); - tokens.Add(new Token("Order.BillingCompany", order.BillingAddress.Company)); - tokens.Add(new Token("Order.BillingAddress1", order.BillingAddress.Address1)); - tokens.Add(new Token("Order.BillingAddress2", order.BillingAddress.Address2)); - tokens.Add(new Token("Order.BillingCity", order.BillingAddress.City)); - tokens.Add(new Token("Order.BillingStateProvince", order.BillingAddress.StateProvince != null ? order.BillingAddress.StateProvince.GetLocalized(x => x.Name) : "")); - tokens.Add(new Token("Order.BillingZipPostalCode", order.BillingAddress.ZipPostalCode)); - tokens.Add(new Token("Order.BillingCountry", order.BillingAddress.Country != null ? order.BillingAddress.Country.GetLocalized(x => x.Name) : "")); - - tokens.Add(new Token("Order.ShippingMethod", order.ShippingMethod)); - - if (order.ShippingAddress != null) - { - tokens.Add(new Token("Order.ShippingFullSalutation", string.Format("{0}{1}", - order.ShippingAddress.Salutation.EmptyNull(), - order.ShippingAddress.Title.HasValue() ? " " + order.ShippingAddress.Title : ""))); - } - else - { - tokens.Add(new Token("Order.ShippingFullSalutation", "")); - } - - tokens.Add(new Token("Order.ShippingSalutation", order.ShippingAddress != null ? order.ShippingAddress.Salutation : "")); - tokens.Add(new Token("Order.ShippingTitle", order.ShippingAddress != null ? order.ShippingAddress.Title : "")); - tokens.Add(new Token("Order.ShippingFirstName", order.ShippingAddress != null ? order.ShippingAddress.FirstName : "")); - tokens.Add(new Token("Order.ShippingLastName", order.ShippingAddress != null ? order.ShippingAddress.LastName : "")); - tokens.Add(new Token("Order.ShippingPhoneNumber", order.ShippingAddress != null ? order.ShippingAddress.PhoneNumber : "")); - tokens.Add(new Token("Order.ShippingEmail", order.ShippingAddress != null ? order.ShippingAddress.Email : "")); - tokens.Add(new Token("Order.ShippingFaxNumber", order.ShippingAddress != null ? order.ShippingAddress.FaxNumber : "")); - tokens.Add(new Token("Order.ShippingCompany", order.ShippingAddress != null ? order.ShippingAddress.Company : "")); - tokens.Add(new Token("Order.ShippingAddress1", order.ShippingAddress != null ? order.ShippingAddress.Address1 : "")); - tokens.Add(new Token("Order.ShippingAddress2", order.ShippingAddress != null ? order.ShippingAddress.Address2 : "")); - tokens.Add(new Token("Order.ShippingCity", order.ShippingAddress != null ? order.ShippingAddress.City : "")); - tokens.Add(new Token("Order.ShippingStateProvince", order.ShippingAddress != null && order.ShippingAddress.StateProvince != null ? order.ShippingAddress.StateProvince.GetLocalized(x => x.Name) : "")); - tokens.Add(new Token("Order.ShippingZipPostalCode", order.ShippingAddress != null ? order.ShippingAddress.ZipPostalCode : "")); - tokens.Add(new Token("Order.ShippingCountry", order.ShippingAddress != null && order.ShippingAddress.Country != null ? order.ShippingAddress.Country.GetLocalized(x => x.Name) : "")); - - string paymentMethodName = null; - var paymentMethod = _providerManager.GetProvider(order.PaymentMethodSystemName); - if (paymentMethod != null) - { - paymentMethodName = GetLocalizedValue(paymentMethod.Metadata, "FriendlyName", x => x.FriendlyName); - } - if (paymentMethodName.IsEmpty()) - { - paymentMethodName = order.PaymentMethodSystemName; - } - - tokens.Add(new Token("Order.PaymentMethod", paymentMethodName)); - tokens.Add(new Token("Order.VatNumber", order.VatNumber)); - tokens.Add(new Token("Order.Product(s)", ProductListToHtmlTable(order, language), true)); - tokens.Add(new Token("Order.CustomerComment", order.CustomerOrderComment, true)); - - if (language != null && !String.IsNullOrEmpty(language.LanguageCulture)) - { - DateTime createdOn = _services.DateTimeHelper.ConvertToUserTime(order.CreatedOnUtc, TimeZoneInfo.Utc, _services.DateTimeHelper.GetCustomerTimeZone(order.Customer)); - tokens.Add(new Token("Order.CreatedOn", createdOn.ToString("D", new CultureInfo(language.LanguageCulture)))); - } - else - { - tokens.Add(new Token("Order.CreatedOn", order.CreatedOnUtc.ToString("D"))); - } - - var orderDetailUrl = ""; - if (order.Customer != null && !order.Customer.IsGuest()) - { - // TODO add a method for getting URL (use routing because it handles all SEO friendly URLs) - orderDetailUrl = string.Format("{0}order/details/{1}", _services.WebHelper.GetStoreLocation(), order.Id); - } - - tokens.Add(new Token("Order.OrderURLForCustomer", orderDetailUrl, true)); - - tokens.Add(new Token("Order.Disclaimer", TopicToHtml("Disclaimer", language.Id), true)); - tokens.Add(new Token("Order.ConditionsOfUse", TopicToHtml("ConditionsOfUse", language.Id), true)); - tokens.Add(new Token("Order.AcceptThirdPartyEmailHandOver", GetBoolResource(order.AcceptThirdPartyEmailHandOver, language.Id))); - - //event notification - _services.EventPublisher.EntityTokensAdded(order, tokens); - } - - private string GetLocalizedValue(ProviderMetadata metadata, string propertyName, Expression> fallback) - { - // TODO: (mc) this actually belongs to PluginMediator, but we simply cannot add a dependency to framework from here. Refactor later! - - Guard.NotNull(metadata, nameof(metadata)); - - string systemName = metadata.SystemName; - var languageId = _services.WorkContext.WorkingLanguage.Id; - var resourceName = metadata.ResourceKeyPattern.FormatInvariant(metadata.SystemName, propertyName); - string result = _services.Localization.GetResource(resourceName, languageId, false, "", true); - - if (result.IsEmpty()) - result = fallback.Compile()(metadata); - - return result; - } - - public virtual void AddShipmentTokens(IList tokens, Shipment shipment, Language language) - { - tokens.Add(new Token("Shipment.ShipmentNumber", shipment.Id.ToString())); - tokens.Add(new Token("Shipment.TrackingNumber", shipment.TrackingNumber)); - tokens.Add(new Token("Shipment.Product(s)", ProductListToHtmlTable(shipment, language), true)); - tokens.Add(new Token("Shipment.URLForCustomer", string.Format("{0}order/shipmentdetails/{1}", _services.WebHelper.GetStoreLocation(), shipment.Id), true)); - - //event notification - _services.EventPublisher.EntityTokensAdded(shipment, tokens); - } - - public virtual void AddOrderNoteTokens(IList tokens, OrderNote orderNote) - { - tokens.Add(new Token("Order.NewNoteText", orderNote.FormatOrderNoteText(), true)); - - //event notification - _services.EventPublisher.EntityTokensAdded(orderNote, tokens); - } - - public virtual void AddRecurringPaymentTokens(IList tokens, RecurringPayment recurringPayment) - { - tokens.Add(new Token("RecurringPayment.ID", recurringPayment.Id.ToString())); - - //event notification - _services.EventPublisher.EntityTokensAdded(recurringPayment, tokens); - } - - public virtual void AddReturnRequestTokens(IList tokens, ReturnRequest returnRequest, OrderItem orderItem) - { - tokens.Add(new Token("ReturnRequest.ID", returnRequest.Id.ToString())); - tokens.Add(new Token("ReturnRequest.Product.Quantity", returnRequest.Quantity.ToString())); - tokens.Add(new Token("ReturnRequest.Product.Name", orderItem.Product.Name)); - tokens.Add(new Token("ReturnRequest.Reason", returnRequest.ReasonForReturn)); - tokens.Add(new Token("ReturnRequest.RequestedAction", returnRequest.RequestedAction)); - tokens.Add(new Token("ReturnRequest.CustomerComment", HtmlUtils.FormatText(returnRequest.CustomerComments, false, true, false, false, false, false), true)); - tokens.Add(new Token("ReturnRequest.StaffNotes", HtmlUtils.FormatText(returnRequest.StaffNotes, false, true, false, false, false, false), true)); - tokens.Add(new Token("ReturnRequest.Status", returnRequest.ReturnRequestStatus.GetLocalizedEnum(_services.Localization, _services.WorkContext))); - - //event notification - _services.EventPublisher.EntityTokensAdded(returnRequest, tokens); - } - - public virtual void AddGiftCardTokens(IList tokens, GiftCard giftCard) - { - var order = (giftCard.PurchasedWithOrderItem != null ? giftCard.PurchasedWithOrderItem.Order : null); - - if (order != null) - { - var remainingAmount = _currencyService.ConvertCurrency(giftCard.GetGiftCardRemainingAmount(), order.CurrencyRate); - - tokens.Add(new Token("GiftCard.RemainingAmount", _priceFormatter.FormatPrice(remainingAmount, true, false))); - } - else - { - tokens.Add(new Token("GiftCard.RemainingAmount", "")); - } - - tokens.Add(new Token("GiftCard.SenderName", giftCard.SenderName)); - tokens.Add(new Token("GiftCard.SenderEmail", giftCard.SenderEmail)); - tokens.Add(new Token("GiftCard.RecipientName", giftCard.RecipientName)); - tokens.Add(new Token("GiftCard.RecipientEmail", giftCard.RecipientEmail)); - tokens.Add(new Token("GiftCard.Amount", _priceFormatter.FormatPrice(giftCard.Amount, true, false))); - tokens.Add(new Token("GiftCard.CouponCode", giftCard.GiftCardCouponCode)); - - var giftCardMesage = !String.IsNullOrWhiteSpace(giftCard.Message) ? - HtmlUtils.FormatText(giftCard.Message, false, true, false, false, false, false) : ""; - - tokens.Add(new Token("GiftCard.Message", giftCardMesage, true)); - - //event notification - _services.EventPublisher.EntityTokensAdded(giftCard, tokens); - } - - public virtual void AddCustomerTokens(IList tokens, Customer customer) - { - tokens.Add(new Token("Customer.ID", customer.Id.ToString())); - tokens.Add(new Token("Customer.Email", customer.Email)); - tokens.Add(new Token("Customer.Username", customer.Username)); - tokens.Add(new Token("Customer.FullName", customer.GetFullName())); - tokens.Add(new Token("Customer.VatNumber", customer.GetAttribute(SystemCustomerAttributeNames.VatNumber))); - tokens.Add(new Token("Customer.VatNumberStatus", ((VatNumberStatus)customer.GetAttribute(SystemCustomerAttributeNames.VatNumberStatusId)).ToString())); - tokens.Add(new Token("Customer.CustomerNumber", customer.GetAttribute(SystemCustomerAttributeNames.CustomerNumber))); - - //note: we do not use SEO friendly URLS because we can get errors caused by having .(dot) in the URL (from the emauk address) - //TODO add a method for getting URL (use routing because it handles all SEO friendly URLs) - string passwordRecoveryUrl = string.Format("{0}customer/passwordrecoveryconfirm?token={1}&email={2}", _services.WebHelper.GetStoreLocation(), - customer.GetAttribute(SystemCustomerAttributeNames.PasswordRecoveryToken), HttpUtility.UrlEncode(customer.Email)); - - string accountActivationUrl = string.Format("{0}customer/activation?token={1}&email={2}", _services.WebHelper.GetStoreLocation(), - customer.GetAttribute(SystemCustomerAttributeNames.AccountActivationToken), HttpUtility.UrlEncode(customer.Email)); - - var wishlistUrl = string.Format("{0}wishlist/{1}", _services.WebHelper.GetStoreLocation(), customer.CustomerGuid); - tokens.Add(new Token("Customer.PasswordRecoveryURL", passwordRecoveryUrl, true)); - tokens.Add(new Token("Customer.AccountActivationURL", accountActivationUrl, true)); - tokens.Add(new Token("Wishlist.URLForCustomer", wishlistUrl, true)); - - //event notification - _services.EventPublisher.EntityTokensAdded(customer, tokens); - } - - public virtual void AddNewsLetterSubscriptionTokens(IList tokens, NewsLetterSubscription subscription) - { - tokens.Add(new Token("NewsLetterSubscription.Email", subscription.Email)); - - var activationUrl = GetRouteUrl("NewsletterActivation", new { token = subscription.NewsLetterSubscriptionGuid, active = true }); - tokens.Add(new Token("NewsLetterSubscription.ActivationUrl", activationUrl, true)); - - var deactivationUrl = GetRouteUrl("NewsletterActivation", new { token = subscription.NewsLetterSubscriptionGuid, active = false }); - tokens.Add(new Token("NewsLetterSubscription.DeactivationUrl", deactivationUrl, true)); - - //event notification - _services.EventPublisher.EntityTokensAdded(subscription, tokens); - } - - public virtual void AddProductReviewTokens(IList tokens, ProductReview productReview) - { - tokens.Add(new Token("ProductReview.ProductName", productReview.Product.Name)); - - //event notification - _services.EventPublisher.EntityTokensAdded(productReview, tokens); - } - - public virtual void AddBlogCommentTokens(IList tokens, BlogComment blogComment) - { - tokens.Add(new Token("BlogComment.BlogPostTitle", blogComment.BlogPost.Title)); - - //event notification - _services.EventPublisher.EntityTokensAdded(blogComment, tokens); - } - - public virtual void AddNewsCommentTokens(IList tokens, NewsComment newsComment) - { - tokens.Add(new Token("NewsComment.NewsTitle", newsComment.NewsItem.Title)); - - //event notification - _services.EventPublisher.EntityTokensAdded(newsComment, tokens); - } - - public virtual void AddProductTokens(IList tokens, Product product, Language language) - { - var storeLocation = _services.WebHelper.GetStoreLocation(); - var productUrl = GetRouteUrl("Product", new { SeName = product.GetSeName() }); - var productName = product.GetLocalized(x => x.Name, language.Id); - - tokens.Add(new Token("Product.ID", product.Id.ToString())); - tokens.Add(new Token("Product.Sku", product.Sku)); - tokens.Add(new Token("Product.Name", productName)); - tokens.Add(new Token("Product.ShortDescription", product.GetLocalized(x => x.ShortDescription, language.Id), true)); - tokens.Add(new Token("Product.StockQuantity", product.StockQuantity.ToString())); - tokens.Add(new Token("Product.ProductURLForCustomer", productUrl, true)); - - var currency = _services.WorkContext.WorkingCurrency; - - var additionalShippingCharge = _currencyService.ConvertFromPrimaryStoreCurrency(product.AdditionalShippingCharge, currency); - var additionalShippingChargeFormatted = _priceFormatter.FormatPrice(additionalShippingCharge, false, currency.CurrencyCode, false, language); - - tokens.Add(new Token("Product.AdditionalShippingCharge", additionalShippingChargeFormatted)); - - if (_mediaSettings.MessageProductThumbPictureSize > 0) - { - var pictureHtml = ProductPictureToHtml(GetPictureFor(product, null), language, productName, productUrl, storeLocation); - - tokens.Add(new Token("Product.Thumbnail", pictureHtml, true)); - } - - //event notification - _services.EventPublisher.EntityTokensAdded(product, tokens); - } - - public virtual void AddForumTopicTokens( - IList tokens, - ForumTopic forumTopic, - int? friendlyForumTopicPageIndex = null, - int? appendedPostIdentifierAnchor = null) - { - string topicUrl = null; - - if (friendlyForumTopicPageIndex.HasValue && friendlyForumTopicPageIndex.Value > 1) - { - topicUrl = GetRouteUrl("TopicSlugPaged", new { id = forumTopic.Id, slug = forumTopic.GetSeName(), page = friendlyForumTopicPageIndex.Value }); - } - else - { - topicUrl = GetRouteUrl("TopicSlug", new { id = forumTopic.Id, slug = forumTopic.GetSeName() }); - } - - if (appendedPostIdentifierAnchor.HasValue && appendedPostIdentifierAnchor.Value > 0) - { - topicUrl = string.Format("{0}#{1}", topicUrl, appendedPostIdentifierAnchor.Value); - - } - - tokens.Add(new Token("Forums.TopicURL", topicUrl, true)); - tokens.Add(new Token("Forums.TopicName", forumTopic.Subject)); - - //event notification - _services.EventPublisher.EntityTokensAdded(forumTopic, tokens); - } - - public virtual void AddForumPostTokens(IList tokens, ForumPost forumPost) - { - tokens.Add(new Token("Forums.PostAuthor", forumPost.Customer.FormatUserName())); - tokens.Add(new Token("Forums.PostBody", forumPost.FormatPostText(), true)); - - //event notification - _services.EventPublisher.EntityTokensAdded(forumPost, tokens); - } - - public virtual void AddForumTokens(IList tokens, Forum forum, Language language) - { - var forumUrl = GetRouteUrl("ForumSlug", new { id = forum.Id, slug = forum.GetSeName(language.Id) }); - - tokens.Add(new Token("Forums.ForumURL", forumUrl, true)); - tokens.Add(new Token("Forums.ForumName", forum.GetLocalized(x => x.Name, language.Id))); - - //event notification - _services.EventPublisher.EntityTokensAdded(forum, tokens); - } - - public virtual void AddPrivateMessageTokens(IList tokens, PrivateMessage privateMessage) - { - tokens.Add(new Token("PrivateMessage.Subject", privateMessage.Subject)); - tokens.Add(new Token("PrivateMessage.Text", privateMessage.FormatPrivateMessageText(), true)); - - //event notification - _services.EventPublisher.EntityTokensAdded(privateMessage, tokens); - } - - public virtual void AddBackInStockTokens(IList tokens, BackInStockSubscription subscription) - { - var customerLangId = subscription.Customer.GetAttribute( - SystemCustomerAttributeNames.LanguageId, - _genericAttributeService, - _services.StoreContext.CurrentStore.Id); - - var store = _services.StoreService.GetStoreById(subscription.StoreId); - var productLink = "{0}{1}".FormatWith(store.Url, subscription.Product.GetSeName(customerLangId, _urlRecordService, _languageService)); - - tokens.Add(new Token("BackInStockSubscription.ProductName", "{1}".FormatWith(productLink, subscription.Product.Name), true)); - - //event notification - _services.EventPublisher.EntityTokensAdded(subscription, tokens); - } - - /// - /// Gets list of allowed (supported) message tokens for campaigns - /// - /// List of allowed (supported) message tokens for campaigns - public virtual string[] GetListOfCampaignAllowedTokens() - { - var allowedTokens = new List() - { - "%Store.Name%", - "%Store.URL%", - "%Store.Email%", - "%NewsLetterSubscription.Email%", - "%NewsLetterSubscription.ActivationUrl%", - "%NewsLetterSubscription.DeactivationUrl%", - "%Store.SupplierIdentification%", - }; - return allowedTokens.ToArray(); - } - - public virtual string[] GetListOfAllowedTokens() - { - var allowedTokens = new List() - { - "%Store.Name%", - "%Store.URL%", - "%Store.Email%", - "%Order.OrderNumber%", - "%Order.CustomerFullName%", - "%Order.CustomerEmail%", - "%Order.BillingFullSalutation%", - "%Order.BillingFirstName%", - "%Order.BillingLastName%", - "%Order.BillingPhoneNumber%", - "%Order.BillingEmail%", - "%Order.BillingFaxNumber%", - "%Order.BillingCompany%", - "%Order.BillingAddress1%", - "%Order.BillingAddress2%", - "%Order.BillingCity%", - "%Order.BillingStateProvince%", - "%Order.BillingZipPostalCode%", - "%Order.BillingCountry%", - "%Order.ShippingMethod%", - "%Order.ShippingFullSalutation%", - "%Order.ShippingFirstName%", - "%Order.ShippingLastName%", - "%Order.ShippingPhoneNumber%", - "%Order.ShippingEmail%", - "%Order.ShippingFaxNumber%", - "%Order.ShippingCompany%", - "%Order.ShippingAddress1%", - "%Order.ShippingAddress2%", - "%Order.ShippingCity%", - "%Order.ShippingStateProvince%", - "%Order.ShippingZipPostalCode%", - "%Order.ShippingCountry%", - "%Order.PaymentMethod%", - "%Order.VatNumber%", - "%Order.CustomerComment%", - "%Order.Product(s)%", - "%Order.CreatedOn%", - "%Order.OrderURLForCustomer%", - "%Order.NewNoteText%", - "%Product.ID%", - "%Product.Sku%", - "%Product.Name%", - "%Product.ShortDescription%", - "%Product.ProductURLForCustomer%", - "%Product.StockQuantity%", - "%Product.AdditionalShippingCharge%", - "%Product.Thumbnail%", - "%RecurringPayment.ID%", - "%Shipment.ShipmentNumber%", - "%Shipment.TrackingNumber%", - "%Shipment.Product(s)%", - "%Shipment.URLForCustomer%", - "%ReturnRequest.ID%", - "%ReturnRequest.Product.Quantity%", - "%ReturnRequest.Product.Name%", - "%ReturnRequest.Reason%", - "%ReturnRequest.RequestedAction%", - "%ReturnRequest.CustomerComment%", - "%ReturnRequest.StaffNotes%", - "%ReturnRequest.Status%", - "%GiftCard.SenderName%", - "%GiftCard.SenderEmail%", - "%GiftCard.RecipientName%", - "%GiftCard.RecipientEmail%", - "%GiftCard.Amount%", - "%GiftCard.RemainingAmount%", - "%GiftCard.CouponCode%", - "%GiftCard.Message%", - "%Customer.Email%", - "%Customer.Username%", - "%Customer.FullName%", - "%Customer.VatNumber%", - "%Customer.VatNumberStatus%", - "%Customer.CustomerNumber%", - "%Customer.PasswordRecoveryURL%", - "%Customer.AccountActivationURL%", - "%Wishlist.URLForCustomer%", - "%NewsLetterSubscription.Email%", - "%NewsLetterSubscription.ActivationUrl%", - "%NewsLetterSubscription.DeactivationUrl%", - "%ProductReview.ProductName%", - "%BlogComment.BlogPostTitle%", - "%NewsComment.NewsTitle%", - "%Forums.TopicURL%", - "%Forums.TopicName%", - "%Forums.PostAuthor%", - "%Forums.PostBody%", - "%Forums.ForumURL%", - "%Forums.ForumName%", - "%PrivateMessage.Subject%", - "%PrivateMessage.Text%", - "%BackInStockSubscription.ProductName%", - "%Order.Disclaimer%", - "%Order.ConditionsOfUse%", - "%Order.AcceptThirdPartyEmailHandOver%", - "%Company.CompanyName%", - "%Company.Salutation%", - "%Company.Title%", - "%Company.Firstname%", - "%Company.Lastname%", - "%Company.CompanyManagementDescription%", - "%Company.CompanyManagement%", - "%Company.Street%", - "%Company.Street2%", - "%Company.ZipCode%", - "%Company.City%", - "%Company.CountryName%", - "%Company.Region%", - "%Company.VatId%", - "%Company.CommercialRegister%", - "%Company.TaxNumber%", - "%Bank.Bankname%", - "%Bank.Bankcode%", - "%Bank.AccountNumber%", - "%Bank.AccountHolder%", - "%Bank.Iban%", - "%Bank.Bic%", - "%Contact.CompanyTelephoneNumber%", - "%Contact.HotlineTelephoneNumber%", - "%Contact.MobileTelephoneNumber%", - "%Contact.CompanyFaxNumber%", - "%Contact.CompanyEmailAddress%", - "%Contact.WebmasterEmailAddress%", - "%Contact.SupportEmailAddress%", - "%Contact.ContactEmailAddress%", - "%Store.SupplierIdentification%", - - }; - return allowedTokens.ToArray(); - } - - public virtual TreeNode GetTreeOfCampaignAllowedTokens() - { - var tokensTree = new TreeNode("_ROOT_"); - FillTokensTree(tokensTree, GetListOfCampaignAllowedTokens()); - return tokensTree; - } - - public virtual TreeNode GetTreeOfAllowedTokens() - { - var tokensTree = new TreeNode("_ROOT_"); - FillTokensTree(tokensTree, GetListOfAllowedTokens()); - return tokensTree; - } - - private void FillTokensTree(TreeNode root, string[] tokens) - { - root.Clear(); - - for (int i = 0; i < tokens.Length; i++) - { - // remove '%' - string token = tokens[i].Trim('%'); - // split 'Order.ID' to [ Order, ID ] parts - var parts = token.Split('.'); - - var node = root; - // iterate parts - foreach (var part in parts) - { - var found = node.SelectNode(x => x.Value == part); - if (found == null) - { - node = node.Append(part); - } - else - { - node = found; - } - } - } - } - - #endregion - } -} diff --git a/src/Libraries/SmartStore.Services/Messages/NewsLetterSubscriptionService.cs b/src/Libraries/SmartStore.Services/Messages/NewsLetterSubscriptionService.cs index dde1d076c9..58a8d28d2f 100644 --- a/src/Libraries/SmartStore.Services/Messages/NewsLetterSubscriptionService.cs +++ b/src/Libraries/SmartStore.Services/Messages/NewsLetterSubscriptionService.cs @@ -50,9 +50,6 @@ public void InsertNewsLetterSubscription(NewsLetterSubscription newsLetterSubscr { PublishSubscriptionEvent(newsLetterSubscription.Email, true, publishSubscriptionEvents); } - - //Publish event - _eventPublisher.EntityInserted(newsLetterSubscription); } /// @@ -101,9 +98,6 @@ public void UpdateNewsLetterSubscription(NewsLetterSubscription newsLetterSubscr //If the previous entry was true, but this one is false PublishSubscriptionEvent(originalSubscription.Email, false, publishSubscriptionEvents); } - - //Publish event - _eventPublisher.EntityUpdated(newsLetterSubscription); } /// @@ -120,9 +114,6 @@ public virtual void DeleteNewsLetterSubscription(NewsLetterSubscription newsLett //Publish the unsubscribe event PublishSubscriptionEvent(newsLetterSubscription.Email, false, publishSubscriptionEvents); - - //event notification - _eventPublisher.EntityDeleted(newsLetterSubscription); } public virtual bool? AddNewsLetterSubscriptionFor(bool add, string email, int storeId) @@ -265,12 +256,12 @@ private void PublishSubscriptionEvent(string email, bool isSubscribe, bool publi { if (isSubscribe) { - _eventPublisher.PublishNewsletterSubscribe(email); - } + _eventPublisher.Publish(new EmailSubscribedEvent(email)); + } else { - _eventPublisher.PublishNewsletterUnsubscribe(email); - } + _eventPublisher.Publish(new EmailUnsubscribedEvent(email)); + } } } diff --git a/src/Libraries/SmartStore.Services/Messages/QueuedEmailService.cs b/src/Libraries/SmartStore.Services/Messages/QueuedEmailService.cs index 80e29ca1b8..8b7ebd258c 100644 --- a/src/Libraries/SmartStore.Services/Messages/QueuedEmailService.cs +++ b/src/Libraries/SmartStore.Services/Messages/QueuedEmailService.cs @@ -93,9 +93,6 @@ public virtual void InsertQueuedEmail(QueuedEmail queuedEmail) } } } - - // event notification - _services.EventPublisher.EntityInserted(queuedEmail); } public virtual void UpdateQueuedEmail(QueuedEmail queuedEmail) @@ -103,9 +100,6 @@ public virtual void UpdateQueuedEmail(QueuedEmail queuedEmail) Guard.NotNull(queuedEmail, nameof(queuedEmail)); _queuedEmailRepository.Update(queuedEmail); - - // event notification - _services.EventPublisher.EntityUpdated(queuedEmail); } public virtual void DeleteQueuedEmail(QueuedEmail queuedEmail) @@ -113,9 +107,6 @@ public virtual void DeleteQueuedEmail(QueuedEmail queuedEmail) Guard.NotNull(queuedEmail, nameof(queuedEmail)); _queuedEmailRepository.Delete(queuedEmail); - - // event notification - _services.EventPublisher.EntityDeleted(queuedEmail); } public virtual int DeleteAllQueuedEmails() @@ -234,14 +225,14 @@ internal EmailMessage ConvertEmail(QueuedEmail qe) // 'internal' for testing purposes var msg = new EmailMessage( - new EmailAddress(qe.To, qe.ToName), - qe.Subject, + new EmailAddress(qe.To), + qe.Subject.Replace("\r\n", string.Empty), qe.Body, - new EmailAddress(qe.From, qe.FromName)); + new EmailAddress(qe.From)); if (qe.ReplyTo.HasValue()) { - msg.ReplyTo.Add(new EmailAddress(qe.ReplyTo, qe.ReplyToName)); + msg.ReplyTo.Add(new EmailAddress(qe.ReplyTo)); } AddEmailAddresses(qe.CC, msg.Cc); @@ -321,8 +312,6 @@ public virtual void DeleteQueuedEmailAttachment(QueuedEmailAttachment attachment } _queuedEmailAttachmentRepository.Delete(attachment); - - _services.EventPublisher.EntityDeleted(attachment); } public virtual byte[] LoadQueuedEmailAttachmentBinary(QueuedEmailAttachment attachment) diff --git a/src/Libraries/SmartStore.Services/Messages/QueuedMessagesSendTask.cs b/src/Libraries/SmartStore.Services/Messages/QueuedMessagesSendTask.cs index a37e7e82c4..0c3e7080a6 100644 --- a/src/Libraries/SmartStore.Services/Messages/QueuedMessagesSendTask.cs +++ b/src/Libraries/SmartStore.Services/Messages/QueuedMessagesSendTask.cs @@ -27,7 +27,8 @@ public void Execute(TaskExecutionContext ctx) PageIndex = i, PageSize = pageSize, Expand = "Attachments", - UnsentOnly = true + UnsentOnly = true, + SendManually = false }; var queuedEmails = _queuedEmailService.SearchEmails(q); diff --git a/src/Libraries/SmartStore.Services/Messages/QueuingEmailEvent.cs b/src/Libraries/SmartStore.Services/Messages/QueuingEmailEvent.cs deleted file mode 100644 index d9efdacd84..0000000000 --- a/src/Libraries/SmartStore.Services/Messages/QueuingEmailEvent.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Generic; -using SmartStore.Core.Domain.Messages; - -namespace SmartStore.Services.Messages -{ - /// - /// An event message, which gets published just before a new instance - /// of is persisted to the database - /// - public class QueuingEmailEvent - { - public QueuedEmail QueuedEmail - { - get; - set; - } - - public MessageTemplate MessageTemplate - { - get; - set; - } - - public EmailAccount EmailAccount - { - get; - set; - } - - public IList Tokens - { - get; - set; - } - - public int LanguageId - { - get; - set; - } - } -} diff --git a/src/Libraries/SmartStore.Services/Messages/QueuingEmailEventConsumer.cs b/src/Libraries/SmartStore.Services/Messages/QueuingEmailEventConsumer.cs deleted file mode 100644 index bf7edb0deb..0000000000 --- a/src/Libraries/SmartStore.Services/Messages/QueuingEmailEventConsumer.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; -using System.Web.Mvc; -using SmartStore.Core.Domain.Common; -using SmartStore.Core.Domain.Media; -using SmartStore.Core.Domain.Messages; -using SmartStore.Core.Events; -using SmartStore.Core.Localization; -using SmartStore.Core.Logging; -using SmartStore.Utilities; - -namespace SmartStore.Services.Messages -{ - public class QueuingEmailEventConsumer : IConsumer - { - private readonly PdfSettings _pdfSettings; - private readonly HttpRequestBase _httpRequest; - private readonly Lazy _fileDownloadManager; - - public QueuingEmailEventConsumer( - PdfSettings pdfSettings, - HttpRequestBase httpRequest, - Lazy fileDownloadManager) - { - this._pdfSettings = pdfSettings; - this._httpRequest = httpRequest; - this._fileDownloadManager = fileDownloadManager; - - Logger = NullLogger.Instance; - T = NullLocalizer.Instance; - } - - public ILogger Logger { get; set; } - public Localizer T { get; set; } - - public void HandleEvent(QueuingEmailEvent eventMessage) - { - var qe = eventMessage.QueuedEmail; - var tpl = eventMessage.MessageTemplate; - - var handledTemplates = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { "OrderPlaced.CustomerNotification", _pdfSettings.AttachOrderPdfToOrderPlacedEmail }, - { "OrderCompleted.CustomerNotification", _pdfSettings.AttachOrderPdfToOrderCompletedEmail } - }; - - var shouldHandle = false; - if (handledTemplates.TryGetValue(tpl.Name, out shouldHandle) && shouldHandle) - { - var orderId = eventMessage.Tokens.First(x => x.Key.IsCaseInsensitiveEqual("Order.ID")).Value.ToInt(); - try - { - var qea = CreatePdfInvoiceAttachment(orderId); - qe.Attachments.Add(qea); - } - catch (Exception ex) - { - Logger.Error(ex, T("Admin.System.QueuedEmails.ErrorCreatingAttachment")); - } - } - } - - private QueuedEmailAttachment CreatePdfInvoiceAttachment(int orderId) - { - var urlHelper = new UrlHelper(_httpRequest.RequestContext); - var path = urlHelper.Action("Print", "Order", new { id = orderId, pdf = true, area = "" }); - - var fileResponse = _fileDownloadManager.Value.DownloadFile(path, true, 5000); - - if (fileResponse == null) - { - throw new InvalidOperationException(T("Admin.System.QueuedEmails.ErrorEmptyAttachmentResult", path)); - } - - if (!fileResponse.ContentType.IsCaseInsensitiveEqual("application/pdf")) - { - throw new InvalidOperationException(T("Admin.System.QueuedEmails.ErrorNoPdfAttachment")); - } - - return new QueuedEmailAttachment - { - StorageLocation = EmailAttachmentStorageLocation.Blob, - MediaStorage = new MediaStorage { Data = fileResponse.Data }, - MimeType = fileResponse.ContentType, - Name = fileResponse.FileName - }; - } - - } -} diff --git a/src/Libraries/SmartStore.Services/Messages/TemplateModel.cs b/src/Libraries/SmartStore.Services/Messages/TemplateModel.cs new file mode 100644 index 0000000000..61037c1e01 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Messages/TemplateModel.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using SmartStore.ComponentModel; + +namespace SmartStore.Services.Messages +{ + public class TemplateModel : HybridExpando + { + public T GetFromBag(string key) + { + Guard.NotEmpty(key, nameof(key)); + + if (base.Contains("Bag") && base["Bag"] is IDictionary bag) + { + if (bag.TryGetValue(key, out var value) && value is T result) + { + return result; + } + } + + return default(T); + } + } +} diff --git a/src/Libraries/SmartStore.Services/Messages/Token.cs b/src/Libraries/SmartStore.Services/Messages/Token.cs deleted file mode 100644 index 72e85c5744..0000000000 --- a/src/Libraries/SmartStore.Services/Messages/Token.cs +++ /dev/null @@ -1,40 +0,0 @@ - -namespace SmartStore.Services.Messages -{ - public sealed class Token - { - private readonly string _key; - private readonly string _value; - private readonly bool _neverHtmlEncoded; - - public Token(string key, string value): - this(key, value, false) - { - - } - public Token(string key, string value, bool neverHtmlEncoded) - { - this._key = key; - this._value = value; - this._neverHtmlEncoded = neverHtmlEncoded; - } - - /// - /// Token key - /// - public string Key { get { return _key; } } - /// - /// Token value - /// - public string Value { get { return _value; } } - /// - /// Indicates whether this token should not be HTML encoded - /// - public bool NeverHtmlEncoded { get { return _neverHtmlEncoded; } } - - public override string ToString() - { - return string.Format("{0}: {1}", Key, Value); - } - } -} diff --git a/src/Libraries/SmartStore.Services/Messages/Tokenizer.cs b/src/Libraries/SmartStore.Services/Messages/Tokenizer.cs deleted file mode 100644 index dcc031791c..0000000000 --- a/src/Libraries/SmartStore.Services/Messages/Tokenizer.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Web; -using SmartStore.Core.Domain.Messages; -using SmartStore.Core.Infrastructure; - -namespace SmartStore.Services.Messages -{ - public partial class Tokenizer : ITokenizer - { - private readonly StringComparison _stringComparison; - - /// - /// Ctor - /// - /// Message templates settings - public Tokenizer(MessageTemplatesSettings settings) - { - _stringComparison = settings.CaseInvariantReplacement ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; - } - - /// - /// Replace all of the token key occurences inside the specified template text with corresponded token values - /// - /// The template with token keys inside - /// The sequence of tokens to use - /// The value indicating whether tokens should be HTML encoded - /// Text with all token keys replaces by token value - public string Replace(string template, IEnumerable tokens, bool htmlEncode) - { - if (string.IsNullOrWhiteSpace(template)) - throw new ArgumentNullException("template"); - - if (tokens == null) - throw new ArgumentNullException("tokens"); - - foreach (var token in tokens) - { - string tokenValue = token.Value; - //do not encode URLs - if (htmlEncode && !token.NeverHtmlEncoded) - tokenValue = HttpUtility.HtmlEncode(tokenValue); - template = Replace(template, String.Format(@"%{0}%", token.Key), tokenValue); - } - return template; - - } - - private string Replace(string original, string pattern, string replacement) - { - if (_stringComparison == StringComparison.Ordinal) - { - return original.Replace(pattern, replacement); - } - else - { - int count, position0, position1; - count = position0 = position1 = 0; - int inc = (original.Length / pattern.Length) * (replacement.Length - pattern.Length); - char[] chars = new char[original.Length + Math.Max(0, inc)]; - while ((position1 = original.IndexOf(pattern, position0, _stringComparison)) != -1) - { - for (int i = position0; i < position1; ++i) - chars[count++] = original[i]; - for (int i = 0; i < replacement.Length; ++i) - chars[count++] = replacement[i]; - position0 = position1 + pattern.Length; - } - if (position0 == 0) return original; - for (int i = position0; i < original.Length; ++i) - chars[count++] = original[i]; - return new string(chars, 0, count); - } - } - - } -} diff --git a/src/Libraries/SmartStore.Services/Messages/WorkflowMessageService.cs b/src/Libraries/SmartStore.Services/Messages/WorkflowMessageService.cs deleted file mode 100644 index 189cb1ce30..0000000000 --- a/src/Libraries/SmartStore.Services/Messages/WorkflowMessageService.cs +++ /dev/null @@ -1,1428 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; -using SmartStore.Core; -using SmartStore.Core.Domain.Blogs; -using SmartStore.Core.Domain.Catalog; -using SmartStore.Core.Domain.Common; -using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Forums; -using SmartStore.Core.Domain.Localization; -using SmartStore.Core.Domain.Messages; -using SmartStore.Core.Domain.News; -using SmartStore.Core.Domain.Orders; -using SmartStore.Core.Domain.Shipping; -using SmartStore.Core.Domain.Stores; -using SmartStore.Core.Events; -using SmartStore.Core.Localization; -using SmartStore.Services.Customers; -using SmartStore.Services.Localization; -using SmartStore.Services.Media; -using SmartStore.Services.Stores; - -namespace SmartStore.Services.Messages -{ - public partial class WorkflowMessageService : IWorkflowMessageService - { - #region Fields - - private readonly IMessageTemplateService _messageTemplateService; - private readonly IQueuedEmailService _queuedEmailService; - private readonly ILanguageService _languageService; - private readonly ITokenizer _tokenizer; - private readonly IEmailAccountService _emailAccountService; - private readonly IMessageTokenProvider _messageTokenProvider; - private readonly IStoreService _storeService; - private readonly IStoreContext _storeContext; - private readonly EmailAccountSettings _emailAccountSettings; - private readonly IEventPublisher _eventPublisher; - private readonly IWorkContext _workContext; - private readonly HttpRequestBase _httpRequest; - private readonly IDownloadService _downloadService; - - #endregion - - #region Ctor - - public WorkflowMessageService( - IMessageTemplateService messageTemplateService, - IQueuedEmailService queuedEmailService, - ILanguageService languageService, - ITokenizer tokenizer, - IEmailAccountService emailAccountService, - IMessageTokenProvider messageTokenProvider, - IStoreService storeService, - IStoreContext storeContext, - EmailAccountSettings emailAccountSettings, - IEventPublisher eventPublisher, - IWorkContext workContext, - HttpRequestBase httpRequest, - IDownloadService downloadService) - { - this._messageTemplateService = messageTemplateService; - this._queuedEmailService = queuedEmailService; - this._languageService = languageService; - this._tokenizer = tokenizer; - this._emailAccountService = emailAccountService; - this._messageTokenProvider = messageTokenProvider; - this._storeService = storeService; - this._storeContext = storeContext; - this._emailAccountSettings = emailAccountSettings; - this._eventPublisher = eventPublisher; - this._workContext = workContext; - this._httpRequest = httpRequest; - this._downloadService = downloadService; - - T = NullLocalizer.Instance; - } - - public Localizer T { get; set; } - - #endregion - - #region Utilities - - protected int SendNotification( - MessageTemplate messageTemplate, - EmailAccount emailAccount, - int languageId, - IList tokens, - string toEmailAddress, - string toName, - string replyTo = null, - string replyToName = null) - { - // retrieve localized message template data - var bcc = messageTemplate.GetLocalized((mt) => mt.BccEmailAddresses, languageId); - var subject = messageTemplate.GetLocalized((mt) => mt.Subject, languageId); - var body = messageTemplate.GetLocalized((mt) => mt.Body, languageId); - - // Replace subject and body tokens - var subjectReplaced = _tokenizer.Replace(subject, tokens, false); - var bodyReplaced = _tokenizer.Replace(body, tokens, true); - - bodyReplaced = WebHelper.MakeAllUrlsAbsolute(bodyReplaced, _httpRequest); - - var email = new QueuedEmail - { - Priority = 5, - From = emailAccount.Email, - FromName = emailAccount.DisplayName, - To = toEmailAddress, - ToName = toName, - CC = string.Empty, - Bcc = bcc, - ReplyTo = replyTo, - ReplyToName = replyToName, - Subject = subjectReplaced, - Body = bodyReplaced, - CreatedOnUtc = DateTime.UtcNow, - EmailAccountId = emailAccount.Id, - SendManually = messageTemplate.SendManually - }; - - // create attachments if any - var fileIds = (new int?[] - { - messageTemplate.GetLocalized(x => x.Attachment1FileId, languageId), - messageTemplate.GetLocalized(x => x.Attachment2FileId, languageId), - messageTemplate.GetLocalized(x => x.Attachment3FileId, languageId) - }) - .Where(x => x.HasValue) - .Select(x => x.Value) - .ToArray(); - - if (fileIds.Any()) - { - var files = _downloadService.GetDownloadsByIds(fileIds); - foreach (var file in files) - { - email.Attachments.Add(new QueuedEmailAttachment - { - StorageLocation = EmailAttachmentStorageLocation.FileReference, - FileId = file.Id, - Name = (file.Filename.NullEmpty() ?? file.Id.ToString()) + file.Extension.EmptyNull(), - MimeType = file.ContentType.NullEmpty() ?? "application/octet-stream" - }); - } - } - - - // publish event so that integrators can add attachments, alter the email etc. - _eventPublisher.Publish(new QueuingEmailEvent - { - EmailAccount = emailAccount, - LanguageId = languageId, - MessageTemplate = messageTemplate, - QueuedEmail = email, - Tokens = tokens - }); - - _queuedEmailService.InsertQueuedEmail(email); - - return email.Id; - } - - protected MessageTemplate GetActiveMessageTemplate(string messageTemplateName, int storeId) - { - var messageTemplate = _messageTemplateService.GetMessageTemplateByName(messageTemplateName, storeId); - - if (messageTemplate == null) - return null; - - //ensure it's active - var isActive = messageTemplate.IsActive; - if (!isActive) - return null; - - return messageTemplate; - } - - protected EmailAccount GetEmailAccountOfMessageTemplate(MessageTemplate messageTemplate, int languageId) - { - var emailAccounId = messageTemplate.GetLocalized(mt => mt.EmailAccountId, languageId); - var emailAccount = _emailAccountService.GetEmailAccountById(emailAccounId); - if (emailAccount == null) - emailAccount = _emailAccountService.GetDefaultEmailAccount(); - - return emailAccount; - } - - private Tuple GetReplyToEmail(Customer customer) - { - if (customer == null || customer.Email.IsEmpty()) - return new Tuple(null, null); - - string email = customer.Email; - string name = GetDisplayNameForCustomer(customer); - - return new Tuple(email, name); - } - - private string GetDisplayNameForCustomer(Customer customer) - { - if (customer == null) - return string.Empty; - - Func getName = (address) => { - if (address == null) - return null; - - string result = string.Empty; - if (address.FirstName.HasValue() || address.LastName.HasValue()) - { - result = string.Format("{0} {1}", address.FirstName, address.LastName).Trim(); - } - - if (address.Company.HasValue()) - { - result = string.Concat(result, result.HasValue() ? ", " : "", address.Company); - } - - return result; - }; - - string name = getName(customer.BillingAddress); - if (name.IsEmpty()) - { - name = getName(customer.ShippingAddress); - } - if (name.IsEmpty()) - { - name = getName(customer.Addresses.FirstOrDefault()); - } - - name = name.TrimSafe().NullEmpty(); - - return name ?? customer.Username.EmptyNull(); - } - - protected Language EnsureLanguageIsActive(int languageId, int storeId) - { - //load language by specified ID - var language = _languageService.GetLanguageById(languageId); - - if (language == null || !language.Published) - { - //load any language from the specified store - language = _languageService.GetAllLanguages(storeId: storeId).FirstOrDefault(); - } - if (language == null || !language.Published) - { - //load any language - language = _languageService.GetAllLanguages().FirstOrDefault(); - } - - if (language == null) - throw new SmartException(T("Common.Error.NoActiveLanguage")); - - return language; - } - - #endregion - - #region Methods - - #region Customer workflow - - /// - /// Sends 'New customer' notification message to a store owner - /// - /// Customer instance - /// Message language identifier - /// Queued email identifier - public virtual int SendCustomerRegisteredNotificationMessage(Customer customer, int languageId) - { - if (customer == null) - throw new ArgumentNullException("customer"); - - var store = _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("NewCustomer.Notification", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddCustomerTokens(tokens, customer); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - - // use customer email as reply address - var replyTo = GetReplyToEmail(customer); - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, replyTo.Item1, replyTo.Item2); - } - - /// - /// Sends a welcome message to a customer - /// - /// Customer instance - /// Message language identifier - /// Queued email identifier - public virtual int SendCustomerWelcomeMessage(Customer customer, int languageId) - { - if (customer == null) - throw new ArgumentNullException("customer"); - - var store = _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("Customer.WelcomeMessage", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddCustomerTokens(tokens, customer); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = customer.Email; - var toName = customer.GetFullName(); - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); - } - - /// - /// Sends an email validation message to a customer - /// - /// Customer instance - /// Message language identifier - /// Queued email identifier - public virtual int SendCustomerEmailValidationMessage(Customer customer, int languageId) - { - if (customer == null) - throw new ArgumentNullException("customer"); - - var store = _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("Customer.EmailValidationMessage", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddCustomerTokens(tokens, customer); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = customer.Email; - var toName = customer.GetFullName(); - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); - } - - /// - /// Sends password recovery message to a customer - /// - /// Customer instance - /// Message language identifier - /// Queued email identifier - public virtual int SendCustomerPasswordRecoveryMessage(Customer customer, int languageId) - { - if (customer == null) - throw new ArgumentNullException("customer"); - - var store = _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("Customer.PasswordRecovery", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddCustomerTokens(tokens, customer); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = customer.Email; - var toName = customer.GetFullName(); - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); - } - - #endregion - - #region Order workflow - - /// - /// Sends an order placed notification to a store owner - /// - /// Order instance - /// Message language identifier - /// Queued email identifier - public virtual int SendOrderPlacedStoreOwnerNotification(Order order, int languageId) - { - if (order == null) - throw new ArgumentNullException("order"); - - var store = _storeService.GetStoreById(order.StoreId) ?? _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("OrderPlaced.StoreOwnerNotification", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddOrderTokens(tokens, order, language); - _messageTokenProvider.AddCustomerTokens(tokens, order.Customer); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - - // use buyer's email as reply address - var replyToEmail = order.BillingAddress.Email; - var replyToName = string.Format("{0} {1}", order.BillingAddress.FirstName, order.BillingAddress.LastName); - if (order.BillingAddress.Company.HasValue()) - { - replyToName += ", " + order.BillingAddress.Company; - } - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, replyToEmail, replyToName); - } - - /// - /// Sends an order placed notification to a customer - /// - /// Order instance - /// Message language identifier - /// Queued email identifier - public virtual int SendOrderPlacedCustomerNotification(Order order, int languageId) - { - if (order == null) - throw new ArgumentNullException("order"); - - var store = _storeService.GetStoreById(order.StoreId) ?? _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("OrderPlaced.CustomerNotification", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddOrderTokens(tokens, order, language); - _messageTokenProvider.AddCustomerTokens(tokens, order.Customer); - - _messageTokenProvider.AddCompanyTokens(tokens); - _messageTokenProvider.AddBankConnectionTokens(tokens); - _messageTokenProvider.AddContactDataTokens(tokens); - - // event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = order.BillingAddress.Email; - var toName = string.Format("{0} {1}", order.BillingAddress.FirstName, order.BillingAddress.LastName); - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); - } - - /// - /// Sends a shipment sent notification to a customer - /// - /// Shipment - /// Message language identifier - /// Queued email identifier - public virtual int SendShipmentSentCustomerNotification(Shipment shipment, int languageId) - { - if (shipment == null) - throw new ArgumentNullException("shipment"); - - var order = shipment.Order; - if (order == null) - throw new SmartException(T("Order.NotFound", shipment.OrderId)); - - var store = _storeService.GetStoreById(order.StoreId) ?? _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("ShipmentSent.CustomerNotification", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddShipmentTokens(tokens, shipment, language); - _messageTokenProvider.AddOrderTokens(tokens, shipment.Order, language); - _messageTokenProvider.AddCustomerTokens(tokens, shipment.Order.Customer); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = order.BillingAddress.Email; - var toName = string.Format("{0} {1}", order.BillingAddress.FirstName, order.BillingAddress.LastName); - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); - } - - /// - /// Sends a shipment delivered notification to a customer - /// - /// Shipment - /// Message language identifier - /// Queued email identifier - public virtual int SendShipmentDeliveredCustomerNotification(Shipment shipment, int languageId) - { - if (shipment == null) - throw new ArgumentNullException("shipment"); - - var order = shipment.Order; - if (order == null) - throw new SmartException(T("Order.NotFound", shipment.OrderId)); - - var store = _storeService.GetStoreById(order.StoreId) ?? _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("ShipmentDelivered.CustomerNotification", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddShipmentTokens(tokens, shipment, language); - _messageTokenProvider.AddOrderTokens(tokens, shipment.Order, language); - _messageTokenProvider.AddCustomerTokens(tokens, shipment.Order.Customer); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = order.BillingAddress.Email; - var toName = string.Format("{0} {1}", order.BillingAddress.FirstName, order.BillingAddress.LastName); - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); - } - - /// - /// Sends an order completed notification to a customer - /// - /// Order instance - /// Message language identifier - /// Queued email identifier - public virtual int SendOrderCompletedCustomerNotification(Order order, int languageId) - { - if (order == null) - throw new ArgumentNullException("order"); - - var store = _storeService.GetStoreById(order.StoreId) ?? _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("OrderCompleted.CustomerNotification", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddOrderTokens(tokens, order, language); - _messageTokenProvider.AddCustomerTokens(tokens, order.Customer); - - _messageTokenProvider.AddCompanyTokens(tokens); - _messageTokenProvider.AddBankConnectionTokens(tokens); - _messageTokenProvider.AddContactDataTokens(tokens); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = order.BillingAddress.Email; - var toName = string.Format("{0} {1}", order.BillingAddress.FirstName, order.BillingAddress.LastName); - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); - } - - /// - /// Sends an order cancelled notification to a customer - /// - /// Order instance - /// Message language identifier - /// Queued email identifier - public virtual int SendOrderCancelledCustomerNotification(Order order, int languageId) - { - if (order == null) - throw new ArgumentNullException("order"); - - var store = _storeService.GetStoreById(order.StoreId) ?? _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("OrderCancelled.CustomerNotification", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddOrderTokens(tokens, order, language); - _messageTokenProvider.AddCustomerTokens(tokens, order.Customer); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = order.BillingAddress.Email; - var toName = string.Format("{0} {1}", order.BillingAddress.FirstName, order.BillingAddress.LastName); - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); - } - - /// - /// Sends a new order note added notification to a customer - /// - /// Order note - /// Message language identifier - /// Queued email identifier - public virtual int SendNewOrderNoteAddedCustomerNotification(OrderNote orderNote, int languageId) - { - if (orderNote == null) - throw new ArgumentNullException("orderNote"); - - var order = orderNote.Order; - - var store = _storeService.GetStoreById(order.StoreId) ?? _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("Customer.NewOrderNote", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddOrderNoteTokens(tokens, orderNote); - _messageTokenProvider.AddOrderTokens(tokens, orderNote.Order, language); - _messageTokenProvider.AddCustomerTokens(tokens, orderNote.Order.Customer); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = order.BillingAddress.Email; - var toName = string.Format("{0} {1}", order.BillingAddress.FirstName, order.BillingAddress.LastName); - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); - } - - /// - /// Sends a "Recurring payment cancelled" notification to a store owner - /// - /// Recurring payment - /// Message language identifier - /// Queued email identifier - public virtual int SendRecurringPaymentCancelledStoreOwnerNotification(RecurringPayment recurringPayment, int languageId) - { - if (recurringPayment == null) - throw new ArgumentNullException("recurringPayment"); - - var store = _storeService.GetStoreById(recurringPayment.InitialOrder.StoreId) ?? _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("RecurringPaymentCancelled.StoreOwnerNotification", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddOrderTokens(tokens, recurringPayment.InitialOrder, language); - _messageTokenProvider.AddCustomerTokens(tokens, recurringPayment.InitialOrder.Customer); - _messageTokenProvider.AddRecurringPaymentTokens(tokens, recurringPayment); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); - } - - #endregion - - #region Newsletter workflow - - /// - /// Sends a newsletter subscription activation message - /// - /// Newsletter subscription - /// Language identifier - /// Queued email identifier - public virtual int SendNewsLetterSubscriptionActivationMessage(NewsLetterSubscription subscription, int languageId) - { - if (subscription == null) - throw new ArgumentNullException("subscription"); - - var store = _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("NewsLetterSubscription.ActivationMessage", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddNewsLetterSubscriptionTokens(tokens, subscription); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = subscription.Email; - var toName = ""; - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); - } - - /// - /// Sends a newsletter subscription deactivation message - /// - /// Newsletter subscription - /// Language identifier - /// Queued email identifier - public virtual int SendNewsLetterSubscriptionDeactivationMessage(NewsLetterSubscription subscription, int languageId) - { - if (subscription == null) - throw new ArgumentNullException("subscription"); - - var store = _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("NewsLetterSubscription.DeactivationMessage", store.Id); - if (messageTemplate == null) - return 0; - - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, _storeContext.CurrentStore); - _messageTokenProvider.AddNewsLetterSubscriptionTokens(tokens, subscription); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = subscription.Email; - var toName = ""; - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); - } - - #endregion - - #region Send a message to a friend - - /// - /// Sends "email a friend" message - /// - /// Customer instance - /// Message language identifier - /// Product instance - /// Customer's email - /// Friend's email - /// Personal message - /// Queued email identifier - public virtual int SendProductEmailAFriendMessage(Customer customer, int languageId, - Product product, string customerEmail, string friendsEmail, string personalMessage) - { - if (customer == null) - throw new ArgumentNullException("customer"); - - if (product == null) - throw new ArgumentNullException("product"); - - var store = _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("Service.EmailAFriend", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddCustomerTokens(tokens, customer); - _messageTokenProvider.AddProductTokens(tokens, product, language); - - tokens.Add(new Token("EmailAFriend.PersonalMessage", personalMessage, true)); - tokens.Add(new Token("EmailAFriend.Email", customerEmail)); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = friendsEmail; - var toName = ""; - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); - } - - public virtual int SendProductQuestionMessage(Customer customer, int languageId, Product product, - string senderEmail, string senderName, string senderPhone, string question) - { - if (customer == null) - throw new ArgumentNullException("customer"); - - if (customer.IsSystemAccount) - return 0; - - if (product == null) - throw new ArgumentNullException("product"); - - var store = _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("Product.AskQuestion", store.Id); - if (messageTemplate == null) - return 0; - - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddCustomerTokens(tokens, customer); - _messageTokenProvider.AddProductTokens(tokens, product, language); - - tokens.Add(new Token("ProductQuestion.Message", question, true)); - tokens.Add(new Token("ProductQuestion.SenderEmail", senderEmail)); - tokens.Add(new Token("ProductQuestion.SenderName", senderName)); - tokens.Add(new Token("ProductQuestion.SenderPhone", senderPhone)); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, senderEmail, senderName); - } - - /// - /// Sends wishlist "email a friend" message - /// - /// Customer - /// Message language identifier - /// Customer's email - /// Friend's email - /// Personal message - /// Queued email identifier - public virtual int SendWishlistEmailAFriendMessage(Customer customer, int languageId, - string customerEmail, string friendsEmail, string personalMessage) - { - if (customer == null) - throw new ArgumentNullException("customer"); - - var store = _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("Wishlist.EmailAFriend", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddCustomerTokens(tokens, customer); - tokens.Add(new Token("Wishlist.PersonalMessage", personalMessage, true)); - tokens.Add(new Token("Wishlist.Email", customerEmail)); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = friendsEmail; - var toName = ""; - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); - } - - #endregion - - #region Return requests - - /// - /// Sends 'New Return Request' message to a store owner - /// - /// Return request - /// Order item - /// Message language identifier - /// Queued email identifier - public virtual int SendNewReturnRequestStoreOwnerNotification(ReturnRequest returnRequest, OrderItem orderItem, int languageId) - { - if (returnRequest == null) - throw new ArgumentNullException("returnRequest"); - - var store = _storeService.GetStoreById(orderItem.Order.StoreId) ?? _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("NewReturnRequest.StoreOwnerNotification", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddCustomerTokens(tokens, returnRequest.Customer); - _messageTokenProvider.AddReturnRequestTokens(tokens, returnRequest, orderItem); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - - // use customer email as reply address - var replyTo = GetReplyToEmail(returnRequest.Customer); - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, replyTo.Item1, replyTo.Item2); - } - - /// - /// Sends 'Return Request status changed' message to a customer - /// - /// Return request - /// Order item - /// Message language identifier - /// Queued email identifier - public virtual int SendReturnRequestStatusChangedCustomerNotification(ReturnRequest returnRequest, OrderItem orderItem, int languageId) - { - if (returnRequest == null) - throw new ArgumentNullException("returnRequest"); - - var store = _storeService.GetStoreById(orderItem.Order.StoreId) ?? _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("ReturnRequestStatusChanged.CustomerNotification", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddCustomerTokens(tokens, returnRequest.Customer); - _messageTokenProvider.AddReturnRequestTokens(tokens, returnRequest, orderItem); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = returnRequest.Customer.FindEmail(); - var toName = returnRequest.Customer.GetFullName(); - - if (toEmail.IsEmpty()) - return 0; - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); - } - - #endregion - - #region Forum Notifications - - /// - /// Sends a forum subscription message to a customer - /// - /// Customer instance - /// Forum Topic - /// Forum - /// Message language identifier - /// Queued email identifier - public int SendNewForumTopicMessage(Customer customer, ForumTopic forumTopic, Forum forum, int languageId) - { - if (customer == null) - { - throw new ArgumentNullException("customer"); - } - - var store = _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("Forums.NewForumTopic", store.Id); - if (messageTemplate == null) - { - return 0; - } - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddCustomerTokens(tokens, customer); - _messageTokenProvider.AddForumTopicTokens(tokens, forumTopic); - _messageTokenProvider.AddForumTokens(tokens, forumTopic.Forum, language); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = customer.Email; - var toName = customer.GetFullName(); - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); - } - - /// - /// Sends a forum subscription message to a customer - /// - /// Customer instance - /// Forum post - /// Forum Topic - /// Forum - /// Friendly (starts with 1) forum topic page to use for URL generation - /// Message language identifier - /// Queued email identifier - public int SendNewForumPostMessage(Customer customer, ForumPost forumPost, ForumTopic forumTopic, Forum forum, int friendlyForumTopicPageIndex, int languageId) - { - if (customer == null) - { - throw new ArgumentNullException("customer"); - } - - var store = _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("Forums.NewForumPost", store.Id); - if (messageTemplate == null) - { - return 0; - } - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddForumPostTokens(tokens, forumPost); - _messageTokenProvider.AddCustomerTokens(tokens, customer); - _messageTokenProvider.AddForumTopicTokens(tokens, forumPost.ForumTopic, friendlyForumTopicPageIndex, forumPost.Id); - _messageTokenProvider.AddForumTokens(tokens, forumPost.ForumTopic.Forum, language); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = customer.Email; - var toName = customer.GetFullName(); - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); - } - - /// - /// Sends a private message notification - /// - /// Private message - /// Message language identifier - /// Queued email identifier - public int SendPrivateMessageNotification(Customer customer, PrivateMessage privateMessage, int languageId) - { - if (privateMessage == null) - { - throw new ArgumentNullException("privateMessage"); - } - - var store = _storeService.GetStoreById(privateMessage.StoreId) ?? _storeContext.CurrentStore; - - var messageTemplate = GetActiveMessageTemplate("Customer.NewPM", store.Id); - if (messageTemplate == null) - { - return 0; - } - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddCustomerTokens(tokens, customer); - _messageTokenProvider.AddPrivateMessageTokens(tokens, privateMessage); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); - var toEmail = privateMessage.ToCustomer.Email; - var toName = privateMessage.ToCustomer.GetFullName(); - - return SendNotification(messageTemplate, emailAccount, languageId, tokens, toEmail, toName); - } - - #endregion - - #region Misc - - public virtual int SendGenericMessage(string messageTemplateName, Action cfg) - { - Guard.NotNull(cfg, nameof(cfg)); - Guard.NotEmpty(messageTemplateName, nameof(messageTemplateName)); - - var ctx = new GenericMessageContext(); - ctx.MessagenTokenProvider = _messageTokenProvider; - - cfg(ctx); - - if (!ctx.StoreId.HasValue) - { - ctx.StoreId = _storeContext.CurrentStore.Id; - } - - if (!ctx.LanguageId.HasValue) - { - ctx.LanguageId = _workContext.WorkingLanguage.Id; - } - - if (ctx.Customer == null) - { - ctx.Customer = _workContext.CurrentCustomer; - } - - if (ctx.Customer.IsSystemAccount) - return 0; - - _messageTokenProvider.AddCustomerTokens(ctx.Tokens, ctx.Customer); - _messageTokenProvider.AddStoreTokens(ctx.Tokens, _storeService.GetStoreById(ctx.StoreId.Value)); - - var language = EnsureLanguageIsActive(ctx.LanguageId.Value, ctx.StoreId.Value); - ctx.LanguageId = language.Id; - - var messageTemplate = GetActiveMessageTemplate(messageTemplateName, ctx.StoreId.Value); - if (messageTemplate == null) - return 0; - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, ctx.LanguageId.Value); - var toEmail = ctx.ToEmail.HasValue() ? ctx.ToEmail : emailAccount.Email; - var toName = ctx.ToName.HasValue() ? ctx.ToName : emailAccount.DisplayName; - - if (ctx.ReplyToCustomer && ctx.Customer != null) - { - // use customer email as reply address - var replyTo = GetReplyToEmail(ctx.Customer); - ctx.ReplyToEmail = replyTo.Item1; - ctx.ReplyToName = replyTo.Item2; - } - - return SendNotification(messageTemplate, emailAccount, ctx.LanguageId.Value, ctx.Tokens, toEmail, toName, ctx.ReplyToEmail, ctx.ReplyToName); - } - - /// - /// Sends a gift card notification - /// - /// Gift card - /// Message language identifier - /// Queued email identifier - public virtual int SendGiftCardNotification(GiftCard giftCard, int languageId) - { - if (giftCard == null) - throw new ArgumentNullException("giftCard"); - - Store store = null; - var order = giftCard.PurchasedWithOrderItem != null ? - giftCard.PurchasedWithOrderItem.Order : - null; - if (order != null) - store = _storeService.GetStoreById(order.StoreId); - if (store == null) - store = _storeContext.CurrentStore; - - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("GiftCard.Notification", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddGiftCardTokens(tokens, giftCard); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = giftCard.RecipientEmail; - var toName = giftCard.RecipientName; - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); - } - - /// - /// Sends a product review notification message to a store owner - /// - /// Product review - /// Message language identifier - /// Queued email identifier - public virtual int SendProductReviewNotificationMessage(ProductReview productReview, int languageId) - { - if (productReview == null) - throw new ArgumentNullException("productReview"); - - var store = _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("Product.ProductReview", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddProductReviewTokens(tokens, productReview); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - - // use customer email as reply address - var replyTo = GetReplyToEmail(productReview.Customer); - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, replyTo.Item1, replyTo.Item2); - } - - /// - /// Sends a "quantity below" notification to a store owner - /// - /// Product - /// Message language identifier - /// Queued email identifier - public virtual int SendQuantityBelowStoreOwnerNotification(Product product, int languageId) - { - if (product == null) - throw new ArgumentNullException("product"); - - var store = _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("QuantityBelow.StoreOwnerNotification", store.Id); - if (messageTemplate == null) - return 0; - - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddProductTokens(tokens, product, language); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); - } - - /// - /// Sends a "new VAT sumitted" notification to a store owner - /// - /// Customer - /// Received VAT name - /// Received VAT address - /// Message language identifier - /// Queued email identifier - public virtual int SendNewVatSubmittedStoreOwnerNotification(Customer customer, string vatName, string vatAddress, int languageId) - { - if (customer == null) - throw new ArgumentNullException("customer"); - - var store = _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("NewVATSubmitted.StoreOwnerNotification", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddCustomerTokens(tokens, customer); - tokens.Add(new Token("VatValidationResult.Name", vatName)); - tokens.Add(new Token("VatValidationResult.Address", vatAddress)); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - - // use customer email as reply address - var replyTo = GetReplyToEmail(customer); - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, replyTo.Item1, replyTo.Item2); - } - - /// - /// Sends a blog comment notification message to a store owner - /// - /// Blog comment - /// Message language identifier - /// Queued email identifier - public virtual int SendBlogCommentNotificationMessage(BlogComment blogComment, int languageId) - { - if (blogComment == null) - throw new ArgumentNullException("blogComment"); - - var store = _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("Blog.BlogComment", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddBlogCommentTokens(tokens, blogComment); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - - // use customer email as reply address - var replyTo = GetReplyToEmail(blogComment.Customer); - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, replyTo.Item1, replyTo.Item2); - } - - /// - /// Sends a news comment notification message to a store owner - /// - /// News comment - /// Message language identifier - /// Queued email identifier - public virtual int SendNewsCommentNotificationMessage(NewsComment newsComment, int languageId) - { - if (newsComment == null) - throw new ArgumentNullException("newsComment"); - - var store = _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("News.NewsComment", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddNewsCommentTokens(tokens, newsComment); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var toEmail = emailAccount.Email; - var toName = emailAccount.DisplayName; - - // use customer email as sender/reply address - var replyTo = GetReplyToEmail(newsComment.Customer); - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, replyTo.Item1, replyTo.Item2); - } - - /// - /// Sends a 'Back in stock' notification message to a customer - /// - /// Subscription - /// Message language identifier - /// Queued email identifier - public virtual int SendBackInStockNotification(BackInStockSubscription subscription, int languageId) - { - if (subscription == null) - throw new ArgumentNullException("subscription"); - - var store = _storeService.GetStoreById(subscription.StoreId) ?? _storeContext.CurrentStore; - var language = EnsureLanguageIsActive(languageId, store.Id); - - var messageTemplate = GetActiveMessageTemplate("Customer.BackInStock", store.Id); - if (messageTemplate == null) - return 0; - - //tokens - var tokens = new List(); - _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddCustomerTokens(tokens, subscription.Customer); - _messageTokenProvider.AddBackInStockTokens(tokens, subscription); - - //event notification - _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); - var customer = subscription.Customer; - var toEmail = customer.Email; - var toName = customer.GetFullName(); - - return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); - } - - #endregion - - #endregion - } -} diff --git a/src/Libraries/SmartStore.Services/News/NewsMessageFactoryExtensions.cs b/src/Libraries/SmartStore.Services/News/NewsMessageFactoryExtensions.cs new file mode 100644 index 0000000000..be7b2f8284 --- /dev/null +++ b/src/Libraries/SmartStore.Services/News/NewsMessageFactoryExtensions.cs @@ -0,0 +1,19 @@ +using System; +using SmartStore.Core.Domain.Messages; +using SmartStore.Core.Domain.News; +using SmartStore.Services.Messages; + +namespace SmartStore.Services.News +{ + public static class NewsMessageFactoryExtensions + { + /// + /// Sends a news comment notification message to a store owner + /// + public static CreateMessageResult SendNewsCommentNotificationMessage(this IMessageFactory factory, NewsComment newsComment, int languageId = 0) + { + Guard.NotNull(newsComment, nameof(newsComment)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewsCommentStoreOwner, languageId, customer: newsComment.Customer), true, newsComment); + } + } +} diff --git a/src/Libraries/SmartStore.Services/News/NewsService.cs b/src/Libraries/SmartStore.Services/News/NewsService.cs index 743ab7f9f4..c7c041db1a 100644 --- a/src/Libraries/SmartStore.Services/News/NewsService.cs +++ b/src/Libraries/SmartStore.Services/News/NewsService.cs @@ -54,9 +54,6 @@ public virtual void DeleteNews(NewsItem newsItem) throw new ArgumentNullException("newsItem"); _newsItemRepository.Delete(newsItem); - - //event notification - _services.EventPublisher.EntityDeleted(newsItem); } /// @@ -157,9 +154,6 @@ public virtual void InsertNews(NewsItem news) throw new ArgumentNullException("news"); _newsItemRepository.Insert(news); - - //event notification - _services.EventPublisher.EntityInserted(news); } /// @@ -172,9 +166,6 @@ public virtual void UpdateNews(NewsItem news) throw new ArgumentNullException("news"); _newsItemRepository.Update(news); - - //event notification - _services.EventPublisher.EntityUpdated(news); } /// diff --git a/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeParser.cs b/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeParser.cs index 8cb16e7959..ed51878fd0 100644 --- a/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeParser.cs +++ b/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeParser.cs @@ -32,8 +32,7 @@ public IList ParseCheckoutAttributeIds(string attributes) if (node1.Attributes != null && node1.Attributes["ID"] != null) { string str1 = node1.Attributes["ID"].InnerText.Trim(); - int id = 0; - if (int.TryParse(str1, out id)) + if (int.TryParse(str1, out var id)) { ids.Add(id); } @@ -76,8 +75,7 @@ public IList ParseCheckoutAttributeValues(string attribu { if (!String.IsNullOrEmpty(caValueStr)) { - int caValueId = 0; - if (int.TryParse(caValueStr, out caValueId)) + if (int.TryParse(caValueStr, out var caValueId)) { var caValue = _checkoutAttributeService.GetCheckoutAttributeValueById(caValueId); if (caValue != null) @@ -105,9 +103,8 @@ public IList ParseValues(string attributes, int checkoutAttributeId) { if (node1.Attributes != null && node1.Attributes["ID"] != null) { - string str1 = node1.Attributes["ID"].InnerText.Trim(); - int id = 0; - if (int.TryParse(str1, out id)) + var str1 = node1.Attributes["ID"].InnerText.Trim(); + if (int.TryParse(str1, out var id)) { if (id == checkoutAttributeId) { @@ -156,8 +153,7 @@ public string AddCheckoutAttribute(string attributes, CheckoutAttribute ca, stri if (node1.Attributes != null && node1.Attributes["ID"] != null) { string str1 = node1.Attributes["ID"].InnerText.Trim(); - int id = 0; - if (int.TryParse(str1, out id)) + if (int.TryParse(str1, out var id)) { if (id == ca.Id) { @@ -224,8 +220,7 @@ public virtual string EnsureOnlyActiveAttributes(string attributes, IList GetCheckoutAttributes(int storeId = 0, bool showHidden = false) @@ -98,9 +95,6 @@ public virtual void InsertCheckoutAttribute(CheckoutAttribute checkoutAttribute) throw new ArgumentNullException("checkoutAttribute"); _checkoutAttributeRepository.Insert(checkoutAttribute); - - //event notification - _eventPublisher.EntityInserted(checkoutAttribute); } public virtual void UpdateCheckoutAttribute(CheckoutAttribute checkoutAttribute) @@ -109,9 +103,6 @@ public virtual void UpdateCheckoutAttribute(CheckoutAttribute checkoutAttribute) throw new ArgumentNullException("checkoutAttribute"); _checkoutAttributeRepository.Update(checkoutAttribute); - - //event notification - _eventPublisher.EntityUpdated(checkoutAttribute); } #endregion @@ -124,9 +115,6 @@ public virtual void DeleteCheckoutAttributeValue(CheckoutAttributeValue checkout throw new ArgumentNullException("checkoutAttributeValue"); _checkoutAttributeValueRepository.Delete(checkoutAttributeValue); - - //event notification - _eventPublisher.EntityDeleted(checkoutAttributeValue); } public virtual IList GetCheckoutAttributeValues(int checkoutAttributeId) @@ -153,9 +141,6 @@ public virtual void InsertCheckoutAttributeValue(CheckoutAttributeValue checkout throw new ArgumentNullException("checkoutAttributeValue"); _checkoutAttributeValueRepository.Insert(checkoutAttributeValue); - - //event notification - _eventPublisher.EntityInserted(checkoutAttributeValue); } public virtual void UpdateCheckoutAttributeValue(CheckoutAttributeValue checkoutAttributeValue) @@ -164,9 +149,6 @@ public virtual void UpdateCheckoutAttributeValue(CheckoutAttributeValue checkout throw new ArgumentNullException("checkoutAttributeValue"); _checkoutAttributeValueRepository.Update(checkoutAttributeValue); - - //event notification - _eventPublisher.EntityUpdated(checkoutAttributeValue); } #endregion diff --git a/src/Libraries/SmartStore.Services/Orders/GiftCardService.cs b/src/Libraries/SmartStore.Services/Orders/GiftCardService.cs index 8df09d65cb..d89896397d 100644 --- a/src/Libraries/SmartStore.Services/Orders/GiftCardService.cs +++ b/src/Libraries/SmartStore.Services/Orders/GiftCardService.cs @@ -49,8 +49,6 @@ public virtual void DeleteGiftCard(GiftCard giftCard) _giftCardRepository.Delete(giftCard); - //event notification - _eventPublisher.EntityDeleted(giftCard); } /// @@ -107,9 +105,6 @@ public virtual void InsertGiftCard(GiftCard giftCard) throw new ArgumentNullException("giftCard"); _giftCardRepository.Insert(giftCard); - - //event notification - _eventPublisher.EntityInserted(giftCard); } /// @@ -122,9 +117,6 @@ public virtual void UpdateGiftCard(GiftCard giftCard) throw new ArgumentNullException("giftCard"); _giftCardRepository.Update(giftCard); - - //event notification - _eventPublisher.EntityUpdated(giftCard); } /// diff --git a/src/Libraries/SmartStore.Services/Orders/IOrderTotalCalculationService.cs b/src/Libraries/SmartStore.Services/Orders/IOrderTotalCalculationService.cs index 28058bcb91..f5c2a08340 100644 --- a/src/Libraries/SmartStore.Services/Orders/IOrderTotalCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Orders/IOrderTotalCalculationService.cs @@ -6,7 +6,7 @@ namespace SmartStore.Services.Orders { - public partial interface IOrderTotalCalculationService + public partial interface IOrderTotalCalculationService { /// /// Gets shopping cart subtotal @@ -149,33 +149,18 @@ decimal GetTaxTotal(IList cart, out SortedDictionary< - /// - /// Gets shopping cart total + /// Gets the shopping cart total /// - /// Cart + /// Shopping cart /// A value indicating whether we should ignore reward points (if enabled and a customer is going to use them) /// A value indicating whether we should use payment method additional fee when calculating order total - decimal? GetShoppingCartTotal(IList cart, bool ignoreRewardPonts = false, + /// Shopping cart total. TotalAmount is null if shopping cart total couldn't be calculated now. + ShoppingCartTotal GetShoppingCartTotal( + IList cart, + bool ignoreRewardPonts = false, bool usePaymentMethodAdditionalFee = true); - /// - /// Gets shopping cart total - /// - /// Cart - /// Applied gift cards - /// Applied discount amount - /// Applied discount - /// Reward points to redeem - /// Reward points amount in primary store currency to redeem - /// A value indicating whether we should ignore reward points (if enabled and a customer is going to use them) - /// A value indicating whether we should use payment method additional fee when calculating order total - /// Shopping cart total;Null if shopping cart total couldn't be calculated now - decimal? GetShoppingCartTotal(IList cart, - out decimal discountAmount, out Discount appliedDiscount, - out List appliedGiftCards, - out int redeemedRewardPoints, out decimal redeemedRewardPointsAmount, - bool ignoreRewardPonts = false, bool usePaymentMethodAdditionalFee = true); /// /// Gets an order discount (applied to order total) diff --git a/src/Libraries/SmartStore.Services/Orders/OrderExtensions.cs b/src/Libraries/SmartStore.Services/Orders/OrderExtensions.cs index 955443d286..e896a7c12e 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderExtensions.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderExtensions.cs @@ -5,6 +5,8 @@ using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Html; +using SmartStore.Services.Directory; +using SmartStore.Services.Payments; namespace SmartStore.Services.Orders { @@ -61,13 +63,50 @@ public static void SetBundleData(this OrderItem orderItem, List + /// Get the order total in the currency of the customer + /// + /// Order + /// Currency service + /// Payment service + /// Rounding amount + /// Order total + public static decimal GetOrderTotalInCustomerCurrency( + this Order order, + ICurrencyService currencyService, + IPaymentService paymentService, + out decimal roundingAmount) + { + Guard.NotNull(order, nameof(order)); + + roundingAmount = order.OrderTotalRounding; + var orderTotal = currencyService.ConvertCurrency(order.OrderTotal, order.CurrencyRate); + + // Avoid rounding a rounded value. It would zero roundingAmount. + if (orderTotal != order.OrderTotal) + { + var currency = currencyService.GetCurrencyByCode(order.CustomerCurrencyCode); + + if (currency != null && currency.RoundOrderTotalEnabled && order.PaymentMethodSystemName.HasValue()) + { + var pm = paymentService.GetPaymentMethodBySystemName(order.PaymentMethodSystemName); + if (pm != null && pm.RoundOrderTotalEnabled) + { + orderTotal = orderTotal.RoundToNearest(currency, out roundingAmount); + } + } + } + + return orderTotal; + } - /// - /// Gets a value indicating whether an order has items to dispatch - /// - /// Order - /// A value indicating whether an order has items to dispatch - public static bool HasItemsToDispatch(this Order order) + + /// + /// Gets a value indicating whether an order has items to dispatch + /// + /// Order + /// A value indicating whether an order has items to dispatch + public static bool HasItemsToDispatch(this Order order) { Guard.NotNull(order, nameof(order)); diff --git a/src/Libraries/SmartStore.Services/Orders/OrderMessageFactoryExtensions.cs b/src/Libraries/SmartStore.Services/Orders/OrderMessageFactoryExtensions.cs new file mode 100644 index 0000000000..50bb45426e --- /dev/null +++ b/src/Libraries/SmartStore.Services/Orders/OrderMessageFactoryExtensions.cs @@ -0,0 +1,126 @@ +using System; +using SmartStore.Core.Domain.Messages; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Shipping; +using SmartStore.Services.Messages; + +namespace SmartStore.Services.Orders +{ + public static class OrderMessageFactoryExtensions + { + /// + /// Sends an order placed notification to a store owner + /// + public static CreateMessageResult SendOrderPlacedStoreOwnerNotification(this IMessageFactory factory, Order order, int languageId = 0) + { + Guard.NotNull(order, nameof(order)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.OrderPlacedStoreOwner, languageId, order.StoreId), true, order, order.Customer); + } + + /// + /// Sends an order placed notification to a customer + /// + public static CreateMessageResult SendOrderPlacedCustomerNotification(this IMessageFactory factory, Order order, int languageId = 0) + { + Guard.NotNull(order, nameof(order)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.OrderPlacedCustomer, languageId, order.StoreId, order.Customer), true, order); + } + + /// + /// Sends a shipment sent notification to a customer + /// + public static CreateMessageResult SendShipmentSentCustomerNotification(this IMessageFactory factory, Shipment shipment, int languageId = 0) + { + Guard.NotNull(shipment, nameof(shipment)); + Guard.NotNull(shipment.Order, nameof(shipment.Order)); + + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.ShipmentSentCustomer, languageId, shipment.Order.StoreId, shipment.Order.Customer), true, shipment, shipment.Order); + } + + /// + /// Sends a shipment delivered notification to a customer + /// + public static CreateMessageResult SendShipmentDeliveredCustomerNotification(this IMessageFactory factory, Shipment shipment, int languageId = 0) + { + Guard.NotNull(shipment, nameof(shipment)); + Guard.NotNull(shipment.Order, nameof(shipment.Order)); + + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.ShipmentDeliveredCustomer, languageId, shipment.Order.StoreId, shipment.Order.Customer), true, shipment, shipment.Order); + } + + /// + /// Sends an order completed notification to a customer + /// + public static CreateMessageResult SendOrderCompletedCustomerNotification(this IMessageFactory factory, Order order, int languageId = 0) + { + Guard.NotNull(order, nameof(order)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.OrderCompletedCustomer, languageId, order.StoreId, order.Customer), true, order); + } + + /// + /// Sends an order cancelled notification to a customer + /// + public static CreateMessageResult SendOrderCancelledCustomerNotification(this IMessageFactory factory, Order order, int languageId = 0) + { + Guard.NotNull(order, nameof(order)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.OrderCancelledCustomer, languageId, order.StoreId, order.Customer), true, order); + } + + /// + /// Sends a new order note added notification to a customer + /// + public static CreateMessageResult SendNewOrderNoteAddedCustomerNotification(this IMessageFactory factory, OrderNote orderNote, int languageId = 0) + { + Guard.NotNull(orderNote, nameof(orderNote)); + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.OrderNoteAddedCustomer, languageId, orderNote.Order?.StoreId, orderNote.Order?.Customer), true, orderNote, orderNote.Order); + } + + /// + /// Sends a "Recurring payment cancelled" notification to a store owner + /// + public static CreateMessageResult SendRecurringPaymentCancelledStoreOwnerNotification(this IMessageFactory factory, RecurringPayment recurringPayment, int languageId = 0) + { + Guard.NotNull(recurringPayment, nameof(recurringPayment)); + + var order = recurringPayment.InitialOrder; + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.RecurringPaymentCancelledStoreOwner, languageId, order?.StoreId, order?.Customer), true, + recurringPayment, order); + } + + /// + /// Sends 'New Return Request' message to a store owner + /// + public static CreateMessageResult SendNewReturnRequestStoreOwnerNotification(this IMessageFactory factory, ReturnRequest returnRequest, OrderItem orderItem, int languageId = 0) + { + Guard.NotNull(returnRequest, nameof(returnRequest)); + Guard.NotNull(orderItem, nameof(orderItem)); + + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.NewReturnRequestStoreOwner, languageId, orderItem.Order?.StoreId, returnRequest.Customer), true, + returnRequest, orderItem, orderItem.Order, orderItem.Product); + } + + /// + /// Sends 'Return Request status changed' message to a customer + /// + public static CreateMessageResult SendReturnRequestStatusChangedCustomerNotification(this IMessageFactory factory, ReturnRequest returnRequest, OrderItem orderItem, int languageId = 0) + { + Guard.NotNull(returnRequest, nameof(returnRequest)); + Guard.NotNull(orderItem, nameof(orderItem)); + + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.ReturnRequestStatusChangedCustomer, languageId, orderItem.Order?.StoreId, returnRequest.Customer), true, returnRequest); + } + + /// + /// Sends a gift card notification + /// + public static CreateMessageResult SendGiftCardNotification(this IMessageFactory factory, GiftCard giftCard, int languageId = 0) + { + Guard.NotNull(giftCard, nameof(giftCard)); + + var orderItem = giftCard.PurchasedWithOrderItem; + var customer = orderItem?.Order?.Customer; + var storeId = orderItem?.Order?.StoreId; + return factory.CreateMessage(MessageContext.Create(MessageTemplateNames.GiftCardCustomer, languageId, storeId, customer), true, giftCard, orderItem, orderItem?.Product); + } + } +} diff --git a/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs b/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs index 62f8b7c2e9..40b07cabe8 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs @@ -9,7 +9,6 @@ using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Discounts; using SmartStore.Core.Domain.Localization; -using SmartStore.Core.Domain.Logging; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; using SmartStore.Core.Domain.Shipping; @@ -33,10 +32,7 @@ namespace SmartStore.Services.Orders { - /// - /// Order processing service - /// - public partial class OrderProcessingService : IOrderProcessingService + public partial class OrderProcessingService : IOrderProcessingService { #region Fields @@ -46,7 +42,6 @@ public partial class OrderProcessingService : IOrderProcessingService private readonly ILanguageService _languageService; private readonly IProductService _productService; private readonly IPaymentService _paymentService; - private readonly ILogger _logger; private readonly IOrderTotalCalculationService _orderTotalCalculationService; private readonly IPriceCalculationService _priceCalculationService; private readonly IPriceFormatter _priceFormatter; @@ -63,7 +58,7 @@ public partial class OrderProcessingService : IOrderProcessingService private readonly IEncryptionService _encryptionService; private readonly IWorkContext _workContext; private readonly IStoreContext _storeContext; - private readonly IWorkflowMessageService _workflowMessageService; + private readonly IMessageFactory _messageFactory; private readonly ICustomerActivityService _customerActivityService; private readonly ICurrencyService _currencyService; private readonly IAffiliateService _affiliateService; @@ -83,50 +78,13 @@ public partial class OrderProcessingService : IOrderProcessingService #region Ctor - /// - /// Ctor - /// - /// Order service - /// Web helper - /// Localization service - /// Language service - /// Product service - /// Payment service - /// Logger - /// Order total calculationservice - /// Price calculation service - /// Price formatter - /// Product attribute parser - /// Product attribute formatter - /// Gift card service - /// Shopping cart service - /// Checkout attribute service - /// Shipping service - /// Shipment service - /// Tax service - /// Customer service - /// Discount service - /// Encryption service - /// Work context - /// Store context - /// Workflow message service - /// Customer activity service - /// Currency service - /// Affiliate service - /// Event published - /// Payment settings - /// Reward points settings - /// Order settings - /// Tax settings - /// Localization settings - /// Currency settings - public OrderProcessingService(IOrderService orderService, + public OrderProcessingService( + IOrderService orderService, IWebHelper webHelper, ILocalizationService localizationService, ILanguageService languageService, IProductService productService, IPaymentService paymentService, - ILogger logger, IOrderTotalCalculationService orderTotalCalculationService, IPriceCalculationService priceCalculationService, IPriceFormatter priceFormatter, @@ -143,7 +101,7 @@ public OrderProcessingService(IOrderService orderService, IEncryptionService encryptionService, IWorkContext workContext, IStoreContext storeContext, - IWorkflowMessageService workflowMessageService, + IMessageFactory messageFactory, ICustomerActivityService customerActivityService, ICurrencyService currencyService, IAffiliateService affiliateService, @@ -158,56 +116,66 @@ public OrderProcessingService(IOrderService orderService, CurrencySettings currencySettings, ShoppingCartSettings shoppingCartSettings) { - this._orderService = orderService; - this._webHelper = webHelper; - this._localizationService = localizationService; - this._languageService = languageService; - this._productService = productService; - this._paymentService = paymentService; - this._logger = logger; - this._orderTotalCalculationService = orderTotalCalculationService; - this._priceCalculationService = priceCalculationService; - this._priceFormatter = priceFormatter; - this._productAttributeParser = productAttributeParser; - this._productAttributeFormatter = productAttributeFormatter; - this._giftCardService = giftCardService; - this._shoppingCartService = shoppingCartService; - this._checkoutAttributeFormatter = checkoutAttributeFormatter; - this._workContext = workContext; - this._storeContext = storeContext; - this._workflowMessageService = workflowMessageService; - this._shippingService = shippingService; - this._shipmentService = shipmentService; - this._taxService = taxService; - this._customerService = customerService; - this._discountService = discountService; - this._encryptionService = encryptionService; - this._customerActivityService = customerActivityService; - this._currencyService = currencyService; - this._affiliateService = affiliateService; - this._eventPublisher = eventPublisher; - this._genericAttributeService = genericAttributeService; - this._newsLetterSubscriptionService = newsLetterSubscriptionService; - this._paymentSettings = paymentSettings; - this._rewardPointsSettings = rewardPointsSettings; - this._orderSettings = orderSettings; - this._taxSettings = taxSettings; - this._localizationSettings = localizationSettings; - this._currencySettings = currencySettings; - this._shoppingCartSettings = shoppingCartSettings; + _orderService = orderService; + _webHelper = webHelper; + _localizationService = localizationService; + _languageService = languageService; + _productService = productService; + _paymentService = paymentService; + _orderTotalCalculationService = orderTotalCalculationService; + _priceCalculationService = priceCalculationService; + _priceFormatter = priceFormatter; + _productAttributeParser = productAttributeParser; + _productAttributeFormatter = productAttributeFormatter; + _giftCardService = giftCardService; + _shoppingCartService = shoppingCartService; + _checkoutAttributeFormatter = checkoutAttributeFormatter; + _workContext = workContext; + _storeContext = storeContext; + _messageFactory = messageFactory; + _shippingService = shippingService; + _shipmentService = shipmentService; + _taxService = taxService; + _customerService = customerService; + _discountService = discountService; + _encryptionService = encryptionService; + _customerActivityService = customerActivityService; + _currencyService = currencyService; + _affiliateService = affiliateService; + _eventPublisher = eventPublisher; + _genericAttributeService = genericAttributeService; + _newsLetterSubscriptionService = newsLetterSubscriptionService; + _paymentSettings = paymentSettings; + _rewardPointsSettings = rewardPointsSettings; + _orderSettings = orderSettings; + _taxSettings = taxSettings; + _localizationSettings = localizationSettings; + _currencySettings = currencySettings; + _shoppingCartSettings = shoppingCartSettings; T = NullLocalizer.Instance; + Logger = NullLogger.Instance; } public Localizer T { get; set; } + public ILogger Logger { get; set; } #endregion #region Utilities - private decimal Round(decimal value) + protected string FormatTaxRates(SortedDictionary taxRates) { - return (_shoppingCartSettings.RoundPricesDuringCalculation ? Math.Round(value, 2) : value); + var result = string.Empty; + + foreach (var rate in taxRates) + { + result += "{0}:{1}; ".FormatInvariant( + rate.Key.ToString(CultureInfo.InvariantCulture), + rate.Value.ToString(CultureInfo.InvariantCulture)); + } + + return result; } private void ProcessErrors(Order order, IList errors, string messageKey) @@ -217,7 +185,7 @@ private void ProcessErrors(Order order, IList errors, string messageKey) var msg = string.Concat(T(messageKey, order.GetOrderNumber()), " ", string.Join(" ", errors)); _orderService.AddOrderNote(order, msg); - _logger.Error(msg); + Logger.Error(msg); } } @@ -315,19 +283,15 @@ protected void SetActivatedValueForPurchasedGiftCards(Order order, bool activate if (gc.GiftCardType == GiftCardType.Virtual) { - //send email for virtual gift card + // Send email for virtual gift card if (!String.IsNullOrEmpty(gc.RecipientEmail) && !String.IsNullOrEmpty(gc.SenderEmail)) { var customerLang = _languageService.GetLanguageById(order.CustomerLanguageId); if (customerLang == null) customerLang = _languageService.GetAllLanguages().FirstOrDefault(); - var queuedEmailId = _workflowMessageService.SendGiftCardNotification(gc, customerLang.Id); - - if (queuedEmailId > 0) - { - isRecipientNotified = true; - } + var qe = _messageFactory.SendGiftCardNotification(gc, customerLang.Id); + isRecipientNotified = qe?.Email.Id != null; } } @@ -370,20 +334,20 @@ protected void SetOrderStatus(Order order, OrderStatus os, bool notifyCustomer) if (prevOrderStatus != OrderStatus.Complete && os == OrderStatus.Complete && notifyCustomer) { //notification - int orderCompletedCustomerNotificationQueuedEmailId = _workflowMessageService.SendOrderCompletedCustomerNotification(order, order.CustomerLanguageId); - if (orderCompletedCustomerNotificationQueuedEmailId > 0) + var msg = _messageFactory.SendOrderCompletedCustomerNotification(order, order.CustomerLanguageId); + if (msg?.Email?.Id != null) { - _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerCompletedEmailQueued", orderCompletedCustomerNotificationQueuedEmailId)); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerCompletedEmailQueued", msg.Email.Id)); } } if (prevOrderStatus != OrderStatus.Cancelled && os == OrderStatus.Cancelled && notifyCustomer) { //notification - int orderCancelledCustomerNotificationQueuedEmailId = _workflowMessageService.SendOrderCancelledCustomerNotification(order, order.CustomerLanguageId); - if (orderCancelledCustomerNotificationQueuedEmailId > 0) + var msg = _messageFactory.SendOrderCancelledCustomerNotification(order, order.CustomerLanguageId); + if (msg?.Email?.Id != null) { - _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerCancelledEmailQueued", orderCancelledCustomerNotificationQueuedEmailId)); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerCancelledEmailQueued", msg.Email.Id)); } } @@ -747,49 +711,39 @@ public virtual PlaceOrderResult PlaceOrder( vatNumber = customer.GetAttribute(SystemCustomerAttributeNames.VatNumber); } - //tax rates - foreach (var kvp in taxRatesDictionary) - { - var taxRate = kvp.Key; - var taxValue = kvp.Value; - taxRates += string.Format("{0}:{1}; ", taxRate.ToString(CultureInfo.InvariantCulture), taxValue.ToString(CultureInfo.InvariantCulture)); - } + taxRates = FormatTaxRates(taxRatesDictionary); } else { orderTaxTotal = initialOrder.OrderTax; - //VAT number + // VAT number vatNumber = initialOrder.VatNumber; } processPaymentRequest.OrderTax = orderTaxTotal; - //order total (and applied discounts, gift cards, reward points) - decimal? orderTotal = null; - decimal orderDiscountAmount = decimal.Zero; - List appliedGiftCards = null; - int redeemedRewardPoints = 0; - decimal redeemedRewardPointsAmount = decimal.Zero; + // Order total (and applied discounts, gift cards, reward points) + ShoppingCartTotal cartTotal = null; + if (!processPaymentRequest.IsRecurringPayment) { - Discount orderAppliedDiscount = null; - orderTotal = _orderTotalCalculationService.GetShoppingCartTotal(cart, - out orderDiscountAmount, out orderAppliedDiscount, out appliedGiftCards, out redeemedRewardPoints, out redeemedRewardPointsAmount); + cartTotal = _orderTotalCalculationService.GetShoppingCartTotal(cart); - if (!orderTotal.HasValue) + if (!cartTotal.TotalAmount.HasValue) throw new SmartException(T("Order.CannotCalculateOrderTotal")); - //discount history - if (orderAppliedDiscount != null && !appliedDiscounts.Any(x => x.Id == orderAppliedDiscount.Id)) + // Discount history + if (cartTotal.AppliedDiscount != null && !appliedDiscounts.Any(x => x.Id == cartTotal.AppliedDiscount.Id)) { - appliedDiscounts.Add(orderAppliedDiscount); + appliedDiscounts.Add(cartTotal.AppliedDiscount); } } else { - orderDiscountAmount = initialOrder.OrderDiscount; - orderTotal = initialOrder.OrderTotal; + cartTotal = new ShoppingCartTotal(initialOrder.OrderTotal); + cartTotal.DiscountAmount = initialOrder.OrderDiscount; } - processPaymentRequest.OrderTotal = orderTotal.Value; + + processPaymentRequest.OrderTotal = cartTotal.TotalAmount.Value; #endregion @@ -858,7 +812,7 @@ public virtual PlaceOrderResult PlaceOrder( // skip payment workflow if order total equals zero var skipPaymentWorkflow = false; - if (orderTotal.Value == decimal.Zero) + if (cartTotal.TotalAmount.Value == decimal.Zero) { skipPaymentWorkflow = true; } @@ -1019,9 +973,10 @@ public virtual PlaceOrderResult PlaceOrder( PaymentMethodAdditionalFeeTaxRate = paymentAdditionalFeeTaxRate, TaxRates = taxRates, OrderTax = orderTaxTotal, - OrderTotal = orderTotal.Value, + OrderTotalRounding = cartTotal.RoundingAmount, + OrderTotal = cartTotal.TotalAmount.Value, RefundedAmount = decimal.Zero, - OrderDiscount = orderDiscountAmount, + OrderDiscount = cartTotal.DiscountAmount, CheckoutAttributeDescription = checkoutAttributeDescription, CheckoutAttributesXml = checkoutAttributesXml, CustomerCurrencyCode = customerCurrencyCode, @@ -1074,12 +1029,12 @@ public virtual PlaceOrderResult PlaceOrder( if (!processPaymentRequest.IsRecurringPayment) { - //move shopping cart items to order products + // Move shopping cart items to order products foreach (var sc in cart) { sc.Item.Product.MergeWithCombination(sc.Item.AttributesXml); - //prices + // Prices decimal taxRate = decimal.Zero; decimal unitPriceTaxRate = decimal.Zero; decimal scUnitPrice = _priceCalculationService.GetUnitPrice(sc, true); @@ -1089,7 +1044,7 @@ public virtual PlaceOrderResult PlaceOrder( decimal scSubTotalInclTax = _taxService.GetProductPrice(sc.Item.Product, scSubTotal, true, customer, out taxRate); decimal scSubTotalExclTax = _taxService.GetProductPrice(sc.Item.Product, scSubTotal, false, customer, out taxRate); - //discounts + // Discounts Discount scDiscount = null; decimal discountAmount = _priceCalculationService.GetDiscountAmount(sc, out scDiscount); decimal discountAmountInclTax = _taxService.GetProductPrice(sc.Item.Product, discountAmount, true, customer, out taxRate); @@ -1100,12 +1055,12 @@ public virtual PlaceOrderResult PlaceOrder( appliedDiscounts.Add(scDiscount); } - //attributes + // Attributes var attributeDescription = _productAttributeFormatter.FormatAttributes(sc.Item.Product, sc.Item.AttributesXml, customer); var itemWeight = _shippingService.GetShoppingCartItemWeight(sc); - //save order item + // Dave order item var orderItem = new OrderItem { OrderItemGuid = Guid.NewGuid(), @@ -1148,7 +1103,7 @@ public virtual PlaceOrderResult PlaceOrder( order.OrderItems.Add(orderItem); _orderService.UpdateOrder(order); - //gift cards + // Gift cards if (sc.Item.Product.IsGiftCard) { string giftCardRecipientName, giftCardRecipientEmail, giftCardSenderName, giftCardSenderEmail, giftCardMessage; @@ -1180,7 +1135,7 @@ public virtual PlaceOrderResult PlaceOrder( _productService.AdjustInventory(sc, true); } - //clear shopping cart + // Clear shopping cart if (!processPaymentRequest.IsMultiOrder) { cart.ToList().ForEach(sci => _shoppingCartService.DeleteShoppingCartItem(sci.Item, false)); @@ -1188,11 +1143,11 @@ public virtual PlaceOrderResult PlaceOrder( } else { - //recurring payment + // Recurring payment var initialOrderItems = initialOrder.OrderItems; foreach (var orderItem in initialOrderItems) { - //save item + // Save item var newOrderItem = new OrderItem { OrderItemGuid = Guid.NewGuid(), @@ -1218,7 +1173,7 @@ public virtual PlaceOrderResult PlaceOrder( order.OrderItems.Add(newOrderItem); _orderService.UpdateOrder(order); - //gift cards + // Gift cards if (orderItem.Product.IsGiftCard) { string giftCardRecipientName, giftCardRecipientEmail, giftCardSenderName, giftCardSenderEmail, giftCardMessage; @@ -1251,7 +1206,7 @@ public virtual PlaceOrderResult PlaceOrder( } } - //discount usage history + // Discount usage history if (!processPaymentRequest.IsRecurringPayment) { foreach (var discount in appliedDiscounts) @@ -1266,10 +1221,10 @@ public virtual PlaceOrderResult PlaceOrder( } } - //gift card usage history - if (!processPaymentRequest.IsRecurringPayment && appliedGiftCards != null) + // Gift card usage history + if (!processPaymentRequest.IsRecurringPayment && cartTotal.AppliedGiftCards != null) { - foreach (var agc in appliedGiftCards) + foreach (var agc in cartTotal.AppliedGiftCards) { var amountUsed = agc.AmountCanBeUsed; var gcuh = new GiftCardUsageHistory @@ -1284,21 +1239,21 @@ public virtual PlaceOrderResult PlaceOrder( } } - //reward points history - if (redeemedRewardPointsAmount > decimal.Zero) + // Reward points history + if (cartTotal.RedeemedRewardPointsAmount > decimal.Zero) { - customer.AddRewardPointsHistoryEntry(-redeemedRewardPoints, + customer.AddRewardPointsHistoryEntry(-cartTotal.RedeemedRewardPoints, _localizationService.GetResource("RewardPoints.Message.RedeemedForOrder", order.CustomerLanguageId).FormatInvariant(order.GetOrderNumber()), order, - redeemedRewardPointsAmount); + cartTotal.RedeemedRewardPointsAmount); _customerService.UpdateCustomer(customer); } - //recurring orders + // Recurring orders if (!processPaymentRequest.IsRecurringPayment && isRecurringShoppingCart) { - //create recurring payment (the first payment) + // Create recurring payment (the first payment) var rp = new RecurringPayment { CycleLength = processPaymentRequest.RecurringCycleLength, @@ -1348,18 +1303,18 @@ public virtual PlaceOrderResult PlaceOrder( // notes, messages _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderPlaced")); - + //send email notifications - int orderPlacedStoreOwnerNotificationQueuedEmailId = _workflowMessageService.SendOrderPlacedStoreOwnerNotification(order, _localizationSettings.DefaultAdminLanguageId); - if (orderPlacedStoreOwnerNotificationQueuedEmailId > 0) + var msg = _messageFactory.SendOrderPlacedStoreOwnerNotification(order, _localizationSettings.DefaultAdminLanguageId); + if (msg?.Email?.Id != null) { - _orderService.AddOrderNote(order, T("Admin.OrderNotice.MerchantEmailQueued", orderPlacedStoreOwnerNotificationQueuedEmailId)); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.MerchantEmailQueued", msg.Email.Id)); } - int orderPlacedCustomerNotificationQueuedEmailId = _workflowMessageService.SendOrderPlacedCustomerNotification(order, order.CustomerLanguageId); - if (orderPlacedCustomerNotificationQueuedEmailId > 0) + msg = _messageFactory.SendOrderPlacedCustomerNotification(order, order.CustomerLanguageId); + if (msg?.Email?.Id != null) { - _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerEmailQueued", orderPlacedCustomerNotificationQueuedEmailId)); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerEmailQueued", msg.Email.Id)); } // check order status @@ -1428,13 +1383,13 @@ public virtual PlaceOrderResult PlaceOrder( } catch (Exception ex) { - _logger.Error(ex); + Logger.Error(ex); result.AddError(ex.Message); } if (result.Errors.Count > 0) { - _logger.Error(string.Join(" ", result.Errors)); + Logger.Error(string.Join(" ", result.Errors)); } return result; @@ -1537,7 +1492,7 @@ public virtual void ProcessNextRecurringPayment(RecurringPayment recurringPaymen } catch (Exception exception) { - _logger.ErrorsAll(exception); + Logger.ErrorsAll(exception); throw; } } @@ -1573,7 +1528,7 @@ public virtual IList CancelRecurringPayment(RecurringPayment recurringPa _orderService.AddOrderNote(initialOrder, T("Admin.OrderNotice.RecurringPaymentCancelled")); //notify a store owner - _workflowMessageService.SendRecurringPaymentCancelledStoreOwnerNotification(recurringPayment, _localizationSettings.DefaultAdminLanguageId); + _messageFactory.SendRecurringPaymentCancelledStoreOwnerNotification(recurringPayment, _localizationSettings.DefaultAdminLanguageId); } } catch (Exception exception) @@ -1663,10 +1618,10 @@ public virtual void Ship(Shipment shipment, bool notifyCustomer) if (notifyCustomer) { //notify customer - int queuedEmailId = _workflowMessageService.SendShipmentSentCustomerNotification(shipment, order.CustomerLanguageId); - if (queuedEmailId > 0) + var msg = _messageFactory.SendShipmentSentCustomerNotification(shipment, order.CustomerLanguageId); + if (msg?.Email?.Id != null) { - _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerShippedEmailQueued", queuedEmailId)); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerShippedEmailQueued", msg.Email.Id)); } } @@ -1706,10 +1661,10 @@ public virtual void Deliver(Shipment shipment, bool notifyCustomer) if (notifyCustomer) { //send email notification - int queuedEmailId = _workflowMessageService.SendShipmentDeliveredCustomerNotification(shipment, order.CustomerLanguageId); - if (queuedEmailId > 0) + var msg = _messageFactory.SendShipmentDeliveredCustomerNotification(shipment, order.CustomerLanguageId); + if (msg?.Email?.Id != null) { - _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerDeliveredEmailQueued", queuedEmailId)); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerDeliveredEmailQueued", msg.Email.Id)); } } @@ -1780,21 +1735,23 @@ public virtual void AutoUpdateOrderDetails(AutoUpdateOrderItemContext context) if (context.UpdateTotals && oi.Order.OrderStatusId <= (int)OrderStatus.Pending) { - decimal priceInclTax = Round(context.QuantityNew * oi.UnitPriceInclTax); - decimal priceExclTax = Round(context.QuantityNew * oi.UnitPriceExclTax); + var currency = _currencyService.GetCurrencyByCode(oi.Order.CustomerCurrencyCode); + + decimal priceInclTax = (context.QuantityNew * oi.UnitPriceInclTax).RoundIfEnabledFor(currency); + decimal priceExclTax = (context.QuantityNew * oi.UnitPriceExclTax).RoundIfEnabledFor(currency); decimal deltaPriceInclTax = priceInclTax - (context.IsNewOrderItem ? decimal.Zero : oi.PriceInclTax); decimal deltaPriceExclTax = priceExclTax - (context.IsNewOrderItem ? decimal.Zero : oi.PriceExclTax); oi.Quantity = context.QuantityNew; - oi.PriceInclTax = Round(priceInclTax); - oi.PriceExclTax = Round(priceExclTax); + oi.PriceInclTax = priceInclTax.RoundIfEnabledFor(currency); + oi.PriceExclTax = priceExclTax.RoundIfEnabledFor(currency); decimal subtotalInclTax = oi.Order.OrderSubtotalInclTax + deltaPriceInclTax; decimal subtotalExclTax = oi.Order.OrderSubtotalExclTax + deltaPriceExclTax; - oi.Order.OrderSubtotalInclTax = Round(subtotalInclTax); - oi.Order.OrderSubtotalExclTax = Round(subtotalExclTax); + oi.Order.OrderSubtotalInclTax = subtotalInclTax.RoundIfEnabledFor(currency); + oi.Order.OrderSubtotalExclTax = subtotalExclTax.RoundIfEnabledFor(currency); decimal discountInclTax = oi.DiscountAmountInclTax * context.QuantityChangeFactor; decimal discountExclTax = oi.DiscountAmountExclTax * context.QuantityChangeFactor; @@ -1802,14 +1759,27 @@ public virtual void AutoUpdateOrderDetails(AutoUpdateOrderItemContext context) decimal deltaDiscountInclTax = discountInclTax - oi.DiscountAmountInclTax; decimal deltaDiscountExclTax = discountExclTax - oi.DiscountAmountExclTax; - oi.DiscountAmountInclTax = Round(discountInclTax); - oi.DiscountAmountExclTax = Round(discountExclTax); + oi.DiscountAmountInclTax = discountInclTax.RoundIfEnabledFor(currency); + oi.DiscountAmountExclTax = discountExclTax.RoundIfEnabledFor(currency); decimal total = Math.Max(oi.Order.OrderTotal + deltaPriceInclTax, 0); decimal tax = Math.Max(oi.Order.OrderTax + (deltaPriceInclTax - deltaPriceExclTax), 0); - oi.Order.OrderTotal = Round(total); - oi.Order.OrderTax = Round(tax); + oi.Order.OrderTotal = total.RoundIfEnabledFor(currency); + oi.Order.OrderTax = tax.RoundIfEnabledFor(currency); + + // Update tax rate value. + var deltaTax = deltaPriceInclTax - deltaPriceExclTax; + if (deltaTax != decimal.Zero) + { + var taxRates = oi.Order.TaxRatesDictionary; + + taxRates[oi.TaxRate] = taxRates.ContainsKey(oi.TaxRate) + ? Math.Max(taxRates[oi.TaxRate] + deltaTax, 0) + : Math.Max(deltaTax, 0); + + oi.Order.TaxRates = FormatTaxRates(taxRates); + } _orderService.UpdateOrder(oi.Order); } @@ -2052,12 +2022,15 @@ public virtual bool CanRefund(Order order) if (order.OrderTotal == decimal.Zero) return false; - //uncomment the lines below in order to allow this operation for cancelled orders - //if (order.OrderStatus == OrderStatus.Cancelled) - // return false; + // Only partial refunds allowed if already refunded. + if (order.RefundedAmount > decimal.Zero) + return false; + + // Uncomment the lines below in order to allow this operation for cancelled orders. + //if (order.OrderStatus == OrderStatus.Cancelled) + // return false; - if (order.PaymentStatus == PaymentStatus.Paid && - _paymentService.SupportRefund(order.PaymentMethodSystemName)) + if (order.PaymentStatus == PaymentStatus.Paid && _paymentService.SupportRefund(order.PaymentMethodSystemName)) return true; return false; @@ -2088,10 +2061,10 @@ public virtual IList Refund(Order order) if (result.Success) { - //total amount refunded + // Total amount refunded. decimal totalAmountRefunded = order.RefundedAmount + request.AmountToRefund; - //update order info + // Update order info. order.RefundedAmount = totalAmountRefunded; order.PaymentStatus = result.NewPaymentStatus; @@ -2099,7 +2072,6 @@ public virtual IList Refund(Order order) _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderRefunded", _priceFormatter.FormatPrice(request.AmountToRefund, true, false))); - //check order status CheckOrderStatus(order); } @@ -2132,11 +2104,15 @@ public virtual bool CanRefundOffline(Order order) if (order.OrderTotal == decimal.Zero) return false; - //uncomment the lines below in order to allow this operation for cancelled orders - //if (order.OrderStatus == OrderStatus.Cancelled) - // return false; + // Only partial refunds allowed if already refunded. + if (order.RefundedAmount > decimal.Zero) + return false; - if (order.PaymentStatus == PaymentStatus.Paid) + // Uncomment the lines below in order to allow this operation for cancelled orders. + //if (order.OrderStatus == OrderStatus.Cancelled) + // return false; + + if (order.PaymentStatus == PaymentStatus.Paid) return true; return false; @@ -2154,13 +2130,13 @@ public virtual void RefundOffline(Order order) if (!CanRefundOffline(order)) throw new SmartException(T("Order.CannotRefund")); - //amout to refund + // Amout to refund. decimal amountToRefund = order.OrderTotal; - //total amount refunded + // Total amount refunded. decimal totalAmountRefunded = order.RefundedAmount + amountToRefund; - //update order info + // Update order info. order.RefundedAmount = totalAmountRefunded; order.PaymentStatus = PaymentStatus.Refunded; @@ -2168,7 +2144,6 @@ public virtual void RefundOffline(Order order) _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsRefunded", _priceFormatter.FormatPrice(amountToRefund, true, false))); - //check order status CheckOrderStatus(order); } diff --git a/src/Libraries/SmartStore.Services/Orders/OrderService.cs b/src/Libraries/SmartStore.Services/Orders/OrderService.cs index 1828b461ae..62714bfebe 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderService.cs @@ -324,9 +324,6 @@ public virtual void InsertOrder(Order order) throw new ArgumentNullException("order"); _orderRepository.Insert(order); - - //event notification - _eventPublisher.EntityInserted(order); } /// @@ -340,8 +337,6 @@ public virtual void UpdateOrder(Order order) _orderRepository.Update(order); - //event notifications - _eventPublisher.EntityUpdated(order); _eventPublisher.PublishOrderUpdated(order); } @@ -371,9 +366,6 @@ public virtual void DeleteOrderNote(OrderNote orderNote) _orderNoteRepository.Delete(orderNote); - //event notifications - _eventPublisher.EntityDeleted(orderNote); - var order = GetOrderById(orderId); _eventPublisher.PublishOrderUpdated(order); } @@ -526,9 +518,6 @@ public virtual void DeleteOrderItem(OrderItem orderItem) _orderItemRepository.Delete(orderItem); - //event notifications - _eventPublisher.EntityDeleted(orderItem); - var order = GetOrderById(orderId); _eventPublisher.PublishOrderUpdated(order); } @@ -574,8 +563,6 @@ public virtual void InsertRecurringPayment(RecurringPayment recurringPayment) _recurringPaymentRepository.Insert(recurringPayment); - //event notification - _eventPublisher.EntityInserted(recurringPayment); _eventPublisher.PublishOrderUpdated(recurringPayment.InitialOrder); } @@ -590,8 +577,6 @@ public virtual void UpdateRecurringPayment(RecurringPayment recurringPayment) _recurringPaymentRepository.Update(recurringPayment); - //event notification - _eventPublisher.EntityUpdated(recurringPayment); _eventPublisher.PublishOrderUpdated(recurringPayment.InitialOrder); } @@ -651,9 +636,6 @@ public virtual void DeleteReturnRequest(ReturnRequest returnRequest) _returnRequestRepository.Delete(returnRequest); - //event notifications - _eventPublisher.EntityDeleted(returnRequest); - var orderItem = GetOrderItemById(orderItemId); _eventPublisher.PublishOrderUpdated(orderItem.Order); } diff --git a/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs b/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs index 7de6c5617f..98d7357f15 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs @@ -5,6 +5,7 @@ using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Discounts; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Shipping; @@ -13,6 +14,7 @@ using SmartStore.Core.Plugins; using SmartStore.Services.Catalog; using SmartStore.Services.Common; +using SmartStore.Services.Directory; using SmartStore.Services.Discounts; using SmartStore.Services.Payments; using SmartStore.Services.Shipping; @@ -20,10 +22,10 @@ namespace SmartStore.Services.Orders { - /// - /// Order service - /// - public partial class OrderTotalCalculationService : IOrderTotalCalculationService + /// + /// Order service + /// + public partial class OrderTotalCalculationService : IOrderTotalCalculationService { private const string CART_TAXING_INFO_KEY = "CartTaxingInfos"; @@ -39,7 +41,9 @@ public partial class OrderTotalCalculationService : IOrderTotalCalculationServic private readonly IDiscountService _discountService; private readonly IGiftCardService _giftCardService; private readonly IGenericAttributeService _genericAttributeService; - private readonly IProductAttributeParser _productAttributeParser; + private readonly IPaymentService _paymentService; + private readonly ICurrencyService _currencyService; + private readonly IProductAttributeParser _productAttributeParser; private readonly TaxSettings _taxSettings; private readonly RewardPointsSettings _rewardPointsSettings; private readonly ShippingSettings _shippingSettings; @@ -68,7 +72,8 @@ public partial class OrderTotalCalculationService : IOrderTotalCalculationServic /// Shipping settings /// Shopping cart settings /// Catalog settings - public OrderTotalCalculationService(IWorkContext workContext, + public OrderTotalCalculationService( + IWorkContext workContext, IStoreContext storeContext, IPriceCalculationService priceCalculationService, ITaxService taxService, @@ -78,29 +83,33 @@ public OrderTotalCalculationService(IWorkContext workContext, IDiscountService discountService, IGiftCardService giftCardService, IGenericAttributeService genericAttributeService, - IProductAttributeParser productAttributeParser, + IPaymentService paymentService, + ICurrencyService currencyService, + IProductAttributeParser productAttributeParser, TaxSettings taxSettings, RewardPointsSettings rewardPointsSettings, ShippingSettings shippingSettings, ShoppingCartSettings shoppingCartSettings, CatalogSettings catalogSettings) { - this._workContext = workContext; - this._storeContext = storeContext; - this._priceCalculationService = priceCalculationService; - this._taxService = taxService; - this._shippingService = shippingService; - this._providerManager = providerManager; - this._checkoutAttributeParser = checkoutAttributeParser; - this._discountService = discountService; - this._giftCardService = giftCardService; - this._genericAttributeService = genericAttributeService; - this._productAttributeParser = productAttributeParser; - this._taxSettings = taxSettings; - this._rewardPointsSettings = rewardPointsSettings; - this._shippingSettings = shippingSettings; - this._shoppingCartSettings = shoppingCartSettings; - this._catalogSettings = catalogSettings; + _workContext = workContext; + _storeContext = storeContext; + _priceCalculationService = priceCalculationService; + _taxService = taxService; + _shippingService = shippingService; + _providerManager = providerManager; + _checkoutAttributeParser = checkoutAttributeParser; + _discountService = discountService; + _giftCardService = giftCardService; + _genericAttributeService = genericAttributeService; + _paymentService = paymentService; + _currencyService = currencyService; + _productAttributeParser = productAttributeParser; + _taxSettings = taxSettings; + _rewardPointsSettings = rewardPointsSettings; + _shippingSettings = shippingSettings; + _shoppingCartSettings = shoppingCartSettings; + _catalogSettings = catalogSettings; T = NullLocalizer.Instance; } @@ -196,7 +205,7 @@ protected virtual void PrepareAuxiliaryServicesTaxingInfos(IList car return; // get the customer - Customer customer = cart.GetCustomer(); + var customer = cart.GetCustomer(); + var currency = _workContext.WorkingCurrency; - // sub totals - decimal subTotalExclTaxWithoutDiscount = decimal.Zero; + // sub totals + decimal subTotalExclTaxWithoutDiscount = decimal.Zero; decimal subTotalInclTaxWithoutDiscount = decimal.Zero; foreach (var shoppingCartItem in cart) @@ -379,7 +389,7 @@ public virtual void GetShoppingCartSubTotal(IList car shoppingCartItem.Item.Product.MergeWithCombination(shoppingCartItem.Item.AttributesXml, _productAttributeParser); - if (_shoppingCartSettings.RoundPricesDuringCalculation) + if (currency.RoundOrderItemsEnabled) { // Gross > Net RoundFix int qty = shoppingCartItem.Item.Quantity; @@ -388,9 +398,9 @@ public virtual void GetShoppingCartSubTotal(IList car // Adaption to eliminate rounding issues sciExclTax = _taxService.GetProductPrice(shoppingCartItem.Item.Product, sciSubTotal, false, customer, out taxRate); - sciExclTax = Math.Round(sciExclTax, 2) * qty; + sciExclTax = sciExclTax.RoundIfEnabledFor(currency) * qty; sciInclTax = _taxService.GetProductPrice(shoppingCartItem.Item.Product, sciSubTotal, true, customer, out taxRate); - sciInclTax = Math.Round(sciInclTax, 2) * qty; + sciInclTax = sciInclTax.RoundIfEnabledFor(currency) * qty; } else { @@ -450,17 +460,22 @@ public virtual void GetShoppingCartSubTotal(IList car } } - //subtotal without discount - if (includingTax) - subTotalWithoutDiscount = subTotalInclTaxWithoutDiscount; - else - subTotalWithoutDiscount = subTotalExclTaxWithoutDiscount; + //subtotal without discount + if (includingTax) + { + subTotalWithoutDiscount = subTotalInclTaxWithoutDiscount; + } + else + { + subTotalWithoutDiscount = subTotalExclTaxWithoutDiscount; + } - if (subTotalWithoutDiscount < decimal.Zero) - subTotalWithoutDiscount = decimal.Zero; + if (subTotalWithoutDiscount < decimal.Zero) + { + subTotalWithoutDiscount = decimal.Zero; + } - if (_shoppingCartSettings.RoundPricesDuringCalculation) - subTotalWithoutDiscount = Math.Round(subTotalWithoutDiscount, 2); + subTotalWithoutDiscount = subTotalWithoutDiscount.RoundIfEnabledFor(currency); /*We calculate discount amount on order subtotal excl tax (discount first)*/ //calculate discount amount ('Applied to order subtotal' discount) @@ -489,8 +504,7 @@ public virtual void GetShoppingCartSubTotal(IList car decimal discountTax = taxRates[taxRate] * (discountAmountExclTax / subTotalExclTaxWithoutDiscount); discountAmountInclTax += discountTax; taxValue = taxRates[taxRate] - discountTax; - if (_shoppingCartSettings.RoundPricesDuringCalculation) - taxValue = Math.Round(taxValue, 2); + taxValue = taxValue.RoundIfEnabledFor(currency); taxRates[taxRate] = taxValue; } @@ -499,8 +513,7 @@ public virtual void GetShoppingCartSubTotal(IList car } } - if (_shoppingCartSettings.RoundPricesDuringCalculation) - discountAmountInclTax = Math.Round(discountAmountInclTax, 2); + discountAmountInclTax = discountAmountInclTax.RoundIfEnabledFor(currency); if (includingTax) { @@ -513,12 +526,13 @@ public virtual void GetShoppingCartSubTotal(IList car discountAmount = discountAmountExclTax; } - //round - if (subTotalWithDiscount < decimal.Zero) - subTotalWithDiscount = decimal.Zero; + //round + if (subTotalWithDiscount < decimal.Zero) + { + subTotalWithDiscount = decimal.Zero; + } - if (_shoppingCartSettings.RoundPricesDuringCalculation) - subTotalWithDiscount = Math.Round(subTotalWithDiscount, 2); + subTotalWithDiscount = subTotalWithDiscount.RoundIfEnabledFor(currency); } /// @@ -559,10 +573,7 @@ public virtual decimal GetOrderSubtotalDiscount(Customer customer, return discountAmount; } - - - - + /// /// Gets shopping cart additional shipping charge /// @@ -577,17 +588,28 @@ public virtual decimal GetShoppingCartAdditionalShippingCharge(IList additionalShippingCharge += (x.Item.Product.AdditionalShippingCharge * x.Item.Quantity)); - } - else - { - additionalShippingCharge += sci.Item.Product.AdditionalShippingCharge * sci.Item.Quantity; - } - } + + if (_shippingSettings.ChargeOnlyHighestProductShippingSurcharge) + { + if (additionalShippingCharge < sci.Item.Product.AdditionalShippingCharge) + { + additionalShippingCharge = sci.Item.Product.AdditionalShippingCharge; + } + } + else + { + if (sci.Item.IsShipEnabled && !sci.Item.IsFreeShipping && sci.Item.Product != null) + { + if (sci.Item.Product.ProductType == ProductType.BundledProduct && sci.Item.Product.BundlePerItemShipping) + { + sci.ChildItems.Each(x => additionalShippingCharge += (x.Item.Product.AdditionalShippingCharge * x.Item.Quantity)); + } + else + { + additionalShippingCharge += sci.Item.Product.AdditionalShippingCharge * sci.Item.Quantity; + } + } + } } return additionalShippingCharge; } @@ -702,12 +724,12 @@ public virtual decimal AdjustShippingRate(decimal shippingRate, IList cart, out So var shippingTax = decimal.Zero; var paymentFeeTax = decimal.Zero; var customer = cart.GetCustomer(); + var currency = _workContext.WorkingCurrency; //// (VATFIX) if (_taxService.IsVatExempt(null, customer)) @@ -961,10 +985,11 @@ public virtual decimal GetTaxTotal(IList cart, out So if (shippingTotal.HasValue) { if (shippingTotal.Value < decimal.Zero) + { shippingTotal = decimal.Zero; + } - if (_shoppingCartSettings.RoundPricesDuringCalculation) - shippingTotal = Math.Round(shippingTotal.Value, 2); + shippingTotal = shippingTotal.Value.RoundIfEnabledFor(currency); PrepareAuxiliaryServicesTaxingInfos(cart); @@ -994,12 +1019,12 @@ public virtual decimal GetTaxTotal(IList cart, out So // fallback to setting if (taxCategoryId == 0) + { taxCategoryId = _taxSettings.ShippingTaxClassId; + } shippingTax = GetShippingTaxAmount(shippingTotal.Value, customer, taxCategoryId, taxRates); - - if (_shoppingCartSettings.RoundPricesDuringCalculation) - shippingTax = Math.Round(shippingTax, 2); + shippingTax = shippingTax.RoundIfEnabledFor(currency); } } @@ -1016,9 +1041,7 @@ public virtual decimal GetTaxTotal(IList cart, out So { var taxCategoryId = 0; var paymentFee = provider.Value.GetAdditionalHandlingFee(cart); - - if (_shoppingCartSettings.RoundPricesDuringCalculation) - paymentFee = Math.Round(paymentFee, 2); + paymentFee = paymentFee.RoundIfEnabledFor(currency); PrepareAuxiliaryServicesTaxingInfos(cart); @@ -1068,9 +1091,7 @@ public virtual decimal GetTaxTotal(IList cart, out So taxTotal = decimal.Zero; // round tax - if (_shoppingCartSettings.RoundPricesDuringCalculation) - taxTotal = Math.Round(taxTotal, 2); - + taxTotal = taxTotal.RoundIfEnabledFor(currency); return taxTotal; } @@ -1078,172 +1099,126 @@ public virtual decimal GetTaxTotal(IList cart, out So - /// - /// Gets shopping cart total - /// - /// Cart - /// A value indicating whether we should ignore reward points (if enabled and a customer is going to use them) - /// A value indicating whether we should use payment method additional fee when calculating order total - /// Shopping cart total;Null if shopping cart total couldn't be calculated now - public virtual decimal? GetShoppingCartTotal(IList cart, - bool ignoreRewardPonts = false, bool usePaymentMethodAdditionalFee = true) + public virtual ShoppingCartTotal GetShoppingCartTotal( + IList cart, + bool ignoreRewardPonts = false, + bool usePaymentMethodAdditionalFee = true) { - decimal discountAmount = decimal.Zero; - Discount appliedDiscount = null; - - int redeemedRewardPoints = 0; - decimal redeemedRewardPointsAmount = decimal.Zero; - List appliedGiftCards = null; - - return GetShoppingCartTotal(cart, out discountAmount, out appliedDiscount, - out appliedGiftCards, out redeemedRewardPoints, out redeemedRewardPointsAmount, ignoreRewardPonts, usePaymentMethodAdditionalFee); - } - - /// - /// Gets shopping cart total - /// - /// Cart - /// Applied gift cards - /// Applied discount amount - /// Applied discount - /// Reward points to redeem - /// Reward points amount in primary store currency to redeem - /// A value indicating whether we should ignore reward points (if enabled and a customer is going to use them) - /// A value indicating whether we should use payment method additional fee when calculating order total - /// Shopping cart total;Null if shopping cart total couldn't be calculated now - public virtual decimal? GetShoppingCartTotal(IList cart, - out decimal discountAmount, out Discount appliedDiscount, - out List appliedGiftCards, - out int redeemedRewardPoints, out decimal redeemedRewardPointsAmount, - bool ignoreRewardPonts = false, bool usePaymentMethodAdditionalFee = true) - { - redeemedRewardPoints = 0; - redeemedRewardPointsAmount = decimal.Zero; - var customer = cart.GetCustomer(); - string paymentMethodSystemName = ""; + var store = _storeContext.CurrentStore; + var currency = _workContext.WorkingCurrency; + var paymentMethodSystemName = ""; + if (customer != null) - { - paymentMethodSystemName = customer.GetAttribute(SystemCustomerAttributeNames.SelectedPaymentMethod, _genericAttributeService, _storeContext.CurrentStore.Id); - } + { + paymentMethodSystemName = customer.GetAttribute(SystemCustomerAttributeNames.SelectedPaymentMethod, _genericAttributeService, store.Id); + } - //subtotal without tax - decimal subtotalBase = decimal.Zero; - decimal orderSubTotalDiscountAmount = decimal.Zero; + // Subtotal without tax + var subtotalBase = decimal.Zero; + var orderSubTotalDiscountAmount = decimal.Zero; Discount orderSubTotalAppliedDiscount = null; - decimal subTotalWithoutDiscountBase = decimal.Zero; - decimal subTotalWithDiscountBase = decimal.Zero; + var subTotalWithoutDiscountBase = decimal.Zero; + var subTotalWithDiscountBase = decimal.Zero; GetShoppingCartSubTotal(cart, false, out orderSubTotalDiscountAmount, out orderSubTotalAppliedDiscount, out subTotalWithoutDiscountBase, out subTotalWithDiscountBase); - //subtotal with discount + // Subtotal with discount subtotalBase = subTotalWithDiscountBase; - //shipping without tax + // Shipping without tax decimal? shoppingCartShipping = GetShoppingCartShippingTotal(cart, false); - //payment method additional fee without tax - decimal paymentMethodAdditionalFeeWithoutTax = decimal.Zero; - if (usePaymentMethodAdditionalFee && !String.IsNullOrEmpty(paymentMethodSystemName)) + // Payment method additional fee without tax + var paymentMethodAdditionalFeeWithoutTax = decimal.Zero; + if (usePaymentMethodAdditionalFee && !string.IsNullOrEmpty(paymentMethodSystemName)) { - var provider = _providerManager.GetProvider(paymentMethodSystemName); - var paymentMethodAdditionalFee = (provider != null ? provider.Value.GetAdditionalHandlingFee(cart) : decimal.Zero); - - if (_shoppingCartSettings.RoundPricesDuringCalculation) - { - paymentMethodAdditionalFee = Math.Round(paymentMethodAdditionalFee, 2); - } + var provider = _providerManager.GetProvider(paymentMethodSystemName); + var paymentMethodAdditionalFee = (provider != null ? provider.Value.GetAdditionalHandlingFee(cart) : decimal.Zero); - paymentMethodAdditionalFeeWithoutTax = _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, false, customer); + paymentMethodAdditionalFee = paymentMethodAdditionalFee.RoundIfEnabledFor(currency); + paymentMethodAdditionalFeeWithoutTax = _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, false, customer); } - //tax - decimal shoppingCartTax = GetTaxTotal(cart, usePaymentMethodAdditionalFee); + // Tax + var shoppingCartTax = GetTaxTotal(cart, usePaymentMethodAdditionalFee); - //order total - decimal resultTemp = decimal.Zero; + // Order total + var resultTemp = decimal.Zero; resultTemp += subtotalBase; if (shoppingCartShipping.HasValue) { resultTemp += shoppingCartShipping.Value; } - resultTemp += paymentMethodAdditionalFeeWithoutTax; - - ////// (VATFIX) - ////resultTemp += shoppingCartTax; - //if (_taxService.IsVatExempt(null, customer)) - //{ - // // add nothing to total - //} - //else - //{ - resultTemp += shoppingCartTax; - //} - if (_shoppingCartSettings.RoundPricesDuringCalculation) - resultTemp = Math.Round(resultTemp, 2); + resultTemp += paymentMethodAdditionalFeeWithoutTax; + resultTemp += shoppingCartTax; + resultTemp = resultTemp.RoundIfEnabledFor(currency); #region Order total discount - discountAmount = GetOrderTotalDiscount(customer, resultTemp, out appliedDiscount); + Discount appliedDiscount = null; + var discountAmount = GetOrderTotalDiscount(customer, resultTemp, out appliedDiscount); - //sub totals with discount + // Sub totals with discount if (resultTemp < discountAmount) discountAmount = resultTemp; - //reduce subtotal + // Reduce subtotal resultTemp -= discountAmount; if (resultTemp < decimal.Zero) + { resultTemp = decimal.Zero; - if (_shoppingCartSettings.RoundPricesDuringCalculation) - resultTemp = Math.Round(resultTemp, 2); + } + + resultTemp = resultTemp.RoundIfEnabledFor(currency); #endregion #region Applied gift cards - //let's apply gift cards now (gift cards that can be used) - appliedGiftCards = new List(); + // Let's apply gift cards now (gift cards that can be used) + var appliedGiftCards = new List(); if (!cart.IsRecurring()) { - //we don't apply gift cards for recurring products - var giftCards = _giftCardService.GetActiveGiftCardsAppliedByCustomer(customer, _storeContext.CurrentStore.Id); - if (giftCards != null) - { - foreach (var gc in giftCards) - { - if (resultTemp > decimal.Zero) - { - decimal remainingAmount = gc.GetGiftCardRemainingAmount(); - decimal amountCanBeUsed = decimal.Zero; - if (resultTemp > remainingAmount) - amountCanBeUsed = remainingAmount; - else - amountCanBeUsed = resultTemp; - - //reduce subtotal - resultTemp -= amountCanBeUsed; - - var appliedGiftCard = new AppliedGiftCard(); - appliedGiftCard.GiftCard = gc; - appliedGiftCard.AmountCanBeUsed = amountCanBeUsed; - appliedGiftCards.Add(appliedGiftCard); - } - } - } + // We don't apply gift cards for recurring products + var giftCards = _giftCardService.GetActiveGiftCardsAppliedByCustomer(customer, store.Id); + if (giftCards != null) + { + foreach (var gc in giftCards) + { + if (resultTemp > decimal.Zero) + { + var remainingAmount = gc.GetGiftCardRemainingAmount(); + var amountCanBeUsed = resultTemp > remainingAmount ? remainingAmount : resultTemp; + + // Reduce subtotal + resultTemp -= amountCanBeUsed; + + appliedGiftCards.Add(new AppliedGiftCard + { + GiftCard = gc, + AmountCanBeUsed = amountCanBeUsed + }); + } + } + } } #endregion #region Reward points + var redeemedRewardPoints = 0; + var redeemedRewardPointsAmount = decimal.Zero; + if (_rewardPointsSettings.Enabled && !ignoreRewardPonts && customer != null && - customer.GetAttribute(SystemCustomerAttributeNames.UseRewardPointsDuringCheckout, _genericAttributeService, _storeContext.CurrentStore.Id)) + customer.GetAttribute(SystemCustomerAttributeNames.UseRewardPointsDuringCheckout, _genericAttributeService, store.Id)) { - int rewardPointsBalance = customer.GetRewardPointsBalance(); - decimal rewardPointsBalanceAmount = ConvertRewardPointsToAmount(rewardPointsBalance); + var rewardPointsBalance = customer.GetRewardPointsBalance(); + var rewardPointsBalanceAmount = ConvertRewardPointsToAmount(rewardPointsBalance); if (resultTemp > decimal.Zero) { @@ -1259,36 +1234,53 @@ public virtual decimal GetTaxTotal(IList cart, out So } } } + #endregion if (resultTemp < decimal.Zero) + { resultTemp = decimal.Zero; - if (_shoppingCartSettings.RoundPricesDuringCalculation) - resultTemp = Math.Round(resultTemp, 2); + } + resultTemp = resultTemp.RoundIfEnabledFor(currency); - - decimal? orderTotal = null; - if (!shoppingCartShipping.HasValue) - { - //return null if we have errors - orderTotal = null; - return orderTotal; - } - else - { - //return result if we have no errors - orderTotal = resultTemp; - } + // Return null if we have errors + var roundingAmount = decimal.Zero; + var roundingAmountConverted = decimal.Zero; + var orderTotal = shoppingCartShipping.HasValue ? resultTemp : (decimal?)null; + var orderTotalConverted = orderTotal; if (orderTotal.HasValue) { orderTotal = orderTotal.Value - redeemedRewardPointsAmount; - if (_shoppingCartSettings.RoundPricesDuringCalculation) - orderTotal = Math.Round(orderTotal.Value, 2); - return orderTotal; + orderTotal = orderTotal.Value.RoundIfEnabledFor(currency); + + orderTotalConverted = _currencyService.ConvertFromPrimaryStoreCurrency(orderTotal.Value, currency, store); + + // Order total rounding + if (currency.RoundOrderTotalEnabled && paymentMethodSystemName.HasValue()) + { + var paymentMethod = _paymentService.GetPaymentMethodBySystemName(paymentMethodSystemName); + if (paymentMethod != null && paymentMethod.RoundOrderTotalEnabled) + { + orderTotal = orderTotal.Value.RoundToNearest(currency, out roundingAmount); + orderTotalConverted = orderTotalConverted.Value.RoundToNearest(currency, out roundingAmountConverted); + } + } } - return null; + + var result = new ShoppingCartTotal(orderTotal); + result.RoundingAmount = roundingAmount; + result.DiscountAmount = discountAmount; + result.AppliedDiscount = appliedDiscount; + result.AppliedGiftCards = appliedGiftCards; + result.RedeemedRewardPoints = redeemedRewardPoints; + result.RedeemedRewardPointsAmount = redeemedRewardPointsAmount; + + result.ConvertedFromPrimaryStoreCurrency.TotalAmount = orderTotalConverted; + result.ConvertedFromPrimaryStoreCurrency.RoundingAmount = roundingAmountConverted; + + return result; } /// @@ -1326,9 +1318,7 @@ public virtual decimal GetOrderTotalDiscount(Customer customer, decimal orderTot if (discountAmount < decimal.Zero) discountAmount = decimal.Zero; - if (_shoppingCartSettings.RoundPricesDuringCalculation) - discountAmount = Math.Round(discountAmount, 2); - + discountAmount = discountAmount.RoundIfEnabledFor(_workContext.WorkingCurrency); return discountAmount; } @@ -1348,8 +1338,8 @@ public virtual decimal ConvertRewardPointsToAmount(int rewardPoints) return decimal.Zero; result = rewardPoints * _rewardPointsSettings.ExchangeRate; - if (_shoppingCartSettings.RoundPricesDuringCalculation) - result = Math.Round(result, 2); + result = result.RoundIfEnabledFor(_workContext.WorkingCurrency); + return result; } diff --git a/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs b/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs index 8e099e89df..9b64e31423 100644 --- a/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs +++ b/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs @@ -27,7 +27,7 @@ public partial class ShoppingCartService : IShoppingCartService { // 0 = CustomerId, 1 = CartType, 2 = StoreId const string CARTITEMS_KEY = "sm.cartitems-{0}-{1}-{2}"; - const string CARTITEMS_PATTERN_KEY = "sm.cartitems-"; + const string CARTITEMS_PATTERN_KEY = "sm.cartitems-*"; private readonly IRepository _sciRepository; private readonly IWorkContext _workContext; @@ -143,7 +143,7 @@ public virtual List GetCartItems(Customer customer, S return result; } - protected List OrganizeCartItems(IEnumerable cart) + protected virtual List OrganizeCartItems(IEnumerable cart) { var result = new List(); @@ -226,9 +226,6 @@ public virtual void DeleteShoppingCartItem( _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.CheckoutAttributes, checkoutAttributesXml); } - // event notification - _eventPublisher.EntityDeleted(shoppingCartItem); - // delete child items if (deleteChildCartItems) { @@ -1099,9 +1096,6 @@ public virtual List AddToCart( existingCartItem.Item.Quantity = newQuantity; existingCartItem.Item.UpdatedOnUtc = DateTime.UtcNow; _customerService.UpdateCustomer(customer); - - // event notification - _eventPublisher.EntityUpdated(existingCartItem.Item); } } else @@ -1147,7 +1141,6 @@ public virtual List AddToCart( { customer.ShoppingCartItems.Add(cartItem); _customerService.UpdateCustomer(customer); - _eventPublisher.EntityInserted(cartItem); } else { @@ -1247,7 +1240,6 @@ public virtual void AddToCartStoring(AddToCartContext ctx) customer.ShoppingCartItems.Add(ctx.Item); _customerService.UpdateCustomer(customer); - _eventPublisher.EntityInserted(ctx.Item); foreach (var childItem in ctx.ChildItems) { @@ -1255,7 +1247,6 @@ public virtual void AddToCartStoring(AddToCartContext ctx) customer.ShoppingCartItems.Add(childItem); _customerService.UpdateCustomer(customer); - _eventPublisher.EntityInserted(childItem); } } } @@ -1286,9 +1277,6 @@ public virtual IList UpdateShoppingCartItem(Customer customer, int shopp shoppingCartItem.Quantity = newQuantity; shoppingCartItem.UpdatedOnUtc = DateTime.UtcNow; _customerService.UpdateCustomer(customer); - - // event notification - _eventPublisher.EntityUpdated(shoppingCartItem); } } else diff --git a/src/Libraries/SmartStore.Services/Orders/ShoppingCartTotal.cs b/src/Libraries/SmartStore.Services/Orders/ShoppingCartTotal.cs new file mode 100644 index 0000000000..487d3fb724 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Orders/ShoppingCartTotal.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using SmartStore.Core.Domain.Discounts; + +namespace SmartStore.Services.Orders +{ + public class ShoppingCartTotal + { + public ShoppingCartTotal(decimal? totalAmount) + { + TotalAmount = totalAmount; + ConvertedFromPrimaryStoreCurrency = new ConvertedAmounts(); + } + + public static implicit operator decimal?(ShoppingCartTotal obj) + { + return obj.TotalAmount; + } + + public static implicit operator ShoppingCartTotal(decimal? obj) + { + return new ShoppingCartTotal(obj); + } + + /// + /// Total amount of the shopping cart. null if the cart total couldn't be calculated now. + /// + public decimal? TotalAmount { get; private set; } + + /// + /// Rounding amount + /// + public decimal RoundingAmount { get; set; } + + /// + /// Applied discount amount + /// + public decimal DiscountAmount { get; set; } + + /// + /// Applied discount + /// + public Discount AppliedDiscount { get; set; } + + /// + /// Applied gift cards + /// + public List AppliedGiftCards { get; set; } + + /// + /// Reward points to redeem + /// + public int RedeemedRewardPoints { get; set; } + + /// + /// Reward points amount to redeem (in primary store currency) + /// + public decimal RedeemedRewardPointsAmount { get; set; } + + public ConvertedAmounts ConvertedFromPrimaryStoreCurrency { get; set; } + + public override string ToString() + { + return (TotalAmount ?? decimal.Zero).FormatInvariant(); + } + + public class ConvertedAmounts + { + /// + /// Converted total amount of the shopping cart. null if the cart total couldn't be calculated now. + /// + public decimal? TotalAmount { get; set; } + + /// + /// Converted rounding amount + /// + public decimal RoundingAmount { get; set; } + } + } +} diff --git a/src/Libraries/SmartStore.Services/Payments/CapturePaymentHook.cs b/src/Libraries/SmartStore.Services/Payments/CapturePaymentHook.cs new file mode 100644 index 0000000000..1b30b02387 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Payments/CapturePaymentHook.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SmartStore.Core.Data.Hooks; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Payments; +using SmartStore.Core.Domain.Shipping; +using SmartStore.Services.Orders; + +namespace SmartStore.Services.Payments +{ + public class CapturePaymentHook : DbSaveHook + { + private readonly Lazy _services; + private readonly Lazy _orderProcessingService; + private readonly HashSet _toCapture = new HashSet(); + + public CapturePaymentHook( + Lazy services, + Lazy orderProcessingService) + { + _services = services; + _orderProcessingService = orderProcessingService; + } + + private bool IsStatusPropertyModifiedTo(IHookedEntity entry, string propertyName, int statusId) + { + var prop = entry.Entry.Property(propertyName); + + if (prop != null && prop.CurrentValue != null) + { + if (!prop.CurrentValue.Equals(prop.OriginalValue)) + { + return (int)prop.CurrentValue == statusId; + } + } + + return false; + } + + protected override void OnUpdating(Order entity, IHookedEntity entry) + { + if (entry.State == Core.Data.EntityState.Modified) + { + var isShipped = IsStatusPropertyModifiedTo(entry, nameof(entity.ShippingStatusId), (int)ShippingStatus.Shipped); + var isDelivered = IsStatusPropertyModifiedTo(entry, nameof(entity.ShippingStatusId), (int)ShippingStatus.Delivered); + + if (isShipped || isDelivered) + { + var settings = _services.Value.Settings.LoadSetting(entity.StoreId); + if (settings.CapturePaymentReason.HasValue) + { + if (isShipped && settings.CapturePaymentReason.Value == CapturePaymentReason.OrderShipped) + { + _toCapture.Add(entity); + } + else if (isDelivered && settings.CapturePaymentReason.Value == CapturePaymentReason.OrderDelivered) + { + _toCapture.Add(entity); + } + } + } + + //if (IsStatusPropertyModifiedTo(entry, nameof(entity.OrderStatusId), (int)OrderStatus.Complete)) + //{ + // That's too late. The payment is already marked as paid and the capture process would never be executed. + //} + } + } + + public override void OnAfterSave(IHookedEntity entry) + { + // Do not remove. + } + + public override void OnAfterSaveCompleted() + { + if (_toCapture.Any()) + { + foreach (var order in _toCapture) + { + if (_orderProcessingService.Value.CanCapture(order)) + { + _orderProcessingService.Value.Capture(order); + } + } + + _toCapture.Clear(); + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/Payments/IPaymentService.cs b/src/Libraries/SmartStore.Services/Payments/IPaymentService.cs index 45cf918f13..d4fbe4e740 100644 --- a/src/Libraries/SmartStore.Services/Payments/IPaymentService.cs +++ b/src/Libraries/SmartStore.Services/Payments/IPaymentService.cs @@ -41,13 +41,15 @@ IEnumerable> LoadActivePaymentMethods( /// Load payment provider by system name /// /// System name + /// true to load only active provider + /// Load records allowed only in specified store; pass 0 to load all records /// Found payment provider Provider LoadPaymentMethodBySystemName(string systemName, bool onlyWhenActive = false, int storeId = 0); /// /// Load all payment providers /// - /// Load records allows only in specified store; pass 0 to load all records + /// Load records allowed only in specified store; pass 0 to load all records /// Payment providers IEnumerable> LoadAllPaymentMethods(int storeId = 0); @@ -55,8 +57,9 @@ IEnumerable> LoadActivePaymentMethods( /// /// Gets all payment method extra data /// + /// Load records allowed only in specified store; pass 0 to load all records /// List of payment method objects - IList GetAllPaymentMethods(); + IList GetAllPaymentMethods(int storeId = 0); /// /// Gets payment method extra data by system name diff --git a/src/Libraries/SmartStore.Services/Payments/PaymentExtentions.cs b/src/Libraries/SmartStore.Services/Payments/PaymentExtentions.cs index e54b382573..85504dad98 100644 --- a/src/Libraries/SmartStore.Services/Payments/PaymentExtentions.cs +++ b/src/Libraries/SmartStore.Services/Payments/PaymentExtentions.cs @@ -58,13 +58,13 @@ public static decimal CalculateAdditionalFee(this IPaymentMethod paymentMethod, var result = decimal.Zero; if (usePercentage) { - //percentage - var orderTotalWithoutPaymentFee = orderTotalCalculationService.GetShoppingCartTotal(cart, usePaymentMethodAdditionalFee: false); + // Percentage + decimal? orderTotalWithoutPaymentFee = orderTotalCalculationService.GetShoppingCartTotal(cart, usePaymentMethodAdditionalFee: false); result = (decimal)((((float)orderTotalWithoutPaymentFee) * ((float)fee)) / 100f); } else { - //fixed value + // Fixed value result = fee; } return result; diff --git a/src/Libraries/SmartStore.Services/Payments/PaymentService.cs b/src/Libraries/SmartStore.Services/Payments/PaymentService.cs index aeaa91dcd3..7e5639bba9 100644 --- a/src/Libraries/SmartStore.Services/Payments/PaymentService.cs +++ b/src/Libraries/SmartStore.Services/Payments/PaymentService.cs @@ -5,10 +5,11 @@ using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; -using SmartStore.Core.Events; +using SmartStore.Core.Domain.Stores; using SmartStore.Core.Infrastructure; using SmartStore.Core.Localization; using SmartStore.Core.Plugins; +using SmartStore.Services.Stores; namespace SmartStore.Services.Payments { @@ -17,37 +18,35 @@ namespace SmartStore.Services.Payments /// public partial class PaymentService : IPaymentService { - #region Constants - - private const string PAYMENTMETHOD_ALL_KEY = "SmartStore.paymentmethod.all"; - - #endregion - #region Fields private readonly static object _lock = new object(); private static IList _paymentMethodFilterTypes = null; private readonly IRepository _paymentMethodRepository; - private readonly PaymentSettings _paymentSettings; + private readonly IRepository _storeMappingRepository; + private readonly IStoreMappingService _storeMappingService; + private readonly PaymentSettings _paymentSettings; private readonly ShoppingCartSettings _shoppingCartSettings; private readonly IProviderManager _providerManager; private readonly ICommonServices _services; private readonly ITypeFinder _typeFinder; - #endregion + #endregion - #region Ctor + #region Ctor - /// - /// Ctor - /// - /// Payment settings - /// Plugin finder - /// Shopping cart settings + /// + /// Ctor + /// + /// Payment settings + /// Plugin finder + /// Shopping cart settings /// Plugin service - public PaymentService( + public PaymentService( IRepository paymentMethodRepository, + IRepository storeMappingRepository, + IStoreMappingService storeMappingService, PaymentSettings paymentSettings, ShoppingCartSettings shoppingCartSettings, IProviderManager providerManager, @@ -55,16 +54,20 @@ public PaymentService( ITypeFinder typeFinder) { _paymentMethodRepository = paymentMethodRepository; - _paymentSettings = paymentSettings; + _storeMappingRepository = storeMappingRepository; + _storeMappingService = storeMappingService; + _paymentSettings = paymentSettings; _shoppingCartSettings = shoppingCartSettings; _providerManager = providerManager; _services = services; _typeFinder = typeFinder; T = NullLocalizer.Instance; + QuerySettings = DbQuerySettings.Default; } public Localizer T { get; set; } + public DbQuerySettings QuerySettings { get; set; } #endregion @@ -154,39 +157,73 @@ public virtual IEnumerable> LoadActivePaymentMethods( return activeProviders; } - /// - /// Load payment provider by system name - /// - /// System name - /// Found payment provider public virtual Provider LoadPaymentMethodBySystemName(string systemName, bool onlyWhenActive = false, int storeId = 0) { var provider = _providerManager.GetProvider(systemName, storeId); - if (provider != null && onlyWhenActive && !provider.IsPaymentMethodActive(_paymentSettings)) + if (provider != null) { - return null; + if (onlyWhenActive && !provider.IsPaymentMethodActive(_paymentSettings)) + { + return null; + } + + if (!QuerySettings.IgnoreMultiStore && storeId > 0) + { + // Return provider if paymentMethod is null! + var paymentMethod = _paymentMethodRepository.TableUntracked.FirstOrDefault(x => x.PaymentMethodSystemName == systemName); + if (paymentMethod != null && !_storeMappingService.Authorize(paymentMethod, storeId)) + { + return null; + } + } } + return provider; } - /// - /// Load all payment providers - /// - /// Load records allows only in specified store; pass 0 to load all records - /// Payment providers public virtual IEnumerable> LoadAllPaymentMethods(int storeId = 0) { - return _providerManager.GetAllProviders(storeId); - } + var providers = _providerManager.GetAllProviders(storeId); + if (providers.Any() && !QuerySettings.IgnoreMultiStore && storeId > 0) + { + var unauthorizedMethods = _paymentMethodRepository.TableUntracked + .Where(x => x.LimitedToStores) + .ToList(); - /// - /// Gets all payment method extra data - /// - /// List of payment method objects - public virtual IList GetAllPaymentMethods() + var unauthorizedMethodNames = unauthorizedMethods + .Where(x => !_storeMappingService.Authorize(x, storeId)) + .Select(x => x.PaymentMethodSystemName) + .ToList(); + + return providers.Where(x => !unauthorizedMethodNames.Contains(x.Metadata.SystemName)); + } + + return providers; + } + + public virtual IList GetAllPaymentMethods(int storeId = 0) { - var methods = _paymentMethodRepository.TableUntracked.ToList(); + var query = _paymentMethodRepository.TableUntracked; + + if (!QuerySettings.IgnoreMultiStore && storeId > 0) + { + query = + from x in query + join sm in _storeMappingRepository.TableUntracked + on new { c1 = x.Id, c2 = "PaymentMethod" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into m_sm + from sm in m_sm.DefaultIfEmpty() + where !x.LimitedToStores || storeId == sm.StoreId + select x; + + query = + from x in query + group x by x.Id into mGroup + orderby mGroup.Key + select mGroup.FirstOrDefault(); + } + + var methods = query.ToList(); return methods; } @@ -201,6 +238,7 @@ public virtual PaymentMethod GetPaymentMethodBySystemName(string systemName) { return _paymentMethodRepository.Table.FirstOrDefault(x => x.PaymentMethodSystemName == systemName); } + return null; } @@ -214,8 +252,6 @@ public virtual void InsertPaymentMethod(PaymentMethod paymentMethod) throw new ArgumentNullException("paymentMethod"); _paymentMethodRepository.Insert(paymentMethod); - - _services.EventPublisher.EntityInserted(paymentMethod); } /// @@ -228,8 +264,6 @@ public virtual void UpdatePaymentMethod(PaymentMethod paymentMethod) throw new ArgumentNullException("paymentMethod"); _paymentMethodRepository.Update(paymentMethod); - - _services.EventPublisher.EntityUpdated(paymentMethod); } /// @@ -242,8 +276,6 @@ public virtual void DeletePaymentMethod(PaymentMethod paymentMethod) throw new ArgumentNullException("paymentMethod"); _paymentMethodRepository.Delete(paymentMethod); - - _services.EventPublisher.EntityDeleted(paymentMethod); } @@ -364,10 +396,7 @@ public virtual decimal GetAdditionalHandlingFee(IList var paymentMethod = LoadPaymentMethodBySystemName(paymentMethodSystemName); var paymentMethodAdditionalFee = (paymentMethod != null ? paymentMethod.Value.GetAdditionalHandlingFee(cart) : decimal.Zero); - if (_shoppingCartSettings.RoundPricesDuringCalculation) - { - paymentMethodAdditionalFee = Math.Round(paymentMethodAdditionalFee, 2); - } + paymentMethodAdditionalFee = paymentMethodAdditionalFee.RoundIfEnabledFor(_services.WorkContext.WorkingCurrency); return paymentMethodAdditionalFee; } diff --git a/src/Libraries/SmartStore.Services/Polls/PollService.cs b/src/Libraries/SmartStore.Services/Polls/PollService.cs index ebcb426f08..327d3f3075 100644 --- a/src/Libraries/SmartStore.Services/Polls/PollService.cs +++ b/src/Libraries/SmartStore.Services/Polls/PollService.cs @@ -115,9 +115,6 @@ public virtual void DeletePoll(Poll poll) throw new ArgumentNullException("poll"); _pollRepository.Delete(poll); - - //event notification - _eventPublisher.EntityDeleted(poll); } public virtual void InsertPoll(Poll poll) @@ -126,9 +123,6 @@ public virtual void InsertPoll(Poll poll) throw new ArgumentNullException("poll"); _pollRepository.Insert(poll); - - //event notification - _eventPublisher.EntityInserted(poll); } public virtual void UpdatePoll(Poll poll) @@ -137,9 +131,6 @@ public virtual void UpdatePoll(Poll poll) throw new ArgumentNullException("poll"); _pollRepository.Update(poll); - - //event notification - _eventPublisher.EntityUpdated(poll); } public virtual PollAnswer GetPollAnswerById(int pollAnswerId) @@ -160,9 +151,6 @@ public virtual void DeletePollAnswer(PollAnswer pollAnswer) throw new ArgumentNullException("pollAnswer"); _pollAnswerRepository.Delete(pollAnswer); - - //event notification - _eventPublisher.EntityDeleted(pollAnswer); } public virtual bool AlreadyVoted(int pollId, int customerId) diff --git a/src/Libraries/SmartStore.Services/Search/CatalogSearchQuery.cs b/src/Libraries/SmartStore.Services/Search/CatalogSearchQuery.cs index d9bb891e3a..00be3a01bc 100644 --- a/src/Libraries/SmartStore.Services/Search/CatalogSearchQuery.cs +++ b/src/Libraries/SmartStore.Services/Search/CatalogSearchQuery.cs @@ -17,12 +17,12 @@ public CatalogSearchQuery() { } - public CatalogSearchQuery(string field, string term, SearchMode mode = SearchMode.StartsWith, bool escape = true, bool isFuzzySearch = false) + public CatalogSearchQuery(string field, string term, SearchMode mode = SearchMode.Contains, bool escape = true, bool isFuzzySearch = false) : base(field.HasValue() ? new[] { field } : null, term, mode, escape, isFuzzySearch) { } - public CatalogSearchQuery(string[] fields, string term, SearchMode mode = SearchMode.StartsWith, bool escape = true, bool isFuzzySearch = false) + public CatalogSearchQuery(string[] fields, string term, SearchMode mode = SearchMode.Contains, bool escape = true, bool isFuzzySearch = false) : base(fields, term, mode, escape, isFuzzySearch) { } diff --git a/src/Libraries/SmartStore.Services/Search/CatalogSearchService.cs b/src/Libraries/SmartStore.Services/Search/CatalogSearchService.cs index 351926220d..5f9b9bfa79 100644 --- a/src/Libraries/SmartStore.Services/Search/CatalogSearchService.cs +++ b/src/Libraries/SmartStore.Services/Search/CatalogSearchService.cs @@ -188,7 +188,7 @@ public IQueryable PrepareQuery(CatalogSearchQuery searchQuery, IQueryab protected virtual void ApplyFacetLabels(IDictionary facets) { - if (facets == null | facets.Count == 0) + if (facets == null || facets.Count == 0) { return; } diff --git a/src/Libraries/SmartStore.Services/Search/Extensions/FacetUtility.cs b/src/Libraries/SmartStore.Services/Search/Extensions/FacetUtility.cs index 05a80e1b5d..4c799338b7 100644 --- a/src/Libraries/SmartStore.Services/Search/Extensions/FacetUtility.cs +++ b/src/Libraries/SmartStore.Services/Search/Extensions/FacetUtility.cs @@ -8,6 +8,8 @@ namespace SmartStore.Services.Search.Extensions { public static class FacetUtility { + private const double MAX_PRICE = 1000000000; + private static int[,] _priceThresholds = new int[,] { { 10, 5 }, @@ -39,7 +41,7 @@ public static double GetNextPrice(double price) return price + _priceThresholds[i, 1]; } - return 1000000000; + return MAX_PRICE; } public static double MakePriceEven(double price) @@ -47,7 +49,7 @@ public static double MakePriceEven(double price) if (price == 0.0) return GetNextPrice(0.0); - // get previous threshold for price + // Get previous threshold for price. var result = 0.0; for (var i = 1; i <= _priceThresholds.GetUpperBound(0) && result == 0.0; ++i) { @@ -55,7 +57,7 @@ public static double MakePriceEven(double price) result = _priceThresholds[i - 1, 0]; } - while (result < price) + while (result < price && result < MAX_PRICE) { result = GetNextPrice(result); } diff --git a/src/Libraries/SmartStore.Services/Search/LinqCatalogSearchService.cs b/src/Libraries/SmartStore.Services/Search/LinqCatalogSearchService.cs index 032ad68ecc..6ffa7e6827 100644 --- a/src/Libraries/SmartStore.Services/Search/LinqCatalogSearchService.cs +++ b/src/Libraries/SmartStore.Services/Search/LinqCatalogSearchService.cs @@ -87,6 +87,33 @@ private void FlattenFilters(ICollection filters, List filters, string fieldName) + { + if (fieldName.HasValue()) + { + foreach (var filter in filters) + { + var attributeFilter = filter as IAttributeSearchFilter; + if (attributeFilter != null && attributeFilter.FieldName == fieldName) + { + return attributeFilter; + } + + var combinedFilter = filter as ICombinedSearchFilter; + if (combinedFilter != null) + { + var filter2 = FindFilter(combinedFilter.Filters, fieldName); + if (filter2 != null) + { + return filter2; + } + } + } + } + + return null; + } + private List GetIdList(List filters, string fieldName) { var result = new List(); @@ -509,13 +536,17 @@ from sm in psm.DefaultIfEmpty() } } - #endregion + #endregion - query = query.GroupBy(x => x.Id).Select(x => x.FirstOrDefault()); + query = + from p in query + group p by p.Id into grp + orderby grp.Key + select grp.FirstOrDefault(); - #region Sorting + #region Sorting - foreach (var sort in searchQuery.Sorting) + foreach (var sort in searchQuery.Sorting) { if (sort.FieldName.IsEmpty()) { @@ -530,7 +561,7 @@ from sm in psm.DefaultIfEmpty() var manufacturerId = manufacturerIds.First(); query = OrderBy(ref ordered, query, x => x.ProductManufacturers.Where(pm => pm.ManufacturerId == manufacturerId).FirstOrDefault().DisplayOrder); } - else if (searchQuery.Filters.OfType().Any(x => x.FieldName == "parentid")) + else if (FindFilter(searchQuery.Filters, "parentid") != null) { query = OrderBy(ref ordered, query, x => x.DisplayOrder); } @@ -559,7 +590,14 @@ from sm in psm.DefaultIfEmpty() if (!ordered) { - query = query.OrderBy(x => x.Id); + if (FindFilter(searchQuery.Filters, "parentid") != null) + { + query = query.OrderBy(x => x.DisplayOrder); + } + else + { + query = query.OrderBy(x => x.Id); + } } #endregion @@ -597,14 +635,14 @@ protected virtual IDictionary GetFacets(CatalogSearchQuery s { #region Category - var categoryQuery = _categoryService.GetCategories(null, false, null, true, storeId); - categoryQuery = categoryQuery.OrderBy(x => x.DisplayOrder).ThenBy(x => x.Name); + var categoryTree = _categoryService.GetCategoryTree(0, false, storeId); + var categories = categoryTree.Flatten(false); + if (descriptor.MaxChoicesCount > 0) { - categoryQuery = categoryQuery.Take(descriptor.MaxChoicesCount); + categories = categories.Take(descriptor.MaxChoicesCount); } - var categories = categoryQuery.ToList(); var nameQuery = _localizedPropertyRepository.TableUntracked .Where(x => x.LocaleKeyGroup == "Category" && x.LocaleKey == "Name" && x.LanguageId == languageId); var names = nameQuery.ToList().ToDictionarySafe(x => x.EntityId, x => x.LocaleValue); diff --git a/src/Libraries/SmartStore.Services/Search/Modelling/CatalogSearchQueryAliasMapper.cs b/src/Libraries/SmartStore.Services/Search/Modelling/CatalogSearchQueryAliasMapper.cs index 8131abc5d6..42562c44c6 100644 --- a/src/Libraries/SmartStore.Services/Search/Modelling/CatalogSearchQueryAliasMapper.cs +++ b/src/Libraries/SmartStore.Services/Search/Modelling/CatalogSearchQueryAliasMapper.cs @@ -24,6 +24,7 @@ public class CatalogSearchQueryAliasMapper : ICatalogSearchQueryAliasMapper private readonly ICacheManager _cacheManager; private readonly IRepository _localizedPropertyRepository; + private readonly IRepository _productAttributeRepository; private readonly IRepository _productVariantAttributeValueRepository; private readonly ISpecificationAttributeService _specificationAttributeService; private readonly ISettingService _settingService; @@ -32,6 +33,7 @@ public class CatalogSearchQueryAliasMapper : ICatalogSearchQueryAliasMapper public CatalogSearchQueryAliasMapper( ICacheManager cacheManager, IRepository localizedPropertyRepository, + IRepository productAttributeRepository, IRepository productVariantAttributeValueRepository, ISpecificationAttributeService specificationAttributeService, ISettingService settingService, @@ -39,6 +41,7 @@ public CatalogSearchQueryAliasMapper( { _cacheManager = cacheManager; _localizedPropertyRepository = localizedPropertyRepository; + _productAttributeRepository = productAttributeRepository; _productVariantAttributeValueRepository = productVariantAttributeValueRepository; _specificationAttributeService = specificationAttributeService; _settingService = settingService; @@ -171,8 +174,8 @@ protected virtual IDictionary GetAttributeAliasByIdMappings() public void ClearAttributeCache() { - _cacheManager.RemoveByPattern(ALL_ATTRIBUTE_ID_BY_ALIAS_KEY); - _cacheManager.RemoveByPattern(ALL_ATTRIBUTE_ALIAS_BY_ID_KEY); + _cacheManager.Remove(ALL_ATTRIBUTE_ID_BY_ALIAS_KEY); + _cacheManager.Remove(ALL_ATTRIBUTE_ALIAS_BY_ID_KEY); } public int GetAttributeIdByAlias(string attributeAlias, int languageId = 0) @@ -252,41 +255,57 @@ protected virtual IDictionary GetVariantIdByAliasMappings() return _cacheManager.Get(ALL_VARIANT_ID_BY_ALIAS_KEY, () => { var result = new Dictionary(StringComparer.OrdinalIgnoreCase); - var optionIdMappings = new Dictionary(); + IPagedList variants = null; IPagedList options = null; var variantId = 0; var pageIndex = 0; - var query = _productVariantAttributeValueRepository.TableUntracked + var variantQuery = _productAttributeRepository.TableUntracked + .Where(x => !string.IsNullOrEmpty(x.Alias)) + .OrderBy(x => x.Id); + + var optionQuery = _productVariantAttributeValueRepository.TableUntracked .Expand(x => x.ProductVariantAttribute) .Expand("ProductVariantAttribute.ProductAttribute") + .Where(x => !string.IsNullOrEmpty(x.Alias)) .OrderBy(x => x.Id); do { - options = new PagedList(query, pageIndex++, 500); + variants = new PagedList(variantQuery, pageIndex++, 500); - foreach (var option in options) + foreach (var variant in variants) { - var variant = option.ProductVariantAttribute.ProductAttribute; - - optionIdMappings[option.Id] = variant.Id; + result[CreateKey("vari", 0, variant.Alias)] = variant.Id; + } + } + while (variants.HasNextPage); + pageIndex = 0; + variants.Clear(); - if (variant.Alias.HasValue()) - { - result[CreateKey("vari", 0, variant.Alias)] = variant.Id; - } + do + { + options = new PagedList(optionQuery, pageIndex++, 500); - if (option.Alias.HasValue()) - { - result[CreateOptionKey("vari.option", 0, variant.Id, option.Alias)] = option.Id; - } + foreach (var option in options) + { + var variant = option.ProductVariantAttribute.ProductAttribute; + result[CreateOptionKey("vari.option", 0, variant.Id, option.Alias)] = option.Id; } } while (options.HasNextPage); - options.Clear(); + var optionIdMappings = _productVariantAttributeValueRepository.TableUntracked + .Expand(x => x.ProductVariantAttribute) + .Expand("ProductVariantAttribute.ProductAttribute") + .Select(x => new + { + OptionId = x.Id, + VariantId = x.ProductVariantAttribute.ProductAttribute.Id + }) + .ToDictionary(x => x.OptionId, x => x.VariantId); + CachedLocalizedAlias("ProductAttribute", x => result[CreateKey("vari", x.LanguageId, x.LocaleValue)] = x.EntityId); CachedLocalizedAlias("ProductVariantAttributeValue", x => { @@ -303,35 +322,41 @@ protected virtual IDictionary GetVariantAliasByIdMappings() return _cacheManager.Get(ALL_VARIANT_ALIAS_BY_ID_KEY, () => { var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + IPagedList variants = null; IPagedList options = null; var pageIndex = 0; - var query = _productVariantAttributeValueRepository.TableUntracked - .Expand(x => x.ProductVariantAttribute) - .Expand("ProductVariantAttribute.ProductAttribute") + var variantQuery = _productAttributeRepository.TableUntracked + .Where(x => !string.IsNullOrEmpty(x.Alias)) + .OrderBy(x => x.Id); + + var optionQuery = _productVariantAttributeValueRepository.TableUntracked + .Where(x => !string.IsNullOrEmpty(x.Alias)) .OrderBy(x => x.Id); do { - options = new PagedList(query, pageIndex++, 500); + variants = new PagedList(variantQuery, pageIndex++, 500); - foreach (var option in options) + foreach (var variant in variants) { - var variant = option.ProductVariantAttribute.ProductAttribute; + result[CreateKey("vari", 0, variant.Id)] = variant.Alias; + } + } + while (variants.HasNextPage); + pageIndex = 0; + variants.Clear(); - if (variant.Alias.HasValue()) - { - result[CreateKey("vari", 0, variant.Id)] = variant.Alias; - } + do + { + options = new PagedList(optionQuery, pageIndex++, 500); - if (option.Alias.HasValue()) - { - result[CreateOptionKey("attr.option", 0, option.Id)] = option.Alias; - } + foreach (var option in options) + { + result[CreateOptionKey("attr.option", 0, option.Id)] = option.Alias; } } while (options.HasNextPage); - options.Clear(); CachedLocalizedAlias("ProductAttribute", x => result[CreateKey("vari", x.LanguageId, x.EntityId)] = x.LocaleValue); @@ -343,8 +368,8 @@ protected virtual IDictionary GetVariantAliasByIdMappings() public void ClearVariantCache() { - _cacheManager.RemoveByPattern(ALL_VARIANT_ID_BY_ALIAS_KEY); - _cacheManager.RemoveByPattern(ALL_VARIANT_ALIAS_BY_ID_KEY); + _cacheManager.Remove(ALL_VARIANT_ID_BY_ALIAS_KEY); + _cacheManager.Remove(ALL_VARIANT_ALIAS_BY_ID_KEY); } public int GetVariantIdByAlias(string variantAlias, int languageId = 0) @@ -455,7 +480,7 @@ protected virtual IDictionary GetCommonFacetAliasByGroupKindMapp public void ClearCommonFacetCache() { - _cacheManager.RemoveByPattern(ALL_COMMONFACET_ALIAS_BY_KIND_KEY); + _cacheManager.Remove(ALL_COMMONFACET_ALIAS_BY_KIND_KEY); } public string GetCommonFacetAliasByGroupKind(FacetGroupKind kind, int languageId) diff --git a/src/Libraries/SmartStore.Services/Search/Modelling/CatalogSearchQueryFactory.cs b/src/Libraries/SmartStore.Services/Search/Modelling/CatalogSearchQueryFactory.cs index e2a70eed7f..808fd62679 100644 --- a/src/Libraries/SmartStore.Services/Search/Modelling/CatalogSearchQueryFactory.cs +++ b/src/Libraries/SmartStore.Services/Search/Modelling/CatalogSearchQueryFactory.cs @@ -35,7 +35,9 @@ namespace SmartStore.Services.Search.Modelling public class CatalogSearchQueryFactory : ICatalogSearchQueryFactory { protected static readonly string[] _tokens = new string[] { "q", "i", "s", "o", "p", "c", "m", "r", "a", "n", "d", "v" }; - protected readonly HttpContextBase _httpContext; + protected static readonly string[] _instantSearchFields = new string[] { "manufacturer", "sku", "gtin", "mpn", "attrname", "variantname" }; + + protected readonly HttpContextBase _httpContext; protected readonly CatalogSettings _catalogSettings; protected readonly SearchSettings _searchSettings; protected readonly ICommonServices _services; @@ -83,18 +85,12 @@ public CatalogSearchQuery CreateFromQuery() fields.Add("shortdescription"); fields.Add("tagname"); - if (_searchSettings.SearchFields.Contains("manufacturer")) - fields.Add("manufacturer"); - - if (_searchSettings.SearchFields.Contains("sku")) - fields.Add("sku"); - - if (_searchSettings.SearchFields.Contains("gtin")) - fields.Add("gtin"); - - if (_searchSettings.SearchFields.Contains("mpn")) - fields.Add("mpn"); - } + foreach (var fieldName in _instantSearchFields) + { + if (_searchSettings.SearchFields.Contains(fieldName)) + fields.Add(fieldName); + } + } else { fields.AddRange(_searchSettings.SearchFields); diff --git a/src/Libraries/SmartStore.Services/Security/AclService.cs b/src/Libraries/SmartStore.Services/Security/AclService.cs index 4117a07035..a5374054da 100644 --- a/src/Libraries/SmartStore.Services/Security/AclService.cs +++ b/src/Libraries/SmartStore.Services/Security/AclService.cs @@ -11,46 +11,27 @@ namespace SmartStore.Services.Security { public partial class AclService : IAclService { - #region Constants - private const string ACLRECORD_BY_ENTITYID_NAME_KEY = "aclrecord:entityid-name-{0}-{1}"; - private const string ACLRECORD_PATTERN_KEY = "aclrecord:"; - - #endregion + private const string ACLRECORD_PATTERN_KEY = "aclrecord:*"; - #region Fields private readonly IRepository _aclRecordRepository; private readonly IWorkContext _workContext; private readonly ICacheManager _cacheManager; private bool? _hasActiveAcl; - #endregion - - #region Ctor - - /// - /// Ctor - /// - /// Cache manager - /// Work context - /// ACL record repository public AclService(ICacheManager cacheManager, IWorkContext workContext, IRepository aclRecordRepository) { - this._cacheManager = cacheManager; - this._workContext = workContext; - this._aclRecordRepository = aclRecordRepository; + _cacheManager = cacheManager; + _workContext = workContext; + _aclRecordRepository = aclRecordRepository; - this.QuerySettings = DbQuerySettings.Default; + QuerySettings = DbQuerySettings.Default; } public DbQuerySettings QuerySettings { get; set; } - #endregion - - #region Members - public bool HasActiveAcl { get @@ -63,26 +44,15 @@ public bool HasActiveAcl } } - /// - /// Deletes an ACL record - /// - /// ACL record public virtual void DeleteAclRecord(AclRecord aclRecord) { - if (aclRecord == null) - throw new ArgumentNullException("aclRecord"); + Guard.NotNull(aclRecord,nameof(aclRecord)); _aclRecordRepository.Delete(aclRecord); - // cache _cacheManager.RemoveByPattern(ACLRECORD_PATTERN_KEY); } - /// - /// Gets an ACL record - /// - /// ACL record identifier - /// ACL record public virtual AclRecord GetAclRecordById(int aclRecordId) { if (aclRecordId == 0) @@ -92,18 +62,11 @@ public virtual AclRecord GetAclRecordById(int aclRecordId) return aclRecord; } - /// - /// Gets ACL records - /// - /// Type - /// Entity - /// ACL records public IList GetAclRecords(T entity) where T : BaseEntity, IAclSupported { - if (entity == null) - throw new ArgumentNullException("entity"); + Guard.NotNull(entity, nameof(entity)); - int entityId = entity.Id; + int entityId = entity.Id; string entityName = typeof(T).Name; return GetAclRecordsFor(entityName, entityId); @@ -123,34 +86,21 @@ public virtual IList GetAclRecordsFor(string entityName, int entityId } - /// - /// Inserts an ACL record - /// - /// ACL record public virtual void InsertAclRecord(AclRecord aclRecord) { - if (aclRecord == null) - throw new ArgumentNullException("aclRecord"); + Guard.NotNull(aclRecord, nameof(aclRecord)); - _aclRecordRepository.Insert(aclRecord); + _aclRecordRepository.Insert(aclRecord); - //cache _cacheManager.RemoveByPattern(ACLRECORD_PATTERN_KEY); } - /// - /// Inserts an ACL record - /// - /// Type - /// Entity - /// Customer role id public virtual void InsertAclRecord(T entity, int customerRoleId) where T : BaseEntity, IAclSupported { - if (entity == null) - throw new ArgumentNullException("entity"); - - if (customerRoleId == 0) - throw new ArgumentOutOfRangeException("customerRoleId"); + Guard.NotNull(entity, nameof(entity)); + + if (customerRoleId == 0) + throw new ArgumentOutOfRangeException(nameof(customerRoleId)); int entityId = entity.Id; string entityName = typeof(T).Name; @@ -165,90 +115,67 @@ public virtual void InsertAclRecord(T entity, int customerRoleId) where T : B InsertAclRecord(aclRecord); } - /// - /// Updates the ACL record - /// - /// ACL record public virtual void UpdateAclRecord(AclRecord aclRecord) { - if (aclRecord == null) - throw new ArgumentNullException("aclRecord"); + Guard.NotNull(aclRecord, nameof(aclRecord)); - _aclRecordRepository.Update(aclRecord); + _aclRecordRepository.Update(aclRecord); _cacheManager.RemoveByPattern(ACLRECORD_PATTERN_KEY); } - /// - /// Find customer role identifiers with granted access - /// - /// Type - /// Entity - /// Customer role identifiers - public virtual int[] GetCustomerRoleIdsWithAccess(T entity) where T : BaseEntity, IAclSupported - { - if (entity == null) - throw new ArgumentNullException("entity"); + public virtual int[] GetCustomerRoleIdsWithAccess(string entityName, int entityId) + { + Guard.NotEmpty(entityName, nameof(entityName)); - int entityId = entity.Id; - string entityName = typeof(T).Name; + if (entityId <= 0) + return new int[0]; - string key = string.Format(ACLRECORD_BY_ENTITYID_NAME_KEY, entityId, entityName); - return _cacheManager.Get(key, () => - { - var query = from ur in _aclRecordRepository.Table - where ur.EntityId == entityId && - ur.EntityName == entityName - select ur.CustomerRoleId; - var result = query.ToArray(); - //little hack here. nulls aren't cacheable so set it to "" - if (result == null) - result = new int[0]; - return result; - }); - } + string key = string.Format(ACLRECORD_BY_ENTITYID_NAME_KEY, entityId, entityName); + return _cacheManager.Get(key, () => + { + var query = from ur in _aclRecordRepository.Table + where ur.EntityId == entityId && + ur.EntityName == entityName + select ur.CustomerRoleId; + + var result = query.ToArray(); + return result; + }); + } - /// - /// Authorize ACL permission - /// - /// Type - /// Wntity - /// true - authorized; otherwise, false - public virtual bool Authorize(T entity) where T : BaseEntity, IAclSupported - { - return Authorize(entity, _workContext.CurrentCustomer); - } + public bool Authorize(string entityName, int entityId) + { + return Authorize(entityName, entityId, _workContext.CurrentCustomer); + } - /// - /// Authorize ACL permission - /// - /// Type - /// Wntity - /// Customer - /// true - authorized; otherwise, false - public virtual bool Authorize(T entity, Customer customer) where T : BaseEntity, IAclSupported - { - if (entity == null) - return false; + public virtual bool Authorize(string entityName, int entityId, Customer customer) + { + Guard.NotEmpty(entityName, nameof(entityName)); - if (customer == null) - return false; + if (entityId <= 0) + return false; + + if (customer == null) + return false; if (QuerySettings.IgnoreAcl) return true; - if (!entity.SubjectToAcl) - return true; - - foreach (var role1 in customer.CustomerRoles.Where(cr => cr.Active)) - foreach (var role2Id in GetCustomerRoleIdsWithAccess(entity)) - if (role1.Id == role2Id) - //yes, we have such permission - return true; + foreach (var role1 in customer.CustomerRoles.Where(cr => cr.Active)) + { + foreach (var role2Id in GetCustomerRoleIdsWithAccess(entityName, entityId)) + { + if (role1.Id == role2Id) + { + // yes, we have such permission + return true; + } + } + } - //no permission found - return false; - } - #endregion - } + // no permission granted + return false; + } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Security/IAclService.cs b/src/Libraries/SmartStore.Services/Security/IAclService.cs index 6d18ce30f4..d4d9eb905c 100644 --- a/src/Libraries/SmartStore.Services/Security/IAclService.cs +++ b/src/Libraries/SmartStore.Services/Security/IAclService.cs @@ -2,11 +2,8 @@ using System.Collections.Generic; using System.Linq; using SmartStore.Core; -using SmartStore.Core.Caching; -using SmartStore.Core.Data; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Security; -using SmartStore.Core.Domain.Seo; namespace SmartStore.Services.Security { @@ -69,29 +66,83 @@ public partial interface IAclService /// ACL record void UpdateAclRecord(AclRecord aclRecord); - /// - /// Find customer role identifiers with granted access - /// - /// Type - /// Wntity - /// Customer role identifiers - int[] GetCustomerRoleIdsWithAccess(T entity) where T : BaseEntity, IAclSupported; + /// + /// Find customer role identifiers with granted access + /// + /// Entity name to check permission for + /// Entity id to check permission for + /// Customer role identifiers + int[] GetCustomerRoleIdsWithAccess(string entityName, int entityId); - /// - /// Authorize ACL permission - /// - /// Type - /// Wntity - /// true - authorized; otherwise, false - bool Authorize(T entity) where T : BaseEntity, IAclSupported; + /// + /// Authorize ACL permission + /// + /// Type + /// Entity name to check permission for + /// Entity id to check permission for + /// true - authorized; otherwise, false + bool Authorize(string entityName, int entityId); - /// - /// Authorize ACL permission - /// - /// Type - /// Wntity - /// Customer - /// true - authorized; otherwise, false - bool Authorize(T entity, Customer customer) where T : BaseEntity, IAclSupported; - } + /// + /// Authorize ACL permission + /// + /// Type + /// Entity name to check permission for + /// Entity id to check permission for + /// Customer + /// true - authorized; otherwise, false + bool Authorize(string entityName, int entityId, Customer customer); + } + + public static class IAclServiceExtensions + { + /// + /// Find customer role identifiers with granted access + /// + /// Type + /// Entity + /// Customer role identifiers + public static int[] GetCustomerRoleIdsWithAccess(this IAclService aclService, T entity) where T : BaseEntity, IAclSupported + { + if (entity == null) + return new int[0]; + + return aclService.GetCustomerRoleIdsWithAccess(typeof(T).Name, entity.Id); + } + + /// + /// Authorize ACL permission + /// + /// Type + /// Entity + /// true - authorized; otherwise, false + public static bool Authorize(this IAclService aclService, T entity) where T : BaseEntity, IAclSupported + { + if (entity == null) + return false; + + if (!entity.SubjectToAcl) + return true; + + return aclService.Authorize(typeof(T).Name, entity.Id); + } + + /// + /// Authorize ACL permission + /// + /// Type + /// Entity + /// Customer + /// true - authorized; otherwise, false + public static bool Authorize(this IAclService aclService, T entity, Customer customer) where T : BaseEntity, IAclSupported + { + if (entity == null) + return false; + + if (!entity.SubjectToAcl) + return true; + + return aclService.Authorize(typeof(T).Name, entity.Id, customer); + } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Security/PermissionService.cs b/src/Libraries/SmartStore.Services/Security/PermissionService.cs index 9000bcc916..16d0fa0ac0 100644 --- a/src/Libraries/SmartStore.Services/Security/PermissionService.cs +++ b/src/Libraries/SmartStore.Services/Security/PermissionService.cs @@ -24,7 +24,7 @@ public partial class PermissionService : IPermissionService /// {1} : permission system name /// private const string PERMISSIONS_ALLOWED_KEY = "permission:allowed-{0}-{1}"; - private const string PERMISSIONS_PATTERN_KEY = "permission:"; + private const string PERMISSIONS_PATTERN_KEY = "permission:*"; #endregion #region Fields diff --git a/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs b/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs index ee14714eb3..93b8e87f68 100644 --- a/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs +++ b/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs @@ -15,7 +15,6 @@ namespace SmartStore.Services.Seo { public static class SeoExtensions { - #region Product tag /// @@ -111,22 +110,49 @@ public static string GetSeName(this ForumTopic forumTopic) return seName; } - #endregion + #endregion - #region General + #region ICategoryNode - /// - /// Get search engine name - /// - /// Entity type - /// Entity - /// Search engine name - public static string GetSeName(this T entity) + /// + /// Get search engine name for a category node + /// + /// Node + /// Search engine name + public static string GetSeName(this ICategoryNode node) + { + Guard.NotNull(node, nameof(node)); + + return GetSeName( + "Category", + node.Id, + EngineContext.Current.Resolve().WorkingLanguage.Id, + EngineContext.Current.Resolve(), + EngineContext.Current.Resolve()); + } + + #endregion + + #region General + + /// + /// Get search engine name + /// + /// Entity type + /// Entity + /// Search engine name + public static string GetSeName(this T entity) where T : BaseEntity, ISlugSupported { - var workContext = EngineContext.Current.Resolve(); - return GetSeName(entity, workContext.WorkingLanguage.Id); - } + Guard.NotNull(entity, nameof(entity)); + + return GetSeName( + typeof(T).Name, + entity.Id, + EngineContext.Current.Resolve().WorkingLanguage.Id, + EngineContext.Current.Resolve(), + EngineContext.Current.Resolve()); + } /// /// Get search engine name @@ -137,11 +163,17 @@ public static string GetSeName(this T entity) /// A value indicating whether to return default value (if language specified one is not found) /// A value indicating whether to ensure that we have at least two published languages; otherwise, load only default value /// Search engine name - public static string GetSeName(this T entity, int languageId, bool returnDefaultValue = true, bool ensureTwoPublishedLanguages = true) + public static string GetSeName(this T entity, + int languageId, + bool returnDefaultValue = true, + bool ensureTwoPublishedLanguages = true) where T : BaseEntity, ISlugSupported { + Guard.NotNull(entity, nameof(entity)); + return GetSeName( - entity, + typeof(T).Name, + entity.Id, languageId, EngineContext.Current.Resolve(), EngineContext.Current.Resolve(), @@ -166,11 +198,28 @@ public static string GetSeName(this T entity, bool ensureTwoPublishedLanguages = true) where T : BaseEntity, ISlugSupported { - if (entity == null) - throw new ArgumentNullException("entity"); + Guard.NotNull(entity, nameof(entity)); + return GetSeName( + typeof(T).Name, + entity.Id, + languageId, + urlRecordService, + languageService, + returnDefaultValue, + ensureTwoPublishedLanguages); + } + + private static string GetSeName( + string entityName, + int entityId, + int languageId, + IUrlRecordService urlRecordService, + ILanguageService languageService, + bool returnDefaultValue = true, + bool ensureTwoPublishedLanguages = true) + { string result = string.Empty; - string entityName = typeof(T).Name; if (languageId > 0) { @@ -184,28 +233,28 @@ public static string GetSeName(this T entity, // localized value if (loadLocalizedValue) { - result = urlRecordService.GetActiveSlug(entity.Id, entityName, languageId); + result = urlRecordService.GetActiveSlug(entityId, entityName, languageId); } } // set default value if required if (String.IsNullOrEmpty(result) && returnDefaultValue) { - result = urlRecordService.GetActiveSlug(entity.Id, entityName, 0); + result = urlRecordService.GetActiveSlug(entityId, entityName, 0); } return result; } - /// - /// Validate search engine name - /// - /// Entity - /// Search engine name to validate - /// User-friendly name used to generate sename - /// Ensreu that sename is not empty - /// Valid sename - public static string ValidateSeName(this T entity, string seName, string name, bool ensureNotEmpty, int? languageId = null) + /// + /// Validate search engine name + /// + /// Entity + /// Search engine name to validate + /// User-friendly name used to generate sename + /// Ensreu that sename is not empty + /// Valid sename + public static string ValidateSeName(this T entity, string seName, string name, bool ensureNotEmpty, int? languageId = null) where T : BaseEntity, ISlugSupported { return entity.ValidateSeName( diff --git a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs index 4b0a8dae38..aacd0eb835 100644 --- a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs +++ b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs @@ -15,7 +15,7 @@ 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_PATTERN = "urlrecord:{0}*"; const string URLRECORD_ALL_PATTERN = "urlrecord:"; const string URLRECORD_ALL_ACTIVESLUGS_KEY = "urlrecord:all-active-slugs"; diff --git a/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs b/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs index bc784c319f..5950c9e11c 100644 --- a/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs +++ b/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs @@ -30,7 +30,7 @@ public partial class XmlSitemapGenerator : IXmlSitemapGenerator /// {2} : current language id /// public const string XMLSITEMAP_DOCUMENT_KEY = "sitemap:xml-idx{0}-{1}-{2}"; - public const string XMLSITEMAP_PATTERN_KEY = "sitemap:xml"; + 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"; diff --git a/src/Libraries/SmartStore.Services/ServiceCacheBuster.cs b/src/Libraries/SmartStore.Services/ServiceCacheBuster.cs new file mode 100644 index 0000000000..feaca72088 --- /dev/null +++ b/src/Libraries/SmartStore.Services/ServiceCacheBuster.cs @@ -0,0 +1,45 @@ +using System; +using SmartStore.Core.Caching; +using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Data.Hooks; +using SmartStore.Core; + +namespace SmartStore.Services +{ + public partial class ServiceCacheBuster : DbSaveHook + { + public const string STORE_LANGUAGE_MAP_KEY = "svc:storelangmap*"; + + private readonly ICacheManager _cacheManager; + + public ServiceCacheBuster(ICacheManager cacheManager) + { + _cacheManager = cacheManager; + } + + protected override void OnInserted(BaseEntity entity, IHookedEntity entry) + { + if (entry.EntityType != typeof(Store) && entry.EntityType != typeof(Language)) + throw new NotImplementedException(); + + _cacheManager.Remove(STORE_LANGUAGE_MAP_KEY); + } + + protected override void OnDeleted(BaseEntity entity, IHookedEntity entry) + { + if (entry.EntityType != typeof(Store) && entry.EntityType != typeof(Language)) + throw new NotImplementedException(); + + _cacheManager.Remove(STORE_LANGUAGE_MAP_KEY); + } + + protected override void OnUpdated(BaseEntity entity, IHookedEntity entry) + { + if (entry.EntityType != typeof(Language)) + throw new NotImplementedException(); + + _cacheManager.Remove(STORE_LANGUAGE_MAP_KEY); + } + } +} diff --git a/src/Libraries/SmartStore.Services/ServiceCacheConsumer.cs b/src/Libraries/SmartStore.Services/ServiceCacheConsumer.cs deleted file mode 100644 index bae5aba97a..0000000000 --- a/src/Libraries/SmartStore.Services/ServiceCacheConsumer.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using SmartStore.Core.Caching; -using SmartStore.Core.Domain.Localization; -using SmartStore.Core.Domain.Stores; -using SmartStore.Core.Events; -using SmartStore.Services.Tasks; -using SmartStore.Services.Stores; - -namespace SmartStore.Services -{ - public class ServiceCacheConsumer : - IConsumer>, - IConsumer>, - IConsumer>, - IConsumer>, - IConsumer> - { - public const string STORE_LANGUAGE_MAP_KEY = "svc:storelangmap"; - - private readonly ICacheManager _cacheManager; - - public ServiceCacheConsumer(ICacheManager cacheManager) - { - _cacheManager = cacheManager; - } - - public void HandleEvent(EntityInserted eventMessage) - { - _cacheManager.Remove(STORE_LANGUAGE_MAP_KEY); - } - - public void HandleEvent(EntityDeleted eventMessage) - { - _cacheManager.Remove(STORE_LANGUAGE_MAP_KEY); - } - - public void HandleEvent(EntityInserted eventMessage) - { - _cacheManager.Remove(STORE_LANGUAGE_MAP_KEY); - } - - public void HandleEvent(EntityUpdated eventMessage) - { - _cacheManager.Remove(STORE_LANGUAGE_MAP_KEY); - } - - public void HandleEvent(EntityDeleted eventMessage) - { - _cacheManager.Remove(STORE_LANGUAGE_MAP_KEY); - } - } -} diff --git a/src/Libraries/SmartStore.Services/Shipping/IShippingService.cs b/src/Libraries/SmartStore.Services/Shipping/IShippingService.cs index f99de8964d..89672e6ec6 100644 --- a/src/Libraries/SmartStore.Services/Shipping/IShippingService.cs +++ b/src/Libraries/SmartStore.Services/Shipping/IShippingService.cs @@ -49,12 +49,13 @@ public partial interface IShippingService ShippingMethod GetShippingMethodById(int shippingMethodId); - /// - /// Gets all shipping methods - /// + /// + /// Gets all shipping methods + /// /// Shipping option request to filter out shipping methods. null to load all shipping methods. - /// Shipping method collection - IList GetAllShippingMethods(GetShippingOptionRequest request = null); + /// Whether to filter methods by store identifier. + /// Shipping method collection + IList GetAllShippingMethods(GetShippingOptionRequest request = null, int storeId = 0); /// /// Inserts a shipping method diff --git a/src/Libraries/SmartStore.Services/Shipping/ShipmentService.cs b/src/Libraries/SmartStore.Services/Shipping/ShipmentService.cs index be5e7b6f2a..8cd9b39030 100644 --- a/src/Libraries/SmartStore.Services/Shipping/ShipmentService.cs +++ b/src/Libraries/SmartStore.Services/Shipping/ShipmentService.cs @@ -62,9 +62,6 @@ public virtual void DeleteShipment(Shipment shipment) _shipmentRepository.Delete(shipment); - //event notifications - _eventPublisher.EntityDeleted(shipment); - if (orderId != 0) { var order = _orderRepository.GetById(orderId); @@ -162,7 +159,6 @@ public virtual void InsertShipment(Shipment shipment) _shipmentRepository.Insert(shipment); //event notification - _eventPublisher.EntityInserted(shipment); _eventPublisher.PublishOrderUpdated(shipment.Order); } @@ -178,7 +174,6 @@ public virtual void UpdateShipment(Shipment shipment) _shipmentRepository.Update(shipment); //event notification - _eventPublisher.EntityUpdated(shipment); _eventPublisher.PublishOrderUpdated(shipment.Order); } @@ -197,9 +192,6 @@ public virtual void DeleteShipmentItem(ShipmentItem shipmentItem) _siRepository.Delete(shipmentItem); - //event notifications - _eventPublisher.EntityDeleted(shipmentItem); - if (orderId != 0) { var order = _orderRepository.GetById(orderId); @@ -232,9 +224,6 @@ public virtual void InsertShipmentItem(ShipmentItem shipmentItem) _siRepository.Insert(shipmentItem); - //event notifications - _eventPublisher.EntityInserted(shipmentItem); - if (shipmentItem.Shipment != null && shipmentItem.Shipment.Order != null) { _eventPublisher.PublishOrderUpdated(shipmentItem.Shipment.Order); @@ -261,9 +250,6 @@ public virtual void UpdateShipmentItem(ShipmentItem shipmentItem) _siRepository.Update(shipmentItem); - //event notifications - _eventPublisher.EntityUpdated(shipmentItem); - if (shipmentItem.Shipment != null && shipmentItem.Shipment.Order != null) { _eventPublisher.PublishOrderUpdated(shipmentItem.Shipment.Order); diff --git a/src/Libraries/SmartStore.Services/Shipping/ShippingExtentions.cs b/src/Libraries/SmartStore.Services/Shipping/ShippingExtentions.cs index fedfbe3891..b83fdc68ee 100644 --- a/src/Libraries/SmartStore.Services/Shipping/ShippingExtentions.cs +++ b/src/Libraries/SmartStore.Services/Shipping/ShippingExtentions.cs @@ -5,7 +5,7 @@ namespace SmartStore.Services.Shipping { - public static class ShippingExtentions + public static class ShippingExtentions { public static bool IsShippingRateComputationMethodActive(this Provider srcm, ShippingSettings shippingSettings) { @@ -23,14 +23,5 @@ public static bool IsShippingRateComputationMethodActive(this Provider c.Id == countryId) != null; - return result; - } } } diff --git a/src/Libraries/SmartStore.Services/Shipping/ShippingService.cs b/src/Libraries/SmartStore.Services/Shipping/ShippingService.cs index 5388f05979..0e4c091f2f 100644 --- a/src/Libraries/SmartStore.Services/Shipping/ShippingService.cs +++ b/src/Libraries/SmartStore.Services/Shipping/ShippingService.cs @@ -7,6 +7,7 @@ using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Shipping; +using SmartStore.Core.Domain.Stores; using SmartStore.Core.Events; using SmartStore.Core.Infrastructure; using SmartStore.Core.Localization; @@ -25,7 +26,8 @@ public partial class ShippingService : IShippingService private static IList _shippingMethodFilterTypes = null; private readonly IRepository _shippingMethodRepository; - private readonly IProductAttributeParser _productAttributeParser; + private readonly IRepository _storeMappingRepository; + private readonly IProductAttributeParser _productAttributeParser; private readonly IProductService _productService; private readonly ICheckoutAttributeParser _checkoutAttributeParser; private readonly IGenericAttributeService _genericAttributeService; @@ -35,10 +37,12 @@ public partial class ShippingService : IShippingService private readonly ISettingService _settingService; private readonly IProviderManager _providerManager; private readonly ITypeFinder _typeFinder; + private readonly ICommonServices _services; public ShippingService( IRepository shippingMethodRepository, - IProductAttributeParser productAttributeParser, + IRepository storeMappingRepository, + IProductAttributeParser productAttributeParser, IProductService productService, ICheckoutAttributeParser checkoutAttributeParser, IGenericAttributeService genericAttributeService, @@ -47,26 +51,31 @@ public ShippingService( ShoppingCartSettings shoppingCartSettings, ISettingService settingService, IProviderManager providerManager, - ITypeFinder typeFinder) + ITypeFinder typeFinder, + ICommonServices services) { - this._shippingMethodRepository = shippingMethodRepository; - this._productAttributeParser = productAttributeParser; - this._productService = productService; - this._checkoutAttributeParser = checkoutAttributeParser; - this._genericAttributeService = genericAttributeService; - this._shippingSettings = shippingSettings; - this._eventPublisher = eventPublisher; - this._shoppingCartSettings = shoppingCartSettings; - this._settingService = settingService; - this._providerManager = providerManager; - this._typeFinder = typeFinder; + _shippingMethodRepository = shippingMethodRepository; + _storeMappingRepository = storeMappingRepository; + _productAttributeParser = productAttributeParser; + _productService = productService; + _checkoutAttributeParser = checkoutAttributeParser; + _genericAttributeService = genericAttributeService; + _shippingSettings = shippingSettings; + _eventPublisher = eventPublisher; + _shoppingCartSettings = shoppingCartSettings; + _settingService = settingService; + _providerManager = providerManager; + _typeFinder = typeFinder; + _services = services; T = NullLocalizer.Instance; Logger = NullLogger.Instance; + QuerySettings = DbQuerySettings.Default; } public Localizer T { get; set; } public ILogger Logger { get; set; } + public DbQuerySettings QuerySettings { get; set; } #region Shipping rate computation methods @@ -142,9 +151,6 @@ public virtual void DeleteShippingMethod(ShippingMethod shippingMethod) throw new ArgumentNullException("shippingMethod"); _shippingMethodRepository.Delete(shippingMethod); - - //event notification - _eventPublisher.EntityDeleted(shippingMethod); } /// @@ -160,24 +166,42 @@ public virtual ShippingMethod GetShippingMethodById(int shippingMethodId) return _shippingMethodRepository.GetById(shippingMethodId); } - public virtual IList GetAllShippingMethods(GetShippingOptionRequest request = null) + public virtual IList GetAllShippingMethods(GetShippingOptionRequest request = null, int storeId = 0) { var query = from sm in _shippingMethodRepository.Table - orderby sm.DisplayOrder select sm; - var allMethods = query.ToList(); + if (!QuerySettings.IgnoreMultiStore && storeId > 0) + { + query = + from x in query + join sm in _storeMappingRepository.TableUntracked + on new { c1 = x.Id, c2 = "ShippingMethod" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into x_sm + from sm in x_sm.DefaultIfEmpty() + where !x.LimitedToStores || storeId == sm.StoreId + select x; + + query = + from x in query + group x by x.Id into grp + orderby grp.Key + select grp.FirstOrDefault(); + } + + var allMethods = query.OrderBy(x => x.DisplayOrder).ToList(); if (request == null) + { return allMethods; + } IList allFilters = null; var filterRequest = new ShippingFilterRequest { Option = request }; var activeShippingMethods = allMethods.Where(s => { - // shipping method filtering + // Shipping method filtering. if (allFilters == null) allFilters = GetAllShippingMethodFilters(); @@ -202,9 +226,6 @@ public virtual void InsertShippingMethod(ShippingMethod shippingMethod) throw new ArgumentNullException("shippingMethod"); _shippingMethodRepository.Insert(shippingMethod); - - //event notification - _eventPublisher.EntityInserted(shippingMethod); } /// @@ -217,9 +238,6 @@ public virtual void UpdateShippingMethod(ShippingMethod shippingMethod) throw new ArgumentNullException("shippingMethod"); _shippingMethodRepository.Update(shippingMethod); - - //event notification - _eventPublisher.EntityUpdated(shippingMethod); } #endregion @@ -362,10 +380,7 @@ public virtual GetShippingOptionResponse GetShippingOptions( { //system name so2.ShippingRateComputationMethodSystemName = srcm.Metadata.SystemName; - - //round - if (_shoppingCartSettings.RoundPricesDuringCalculation) - so2.Rate = Math.Round(so2.Rate, 2); + so2.Rate = so2.Rate.RoundIfEnabledFor(_services.WorkContext.WorkingCurrency); result.ShippingOptions.Add(so2); } diff --git a/src/Libraries/SmartStore.Services/SmartStore.Services.csproj b/src/Libraries/SmartStore.Services/SmartStore.Services.csproj index e9b31c01f8..4993603b7b 100644 --- a/src/Libraries/SmartStore.Services/SmartStore.Services.csproj +++ b/src/Libraries/SmartStore.Services/SmartStore.Services.csproj @@ -10,7 +10,7 @@ Properties SmartStore.Services SmartStore.Services - v4.5.2 + v4.6.1 512 @@ -62,6 +62,9 @@ MinimumRecommendedRules.ruleset + + ..\..\packages\AngleSharp.0.9.9\lib\net45\AngleSharp.dll + ..\..\packages\Autofac.4.5.0\lib\net45\Autofac.dll True @@ -75,23 +78,19 @@ True - False - ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll + ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll + True - False - ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll + ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll + True ..\..\packages\EPPlus.4.1.0\lib\net40\EPPlus.dll True - - ..\..\packages\ImageResizer.4.0.5\lib\net45\ImageResizer.dll - True - - - ..\..\packages\ImageResizer.Plugins.PrettyGifs.4.0.5\lib\net45\ImageResizer.Plugins.PrettyGifs.dll + + ..\..\packages\ImageProcessor.2.5.6\lib\net45\ImageProcessor.dll True @@ -118,6 +117,9 @@ ..\..\packages\NReco.PdfGenerator.1.1.15\lib\net20\NReco.PdfGenerator.dll True + + ..\..\packages\PreMailer.Net.1.5.5\lib\net45\PreMailer.Net.dll + @@ -131,6 +133,9 @@ + + ..\..\packages\System.ValueTuple.4.4.0\lib\net461\System.ValueTuple.dll + False @@ -173,6 +178,9 @@ Properties\AssemblyVersionInfo.cs + + + @@ -189,6 +197,7 @@ + @@ -254,17 +263,24 @@ + + + + + + + @@ -277,10 +293,23 @@ + + + + + + + + + + - - + + + + + @@ -372,15 +401,11 @@ - + - - - + - - @@ -410,7 +435,7 @@ - + @@ -419,21 +444,14 @@ - - - - - - - @@ -536,7 +554,7 @@ - + @@ -554,7 +572,6 @@ - diff --git a/src/Libraries/SmartStore.Services/Stores/IStoreMappingService.cs b/src/Libraries/SmartStore.Services/Stores/IStoreMappingService.cs index ee938acb15..7eed774f70 100644 --- a/src/Libraries/SmartStore.Services/Stores/IStoreMappingService.cs +++ b/src/Libraries/SmartStore.Services/Stores/IStoreMappingService.cs @@ -2,12 +2,7 @@ using System.Collections.Generic; using System.Linq; using SmartStore.Core; -using SmartStore.Core.Caching; -using SmartStore.Core.Data; -using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Security; using SmartStore.Core.Domain.Stores; -using SmartStore.Services.Security; namespace SmartStore.Services.Stores { @@ -73,29 +68,81 @@ public partial interface IStoreMappingService /// Store mapping void UpdateStoreMapping(StoreMapping storeMapping); + /// + /// Find store identifiers with granted access (mapped to the entity) + /// + /// Entity name to check + /// Entity id to check + /// Store identifiers + int[] GetStoresIdsWithAccess(string entityName, int entityId); + + /// + /// Checks whether an entity can be accessed in a store (mapped to this store) + /// + /// Entity name to check + /// Entity id to check + /// true - authorized; otherwise, false + bool Authorize(string entityName, int entityId); + + /// + /// Checks whether an entity can be accessed in a store (mapped to this store) + /// + /// Entity name to check + /// Entity id to check + /// Store identifier to check against + /// true - authorized; otherwise, false + bool Authorize(string entityName, int entityId, int storeId); + } + + public static class IStoreMappingServiceExtensions + { /// /// Find store identifiers with granted access (mapped to the entity) /// /// Type - /// Wntity + /// Entity /// Store identifiers - int[] GetStoresIdsWithAccess(T entity) where T : BaseEntity, IStoreMappingSupported; + public static int[] GetStoresIdsWithAccess(this IStoreMappingService svc, T entity) where T : BaseEntity, IStoreMappingSupported + { + if (entity == null) + return new int[0]; + + return svc.GetStoresIdsWithAccess(typeof(T).Name, entity.Id); + } /// /// Authorize whether entity could be accessed in the current store (mapped to this store) /// /// Type - /// Wntity + /// Entity /// true - authorized; otherwise, false - bool Authorize(T entity) where T : BaseEntity, IStoreMappingSupported; + public static bool Authorize(this IStoreMappingService svc, T entity) where T : BaseEntity, IStoreMappingSupported + { + if (entity == null) + return false; + + if (!entity.LimitedToStores) + return true; + + return svc.Authorize(typeof(T).Name, entity.Id); + } /// /// Authorize whether entity could be accessed in a store (mapped to this store) /// /// Type /// Entity - /// Store identifier + /// Store identifier to check against /// true - authorized; otherwise, false - bool Authorize(T entity, int storeId) where T : BaseEntity, IStoreMappingSupported; + public static bool Authorize(this IStoreMappingService svc, T entity, int storeId) where T : BaseEntity, IStoreMappingSupported + { + if (entity == null) + return false; + + if (!entity.LimitedToStores) + return true; + + return svc.Authorize(typeof(T).Name, entity.Id, storeId); + } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Stores/IStoreService.cs b/src/Libraries/SmartStore.Services/Stores/IStoreService.cs index 31d9e5d0bd..90a6da14ce 100644 --- a/src/Libraries/SmartStore.Services/Stores/IStoreService.cs +++ b/src/Libraries/SmartStore.Services/Stores/IStoreService.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Domain.Security; namespace SmartStore.Services.Stores { @@ -49,5 +50,16 @@ public partial interface IStoreService /// /// Store entity bool IsStoreDataValid(Store store); + + /// + /// Gets the store host name + /// + /// The store to get the host name for + /// + /// If null, checks whether all pages should be secured per . + /// If true, returns the secure url, but only if SSL is enabled for the store. + /// + /// The host name + string GetHost(Store store, bool? secure = null); } } \ 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 5dba041813..ef4f36513c 100644 --- a/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs +++ b/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs @@ -8,74 +8,40 @@ namespace SmartStore.Services.Stores { - /// - /// Store mapping service - /// public partial class StoreMappingService : IStoreMappingService { - #region Constants - private const string STOREMAPPING_BY_ENTITYID_NAME_KEY = "storemapping:entityid-name-{0}-{1}"; - private const string STOREMAPPING_PATTERN_KEY = "storemapping:"; - - #endregion - - #region Fields + private const string STOREMAPPING_PATTERN_KEY = "storemapping:*"; private readonly IRepository _storeMappingRepository; private readonly IStoreContext _storeContext; private readonly IStoreService _storeService; private readonly ICacheManager _cacheManager; - #endregion - - #region Ctor - - /// - /// Ctor - /// - /// Cache manager - /// Store context - /// Store mapping repository public StoreMappingService(ICacheManager cacheManager, IStoreContext storeContext, IStoreService storeService, IRepository storeMappingRepository) { - this._cacheManager = cacheManager; - this._storeContext = storeContext; - this._storeService = storeService; - this._storeMappingRepository = storeMappingRepository; + _cacheManager = cacheManager; + _storeContext = storeContext; + _storeService = storeService; + _storeMappingRepository = storeMappingRepository; - this.QuerySettings = DbQuerySettings.Default; + QuerySettings = DbQuerySettings.Default; } public DbQuerySettings QuerySettings { get; set; } - #endregion - - #region Methods - - /// - /// Deletes a store mapping record - /// - /// Store mapping record public virtual void DeleteStoreMapping(StoreMapping storeMapping) { - if (storeMapping == null) - throw new ArgumentNullException("storeMapping"); + Guard.NotNull(storeMapping, nameof(storeMapping)); _storeMappingRepository.Delete(storeMapping); - //cache _cacheManager.RemoveByPattern(STOREMAPPING_PATTERN_KEY); } - /// - /// Gets a store mapping record - /// - /// Store mapping record identifier - /// Store mapping record public virtual StoreMapping GetStoreMappingById(int storeMappingId) { if (storeMappingId == 0) @@ -85,16 +51,9 @@ public virtual StoreMapping GetStoreMappingById(int storeMappingId) return storeMapping; } - /// - /// Gets store mapping records - /// - /// Type - /// Entity - /// Store mapping records public virtual IList GetStoreMappings(T entity) where T : BaseEntity, IStoreMappingSupported { - if (entity == null) - throw new ArgumentNullException("entity"); + Guard.NotNull(entity, nameof(entity)); int entityId = entity.Id; string entityName = typeof(T).Name; @@ -107,12 +66,6 @@ public virtual IList GetStoreMappings(T entity) where T : BaseE return storeMappings; } - /// - /// Gets store mapping records - /// - /// Could be null - /// Could be 0 - /// Store mapping record query public virtual IQueryable GetStoreMappingsFor(string entityName, int entityId) { var query = _storeMappingRepository.Table; @@ -126,12 +79,6 @@ public virtual IQueryable GetStoreMappingsFor(string entityName, i return query; } - /// - /// Save the store napping for an entity - /// - /// Entity type - /// The entity - /// Array of selected store ids public virtual void SaveStoreMappings(T entity, int[] selectedStoreIds) where T : BaseEntity, IStoreMappingSupported { var existingStoreMappings = GetStoreMappings(entity); @@ -153,34 +100,21 @@ public virtual void SaveStoreMappings(T entity, int[] selectedStoreIds) where } } - /// - /// Inserts a store mapping record - /// - /// Store mapping public virtual void InsertStoreMapping(StoreMapping storeMapping) { - if (storeMapping == null) - throw new ArgumentNullException("storeMapping"); + Guard.NotNull(storeMapping, nameof(storeMapping)); _storeMappingRepository.Insert(storeMapping); - //cache _cacheManager.RemoveByPattern(STOREMAPPING_PATTERN_KEY); } - /// - /// Inserts a store mapping record - /// - /// Type - /// Store id - /// Entity public virtual void InsertStoreMapping(T entity, int storeId) where T : BaseEntity, IStoreMappingSupported { - if (entity == null) - throw new ArgumentNullException("entity"); + Guard.NotNull(entity, nameof(entity)); if (storeId == 0) - throw new ArgumentOutOfRangeException("storeId"); + throw new ArgumentOutOfRangeException(nameof(storeId)); int entityId = entity.Id; string entityName = typeof(T).Name; @@ -195,34 +129,21 @@ public virtual void InsertStoreMapping(T entity, int storeId) where T : BaseE InsertStoreMapping(storeMapping); } - /// - /// Updates the store mapping record - /// - /// Store mapping public virtual void UpdateStoreMapping(StoreMapping storeMapping) { - if (storeMapping == null) - throw new ArgumentNullException("storeMapping"); + Guard.NotNull(storeMapping, nameof(storeMapping)); _storeMappingRepository.Update(storeMapping); - //cache _cacheManager.RemoveByPattern(STOREMAPPING_PATTERN_KEY); } - /// - /// Find store identifiers with granted access (mapped to the entity) - /// - /// Type - /// Wntity - /// Store identifiers - public virtual int[] GetStoresIdsWithAccess(T entity) where T : BaseEntity, IStoreMappingSupported + public virtual int[] GetStoresIdsWithAccess(string entityName, int entityId) { - if (entity == null) - return new int[0]; + Guard.NotEmpty(entityName, nameof(entityName)); - int entityId = entity.Id; - string entityName = typeof(T).Name; + if (entityId <= 0) + return new int[0]; string key = string.Format(STOREMAPPING_BY_ENTITYID_NAME_KEY, entityId, entityName); return _cacheManager.Get(key, () => @@ -231,56 +152,42 @@ public virtual int[] GetStoresIdsWithAccess(T entity) where T : BaseEntity, I where sm.EntityId == entityId && sm.EntityName == entityName select sm.StoreId; + var result = query.ToArray(); - //little hack here. nulls aren't cacheable so set it to "" - if (result == null) - result = new int[0]; return result; }); } - /// - /// Authorize whether entity could be accessed in the current store (mapped to this store) - /// - /// Type - /// Wntity - /// true - authorized; otherwise, false - public virtual bool Authorize(T entity) where T : BaseEntity, IStoreMappingSupported + public bool Authorize(string entityName, int entityId) { - return Authorize(entity, _storeContext.CurrentStore.Id); + return Authorize(entityName, entityId, _storeContext.CurrentStore.Id); } - /// - /// Authorize whether entity could be accessed in a store (mapped to this store) - /// - /// Type - /// Entity - /// Store - /// true - authorized; otherwise, false - public virtual bool Authorize(T entity, int storeId) where T : BaseEntity, IStoreMappingSupported + public virtual bool Authorize(string entityName, int entityId, int storeId) { - if (entity == null) + Guard.NotEmpty(entityName, nameof(entityName)); + + if (entityId <= 0) return false; - if (storeId == 0) - //return true if no store specified/found + if (storeId <= 0) + // return true if no store specified/found return true; if (QuerySettings.IgnoreMultiStore) return true; - if (!entity.LimitedToStores) - return true; - - foreach (var storeIdWithAccess in GetStoresIdsWithAccess(entity)) + foreach (var storeIdWithAccess in GetStoresIdsWithAccess(entityName, entityId)) + { if (storeId == storeIdWithAccess) - //yes, we have such permission + { + // yes, we have such permission return true; + } + } - //no permission found + // no permission granted return false; } - - #endregion } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Stores/StoreService.cs b/src/Libraries/SmartStore.Services/Stores/StoreService.cs index 15409b78b4..e1bbcd8b6f 100644 --- a/src/Libraries/SmartStore.Services/Stores/StoreService.cs +++ b/src/Libraries/SmartStore.Services/Stores/StoreService.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using SmartStore.Data; using SmartStore.Core.Data; using SmartStore.Core.Domain.Stores; using SmartStore.Core.Events; using SmartStore.Data.Caching; +using SmartStore.Services.Media; +using SmartStore.Core.Domain.Security; namespace SmartStore.Services.Stores { @@ -12,29 +15,26 @@ public partial class StoreService : IStoreService { private readonly IRepository _storeRepository; private readonly IEventPublisher _eventPublisher; + private readonly SecuritySettings _securitySettings; + private bool? _isSingleStoreMode = null; - public StoreService( - IRepository storeRepository, - IEventPublisher eventPublisher) + public StoreService(IRepository storeRepository, IEventPublisher eventPublisher, SecuritySettings securitySettings) { - this._storeRepository = storeRepository; - this._eventPublisher = eventPublisher; + _storeRepository = storeRepository; + _eventPublisher = eventPublisher; + _securitySettings = securitySettings; } public virtual void DeleteStore(Store store) { - if (store == null) - throw new ArgumentNullException("store"); + Guard.NotNull(store, nameof(store)); var allStores = GetAllStores(); if (allStores.Count == 1) throw new Exception("You cannot delete the only configured store."); _storeRepository.Delete(store); - - //event notification - _eventPublisher.EntityDeleted(store); } public virtual IList GetAllStores() @@ -59,24 +59,16 @@ public virtual Store GetStoreById(int storeId) public virtual void InsertStore(Store store) { - if (store == null) - throw new ArgumentNullException("store"); + Guard.NotNull(store, nameof(store)); _storeRepository.Insert(store); - - //event notification - _eventPublisher.EntityInserted(store); } public virtual void UpdateStore(Store store) { - if (store == null) - throw new ArgumentNullException("store"); + Guard.NotNull(store, nameof(store)); _storeRepository.Update(store); - - //event notification - _eventPublisher.EntityUpdated(store); } public virtual bool IsSingleStoreMode() @@ -91,8 +83,7 @@ public virtual bool IsSingleStoreMode() public virtual bool IsStoreDataValid(Store store) { - if (store == null) - throw new ArgumentNullException("store"); + Guard.NotNull(store, nameof(store)); if (store.Url.IsEmpty()) return false; @@ -115,10 +106,17 @@ public virtual bool IsStoreDataValid(Store store) return store.Url.IsWebUrl(); } } - catch (Exception) + catch { return false; } } + + public string GetHost(Store store, bool? secure = null) + { + Guard.NotNull(store, nameof(store)); + + return store.GetHost(secure ?? _securitySettings.ForceSslForAllPages); + } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Tasks/ChangeTaskSchedulerBaseUrlConsumer.cs b/src/Libraries/SmartStore.Services/Tasks/ChangeTaskSchedulerBaseUrlConsumer.cs deleted file mode 100644 index a79a13c4ec..0000000000 --- a/src/Libraries/SmartStore.Services/Tasks/ChangeTaskSchedulerBaseUrlConsumer.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Web; -using SmartStore.Core.Domain.Stores; -using SmartStore.Core.Events; -using SmartStore.Services.Stores; -using SmartStore.Utilities; - -namespace SmartStore.Services.Tasks -{ - public class ChangeTaskSchedulerBaseUrlConsumer : - IConsumer>, - IConsumer>, - IConsumer> - { - private readonly ITaskScheduler _taskScheduler; - private readonly IStoreService _storeService; - private readonly HttpContextBase _httpContext; - private readonly bool _shouldChange; - - public ChangeTaskSchedulerBaseUrlConsumer(ITaskScheduler taskScheduler, IStoreService storeService, HttpContextBase httpContext) - { - this._taskScheduler = taskScheduler; - this._storeService = storeService; - this._httpContext = httpContext; - this._shouldChange = CommonHelper.GetAppSetting("sm:TaskSchedulerBaseUrl").IsWebUrl() == false; - } - - public void HandleEvent(EntityInserted eventMessage) - { - HandleEventCore(); - } - - public void HandleEvent(EntityUpdated eventMessage) - { - HandleEventCore(); - } - - public void HandleEvent(EntityDeleted eventMessage) - { - HandleEventCore(); - } - - private void HandleEventCore() - { - if (_shouldChange) - { - _taskScheduler.SetBaseUrl(_storeService, _httpContext); - } - } - } -} diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs b/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs index 34ade8a687..f9de1eb89a 100644 --- a/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs +++ b/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs @@ -93,7 +93,7 @@ public virtual void SetProgress(int? progress, string message, bool immediately try // dont't let this abort the task on failure { var dbContext = _componentContext.Resolve(); - dbContext.ChangeState(_originalTask, System.Data.Entity.EntityState.Modified); + //dbContext.ChangeState(_originalTask, System.Data.Entity.EntityState.Modified); dbContext.SaveChanges(); } catch { } diff --git a/src/Libraries/SmartStore.Services/Tax/ITaxService.cs b/src/Libraries/SmartStore.Services/Tax/ITaxService.cs index 7ddae4134a..95bbe83faf 100644 --- a/src/Libraries/SmartStore.Services/Tax/ITaxService.cs +++ b/src/Libraries/SmartStore.Services/Tax/ITaxService.cs @@ -6,6 +6,7 @@ using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Tax; using SmartStore.Core.Plugins; +using SmartStore.Core.Domain.Directory; namespace SmartStore.Services.Tax { @@ -83,31 +84,48 @@ public partial interface ITaxService /// Price decimal GetProductPrice(Product product, decimal price, Customer customer, out decimal taxRate); - /// - /// Gets price - /// + /// + /// Gets price + /// /// Product - /// Price - /// A value indicating whether calculated price should include tax - /// Customer - /// Tax rate - /// Price - decimal GetProductPrice(Product product, decimal price, bool includingTax, Customer customer, out decimal taxRate); + /// Price + /// Customer + /// Currency + /// Tax rate + /// Price + decimal GetProductPrice(Product product, decimal price, Customer customer, Currency currency, out decimal taxRate); - /// - /// Gets price - /// + /// + /// Gets price + /// /// Product - /// Tax category identifier - /// Price - /// A value indicating whether calculated price should include tax - /// Customer - /// A value indicating whether price already includes tax - /// Tax rate - /// Price - decimal GetProductPrice(Product product, int taxCategoryId, decimal price, - bool includingTax, Customer customer, - bool priceIncludesTax, out decimal taxRate); + /// Price + /// A value indicating whether calculated price should include tax + /// Customer + /// Tax rate + /// Price + decimal GetProductPrice(Product product, decimal price, bool includingTax, Customer customer, out decimal taxRate); + + /// + /// Gets price + /// + /// Product + /// Tax category identifier + /// Price + /// A value indicating whether calculated price should include tax + /// Customer + /// Currency + /// A value indicating whether price already includes tax + /// Tax rate + /// Price + decimal GetProductPrice(Product product, + int taxCategoryId, + decimal price, + bool includingTax, + Customer customer, + Currency currency, + bool priceIncludesTax, + out decimal taxRate); diff --git a/src/Libraries/SmartStore.Services/Tax/TaxCategoryService.cs b/src/Libraries/SmartStore.Services/Tax/TaxCategoryService.cs index 6aae18a827..4038498b9b 100644 --- a/src/Libraries/SmartStore.Services/Tax/TaxCategoryService.cs +++ b/src/Libraries/SmartStore.Services/Tax/TaxCategoryService.cs @@ -27,9 +27,6 @@ public virtual void DeleteTaxCategory(TaxCategory taxCategory) throw new ArgumentNullException("taxCategory"); _taxCategoryRepository.Delete(taxCategory); - - //event notification - _eventPublisher.EntityDeleted(taxCategory); } public virtual IList GetAllTaxCategories() @@ -56,9 +53,6 @@ public virtual void InsertTaxCategory(TaxCategory taxCategory) throw new ArgumentNullException("taxCategory"); _taxCategoryRepository.Insert(taxCategory); - - //event notification - _eventPublisher.EntityInserted(taxCategory); } public virtual void UpdateTaxCategory(TaxCategory taxCategory) @@ -67,9 +61,6 @@ public virtual void UpdateTaxCategory(TaxCategory taxCategory) throw new ArgumentNullException("taxCategory"); _taxCategoryRepository.Update(taxCategory); - - //event notification - _eventPublisher.EntityUpdated(taxCategory); } } } diff --git a/src/Libraries/SmartStore.Services/Tax/TaxService.cs b/src/Libraries/SmartStore.Services/Tax/TaxService.cs index fdd1a18628..bcf14821e3 100644 --- a/src/Libraries/SmartStore.Services/Tax/TaxService.cs +++ b/src/Libraries/SmartStore.Services/Tax/TaxService.cs @@ -11,6 +11,7 @@ using SmartStore.Core.Plugins; using SmartStore.Services.Common; using SmartStore.Services.Directory; +using SmartStore.Core.Domain.Directory; namespace SmartStore.Services.Tax { @@ -222,7 +223,7 @@ protected virtual Address GetTaxAddress(Customer customer, Product product = nul /// Percent /// Increase /// New price - protected decimal CalculatePrice(decimal price, decimal percent, bool increase) + protected decimal CalculatePrice(decimal price, decimal percent, bool increase, Currency currency) { decimal result = decimal.Zero; if (percent == decimal.Zero) @@ -234,16 +235,12 @@ protected decimal CalculatePrice(decimal price, decimal percent, bool increase) } else { - if (_cartSettings.RoundPricesDuringCalculation) - { - // Gross > Net RoundFix - result = price - Math.Round((price) / (100 + percent) * percent, 2); - } - else - { - result = price - (price) / (100 + percent) * percent; - } + var decreaseValue = (price) / (100 + percent) * percent; + result = price - decreaseValue; } + + // Gross > Net RoundFix + result = result.RoundIfEnabledFor(currency); return result; } @@ -379,11 +376,9 @@ protected virtual decimal GetTaxRateCore(Product product, int taxCategoryId, Cus /// Price /// Tax rate /// Price - public virtual decimal GetProductPrice(Product product, decimal price, - out decimal taxRate) + public virtual decimal GetProductPrice(Product product, decimal price, out decimal taxRate) { - var customer = _workContext.CurrentCustomer; - return GetProductPrice(product, price, customer, out taxRate); + return GetProductPrice(product, price, _workContext.CurrentCustomer, out taxRate); } /// @@ -394,49 +389,58 @@ public virtual decimal GetProductPrice(Product product, decimal price, /// Customer /// Tax rate /// Price - public virtual decimal GetProductPrice(Product product, decimal price, - Customer customer, out decimal taxRate) + public virtual decimal GetProductPrice(Product product, decimal price, Customer customer, out decimal taxRate) { - bool includingTax = _workContext.TaxDisplayType == TaxDisplayType.IncludingTax; + var includingTax = _workContext.TaxDisplayType == TaxDisplayType.IncludingTax; + return GetProductPrice(product, price, includingTax, customer, out taxRate); } - /// - /// Gets price - /// + public virtual decimal GetProductPrice(Product product, decimal price, Customer customer, Currency currency, out decimal taxRate) + { + var includingTax = _workContext.TaxDisplayType == TaxDisplayType.IncludingTax; + var priceIncludesTax = _taxSettings.PricesIncludeTax; + var taxCategoryId = product.TaxCategoryId; // 0; // (VATFIX) + + return GetProductPrice(product, taxCategoryId, price, includingTax, customer, currency, priceIncludesTax, out taxRate); + } + + /// + /// Gets price + /// /// Product - /// Price - /// A value indicating whether calculated price should include tax - /// Customer - /// Tax rate - /// Price - public virtual decimal GetProductPrice(Product product, decimal price, - bool includingTax, Customer customer, out decimal taxRate) + /// Price + /// A value indicating whether calculated price should include tax + /// Customer + /// Tax rate + /// Price + public virtual decimal GetProductPrice(Product product, decimal price, bool includingTax, Customer customer, out decimal taxRate) { - bool priceIncludesTax = _taxSettings.PricesIncludeTax; - int taxCategoryId = product.TaxCategoryId; // 0; // (VATFIX) - return GetProductPrice(product, taxCategoryId, price, includingTax, - customer, priceIncludesTax, out taxRate); - } + var priceIncludesTax = _taxSettings.PricesIncludeTax; + var taxCategoryId = product.TaxCategoryId; // 0; // (VATFIX) - /// - /// Gets price - /// + return GetProductPrice(product, taxCategoryId, price, includingTax, customer, _workContext.WorkingCurrency, priceIncludesTax, out taxRate); + } + + /// + /// Gets price + /// /// Product - /// Tax category identifier - /// Price - /// A value indicating whether calculated price should include tax - /// Customer - /// A value indicating whether price already includes tax - /// Tax rate - /// Price - public virtual decimal GetProductPrice( + /// Tax category identifier + /// Price + /// A value indicating whether calculated price should include tax + /// Customer + /// A value indicating whether price already includes tax + /// Tax rate + /// Price + public virtual decimal GetProductPrice( Product product, int taxCategoryId, decimal price, bool includingTax, Customer customer, - bool priceIncludesTax, + Currency currency, + bool priceIncludesTax, out decimal taxRate) { // don't calculate if price is 0 @@ -453,7 +457,7 @@ public virtual decimal GetProductPrice( { if (!includingTax) { - price = CalculatePrice(price, taxRate, false); + price = CalculatePrice(price, taxRate, false, currency); } } // Admin: NET prices @@ -461,7 +465,7 @@ public virtual decimal GetProductPrice( { if (includingTax) { - price = CalculatePrice(price, taxRate, true); + price = CalculatePrice(price, taxRate, true, currency); } } @@ -505,6 +509,7 @@ public virtual decimal GetShippingPrice(decimal price, bool includingTax, Custom price, includingTax, customer, + _workContext.WorkingCurrency, _taxSettings.ShippingPriceIncludesTax, out taxRate); @@ -543,6 +548,7 @@ public virtual decimal GetPaymentMethodAdditionalFee(decimal price, bool includi price, includingTax, customer, + _workContext.WorkingCurrency, _taxSettings.PaymentMethodAdditionalFeeIncludesTax, out taxRate); @@ -604,17 +610,16 @@ public virtual decimal GetCheckoutAttributePrice(CheckoutAttributeValue cav, taxRate = decimal.Zero; - bool priceIncludesTax = _taxSettings.PricesIncludeTax; + var priceIncludesTax = _taxSettings.PricesIncludeTax; + var taxClassId = cav.CheckoutAttribute.TaxCategoryId; + var price = cav.PriceAdjustment; - decimal price = cav.PriceAdjustment; if (cav.CheckoutAttribute.IsTaxExempt) { return price; } - int taxClassId = cav.CheckoutAttribute.TaxCategoryId; - return GetProductPrice(null, taxClassId, price, includingTax, customer, - priceIncludesTax, out taxRate); + return GetProductPrice(null, taxClassId, price, includingTax, customer, _workContext.WorkingCurrency, priceIncludesTax, out taxRate); } diff --git a/src/Libraries/SmartStore.Services/Themes/ThemeVariablesService.cs b/src/Libraries/SmartStore.Services/Themes/ThemeVariablesService.cs index 9814d1a532..79f000af32 100644 --- a/src/Libraries/SmartStore.Services/Themes/ThemeVariablesService.cs +++ b/src/Libraries/SmartStore.Services/Themes/ThemeVariablesService.cs @@ -65,7 +65,7 @@ public virtual ExpandoObject GetThemeVariables(string themeName, int storeId) }); // ...then merge with persisted runtime records - var query = from v in _rsVariables.Table + var query = from v in _rsVariables.TableUntracked where v.StoreId == storeId && v.Theme.Equals(themeName, StringComparison.OrdinalIgnoreCase) select v; @@ -83,10 +83,10 @@ public virtual ExpandoObject GetThemeVariables(string themeName, int storeId) public virtual void DeleteThemeVariables(string themeName, int storeId) { - DeleteThemeVariablesInternal(themeName, storeId, true); + DeleteThemeVariablesInternal(themeName, storeId); } - private void DeleteThemeVariablesInternal(string themeName, int storeId, bool publishEvents) + private void DeleteThemeVariablesInternal(string themeName, int storeId) { Guard.NotEmpty(themeName, nameof(themeName)); @@ -101,7 +101,6 @@ private void DeleteThemeVariablesInternal(string themeName, int storeId, bool pu query.Each(v => { _rsVariables.Delete(v); - if (publishEvents) _eventPublisher.EntityDeleted(v); }); _requestCache.Remove(THEMEVARS_BY_THEME_KEY.FormatInvariant(themeName, storeId)); @@ -141,7 +140,7 @@ public virtual int SaveThemeVariables(string themeName, int storeId, IDictionary // Restore previous vars try { - DeleteThemeVariablesInternal(themeName, storeId, false); + DeleteThemeVariablesInternal(themeName, storeId); } finally { @@ -151,10 +150,6 @@ public virtual int SaveThemeVariables(string themeName, int storeId, IDictionary throw new ThemeValidationException(error, variables); } - - result.Deleted.Each(x => _eventPublisher.EntityDeleted(x)); - result.Inserted.Each(x => _eventPublisher.EntityInserted(x)); - result.Updated.Each(x => _eventPublisher.EntityUpdated(x)); } return result.TouchedVariablesCount; diff --git a/src/Libraries/SmartStore.Services/Topics/TopicService.cs b/src/Libraries/SmartStore.Services/Topics/TopicService.cs index 2c0c749a1f..7f0117955a 100644 --- a/src/Libraries/SmartStore.Services/Topics/TopicService.cs +++ b/src/Libraries/SmartStore.Services/Topics/TopicService.cs @@ -31,13 +31,9 @@ public TopicService( public virtual void DeleteTopic(Topic topic) { - if (topic == null) - throw new ArgumentNullException("topic"); + Guard.NotNull(topic, nameof(topic)); - _topicRepository.Delete(topic); - - //event notification - _eventPublisher.EntityDeleted(topic); + _topicRepository.Delete(topic); } public virtual Topic GetTopicById(int topicId) @@ -50,8 +46,7 @@ public virtual Topic GetTopicById(int topicId) public virtual Topic GetTopicBySystemName(string systemName, int storeId) { - if (String.IsNullOrEmpty(systemName)) - return null; + Guard.NotEmpty(systemName, nameof(systemName)); var allTopics = GetAllTopics(storeId); @@ -66,7 +61,7 @@ public virtual IList GetAllTopics(int storeId) { var query = _topicRepository.Table; - //Store mapping + // Store mapping if (storeId > 0 && !QuerySettings.IgnoreMultiStore) { query = from t in query @@ -76,7 +71,7 @@ from sm in t_sm.DefaultIfEmpty() where !t.LimitedToStores || storeId == sm.StoreId select t; - //only distinct items (group by ID) + // Only distinct items (group by ID) query = from t in query group t by t.Id into tGroup orderby tGroup.Key @@ -90,24 +85,16 @@ orderby tGroup.Key public virtual void InsertTopic(Topic topic) { - if (topic == null) - throw new ArgumentNullException("topic"); + Guard.NotNull(topic, nameof(topic)); - _topicRepository.Insert(topic); - - //event notification - _eventPublisher.EntityInserted(topic); + _topicRepository.Insert(topic); } public virtual void UpdateTopic(Topic topic) { - if (topic == null) - throw new ArgumentNullException("topic"); - - _topicRepository.Update(topic); + Guard.NotNull(topic, nameof(topic)); - //event notification - _eventPublisher.EntityUpdated(topic); + _topicRepository.Update(topic); } } } diff --git a/src/Libraries/SmartStore.Services/app.config b/src/Libraries/SmartStore.Services/app.config index cce84e581d..48f67fefef 100644 --- a/src/Libraries/SmartStore.Services/app.config +++ b/src/Libraries/SmartStore.Services/app.config @@ -31,6 +31,10 @@ + + + + @@ -39,4 +43,4 @@ - + diff --git a/src/Libraries/SmartStore.Services/packages.config b/src/Libraries/SmartStore.Services/packages.config index 8fb8605deb..dc8b84bdc8 100644 --- a/src/Libraries/SmartStore.Services/packages.config +++ b/src/Libraries/SmartStore.Services/packages.config @@ -1,12 +1,12 @@  + - + - - + @@ -16,6 +16,8 @@ + + \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/AmazonPaySettings.cs b/src/Plugins/SmartStore.AmazonPay/AmazonPaySettings.cs new file mode 100644 index 0000000000..21cce48dbe --- /dev/null +++ b/src/Plugins/SmartStore.AmazonPay/AmazonPaySettings.cs @@ -0,0 +1,65 @@ +using SmartStore.AmazonPay.Services; +using SmartStore.Core.Configuration; + +namespace SmartStore.AmazonPay +{ + public class AmazonPaySettings : ISettings + { + public AmazonPaySettings() + { + Marketplace = "de"; + DataFetching = AmazonPayDataFetchingType.Ipn; + TransactionType = AmazonPayTransactionType.Authorize; + AuthorizeMethod = AmazonPayAuthorizeMethod.Omnichronous; + SaveEmailAndPhone = AmazonPaySaveDataType.OnlyIfEmpty; + AddOrderNotes = true; + InformCustomerAboutErrors = true; + InformCustomerAddErrors = true; + PollingMaxOrderCreationDays = 31; + + PayButtonColor = "Gold"; + PayButtonSize = "small"; + AuthButtonType = "LwA"; + AuthButtonColor = "Gold"; + AuthButtonSize = "medium"; + } + + public bool UseSandbox { get; set; } + + public string SellerId { get; set; } + public string AccessKey { get; set; } + public string SecretKey { get; set; } + public string ClientId { get; set; } + public string Marketplace { get; set; } + + public AmazonPayDataFetchingType DataFetching { get; set; } + public AmazonPayTransactionType TransactionType { get; set; } + public AmazonPayAuthorizeMethod AuthorizeMethod { get; set; } + + public AmazonPaySaveDataType? SaveEmailAndPhone { get; set; } + public bool ShowPayButtonForAdminOnly { get; set; } + public bool ShowButtonInMiniShoppingCart { get; set; } + + public int PollingMaxOrderCreationDays { get; set; } + + public decimal AdditionalFee { get; set; } + public bool AdditionalFeePercentage { get; set; } + + public bool AddOrderNotes { get; set; } + + public bool InformCustomerAboutErrors { get; set; } + public bool InformCustomerAddErrors { get; set; } + + public string PayButtonColor { get; set; } + public string PayButtonSize { get; set; } + + public string AuthButtonType { get; set; } + public string AuthButtonColor { get; set; } + public string AuthButtonSize { get; set; } + + public bool CanSaveEmailAndPhone(string value) + { + return (SaveEmailAndPhone == AmazonPaySaveDataType.Always || (SaveEmailAndPhone == AmazonPaySaveDataType.OnlyIfEmpty && value.IsEmpty())); + } + } +} diff --git a/src/Plugins/SmartStore.AmazonPay/Content/SmartStore.AmazonPay.css b/src/Plugins/SmartStore.AmazonPay/Content/SmartStore.AmazonPay.css new file mode 100644 index 0000000000..c6a9d38b4c --- /dev/null +++ b/src/Plugins/SmartStore.AmazonPay/Content/SmartStore.AmazonPay.css @@ -0,0 +1,36 @@ + +#amazon-pay-address-book-widget { + min-width: 300px; + width: 100%; + max-width: 900px; + min-height: 228px; + height: 240px; + max-height: 400px; +} + +#amazon-pay-wallet-widget { + min-width: 300px; + width: 100%; + max-width: 900px; + min-height: 228px; + height: 240px; + max-height: 400px; +} + +#amazon-pay-read-address-book-widget { + min-width: 266px; + width: 100%; + max-width: 900px; + min-height: 145px; + height: 165px; + max-height: 180px; +} + +#amazon-pay-read-wallet-widget { + min-width: 266px; + width: 100%; + max-width: 900px; + min-height: 145px; + height: 165px; + max-height: 180px; +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Content/branding.gif b/src/Plugins/SmartStore.AmazonPay/Content/branding.gif deleted file mode 100644 index bc6f68849f..0000000000 Binary files a/src/Plugins/SmartStore.AmazonPay/Content/branding.gif and /dev/null differ diff --git a/src/Plugins/SmartStore.AmazonPay/Content/branding.png b/src/Plugins/SmartStore.AmazonPay/Content/branding.png new file mode 100644 index 0000000000..179fb46130 Binary files /dev/null and b/src/Plugins/SmartStore.AmazonPay/Content/branding.png differ diff --git a/src/Plugins/SmartStore.AmazonPay/Content/images/logo.gif b/src/Plugins/SmartStore.AmazonPay/Content/images/logo.gif deleted file mode 100644 index bc6f68849f..0000000000 Binary files a/src/Plugins/SmartStore.AmazonPay/Content/images/logo.gif and /dev/null differ diff --git a/src/Plugins/SmartStore.AmazonPay/Content/smartstore.amazonpay.css b/src/Plugins/SmartStore.AmazonPay/Content/smartstore.amazonpay.css deleted file mode 100644 index 2920ce6f7f..0000000000 --- a/src/Plugins/SmartStore.AmazonPay/Content/smartstore.amazonpay.css +++ /dev/null @@ -1,14 +0,0 @@ -.config-logo { - width: 339px; - height: 74px; -} - -.amazon-pay-button .selection-text { - text-align: right; - font-weight: bold; - padding: 12px 15px 5px 0; -} - -.amazon-pay-button .button-container { - text-align: right; -} diff --git a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayCheckoutController.cs b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayCheckoutController.cs index a189fd67ac..0c44f89540 100644 --- a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayCheckoutController.cs +++ b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayCheckoutController.cs @@ -1,15 +1,60 @@ -using System.Web.Mvc; +using System; +using System.Web; +using System.Web.Mvc; using SmartStore.AmazonPay.Services; +using SmartStore.Services.Common; namespace SmartStore.AmazonPay.Controllers { public class AmazonPayCheckoutController : AmazonPayControllerBase { + private readonly HttpContextBase _httpContext; private readonly IAmazonPayService _apiService; + private readonly IGenericAttributeService _genericAttributeService; - public AmazonPayCheckoutController(IAmazonPayService apiService) + public AmazonPayCheckoutController( + HttpContextBase httpContext, + IAmazonPayService apiService, + IGenericAttributeService genericAttributeService) { + _httpContext = httpContext; _apiService = apiService; + _genericAttributeService = genericAttributeService; + } + + public ActionResult OrderReferenceCreated(string orderReferenceId/*, string accessToken*/) + { + var success = false; + var error = string.Empty; + + try + { + var state = _httpContext.GetAmazonPayState(Services.Localization); + state.OrderReferenceId = orderReferenceId; + + //if (accessToken.HasValue()) + //{ + // state.AccessToken = accessToken; + //} + + if (state.OrderReferenceId.IsEmpty()) + { + success = false; + error = T("Plugins.Payments.AmazonPay.MissingOrderReferenceId"); + } + + if (state.AccessToken.IsEmpty()) + { + success = false; + error = error.Grow(T("Plugins.Payments.AmazonPay.MissingAddressConsentToken"), " "); + } + } + catch (Exception exception) + { + error = exception.Message; + } + + return new JsonResult { Data = new { success = success, error = error } }; } public ActionResult BillingAddress() @@ -19,21 +64,23 @@ public ActionResult BillingAddress() public ActionResult ShippingAddress() { - var model = _apiService.ProcessPluginRequest(AmazonPayRequestType.Address, TempData); + var model = _apiService.CreateViewModel(AmazonPayRequestType.Address, TempData); return GetActionResult(model); } public ActionResult PaymentMethod() { - var model = _apiService.ProcessPluginRequest(AmazonPayRequestType.Payment, TempData); + var model = _apiService.CreateViewModel(AmazonPayRequestType.PaymentMethod, TempData); return GetActionResult(model); } [HttpPost] - public ActionResult PaymentMethod(bool? UseRewardPoints) + public ActionResult PaymentMethod(FormCollection form) { + _apiService.GetBillingAddress(); + return RedirectToAction("Confirm", "Checkout", new { area = "" }); } @@ -41,5 +88,16 @@ public ActionResult PaymentInfo() { return RedirectToAction("PaymentMethod", "Checkout", new { area = "" }); } + + public ActionResult CheckoutCompleted() + { + var note = _httpContext.Session["AmazonPayCheckoutCompletedNote"] as string; + if (note.HasValue()) + { + return Content(note); + } + + return new EmptyResult(); + } } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayController.cs b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayController.cs index 233ae279e2..802b2eb195 100644 --- a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayController.cs +++ b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayController.cs @@ -1,11 +1,14 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Web.Mvc; using SmartStore.AmazonPay.Models; using SmartStore.AmazonPay.Services; -using SmartStore.AmazonPay.Settings; -using SmartStore.Services; +using SmartStore.ComponentModel; +using SmartStore.Core.Domain.Customers; +using SmartStore.Services.Authentication.External; using SmartStore.Services.Payments; -using SmartStore.Services.Stores; +using SmartStore.Services.Tasks; +using SmartStore.Web.Framework; using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.Settings; @@ -15,17 +18,20 @@ namespace SmartStore.AmazonPay.Controllers public class AmazonPayController : PaymentControllerBase { private readonly IAmazonPayService _apiService; - private readonly ICommonServices _services; - private readonly IStoreService _storeService; + private readonly Lazy _scheduleTaskService; + private readonly Lazy _openAuthenticationService; + private readonly Lazy _externalAuthenticationSettings; public AmazonPayController( IAmazonPayService apiService, - ICommonServices services, - IStoreService storeService) + Lazy scheduleTaskService, + Lazy openAuthenticationService, + Lazy externalAuthenticationSettings) { _apiService = apiService; - _services = services; - _storeService = storeService; + _scheduleTaskService = scheduleTaskService; + _openAuthenticationService = openAuthenticationService; + _externalAuthenticationSettings = externalAuthenticationSettings; } [NonAction] @@ -42,59 +48,147 @@ public override ProcessPaymentRequest GetPaymentInfo(FormCollection form) return paymentInfo; } - [AdminAuthorize] - public ActionResult Configure() + [AdminAuthorize, LoadSetting] + public ActionResult Configure(AmazonPaySettings settings) { var model = new ConfigurationModel(); - int storeScope = this.GetActiveStoreScopeConfiguration(_storeService, _services.WorkContext); - var settings = _services.Settings.LoadSetting(storeScope); - - model.Copy(settings, true); + MiniMapper.Map(settings, model); _apiService.SetupConfiguration(model); - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - storeDependingSettingHelper.GetOverrideKeys(settings, model, storeScope, _services.Settings); - return View(model); } [HttpPost, AdminAuthorize] public ActionResult Configure(ConfigurationModel model, FormCollection form) { + var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); + if (!ModelState.IsValid) - return Configure(); + return Configure(settings); ModelState.Clear(); + MiniMapper.Map(model, settings); - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - int storeScope = this.GetActiveStoreScopeConfiguration(_storeService, _services.WorkContext); - var settings = _services.Settings.LoadSetting(storeScope); + using (Services.Settings.BeginScope()) + { + storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, Services.Settings); + } - model.Copy(settings, false); + using (Services.Settings.BeginScope()) + { + Services.Settings.SaveSetting(settings, x => x.DataFetching, 0, false); + Services.Settings.SaveSetting(settings, x => x.PollingMaxOrderCreationDays, 0, false); + } - using (_services.Settings.BeginScope()) + var task = _scheduleTaskService.Value.GetTaskByType(); + if (task != null) { - storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, _services.Settings); + task.Enabled = settings.DataFetching == AmazonPayDataFetchingType.Polling; - _services.Settings.SaveSetting(settings, x => x.DataFetching, 0, false); - _services.Settings.SaveSetting(settings, x => x.PollingMaxOrderCreationDays, 0, false); + _scheduleTaskService.Value.UpdateTask(task); } - _apiService.DataPollingTaskUpdate(settings.DataFetching == AmazonPayDataFetchingType.Polling, model.PollingTaskMinutes * 60); + NotifySuccess(T("Plugins.Payments.AmazonPay.ConfigSaveNote")); + + return RedirectToConfiguration(AmazonPayPlugin.SystemName); + } - NotifySuccess(_services.Localization.GetResource("Plugins.Payments.AmazonPay.ConfigSaveNote")); + [HttpPost, AdminAuthorize] + public ActionResult SaveAccessData(string accessData) + { + try + { + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + _apiService.ShareKeys(accessData, storeScope); - return Configure(); + NotifySuccess(T("Plugins.Payments.AmazonPay.SaveAccessDataSucceeded")); + } + catch (Exception exception) + { + NotifyError(exception.Message); + } + + return RedirectToConfiguration(AmazonPayPlugin.SystemName); + } + + [ValidateInput(false)] + public ActionResult ShareKey(string payload) + { + Response.AddHeader("Access-Control-Allow-Origin", "https://payments.amazon.com"); + Response.AddHeader("Access-Control-Allow-Methods", "GET, POST"); + Response.AddHeader("Access-Control-Allow-Headers", "Content-Type"); + + try + { + _apiService.ShareKeys(payload, 0); + } + catch (Exception exception) + { + Response.StatusCode = 400; + return Json(new { result = "error", message = exception.Message }); + } + + return Json(new { result = "success" }); } [HttpPost] [ValidateInput(false)] - [RequireHttpsByConfigAttribute(SslRequirement.Yes)] public ActionResult IPNHandler() { _apiService.ProcessIpn(Request); return Content("OK"); } + + // Authentication + + [ChildActionOnly] + public ActionResult AuthenticationPublicInfo() + { + var model = _apiService.CreateViewModel(AmazonPayRequestType.AuthenticationPublicInfo, TempData); + if (model != null) + { + return View(model); + } + + return new EmptyResult(); + } + + public ActionResult AuthenticationButtonHandler() + { + var processor = _openAuthenticationService.Value.LoadExternalAuthenticationMethodBySystemName(AmazonPayPlugin.SystemName, Services.StoreContext.CurrentStore.Id); + if (processor == null || !processor.IsMethodActive(_externalAuthenticationSettings.Value)) + { + throw new SmartException(T("Plugins.Payments.AmazonPay.AuthenticationNotActive")); + } + + var returnUrl = Session["AmazonAuthReturnUrl"] as string; + var result = _apiService.Authorize(returnUrl); + + switch (result.AuthenticationStatus) + { + case OpenAuthenticationStatus.Error: + result.Errors.Each(x => NotifyError(x)); + return new RedirectResult(Url.LogOn(returnUrl)); + case OpenAuthenticationStatus.AssociateOnLogon: + return new RedirectResult(Url.LogOn(returnUrl)); + case OpenAuthenticationStatus.AutoRegisteredEmailValidation: + return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.EmailValidation, returnUrl }); + case OpenAuthenticationStatus.AutoRegisteredAdminApproval: + return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.AdminApproval, returnUrl }); + case OpenAuthenticationStatus.AutoRegisteredStandard: + return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.Standard, returnUrl }); + default: + if (result.Result != null) + return result.Result; + + if (HttpContext.Request.IsAuthenticated) + return RedirectToReferrer(returnUrl, "~/"); + + return new RedirectResult(Url.LogOn(returnUrl)); + } + } } } diff --git a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayShoppingCartController.cs b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayShoppingCartController.cs index ef5a8c90d9..e90edbd309 100644 --- a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayShoppingCartController.cs +++ b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayShoppingCartController.cs @@ -12,10 +12,9 @@ public AmazonPayShoppingCartController(IAmazonPayService apiService) _apiService = apiService; } - public ActionResult LoginHandler(string orderReferenceId) + public ActionResult PayButtonHandler() { - var model = _apiService.ProcessPluginRequest(AmazonPayRequestType.LoginHandler, TempData, orderReferenceId); - + var model = _apiService.CreateViewModel(AmazonPayRequestType.PayButtonHandler, TempData); return GetActionResult(model); } @@ -24,7 +23,7 @@ public ActionResult ShoppingCart() { if (ControllerContext.ParentActionViewContext.RequestContext.RouteData.IsRouteEqual("ShoppingCart", "Cart")) { - var model = _apiService.ProcessPluginRequest(AmazonPayRequestType.ShoppingCart, TempData); + var model = _apiService.CreateViewModel(AmazonPayRequestType.ShoppingCart, TempData); return GetActionResult(model); } @@ -36,7 +35,7 @@ public ActionResult OrderReviewData(bool renderAmazonPayView) { if (renderAmazonPayView) { - var model = _apiService.ProcessPluginRequest(AmazonPayRequestType.OrderReviewData, TempData); + var model = _apiService.CreateViewModel(AmazonPayRequestType.OrderReviewData, TempData); return View(model); } @@ -48,24 +47,11 @@ public ActionResult MiniShoppingCart(bool renderAmazonPayView) { if (renderAmazonPayView) { - var model = _apiService.ProcessPluginRequest(AmazonPayRequestType.MiniShoppingCart, TempData); + var model = _apiService.CreateViewModel(AmazonPayRequestType.MiniShoppingCart, TempData); return GetActionResult(model); } return new EmptyResult(); } - - [ChildActionOnly] - public ActionResult WidgetLibrary() - { - // not possible to load it asynchronously cause of document.write inside - string widgetUrl = _apiService.GetWidgetUrl(); - - if (widgetUrl.HasValue()) - { - return this.Content("".FormatWith(widgetUrl)); - } - return new EmptyResult(); - } } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/DependencyRegistrar.cs b/src/Plugins/SmartStore.AmazonPay/DependencyRegistrar.cs index a8a2263ede..b163acf6e8 100644 --- a/src/Plugins/SmartStore.AmazonPay/DependencyRegistrar.cs +++ b/src/Plugins/SmartStore.AmazonPay/DependencyRegistrar.cs @@ -1,11 +1,9 @@ using Autofac; using Autofac.Integration.Mvc; -using SmartStore.Core.Infrastructure; -using SmartStore.Core.Infrastructure.DependencyManagement; -using SmartStore.Core.Plugins; -using SmartStore.AmazonPay.Api; using SmartStore.AmazonPay.Filters; using SmartStore.AmazonPay.Services; +using SmartStore.Core.Infrastructure; +using SmartStore.Core.Infrastructure.DependencyManagement; using SmartStore.Web.Controllers; namespace SmartStore.AmazonPay @@ -15,7 +13,6 @@ public class DependencyRegistrar : IDependencyRegistrar public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, bool isActiveModule) { builder.RegisterType().As().InstancePerRequest(); - builder.RegisterType().As().InstancePerRequest(); if (isActiveModule) { diff --git a/src/Plugins/SmartStore.AmazonPay/Description.txt b/src/Plugins/SmartStore.AmazonPay/Description.txt index 923b6f4b77..642b86adcc 100644 --- a/src/Plugins/SmartStore.AmazonPay/Description.txt +++ b/src/Plugins/SmartStore.AmazonPay/Description.txt @@ -1,7 +1,7 @@ -FriendlyName: Pay with Amazon +FriendlyName: Login and Pay with Amazon SystemName: SmartStore.AmazonPay Group: Payment -Version: 3.0.3 +Version: 3.0.3.2 MinAppVersion: 3.0.0 Author: SmartStore AG DisplayOrder: 1 diff --git a/src/Plugins/SmartStore.AmazonPay/Events/MessageTokenEventConsumer.cs b/src/Plugins/SmartStore.AmazonPay/Events/MessageTokenEventConsumer.cs index f892459ac4..915c992002 100644 --- a/src/Plugins/SmartStore.AmazonPay/Events/MessageTokenEventConsumer.cs +++ b/src/Plugins/SmartStore.AmazonPay/Events/MessageTokenEventConsumer.cs @@ -1,5 +1,5 @@ -using System.Linq; -using SmartStore.AmazonPay.Services; +using System.Collections.Generic; +using System.Linq; using SmartStore.Core.Domain.Messages; using SmartStore.Core.Events; using SmartStore.Core.Plugins; @@ -7,11 +7,10 @@ using SmartStore.Services.Messages; using SmartStore.Services.Orders; using SmartStore.Web.Framework; -using System; namespace SmartStore.AmazonPay.Events { - public class MessageTokenEventConsumer : IConsumer> + public class MessageTokenEventConsumer : IConsumer { private readonly IPluginFinder _pluginFinder; private readonly ICommonServices _services; @@ -27,23 +26,35 @@ public MessageTokenEventConsumer( _orderService = orderService; } - public void HandleEvent(MessageTokensAddedEvent messageTokenEvent) + public void HandleEvent(MessageModelCreatedEvent message) { - if (!messageTokenEvent.Message.Name.IsCaseInsensitiveEqual("OrderPlaced.CustomerNotification")) + if (message.MessageContext.MessageTemplate.Name != MessageTemplateNames.OrderPlacedCustomer) return; var storeId = _services.StoreContext.CurrentStore.Id; - if (!_pluginFinder.IsPluginReady(_services.Settings, AmazonPayCore.SystemName, storeId)) + if (!_pluginFinder.IsPluginReady(_services.Settings, AmazonPayPlugin.SystemName, storeId)) return; - var orderId = messageTokenEvent.Tokens.Where(x => x.Key.Equals("Order.ID")).FirstOrDefault(); - var order = _orderService.GetOrderById(Convert.ToInt32(orderId.Value)); + dynamic model = message.Model; - var isAmazonPayment = (order != null && order.PaymentMethodSystemName.IsCaseInsensitiveEqual(AmazonPayCore.SystemName)); - var tokenValue = (isAmazonPayment ? _services.Localization.GetResource("Plugins.Payments.AmazonPay.BillingAddressMessageNote") : ""); + if (model.Order == null) + return; + + var orderId = model.Order.ID; + + if (orderId is int id) + { + var order = _orderService.GetOrderById(id); + + var isAmazonPayment = (order != null && order.PaymentMethodSystemName.IsCaseInsensitiveEqual(AmazonPayPlugin.SystemName)); + var tokenValue = (isAmazonPayment ? _services.Localization.GetResource("Plugins.Payments.AmazonPay.BillingAddressMessageNote") : ""); - messageTokenEvent.Tokens.Add(new Token("SmartStore.AmazonPay.BillingAddressMessageNote", tokenValue)); + model.AmazonPay = new Dictionary + { + { "BillingAddressMessageNote", tokenValue } + }; + } } } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Extensions/AmazonPayApiExtensions.cs b/src/Plugins/SmartStore.AmazonPay/Extensions/AmazonPayApiExtensions.cs deleted file mode 100644 index 8d1c2e6f08..0000000000 --- a/src/Plugins/SmartStore.AmazonPay/Extensions/AmazonPayApiExtensions.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System; -using System.Linq; -using System.Text; -using OffAmazonPaymentsService; -using OffAmazonPaymentsService.Model; -using SmartStore.AmazonPay.Extensions; -using SmartStore.AmazonPay.Services; -using SmartStore.Core.Data; -using SmartStore.Core.Domain.Orders; -using SmartStore.Services.Directory; -using SmartStore.Services.Localization; - -namespace SmartStore.AmazonPay.Api -{ - public static class AmazonPayApiExtensions - { - public static bool GetErrorStrings(this OffAmazonPaymentsServiceException exception, out string shortMessage, out string fullMessage) - { - shortMessage = fullMessage = null; - - try - { - - if (exception.Message.HasValue()) - { - shortMessage = exception.Message; - var sb = new StringBuilder(); - - sb.AppendLine("Caught Exception: " + exception.Message); - sb.AppendLine("Response Status Code: " + exception.StatusCode); - sb.AppendLine("Error Code: " + exception.ErrorCode); - sb.AppendLine("Error Type: " + exception.ErrorType); - sb.AppendLine("Request ID: " + exception.RequestId); - sb.AppendLine("XML: " + exception.XML); - - if (exception.ResponseHeaderMetadata != null) - sb.AppendLine("ResponseHeaderMetadata: " + exception.ResponseHeaderMetadata.ToString()); - - fullMessage = sb.ToString(); - } - } - catch (Exception) { } - - return shortMessage.HasValue(); - } - - public static string ToFormatedAddress(this Address amazonAddress, ICountryService countryService, IStateProvinceService stateProvinceService) - { - var sb = new StringBuilder(); - - try - { - var city = (amazonAddress.IsSetCity() ? amazonAddress.City : null); - var zip = (amazonAddress.IsSetPostalCode() ? amazonAddress.PostalCode : null); - - sb.AppendLine(""); - - if (amazonAddress.Name.HasValue()) - sb.AppendLine(amazonAddress.Name); - - if (amazonAddress.AddressLine1.HasValue()) - sb.AppendLine(amazonAddress.AddressLine1); - - if (amazonAddress.AddressLine2.HasValue()) - sb.AppendLine(amazonAddress.AddressLine2); - - if (amazonAddress.AddressLine3.HasValue()) - sb.AppendLine(amazonAddress.AddressLine3); - - sb.AppendLine(zip.Grow(city, " ")); - - if (amazonAddress.IsSetStateOrRegion()) - { - var stateProvince = stateProvinceService.GetStateProvinceByAbbreviation(amazonAddress.StateOrRegion); - - if (stateProvince == null) - sb.AppendLine(amazonAddress.StateOrRegion); - else - sb.AppendLine("{0} {1}".FormatWith(amazonAddress.StateOrRegion, stateProvince.GetLocalized(x => x.Name))); - } - - if (amazonAddress.IsSetCountryCode()) - { - var country = countryService.GetCountryByTwoOrThreeLetterIsoCode(amazonAddress.CountryCode); - - if (country == null) - sb.AppendLine(amazonAddress.CountryCode); - else - sb.AppendLine("{0} {1}".FormatWith(amazonAddress.CountryCode, country.GetLocalized(x => x.Name))); - } - - if (amazonAddress.Phone.HasValue()) - { - sb.AppendLine(amazonAddress.Phone); - } - } - catch (Exception exc) - { - exc.Dump(); - } - - return sb.ToString(); - } - - public static void ToAddress(this Address amazonAddress, SmartStore.Core.Domain.Common.Address address, ICountryService countryService, - IStateProvinceService stateProvinceService, out bool countryAllowsShipping, out bool countryAllowsBilling) - { - countryAllowsShipping = countryAllowsBilling = true; - - if (amazonAddress.IsSetName()) - { - address.ToFirstAndLastName(amazonAddress.Name); - } - - if (amazonAddress.IsSetAddressLine1()) - { - address.Address1 = amazonAddress.AddressLine1.TrimSafe().Truncate(4000); - } - - if (amazonAddress.IsSetAddressLine2()) - { - address.Address2 = amazonAddress.AddressLine2.TrimSafe().Truncate(4000); - } - - if (amazonAddress.IsSetAddressLine3()) - { - address.Address2 = address.Address2.Grow(amazonAddress.AddressLine3.TrimSafe(), ", ").Truncate(4000); - } - - // normalize - if (address.Address1.IsEmpty() && address.Address2.HasValue()) - { - address.Address1 = address.Address2; - address.Address2 = null; - } - else if (address.Address1.HasValue() && address.Address1 == address.Address2) - { - address.Address2 = null; - } - - if (amazonAddress.IsSetCity()) - { - address.City = amazonAddress.City.TrimSafe().Truncate(4000); - } - - if (amazonAddress.IsSetPostalCode()) - { - address.ZipPostalCode = amazonAddress.PostalCode.TrimSafe().Truncate(4000); - } - - if (amazonAddress.IsSetPhone()) - { - address.PhoneNumber = amazonAddress.Phone.TrimSafe().Truncate(4000); - } - - if (amazonAddress.IsSetCountryCode()) - { - var country = countryService.GetCountryByTwoOrThreeLetterIsoCode(amazonAddress.CountryCode); - - if (country != null) - { - address.CountryId = country.Id; - countryAllowsShipping = country.AllowsShipping; - countryAllowsBilling = country.AllowsBilling; - } - } - - if (amazonAddress.IsSetStateOrRegion()) - { - var stateProvince = stateProvinceService.GetStateProvinceByAbbreviation(amazonAddress.StateOrRegion); - - if (stateProvince != null) - address.StateProvinceId = stateProvince.Id; - } - - //amazonAddress.District, amazonAddress.County ?? - - if (address.CountryId == 0) - address.CountryId = null; - - if (address.StateProvinceId == 0) - address.StateProvinceId = null; - } - - public static void ToAddress(this OrderReferenceDetails details, SmartStore.Core.Domain.Common.Address address, ICountryService countryService, - IStateProvinceService stateProvinceService, out bool countryAllowsShipping, out bool countryAllowsBilling) - { - countryAllowsShipping = countryAllowsBilling = true; - - if (details.IsSetBuyer() && details.Buyer.IsSetEmail()) - { - address.Email = details.Buyer.Email; - } - - if (details.IsSetDestination() && details.Destination.IsSetPhysicalDestination()) - { - details.Destination.PhysicalDestination.ToAddress(address, countryService, stateProvinceService, out countryAllowsShipping, out countryAllowsBilling); - } - } - - public static Order GetOrderByAmazonId(this IRepository orderRepository, string amazonId) - { - // S02-9777218-8608106 OrderReferenceId - // S02-9777218-8608106-A088344 Auth ID - // S02-9777218-8608106-C088344 Capture ID - - if (amazonId.HasValue()) - { - string amazonOrderReferenceId = amazonId.Substring(0, amazonId.LastIndexOf('-')); - if (amazonOrderReferenceId.HasValue()) - { - var orders = orderRepository.Table - .Where(x => x.PaymentMethodSystemName == AmazonPayCore.SystemName && x.AuthorizationTransactionId.StartsWith(amazonOrderReferenceId)) - .ToList(); - - if (orders.Count() == 1) - return orders.FirstOrDefault(); - } - } - return null; - } - } -} \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Extensions/MiscExtensions.cs b/src/Plugins/SmartStore.AmazonPay/Extensions/MiscExtensions.cs deleted file mode 100644 index e065cff7a1..0000000000 --- a/src/Plugins/SmartStore.AmazonPay/Extensions/MiscExtensions.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Web; -using SmartStore.AmazonPay.Services; -using SmartStore.Core.Domain.Common; -using SmartStore.Services.Common; -using SmartStore.Services.Localization; - -namespace SmartStore.AmazonPay.Extensions -{ - public static class MiscExtensions - { - public static void ToFirstAndLastName(this string name, out string firstName, out string lastName) - { - if (!string.IsNullOrWhiteSpace(name)) - { - int index = name.LastIndexOf(' '); - if (index == -1) - { - firstName = ""; - lastName = name; - } - else - { - firstName = name.Substring(0, index); - lastName = name.Substring(index + 1); - } - - firstName = firstName.EmptyNull().Truncate(4000); - lastName = lastName.EmptyNull().Truncate(4000); - } - else - { - firstName = lastName = ""; - } - } - - public static void ToFirstAndLastName(this Address address, string name) - { - string firstName, lastName; - name.ToFirstAndLastName(out firstName, out lastName); - - address.FirstName = firstName; - address.LastName = lastName; - } - - public static bool HasAmazonPayState(this HttpContextBase httpContext) - { - var checkoutState = httpContext.GetCheckoutState(); - - if (checkoutState != null && checkoutState.CustomProperties.ContainsKey(AmazonPayCore.AmazonPayCheckoutStateKey)) - { - var state = checkoutState.CustomProperties[AmazonPayCore.AmazonPayCheckoutStateKey] as AmazonPayCheckoutState; - - return (state != null && state.OrderReferenceId.HasValue()); - } - return false; - } - - public static AmazonPayCheckoutState GetAmazonPayState(this HttpContextBase httpContext, ILocalizationService localizationService) - { - var checkoutState = httpContext.GetCheckoutState(); - - if (checkoutState == null) - throw new SmartException(localizationService.GetResource("Plugins.Payments.AmazonPay.MissingCheckoutSessionState")); - - var state = checkoutState.CustomProperties.Get(AmazonPayCore.AmazonPayCheckoutStateKey) as AmazonPayCheckoutState; - - if (state == null) - throw new SmartException(localizationService.GetResource("Plugins.Payments.AmazonPay.MissingCheckoutSessionState")); - - return state; - } - - public static Address FindAddress(this List
addresses, Address address, bool uncompleteToo) - { - var match = addresses.FindAddress(address.FirstName, address.LastName, - address.PhoneNumber, address.Email, address.FaxNumber, address.Company, - address.Address1, address.Address2, - address.City, address.StateProvinceId, address.ZipPostalCode, address.CountryId); - - if (match == null && uncompleteToo) - { - // compare with AmazonPayApiExtensions.ToAddress - - match = addresses.FirstOrDefault(x => - x.FirstName == null && x.LastName == null && - x.Address1 == null && x.Address2 == null && - x.City == address.City && x.ZipPostalCode == address.ZipPostalCode && - x.PhoneNumber == null && - x.CountryId == address.CountryId && x.StateProvinceId == address.StateProvinceId - ); - } - - return match; - } - } -} diff --git a/src/Plugins/SmartStore.AmazonPay/Filters/AmazonPayCheckoutFilter.cs b/src/Plugins/SmartStore.AmazonPay/Filters/AmazonPayCheckoutFilter.cs index 9efdbb4fd7..ef444da26e 100644 --- a/src/Plugins/SmartStore.AmazonPay/Filters/AmazonPayCheckoutFilter.cs +++ b/src/Plugins/SmartStore.AmazonPay/Filters/AmazonPayCheckoutFilter.cs @@ -2,7 +2,6 @@ using System.Linq; using System.Web.Mvc; using System.Web.Routing; -using SmartStore.AmazonPay.Extensions; using SmartStore.AmazonPay.Services; namespace SmartStore.AmazonPay.Filters @@ -27,20 +26,24 @@ public void OnActionExecuting(ActionExecutingContext filterContext) if (filterContext == null || filterContext.ActionDescriptor == null || filterContext.HttpContext == null || filterContext.HttpContext.Request == null) return; - if (!filterContext.HttpContext.HasAmazonPayState()) - return; - - string actionName = filterContext.ActionDescriptor.ActionName; + var actionName = filterContext.ActionDescriptor.ActionName; if (!IsInterceptableAction(actionName)) return; + if (actionName.IsCaseInsensitiveEqual("ShippingMethod") || actionName.IsCaseInsensitiveEqual("PaymentMethod")) + { + if (!filterContext.HttpContext.HasAmazonPayState()) + return; + } + if (actionName.IsCaseInsensitiveEqual("ShippingMethod")) { - var model = _apiService.Value.ProcessPluginRequest(AmazonPayRequestType.ShippingMethod, filterContext.Controller.TempData); + var model = _apiService.Value.CreateViewModel(AmazonPayRequestType.ShippingMethod, filterContext.Controller.TempData); - if (model.Result == AmazonPayResultType.Redirect) // shipping to selected address not possible + if (model.Result == AmazonPayResultType.Redirect) { + // Shipping to selected address not possible. var urlHelper = new UrlHelper(filterContext.HttpContext.Request.RequestContext); var url = urlHelper.Action("ShippingAddress", "Checkout", new { area = "" }); @@ -54,18 +57,18 @@ public void OnActionExecuted(ActionExecutedContext filterContext) if (filterContext == null || filterContext.ActionDescriptor == null || filterContext.HttpContext == null || filterContext.HttpContext.Request == null) return; - if (!filterContext.HttpContext.HasAmazonPayState()) - return; + var actionName = filterContext.ActionDescriptor.ActionName; - string actionName = filterContext.ActionDescriptor.ActionName; + if (actionName.IsCaseInsensitiveEqual("ShippingMethod")) + return; - if (!IsInterceptableAction(actionName)) + if (!IsInterceptableAction(actionName)) return; - if (actionName.IsCaseInsensitiveEqual("ShippingMethod")) - return; + if (!filterContext.HttpContext.HasAmazonPayState()) + return; - var routeValues = new RouteValueDictionary(new { action = actionName, controller = "AmazonPayCheckout" }); + var routeValues = new RouteValueDictionary(new { action = actionName, controller = "AmazonPayCheckout" }); filterContext.Result = new RedirectToRouteResult("SmartStore.AmazonPay", routeValues); } diff --git a/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml b/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml index 53cdca3de0..bd3e9f13d8 100644 --- a/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml @@ -1,50 +1,57 @@  - Bezahlen mit Amazon + Amazon Pay - Bezahlen mit Amazon + Amazon Pay - Registrieren Sie sich zunächst bei Amazon Payments.

-

So richten Sie "Bezahlen mit Amazon" ein:

    -
  • Tragen Sie Ihre Amazon-Zugangsdaten unten in die dafür vorgesehenen Felder ein. Sie finden diese Daten in Ihrem Amazon Seller Central Konto.
  • -
  • Ihre Händlernummer finden Sie dort rechts oben unter Einstellungen > Integrationseinstellungen.
  • -
  • Die beiden Zugangsschlüssel finden Sie dort links oben unter Integration > MWS Access Key. In diesem Dokument finden Sie Bilder, wie Sie diese erstellen.
  • + So richten Sie Login und Bezahlen mit Amazon ein:
      +
    • Tragen Sie Ihre Amazon-Zugangsdaten unten in die dafür vorgesehenen Felder ein. +Die Zugangsdaten finden Sie in Ihrem Amazon Seller Central Konto oben links unter Integration > MWS Access Key.
    • Falls Sie Sofortbenachrichtigungen (IPN) erhalten möchten (SSL zwingend erforderlich!), so tragen Sie die unten aufgeführte IPN URL unter Einstellungen > Integrationseinstellungen > Sofortbenachrichtigungs-Einstellungen > Händler-URL ein.
    -

    Bitte fügen Sie Informationen zu "Bezahlen mit Amazon" auf Ihrer Seite der Zahlungsarten ein (siehe CMS > Seiten). Bildmaterial finden Sie hier. -Textvorschläge:

      -
    • Option 1: Bezahlen mit Amazon: Zahlen Sie jetzt mit den Zahl- und Lieferinformationen aus Ihrem Amazon-Konto.
    • +
      Bitte fügen Sie Informationen zu Login und Bezahlen mit Amazon auf Ihrer Seite der Zahlungsarten ein (siehe CMS > Seiten). Bildmaterial finden Sie hier. +Textvorschläge:
        +
      • Option 1: Amazon Pay: Zahlen Sie jetzt mit den Zahl- und Lieferinformationen aus Ihrem Amazon-Konto.
      • Option 2: Sie sind Amazon-Kunde? Zahlen Sie jetzt mit den Zahl- und Lieferinformationen aus Ihrem Amazon-Konto.
      • Option 3: Sie sind Amazon-Kunde? Zahlen Sie jetzt mit den Daten aus Ihrem Amazon-Konto.
      • -

      ]]> +
    ]]> - + Bitte beachten Sie, dass es sich bei dieser Rechnungsadresse unter Umständen nicht um die für diese Bestellung gültige handelt! - Es wurde keine Auftrags-Referenz-ID durch Amazon übermittelt! + Es wurde keine Auftrags-Referenz-ID durch Amazon übermittelt. + + Es wurde keine Access-Token durch Amazon übermittelt. + + + Unvollständige Amazon Profildaten! + - Die Zahlungsart "Bezahlen mit Amazon" ist für Shop "{0}" nicht verfügbar. + Die Zahlungsart "Login und Bezahlen mit Amazon" ist für Shop "{0}" nicht verfügbar. - Fehlender Checkout-Sitzungsstatus für "Bezahlen mit Amazon". Ihre Zahlung kann leider nicht bearbeitet werden. Bitte gehen Sie zurück in den Warenkorb und Durchlaufen Sie den Checkout erneut. + Fehlender Checkout-Sitzungsstatus für "Login und Bezahlen mit Amazon". Ihre Zahlung kann leider nicht bearbeitet werden. Bitte gehen Sie zurück in den Warenkorb und Durchlaufen Sie den Checkout erneut. - Ein Auftrag mit der Zahlungsart "Bezahlen mit Amazon" und der Kennung {0} wurde nicht gefunden. + Ein Auftrag mit der Zahlungsart "Login und Bezahlen mit Amazon" und der Kennung '{0}' wurde nicht gefunden. + + + Leider wurde die Zahlung zu Ihrer Bestellung in unserem Onlineshop von Amazon Pay zurückgewiesen. Bitte wählen Sie eine andere Zahlart. - Leider wurde die Zahlung zu Ihrer Bestellung in unserem Onlineshop von Amazon Payments zurückgewiesen. Bitte kontaktieren Sie uns.

    ]]> + Leider wurde die Zahlung zu Ihrer Bestellung in unserem Onlineshop von Amazon Pay zurückgewiesen. Bitte kontaktieren Sie uns.

    ]]>
    - Amazon Payments meldet + Amazon Pay meldet Ein Versand in das ausgewählte Land ist leider nicht möglich. Bitte wählen Sie eine andere Versandadresse. @@ -52,21 +59,69 @@ Textvorschläge:
      Die Einstellungen wurden erfolgreich gespeichert. Starten Sie die Anwendung bitte neu, falls "Aktualisierung des Zahlungsstatus" geändert wurde. - - Zugangsdaten;Datenaustausch;Gestaltung;Sonstiges + + Amazon Authentifizierung ist nicht aktiv! + + + Ihre Zahlung mit Amazon Pay ist derzeit noch in Prüfung. Bitte beachten Sie, dass wir uns mit Ihnen in Kürze per E-Mail in Verbindung setzen werden, falls noch Unklarheiten bestehen sollten. + + + Jetzt registrieren + + + + Jetzt registrieren, falls Sie noch über keine Zugangsdaten verfügen. Sie erhalten durch Amazon Pay einen Satz neuer Zugangsdaten, die Sie bitte im Dialog Zugangsdaten einfügen eintragen.]]> + - - {0};Die Rechnungsadresse wurde nicht von Amazon übernommen, da sie entweder fehlt oder eine Rechnungslegung in dieses Land deaktiviert ist.{0}]]> + + Zugangsdaten speichern + + + Zugangsdaten einfügen + + + Fügen Sie hier Ihre Zugangsdaten ein (merchant_id, access_key, secret_key, client_id etc.)... + + + Der Access-Token fehlt! + + + Amazon Datenabruf + + + Die Zugangsdaten wurden erfolgreich gespeichert. + + + Der Payload Parameter fehlt. + + + Eine Verschlüsselung von Zugangsdaten wird nicnt unterstützt. + + + Die Amazon Rechnungsanschrift des Kunden fehlt. + + + Daten von Amazon wurden verarbeitet - + + Zugangsdaten;Datenaustausch;Layout;Verschiedenes + + Mitteilungstyp;Mitteilungs ID;Autorisierungs ID;Buchungs ID;Rückerstatattungs ID;Referenz ID;Status;Statusaktualisierung;Gebühr;Autorisierungsbetrag;Buchungsbetrag;Erstattungsbetrag;Sofort buchen;Erstellt am;Verfällt am + Sandbox benutzen Legt fest, ob die Sandbox (Testumgebung) genutzt werden soll. + + Zahlungs-Button nur für Administratoren sichtbar + + + Legt fest, ob der Zahlungs-Button nur für Administratoren sichtbar ist. Dient dem Testen ohne Kunden die Zahlart bereits anbieten zu müssen. + Ihre Händlernummer @@ -85,53 +140,109 @@ Textvorschläge:
        Großbritannien + + USA + + + Japan + - Ihr Zugangsschlüssel + Ihre Access Key ID - Den Zugangsschlüssel finden Sie bei Amazon's Seller Central unter Integration - MWS Access Key. + Die Access Key ID finden Sie bei Amazon's Seller Central unter Integration - MWS Access Key. - Ihr geheimer Schlüssel + Ihr Secret Access Key - Den geheimen Schlüssel finden Sie bei Amazon's Seller Central unter Integration - MWS Access Key. + Den Secret Access Key finden Sie bei Amazon's Seller Central unter Integration - MWS Access Key. + + + Ihre Client-ID - - Farbe des Amazon-Login-Button + + Die Client-ID finden Sie bei Amazon's Seller Central unter Integration - MWS Access Key. - - Die bevorzugte Farbe des Amazon-Login-Button im Warenkorb. + + Zulässige JavaScript-Ursprünge - + + Bitte geben Sie diese Ursprünge bei Amazon Seller Central unter Login mit Amazon ein. Die Ursprünge müssen das HTTPS-Protokoll verwenden. + + + Zulässige Rückleitungs-URLs + + + Bitte geben Sie diese URLs bei Amazon Seller Central unter Login mit Amazon ein. Das Protokoll der Rückleitungs-URL muss HTTPS sein. + + + Farbe des Zahlungs-Button + + + Legt die bevorzugte Farbe des Zahlungs-Button (Pay-Button) fest. + + Orange - + Hellgrau - - Größe des Amazon-Login-Buttons + + Dunkelgrau + + + Größe des Zahlungs-Button - - Die Größe "Bezahlen mit Amazon" Buttons auf der Warenkorbseite. + + Legt die Größe Zahlungs-Button (Pay-Button) fest. - - Medium (126 x 24 Pixel) + + Klein + + + Medium - - Groß (151 x 27 Pixel) + + Groß - - Extra-Groß (173 x 27 Pixel) + + Extra-Groß + + Art des Anmelde-Button + + + Legt die bevorzugte Art des Anmelde-Button (Loogin-Button) fest. + + + Login + + + Login mit Amazon + + + Farbe des Anmelde-Button + + + Legt die bevorzugte Farbe des Anmelde-Button (Login-Button) fest. + + + Größe des Anmelde-Button + + + Legt die Größe Anmelde-Button (Login-Button) fest. + Aktualisierung des Zahlungsstatus - Legt die Methode fest mit deren Hilfe der Zahlungsstatus aktualisiert werden soll. + Legt die Methode fest, mit deren Hilfe der Zahlungsstatus aktualisiert werden soll. - IPN (Sofortbenachrichtigungen) erfordert, dass ein gültiges SSL-Zertifikat auf diesem Server installiert ist. Achten Sie darauf, das das SSL-Zertifikat von einer vertrauenswürdigen Zertifizierungsstelle ausgegeben werden muss, selbstsignierte Zertifikate sind nicht zulässig. + + + IPN (Instant Payment Notification) @@ -143,7 +254,7 @@ Textvorschläge:
          IPN URL - Bitte geben Sie diese URL bei Amazon Seller Central unter Integrationseinstellungen - Sofortbenachrichtigungs-Einstellungen - Händler-URL ein. + Bitte geben Sie diese URL bei Amazon Seller Central unter Integrationseinstellungen - Sofortbenachrichtigungs-Einstellungen - Händler-URL ein. Zahlungsaktion @@ -158,7 +269,24 @@ Textvorschläge:
            Autorisierung sofort, Abbuchung später - Bitte benutzen Sie "Sofort abbuchen" nur, wenn Sie Ware am selben Tag der Bestellung verschicken und Sie für diesen Dienst zugelassen sind. Aktivieren Sie diese Option bitte erst nach Rücksprache mit Amazon Payments. + + Sofort abbuchen nur, wenn Sie Ware am selben Tag der Bestellung verschicken und Sie für diesen Dienst zugelassen sind. Aktivieren Sie diese Option bitte erst nach Rücksprache mit Amazon Pay.]]> + + + + Autorisierungsmethode + + + Legt die Methode fest, mit der Zahlungen autorisiert werden sollen. + + + Standard (Omnichron) + + + Asynchron + + + Synchron Kundendaten übernehmen @@ -176,61 +304,16 @@ Textvorschläge:
              Button im Miniwarenkorb anzeigen - Legt fest, ob der "Bezahlen mit Amazon" Button auch im Miniwarenkorb angezeigt werden soll. - - - Breite des Adressen-Widgets - - - In Pixel. Gültige Werte für einspaltige Widgets sind 200 bis 399 und für zweispaltige Widgets 400 bis 600. - - - Höhe des Adressen-Widgets - - - In Pixel. Gültige Werte sind 228 bis 400. - - - Breite des Zahlungs-Widgets - - - In Pixel. Gültige Werte für einspaltige Widgets sind 200 bis 399 und für zweispaltige Widgets 400 bis 600. - - - Höhe des Zahlungs-Widgets - - - In Pixel. Gültige Werte sind 228 bis 400. - - - Zusätzliche Gebühren - - - Zusätzliche Gebühren, die dem Kunden für die Inanspruchnahme des Dienstes berechnet werden sollen. - - - Bitte berechnen Sie zusätzliche Gebühren erst nach Rücksprache mit Amazon Payments. Hierfür ist eine ausdrückliche Genehmigung erforderlich. - - - Zusätzliche Gebühren (prozentual) - - - Zusätzliche prozentuale Gebühr zum Gesamtbetrag. Es wird ein fester Wert verwendet, falls diese Option nicht aktiviert ist. + Legt fest, ob der Amazon Pay Button auch im Miniwarenkorb angezeigt werden soll. Auftragsnotizen anlegen - Legt fest, dass Auftragsnotizen hinsichtlich des Datenaustausches mit Amazon Payment angelegt werden sollen. - - - Zeitspanne (in Minuten) - - - Legt fest, wie oft Zahlungsdaten vom Amazon Payments Server abgerufen werden sollen. + Legt fest, dass Auftragsnotizen hinsichtlich des Datenaustausches mit Amazon Pay angelegt werden sollen. - Maximales Auftragsalter (in Tagen) + Maximales Auftragsalter Legt fest, dass nur Aufträge, die nicht älter als x Tage sind, in die Aktualisierung der Zahlungsdaten einbezogen werden sollen. @@ -239,12 +322,12 @@ Textvorschläge:
                Über Ablehnung einer Autorisierung informieren - Legt fest, dass Auftragsnotizen im Fall einer Ablehnung einer Autorisierung durch Amazon angelegt werden, die auch für den Kunden einsehbar sind. Zusätzlich wir der Kunde per Email über den Sachverhalt informiert. + Legt fest, dass Auftragsnotizen im Fall einer Ablehnung einer Autorisierung durch Amazon angelegt werden, die auch für den Kunden einsehbar sind. Zusätzlich wird der Kunde per E-Mail über den Sachverhalt informiert. Fehlermeldung anhängen - Legt fest, ob der genaue Wortlaut der Fehlermeldung der Auftragsnotiz bzw. Email angehängt werden soll. + Legt fest, ob der genaue Wortlaut der Fehlermeldung der Auftragsnotiz bzw. E-Mail angehängt werden soll. \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Localization/resources.en-us.xml b/src/Plugins/SmartStore.AmazonPay/Localization/resources.en-us.xml index 1d61c1b878..50fe5bf7a1 100644 --- a/src/Plugins/SmartStore.AmazonPay/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.AmazonPay/Localization/resources.en-us.xml @@ -1,50 +1,57 @@  - Pay with Amazon + Amazon Pay - Pay with Amazon + Amazon Pay - Register now at Amazon Payments.

                -

                How to set up "Pay with Amazon":

                  -
                • Enter your Amazon credentials in the fields provided below. You can find these credentials in your Amazon Seller Central account.
                • -
                • You can find the Merchant ID in Seller Central at Settings > Integration Settings.
                • -
                • You can find both access keys in Seller Central at Integration > MWS Access Key.
                • + How to set up Login and Pay with Amazon:
                    +
                  • Enter your Amazon credentials in the fields provided below. +You can find these credentials in your Amazon Seller Central account at Integration > MWS Access Key.
                  • If you would like to receive instant payment notifications (SSL required!) enter the IPN URL listed bewlow under Settings > Integration Settings > Instant Notification Settings > Merchant URL.
                  -

                  Please add information about "Pay with Amazon" on your payment page (see CMS > Topics). You will find picture material here. -Text suggestions:

                    -
                  • Option 1: Pay with Amazon: Pay now with the payment and shipping information from your Amazon account.
                  • +
                    Please add information about Login and Pay with Amazon on your payment page (see CMS > Topics). You will find picture material here. +Text suggestions:
                      +
                    • Option 1: Amazon Pay: Pay now with the payment and shipping information from your Amazon account.
                    • Option 2: Already Amazon customer? Pay now with the payment and shipping information from your Amazon account.
                    • Option 3: Already Amazon customer? Pay now with the data from your Amazon account.
                    • -

                    ]]> +
                  ]]> Please note that this billing address is possibly not the valid billing address for this order! - There was no order reference ID transmitted by Amazon! + There was no order reference ID transmitted by Amazon. - - Payment method "Pay with Amazon" is not available for store "{0}". + + There was no access token transmitted by Amazon. + + + Incomplete Amazon profile data! + + + Payment method "Login and Pay with Amazon" is not available for store "{0}". - Missing checkout session state for "Pay with Amazon". Your payment cannot be processed. Please go to your shopping cart and checkout again. + Missing checkout session state for "Login and Pay with Amazon". Your payment cannot be processed. Please go to your shopping cart and checkout again. - Cannot find an order with the payment method "Pay with Amazon" and the identification {0}. + Cannot find an order with the payment method "Login and Pay with Amazon" and the identification '{0}'. + + + Unfortunately Amazon Pay declined the payment for your order in our online shop. Please choose another payment method. - Unfortunately Amazon Payments declined the payment for your order in our online shop. Please contact us.

                  ]]> + Unfortunately Amazon Pay declined the payment for your order in our online shop. Please contact us.

                  ]]>
                  - Amazon Payments reports + Amazon Pay reports Shipping to the selected country is not allowed. Please choose an other shipping address. @@ -52,21 +59,69 @@ Text suggestions:
                    The settings were successfully saved. Please restart the application if "Updating the payment status" has been changed. - - Access data;Data exchange;Layout;Miscellaneous - - - {0}The billing address from Amazon was not applied because it is missing or billing to that country is deactivated.{0};]]> + + Amazon authentication is not active! + + + Your transaction with Amazon Pay is currently being validated. Please be aware that we will inform you shortly as needed. + + + Register now + + + + Register now if you don't have an account yet. Amazon Pay will provide you with a set of new access data, which you can enter in the Paste access data dialog.]]> + - + + Save access data + + + Paste access data + + + Paste here your access data (merchant_id, access_key, secret_key, client_id etc.)... + + + Missing access token! + + + Amazon data polling + + + The access data has been saved successfully. + + + The payload parameter is missing. + + + Encryption of access data is not supported. + + + Missing Amazon billing address of the customer. + + + Data from Amazon has been processed + + + Access data;Data exchange;Layout;Miscellaneous + + Message type;Message ID;Authorization ID;Capture ID;Refund ID;Reference ID;State;State update;Fee;Authorized amount;Captured amount;Refunded amount;Capture now;Creation;Expiration + Use Sandbox Check the box to use the sandbox (testing environment). + + Show pay button for administrators only + + + Specifies whether the pay button is visible to administrators only. Intended for testing without having to offer the payment method to customers. + Your Merchant ID @@ -85,45 +140,99 @@ Text suggestions:
                      United Kingdom of Great Britain + + USA + + + Japan + - Your access key + Your Access Key ID - You can find the access key at Amazon's Seller Central under Integration - MWS Access Key. + You can find the Access Key ID at Amazon's Seller Central under Integration - MWS Access Key. - Your secret key + Your Secret Access Key - You can find the secret key at Amazon's Seller Central under Integration - MWS Access Key. + You can find the Secret Access Key at Amazon's Seller Central under Integration - MWS Access Key. - - Amazon login button color + + Your Client ID - - The prefered color of the Amazon login button on shopping cart page. + + You can find the Client ID at Amazon's Seller Central under Integration - MWS Access Key. - + + Allowed JavaScript origins + + + Please enter these origins at Amazon Seller Central under Login with Amazon. The origins must use the HTTPS protocol. + + + Allowed redirect URLs + + + Please enter these URLs at Amazon Seller Central under Login with Amazon. The redirect URL protocol must be HTTPS. + + + Pay button color + + + Specifies the prefered color of the Pay button. + + Orange - + Light gray - - Size of Amazon login button + + Dark gray + + + Pay button size - - The size of the "Pay with Amazon" button on the shopping cart page. + + Specifies the size of the Pay button. - - Medium (126 x 24 pixel) + + Small + + + Medium - - Large (151 x 27 pixel) + + Large - - Extra large (173 x 27 ixel) + + Extra large + + Login button type + + + Specifies the prefered type of the Login button. + + + Login + + + Login with Amazon + + + Login button color + + + Specifies the prefered color of the Login button. + + + Login button size + + + Specifies the size of the Login button. + Updating the payment status @@ -131,7 +240,9 @@ Text suggestions:
                        Specifies the method used to update the payment status. - IPN (instant payment notification) requires valid SSL certificate to be installed on this server. Pay attention that the SSL certificate must be issued by a trusted Certificate Authority, self-signed certificates are not permittted. + + + IPN (Instant Payment Notification) @@ -158,7 +269,24 @@ Text suggestions:
                          Authorize immediately, debit later - Please use "Immediately debit" method only in the case you are shipping goods on the same day they are ordered and you have been white-listed for this service. Do not activate this option without contacting Amazon Payments first. + + Immediately debit method only in the case you are shipping goods on the same day they are ordered and you have been white-listed for this service. Do not activate this option without contacting Amazon Pay first.]]> + + + + Authorize method + + + Specifies the method by which payments are to be authorized. + + + Standard (Omnichronous) + + + Asynchronous + + + Synchronous Apply customer data @@ -176,61 +304,16 @@ Text suggestions:
                            Show button in mini shopping cart - Specifies to show the "Pay with Amazon" button in the mini shopping cart too. - - - Width of address widget - - - Valid values for one-column widgets are 200 to 399 pixel and for two-column widgets 400 to 600 pixel. - - - Height of address widget - - - Valid values are 228 to 400 pixel. - - - Width of payment widget - - - Valid values for one-column widgets are 200 to 399 pixel and for two-column widgets 400 to 600 pixel. - - - Height of payment widget - - - Valid values are 228 to 400 pixel. - - - Additional fee - - - Enter additional fee to charge your customers. - - - This option requires a permission through Amazon Payments. Please do not calculate additional fees without contacting Amazon Payments first. - - - Additional fee percentage - - - Specifies whether to apply a percentage additional fee to the order total. If not enabled, a fixed value is used. + Specifies to show the Amazon Pay button in the mini shopping cart too. Create order notes - Specifies that order notes should be created in context of the data exchange with Amazon Payments. - - - Frequency (in minutes) - - - Specifies how often status of the different object shall be polled from Amazon Payments servers. + Specifies that order notes should be created in context of the data exchange with Amazon Pay. - Maximal order age (in days) + Maximal order age Specifies that only orders which are not older than x days to be included in payment data updates. @@ -239,7 +322,7 @@ Text suggestions:
                              Inform about a refusal of an authorization - Specifies to create order notes in case of a declination of an Amazon payment, which are visible for customers too. In addition the customer is informed by email about the case. + Specifies to create order notes in case of a declination of an Amazon Pay, which are visible for customers too. In addition the customer is informed by email about the case. Append error message diff --git a/src/Plugins/SmartStore.AmazonPay/Models/AmazonPayViewModel.cs b/src/Plugins/SmartStore.AmazonPay/Models/AmazonPayViewModel.cs index 066e9dd9db..d56a233de9 100644 --- a/src/Plugins/SmartStore.AmazonPay/Models/AmazonPayViewModel.cs +++ b/src/Plugins/SmartStore.AmazonPay/Models/AmazonPayViewModel.cs @@ -1,6 +1,6 @@ -using System; -using SmartStore.AmazonPay.Services; +using SmartStore.AmazonPay.Services; using SmartStore.Web.Framework.Modelling; +using SmartStore.Web.Models.Common; namespace SmartStore.AmazonPay.Models { @@ -12,48 +12,43 @@ public AmazonPayViewModel() RedirectAction = "Cart"; RedirectController = "ShoppingCart"; Result = AmazonPayResultType.PluginView; - WidgetUrl = AmazonPayCore.UrlWidgetProduction.FormatWith("de"); + BillingAddress = new AddressModel(); } - public string SystemName { get { return AmazonPayCore.SystemName; } } + public string SystemName + { + get { return AmazonPayPlugin.SystemName; } + } public string SellerId { get; set; } public string ClientId { get; set; } + /// + /// Amazon widget script URL + /// public string WidgetUrl { get; set; } - public string ButtonUrl { get; set; } - public string LoginHandlerUrl { get; set; } + public string ButtonHandlerUrl { get; set; } public bool IsShippable { get; set; } public bool IsRecurring { get; set; } + public string LanguageCode { get; set; } public AmazonPayRequestType Type { get; set; } public AmazonPayResultType Result { get; set; } + public string RedirectAction { get; set; } public string RedirectController { get; set; } public string OrderReferenceId { get; set; } + public string AddressConsentToken { get; set; } public string Warning { get; set; } + public bool Logout { get; set; } - public int AddressWidgetWidth { get; set; } - public int AddressWidgetHeight { get; set; } - - public int PaymentWidgetWidth { get; set; } - public int PaymentWidgetHeight { get; set; } - - public bool DisplayRewardPoints { get; set; } - public int RewardPointsBalance { get; set; } - public string RewardPointsAmount { get; set; } - public bool UseRewardPoints { get; set; } + public string ButtonType { get; set; } + public string ButtonColor { get; set; } + public string ButtonSize { get; set; } public string ShippingMethod { get; set; } - - public string GetWidgetId - { - get - { - return "AmazonPay" + Type.ToString(); - } - } - } + public AddressModel BillingAddress { get; set; } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Models/ConfigurationModel.cs b/src/Plugins/SmartStore.AmazonPay/Models/ConfigurationModel.cs index aa40e32f0f..cf304df151 100644 --- a/src/Plugins/SmartStore.AmazonPay/Models/ConfigurationModel.cs +++ b/src/Plugins/SmartStore.AmazonPay/Models/ConfigurationModel.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Web.Mvc; using SmartStore.AmazonPay.Services; -using SmartStore.AmazonPay.Settings; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Modelling; @@ -10,6 +10,7 @@ namespace SmartStore.AmazonPay.Models public class ConfigurationModel : ModelBase { public string[] ConfigGroups { get; set; } + public string PrimaryStoreCurrencyCode { get; set; } [SmartResourceDisplayName("Plugins.Payments.AmazonPay.UseSandbox")] public bool UseSandbox { get; set; } @@ -21,17 +22,15 @@ public class ConfigurationModel : ModelBase public string AccessKey { get; set; } [SmartResourceDisplayName("Plugins.Payments.AmazonPay.SecretKey")] + [DataType(DataType.Password)] public string SecretKey { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.ClientId")] + public string ClientId { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.Marketplace")] public string Marketplace { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AmazonButtonColor")] - public string AmazonButtonColor { get; set; } - - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AmazonButtonSize")] - public string AmazonButtonSize { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.DataFetching")] public AmazonPayDataFetchingType DataFetching { get; set; } public List DataFetchings { get; set; } @@ -39,9 +38,6 @@ public class ConfigurationModel : ModelBase [SmartResourceDisplayName("Plugins.Payments.AmazonPay.IpnUrl")] public string IpnUrl { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.PollingTaskMinutes")] - public int PollingTaskMinutes { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.PollingMaxOrderCreationDays")] public int PollingMaxOrderCreationDays { get; set; } @@ -49,27 +45,24 @@ public class ConfigurationModel : ModelBase public AmazonPayTransactionType TransactionType { get; set; } public List TransactionTypes { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AuthorizeMethod")] + public AmazonPayAuthorizeMethod AuthorizeMethod { get; set; } + public SelectList AuthorizeMethods { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.SaveEmailAndPhone")] public AmazonPaySaveDataType? SaveEmailAndPhone { get; set; } public List SaveEmailAndPhones { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.ShowPayButtonForAdminOnly")] + public bool ShowPayButtonForAdminOnly { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.ShowButtonInMiniShoppingCart")] public bool ShowButtonInMiniShoppingCart { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AddressWidgetWidth")] - public int AddressWidgetWidth { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AddressWidgetHeight")] - public int AddressWidgetHeight { get; set; } - - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.PaymentWidgetWidth")] - public int PaymentWidgetWidth { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.PaymentWidgetHeight")] - public int PaymentWidgetHeight { get; set; } - - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AdditionalFee")] + [SmartResourceDisplayName("Admin.Configuration.Payment.Methods.AdditionalFee")] public decimal AdditionalFee { get; set; } - [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AdditionalFeePercentage")] + [SmartResourceDisplayName("Admin.Configuration.Payment.Methods.AdditionalFeePercentage")] public bool AdditionalFeePercentage { get; set; } [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AddOrderNotes")] @@ -81,56 +74,55 @@ public class ConfigurationModel : ModelBase [SmartResourceDisplayName("Plugins.Payments.AmazonPay.InformCustomerAddErrors")] public bool InformCustomerAddErrors { get; set; } - public void Copy(AmazonPaySettings settings, bool fromSettings) - { - if (fromSettings) - { - UseSandbox = settings.UseSandbox; - SellerId = settings.SellerId; - AccessKey = settings.AccessKey; - SecretKey = settings.SecretKey; - Marketplace = settings.Marketplace; - DataFetching = settings.DataFetching; - PollingMaxOrderCreationDays = settings.PollingMaxOrderCreationDays; - TransactionType = settings.TransactionType; - SaveEmailAndPhone = settings.SaveEmailAndPhone; - ShowButtonInMiniShoppingCart = settings.ShowButtonInMiniShoppingCart; - AmazonButtonColor = settings.AmazonButtonColor; - AmazonButtonSize = settings.AmazonButtonSize; - AddressWidgetWidth = settings.AddressWidgetWidth; - AddressWidgetHeight = settings.AddressWidgetHeight; - PaymentWidgetWidth = settings.PaymentWidgetWidth; - PaymentWidgetHeight = settings.PaymentWidgetHeight; - AdditionalFee = settings.AdditionalFee; - AdditionalFeePercentage = settings.AdditionalFeePercentage; - AddOrderNotes = settings.AddOrderNotes; - InformCustomerAboutErrors = settings.InformCustomerAboutErrors; - InformCustomerAddErrors = settings.InformCustomerAddErrors; - } - else - { - settings.UseSandbox = UseSandbox; - settings.SellerId = SellerId; - settings.AccessKey = AccessKey; - settings.SecretKey = SecretKey; - settings.Marketplace = Marketplace; - settings.DataFetching = DataFetching; - settings.PollingMaxOrderCreationDays = PollingMaxOrderCreationDays; - settings.TransactionType = TransactionType; - settings.SaveEmailAndPhone = SaveEmailAndPhone; - settings.ShowButtonInMiniShoppingCart = ShowButtonInMiniShoppingCart; - settings.AmazonButtonColor = AmazonButtonColor; - settings.AmazonButtonSize = AmazonButtonSize; - settings.AddressWidgetWidth = AddressWidgetWidth; - settings.AddressWidgetHeight = AddressWidgetHeight; - settings.PaymentWidgetWidth = PaymentWidgetWidth; - settings.PaymentWidgetHeight = PaymentWidgetHeight; - settings.AdditionalFee = AdditionalFee; - settings.AdditionalFeePercentage = AdditionalFeePercentage; - settings.AddOrderNotes = AddOrderNotes; - settings.InformCustomerAboutErrors = InformCustomerAboutErrors; - settings.InformCustomerAddErrors = InformCustomerAddErrors; - } - } + + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.PayButtonColor")] + public string PayButtonColor { get; set; } + + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.PayButtonSize")] + public string PayButtonSize { get; set; } + + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AuthButtonType")] + public string AuthButtonType { get; set; } + + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AuthButtonColor")] + public string AuthButtonColor { get; set; } + + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.AuthButtonSize")] + public string AuthButtonSize { get; set; } + + #region Registration data + + public string RegisterUrl { get; set; } + public string SoftwareVersion { get; set; } + public string PluginVersion { get; set; } + public string LeadCode { get; set; } + public string PlatformId { get; set; } + public string PublicKey { get; set; } + public string KeyShareUrl { get; set; } + public string LanguageLocale { get; set; } + + /// + /// Including all domains and sub domains where the login button appears. SSL mandatory. + /// + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.MerchantLoginDomains")] + public HashSet MerchantLoginDomains { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.MerchantLoginDomains")] + public HashSet CurrentMerchantLoginDomains { get; set; } + + /// + /// Used to populate Allowed Return URLs on the Login with Amazon application. SSL mandatory. Max 512 characters. + /// + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.MerchantLoginRedirectUrls")] + public HashSet MerchantLoginRedirectUrls { get; set; } + [SmartResourceDisplayName("Plugins.Payments.AmazonPay.MerchantLoginRedirectUrls")] + public HashSet CurrentMerchantLoginRedirectUrls { get; set; } + + public string MerchantStoreDescription { get; set; } + public string MerchantPrivacyNoticeUrl { get; set; } + public string MerchantCountry { get; set; } + public string MerchantSandboxIpnUrl { get; set; } + public string MerchantProductionIpnUrl { get; set; } + + #endregion } } diff --git a/src/Plugins/SmartStore.AmazonPay/Plugin.cs b/src/Plugins/SmartStore.AmazonPay/Plugin.cs index 61a133c505..9006d725cf 100644 --- a/src/Plugins/SmartStore.AmazonPay/Plugin.cs +++ b/src/Plugins/SmartStore.AmazonPay/Plugin.cs @@ -3,50 +3,69 @@ using System.Web.Routing; using SmartStore.AmazonPay.Controllers; using SmartStore.AmazonPay.Services; -using SmartStore.AmazonPay.Settings; using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Logging; using SmartStore.Core.Plugins; using SmartStore.Services; +using SmartStore.Services.Authentication.External; +using SmartStore.Services.Customers; using SmartStore.Services.Orders; using SmartStore.Services.Payments; +using SmartStore.Services.Tasks; namespace SmartStore.AmazonPay { [DependentWidgets("Widgets.AmazonPay")] - public class Plugin : PaymentPluginBase, IConfigurable + [FriendlyName("Amazon Pay")] + [DisplayOrder(-1)] + public class AmazonPayPlugin : PaymentPluginBase, IExternalAuthenticationMethod, IConfigurable { private readonly IAmazonPayService _apiService; - private readonly IOrderTotalCalculationService _orderTotalCalculationService; private readonly ICommonServices _services; + private readonly IOrderTotalCalculationService _orderTotalCalculationService; + private readonly IScheduleTaskService _scheduleTaskService; - public Plugin( + public AmazonPayPlugin( IAmazonPayService apiService, + ICommonServices services, IOrderTotalCalculationService orderTotalCalculationService, - ICommonServices services) + IScheduleTaskService scheduleTaskService) { _apiService = apiService; - _orderTotalCalculationService = orderTotalCalculationService; _services = services; + _orderTotalCalculationService = orderTotalCalculationService; + _scheduleTaskService = scheduleTaskService; + + Logger = NullLogger.Instance; } - public override void Install() + public ILogger Logger { get; set; } + + public static string SystemName { - _services.Settings.SaveSetting(new AmazonPaySettings()); + get { return "SmartStore.AmazonPay"; } + } - _services.Localization.ImportPluginResourcesFromXml(this.PluginDescriptor); + public override void Install() + { + _services.Settings.SaveSetting(new AmazonPaySettings()); + _services.Localization.ImportPluginResourcesFromXml(PluginDescriptor); - _apiService.DataPollingTaskInit(); + // Polling task every 30 minutes. + _scheduleTaskService.GetOrAddTask(x => + { + x.Name = _services.Localization.GetResource("Plugins.Payments.AmazonPay.TaskName"); + x.CronExpression = "*/30 * * * *"; + }); base.Install(); } public override void Uninstall() { - _apiService.DataPollingTaskDelete(); - + _scheduleTaskService.TryDeleteTask(); _services.Settings.DeleteSetting(); - - _services.Localization.DeleteLocaleStringResources(this.PluginDescriptor.ResourceRootKey); + _services.Localization.DeleteLocaleStringResources(PluginDescriptor.ResourceRootKey); base.Uninstall(); } @@ -71,18 +90,19 @@ public override void PostProcessPayment(PostProcessPaymentRequest postProcessPay public override decimal GetAdditionalHandlingFee(IList cart) { var result = decimal.Zero; + try { var settings = _services.Settings.LoadSetting(_services.StoreContext.CurrentStore.Id); result = this.CalculateAdditionalFee(_orderTotalCalculationService, cart, settings.AdditionalFee, settings.AdditionalFeePercentage); } - catch (Exception exc) + catch (Exception exception) { - _apiService.LogError(exc); + Logger.Error(exception); } - return result; + return result; } public override CapturePaymentResult Capture(CapturePaymentRequest capturePaymentRequest) @@ -108,14 +128,36 @@ public override void GetConfigurationRoute(out string actionName, out string con { actionName = "Configure"; controllerName = "AmazonPay"; - routeValues = new RouteValueDictionary() { { "Namespaces", "SmartStore.AmazonPay.Controllers" }, { "area", AmazonPayCore.SystemName } }; + routeValues = new RouteValueDictionary { { "Namespaces", "SmartStore.AmazonPay.Controllers" }, { "area", SystemName } }; + } + + public void GetPublicInfoRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) + { + actionName = "AuthenticationPublicInfo"; + controllerName = "AmazonPay"; + routeValues = new RouteValueDictionary { { "Namespaces", "SmartStore.AmazonPay.Controllers" }, { "area", SystemName } }; } public override void GetPaymentInfoRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) { + try + { + var settings = _services.Settings.LoadSetting(_services.StoreContext.CurrentStore.Id); + if (settings.ShowPayButtonForAdminOnly && !_services.WorkContext.CurrentCustomer.IsAdmin()) + { + actionName = controllerName = null; + routeValues = null; + return; + } + } + catch (Exception exception) + { + Logger.Error(exception); + } + actionName = "ShoppingCart"; controllerName = "AmazonPayShoppingCart"; - routeValues = new RouteValueDictionary() { { "Namespaces", "SmartStore.AmazonPay.Controllers" }, { "area", AmazonPayCore.SystemName } }; + routeValues = new RouteValueDictionary { { "Namespaces", "SmartStore.AmazonPay.Controllers" }, { "area", SystemName } }; } public override Type GetControllerType() diff --git a/src/Plugins/SmartStore.AmazonPay/RouteProvider.cs b/src/Plugins/SmartStore.AmazonPay/RouteProvider.cs index a44b2d50f3..d7d33434cb 100644 --- a/src/Plugins/SmartStore.AmazonPay/RouteProvider.cs +++ b/src/Plugins/SmartStore.AmazonPay/RouteProvider.cs @@ -14,7 +14,7 @@ public void RegisterRoutes(RouteCollection routes) new { controller = "AmazonPay" }, new[] { "SmartStore.AmazonPay.Controllers" } ) - .DataTokens["area"] = AmazonPayCore.SystemName; + .DataTokens["area"] = AmazonPayPlugin.SystemName; // for backward compatibility (IPN!) routes.MapRoute("SmartStore.AmazonPay.Legacy", @@ -22,7 +22,7 @@ public void RegisterRoutes(RouteCollection routes) new { controller = "AmazonPay" }, new[] { "SmartStore.AmazonPay.Controllers" } ) - .DataTokens["area"] = AmazonPayCore.SystemName; + .DataTokens["area"] = AmazonPayPlugin.SystemName; } public int Priority { get { return 0; } } diff --git a/src/Plugins/SmartStore.AmazonPay/Scripts/smartstore.amazonpay.js b/src/Plugins/SmartStore.AmazonPay/Scripts/smartstore.amazonpay.js deleted file mode 100644 index a6859e9443..0000000000 --- a/src/Plugins/SmartStore.AmazonPay/Scripts/smartstore.amazonpay.js +++ /dev/null @@ -1,33 +0,0 @@ - -(function (amazonpay, $, undefined) { - - amazonpay.init = function () { - - // show/hide transaction type warning - $('#TransactionType').change(function () { - $('#TransactionTypeWarning').toggle($(this).val() === '2'); - }).trigger('change'); - - // show/hide status fetching warning - $('#DataFetching').change(function () { - var val = $(this).val(), - configTable = $('#AmazonPayConfigTable'); - - $('#DataFetchingWarning').toggle(val === '1'); - - configTable.find('.data-fetching').hide(); - if (val === '1') - configTable.find('.data-fetching-ipn').show(); - else if (val === '2') - configTable.find('.data-fetching-polling').show(); - - }).trigger('change'); - - // show/hide inform customer add errors option - $('#InformCustomerAboutErrors').change(function () { - $('#InformCustomerAddErrorsContainer').toggle($(this).is(':checked')); - }).trigger('change'); - - }; - -}(window.amazonpay = window.amazonpay || {}, jQuery)); \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Services/AmazonAuthenticationParameters.cs b/src/Plugins/SmartStore.AmazonPay/Services/AmazonAuthenticationParameters.cs new file mode 100644 index 0000000000..d64719b0e7 --- /dev/null +++ b/src/Plugins/SmartStore.AmazonPay/Services/AmazonAuthenticationParameters.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using SmartStore.Services.Authentication.External; + +namespace SmartStore.AmazonPay.Services +{ + [Serializable] + public class AmazonAuthenticationParameters : OpenAuthenticationParameters + { + private IList _claims; + + public override string ProviderSystemName + { + get { return AmazonPayPlugin.SystemName; } + } + + public override IList UserClaims + { + get { return _claims; } + } + + public void AddClaim(UserClaims claim) + { + if (_claims == null) + { + _claims = new List(); + } + + _claims.Add(claim); + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayApi.cs b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayApi.cs deleted file mode 100644 index 59e41a1e2b..0000000000 --- a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayApi.cs +++ /dev/null @@ -1,815 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Web; -using OffAmazonPaymentsService; -using OffAmazonPaymentsService.Model; -using SmartStore.AmazonPay.Extensions; -using SmartStore.AmazonPay.Services; -using SmartStore.AmazonPay.Settings; -using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Directory; -using SmartStore.Core.Domain.Discounts; -using SmartStore.Core.Domain.Orders; -using SmartStore.Core.Infrastructure; -using SmartStore.Core.Plugins; -using SmartStore.Services; -using SmartStore.Services.Common; -using SmartStore.Services.Directory; -using SmartStore.Services.Helpers; -using SmartStore.Services.Orders; -using SmartStore.Services.Payments; -using SmartStore.Utilities; - -namespace SmartStore.AmazonPay.Api -{ - public class AmazonPayApi : IAmazonPayApi - { - private readonly ICountryService _countryService; - private readonly IStateProvinceService _stateProvinceService; - private readonly IOrderService _orderService; - private readonly IAddressService _addressService; - private readonly IDateTimeHelper _dateTimeHelper; - private readonly CurrencySettings _currencySettings; - private readonly IOrderTotalCalculationService _orderTotalCalculationService; - private readonly ICommonServices _services; - - public AmazonPayApi( - ICountryService countryService, - IStateProvinceService stateProvinceService, - IOrderService orderService, - IAddressService addressService, - IDateTimeHelper dateTimeHelper, - CurrencySettings currencySettings, - IOrderTotalCalculationService orderTotalCalculationService, - ICommonServices services) - { - _countryService = countryService; - _stateProvinceService = stateProvinceService; - _orderService = orderService; - _addressService = addressService; - _dateTimeHelper = dateTimeHelper; - _currencySettings = currencySettings; - _orderTotalCalculationService = orderTotalCalculationService; - _services = services; - } - - private string GetRandomId(string prefix) - { - string str = prefix + CommonHelper.GenerateRandomDigitCode(20); - return str.Truncate(32); - } - - public void GetConstraints(OrderReferenceDetails details, IList warnings) - { - try - { - if (details != null && warnings != null && details.IsSetConstraints()) - { - foreach (var constraint in details.Constraints.Constraint) - { - string warning = "{0} ({1})".FormatWith(constraint.Description, constraint.ConstraintID); - warnings.Add(warning); - } - } - } - catch (Exception) { } - } - - public bool FindAndApplyAddress(OrderReferenceDetails details, Customer customer, bool isShippable, bool forceToTakeAmazonAddress) - { - // PlaceOrder requires billing address but we don't get one from Amazon here. so use shipping address instead until we get it from amazon. - bool countryAllowsShipping, countryAllowsBilling; - - var amazonAddress = new SmartStore.Core.Domain.Common.Address() - { - CreatedOnUtc = DateTime.UtcNow - }; - - details.ToAddress(amazonAddress, _countryService, _stateProvinceService, out countryAllowsShipping, out countryAllowsBilling); - - if (isShippable && !countryAllowsShipping) - return false; - - if (amazonAddress.Email.IsEmpty()) - amazonAddress.Email = customer.Email; - - if (forceToTakeAmazonAddress) - { - // first time to get in touch with an amazon address - var existingAddress = customer.Addresses.ToList().FindAddress(amazonAddress, true); - - if (existingAddress == null) - { - customer.Addresses.Add(amazonAddress); - customer.BillingAddress = amazonAddress; - } - else - { - customer.BillingAddress = existingAddress; - } - } - else - { - if (customer.BillingAddress == null) - { - customer.Addresses.Add(amazonAddress); - customer.BillingAddress = amazonAddress; - } - - // we already have the address but it is uncomplete, so just complete it - details.ToAddress(customer.BillingAddress, _countryService, _stateProvinceService, out countryAllowsShipping, out countryAllowsBilling); - - // but now we could have dublicates - int newAddressId = customer.BillingAddress.Id; - var addresses = customer.Addresses.Where(x => x.Id != newAddressId).ToList(); - - var existingAddress = addresses.FindAddress(customer.BillingAddress, false); - - if (existingAddress != null) - { - // remove the new and take the old one - customer.RemoveAddress(customer.BillingAddress); - customer.BillingAddress = existingAddress; - - try - { - _addressService.DeleteAddress(newAddressId); - } - catch (Exception exc) - { - exc.Dump(); - } - } - } - - customer.ShippingAddress = (isShippable ? customer.BillingAddress : null); - - return true; - } - public bool FulfillBillingAddress(AmazonPaySettings settings, Order order, AuthorizationDetails details, out string formattedAddress) - { - formattedAddress = ""; - - if (details == null) - { - AmazonPayApiData data; - details = GetAuthorizationDetails(new AmazonPayClient(settings), order.AuthorizationTransactionId, out data); - } - - if (details == null || !details.IsSetAuthorizationBillingAddress()) - return false; - - bool countryAllowsShipping, countryAllowsBilling; - - // override billing address cause it is just copy of the shipping address - details.AuthorizationBillingAddress.ToAddress(order.BillingAddress, _countryService, _stateProvinceService, out countryAllowsShipping, out countryAllowsBilling); - - if (!countryAllowsBilling) - { - formattedAddress = details.AuthorizationBillingAddress.ToFormatedAddress(_countryService, _stateProvinceService); - return false; - } - - order.BillingAddress.CreatedOnUtc = DateTime.UtcNow; - - if (order.BillingAddress.Email.IsEmpty()) - order.BillingAddress.Email = order.Customer.Email; - - _orderService.UpdateOrder(order); - - formattedAddress = details.AuthorizationBillingAddress.ToFormatedAddress(_countryService, _stateProvinceService); - - return true; - } - - public OrderReferenceDetails GetOrderReferenceDetails(AmazonPayClient client, string orderReferenceId, string addressConsentToken = null) - { - var request = new GetOrderReferenceDetailsRequest(); - request.SellerId = client.Settings.SellerId; - request.AmazonOrderReferenceId = orderReferenceId; - request.AddressConsentToken = addressConsentToken; - - var response = client.Service.GetOrderReferenceDetails(request); - - if (response != null && response.IsSetGetOrderReferenceDetailsResult()) - { - var detailsResult = response.GetOrderReferenceDetailsResult; - - if (detailsResult.IsSetOrderReferenceDetails()) - return detailsResult.OrderReferenceDetails; - } - return null; - } - - public OrderReferenceDetails SetOrderReferenceDetails(AmazonPayClient client, string orderReferenceId, decimal? orderTotalAmount, - string currencyCode, string orderGuid = null, string storeName = null) - { - var request = new SetOrderReferenceDetailsRequest(); - request.SellerId = client.Settings.SellerId; - request.AmazonOrderReferenceId = orderReferenceId; - - var attributes = new OrderReferenceAttributes(); - //attributes.SellerNote = client.Settings.SellerNoteOrderReference.Truncate(1024); - attributes.PlatformId = AmazonPayCore.PlatformId; - - if (orderTotalAmount.HasValue) - { - attributes.OrderTotal = new OrderTotal - { - Amount = orderTotalAmount.Value.ToString("0.00", CultureInfo.InvariantCulture), - CurrencyCode = currencyCode ?? "EUR" - }; - } - - if (orderGuid.HasValue()) - { - attributes.SellerOrderAttributes = new SellerOrderAttributes - { - SellerOrderId = orderGuid, - StoreName = storeName - }; - } - - request.OrderReferenceAttributes = attributes; - - var response = client.Service.SetOrderReferenceDetails(request); - - if (response != null && response.IsSetSetOrderReferenceDetailsResult()) - { - var detailsResult = response.SetOrderReferenceDetailsResult; - - if (detailsResult.IsSetOrderReferenceDetails()) - return detailsResult.OrderReferenceDetails; - } - return null; - } - - public OrderReferenceDetails SetOrderReferenceDetails(AmazonPayClient client, string orderReferenceId, string currencyCode, List cart) - { - decimal orderTotalDiscountAmountBase = decimal.Zero; - Discount orderTotalAppliedDiscount = null; - List appliedGiftCards = null; - int redeemedRewardPoints = 0; - decimal redeemedRewardPointsAmount = decimal.Zero; - - decimal? shoppingCartTotalBase = _orderTotalCalculationService.GetShoppingCartTotal(cart, - out orderTotalDiscountAmountBase, out orderTotalAppliedDiscount, out appliedGiftCards, out redeemedRewardPoints, out redeemedRewardPointsAmount); - - if (shoppingCartTotalBase.HasValue) - { - return SetOrderReferenceDetails(client, orderReferenceId, shoppingCartTotalBase, currencyCode); - } - - return null; - } - - /// Confirm an order reference informs Amazon that the buyer has placed the order. - public void ConfirmOrderReference(AmazonPayClient client, string orderReferenceId) - { - var request = new ConfirmOrderReferenceRequest(); - request.SellerId = client.Settings.SellerId; - request.AmazonOrderReferenceId = orderReferenceId; - - var response = client.Service.ConfirmOrderReference(request); - } - - public void CancelOrderReference(AmazonPayClient client, string orderReferenceId) - { - var request = new CancelOrderReferenceRequest(); - request.SellerId = client.Settings.SellerId; - request.AmazonOrderReferenceId = orderReferenceId; - - var response = client.Service.CancelOrderReference(request); - } - - public void CloseOrderReference(AmazonPayClient client, string orderReferenceId) - { - var request = new CloseOrderReferenceRequest(); - request.SellerId = client.Settings.SellerId; - request.AmazonOrderReferenceId = orderReferenceId; - - var response = client.Service.CloseOrderReference(request); - } - - /// Asynchronous as long as we do not set TransactionTimeout to 0. So transaction is always in pending state after return. - public void Authorize(AmazonPayClient client, ProcessPaymentResult result, List errors, string orderReferenceId, decimal orderTotalAmount, string currencyCode, string orderGuid) - { - var request = new AuthorizeRequest(); - request.SellerId = client.Settings.SellerId; - request.AmazonOrderReferenceId = orderReferenceId; - request.AuthorizationReferenceId = GetRandomId("Authorize"); - request.CaptureNow = (client.Settings.TransactionType == AmazonPayTransactionType.AuthorizeAndCapture); - //request.SellerAuthorizationNote = client.Settings.SellerNoteAuthorization.Truncate(256); - - request.AuthorizationAmount = new Price() - { - Amount = orderTotalAmount.ToString("0.00", CultureInfo.InvariantCulture), - CurrencyCode = currencyCode ?? "EUR" - }; - - var response = client.Service.Authorize(request); - - if (response != null && response.IsSetAuthorizeResult() && response.AuthorizeResult.IsSetAuthorizationDetails()) - { - var details = response.AuthorizeResult.AuthorizationDetails; - - result.AuthorizationTransactionId = details.AmazonAuthorizationId; - result.AuthorizationTransactionCode = details.AuthorizationReferenceId; - - if (details.IsSetAuthorizationStatus()) - { - var status = details.AuthorizationStatus; - - if (status.IsSetState()) - { - result.AuthorizationTransactionResult = status.State.ToString(); - } - - if (request.CaptureNow && details.IsSetIdList() && details.IdList.IsSetmember() && details.IdList.member.Count() > 0) - { - result.CaptureTransactionId = details.IdList.member[0]; - } - - if (status.IsSetReasonCode()) - { - if (status.ReasonCode.IsCaseInsensitiveEqual("InvalidPaymentMethod") || status.ReasonCode.IsCaseInsensitiveEqual("AmazonRejected") || - status.ReasonCode.IsCaseInsensitiveEqual("ProcessingFailure") || status.ReasonCode.IsCaseInsensitiveEqual("TransactionTimedOut") || - status.ReasonCode.IsCaseInsensitiveEqual("TransactionTimeout")) - { - if (status.IsSetReasonDescription()) - errors.Add("{0}: {1}".FormatWith(status.ReasonCode, status.ReasonDescription)); - else - errors.Add(status.ReasonCode); - } - } - } - } - - // The response to the Authorize call includes the AuthorizationStatus response element, which will be always be - // set to Pending if you have selected the asynchronous mode of operation. - - result.NewPaymentStatus = Core.Domain.Payments.PaymentStatus.Pending; - } - - public AuthorizationDetails GetAuthorizationDetails(AmazonPayClient client, string authorizationId, out AmazonPayApiData data) - { - data = new AmazonPayApiData(); - - AuthorizationDetails details = null; - var request = new GetAuthorizationDetailsRequest(); - request.SellerId = client.Settings.SellerId; - request.AmazonAuthorizationId = authorizationId; - - var response = client.Service.GetAuthorizationDetails(request); - - if (response.IsSetGetAuthorizationDetailsResult()) - { - var result = response.GetAuthorizationDetailsResult; - - if (result != null && result.IsSetAuthorizationDetails()) - details = result.AuthorizationDetails; - } - - try - { - data.MessageType = "GetAuthorizationDetails"; - - if (response.IsSetResponseMetadata() && response.ResponseMetadata.IsSetRequestId()) - data.MessageId = response.ResponseMetadata.RequestId; - - if (details != null) - { - if (details.IsSetAmazonAuthorizationId()) - data.AuthorizationId = details.AmazonAuthorizationId; - - if (details.IsSetAuthorizationReferenceId()) - data.ReferenceId = details.AuthorizationReferenceId; - - if (details.IsSetIdList() && details.IdList.IsSetmember()) - data.CaptureId = (details.IdList.member != null && details.IdList.member.Count > 0 ? details.IdList.member[0] : null); - - if (details.IsSetAuthorizationFee()) - data.Fee = new AmazonPayApiPrice(details.AuthorizationFee.Amount, details.AuthorizationFee.CurrencyCode); - - if (details.IsSetAuthorizationAmount()) - data.AuthorizedAmount = new AmazonPayApiPrice(details.AuthorizationAmount.Amount, details.AuthorizationAmount.CurrencyCode); - - if (details.IsSetCapturedAmount()) - data.CapturedAmount = new AmazonPayApiPrice(details.CapturedAmount.Amount, details.CapturedAmount.CurrencyCode); - - if (details.IsSetCaptureNow()) - data.CaptureNow = details.CaptureNow; - - if (details.IsSetCreationTimestamp()) - data.Creation = details.CreationTimestamp; - - if (details.IsSetExpirationTimestamp()) - data.Expiration = details.ExpirationTimestamp; - - if (details.IsSetAuthorizationStatus()) - { - data.ReasonCode = details.AuthorizationStatus.ReasonCode; - data.ReasonDescription = details.AuthorizationStatus.ReasonDescription; - data.State = details.AuthorizationStatus.State.ToString(); - data.StateLastUpdate = details.AuthorizationStatus.LastUpdateTimestamp; - } - } - } - catch (Exception exc) - { - exc.Dump(); - } - return details; - } - - public void Capture(AmazonPayClient client, CapturePaymentRequest capture, CapturePaymentResult result) - { - result.NewPaymentStatus = capture.Order.PaymentStatus; - - var request = new CaptureRequest(); - var store = _services.StoreService.GetStoreById(capture.Order.StoreId); - - request.SellerId = client.Settings.SellerId; - request.AmazonAuthorizationId = capture.Order.AuthorizationTransactionId; - request.CaptureReferenceId = GetRandomId("Capture"); - //request.SellerCaptureNote = client.Settings.SellerNoteCapture.Truncate(255); - - request.CaptureAmount = new Price - { - Amount = capture.Order.OrderTotal.ToString("0.00", CultureInfo.InvariantCulture), - CurrencyCode = store.PrimaryStoreCurrency.CurrencyCode - }; - - var response = client.Service.Capture(request); - - if (response != null && response.IsSetCaptureResult() && response.CaptureResult.IsSetCaptureDetails()) - { - var details = response.CaptureResult.CaptureDetails; - - result.CaptureTransactionId = details.AmazonCaptureId; - - if (details.IsSetCaptureStatus() && details.CaptureStatus.IsSetState()) - { - result.CaptureTransactionResult = details.CaptureStatus.State.ToString().Grow(details.CaptureStatus.ReasonCode, " "); - - if (details.CaptureStatus.State == PaymentStatus.COMPLETED) - result.NewPaymentStatus = Core.Domain.Payments.PaymentStatus.Paid; - } - } - } - - public CaptureDetails GetCaptureDetails(AmazonPayClient client, string captureId, out AmazonPayApiData data) - { - data = new AmazonPayApiData(); - - CaptureDetails details = null; - var request = new GetCaptureDetailsRequest(); - request.SellerId = client.Settings.SellerId; - request.AmazonCaptureId = captureId; - - var response = client.Service.GetCaptureDetails(request); - - if (response != null && response.IsSetGetCaptureDetailsResult()) - { - var result = response.GetCaptureDetailsResult; - if (result != null && result.IsSetCaptureDetails()) - details = result.CaptureDetails; - } - - try - { - data.MessageType = "GetCaptureDetails"; - - if (response.IsSetResponseMetadata() && response.ResponseMetadata.IsSetRequestId()) - data.MessageId = response.ResponseMetadata.RequestId; - - if (details != null) - { - if (details.IsSetAmazonCaptureId()) - data.CaptureId = details.AmazonCaptureId; - - if (details.IsSetCaptureReferenceId()) - data.ReferenceId = details.CaptureReferenceId; - - if (details.IsSetCaptureFee()) - data.Fee = new AmazonPayApiPrice(details.CaptureFee.Amount, details.CaptureFee.CurrencyCode); - - if (details.IsSetCaptureAmount()) - data.CapturedAmount = new AmazonPayApiPrice(details.CaptureAmount.Amount, details.CaptureAmount.CurrencyCode); - - if (details.IsSetRefundedAmount()) - data.RefundedAmount = new AmazonPayApiPrice(details.RefundedAmount.Amount, details.RefundedAmount.CurrencyCode); - - if (details.IsSetCreationTimestamp()) - data.Creation = details.CreationTimestamp; - - if (details.IsSetCaptureStatus()) - { - data.ReasonCode = details.CaptureStatus.ReasonCode; - data.ReasonDescription = details.CaptureStatus.ReasonDescription; - data.State = details.CaptureStatus.State.ToString(); - data.StateLastUpdate = details.CaptureStatus.LastUpdateTimestamp; - } - } - } - catch (Exception exc) - { - exc.Dump(); - } - return details; - } - - public string Refund(AmazonPayClient client, RefundPaymentRequest refund, RefundPaymentResult result) - { - result.NewPaymentStatus = refund.Order.PaymentStatus; - - string amazonRefundId = null; - var store = _services.StoreService.GetStoreById(refund.Order.StoreId); - - var request = new RefundRequest(); - request.SellerId = client.Settings.SellerId; - request.AmazonCaptureId = refund.Order.CaptureTransactionId; - request.RefundReferenceId = GetRandomId("Refund"); - //request.SellerRefundNote = client.Settings.SellerNoteRefund.Truncate(255); - - request.RefundAmount = new Price - { - Amount = refund.AmountToRefund.ToString("0.00", CultureInfo.InvariantCulture), - CurrencyCode = store.PrimaryStoreCurrency.CurrencyCode - }; - - var response = client.Service.Refund(request); - - if (response != null && response.IsSetRefundResult() && response.RefundResult.IsSetRefundDetails()) - { - var details = response.RefundResult.RefundDetails; - - amazonRefundId = details.AmazonRefundId; - - if (details.IsSetRefundStatus() && details.RefundStatus.IsSetState()) - { - if (refund.IsPartialRefund) - result.NewPaymentStatus = Core.Domain.Payments.PaymentStatus.PartiallyRefunded; - else - result.NewPaymentStatus = Core.Domain.Payments.PaymentStatus.Refunded; - } - } - return amazonRefundId; - } - - public RefundDetails GetRefundDetails(AmazonPayClient client, string refundId, out AmazonPayApiData data) - { - data = new AmazonPayApiData(); - - RefundDetails details = null; - var request = new GetRefundDetailsRequest(); - request.SellerId = client.Settings.SellerId; - request.AmazonRefundId = refundId; - - var response = client.Service.GetRefundDetails(request); - - if (response != null && response.IsSetGetRefundDetailsResult()) - { - var result = response.GetRefundDetailsResult; - if (result != null && result.IsSetRefundDetails()) - details = result.RefundDetails; - } - - try - { - data.MessageType = "GetRefundDetails"; - - if (response.IsSetResponseMetadata() && response.ResponseMetadata.IsSetRequestId()) - data.MessageId = response.ResponseMetadata.RequestId; - - if (details != null) - { - if (details.IsSetAmazonRefundId()) - data.RefundId = details.AmazonRefundId; - - if (details.IsSetRefundReferenceId()) - data.ReferenceId = details.RefundReferenceId; - - if (details.IsSetFeeRefunded()) - data.Fee = new AmazonPayApiPrice(details.FeeRefunded.Amount, details.FeeRefunded.CurrencyCode); - - if (details.IsSetRefundAmount()) - data.RefundedAmount = new AmazonPayApiPrice(details.RefundAmount.Amount, details.RefundAmount.CurrencyCode); - - if (details.IsSetCreationTimestamp()) - data.Creation = details.CreationTimestamp; - - if (details.IsSetRefundStatus()) - { - data.ReasonCode = details.RefundStatus.ReasonCode; - data.ReasonDescription = details.RefundStatus.ReasonDescription; - data.State = details.RefundStatus.State.ToString(); - data.StateLastUpdate = details.RefundStatus.LastUpdateTimestamp; - } - } - } - catch (Exception exc) - { - exc.Dump(); - } - return details; - } - - public string ToInfoString(AmazonPayApiData data) - { - var sb = new StringBuilder(); - - try - { - string[] strings = _services.Localization.GetResource("Plugins.Payments.AmazonPay.MessageStrings").SplitSafe(";"); - - string state = data.State.Grow(data.ReasonCode, " "); - if (data.ReasonDescription.HasValue()) - state = "{0} ({1})".FormatWith(state, data.ReasonDescription); - - sb.AppendLine("{0}: {1}".FormatWith(strings.SafeGet((int)AmazonPayMessage.MessageTyp), data.MessageType.NaIfEmpty())); - - sb.AppendLine("{0}: {1}".FormatWith(strings.SafeGet((int)AmazonPayMessage.State), state)); - - var stateDate = _dateTimeHelper.ConvertToUserTime(data.StateLastUpdate, DateTimeKind.Utc); - sb.AppendLine("{0}: {1}".FormatWith(strings.SafeGet((int)AmazonPayMessage.StateUpdate), stateDate.ToString())); - - sb.AppendLine("{0}: {1}".FormatWith(strings.SafeGet((int)AmazonPayMessage.MessageId), data.MessageId.NaIfEmpty())); - - if (data.AuthorizationId.HasValue()) - sb.AppendLine("{0}: {1}".FormatWith(strings.SafeGet((int)AmazonPayMessage.AuthorizationID), data.AuthorizationId)); - - if (data.CaptureId.HasValue()) - sb.AppendLine("{0}: {1}".FormatWith(strings.SafeGet((int)AmazonPayMessage.CaptureID), data.CaptureId)); - - if (data.RefundId.HasValue()) - sb.AppendLine("{0}: {1}".FormatWith(strings.SafeGet((int)AmazonPayMessage.RefundID), data.RefundId)); - - sb.AppendLine("{0}: {1}".FormatWith(strings.SafeGet((int)AmazonPayMessage.ReferenceID), data.ReferenceId.NaIfEmpty())); - - if (data.Fee != null && data.Fee.Amount != 0.0) - { - bool isSigned = (data.MessageType.IsCaseInsensitiveEqual("RefundNotification") || data.MessageType.IsCaseInsensitiveEqual("GetRefundDetails")); - sb.AppendLine("{0}: {1}".FormatWith(strings.SafeGet((int)AmazonPayMessage.Fee), (isSigned ? "-" : "") + data.Fee.ToString())); - } - - if (data.AuthorizedAmount != null && data.AuthorizedAmount.Amount != 0.0) - sb.AppendLine("{0}: {1}".FormatWith(strings.SafeGet((int)AmazonPayMessage.AuthorizedAmount), data.AuthorizedAmount.ToString())); - - if (data.CapturedAmount != null && data.CapturedAmount.Amount != 0.0) - sb.AppendLine("{0}: {1}".FormatWith(strings.SafeGet((int)AmazonPayMessage.CapturedAmount), data.CapturedAmount.ToString())); - - if (data.RefundedAmount != null && data.RefundedAmount.Amount != 0.0) - sb.AppendLine("{0}: {1}".FormatWith(strings.SafeGet((int)AmazonPayMessage.RefundedAmount), data.RefundedAmount.ToString())); - - if (data.CaptureNow.HasValue) - sb.AppendLine("{0}: {1}".FormatWith(strings.SafeGet((int)AmazonPayMessage.CaptureNow), data.CaptureNow.Value.ToString())); - - var creationDate = _dateTimeHelper.ConvertToUserTime(data.Creation, DateTimeKind.Utc); - sb.AppendLine("{0}: {1}".FormatWith(strings.SafeGet((int)AmazonPayMessage.Creation), creationDate.ToString())); - - if (data.Expiration.HasValue) - { - var expirationDate = _dateTimeHelper.ConvertToUserTime(data.Expiration.Value, DateTimeKind.Utc); - sb.AppendLine("{0}: {1}".FormatWith(strings.SafeGet((int)AmazonPayMessage.Expiration), expirationDate.ToString())); - } - } - catch (Exception exc) - { - exc.Dump(); - } - return sb.ToString(); - } - - public AmazonPayApiData ParseNotification(HttpRequestBase request) - { - string json = null; - - using (var reader = new StreamReader(request.InputStream)) - { - json = reader.ReadToEnd(); - } - - var parser = new OffAmazonPaymentsNotifications.NotificationsParser(); - var message = parser.ParseRawMessage(request.Headers, json); - - var data = new AmazonPayApiData() - { - MessageType = message.NotificationType.ToString(), - MessageId = ((OffAmazonPaymentsNotifications.IpnNotificationMetadata)message.NotificationMetadata).NotificationReferenceId - }; - - if (message.NotificationType == OffAmazonPaymentsNotifications.NotificationType.AuthorizationNotification) - { - var details = ((OffAmazonPaymentsNotifications.AuthorizationNotification)message).AuthorizationDetails; - - data.AuthorizationId = details.AmazonAuthorizationId; - data.CaptureId = details.IdList.SafeGet(0); - data.ReferenceId = details.AuthorizationReferenceId; - data.CaptureNow = details.CaptureNow; - data.Creation = details.CreationTimestamp; - - if (details.AuthorizationFee != null) - data.Fee = new AmazonPayApiPrice(details.AuthorizationFee.Amount, details.AuthorizationFee.CurrencyCode); - - if (details.AuthorizationAmount != null) - data.AuthorizedAmount = new AmazonPayApiPrice(details.AuthorizationAmount.Amount, details.AuthorizationAmount.CurrencyCode); - - if (details.CapturedAmount != null) - data.CapturedAmount = new AmazonPayApiPrice(details.CapturedAmount.Amount, details.CapturedAmount.CurrencyCode); - - if (details.ExpirationTimestampSpecified) - data.Expiration = details.ExpirationTimestamp; - - if (details.AuthorizationStatus != null) - { - data.ReasonCode = details.AuthorizationStatus.ReasonCode; - data.ReasonDescription = details.AuthorizationStatus.ReasonDescription; - data.State = details.AuthorizationStatus.State; - data.StateLastUpdate = details.AuthorizationStatus.LastUpdateTimestamp; - } - } - else if (message.NotificationType == OffAmazonPaymentsNotifications.NotificationType.CaptureNotification) - { - var details = ((OffAmazonPaymentsNotifications.CaptureNotification)message).CaptureDetails; - - data.CaptureId = details.AmazonCaptureId; - data.ReferenceId = details.CaptureReferenceId; - data.Creation = details.CreationTimestamp; - - if (details.CaptureFee != null) - data.Fee = new AmazonPayApiPrice(details.CaptureFee.Amount, details.CaptureFee.CurrencyCode); - - if (details.CaptureAmount != null) - data.CapturedAmount = new AmazonPayApiPrice(details.CaptureAmount.Amount, details.CaptureAmount.CurrencyCode); - - if (details.RefundedAmount != null) - data.RefundedAmount = new AmazonPayApiPrice(details.RefundedAmount.Amount, details.RefundedAmount.CurrencyCode); - - if (details.CaptureStatus != null) - { - data.ReasonCode = details.CaptureStatus.ReasonCode; - data.ReasonDescription = details.CaptureStatus.ReasonDescription; - data.State = details.CaptureStatus.State; - data.StateLastUpdate = details.CaptureStatus.LastUpdateTimestamp; - } - } - else if (message.NotificationType == OffAmazonPaymentsNotifications.NotificationType.RefundNotification) - { - var details = ((OffAmazonPaymentsNotifications.RefundNotification)message).RefundDetails; - - data.RefundId = details.AmazonRefundId; - data.ReferenceId = details.RefundReferenceId; - data.Creation = details.CreationTimestamp; - - if (details.FeeRefunded != null) - data.Fee = new AmazonPayApiPrice(details.FeeRefunded.Amount, details.FeeRefunded.CurrencyCode); - - if (details.RefundAmount != null) - data.RefundedAmount = new AmazonPayApiPrice(details.RefundAmount.Amount, details.RefundAmount.CurrencyCode); - - if (details.RefundStatus != null) - { - data.ReasonCode = details.RefundStatus.ReasonCode; - data.ReasonDescription = details.RefundStatus.ReasonDescription; - data.State = details.RefundStatus.State; - data.StateLastUpdate = details.RefundStatus.LastUpdateTimestamp; - } - } - - return data; - } - } - - - public class AmazonPayClient - { - public AmazonPayClient(AmazonPaySettings settings) - { - string appVersion = "1.0"; - - try - { - appVersion = EngineContext.Current.Resolve().GetPluginDescriptorBySystemName(AmazonPayCore.SystemName).Version.ToString(); - } - catch (Exception) { } - - var config = new OffAmazonPaymentsServiceConfig() - { - ServiceURL = (settings.UseSandbox ? AmazonPayCore.UrlApiEuSandbox : AmazonPayCore.UrlApiEuProduction) - }; - - config.SetUserAgent(AmazonPayCore.AppName, appVersion); - - Settings = settings; - Service = new OffAmazonPaymentsServiceClient(AmazonPayCore.AppName, appVersion, settings.AccessKey, settings.SecretKey, config); - } - - public IOffAmazonPaymentsService Service { get; private set; } - public AmazonPaySettings Settings { get; private set; } - } -} \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayCore.cs b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayCore.cs deleted file mode 100644 index fe1f8942a2..0000000000 --- a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayCore.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using SmartStore.AmazonPay.Api; -using SmartStore.AmazonPay.Settings; -using SmartStore.Core.Domain.Orders; - -namespace SmartStore.AmazonPay.Services -{ - public static class AmazonPayCore - { - public static string PlatformId { get { return "A3OJ83WFYM72IY"; } } - public static string AppName { get { return "SmartStore.Net " + SystemName; } } - public static string SystemName { get { return "SmartStore.AmazonPay"; } } - public static string AmazonPayCheckoutStateKey { get { return SystemName + ".CheckoutState"; } } - public static string AmazonPayOrderAttributeKey { get { return SystemName + ".OrderAttribute"; } } - public static string AmazonPayRefundIdKey { get { return SystemName + ".RefundId"; } } - public static string DataPollingTaskType { get { return "SmartStore.AmazonPay.DataPollingTask, SmartStore.AmazonPay"; } } - - public static string UrlApiEuSandbox { get { return "https://mws-eu.amazonservices.com/OffAmazonPayments_Sandbox/2013-01-01/"; } } - public static string UrlApiEuProduction { get { return "https://mws-eu.amazonservices.com/OffAmazonPayments/2013-01-01/"; } } - - public static string UrlWidgetSandbox { get { return "https://static-eu.payments-amazon.com/OffAmazonPayments/{0}/sandbox/js/Widgets.js"; } } - public static string UrlWidgetProduction { get { return "https://static-eu.payments-amazon.com/OffAmazonPayments/{0}/js/Widgets.js"; } } - - public static string UrlButtonSandbox { get { return "https://payments-sandbox.amazon.{0}/gp/widgets/button"; } } - public static string UrlButtonProduction { get { return "https://payments.amazon.{0}/gp/widgets/button"; } } - - public static string UrlIpnSchema { get { return "https://amazonpayments.s3.amazonaws.com/documents/payments_ipn.xsd"; } } - } - - - [Serializable] - public class AmazonPayCheckoutState - { - public string OrderReferenceId { get; set; } - } - - - public class AmazonPayActionState - { - public Guid OrderGuid { get; set; } - public List Errors { get; set; } - } - - - [Serializable] - public class AmazonPayOrderAttribute - { - public string OrderReferenceId { get; set; } - public bool IsBillingAddressApplied { get; set; } - } - - - public class PollingLoopData - { - public PollingLoopData(int orderId) - { - OrderId = orderId; - } - - public int OrderId { get; private set; } - public Order Order { get; set; } - public AmazonPaySettings Settings { get; set; } - public AmazonPayClient Client { get; set; } - } - - - public class AmazonPayApiData - { - public string MessageType { get; set; } - public string MessageId { get; set; } - public string AuthorizationId { get; set; } - public string CaptureId { get; set; } - public string RefundId { get; set; } - public string ReferenceId { get; set; } - - public string ReasonCode { get; set; } - public string ReasonDescription { get; set; } - public string State { get; set; } - public DateTime StateLastUpdate { get; set; } - - public AmazonPayApiPrice Fee { get; set; } - public AmazonPayApiPrice AuthorizedAmount { get; set; } - public AmazonPayApiPrice CapturedAmount { get; set; } - public AmazonPayApiPrice RefundedAmount { get; set; } - - public bool? CaptureNow { get; set; } - public DateTime Creation { get; set; } - public DateTime? Expiration { get; set; } - - public string AnyAmazonId - { - get - { - if (CaptureId.HasValue()) - return CaptureId; - if (AuthorizationId.HasValue()) - return AuthorizationId; - return RefundId; - } - } - } - - - public class AmazonPayApiPrice - { - public AmazonPayApiPrice() - { - } - public AmazonPayApiPrice(double amount, string currenycCode) - { - Amount = amount; - CurrencyCode = currenycCode; - } - public AmazonPayApiPrice(string amount, string currenycCode) - { - double d; - if (amount.HasValue() && double.TryParse(amount, NumberStyles.Any, CultureInfo.InvariantCulture, out d)) - Amount = d; - - CurrencyCode = currenycCode; - } - - public double Amount { get; set; } - public string CurrencyCode { get; set; } - - public override string ToString() - { - string str = Amount.ToString("0.00", CultureInfo.InvariantCulture); - return str.Grow(CurrencyCode, " "); - } - } - - - public enum AmazonPayRequestType - { - None = 0, - ShoppingCart, - Address, - Payment, - OrderReviewData, - ShippingMethod, - MiniShoppingCart, - LoginHandler - } - - public enum AmazonPayTransactionType - { - None = 0, - Authorize, - AuthorizeAndCapture - } - - public enum AmazonPaySaveDataType - { - None = 0, - OnlyIfEmpty, - Always - } - - public enum AmazonPayDataFetchingType - { - None = 0, - Ipn, - Polling - } - - public enum AmazonPayResultType - { - None = 0, - PluginView, - Redirect, - Unauthorized - } - - public enum AmazonPayOrderNote - { - FunctionExecuted = 0, - Answer, - BillingAddressApplied, - AmazonMessageProcessed, - BillingAddressCountryNotAllowed - } - - public enum AmazonPayMessage - { - MessageTyp = 0, - MessageId, - AuthorizationID, - CaptureID, - RefundID, - ReferenceID, - State, - StateUpdate, - Fee, - AuthorizedAmount, - CapturedAmount, - RefundedAmount, - CaptureNow, - Creation, - Expiration - } -} diff --git a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayEnums.cs b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayEnums.cs new file mode 100644 index 0000000000..4e9144bc9a --- /dev/null +++ b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayEnums.cs @@ -0,0 +1,78 @@ +namespace SmartStore.AmazonPay.Services +{ + public enum AmazonPayRequestType + { + None = 0, + ShoppingCart, + Address, + PaymentMethod, + OrderReviewData, + ShippingMethod, + MiniShoppingCart, + + /// + /// Amazon Pay button clicked + /// + PayButtonHandler, + + /// + /// Display authentication button on login page + /// + AuthenticationPublicInfo + } + + public enum AmazonPayTransactionType + { + None = 0, + Authorize, + AuthorizeAndCapture + } + + public enum AmazonPayAuthorizeMethod + { + Omnichronous = 0, + Asynchronous, + Synchronous + } + + public enum AmazonPaySaveDataType + { + None = 0, + OnlyIfEmpty, + Always + } + + public enum AmazonPayDataFetchingType + { + None = 0, + Ipn, + Polling + } + + public enum AmazonPayResultType + { + None = 0, + PluginView, + Redirect, + Unauthorized + } + + public enum AmazonPayMessage + { + MessageTyp = 0, + MessageId, + AuthorizationID, + CaptureID, + RefundID, + ReferenceID, + State, + StateUpdate, + Fee, + AuthorizedAmount, + CapturedAmount, + RefundedAmount, + CaptureNow, + Creation, + Expiration + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayService.cs b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayService.cs index 762697b3ce..b643ae9d01 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayService.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayService.cs @@ -1,94 +1,116 @@ -using Autofac; -using OffAmazonPaymentsService; -using SmartStore.AmazonPay.Api; -using SmartStore.AmazonPay.Extensions; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using System.Web.Mvc; +using AmazonPay; +using AmazonPay.Responses; +using AmazonPay.StandardPaymentRequests; +using Autofac; +using Newtonsoft.Json.Linq; using SmartStore.AmazonPay.Models; -using SmartStore.AmazonPay.Settings; +using SmartStore.AmazonPay.Services.Internal; +using SmartStore.Core; using SmartStore.Core.Async; using SmartStore.Core.Data; using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Localization; using SmartStore.Core.Logging; +using SmartStore.Core.Plugins; using SmartStore.Services; +using SmartStore.Services.Authentication.External; using SmartStore.Services.Catalog; using SmartStore.Services.Common; using SmartStore.Services.Customers; using SmartStore.Services.Directory; +using SmartStore.Services.Helpers; +using SmartStore.Services.Localization; using SmartStore.Services.Messages; using SmartStore.Services.Orders; using SmartStore.Services.Payments; -using SmartStore.Services.Tasks; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Web; -using System.Web.Mvc; -using System.Xml.Serialization; +using SmartStore.Web; +using SmartStore.Web.Framework; namespace SmartStore.AmazonPay.Services { - public class AmazonPayService : IAmazonPayService + public partial class AmazonPayService : IAmazonPayService { - private readonly IAmazonPayApi _api; private readonly HttpContextBase _httpContext; + private readonly IRepository _orderRepository; private readonly ICommonServices _services; private readonly IPaymentService _paymentService; private readonly IGenericAttributeService _genericAttributeService; private readonly IOrderTotalCalculationService _orderTotalCalculationService; private readonly ICurrencyService _currencyService; - private readonly CurrencySettings _currencySettings; private readonly ICustomerService _customerService; - private readonly IPriceFormatter _priceFormatter; - private readonly OrderSettings _orderSettings; + private readonly ICountryService _countryService; + private readonly IStateProvinceService _stateProvinceService; + private readonly IAddressService _addressService; private readonly IOrderService _orderService; - private readonly IRepository _orderRepository; private readonly IOrderProcessingService _orderProcessingService; - private readonly IScheduleTaskService _scheduleTaskService; - private readonly IWorkflowMessageService _workflowMessageService; + + private readonly IPriceFormatter _priceFormatter; + private readonly IDateTimeHelper _dateTimeHelper; + private readonly IPluginFinder _pluginFinder; + private readonly Lazy _authorizer; + private readonly AddressSettings _addressSettings; + private readonly OrderSettings _orderSettings; + private readonly CompanyInformationSettings _companyInformationSettings; + private readonly Lazy _externalAuthenticationSettings; public AmazonPayService( - IAmazonPayApi api, HttpContextBase httpContext, + IRepository orderRepository, ICommonServices services, IPaymentService paymentService, IGenericAttributeService genericAttributeService, IOrderTotalCalculationService orderTotalCalculationService, ICurrencyService currencyService, - CurrencySettings currencySettings, ICustomerService customerService, - IPriceFormatter priceFormatter, - OrderSettings orderSettings, + ICountryService countryService, + IStateProvinceService stateProvinceService, + IAddressService addressService, IOrderService orderService, - IRepository orderRepository, IOrderProcessingService orderProcessingService, - IScheduleTaskService scheduleTaskService, - IWorkflowMessageService workflowMessageService) + IPriceFormatter priceFormatter, + IDateTimeHelper dateTimeHelper, + IPluginFinder pluginFinder, + Lazy authorizer, + AddressSettings addressSettings, + OrderSettings orderSettings, + CompanyInformationSettings companyInformationSettings, + Lazy externalAuthenticationSettings) { - _api = api; _httpContext = httpContext; + _orderRepository = orderRepository; _services = services; _paymentService = paymentService; _genericAttributeService = genericAttributeService; _orderTotalCalculationService = orderTotalCalculationService; _currencyService = currencyService; - _currencySettings = currencySettings; _customerService = customerService; - _priceFormatter = priceFormatter; - _orderSettings = orderSettings; + _countryService = countryService; + _stateProvinceService = stateProvinceService; + _addressService = addressService; _orderService = orderService; - _orderRepository = orderRepository; _orderProcessingService = orderProcessingService; - _scheduleTaskService = scheduleTaskService; - _workflowMessageService = workflowMessageService; + + _priceFormatter = priceFormatter; + _dateTimeHelper = dateTimeHelper; + _pluginFinder = pluginFinder; + _authorizer = authorizer; + _addressSettings = addressSettings; + _orderSettings = orderSettings; + _companyInformationSettings = companyInformationSettings; + _externalAuthenticationSettings = externalAuthenticationSettings; T = NullLocalizer.Instance; Logger = NullLogger.Instance; @@ -97,173 +119,83 @@ public AmazonPayService( public Localizer T { get; set; } public ILogger Logger { get; set; } - private string GetPluginUrl(string action, bool useSsl = false) - { - string pluginUrl = "{0}Plugins/SmartStore.AmazonPay/AmazonPay/{1}".FormatWith(_services.WebHelper.GetStoreLocation(useSsl), action); - return pluginUrl; - } - - //private decimal? GetOrderTotal() - //{ - // decimal orderTotalDiscountAmountBase = decimal.Zero; - // Discount orderTotalAppliedDiscount = null; - // List appliedGiftCards = null; - // int redeemedRewardPoints = 0; - // decimal redeemedRewardPointsAmount = decimal.Zero; - - // var cart = _services.WorkContext.CurrentCustomer.GetCartItems(ShoppingCartType.ShoppingCart, _services.StoreContext.CurrentStore.Id); - - // decimal? shoppingCartTotalBase = _orderTotalCalculationService.GetShoppingCartTotal(cart, - // out orderTotalDiscountAmountBase, out orderTotalAppliedDiscount, out appliedGiftCards, out redeemedRewardPoints, out redeemedRewardPointsAmount); - - // if (shoppingCartTotalBase.HasValue) // shipping method needs to be selected here! - // { - // decimal shoppingCartTotal = _currencyService.ConvertFromPrimaryStoreCurrency(shoppingCartTotalBase.Value, _services.WorkContext.WorkingCurrency); - - // return shoppingCartTotal; - // } - // return null; - //} - - private void SerializeOrderAttribute(AmazonPayOrderAttribute attribute, Order order) - { - if (attribute != null) - { - var sb = new StringBuilder(); - using (var writer = new StringWriter(sb)) - { - var serializer = new XmlSerializer(typeof(AmazonPayOrderAttribute)); - serializer.Serialize(writer, attribute); - - _genericAttributeService.SaveAttribute(order, AmazonPayCore.AmazonPayOrderAttributeKey, sb.ToString(), order.StoreId); - } - } - } - private AmazonPayOrderAttribute DeserializeOrderAttribute(Order order) + public void SetupConfiguration(ConfigurationModel model) { - var serialized = order.GetAttribute(AmazonPayCore.AmazonPayOrderAttributeKey, _genericAttributeService, order.StoreId); + var store = _services.StoreContext.CurrentStore; + var language = _services.WorkContext.WorkingLanguage; + var descriptor = _pluginFinder.GetPluginDescriptorBySystemName(AmazonPayPlugin.SystemName); + var allStores = _services.StoreService.GetAllStores(); + var urlHelper = new UrlHelper(_httpContext.Request.RequestContext); - if (!serialized.HasValue()) - { - var attribute = new AmazonPayOrderAttribute(); - - // legacy < v.1.14 - attribute.OrderReferenceId = order.GetAttribute(AmazonPayCore.SystemName + ".OrderReferenceId", order.StoreId); - - return attribute; - } + model.IpnUrl = GetPluginUrl("IPNHandler", true); + model.ConfigGroups = T("Plugins.Payments.AmazonPay.ConfigGroups").Text.SplitSafe(";"); + model.PrimaryStoreCurrencyCode = _services.StoreContext.CurrentStore.PrimaryStoreCurrency.CurrencyCode; - using (var reader = new StringReader(serialized)) + model.RegisterUrl = "https://payments-eu.amazon.com/register"; + model.SoftwareVersion = SmartStoreVersion.CurrentFullVersion; + if (descriptor != null) { - var serializer = new XmlSerializer(typeof(AmazonPayOrderAttribute)); - return (AmazonPayOrderAttribute)serializer.Deserialize(reader); + model.PluginVersion = descriptor.Version.ToString(); } - } - - public void LogError(Exception exception, string shortMessage = null, string fullMessage = null, bool notify = false, IList errors = null) - { - try - { - if (exception != null) - { - shortMessage = exception.Message; - exception.Dump(); - } - - if (shortMessage.HasValue()) + model.LeadCode = LeadCode; + model.PlatformId = PlatformId; + // Not implemented. Not available for europe at the moment. + model.PublicKey = string.Empty; + model.KeyShareUrl = GetPluginUrl("ShareKey", store.SslEnabled); + model.LanguageLocale = language.UniqueSeoCode.ToAmazonLanguageCode('_'); + model.MerchantStoreDescription = store.Name.Truncate(2048); + model.MerchantPrivacyNoticeUrl = urlHelper.RouteUrl("Topic", new { SystemName = "privacyinfo" }, store.SslEnabled ? "https" : "http"); + model.MerchantSandboxIpnUrl = model.IpnUrl; + model.MerchantProductionIpnUrl = model.IpnUrl; + + model.MerchantLoginDomains = new HashSet(StringComparer.OrdinalIgnoreCase); + model.MerchantLoginRedirectUrls = new HashSet(StringComparer.OrdinalIgnoreCase); + model.CurrentMerchantLoginDomains = new HashSet(StringComparer.OrdinalIgnoreCase); + model.CurrentMerchantLoginRedirectUrls = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var entity in allStores) + { + if (entity.SecureUrl.HasValue()) { - Logger.Error(exception, shortMessage); - - if (notify) - _services.Notifier.Error(new LocalizedString(shortMessage)); - } - } - catch (Exception) { } + try + { + var uri = new Uri(entity.SecureUrl); + // Only protocol and domain name. + var loginDomain = uri.GetLeftPart(UriPartial.Scheme | UriPartial.Authority).EmptyNull().TrimEnd('/'); + model.MerchantLoginDomains.Add(loginDomain); - if (errors != null && shortMessage.HasValue()) - errors.Add(shortMessage); - } - public void LogAmazonError(OffAmazonPaymentsServiceException exception, bool notify = false, IList errors = null) - { - try - { - string shortMessage, fullMessage; + if (entity.Id == store.Id) + { + model.CurrentMerchantLoginDomains.Add(loginDomain); + } + } + catch { } - if (exception.GetErrorStrings(out shortMessage, out fullMessage)) - { - Logger.Error(exception, shortMessage); + var urlRoot = entity.SecureUrl.EnsureEndsWith("/"); + var payHandlerUrl = urlRoot + "Plugins/SmartStore.AmazonPay/AmazonPayShoppingCart/PayButtonHandler"; + var authHandlerUrl = urlRoot + "Plugins/SmartStore.AmazonPay/AmazonPay/AuthenticationButtonHandler"; - if (notify) - _services.Notifier.Error(new LocalizedString(shortMessage)); + model.MerchantLoginRedirectUrls.Add(payHandlerUrl); + model.MerchantLoginRedirectUrls.Add(authHandlerUrl); - if (errors != null) - errors.Add(shortMessage); + if (entity.Id == store.Id) + { + model.CurrentMerchantLoginRedirectUrls.Add(payHandlerUrl); + model.CurrentMerchantLoginRedirectUrls.Add(authHandlerUrl); + } } } - catch (Exception) { } - } - - private bool IsActive(int storeId, bool logInactive = false) - { - bool isActive = _paymentService.IsPaymentMethodActive(AmazonPayCore.SystemName, storeId); - - if (!isActive && logInactive) - { - LogError(null, T("Plugins.Payments.AmazonPay.PaymentMethodNotActive", _services.StoreContext.CurrentStore.Name)); - } - return isActive; - } - public void AddOrderNote(AmazonPaySettings settings, Order order, AmazonPayOrderNote note, string anyString = null, bool isIpn = false) - { - try + if (_companyInformationSettings.CountryId != 0) { - if (!settings.AddOrderNotes || order == null) - return; - - var sb = new StringBuilder(); - - string[] orderNoteStrings = T("Plugins.Payments.AmazonPay.OrderNoteStrings").Text.SplitSafe(";"); - string faviconUrl = "{0}Plugins/{1}/Content/images/favicon.png".FormatWith(_services.WebHelper.GetStoreLocation(false), AmazonPayCore.SystemName); - - sb.AppendFormat("", faviconUrl); - - if (anyString.HasValue()) - { - anyString = orderNoteStrings.SafeGet((int)note).FormatWith(anyString); - } - else + var merchantCountry = _countryService.GetCountryById(_companyInformationSettings.CountryId); + if (merchantCountry != null) { - anyString = orderNoteStrings.SafeGet((int)note); - anyString = anyString.Replace("{0}", ""); + model.MerchantCountry = merchantCountry.GetLocalized(x => x.Name, language.Id, false, false); } - - if (anyString.HasValue()) - { - sb.AppendFormat("{0}", anyString); - } - - if (isIpn) - order.HasNewPaymentNotification = true; - - order.OrderNotes.Add(new OrderNote - { - Note = sb.ToString(), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - - _orderService.UpdateOrder(order); } - catch (Exception exc) - { - LogError(exc); - } - } - public void SetupConfiguration(ConfigurationModel model) - { - model.DataFetchings = new List() + model.DataFetchings = new List { new SelectListItem { @@ -284,7 +216,7 @@ public void SetupConfiguration(ConfigurationModel model) } }; - model.TransactionTypes = new List() + model.TransactionTypes = new List { new SelectListItem { @@ -300,7 +232,9 @@ public void SetupConfiguration(ConfigurationModel model) } }; - model.SaveEmailAndPhones = new List() + model.AuthorizeMethods = model.AuthorizeMethod.ToSelectList(); + + model.SaveEmailAndPhones = new List { new SelectListItem { @@ -320,40 +254,9 @@ public void SetupConfiguration(ConfigurationModel model) Value = ((int)AmazonPaySaveDataType.Always).ToString() } }; - - model.IpnUrl = GetPluginUrl("IPNHandler", _services.StoreContext.CurrentStore.SslEnabled); - - model.ConfigGroups = T("Plugins.Payments.AmazonPay.ConfigGroups").Text.SplitSafe(";"); - - var task = _scheduleTaskService.GetTaskByType(AmazonPayCore.DataPollingTaskType); - - if (task == null) - model.PollingTaskMinutes = 30; - else - model.PollingTaskMinutes = 30; // (task.Seconds / 60); - } - - public string GetWidgetUrl() - { - try - { - var store = _services.StoreContext.CurrentStore; - - if (IsActive(store.Id)) - { - var settings = _services.Settings.LoadSetting(store.Id); - if (settings.SellerId.HasValue()) - return settings.GetWidgetUrl(); - } - } - catch (Exception exc) - { - LogError(exc); - } - return ""; } - public AmazonPayViewModel ProcessPluginRequest(AmazonPayRequestType type, TempDataDictionary tempData, string orderReferenceId = null) + public AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDictionary tempData) { var model = new AmazonPayViewModel(); model.Type = type; @@ -362,31 +265,41 @@ public AmazonPayViewModel ProcessPluginRequest(AmazonPayRequestType type, TempDa { var store = _services.StoreContext.CurrentStore; var customer = _services.WorkContext.CurrentCustomer; + var language = _services.WorkContext.WorkingLanguage; var cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, store.Id); + var storeLocation = _services.WebHelper.GetStoreLocation(store.SslEnabled); + var settings = _services.Settings.LoadSetting(store.Id); + + model.ButtonHandlerUrl = $"{storeLocation}Plugins/SmartStore.AmazonPay/AmazonPayShoppingCart/PayButtonHandler"; + model.LanguageCode = language.UniqueSeoCode.ToAmazonLanguageCode(); - if (type == AmazonPayRequestType.LoginHandler) + if (type == AmazonPayRequestType.PayButtonHandler) { - if (string.IsNullOrWhiteSpace(orderReferenceId)) + if (cart.Count <= 0 || !IsPaymentMethodActive(store.Id)) { - LogError(null, T("Plugins.Payments.AmazonPay.MissingOrderReferenceId"), null, true); model.Result = AmazonPayResultType.Redirect; return model; } - if (cart.Count <= 0 || !IsActive(store.Id)) + if (customer.IsGuest() && !_orderSettings.AnonymousCheckoutAllowed) { - model.Result = AmazonPayResultType.Redirect; + model.Result = AmazonPayResultType.Unauthorized; return model; } - if (customer.IsGuest() && !_orderSettings.AnonymousCheckoutAllowed) + var accessToken = _httpContext.Request.QueryString["access_token"]; + if (accessToken.IsEmpty()) { - model.Result = AmazonPayResultType.Unauthorized; + var msg = T("Plugins.Payments.AmazonPay.MissingAddressConsentToken"); + Logger.Error(null, msg); + _services.Notifier.Error(new LocalizedString(msg)); + + model.Result = AmazonPayResultType.Redirect; return model; } + // Create session state object. var checkoutState = _httpContext.GetCheckoutState(); - if (checkoutState == null) { Logger.Warn("Checkout state is null in AmazonPayService.ValidateAndInitiateCheckout!"); @@ -394,37 +307,36 @@ public AmazonPayViewModel ProcessPluginRequest(AmazonPayRequestType type, TempDa return model; } - var state = new AmazonPayCheckoutState() - { - OrderReferenceId = orderReferenceId - }; - - if (checkoutState.CustomProperties.ContainsKey(AmazonPayCore.AmazonPayCheckoutStateKey)) - checkoutState.CustomProperties[AmazonPayCore.AmazonPayCheckoutStateKey] = state; - else - checkoutState.CustomProperties.Add(AmazonPayCore.AmazonPayCheckoutStateKey, state); - - //_httpContext.Session.SafeSet(AmazonPayCore.AmazonPayCheckoutStateKey, state); + checkoutState.CustomProperties[AmazonPayPlugin.SystemName + ".CheckoutState"] = new AmazonPayCheckoutState { AccessToken = accessToken }; model.RedirectAction = "Index"; model.RedirectController = "Checkout"; model.Result = AmazonPayResultType.Redirect; return model; } + else if (type == AmazonPayRequestType.AuthenticationPublicInfo) + { + if (settings.SellerId.IsEmpty() || settings.AccessKey.IsEmpty() || settings.SecretKey.IsEmpty()) + { + return null; + } + + model.ButtonHandlerUrl = $"{storeLocation}Plugins/SmartStore.AmazonPay/AmazonPay/AuthenticationButtonHandler"; + + // Do not append returnUrl to button handler URL. Handler URLs must be whitelisted in Amazon Seller Central. + _httpContext.Session["AmazonAuthReturnUrl"] = _httpContext.Request.QueryString["returnUrl"]; + } else if (type == AmazonPayRequestType.ShoppingCart || type == AmazonPayRequestType.MiniShoppingCart) { - if (cart.Count <= 0 || !IsActive(store.Id)) + if (cart.Count <= 0 || !IsPaymentMethodActive(store.Id)) { model.Result = AmazonPayResultType.None; return model; } - - string storeLocation = _services.WebHelper.GetStoreLocation(store.SslEnabled); - model.LoginHandlerUrl = "{0}Plugins/SmartStore.AmazonPay/AmazonPayShoppingCart/LoginHandler".FormatWith(storeLocation); } else { - if (!_httpContext.HasAmazonPayState() || cart.Count <= 0) + if (cart.Count <= 0) { model.Result = AmazonPayResultType.Redirect; return model; @@ -437,32 +349,59 @@ public AmazonPayViewModel ProcessPluginRequest(AmazonPayRequestType type, TempDa } var state = _httpContext.GetAmazonPayState(_services.Localization); - model.OrderReferenceId = state.OrderReferenceId; + model.AddressConsentToken = state.AccessToken; //model.IsOrderConfirmed = state.IsOrderConfirmed; + + if (type == AmazonPayRequestType.ShippingMethod || type == AmazonPayRequestType.PaymentMethod) + { + if (state.OrderReferenceId.IsEmpty() || state.AccessToken.IsEmpty()) + { + model.Result = AmazonPayResultType.Redirect; + return model; + } + } } var currency = store.PrimaryStoreCurrency; - var settings = _services.Settings.LoadSetting(store.Id); model.SellerId = settings.SellerId; - model.ClientId = settings.AccessKey; + model.ClientId = settings.ClientId; model.IsShippable = cart.RequiresShipping(); model.IsRecurring = cart.IsRecurring(); - model.WidgetUrl = settings.GetWidgetUrl(); - model.ButtonUrl = settings.GetButtonUrl(type); - model.AddressWidgetWidth = Math.Max(settings.AddressWidgetWidth, 200); - model.AddressWidgetHeight = Math.Max(settings.AddressWidgetHeight, 228); - model.PaymentWidgetWidth = Math.Max(settings.PaymentWidgetWidth, 200); - model.PaymentWidgetHeight = Math.Max(settings.PaymentWidgetHeight, 228); + model.WidgetUrl = GetWidgetUrl(settings); - if (type == AmazonPayRequestType.MiniShoppingCart) + if (type == AmazonPayRequestType.MiniShoppingCart || type == AmazonPayRequestType.ShoppingCart) { - if (!settings.ShowButtonInMiniShoppingCart) + if (type == AmazonPayRequestType.MiniShoppingCart && !settings.ShowButtonInMiniShoppingCart) + { + model.Result = AmazonPayResultType.None; + return model; + } + if (settings.ShowPayButtonForAdminOnly && !customer.IsAdmin()) { model.Result = AmazonPayResultType.None; return model; } + + // AmazonPay review: The setting for payment button type has been removed. + model.ButtonType = "PwA"; + model.ButtonColor = settings.PayButtonColor; + model.ButtonSize = settings.PayButtonSize; + + var failedPaymentReason = _httpContext.Session["AmazonPayFailedPaymentReason"] as string; + if (failedPaymentReason.IsCaseInsensitiveEqual("AmazonRejected")) + { + model.Logout = true; + _services.Notifier.Error(new LocalizedString(T("Plugins.Payments.AmazonPay.AuthorizationSoftDeclineMessage"))); + } + _httpContext.Session.SafeRemove("AmazonPayFailedPaymentReason"); + } + else if (type == AmazonPayRequestType.AuthenticationPublicInfo) + { + model.ButtonType = settings.AuthButtonType; + model.ButtonColor = settings.AuthButtonColor; + model.ButtonSize = settings.AuthButtonSize; } else if (type == AmazonPayRequestType.Address) { @@ -474,10 +413,12 @@ public AmazonPayViewModel ProcessPluginRequest(AmazonPayRequestType type, TempDa return model; } - var shippingToCountryNotAllowed = tempData[AmazonPayCore.SystemName + "ShippingToCountryNotAllowed"]; + var shippingToCountryNotAllowed = tempData[AmazonPayPlugin.SystemName + "ShippingToCountryNotAllowed"]; if (shippingToCountryNotAllowed != null && true == (bool)shippingToCountryNotAllowed) + { model.Warning = T("Plugins.Payments.AmazonPay.ShippingToCountryNotAllowed"); + } } else if (type == AmazonPayRequestType.ShippingMethod) { @@ -485,33 +426,107 @@ public AmazonPayViewModel ProcessPluginRequest(AmazonPayRequestType type, TempDa if (model.IsShippable) { - var client = new AmazonPayClient(settings); - var details = _api.GetOrderReferenceDetails(client, model.OrderReferenceId); - - if (_api.FindAndApplyAddress(details, customer, model.IsShippable, true)) + var client = CreateClient(settings); + var getOrderRequest = new GetOrderReferenceDetailsRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonOrderReferenceId(model.OrderReferenceId) + .WithAccessToken(model.AddressConsentToken); + + var getOrderResponse = client.GetOrderReferenceDetails(getOrderRequest); + if (getOrderResponse.GetSuccess()) { + // Billing address not available here. getOrderResponse.GetBillingAddressDetails() is null. + //if (FindAndApplyAddress(getOrderResponse, customer, model.IsShippable, true)) + var countryAllowsShipping = true; + var countryAllowsBilling = true; + + var address = CreateAddress( + getOrderResponse.GetEmail(), + getOrderResponse.GetBuyerShippingName(), + getOrderResponse.GetAddressLine1(), + getOrderResponse.GetAddressLine2(), + getOrderResponse.GetAddressLine3(), + getOrderResponse.GetCity(), + getOrderResponse.GetPostalCode(), + getOrderResponse.GetPhone(), + getOrderResponse.GetCountryCode(), + getOrderResponse.GetStateOrRegion(), + getOrderResponse.GetCounty(), + getOrderResponse.GetDistrict(), + out countryAllowsShipping, + out countryAllowsBilling); + + if (model.IsShippable && !countryAllowsShipping) + { + tempData[AmazonPayPlugin.SystemName + "ShippingToCountryNotAllowed"] = true; + model.RedirectAction = "ShippingAddress"; + model.RedirectController = "Checkout"; + model.Result = AmazonPayResultType.Redirect; + return model; + } + + if (address.Email.IsEmpty()) + { + address.Email = customer.Email; + } + + var existingAddress = customer.Addresses.ToList().FindAddress(address, true); + if (existingAddress == null) + { + customer.Addresses.Add(address); + customer.ShippingAddress = model.IsShippable ? address : null; + } + else + { + customer.ShippingAddress = model.IsShippable ? existingAddress : null; + } + _customerService.UpdateCustomer(customer); model.Result = AmazonPayResultType.None; return model; } else { - tempData[AmazonPayCore.SystemName + "ShippingToCountryNotAllowed"] = true; - model.RedirectAction = "ShippingAddress"; - model.RedirectController = "Checkout"; - model.Result = AmazonPayResultType.Redirect; - return model; + LogError(getOrderResponse); } } } - else if (type == AmazonPayRequestType.Payment) + else if (type == AmazonPayRequestType.PaymentMethod) { - _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedPaymentMethod, AmazonPayCore.SystemName, store.Id); + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedPaymentMethod, AmazonPayPlugin.SystemName, store.Id); - var client = new AmazonPayClient(settings); - var unused = _api.SetOrderReferenceDetails(client, model.OrderReferenceId, store.PrimaryStoreCurrency.CurrencyCode, cart); + var failedPaymentReason = _httpContext.Session["AmazonPayFailedPaymentReason"] as string; + if (failedPaymentReason.IsCaseInsensitiveEqual("InvalidPaymentMethod")) + { + _services.Notifier.Warning(new LocalizedString(T("Payment.PayingFailed"))); + } + else if (failedPaymentReason.IsCaseInsensitiveEqual("PaymentMethodNotAllowed")) + { + _services.Notifier.Warning(new LocalizedString(T("Plugins.Payments.AmazonPay.AuthorizationSoftDeclineMessage"))); + } + else + { + decimal? shoppingCartTotalBase = _orderTotalCalculationService.GetShoppingCartTotal(cart); + if (shoppingCartTotalBase.HasValue) + { + var client = CreateClient(settings); + var setOrderRequest = new SetOrderReferenceDetailsRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonOrderReferenceId(model.OrderReferenceId) + .WithPlatformId(PlatformId) + .WithAmount(shoppingCartTotalBase.Value) + .WithCurrencyCode(ConvertCurrency(store.PrimaryStoreCurrency.CurrencyCode)) + .WithStoreName(store.Name); + + var setOrderResponse = client.SetOrderReferenceDetails(setOrderRequest); + if (!setOrderResponse.GetSuccess()) + { + LogError(setOrderResponse); + } + } + } - // this is ugly... + // This is ugly... var paymentRequest = _httpContext.Session["OrderPaymentInfo"] as ProcessPaymentRequest; if (paymentRequest == null) { @@ -526,92 +541,32 @@ public AmazonPayViewModel ProcessPluginRequest(AmazonPayRequestType type, TempDa if (shippingOption != null) model.ShippingMethod = shippingOption.Name; } - } - } - catch (OffAmazonPaymentsServiceException exc) - { - LogAmazonError(exc, notify: true); - } - catch (Exception exc) - { - LogError(exc, notify: true); - } - return model; - } - - private string GetAuthorizationState(AmazonPayClient client, string authorizationId) - { - try - { - if (authorizationId.HasValue()) - { - AmazonPayApiData data; - if (_api.GetAuthorizationDetails(client, authorizationId, out data) != null) - return data.State; + if (customer.BillingAddress != null) + { + model.BillingAddress.PrepareModel(customer.BillingAddress, false, _addressSettings); + } } } - catch (OffAmazonPaymentsServiceException exc) - { - LogAmazonError(exc); - } - catch (Exception exc) + catch (Exception exception) { - LogError(exc); + Logger.Error(exception); + _services.Notifier.Error(new LocalizedString(exception.Message)); } - return null; - } - - private void CloseOrderReference(AmazonPaySettings settings, Order order) - { - // You can still perform captures against any open authorizations, but you cannot create any new authorizations on the - // Order Reference object. You can still execute refunds against the Order Reference object. - - try - { - var client = new AmazonPayClient(settings); - - var orderAttribute = DeserializeOrderAttribute(order); - _api.CloseOrderReference(client, orderAttribute.OrderReferenceId); - } - catch (OffAmazonPaymentsServiceException exc) - { - LogAmazonError(exc); - } - catch (Exception exc) - { - LogError(exc); - } + return model; } - private void ProcessAuthorizationResult(AmazonPayClient client, Order order, AmazonPayApiData data, OffAmazonPaymentsService.Model.AuthorizationDetails details) + private void ProcessAuthorizationResult(AmazonPaySettings settings, Order order, AmazonPayData data) { - string formattedAddress; var orderAttribute = DeserializeOrderAttribute(order); - if (!orderAttribute.IsBillingAddressApplied) - { - if (_api.FulfillBillingAddress(client.Settings, order, details, out formattedAddress)) - { - AddOrderNote(client.Settings, order, AmazonPayOrderNote.BillingAddressApplied, formattedAddress); - - orderAttribute.IsBillingAddressApplied = true; - SerializeOrderAttribute(orderAttribute, order); - } - else if (formattedAddress.HasValue()) - { - AddOrderNote(client.Settings, order, AmazonPayOrderNote.BillingAddressCountryNotAllowed, formattedAddress); - - orderAttribute.IsBillingAddressApplied = true; - SerializeOrderAttribute(orderAttribute, order); - } - } - if (data.State.IsCaseInsensitiveEqual("Pending")) + { return; + } - string newResult = data.State.Grow(data.ReasonCode, " "); + var newResult = data.State.Grow(data.ReasonCode, " "); if (_orderProcessingService.CanMarkOrderAsAuthorized(order)) { @@ -620,7 +575,8 @@ private void ProcessAuthorizationResult(AmazonPayClient client, Order order, Ama if (data.State.IsCaseInsensitiveEqual("Closed") && data.ReasonCode.IsCaseInsensitiveEqual("OrderReferenceCanceled") && _orderProcessingService.CanVoidOffline(order)) { - _orderProcessingService.VoidOffline(order); // cancelation at amazon seller central + // Cancelation at amazon seller central. + _orderProcessingService.VoidOffline(order); } else if (data.State.IsCaseInsensitiveEqual("Declined") && _orderProcessingService.CanVoidOffline(order)) { @@ -632,31 +588,61 @@ private void ProcessAuthorizationResult(AmazonPayClient client, Order order, Ama order.AuthorizationTransactionResult = newResult; if (order.CaptureTransactionId.IsEmpty() && data.CaptureId.HasValue()) - order.CaptureTransactionId = data.CaptureId; // captured at amazon seller central + { + // Captured at amazon seller central. + order.CaptureTransactionId = data.CaptureId; + } _orderService.UpdateOrder(order); - AddOrderNote(client.Settings, order, AmazonPayOrderNote.AmazonMessageProcessed, _api.ToInfoString(data), true); + AddOrderNote(settings, order, ToInfoString(data), true); } } - private void ProcessCaptureResult(AmazonPayClient client, Order order, AmazonPayApiData data) + + private void ProcessCaptureResult(Client client, AmazonPaySettings settings, Order order, AmazonPayData data) { if (data.State.IsCaseInsensitiveEqual("Pending")) + { return; + } - string newResult = data.State.Grow(data.ReasonCode, " "); + var newResult = data.State.Grow(data.ReasonCode, " "); if (data.State.IsCaseInsensitiveEqual("Completed") && _orderProcessingService.CanMarkOrderAsPaid(order)) { _orderProcessingService.MarkOrderAsPaid(order); - CloseOrderReference(client.Settings, order); + // You can still perform captures against any open authorizations, but you cannot create any new authorizations on the + // Order Reference object. You can still execute refunds against the Order Reference object. + var orderAttribute = DeserializeOrderAttribute(order); + + var closeRequest = new CloseOrderReferenceRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonOrderReferenceId(orderAttribute.OrderReferenceId); + + var closeResponse = client.CloseOrderReference(closeRequest); + if (!closeResponse.GetSuccess()) + { + LogError(closeResponse, true); + } } else if (data.State.IsCaseInsensitiveEqual("Declined") && _orderProcessingService.CanVoidOffline(order)) { - if (!GetAuthorizationState(client, order.AuthorizationTransactionId).IsCaseInsensitiveEqual("Open")) + var authDetailsRequest = new GetAuthorizationDetailsRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonAuthorizationId(order.AuthorizationTransactionId); + + var authDetailsResponse = client.GetAuthorizationDetails(authDetailsRequest); + if (authDetailsResponse.GetSuccess()) + { + if (authDetailsResponse.GetAuthorizationState().IsCaseInsensitiveEqual("Open")) + { + _orderProcessingService.VoidOffline(order); + } + } + else { - _orderProcessingService.VoidOffline(order); + LogError(authDetailsResponse); } } @@ -665,20 +651,24 @@ private void ProcessCaptureResult(AmazonPayClient client, Order order, AmazonPay order.CaptureTransactionResult = newResult; _orderService.UpdateOrder(order); - AddOrderNote(client.Settings, order, AmazonPayOrderNote.AmazonMessageProcessed, _api.ToInfoString(data), true); + AddOrderNote(settings, order, ToInfoString(data), true); } } - private void ProcessRefundResult(AmazonPayClient client, Order order, AmazonPayApiData data) + + private void ProcessRefundResult(Client client, AmazonPaySettings settings, Order order, AmazonPayData data) { if (data.State.IsCaseInsensitiveEqual("Pending")) + { return; + } - if (data.RefundedAmount != null && data.RefundedAmount.Amount != 0.0) // totally refunded amount + if (data.RefundedAmount != null && data.RefundedAmount.Amount != decimal.Zero) { - // we could only process it once cause otherwise order.RefundedAmount would getting wrong. + // Totally refunded amount. + // We could only process it once cause otherwise order.RefundedAmount would getting wrong. if (order.RefundedAmount == decimal.Zero) { - decimal refundAmount = Convert.ToDecimal(data.RefundedAmount.Amount); + decimal refundAmount = data.RefundedAmount.Amount; decimal receivable = order.OrderTotal - refundAmount; if (receivable <= decimal.Zero) @@ -687,8 +677,10 @@ private void ProcessRefundResult(AmazonPayClient client, Order order, AmazonPayA { _orderProcessingService.RefundOffline(order); - if (client.Settings.DataFetching == AmazonPayDataFetchingType.Polling) - AddOrderNote(client.Settings, order, AmazonPayOrderNote.AmazonMessageProcessed, _api.ToInfoString(data), true); + if (settings.DataFetching == AmazonPayDataFetchingType.Polling) + { + AddOrderNote(settings, order, ToInfoString(data), true); + } } } else @@ -697,15 +689,19 @@ private void ProcessRefundResult(AmazonPayClient client, Order order, AmazonPayA { _orderProcessingService.PartiallyRefundOffline(order, refundAmount); - if (client.Settings.DataFetching == AmazonPayDataFetchingType.Polling) - AddOrderNote(client.Settings, order, AmazonPayOrderNote.AmazonMessageProcessed, _api.ToInfoString(data), true); + if (settings.DataFetching == AmazonPayDataFetchingType.Polling) + { + AddOrderNote(settings, order, ToInfoString(data), true); + } } } } } - if (client.Settings.DataFetching == AmazonPayDataFetchingType.Ipn) - AddOrderNote(client.Settings, order, AmazonPayOrderNote.AmazonMessageProcessed, _api.ToInfoString(data), true); + if (settings.DataFetching == AmazonPayDataFetchingType.Ipn) + { + AddOrderNote(settings, order, ToInfoString(data), true); + } } private void PollingLoop(PollingLoopData data, Func poll) @@ -718,14 +714,14 @@ private void PollingLoop(PollingLoopData data, Func poll) for (int i = 0; i < 99 && (DateTime.Now.TimeOfDay.Milliseconds - startTime.Milliseconds) <= loopMillSec; ++i) { - // inside the loop cause other instances are also updating the order + // Inside the loop cause other instances are also updating the order. data.Order = _orderService.GetOrderById(data.OrderId); if (data.Settings == null) data.Settings = _services.Settings.LoadSetting(data.Order.StoreId); if (data.Client == null) - data.Client = new AmazonPayClient(data.Settings); + data.Client = CreateClient(data.Settings); if (!poll()) break; @@ -733,20 +729,14 @@ private void PollingLoop(PollingLoopData data, Func poll) Thread.Sleep(sleepMillSec); } } - catch (OffAmazonPaymentsServiceException exc) - { - LogAmazonError(exc); - } - catch (Exception exc) + catch (Exception exception) { - LogError(exc); + Logger.Error(exception); } } private void EarlyPolling(int orderId, AmazonPaySettings settings) { - // the Authorization object moves to the Open state after remaining in the Pending state for 30 seconds. - - AmazonPayApiData data; + // The Authorization object moves to the Open state after remaining in the Pending state for 30 seconds. var d = new PollingLoopData(orderId); d.Settings = settings; @@ -755,13 +745,21 @@ private void EarlyPolling(int orderId, AmazonPaySettings settings) if (d.Order.AuthorizationTransactionId.IsEmpty()) return false; - var details = _api.GetAuthorizationDetails(d.Client, d.Order.AuthorizationTransactionId, out data); + var authDetailsRequest = new GetAuthorizationDetailsRequest() + .WithMerchantId(d.Settings.SellerId) + .WithAmazonAuthorizationId(d.Order.AuthorizationTransactionId); + + var authDetailsResponse = d.Client.GetAuthorizationDetails(authDetailsRequest); + if (!authDetailsResponse.GetSuccess()) + return false; - if (!data.State.IsCaseInsensitiveEqual("pending")) + var details = GetDetails(authDetailsResponse); + if (!details.State.IsCaseInsensitiveEqual("pending")) { - ProcessAuthorizationResult(d.Client, d.Order, data, details); + ProcessAuthorizationResult(d.Settings, d.Order, details); return false; } + return true; }); @@ -771,50 +769,21 @@ private void EarlyPolling(int orderId, AmazonPaySettings settings) if (d.Order.CaptureTransactionId.IsEmpty()) return false; - _api.GetCaptureDetails(d.Client, d.Order.CaptureTransactionId, out data); + var captureDetailsRequest = new GetCaptureDetailsRequest() + .WithMerchantId(d.Settings.SellerId) + .WithAmazonCaptureId(d.Order.CaptureTransactionId); - ProcessCaptureResult(d.Client, d.Order, data); + var captureDetailsResponse = d.Client.GetCaptureDetails(captureDetailsRequest); + if (!captureDetailsResponse.GetSuccess()) + return false; + + var details = GetDetails(captureDetailsResponse); + ProcessCaptureResult(d.Client, d.Settings, d.Order, details); - return data.State.IsCaseInsensitiveEqual("pending"); + return details.State.IsCaseInsensitiveEqual("pending"); }); } - private Order FindOrder(AmazonPayApiData data) - { - Order order = null; - string errorId = null; - - if (data.MessageType.IsCaseInsensitiveEqual("AuthorizationNotification")) - { - if ((order = _orderService.GetOrderByPaymentAuthorization(AmazonPayCore.SystemName, data.AuthorizationId)) == null) - errorId = "AuthorizationId {0}".FormatWith(data.AuthorizationId); - } - else if (data.MessageType.IsCaseInsensitiveEqual("CaptureNotification")) - { - if ((order = _orderService.GetOrderByPaymentCapture(AmazonPayCore.SystemName, data.CaptureId)) == null) - order = _orderRepository.GetOrderByAmazonId(data.AnyAmazonId); - - if (order == null) - errorId = "CaptureId {0}".FormatWith(data.CaptureId); - } - else if (data.MessageType.IsCaseInsensitiveEqual("RefundNotification")) - { - var attribute = _genericAttributeService.GetAttributes(AmazonPayCore.AmazonPayRefundIdKey, "Order") - .Where(x => x.Value == data.RefundId) - .FirstOrDefault(); - - if (attribute == null || (order = _orderService.GetOrderById(attribute.EntityId)) == null) - order = _orderRepository.GetOrderByAmazonId(data.AnyAmazonId); - - if (order == null) - errorId = "RefundId {0}".FormatWith(data.RefundId); - } - - if (errorId.HasValue()) - Logger.Warn(T("Plugins.Payments.AmazonPay.OrderNotFound", errorId)); - - return order; - } public void AddCustomerOrderNoteLoop(AmazonPayActionState state) { try @@ -823,7 +792,7 @@ public void AddCustomerOrderNoteLoop(AmazonPayActionState state) var loopMillSec = 40000; var startTime = DateTime.Now.TimeOfDay; - for (int i = 0; i < 99 && (DateTime.Now.TimeOfDay.Milliseconds - startTime.Milliseconds) <= loopMillSec; ++i) + for (var i = 0; i < 99 && (DateTime.Now.TimeOfDay.Milliseconds - startTime.Milliseconds) <= loopMillSec; ++i) { var order = _orderService.GetOrderByGuid(state.OrderGuid); if (order != null) @@ -833,10 +802,12 @@ public void AddCustomerOrderNoteLoop(AmazonPayActionState state) if (state.Errors != null) { foreach (var error in state.Errors) + { sb.AppendFormat("

                              {0}

                              ", error); + } } - var orderNote = new OrderNote() + var orderNote = new OrderNote { DisplayToCustomer = true, Note = sb.ToString(), @@ -845,82 +816,192 @@ public void AddCustomerOrderNoteLoop(AmazonPayActionState state) order.OrderNotes.Add(orderNote); _orderService.UpdateOrder(order); - - _workflowMessageService.SendNewOrderNoteAddedCustomerNotification(orderNote, _services.WorkContext.WorkingLanguage.Id); + + _services.MessageFactory.SendNewOrderNoteAddedCustomerNotification(orderNote, _services.WorkContext.WorkingLanguage.Id); break; } Thread.Sleep(sleepMillSec); } } - catch (Exception exc) + catch (Exception exception) + { + Logger.Error(exception); + } + } + + public void GetBillingAddress() + { + var store = _services.StoreContext.CurrentStore; + var customer = _services.WorkContext.CurrentCustomer; + var settings = _services.Settings.LoadSetting(store.Id); + var state = _httpContext.GetAmazonPayState(_services.Localization); + var client = CreateClient(settings); + + var getOrderRequest = new GetOrderReferenceDetailsRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonOrderReferenceId(state.OrderReferenceId) + .WithAccessToken(state.AccessToken); + + var getOrderResponse = client.GetOrderReferenceDetails(getOrderRequest); + if (getOrderResponse.GetSuccess()) + { + var details = getOrderResponse.GetBillingAddressDetails(); + if (details != null) + { + var countryAllowsShipping = true; + var countryAllowsBilling = true; + var email = getOrderResponse.GetEmail(); + + var address = CreateAddress( + email, + details.GetName(), + details.GetAddressLine1(), + details.GetAddressLine2(), + details.GetAddressLine3(), + details.GetCity(), + details.GetPostalCode(), + details.GetPhone(), + details.GetCountryCode(), + details.GetStateOrRegion(), + details.GetCounty(), + details.GetDistrict(), + out countryAllowsShipping, + out countryAllowsBilling); + + // We must ignore countryAllowsBilling because the customer cannot choose another billing address in Amazon checkout. + //if (!countryAllowsBilling) + // return false; + + var existingAddress = customer.Addresses.ToList().FindAddress(address, true); + if (existingAddress == null) + { + customer.Addresses.Add(address); + customer.BillingAddress = address; + } + else + { + customer.BillingAddress = existingAddress; + } + + if (settings.CanSaveEmailAndPhone(customer.Email)) + { + customer.Email = email; + } + _customerService.UpdateCustomer(customer); + + if (settings.CanSaveEmailAndPhone(customer.GetAttribute(SystemCustomerAttributeNames.Phone, store.Id))) + { + var phone = details.GetPhone(); + if (phone.IsEmpty()) + { + phone = getOrderResponse.GetPhone(); + } + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.Phone, phone); + } + } + else + { + Logger.Error(new Exception(getOrderResponse.GetJson()), T("Plugins.Payments.AmazonPay.MissingBillingAddress")); + } + } + else { - LogError(exc); + LogError(getOrderResponse); } } public PreProcessPaymentResult PreProcessPayment(ProcessPaymentRequest request) { - // fulfill the Amazon checkout + // Fulfill the Amazon checkout. var result = new PreProcessPaymentResult(); try { - var orderGuid = request.OrderGuid.ToString(); var store = _services.StoreService.GetStoreById(request.StoreId); - var customer = _customerService.GetCustomerById(request.CustomerId); - var currency = store.PrimaryStoreCurrency; - var settings = _services.Settings.LoadSetting(store.Id); - var state = _httpContext.GetAmazonPayState(_services.Localization); - var client = new AmazonPayClient(settings); - if (!IsActive(store.Id, true)) + if (!IsPaymentMethodActive(store.Id, true)) { - //_httpContext.ResetCheckoutState(); - result.AddError(T("Plugins.Payments.AmazonPay.PaymentMethodNotActive", store.Name)); return result; } - var preConfirmDetails = _api.SetOrderReferenceDetails(client, state.OrderReferenceId, request.OrderTotal, currency.CurrencyCode, orderGuid, store.Name); - - _api.GetConstraints(preConfirmDetails, result.Errors); - - if (!result.Success) - return result; - - _api.ConfirmOrderReference(client, state.OrderReferenceId); - - // address and payment cannot be changed if order is in open state, amazon widgets then might show an error. - //state.IsOrderConfirmed = true; + var orderGuid = request.OrderGuid.ToString(); + var customer = _customerService.GetCustomerById(request.CustomerId); + var settings = _services.Settings.LoadSetting(store.Id); + var state = _httpContext.GetAmazonPayState(_services.Localization); + var client = CreateClient(settings); - var cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, store.Id); - var isShippable = cart.RequiresShipping(); + var failedPaymentReason = _httpContext.Session["AmazonPayFailedPaymentReason"] as string; + if (!failedPaymentReason.IsCaseInsensitiveEqual("InvalidPaymentMethod")) + { + var setOrderRequest = new SetOrderReferenceDetailsRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonOrderReferenceId(state.OrderReferenceId) + .WithPlatformId(PlatformId) + .WithAmount(request.OrderTotal) + .WithCurrencyCode(ConvertCurrency(store.PrimaryStoreCurrency.CurrencyCode)) + .WithSellerOrderId(orderGuid) + .WithStoreName(store.Name); + + // See https://pay.amazon.com/de/developer/documentation/lpwa/201956480 + //{"SandboxSimulation":{"Constraint":"PaymentMethodNotAllowed"}} + //if (settings.UseSandbox) + //{ + // var orderReferenceNote = _services.Settings.GetSettingByKey("SmartStore.AmazonPay.SellerOrderReferenceNote"); + // if (orderReferenceNote.HasValue()) + // { + // setOrderRequest = setOrderRequest.WithSellerNote(orderReferenceNote); + // } + //} + + var setOrderResponse = client.SetOrderReferenceDetails(setOrderRequest); + if (setOrderResponse.GetSuccess()) + { + if (setOrderResponse.GetHasConstraint()) + { + var ids = setOrderResponse.GetConstraintIdList(); + var descriptions = setOrderResponse.GetDescriptionList(); - // note: billing address is only available after authorization is in a non-pending and non-declined state. - var details = _api.GetOrderReferenceDetails(client, state.OrderReferenceId); + foreach (var id in ids) + { + var idx = ids.IndexOf(id); + if (idx < descriptions.Count) + { + result.Errors.Add($"{descriptions[idx]} ({id})"); + } - _api.FindAndApplyAddress(details, customer, isShippable, false); + if (id.IsCaseInsensitiveEqual("PaymentMethodNotAllowed")) + { + // Must be redirected to checkout payment page. + _httpContext.Session["AmazonPayFailedPaymentReason"] = id; + _httpContext.Response.RedirectToRoute(new { Controller = "Checkout", Action = "PaymentMethod", Area = "" }); + } + } + } + } + else + { + var message = LogError(setOrderResponse); + result.AddError(message); + } - if (details.IsSetBuyer() && details.Buyer.IsSetEmail() && settings.CanSaveEmailAndPhone(customer.Email)) - { - customer.Email = details.Buyer.Email; + if (!result.Success) + { + return result; + } } - _customerService.UpdateCustomer(customer); + var confirmRequest = new ConfirmOrderReferenceRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonOrderReferenceId(state.OrderReferenceId); - if (details.IsSetBuyer() && details.Buyer.IsSetPhone() && settings.CanSaveEmailAndPhone(customer.GetAttribute(SystemCustomerAttributeNames.Phone, store.Id))) - { - _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.Phone, details.Buyer.Phone); - } + client.ConfirmOrderReference(confirmRequest); } - catch (OffAmazonPaymentsServiceException exc) + catch (Exception exception) { - LogAmazonError(exc, errors: result.Errors); - } - catch (Exception exc) - { - LogError(exc, errors: result.Errors); + Logger.Error(exception); + result.AddError(exception.Message); } return result; @@ -928,46 +1009,145 @@ public PreProcessPaymentResult PreProcessPayment(ProcessPaymentRequest request) public ProcessPaymentResult ProcessPayment(ProcessPaymentRequest request) { - // initiate Amazon payment. We do not add errors to request.Errors cause of asynchronous processing. var result = new ProcessPaymentResult(); - var errors = new List(); - bool informCustomerAboutErrors = false; - bool informCustomerAddErrors = false; + var orderNoteErrors = new List(); + var informCustomerAboutErrors = false; + var informCustomerAddErrors = false; + var isSynchronous = false; + string error = null; + + result.NewPaymentStatus = PaymentStatus.Pending; + + _httpContext.Session.SafeRemove("AmazonPayFailedPaymentReason"); + _httpContext.Session.SafeRemove("AmazonPayCheckoutCompletedNote"); try { - var orderGuid = request.OrderGuid.ToString(); var store = _services.StoreService.GetStoreById(request.StoreId); - var currency = store.PrimaryStoreCurrency; var settings = _services.Settings.LoadSetting(store.Id); + var captureNow = settings.TransactionType == AmazonPayTransactionType.AuthorizeAndCapture; var state = _httpContext.GetAmazonPayState(_services.Localization); - var client = new AmazonPayClient(settings); + var client = CreateClient(settings); + AuthorizeResponse authResponse = null; informCustomerAboutErrors = settings.InformCustomerAboutErrors; informCustomerAddErrors = settings.InformCustomerAddErrors; - _api.Authorize(client, result, errors, state.OrderReferenceId, request.OrderTotal, currency.CurrencyCode, orderGuid); + // Authorize. + if (settings.AuthorizeMethod == AmazonPayAuthorizeMethod.Omnichronous) + { + // First try synchronously. + authResponse = AuthorizePayment(settings, state, store, request, client, true); + + if (authResponse.GetAuthorizationState().IsCaseInsensitiveEqual("Declined") && + authResponse.GetReasonCode().IsCaseInsensitiveEqual("TransactionTimedOut")) + { + // Second try asynchronously. + // Transaction is always in pending state after return. + authResponse = AuthorizePayment(settings, state, store, request, client, false); + } + else + { + isSynchronous = true; + } + } + else + { + isSynchronous = settings.AuthorizeMethod == AmazonPayAuthorizeMethod.Synchronous; + authResponse = AuthorizePayment(settings, state, store, request, client, isSynchronous); + } + + // Process authorization response. + if (authResponse.GetSuccess()) + { + var reason = authResponse.GetReasonCode(); + + result.AuthorizationTransactionId = authResponse.GetAuthorizationId(); + result.AuthorizationTransactionCode = authResponse.GetAuthorizationReferenceId(); + result.AuthorizationTransactionResult = authResponse.GetAuthorizationState(); + + if (captureNow) + { + var idList = authResponse.GetCaptureIdList(); + if (idList.Any()) + { + result.CaptureTransactionId = idList.First(); + } + } + + if (isSynchronous) + { + if (result.AuthorizationTransactionResult.IsCaseInsensitiveEqual("Open")) + { + result.NewPaymentStatus = PaymentStatus.Authorized; + } + else if (result.AuthorizationTransactionResult.IsCaseInsensitiveEqual("Closed")) + { + if (captureNow && reason.IsCaseInsensitiveEqual("MaxCapturesProcessed")) + { + result.NewPaymentStatus = PaymentStatus.Paid; + } + } + } + else + { + _httpContext.Session["AmazonPayCheckoutCompletedNote"] = T("Plugins.Payments.AmazonPay.AsyncPaymentAuthrizationNote").Text; + } + + if (reason.IsCaseInsensitiveEqual("InvalidPaymentMethod") || reason.IsCaseInsensitiveEqual("AmazonRejected") || + reason.IsCaseInsensitiveEqual("ProcessingFailure") || reason.IsCaseInsensitiveEqual("TransactionTimedOut") || + reason.IsCaseInsensitiveEqual("TransactionTimeout")) + { + error = authResponse.GetReasonDescription(); + error = error.HasValue() ? $"{reason}: {error}" : reason; + + if (reason.IsCaseInsensitiveEqual("AmazonRejected")) + { + // Must be logged out and redirected to shopping cart. + _httpContext.Session["AmazonPayFailedPaymentReason"] = reason; + _httpContext.Response.RedirectToRoute("ShoppingCart"); + } + else if (reason.IsCaseInsensitiveEqual("InvalidPaymentMethod")) + { + // Must be redirected to checkout payment page. + _httpContext.Session["AmazonPayFailedPaymentReason"] = reason; + _httpContext.Response.RedirectToRoute(new { Controller = "Checkout", Action = "PaymentMethod", Area = "" }); + } + } + } + else + { + error = LogError(authResponse); + } } - catch (OffAmazonPaymentsServiceException exc) + catch (Exception exception) { - LogAmazonError(exc, errors: errors); + Logger.Error(exception); + error = exception.Message; } - catch (Exception exc) + + if (error.HasValue()) { - LogError(exc, errors: errors); + if (isSynchronous) + { + result.AddError(error); + } + else + { + orderNoteErrors.Add(error); + } } - if (informCustomerAboutErrors && errors != null && errors.Count > 0) + // Customer needs to be informed of an Amazon error here. Hooking OrderPlaced.CustomerNotification won't work + // cause of asynchronous processing. Solution: we add a customer order note that is also send as an email. + if (informCustomerAboutErrors && orderNoteErrors.Any()) { - // customer needs to be informed of an amazon error here. hooking OrderPlaced.CustomerNotification won't work - // cause of asynchronous processing. solution: we add a customer order note that is also send as an email. - - var state = new AmazonPayActionState() { OrderGuid = request.OrderGuid }; + var state = new AmazonPayActionState { OrderGuid = request.OrderGuid }; if (informCustomerAddErrors) { state.Errors = new List(); - state.Errors.AddRange(errors); + state.Errors.AddRange(orderNoteErrors); } AsyncRunner.Run((container, ct, o) => @@ -999,29 +1179,25 @@ public void PostProcessPayment(PostProcessPaymentRequest request) // settings, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); // } //} - //catch (OffAmazonPaymentsServiceException exc) - //{ - // LogAmazonError(exc); - //} //catch (Exception exc) //{ - // LogError(exc); + // Logger.Error(exc); //} try { var state = _httpContext.GetAmazonPayState(_services.Localization); - var orderAttribute = new AmazonPayOrderAttribute() + var orderAttribute = new AmazonPayOrderAttribute { OrderReferenceId = state.OrderReferenceId }; SerializeOrderAttribute(orderAttribute, request.Order); } - catch (Exception exc) + catch (Exception exception) { - LogError(exc); + Logger.Error(exception); } } @@ -1035,24 +1211,47 @@ public CapturePaymentResult Capture(CapturePaymentRequest request) try { var settings = _services.Settings.LoadSetting(request.Order.StoreId); - var client = new AmazonPayClient(settings); + var store = _services.StoreService.GetStoreById(request.Order.StoreId); + var client = CreateClient(settings); + + var captureRequest = new CaptureRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonAuthorizationId(request.Order.AuthorizationTransactionId) + .WithCaptureReferenceId(GetRandomId("Capture")) + .WithCurrencyCode(ConvertCurrency(store.PrimaryStoreCurrency.CurrencyCode)) + .WithAmount(request.Order.OrderTotal); + + var captureResponse = client.Capture(captureRequest); + if (captureResponse.GetSuccess()) + { + var state = captureResponse.GetCaptureState(); - _api.Capture(client, request, result); - } - catch (OffAmazonPaymentsServiceException exc) - { - LogAmazonError(exc, errors: result.Errors); + result.CaptureTransactionId = captureResponse.GetCaptureId(); + result.CaptureTransactionResult = state.Grow(captureResponse.GetReasonCode(), " "); + + if (state.IsCaseInsensitiveEqual("completed")) + { + result.NewPaymentStatus = PaymentStatus.Paid; + } + } + else + { + var message = LogError(captureResponse); + result.AddError(message); + } } - catch (Exception exc) + catch (Exception exception) { - LogError(exc, errors: result.Errors); + Logger.Error(exception); + result.AddError(exception.Message); } + return result; } public RefundPaymentResult Refund(RefundPaymentRequest request) { - var result = new RefundPaymentResult() + var result = new RefundPaymentResult { NewPaymentStatus = request.Order.PaymentStatus }; @@ -1060,36 +1259,52 @@ public RefundPaymentResult Refund(RefundPaymentRequest request) try { var settings = _services.Settings.LoadSetting(request.Order.StoreId); - var client = new AmazonPayClient(settings); - - string amazonRefundId = _api.Refund(client, request, result); - - if (amazonRefundId.HasValue() && request.Order.Id != 0) + var store = _services.StoreService.GetStoreById(request.Order.StoreId); + var client = CreateClient(settings); + + var refundRequest = new RefundRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonCaptureId(request.Order.CaptureTransactionId) + .WithRefundReferenceId(GetRandomId("Refund")) + .WithCurrencyCode(ConvertCurrency(store.PrimaryStoreCurrency.CurrencyCode)) + .WithAmount(request.AmountToRefund); + + var refundResponse = client.Refund(refundRequest); + if (refundResponse.GetSuccess()) { - _genericAttributeService.InsertAttribute(new GenericAttribute() + result.NewPaymentStatus = request.IsPartialRefund ? PaymentStatus.PartiallyRefunded : PaymentStatus.Refunded; + + var refundId = refundResponse.GetAmazonRefundId(); + if (refundId.HasValue() && request.Order.Id != 0) { - EntityId = request.Order.Id, - KeyGroup = "Order", - Key = AmazonPayCore.AmazonPayRefundIdKey, - Value = amazonRefundId, - StoreId = request.Order.StoreId - }); + _genericAttributeService.InsertAttribute(new GenericAttribute + { + EntityId = request.Order.Id, + KeyGroup = "Order", + Key = AmazonPayPlugin.SystemName + ".RefundId", + Value = refundId, + StoreId = request.Order.StoreId + }); + } + } + else + { + var message = LogError(refundResponse); + result.AddError(message); } } - catch (OffAmazonPaymentsServiceException exc) - { - LogAmazonError(exc, errors: result.Errors); - } - catch (Exception exc) + catch (Exception exception) { - LogError(exc, errors: result.Errors); + Logger.Error(exception); + result.AddError(exception.Message); } + return result; } public VoidPaymentResult Void(VoidPaymentRequest request) { - var result = new VoidPaymentResult() + var result = new VoidPaymentResult { NewPaymentStatus = request.Order.PaymentStatus }; @@ -1115,21 +1330,31 @@ public VoidPaymentResult Void(VoidPaymentRequest request) if (request.Order.PaymentStatus == PaymentStatus.Pending || request.Order.PaymentStatus == PaymentStatus.Authorized) { var settings = _services.Settings.LoadSetting(request.Order.StoreId); - var client = new AmazonPayClient(settings); - var orderAttribute = DeserializeOrderAttribute(request.Order); + var client = CreateClient(settings); - _api.CancelOrderReference(client, orderAttribute.OrderReferenceId); + var cancelRequest = new CancelOrderReferenceRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonOrderReferenceId(orderAttribute.OrderReferenceId); + + var cancelResponse = client.CancelOrderReference(cancelRequest); + if (cancelResponse.GetSuccess()) + { + result.NewPaymentStatus = PaymentStatus.Voided; + } + else + { + var message = LogError(cancelResponse); + result.AddError(message); + } } } - catch (OffAmazonPaymentsServiceException exc) - { - LogAmazonError(exc, errors: result.Errors); - } - catch (Exception exc) + catch (Exception exception) { - LogError(exc, errors: result.Errors); + Logger.Error(exception); + result.AddError(exception.Message); } + return result; } @@ -1137,77 +1362,161 @@ public void ProcessIpn(HttpRequestBase request) { try { - var data = _api.ParseNotification(request); - var order = FindOrder(data); - - if (order == null || !IsActive(order.StoreId)) - return; + string json = null; + using (var reader = new StreamReader(request.InputStream)) + { + json = reader.ReadToEnd(); + } - var client = new AmazonPayClient(_services.Settings.LoadSetting(order.StoreId)); + var parser = new IpnHandler(request.Headers, json); + var type = parser.GetNotificationType(); + AmazonPayData data = null; + Order order = null; + string errorId = null; + var isAuthorize = false; + var isCapture = false; + var isRefund = false; - if (client.Settings.DataFetching != AmazonPayDataFetchingType.Ipn) + if (type.IsCaseInsensitiveEqual("PaymentAuthorize")) + { + isAuthorize = true; + var response = parser.GetAuthorizeResponse(); + data = GetDetails(response); + } + else if (type.IsCaseInsensitiveEqual("PaymentCapture")) + { + isCapture = true; + var response = parser.GetCaptureResponse(); + data = GetDetails(response); + } + else if (type.IsCaseInsensitiveEqual("PaymentRefund")) + { + isRefund = true; + var response = parser.GetRefundResponse(); + data = GetDetails(response); + } + else + { + // Ignore, e.g. OrderReferenceNotification. return; + } - if (data.MessageType.IsCaseInsensitiveEqual("AuthorizationNotification")) + if (data == null) { - ProcessAuthorizationResult(client, order, data, null); + Logger.Error($"No IPN details for notification type {type}"); return; } - else if (data.MessageType.IsCaseInsensitiveEqual("CaptureNotification")) + + data.MessageType = type; + data.MessageId = parser.GetNotificationReferenceId(); + + // Find order. + if (isAuthorize) + { + if ((order = _orderService.GetOrderByPaymentAuthorization(AmazonPayPlugin.SystemName, data.AuthorizationId)) == null) + errorId = $"AuthorizationId {data.AuthorizationId.NaIfEmpty()}"; + } + else if (isCapture) + { + if ((order = _orderService.GetOrderByPaymentCapture(AmazonPayPlugin.SystemName, data.CaptureId)) == null) + order = _orderRepository.GetOrderByAmazonId(data.AnyAmazonId); + + if (order == null) + errorId = $"CaptureId {data.CaptureId.NaIfEmpty()}"; + } + else if (isRefund) + { + var attribute = _genericAttributeService.GetAttributes(AmazonPayPlugin.SystemName + ".RefundId", "Order") + .Where(x => x.Value == data.RefundId) + .FirstOrDefault(); + + if (attribute == null || (order = _orderService.GetOrderById(attribute.EntityId)) == null) + order = _orderRepository.GetOrderByAmazonId(data.AnyAmazonId); + + if (order == null) + errorId = $"RefundId {data.RefundId.NaIfEmpty()}"; + } + + if (errorId.HasValue()) + { + Logger.Warn(T("Plugins.Payments.AmazonPay.OrderNotFound", errorId)); + } + + if (order == null || !IsPaymentMethodActive(order.StoreId)) { - ProcessCaptureResult(client, order, data); return; } - else if (data.MessageType.IsCaseInsensitiveEqual("RefundNotification")) + + var settings = _services.Settings.LoadSetting(order.StoreId); + if (settings.DataFetching != AmazonPayDataFetchingType.Ipn) { - ProcessRefundResult(client, order, data); return; } + + if (isAuthorize) + { + ProcessAuthorizationResult(settings, order, data); + } + else if (isCapture) + { + var client = CreateClient(settings); + ProcessCaptureResult(client, settings, order, data); + } + else if (isRefund) + { + var client = CreateClient(settings); + ProcessRefundResult(client, settings, order, data); + } } - catch (OffAmazonPaymentsServiceException exc) - { - LogAmazonError(exc); - } - catch (Exception exc) + catch (Exception exception) { - LogError(exc); + Logger.Error(exception); } } - public void DataPollingTaskProcess() + public void StartDataPolling() { try { - // ignore cancelled and completed (paid and shipped) orders. ignore old orders too. - - var data = new AmazonPayApiData(); - int pollingMaxOrderCreationDays = _services.Settings.GetSettingByKey("AmazonPaySettings.PollingMaxOrderCreationDays", 31); + // Ignore cancelled and completed (paid and shipped) orders. ignore old orders too. + var pollingMaxOrderCreationDays = _services.Settings.GetSettingByKey("AmazonPaySettings.PollingMaxOrderCreationDays", 31); var isTooOld = DateTime.UtcNow.AddDays(-(pollingMaxOrderCreationDays)); var query = from x in _orderRepository.Table - where x.PaymentMethodSystemName == AmazonPayCore.SystemName && x.CreatedOnUtc > isTooOld && + where x.PaymentMethodSystemName == AmazonPayPlugin.SystemName && x.CreatedOnUtc > isTooOld && !x.Deleted && x.OrderStatusId < (int)OrderStatus.Complete && x.PaymentStatusId != (int)PaymentStatus.Voided orderby x.Id descending select x; var orders = query.ToList(); - //"- start polling {0} orders".FormatWith(orders.Count).Dump(); foreach (var order in orders) { try { - var client = new AmazonPayClient(_services.Settings.LoadSetting(order.StoreId)); - - if (client.Settings.DataFetching == AmazonPayDataFetchingType.Polling) + var settings = _services.Settings.LoadSetting(order.StoreId); + if (settings.DataFetching == AmazonPayDataFetchingType.Polling) { + var client = CreateClient(settings); + if (order.AuthorizationTransactionId.HasValue()) { - var details = _api.GetAuthorizationDetails(client, order.AuthorizationTransactionId, out data); + var authDetailsRequest = new GetAuthorizationDetailsRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonAuthorizationId(order.AuthorizationTransactionId); - ProcessAuthorizationResult(client, order, data, details); + var authDetailsResponse = client.GetAuthorizationDetails(authDetailsRequest); + if (authDetailsResponse.GetSuccess()) + { + var details = GetDetails(authDetailsResponse); + ProcessAuthorizationResult(settings, order, details); + } + else + { + LogError(authDetailsResponse); + } } if (order.CaptureTransactionId.HasValue()) @@ -1215,65 +1524,143 @@ orderby x.Id descending if (_orderProcessingService.CanMarkOrderAsPaid(order) || _orderProcessingService.CanVoidOffline(order) || _orderProcessingService.CanRefundOffline(order) || _orderProcessingService.CanPartiallyRefundOffline(order, 0.01M)) { - var details = _api.GetCaptureDetails(client, order.CaptureTransactionId, out data); + var captureDetailsRequest = new GetCaptureDetailsRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonCaptureId(order.CaptureTransactionId); - ProcessCaptureResult(client, order, data); - - if (_orderProcessingService.CanRefundOffline(order) || _orderProcessingService.CanPartiallyRefundOffline(order, 0.01M)) + var captureDetailsResponse = client.GetCaptureDetails(captureDetailsRequest); + if (captureDetailsResponse.GetSuccess()) { - // note status polling: we cannot use GetRefundDetails to reflect refund(s) made at Amazon seller central cause we - // do not have any refund-id and there is no api endpoint that serves them. so we only can process CaptureDetails.RefundedAmount. - - ProcessRefundResult(client, order, data); + var details = GetDetails(captureDetailsResponse); + ProcessCaptureResult(client, settings, order, details); + + if (_orderProcessingService.CanRefundOffline(order) || _orderProcessingService.CanPartiallyRefundOffline(order, 0.01M)) + { + // Note status polling: we cannot use GetRefundDetails to reflect refund(s) made at Amazon seller central cause we + // do not have any refund-id and there is no api endpoint that provide them. So we only can process CaptureDetails.RefundedAmount. + ProcessRefundResult(client, settings, order, details); + } + } + else + { + LogError(captureDetailsResponse); } } } } } - catch (OffAmazonPaymentsServiceException exc) + catch (Exception exception) { - LogAmazonError(exc); - } - catch (Exception exc) - { - LogError(exc); + Logger.Error(exception); } } } - catch (OffAmazonPaymentsServiceException exc) - { - LogAmazonError(exc); - } - catch (Exception exc) + catch (Exception exception) { - LogError(exc); + Logger.Error(exception); } } - public void DataPollingTaskInit() + public void ShareKeys(string payload, int storeId) { - _scheduleTaskService.GetOrAddTask(x => + if (payload.IsEmpty()) { - x.Name = "{0} data polling".FormatWith(AmazonPayCore.SystemName); - x.CronExpression = "*/30 * * * *"; // Every 30 minutes - }); - } + throw new SmartException(T("Plugins.Payments.AmazonPay.MissingPayloadParameter")); + } - public void DataPollingTaskUpdate(bool enabled, int seconds) - { - var task = _scheduleTaskService.GetTaskByType(); - if (task != null) + dynamic json = JObject.Parse(payload); + var settings = _services.Settings.LoadSetting(storeId); + + var encryptedPayload = (string)json.encryptedPayload; + if (encryptedPayload.HasValue()) { - task.Enabled = enabled; - //task.Seconds = seconds; + throw new SmartException(T("Plugins.Payments.AmazonPay.EncryptionNotSupported")); + } + else + { + settings.SellerId = (string)json.merchant_id; + settings.AccessKey = (string)json.access_key; + settings.SecretKey = (string)json.secret_key; + settings.ClientId = (string)json.client_id; + //settings.ClientSecret = (string)json.client_secret; + } - _scheduleTaskService.UpdateTask(task); + using (_services.Settings.BeginScope()) + { + _services.Settings.SaveSetting(settings, x => x.SellerId, storeId, false); + _services.Settings.SaveSetting(settings, x => x.AccessKey, storeId, false); + _services.Settings.SaveSetting(settings, x => x.SecretKey, storeId, false); + _services.Settings.SaveSetting(settings, x => x.ClientId, storeId, false); } } - public void DataPollingTaskDelete() + #region IExternalProviderAuthorizer + + public AuthorizeState Authorize(string returnUrl, bool? verifyResponse = null) { - _scheduleTaskService.TryDeleteTask(); + string error = null; + string email = null; + string name = null; + string userId = null; + var accessToken = _httpContext.Request.QueryString["access_token"]; + + if (accessToken.HasValue()) + { + var settings = _services.Settings.LoadSetting(); + var client = CreateClient(settings); + var jsonString = client.GetUserInfo(accessToken); + if (jsonString.HasValue()) + { + var json = JObject.Parse(jsonString); + + email = json.GetValue("email").ToString(); + name = json.GetValue("name").ToString(); + userId = json.GetValue("user_id").ToString(); + + if (email.IsEmpty() || name.IsEmpty() || userId.IsEmpty()) + { + error = T("Plugins.Payments.AmazonPay.IncompleteProfileDetails") + + $" Email: {email.NaIfEmpty()}, name: {name.NaIfEmpty()}, userId: {userId.NaIfEmpty()}."; + } + } + else + { + error = T("Plugins.Payments.AmazonPay.IncompleteProfileDetails"); + } + } + else + { + error = T("Plugins.Payments.AmazonPay.MissingAccessToken"); + } + + if (error.HasValue()) + { + var state = new AuthorizeState("", OpenAuthenticationStatus.Error); + state.AddError(error); + Logger.Error(error); + return state; + } + + string firstName, lastName; + name.ToFirstAndLastName(out firstName, out lastName); + + var claims = new UserClaims(); + claims.Name = new NameClaims(); + claims.Contact = new ContactClaims(); + claims.Contact.Email = email; + claims.Name.FullName = name; + claims.Name.First = firstName; + claims.Name.Last = lastName; + + var parameters = new AmazonAuthenticationParameters(); + parameters.ExternalIdentifier = userId; + parameters.AddClaim(claims); + + var result = _authorizer.Value.Authorize(parameters); + + return new AuthorizeState(returnUrl, result); } + + #endregion } } diff --git a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayServiceHelper.cs b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayServiceHelper.cs new file mode 100644 index 0000000000..e612df6b22 --- /dev/null +++ b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayServiceHelper.cs @@ -0,0 +1,596 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml.Serialization; +using AmazonPay; +using AmazonPay.CommonRequests; +using AmazonPay.Responses; +using AmazonPay.StandardPaymentRequests; +using SmartStore.AmazonPay.Services.Internal; +using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Logging; +using SmartStore.Services.Common; +using SmartStore.Services.Payments; +using SmartStore.Utilities; + +namespace SmartStore.AmazonPay.Services +{ + /// + /// Helper with utilities to keep the AmazonPayService tidy. + /// + public partial class AmazonPayService + { + /// + /// Also named "spId". + /// + internal static string PlatformId => "A3OJ83WFYM72IY"; + + internal static string LeadCode => "SPEXDEAPA-SmartStore.Net-CP-DP"; + + internal static string GetWidgetUrl(AmazonPaySettings settings) + { + switch (settings.Marketplace.EmptyNull().ToLower()) + { + case "us": + return settings.UseSandbox + ? "https://static-na.payments-amazon.com/OffAmazonPayments/us/sandbox/js/Widgets.js" + : "https://static-na.payments-amazon.com/OffAmazonPayments/us/js/Widgets.js"; + case "uk": + return settings.UseSandbox + ? "https://static-eu.payments-amazon.com/OffAmazonPayments/gbp/sandbox/lpa/js/Widgets.js" + : "https://static-eu.payments-amazon.com/OffAmazonPayments/gbp/lpa/js/Widgets.js"; + case "jp": + return settings.UseSandbox + ? "https://static-fe.payments-amazon.com/OffAmazonPayments/jp/sandbox/lpa/js/Widgets.js" + : "https://static-fe.payments-amazon.com/OffAmazonPayments/jp/lpa/js/Widgets.js"; + default: + return settings.UseSandbox + ? "https://static-eu.payments-amazon.com/OffAmazonPayments/eur/sandbox/lpa/js/Widgets.js" + : "https://static-eu.payments-amazon.com/OffAmazonPayments/eur/lpa/js/Widgets.js"; + } + } + + private string GetPluginUrl(string action, bool useSsl = false) + { + var pluginUrl = "{0}Plugins/SmartStore.AmazonPay/AmazonPay/{1}".FormatInvariant(_services.WebHelper.GetStoreLocation(useSsl), action); + return pluginUrl; + } + + private void SerializeOrderAttribute(AmazonPayOrderAttribute attribute, Order order) + { + if (attribute != null) + { + var sb = new StringBuilder(); + using (var writer = new StringWriter(sb)) + { + var serializer = new XmlSerializer(typeof(AmazonPayOrderAttribute)); + serializer.Serialize(writer, attribute); + + _genericAttributeService.SaveAttribute(order, AmazonPayPlugin.SystemName + ".OrderAttribute", sb.ToString(), order.StoreId); + } + } + } + + private AmazonPayOrderAttribute DeserializeOrderAttribute(Order order) + { + var serialized = order.GetAttribute(AmazonPayPlugin.SystemName + ".OrderAttribute", _genericAttributeService, order.StoreId); + + if (!serialized.HasValue()) + { + var attribute = new AmazonPayOrderAttribute(); + + // legacy < v.1.14 + attribute.OrderReferenceId = order.GetAttribute(AmazonPayPlugin.SystemName + ".OrderReferenceId", order.StoreId); + + return attribute; + } + + using (var reader = new StringReader(serialized)) + { + var serializer = new XmlSerializer(typeof(AmazonPayOrderAttribute)); + return (AmazonPayOrderAttribute)serializer.Deserialize(reader); + } + } + + private bool IsPaymentMethodActive(int storeId, bool logInactive = false) + { + var isActive = _paymentService.IsPaymentMethodActive(AmazonPayPlugin.SystemName, storeId); + + if (!isActive && logInactive) + { + Logger.Error(null, T("Plugins.Payments.AmazonPay.PaymentMethodNotActive", _services.StoreContext.CurrentStore.Name)); + } + + return isActive; + } + + private void AddOrderNote(AmazonPaySettings settings, Order order, string anyString = null, bool isIpn = false) + { + try + { + if (!settings.AddOrderNotes || order == null || anyString.IsEmpty()) + return; + + var sb = new StringBuilder(); + var faviconUrl = "{0}Plugins/{1}/Content/images/favicon.png".FormatInvariant(_services.WebHelper.GetStoreLocation(false), AmazonPayPlugin.SystemName); + + sb.AppendFormat("", faviconUrl); + sb.AppendFormat("{0}", T("Plugins.Payments.AmazonPay.AmazonDataProcessed")); + sb.Append(":
                              "); + sb.Append(anyString); + + if (isIpn) + { + order.HasNewPaymentNotification = true; + } + + order.OrderNotes.Add(new OrderNote + { + Note = sb.ToString(), + DisplayToCustomer = false, + CreatedOnUtc = DateTime.UtcNow + }); + + _orderService.UpdateOrder(order); + } + catch (Exception exception) + { + Logger.Error(exception); + } + } + + private Regions.currencyCode ConvertCurrency(string currencyCode) + { + switch (currencyCode.EmptyNull().ToLower()) + { + case "usd": + return Regions.currencyCode.USD; + case "gbp": + return Regions.currencyCode.GBP; + case "jpy": + return Regions.currencyCode.JPY; + case "aud": + return Regions.currencyCode.AUD; + case "zar": + return Regions.currencyCode.ZAR; + case "chf": + return Regions.currencyCode.CHF; + case "nok": + return Regions.currencyCode.NOK; + case "dkk": + return Regions.currencyCode.DKK; + case "sek": + return Regions.currencyCode.SEK; + case "nzd": + return Regions.currencyCode.NZD; + case "hkd": + return Regions.currencyCode.HKD; + default: + return Regions.currencyCode.EUR; + } + } + + private Address CreateAddress( + string email, + string buyerName, + string addressLine1, + string addressLine2, + string addressLine3, + string city, + string postalCode, + string phone, + string countryCode, + string stateRegion, + string county, + string destrict, + out bool countryAllowsShipping, + out bool countryAllowsBilling) + { + countryAllowsShipping = countryAllowsBilling = true; + + var address = new Address(); + address.CreatedOnUtc = DateTime.UtcNow; + address.Email = email; + address.ToFirstAndLastName(buyerName); + address.Address1 = addressLine1.EmptyNull().Trim().Truncate(4000); + address.Address2 = addressLine2.EmptyNull().Trim().Truncate(4000); + address.Address2 = address.Address2.Grow(addressLine3.EmptyNull().Trim(), ", ").Truncate(4000); + address.City = county.Grow(destrict, " ").Grow(city, " ").EmptyNull().Trim().Truncate(4000); + address.ZipPostalCode = postalCode.EmptyNull().Trim().Truncate(4000); + address.PhoneNumber = phone.EmptyNull().Trim().Truncate(4000); + + if (countryCode.HasValue()) + { + var country = _countryService.GetCountryByTwoOrThreeLetterIsoCode(countryCode); + if (country != null) + { + address.CountryId = country.Id; + countryAllowsShipping = country.AllowsShipping; + countryAllowsBilling = country.AllowsBilling; + } + } + + if (stateRegion.HasValue()) + { + var stateProvince = _stateProvinceService.GetStateProvinceByAbbreviation(stateRegion); + if (stateProvince != null) + { + address.StateProvinceId = stateProvince.Id; + } + } + + // Normalize. + if (address.Address1.IsEmpty() && address.Address2.HasValue()) + { + address.Address1 = address.Address2; + address.Address2 = null; + } + else if (address.Address1.HasValue() && address.Address1 == address.Address2) + { + address.Address2 = null; + } + + if (address.CountryId == 0) + { + address.CountryId = null; + } + + if (address.StateProvinceId == 0) + { + address.StateProvinceId = null; + } + + return address; + } + + //private void GetAddress(OrderReferenceDetailsResponse details, Address address, out bool countryAllowsShipping, out bool countryAllowsBilling) + //{ + // countryAllowsShipping = countryAllowsBilling = true; + + // address.Email = details.GetEmail(); + // address.ToFirstAndLastName(details.GetBuyerName()); + // address.Address1 = details.GetAddressLine1().EmptyNull().Trim().Truncate(4000); + // address.Address2 = details.GetAddressLine2().EmptyNull().Trim().Truncate(4000); + // address.Address2 = address.Address2.Grow(details.GetAddressLine3().EmptyNull().Trim(), ", ").Truncate(4000); + // address.City = details.GetCity().EmptyNull().Trim().Truncate(4000); + // address.ZipPostalCode = details.GetPostalCode().EmptyNull().Trim().Truncate(4000); + // address.PhoneNumber = details.GetPhone().EmptyNull().Trim().Truncate(4000); + + // var countryCode = details.GetCountryCode(); + // if (countryCode.HasValue()) + // { + // var country = _countryService.GetCountryByTwoOrThreeLetterIsoCode(countryCode); + // if (country != null) + // { + // address.CountryId = country.Id; + // countryAllowsShipping = country.AllowsShipping; + // countryAllowsBilling = country.AllowsBilling; + // } + // } + + // var stateRegion = details.GetStateOrRegion(); + // if (stateRegion.HasValue()) + // { + // var stateProvince = _stateProvinceService.GetStateProvinceByAbbreviation(stateRegion); + // if (stateProvince != null) + // { + // address.StateProvinceId = stateProvince.Id; + // } + // } + + // // Normalize. + // if (address.Address1.IsEmpty() && address.Address2.HasValue()) + // { + // address.Address1 = address.Address2; + // address.Address2 = null; + // } + // else if (address.Address1.HasValue() && address.Address1 == address.Address2) + // { + // address.Address2 = null; + // } + + // if (address.CountryId == 0) + // { + // address.CountryId = null; + // } + + // if (address.StateProvinceId == 0) + // { + // address.StateProvinceId = null; + // } + //} + + //private bool FindAndApplyAddress(OrderReferenceDetailsResponse details, Customer customer, bool isShippable, bool forceToTakeAmazonAddress) + //{ + // // PlaceOrder requires billing address but we don't get one from Amazon here. so use shipping address instead until we get it from amazon. + // var countryAllowsShipping = true; + // var countryAllowsBilling = true; + + // var address = new Address(); + // address.CreatedOnUtc = DateTime.UtcNow; + + // GetAddress(details, address, out countryAllowsShipping, out countryAllowsBilling); + + // if (isShippable && !countryAllowsShipping) + // return false; + + // if (address.Email.IsEmpty()) + // { + // address.Email = customer.Email; + // } + + // if (forceToTakeAmazonAddress) + // { + // // First time to get in touch with an amazon address. + // var existingAddress = customer.Addresses.ToList().FindAddress(address, true); + // if (existingAddress == null) + // { + // customer.Addresses.Add(address); + // customer.BillingAddress = address; + // } + // else + // { + // customer.BillingAddress = existingAddress; + // } + // } + // else + // { + // if (customer.BillingAddress == null) + // { + // customer.Addresses.Add(address); + // customer.BillingAddress = address; + // } + + // GetAddress(details, customer.BillingAddress, out countryAllowsShipping, out countryAllowsBilling); + + // // But now we could have dublicates. + // var newAddressId = customer.BillingAddress.Id; + // var addresses = customer.Addresses.Where(x => x.Id != newAddressId).ToList(); + + // var existingAddress = addresses.FindAddress(customer.BillingAddress, false); + // if (existingAddress != null) + // { + // // Remove the new and take the old one. + // customer.RemoveAddress(customer.BillingAddress); + // customer.BillingAddress = existingAddress; + + // try + // { + // _addressService.DeleteAddress(newAddressId); + // } + // catch (Exception exception) + // { + // exception.Dump(); + // } + // } + // } + + // customer.ShippingAddress = (isShippable ? customer.BillingAddress : null); + + // return true; + //} + + private AmazonPayData GetDetails(AuthorizeResponse response) + { + var data = new AmazonPayData(); + data.MessageType = "GetAuthorizationDetails"; + data.MessageId = response.GetRequestId(); + data.AuthorizationId = response.GetAuthorizationId(); + data.ReferenceId = response.GetAuthorizationReferenceId(); + + var ids = response.GetCaptureIdList(); + if (ids.Any()) + { + data.CaptureId = ids.First(); + } + + data.Fee = new AmazonPayPrice(response.GetAuthorizationFee(), response.GetAuthorizationFeeCurrencyCode()); + data.AuthorizedAmount = new AmazonPayPrice(response.GetAuthorizationAmount(), response.GetAuthorizationAmountCurrencyCode()); + data.CapturedAmount = new AmazonPayPrice(response.GetCapturedAmount(), response.GetCapturedAmountCurrencyCode()); + data.CaptureNow = response.GetCaptureNow(); + data.Creation = response.GetCreationTimestamp(); + data.Expiration = response.GetExpirationTimestamp(); + data.ReasonCode = response.GetReasonCode(); + data.ReasonDescription = response.GetReasonDescription(); + data.State = response.GetAuthorizationState(); + data.StateLastUpdate = response.GetLastUpdateTimestamp(); + + return data; + } + private AmazonPayData GetDetails(CaptureResponse response) + { + var data = new AmazonPayData(); + data.MessageType = "GetCaptureDetails"; + data.MessageId = response.GetRequestId(); + data.CaptureId = response.GetCaptureId(); + data.ReferenceId = response.GetCaptureReferenceId(); + data.Fee = new AmazonPayPrice(response.GetCaptureFee(), response.GetCaptureFeeCurrencyCode()); + data.CapturedAmount = new AmazonPayPrice(response.GetCaptureAmount(), response.GetCaptureAmountCurrencyCode()); + data.Creation = response.GetCreationTimestamp(); + data.ReasonCode = response.GetReasonCode(); + data.ReasonDescription = response.GetReasonDescription(); + data.State = response.GetCaptureState(); + data.StateLastUpdate = response.GetLastUpdatedTimestamp(); + + return data; + } + private AmazonPayData GetDetails(RefundResponse response) + { + var data = new AmazonPayData(); + data.MessageType = "GetRefundDetails"; + data.MessageId = response.GetRequestId(); + data.ReferenceId = response.GetRefundReferenceId(); + data.Creation = response.GetCreationTimestamp(); + data.Fee = new AmazonPayPrice(response.GetRefundFee(), response.GetRefundFeeCurrencyCode()); + data.RefundedAmount = new AmazonPayPrice(response.GetRefundAmount(), response.GetRefundAmountCurrencyCode()); + data.ReasonCode = response.GetReasonCode(); + data.ReasonDescription = response.GetReasonDescription(); + data.State = response.GetRefundState(); + data.StateLastUpdate = response.GetLastUpdateTimestamp(); + + return data; + } + + private string GetRandomId(string prefix) + { + var str = prefix + CommonHelper.GenerateRandomDigitCode(20); + return str.Truncate(32); + } + + private string LogError(IResponse response, bool isWarning = false) + { + var message = $"{response.GetErrorMessage().NaIfEmpty()} ({response.GetErrorCode().NaIfEmpty()})"; + + Logger.Log(isWarning ? LogLevel.Warning : LogLevel.Error, new Exception(response.GetJson()), message, null); + + return message; + } + + private string ToInfoString(AmazonPayData data) + { + var sb = new StringBuilder(); + + try + { + var strings = _services.Localization.GetResource("Plugins.Payments.AmazonPay.MessageStrings").SplitSafe(";"); + var state = data.State.Grow(data.ReasonCode, " "); + + if (data.ReasonDescription.HasValue()) + state = $"{state} ({data.ReasonDescription})"; + + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.MessageTyp)}: {data.MessageType.NaIfEmpty()}"); + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.State)}: {state}"); + + var stateDate = _dateTimeHelper.ConvertToUserTime(data.StateLastUpdate, DateTimeKind.Utc); + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.StateUpdate)}: {stateDate.ToString()}"); + + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.MessageId)}: {data.MessageId.NaIfEmpty()}"); + + if (data.AuthorizationId.HasValue()) + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.AuthorizationID)}: {data.AuthorizationId}"); + + if (data.CaptureId.HasValue()) + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.CaptureID)}: {data.CaptureId}"); + + if (data.RefundId.HasValue()) + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.RefundID)}: {data.RefundId}"); + + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.ReferenceID)}: {data.ReferenceId.NaIfEmpty()}"); + + if (data.Fee != null && data.Fee.Amount != decimal.Zero) + { + var signed = data.MessageType.IsCaseInsensitiveEqual("RefundNotification") || data.MessageType.IsCaseInsensitiveEqual("GetRefundDetails") ? "-" : ""; + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.Fee)}: {signed}"); + } + + if (data.AuthorizedAmount != null && data.AuthorizedAmount.Amount != decimal.Zero) + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.AuthorizedAmount)}: {data.AuthorizedAmount.ToString()}"); + + if (data.CapturedAmount != null && data.CapturedAmount.Amount != decimal.Zero) + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.CapturedAmount)}: {data.CapturedAmount.ToString()}"); + + if (data.RefundedAmount != null && data.RefundedAmount.Amount != decimal.Zero) + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.RefundedAmount)}: {data.RefundedAmount.ToString()}"); + + if (data.CaptureNow.HasValue) + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.CaptureNow)}: {data.CaptureNow.Value.ToString()}"); + + var creationDate = _dateTimeHelper.ConvertToUserTime(data.Creation, DateTimeKind.Utc); + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.Creation)}: {creationDate.ToString()}"); + + if (data.Expiration.HasValue) + { + var expirationDate = _dateTimeHelper.ConvertToUserTime(data.Expiration.Value, DateTimeKind.Utc); + sb.AppendLine($"{strings.SafeGet((int)AmazonPayMessage.Expiration)}: {expirationDate.ToString()}"); + } + } + catch (Exception exception) + { + exception.Dump(); + } + + return sb.ToString(); + } + + /// + /// Creates an API client. + /// + /// AmazonPay settings + /// Currency code of primary store currency + /// AmazonPay client + private Client CreateClient(AmazonPaySettings settings) + { + var descriptor = _pluginFinder.GetPluginDescriptorBySystemName(AmazonPayPlugin.SystemName); + var appVersion = descriptor != null ? descriptor.Version.ToString() : "1.0"; + + Regions.supportedRegions region; + switch (settings.Marketplace.EmptyNull().ToLower()) + { + case "us": + region = Regions.supportedRegions.us; + break; + case "uk": + region = Regions.supportedRegions.uk; + break; + case "jp": + region = Regions.supportedRegions.jp; + break; + default: + region = Regions.supportedRegions.de; + break; + } + + var config = new Configuration() + .WithAccessKey(settings.AccessKey) + .WithClientId(settings.ClientId) + .WithSecretKey(settings.SecretKey) + .WithSandbox(settings.UseSandbox) + .WithApplicationName("SmartStore.Net " + AmazonPayPlugin.SystemName) + .WithApplicationVersion(appVersion) + .WithRegion(region); + + var client = new Client(config); + return client; + } + + private AuthorizeResponse AuthorizePayment( + AmazonPaySettings settings, + AmazonPayCheckoutState state, + Store store, + ProcessPaymentRequest request, + Client client, + bool synchronously) + { + var authRequest = new AuthorizeRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonOrderReferenceId(state.OrderReferenceId) + .WithAuthorizationReferenceId(GetRandomId("Authorize")) + .WithCaptureNow(settings.TransactionType == AmazonPayTransactionType.AuthorizeAndCapture) + .WithCurrencyCode(ConvertCurrency(store.PrimaryStoreCurrency.CurrencyCode)) + .WithAmount(request.OrderTotal); + + if (synchronously) + { + authRequest = authRequest.WithTransactionTimeout(0); + } + + // See https://pay.amazon.com/de/developer/documentation/lpwa/201956480 + //{"SandboxSimulation": {"State":"Declined", "ReasonCode":"InvalidPaymentMethod", "PaymentMethodUpdateTimeInMins":5}} + //{"SandboxSimulation": {"State":"Declined", "ReasonCode":"AmazonRejected"}} + //if (settings.UseSandbox) + //{ + // var authNote = _services.Settings.GetSettingByKey("SmartStore.AmazonPay.SellerAuthorizationNote"); + // if (authNote.HasValue()) + // { + // authRequest = authRequest.WithSellerAuthorizationNote(authNote); + // } + //} + + var authResponse = client.Authorize(authRequest); + return authResponse; + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayUtilities.cs b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayUtilities.cs new file mode 100644 index 0000000000..b5316fc229 --- /dev/null +++ b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayUtilities.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using AmazonPay; +using SmartStore.Core.Domain.Orders; + +namespace SmartStore.AmazonPay.Services +{ + [Serializable] + public class AmazonPayCheckoutState + { + public string OrderReferenceId { get; set; } + public string AccessToken { get; set; } + } + + + public class AmazonPayActionState + { + public Guid OrderGuid { get; set; } + public List Errors { get; set; } + } + + + [Serializable] + public class AmazonPayOrderAttribute + { + public string OrderReferenceId { get; set; } + } + + + internal class PollingLoopData + { + public PollingLoopData(int orderId) + { + OrderId = orderId; + } + + public int OrderId { get; private set; } + public Order Order { get; set; } + public AmazonPaySettings Settings { get; set; } + public Client Client { get; set; } + } +} diff --git a/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayApi.cs b/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayApi.cs deleted file mode 100644 index 70d72d9aa1..0000000000 --- a/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayApi.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using System.Web; -using OffAmazonPaymentsService.Model; -using SmartStore.AmazonPay.Services; -using SmartStore.AmazonPay.Settings; -using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Orders; -using SmartStore.Services.Payments; - -namespace SmartStore.AmazonPay.Api -{ - public partial interface IAmazonPayApi - { - void GetConstraints(OrderReferenceDetails details, IList warnings); - - bool FindAndApplyAddress(OrderReferenceDetails details, Customer customer, bool isShippable, bool forceToTakeAmazonAddress); - bool FulfillBillingAddress(AmazonPaySettings settings, Order order, AuthorizationDetails details, out string formattedAddress); - - OrderReferenceDetails GetOrderReferenceDetails(AmazonPayClient client, string orderReferenceId, string addressConsentToken = null); - - OrderReferenceDetails SetOrderReferenceDetails(AmazonPayClient client, string orderReferenceId, decimal? orderTotalAmount, - string currencyCode, string orderGuid = null, string storeName = null); - - OrderReferenceDetails SetOrderReferenceDetails(AmazonPayClient client, string orderReferenceId, string currencyCode, List cart); - - void ConfirmOrderReference(AmazonPayClient client, string orderReferenceId); - - void CancelOrderReference(AmazonPayClient client, string orderReferenceId); - - void CloseOrderReference(AmazonPayClient client, string orderReferenceId); - - void Authorize(AmazonPayClient client, ProcessPaymentResult result, List errors, string orderReferenceId, decimal orderTotalAmount, - string currencyCode, string orderGuid); - - AuthorizationDetails GetAuthorizationDetails(AmazonPayClient client, string authorizationId, out AmazonPayApiData data); - - void Capture(AmazonPayClient client, CapturePaymentRequest capture, CapturePaymentResult result); - - CaptureDetails GetCaptureDetails(AmazonPayClient client, string captureId, out AmazonPayApiData data); - - string Refund(AmazonPayClient client, RefundPaymentRequest refund, RefundPaymentResult result); - - RefundDetails GetRefundDetails(AmazonPayClient client, string refundId, out AmazonPayApiData data); - - string ToInfoString(AmazonPayApiData data); - - AmazonPayApiData ParseNotification(HttpRequestBase request); - } -} \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayService.cs b/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayService.cs index 03d54173e3..baa64192a8 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayService.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayService.cs @@ -1,30 +1,21 @@ -using System; -using System.Web; +using System.Web; using System.Web.Mvc; -using System.Collections.Generic; -using OffAmazonPaymentsService; using SmartStore.AmazonPay.Models; -using SmartStore.AmazonPay.Settings; -using SmartStore.Core.Domain.Orders; +using SmartStore.Services.Authentication.External; using SmartStore.Services.Payments; namespace SmartStore.AmazonPay.Services { - public partial interface IAmazonPayService + public partial interface IAmazonPayService : IExternalProviderAuthorizer { - void LogError(Exception exception, string shortMessage = null, string fullMessage = null, bool notify = false, IList errors = null); - void LogAmazonError(OffAmazonPaymentsServiceException exception, bool notify = false, IList errors = null); - - void AddOrderNote(AmazonPaySettings settings, Order order, AmazonPayOrderNote note, string anyString = null, bool isIpn = false); - void SetupConfiguration(ConfigurationModel model); - string GetWidgetUrl(); + AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDictionary tempData); - AmazonPayViewModel ProcessPluginRequest(AmazonPayRequestType type, TempDataDictionary tempData, string orderReferenceId = null); - void AddCustomerOrderNoteLoop(AmazonPayActionState state); + void GetBillingAddress(); + PreProcessPaymentResult PreProcessPayment(ProcessPaymentRequest request); ProcessPaymentResult ProcessPayment(ProcessPaymentRequest request); @@ -39,12 +30,8 @@ public partial interface IAmazonPayService void ProcessIpn(HttpRequestBase request); - void DataPollingTaskProcess(); - - void DataPollingTaskInit(); - - void DataPollingTaskUpdate(bool enabled, int seconds); + void StartDataPolling(); - void DataPollingTaskDelete(); + void ShareKeys(string payload, int storeId); } } diff --git a/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayData.cs b/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayData.cs new file mode 100644 index 0000000000..8df3aa6d48 --- /dev/null +++ b/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayData.cs @@ -0,0 +1,40 @@ +using System; + +namespace SmartStore.AmazonPay.Services.Internal +{ + internal class AmazonPayData + { + public string MessageType { get; set; } + public string MessageId { get; set; } + public string AuthorizationId { get; set; } + public string CaptureId { get; set; } + public string RefundId { get; set; } + public string ReferenceId { get; set; } + + public string ReasonCode { get; set; } + public string ReasonDescription { get; set; } + public string State { get; set; } + public DateTime StateLastUpdate { get; set; } + + public AmazonPayPrice Fee { get; set; } + public AmazonPayPrice AuthorizedAmount { get; set; } + public AmazonPayPrice CapturedAmount { get; set; } + public AmazonPayPrice RefundedAmount { get; set; } + + public bool? CaptureNow { get; set; } + public DateTime Creation { get; set; } + public DateTime? Expiration { get; set; } + + public string AnyAmazonId + { + get + { + if (CaptureId.HasValue()) + return CaptureId; + if (AuthorizationId.HasValue()) + return AuthorizationId; + return RefundId; + } + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayExtensions.cs b/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayExtensions.cs new file mode 100644 index 0000000000..f77931cc01 --- /dev/null +++ b/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayExtensions.cs @@ -0,0 +1,141 @@ +using System.Collections.Generic; +using System.Linq; +using System.Web; +using SmartStore.AmazonPay.Services; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Orders; +using SmartStore.Services.Common; +using SmartStore.Services.Localization; + +namespace SmartStore.AmazonPay +{ + internal static class AmazonPayExtensions + { + internal static void ToFirstAndLastName(this string name, out string firstName, out string lastName) + { + if (!string.IsNullOrWhiteSpace(name)) + { + int index = name.LastIndexOf(' '); + if (index == -1) + { + firstName = ""; + lastName = name; + } + else + { + firstName = name.Substring(0, index); + lastName = name.Substring(index + 1); + } + + firstName = firstName.EmptyNull().Truncate(4000); + lastName = lastName.EmptyNull().Truncate(4000); + } + else + { + firstName = lastName = ""; + } + } + + internal static void ToFirstAndLastName(this Address address, string name) + { + string firstName, lastName; + name.ToFirstAndLastName(out firstName, out lastName); + + address.FirstName = firstName; + address.LastName = lastName; + } + + internal static Address FindAddress(this List
                              addresses, Address address, bool uncompleteToo) + { + var match = addresses.FindAddress(address.FirstName, address.LastName, + address.PhoneNumber, address.Email, address.FaxNumber, address.Company, + address.Address1, address.Address2, + address.City, address.StateProvinceId, address.ZipPostalCode, address.CountryId); + + if (match == null && uncompleteToo) + { + // Compare with ToAddress + match = addresses.FirstOrDefault(x => + x.FirstName == null && x.LastName == null && + x.Address1 == null && x.Address2 == null && + x.City == address.City && x.ZipPostalCode == address.ZipPostalCode && + x.PhoneNumber == null && + x.CountryId == address.CountryId && x.StateProvinceId == address.StateProvinceId + ); + } + + return match; + } + + internal static string ToAmazonLanguageCode(this string twoLetterLanguageCode, char delimiter = '-') + { + switch (twoLetterLanguageCode.EmptyNull().ToLower()) + { + case "en": + return $"en{delimiter}GB"; + case "fr": + return $"fr{delimiter}FR"; + case "it": + return $"it{delimiter}IT"; + case "es": + return $"es{delimiter}ES"; + case "de": + default: + return $"de{delimiter}DE"; + } + } + + internal static bool HasAmazonPayState(this HttpContextBase httpContext) + { + var checkoutState = httpContext.GetCheckoutState(); + var checkoutStateKey = AmazonPayPlugin.SystemName + ".CheckoutState"; + + if (checkoutState != null && checkoutState.CustomProperties.ContainsKey(checkoutStateKey)) + { + var state = checkoutState.CustomProperties[checkoutStateKey] as AmazonPayCheckoutState; + + return state != null && state.AccessToken.HasValue(); + } + + return false; + } + + internal static AmazonPayCheckoutState GetAmazonPayState(this HttpContextBase httpContext, ILocalizationService localizationService) + { + var checkoutState = httpContext.GetCheckoutState(); + + if (checkoutState == null) + throw new SmartException(localizationService.GetResource("Plugins.Payments.AmazonPay.MissingCheckoutSessionState")); + + var state = checkoutState.CustomProperties.Get(AmazonPayPlugin.SystemName + ".CheckoutState") as AmazonPayCheckoutState; + + if (state == null) + throw new SmartException(localizationService.GetResource("Plugins.Payments.AmazonPay.MissingCheckoutSessionState")); + + return state; + } + + internal static Order GetOrderByAmazonId(this IRepository orderRepository, string amazonId) + { + // S02-9777218-8608106 OrderReferenceId + // S02-9777218-8608106-A088344 Auth ID + // S02-9777218-8608106-C088344 Capture ID + + if (amazonId.HasValue()) + { + string amazonOrderReferenceId = amazonId.Substring(0, amazonId.LastIndexOf('-')); + if (amazonOrderReferenceId.HasValue()) + { + var orders = orderRepository.Table + .Where(x => x.PaymentMethodSystemName == AmazonPayPlugin.SystemName && x.AuthorizationTransactionId.StartsWith(amazonOrderReferenceId)) + .ToList(); + + if (orders.Count() == 1) + return orders.FirstOrDefault(); + } + } + return null; + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayPrice.cs b/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayPrice.cs new file mode 100644 index 0000000000..cfa3fef830 --- /dev/null +++ b/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayPrice.cs @@ -0,0 +1,22 @@ +using System.Globalization; + +namespace SmartStore.AmazonPay.Services.Internal +{ + internal class AmazonPayPrice + { + public AmazonPayPrice(decimal amount, string currencyCode) + { + Amount = amount; + CurrencyCode = currencyCode; + } + + public decimal Amount { get; private set; } + public string CurrencyCode { get; private set; } + + public override string ToString() + { + var str = Amount.ToString("0.00", CultureInfo.InvariantCulture); + return str.Grow(CurrencyCode, " "); + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Settings/AmazonPaySettings.cs b/src/Plugins/SmartStore.AmazonPay/Settings/AmazonPaySettings.cs deleted file mode 100644 index 72459d6b42..0000000000 --- a/src/Plugins/SmartStore.AmazonPay/Settings/AmazonPaySettings.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System.Web; -using SmartStore.Core.Configuration; -using SmartStore.AmazonPay.Services; - -namespace SmartStore.AmazonPay.Settings -{ - public class AmazonPaySettings : ISettings - { - public AmazonPaySettings() - { - Marketplace = "de"; - DataFetching = AmazonPayDataFetchingType.Ipn; - TransactionType = AmazonPayTransactionType.Authorize; - SaveEmailAndPhone = AmazonPaySaveDataType.OnlyIfEmpty; - AmazonButtonColor = "orange"; - AmazonButtonSize = "x-large"; - AddressWidgetWidth = PaymentWidgetWidth = 400; - AddressWidgetHeight = PaymentWidgetHeight = 260; - AddOrderNotes = true; - InformCustomerAboutErrors = true; - InformCustomerAddErrors = true; - PollingMaxOrderCreationDays = 31; - } - - public bool UseSandbox { get; set; } - - public string SellerId { get; set; } - public string AccessKey { get; set; } - public string SecretKey { get; set; } - public string Marketplace { get; set; } - - public AmazonPayDataFetchingType DataFetching { get; set; } - public AmazonPayTransactionType TransactionType { get; set; } - - public AmazonPaySaveDataType? SaveEmailAndPhone { get; set; } - public bool ShowButtonInMiniShoppingCart { get; set; } - - public int PollingMaxOrderCreationDays { get; set; } - - public string AmazonButtonColor { get; set; } - public string AmazonButtonSize { get; set; } - - public int AddressWidgetWidth { get; set; } - public int AddressWidgetHeight { get; set; } - - public int PaymentWidgetWidth { get; set; } - public int PaymentWidgetHeight { get; set; } - - public decimal AdditionalFee { get; set; } - public bool AdditionalFeePercentage { get; set; } - - public bool AddOrderNotes { get; set; } - - public bool InformCustomerAboutErrors { get; set; } - public bool InformCustomerAddErrors { get; set; } - - public string GetApiUrl() - { - return (UseSandbox ? AmazonPayCore.UrlApiEuSandbox : AmazonPayCore.UrlApiEuProduction); - } - - public string GetWidgetUrl() - { - if (SellerId.IsEmpty()) - return null; - - string url = (UseSandbox ? AmazonPayCore.UrlWidgetSandbox : AmazonPayCore.UrlWidgetProduction); - url = url.FormatWith(Marketplace ?? "de"); - - return "{0}?sellerId={1}".FormatWith( - url, - HttpUtility.UrlEncode(SellerId) - ); - } - - public string GetButtonUrl(AmazonPayRequestType view) - { - //bool isGerman = _services.WorkContext.WorkingLanguage.UniqueSeoCode.IsCaseInsensitiveEqual("DE"); - string marketplace = Marketplace ?? "de"; - if (marketplace.IsCaseInsensitiveEqual("uk")) - marketplace = "co.uk"; - - string buttonSize = (view == AmazonPayRequestType.MiniShoppingCart ? "large" : AmazonButtonSize); - - string url = (UseSandbox ? AmazonPayCore.UrlButtonSandbox : AmazonPayCore.UrlButtonProduction); - url = url.FormatWith(marketplace); - - return "{0}?sellerId={1}&size={2}&color={3}".FormatWith( - url, - HttpUtility.UrlEncode(SellerId), - HttpUtility.UrlEncode(buttonSize ?? "x-large"), - HttpUtility.UrlEncode(AmazonButtonColor ?? "orange") - ); - } - - public bool CanSaveEmailAndPhone(string value) - { - return ( - SaveEmailAndPhone == AmazonPaySaveDataType.Always || (SaveEmailAndPhone == AmazonPaySaveDataType.OnlyIfEmpty && value.IsEmpty()) - ); - } - } -} diff --git a/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj b/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj index ee182de2bc..fc7185bef3 100644 --- a/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj +++ b/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj @@ -1,5 +1,6 @@  + @@ -18,7 +19,7 @@ Properties SmartStore.AmazonPay SmartStore.AmazonPay - v4.5.2 + v4.6.1 512 @@ -37,6 +38,9 @@ + + + true @@ -74,24 +78,29 @@ MinimumRecommendedRules.ruleset + + ..\..\packages\AmazonPay.3.3.1\lib\net20\AmazonPay.dll + True + ..\..\packages\Autofac.4.5.0\lib\net45\Autofac.dll ..\..\packages\Autofac.Mvc5.4.0.2\lib\net45\Autofac.Integration.Mvc.dll + + ..\..\packages\Common.Logging.2.0.0\lib\2.0\Common.Logging.dll + True + ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - False - lib\OffAmazonPaymentsNotifications.dll - - - False - lib\OffAmazonPaymentsService.dll + + ..\..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll + False + @@ -135,22 +144,27 @@ Properties\AssemblyVersionInfo.cs + + PreserveNewest + - - + + - - - + + + + + - + @@ -159,11 +173,12 @@ + + PreserveNewest + PreserveNewest - - PreserveNewest @@ -192,27 +207,21 @@ PreserveNewest - + PreserveNewest PreserveNewest - - - - PreserveNewest - - + PreserveNewest + + Always - Always - - PreserveNewest @@ -223,9 +232,6 @@ Designer Always - - Always - @@ -274,8 +280,15 @@ - xcopy /s /y "$(ProjectDir)lib\*.*" "$(TargetDir)*.*" + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + - + @@ -135,13 +143,10 @@ + + + + - - - - - - - - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.Clickatell/AdminMenu.cs b/src/Plugins/SmartStore.Clickatell/AdminMenu.cs index 27ea086787..0795b68f34 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("send-o") + .Icon("paper-plane-o") .Action("ConfigurePlugin", "Plugin", new { systemName = "SmartStore.Clickatell", area = "Admin" }) .ToItem(); diff --git a/src/Plugins/SmartStore.Clickatell/Controllers/SmsClickatellController.cs b/src/Plugins/SmartStore.Clickatell/Controllers/SmsClickatellController.cs index 7540c451eb..314e14070c 100644 --- a/src/Plugins/SmartStore.Clickatell/Controllers/SmsClickatellController.cs +++ b/src/Plugins/SmartStore.Clickatell/Controllers/SmsClickatellController.cs @@ -1,8 +1,8 @@ using System; using System.Web.Mvc; using SmartStore.Clickatell.Models; +using SmartStore.ComponentModel; using SmartStore.Core.Plugins; -using SmartStore.Services; using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Framework.Filters; using SmartStore.Web.Framework.Security; @@ -13,57 +13,37 @@ namespace SmartStore.Clickatell.Controllers [AdminAuthorize] public class SmsClickatellController : PluginControllerBase { - private readonly ICommonServices _services; - private readonly IPluginFinder _pluginFinder; + private readonly IPluginFinder _pluginFinder; - public SmsClickatellController( - ICommonServices services, - IPluginFinder pluginFinder) + public SmsClickatellController(IPluginFinder pluginFinder) { - _services = services; _pluginFinder = pluginFinder; } - public ActionResult Configure() + [LoadSetting] + public ActionResult Configure(ClickatellSettings settings) { - var storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); - var settings = _services.Settings.LoadSetting(storeScope); - - var model = new SmsClickatellModel - { - Enabled = settings.Enabled, - PhoneNumber = settings.PhoneNumber, - ApiId = settings.ApiId - }; - - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - storeDependingSettingHelper.GetOverrideKeys(settings, model, storeScope, _services.Settings); + var model = new SmsClickatellModel(); + MiniMapper.Map(settings, model); return View(model); } - [HttpPost] - public ActionResult Configure(SmsClickatellModel model, FormCollection form) + [HttpPost, SaveSetting, FormValueRequired("save")] + public ActionResult Configure(ClickatellSettings settings, SmsClickatellModel model) { - if (ModelState.IsValid) + if (!ModelState.IsValid) { - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - int storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); - var settings = _services.Settings.LoadSetting(storeScope); - - settings.Enabled = model.Enabled; - settings.PhoneNumber = model.PhoneNumber; - settings.ApiId = model.ApiId; - - storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, _services.Settings); - - NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); + return Configure(settings); } - return Configure(); + MiniMapper.Map(model, settings); + NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); + + return RedirectToConfiguration(ClickatellSmsProvider.SystemName); } - [HttpPost, ActionName("Configure"), FormValueRequired("test-sms")] + [HttpPost, ActionName("Configure"), FormValueRequired("test-sms")] public ActionResult TestSms(SmsClickatellModel model) { try @@ -75,7 +55,7 @@ public ActionResult TestSms(SmsClickatellModel model) } else { - var pluginDescriptor = _pluginFinder.GetPluginDescriptorBySystemName("SmartStore.Clickatell"); + var pluginDescriptor = _pluginFinder.GetPluginDescriptorBySystemName(ClickatellSmsProvider.SystemName); var plugin = pluginDescriptor.Instance() as ClickatellSmsProvider; plugin.SendSms(model.TestMessage); diff --git a/src/Plugins/SmartStore.Clickatell/SmartStore.Clickatell.csproj b/src/Plugins/SmartStore.Clickatell/SmartStore.Clickatell.csproj index fe039fb052..00f3bbd9a6 100644 --- a/src/Plugins/SmartStore.Clickatell/SmartStore.Clickatell.csproj +++ b/src/Plugins/SmartStore.Clickatell/SmartStore.Clickatell.csproj @@ -20,7 +20,7 @@ Properties SmartStore.Clickatell SmartStore.Clickatell - v4.5.2 + v4.6.1 512 diff --git a/src/Plugins/SmartStore.Clickatell/Views/SmsClickatell/Configure.cshtml b/src/Plugins/SmartStore.Clickatell/Views/SmsClickatell/Configure.cshtml index 425e1c1adb..e6fa35f71e 100644 --- a/src/Plugins/SmartStore.Clickatell/Views/SmsClickatell/Configure.cshtml +++ b/src/Plugins/SmartStore.Clickatell/Views/SmsClickatell/Configure.cshtml @@ -4,14 +4,21 @@ Layout = ""; } +
                              + +
                              +
                              -
                              - +
                              @Html.Raw(@T("Plugins.Sms.Clickatell.AdminInstruction")) +
                              -
                              +
                              Clickatell @@ -24,11 +31,6 @@ @using (Html.BeginForm()) { -
                              - -
                              + + + +
                              @@ -73,23 +75,31 @@ @Html.ValidationMessageFor(model => model.TestMessage)
                              +   + + @if (Model.TestSmsResult.HasValue()) + { +
                              + @Model.TestSmsResult @Model.TestSmsDetailResult + +
                              + } +
                                -
                              } -@if (Model.TestSmsResult.HasValue()) -{ -
                              - - @Model.TestSmsResult @Model.TestSmsDetailResult -
                              -} \ No newline at end of file diff --git a/src/Plugins/SmartStore.Clickatell/web.config b/src/Plugins/SmartStore.Clickatell/web.config index bf9944de8a..bb33bef252 100644 --- a/src/Plugins/SmartStore.Clickatell/web.config +++ b/src/Plugins/SmartStore.Clickatell/web.config @@ -5,8 +5,16 @@ + - + @@ -120,13 +128,10 @@ + + + + - - - - - - - - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/AdminMenu.cs b/src/Plugins/SmartStore.DevTools/AdminMenu.cs index 653036204f..8808b9731c 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("code") + .Icon("terminal") .Action("ConfigurePlugin", "Plugin", new { systemName = "SmartStore.DevTools", area = "Admin" }) .ToItem(); @@ -21,7 +21,6 @@ protected override void BuildMenuCore(TreeNode pluginsNode) // .Icon("area-chart") // .Action("BackendExtension", "DevTools", new { area = "SmartStore.DevTools" }) // .ToItem(); - //pluginsNode.Append(backendExtensionItem); // uncomment to add a sub-menu (see plugin sub-menu) @@ -29,14 +28,12 @@ protected override void BuildMenuCore(TreeNode pluginsNode) // .Text("Sub Menu") // .Action("BackendExtension", "DevTools", new { area = "SmartStore.DevTools" }) // .ToItem(); - //var subMenuNode = pluginsNode.Append(subMenu); //var subMenuItem = new MenuItem().ToBuilder() // .Text("Sub Menu Item 1") // .Action("BackendExtension", "DevTools", new { area = "SmartStore.DevTools" }) // .ToItem(); - //subMenuNode.Append(subMenuItem); } } diff --git a/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs b/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs index fe3d6394a2..e4bb0dfecf 100644 --- a/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs +++ b/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs @@ -18,34 +18,16 @@ public DevToolsController(ICommonServices services) _services = services; } - [AdminAuthorize, ChildActionOnly] - public ActionResult Configure() + [LoadSetting, ChildActionOnly] + public ActionResult Configure(ProfilerSettings settings) { - // load settings for a chosen store scope - var storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); - var settings = _services.Settings.LoadSetting(storeScope); - - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - storeDependingSettingHelper.GetOverrideKeys(settings, settings, storeScope, _services.Settings); - return View(settings); } - [HttpPost, AdminAuthorize, ChildActionOnly] - public ActionResult Configure(ProfilerSettings model, FormCollection form) + [SaveSetting(false), HttpPost, ChildActionOnly, ActionName("Configure")] + public ActionResult ConfigurePost(ProfilerSettings settings) { - if (!ModelState.IsValid) - return Configure(); - - ModelState.Clear(); - - // Load settings for a chosen store scope - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - var storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); - - storeDependingSettingHelper.UpdateSettings(model /*settings*/, form, storeScope, _services.Settings); - - return Configure(); + return RedirectToConfiguration("SmartStore.DevTools"); } public ActionResult MiniProfiler() @@ -85,5 +67,23 @@ public ActionResult BackendExtension() return View(model); } + + [AdminAuthorize] + public ActionResult ProductEditTab(int productId, FormCollection form) + { + var model = new BackendExtensionModel + { + Welcome = "Hello world!" + }; + + var result = PartialView(model); + result.ViewData.TemplateInfo = new TemplateInfo { HtmlFieldPrefix = "CustomProperties[DevTools]" }; + return result; + } + + public ActionResult MyDemoWidget() + { + return Content("Hello world! This is a sample widget created for demonstration purposes by Dev-Tools plugin."); + } } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs b/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs index 143fb7712e..761f7820a2 100644 --- a/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs +++ b/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs @@ -6,6 +6,7 @@ using SmartStore.Core.Logging; using SmartStore.DevTools.Filters; using SmartStore.DevTools.Services; +using SmartStore.Web.Controllers; using SmartStore.Web.Framework.Controllers; namespace SmartStore.DevTools @@ -25,12 +26,17 @@ public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, b builder.RegisterType().AsActionFilterFor(); builder.RegisterType().AsResultFilterFor(); - //// intercept CatalogController's Product action - //builder.RegisterType().AsResultFilterFor(x => x.Product(default(int), default(string))).InstancePerRequest(); - //builder.RegisterType().AsActionFilterFor().InstancePerRequest(); - //// intercept CheckoutController's Index action (to hijack the checkout or payment workflow) - //builder.RegisterType().AsActionFilterFor(x => x.Index()).InstancePerRequest(); - } + // Add an action to product detail offer actions + //builder.RegisterType() + // .AsActionFilterFor(x => x.ProductDetails(default(int), default(string), null)) + // .InstancePerRequest(); + + //// intercept CatalogController's Product action + //builder.RegisterType().AsResultFilterFor(x => x.Product(default(int), default(string))).InstancePerRequest(); + //builder.RegisterType().AsActionFilterFor().InstancePerRequest(); + //// intercept CheckoutController's Index action (to hijack the checkout or payment workflow) + //builder.RegisterType().AsActionFilterFor(x => x.Index()).InstancePerRequest(); + } } } diff --git a/src/Plugins/SmartStore.DevTools/DevToolsPlugin.cs b/src/Plugins/SmartStore.DevTools/DevToolsPlugin.cs index c63088dce6..a7ab67eadc 100644 --- a/src/Plugins/SmartStore.DevTools/DevToolsPlugin.cs +++ b/src/Plugins/SmartStore.DevTools/DevToolsPlugin.cs @@ -1,17 +1,22 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Web.Routing; using SmartStore.Core.Logging; using SmartStore.Core.Plugins; using SmartStore.Data; using SmartStore.Data.Setup; +using SmartStore.Services.Cms; using SmartStore.Services.Common; using SmartStore.Services.Configuration; namespace SmartStore.DevTools { - public class DevToolsPlugin : BasePlugin, IConfigurable - { + [DisplayOrder(10)] + [SystemName("Widgets.DevToolsDemo")] + [FriendlyName("Dev-Tools Demo Widget")] + public class DevToolsPlugin : BasePlugin, IConfigurable, IWidget + { private readonly ISettingService _settingService; public DevToolsPlugin(ISettingService settingService) @@ -22,7 +27,21 @@ public DevToolsPlugin(ISettingService settingService) public ILogger Logger { get; set; } - public void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) + public IList GetWidgetZones() => new List { "home_page_top" }; + + public void GetDisplayWidgetRoute(string widgetZone, object model, int storeId, out string actionName, out string controllerName, out RouteValueDictionary routeValues) + { + actionName = "MyDemoWidget"; + controllerName = "DevTools"; + + routeValues = new RouteValueDictionary + { + { "Namespaces", "SmartStore.DevTools.Controllers" }, + { "area", "SmartStore.DevTools" } + }; + } + + public void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) { actionName = "Configure"; controllerName = "DevTools"; @@ -72,5 +91,5 @@ internal static bool HasPendingMigrations() return result; } - } + } } diff --git a/src/Plugins/SmartStore.DevTools/Events/CustomTab.cs b/src/Plugins/SmartStore.DevTools/Events/CustomTab.cs new file mode 100644 index 0000000000..b48c84c76a --- /dev/null +++ b/src/Plugins/SmartStore.DevTools/Events/CustomTab.cs @@ -0,0 +1,61 @@ +using SmartStore.Core.Events; +using SmartStore.DevTools.Models; +using SmartStore.Services; +using SmartStore.Web.Framework.Events; +using SmartStore.Web.Framework.Modelling; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace SmartStore.DevTools.Events +{ + public class CustomTab : IConsumer, IConsumer + { + private readonly ICommonServices _services; + + public CustomTab(ICommonServices services) + { + _services = services; + } + + public void HandleEvent(TabStripCreated eventMessage) + { + // add a form to product detail configuration + if (eventMessage.TabStripName == "product-edit") + { + var productId = ((TabbableModel)eventMessage.Model).Id; + + // add in a predefined tab "Plugins" which serves as container for plugins to obtain data + + //eventMessage.AddWidget(new RouteInfo( + // "ProductEditTab", + // "DevTools", + // new { area = "SmartStore.DevTools", productId = productId } + //)); + + // add in an own tab + + //eventMessage.ItemFactory.Add().Text("Dev Tools") + // .Name("tab-dt") + // .Icon("fa fa-code fa-lg fa-fw") + // .LinkHtmlAttributes(new { data_tab_name = "DevTools" }) + // .Route("SmartStore.DevTools", new { action = "ProductEditTab", productId = productId }) + // .Ajax(); + + } + } + + public void HandleEvent(ModelBoundEvent eventMessage) + { + if (!eventMessage.BoundModel.CustomProperties.ContainsKey("DevTools")) + return; + + var model = eventMessage.BoundModel.CustomProperties["DevTools"] as BackendExtensionModel; + if (model == null) + return; + + // Do something with the model now: e.g. store it ;-) + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleActionFilter.cs b/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleActionFilter.cs index e1cb06dc72..3c010c46fd 100644 --- a/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleActionFilter.cs +++ b/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleActionFilter.cs @@ -24,7 +24,7 @@ public void OnActionExecuting(ActionExecutingContext filterContext) { Debug.WriteLine("Executing: {0} - {1}".FormatInvariant(filterContext.ActionDescriptor.ControllerDescriptor.ControllerName, filterContext.ActionDescriptor.ActionName)); _notifier.Information("Yeah, my plugin action filter works. NICE!"); - // Do somethid meaningful here ;-) + // Do something meaningful here ;-) } public void OnActionExecuted(ActionExecutedContext filterContext) diff --git a/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleProductDetailActionFilter.cs b/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleProductDetailActionFilter.cs new file mode 100644 index 0000000000..fb4307fc1a --- /dev/null +++ b/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleProductDetailActionFilter.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using System.Web.Mvc; +using SmartStore.Core.Logging; +using SmartStore.Core.Localization; +using SmartStore.Web.Models.Catalog; +using SmartStore.Services; + +namespace SmartStore.DevTools.Filters +{ + public class SampleProductDetailActionFilter : IActionFilter + { + private readonly ICommonServices _services; + private readonly UrlHelper _urlHelper; + + public SampleProductDetailActionFilter(ICommonServices services, UrlHelper urlHelper) + { + _services = services; + _urlHelper = urlHelper; + } + + public void OnActionExecuting(ActionExecutingContext filterContext) + { + + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { + var result = filterContext.Result as ViewResultBase; + if (result == null) + { + // The controller action didn't return a view result + // => no need to continue any further + return; + } + + var model = result.Model as ProductDetailsModel; + if (model == null) + { + // there's no model or the model was not of the expected type + // => no need to continue any further + return; + } + + // modify some property value + model.ActionItems["dev"] = new ProductDetailsModel.ActionItemModel + { + Key = "dev", + Title = _services.Localization.GetResource("Dev"), + Tooltip = _services.Localization.GetResource("Dev.Hint"), + CssClass = "action-dev x-ajax-cart-link", + IconCssClass = "icm icm-code", + //Href = _urlHelper.Action("MyOwnAction", "MyPlugin", new { id = model.Id }) + Href = "https://www.smartstore.com" + }; ; + } + } +} diff --git a/src/Plugins/SmartStore.DevTools/Localization/resources.de-de.xml b/src/Plugins/SmartStore.DevTools/Localization/resources.de-de.xml index 82fb7ec059..67cf6df4a5 100644 --- a/src/Plugins/SmartStore.DevTools/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.DevTools/Localization/resources.de-de.xml @@ -1,4 +1,8 @@  + + Dev-Tools Demo Widget + + Mini-Profiler für Frontend aktivieren diff --git a/src/Plugins/SmartStore.DevTools/Localization/resources.en-us.xml b/src/Plugins/SmartStore.DevTools/Localization/resources.en-us.xml index b292c239a0..0ed80f9443 100644 --- a/src/Plugins/SmartStore.DevTools/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.DevTools/Localization/resources.en-us.xml @@ -1,4 +1,8 @@  + + Dev-Tools Demo Widget + + Enable Mini-Profiler in public store diff --git a/src/Plugins/SmartStore.DevTools/Models/BackendExtensionModel.cs b/src/Plugins/SmartStore.DevTools/Models/BackendExtensionModel.cs index 2db540028f..7de7bbe5e1 100644 --- a/src/Plugins/SmartStore.DevTools/Models/BackendExtensionModel.cs +++ b/src/Plugins/SmartStore.DevTools/Models/BackendExtensionModel.cs @@ -5,5 +5,7 @@ namespace SmartStore.DevTools.Models public class BackendExtensionModel : ModelBase { public string Welcome { get; set; } - } + + public int ProductId { get; set; } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj b/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj index 48a0725fc6..443061f98f 100644 --- a/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj +++ b/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj @@ -20,7 +20,7 @@ Properties SmartStore.DevTools SmartStore.DevTools - v4.5.2 + v4.6.1 512 ..\..\ @@ -36,6 +36,7 @@ + true @@ -82,12 +83,12 @@ ..\..\packages\Autofac.Mvc5.4.0.2\lib\net45\Autofac.Integration.Mvc.dll - False - ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll + ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll + True - False - ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll + ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll + True ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll @@ -149,7 +150,9 @@ + + @@ -181,6 +184,11 @@ {75fd4163-333c-4dd5-854d-2ef294e45d94} SmartStore.Web.Framework + + {4f1f649c-1020-45be-a487-f416d9297ff3} + SmartStore.Web + False + @@ -223,6 +231,9 @@ PreserveNewest + + PreserveNewest + @@ -232,6 +243,10 @@ bin\ + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + @@ -262,7 +277,7 @@ del "$(TargetDir)MiniProfiler.pdb" /q /s - +del "$(TargetDir)MiniProfiler.EntityFramework6.pdb" /q /s - + @@ -128,13 +136,10 @@ + + + + - - - - - - - - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/packages.config b/src/Plugins/SmartStore.DevTools/packages.config index 5229e84c2e..78e5bfe16a 100644 --- a/src/Plugins/SmartStore.DevTools/packages.config +++ b/src/Plugins/SmartStore.DevTools/packages.config @@ -2,7 +2,7 @@ - + diff --git a/src/Plugins/SmartStore.DiscountRules/Localization/resources.en-us.xml b/src/Plugins/SmartStore.DiscountRules/Localization/resources.en-us.xml index 66720d5e11..59f4d39fe1 100644 --- a/src/Plugins/SmartStore.DiscountRules/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.DiscountRules/Localization/resources.en-us.xml @@ -1,5 +1,12 @@  + + Standard discount rules + + + Provides standard discount rules, e.g. "Country is", "Customer group is", "Has amount x spent" etc. + + Discount could not be loaded @@ -8,6 +15,9 @@ + + The customer must be assigned to a customer group + @@ -23,6 +33,9 @@ + + Country of invoice address is + @@ -39,6 +52,9 @@ + + Country of delivery address is + @@ -55,6 +71,9 @@ + + Limited to specific shop + @@ -71,6 +90,9 @@ + + Customer has one of the following products in his shopping cart + @@ -84,6 +106,9 @@ + + Customer has the following products in his shopping cart + @@ -97,6 +122,9 @@ + + Customer has spent amount x + diff --git a/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj b/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj index 424b87957d..06e2098cb6 100644 --- a/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj +++ b/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj @@ -1,5 +1,6 @@  + @@ -20,7 +21,7 @@ Properties SmartStore.DiscountRules SmartStore.DiscountRules - v4.5.2 + v4.6.1 512 @@ -43,6 +44,9 @@ + + + true @@ -240,6 +244,12 @@ + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + - + @@ -120,13 +128,10 @@ + + + + - - - - - - - - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs b/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs index dfc2dae620..616ad9e28d 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs +++ b/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs @@ -1,4 +1,5 @@ using System.Web.Mvc; +using SmartStore.ComponentModel; using SmartStore.Core.Domain.Customers; using SmartStore.FacebookAuth.Core; using SmartStore.FacebookAuth.Models; @@ -12,7 +13,7 @@ namespace SmartStore.FacebookAuth.Controllers { - //[UnitOfWork] + //[UnitOfWork] public class ExternalAuthFacebookController : PluginControllerBase { private readonly IOAuthProviderFacebookAuthorizer _oAuthProviderFacebookAuthorizer; @@ -26,15 +27,15 @@ public ExternalAuthFacebookController( ExternalAuthenticationSettings externalAuthenticationSettings, ICommonServices services) { - this._oAuthProviderFacebookAuthorizer = oAuthProviderFacebookAuthorizer; - this._openAuthenticationService = openAuthenticationService; - this._externalAuthenticationSettings = externalAuthenticationSettings; - this._services = services; + _oAuthProviderFacebookAuthorizer = oAuthProviderFacebookAuthorizer; + _openAuthenticationService = openAuthenticationService; + _externalAuthenticationSettings = externalAuthenticationSettings; + _services = services; } private bool HasPermission(bool notify = true) { - bool hasPermission = _services.Permissions.Authorize(StandardPermissionProvider.ManageExternalAuthenticationMethods); + var hasPermission = _services.Permissions.Authorize(StandardPermissionProvider.ManageExternalAuthenticationMethods); if (notify && !hasPermission) NotifyError(_services.Localization.GetResource("Admin.AccessDenied.Description")); @@ -42,52 +43,45 @@ private bool HasPermission(bool notify = true) return hasPermission; } - [AdminAuthorize, ChildActionOnly] - public ActionResult Configure() + [LoadSetting, AdminAuthorize, ChildActionOnly] + public ActionResult Configure(FacebookExternalAuthSettings settings) { if (!HasPermission(false)) return AccessDeniedPartialView(); var model = new ConfigurationModel(); - int storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); - var settings = _services.Settings.LoadSetting(storeScope); + MiniMapper.Map(settings, model); - model.ClientKeyIdentifier = settings.ClientKeyIdentifier; - model.ClientSecret = settings.ClientSecret; + var host = _services.StoreContext.CurrentStore.GetHost(true); + model.RedirectUrl = $"{host}Plugins/SmartStore.FacebookAuth/logincallback/"; - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - storeDependingSettingHelper.GetOverrideKeys(settings, model, storeScope, _services.Settings); - - return View(model); + return View(model); } - [HttpPost, AdminAuthorize, ChildActionOnly] - public ActionResult Configure(ConfigurationModel model, FormCollection form) + [SaveSetting, HttpPost, AdminAuthorize, ChildActionOnly] + public ActionResult Configure(FacebookExternalAuthSettings settings, ConfigurationModel model) { if (!HasPermission(false)) - return Configure(); + return Configure(settings); if (!ModelState.IsValid) - return Configure(); - - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - int storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); - var settings = _services.Settings.LoadSetting(storeScope); - - settings.ClientKeyIdentifier = model.ClientKeyIdentifier; - settings.ClientSecret = model.ClientSecret; - - storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, _services.Settings); + return Configure(settings); + MiniMapper.Map(model, settings); NotifySuccess(_services.Localization.GetResource("Admin.Common.DataSuccessfullySaved")); - return Configure(); + return RedirectToConfiguration(FacebookExternalAuthMethod.SystemName, true); } [ChildActionOnly] public ActionResult PublicInfo() { - return View(); + var settings = _services.Settings.LoadSetting(_services.StoreContext.CurrentStore.Id); + + if (settings.ClientKeyIdentifier.HasValue() && settings.ClientSecret.HasValue()) + return View(); + else + return new EmptyResult(); } public ActionResult Login(string returnUrl) @@ -103,7 +97,7 @@ public ActionResult LoginCallback(string returnUrl) [NonAction] private ActionResult LoginInternal(string returnUrl, bool verifyResponse) { - var processor = _openAuthenticationService.LoadExternalAuthenticationMethodBySystemName(Provider.SystemName, _services.StoreContext.CurrentStore.Id); + var processor = _openAuthenticationService.LoadExternalAuthenticationMethodBySystemName(FacebookExternalAuthMethod.SystemName, _services.StoreContext.CurrentStore.Id); if (processor == null || !processor.IsMethodActive(_externalAuthenticationSettings)) { throw new SmartException("Facebook module cannot be loaded"); @@ -118,8 +112,9 @@ private ActionResult LoginInternal(string returnUrl, bool verifyResponse) case OpenAuthenticationStatus.Error: { if (!result.Success) - foreach (var error in result.Errors) - NotifyError(error); + { + result.Errors.Each(x => NotifyError(x)); + } return new RedirectResult(Url.LogOn(returnUrl)); } @@ -129,16 +124,15 @@ private ActionResult LoginInternal(string returnUrl, bool verifyResponse) } case OpenAuthenticationStatus.AutoRegisteredEmailValidation: { - //result - return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.EmailValidation }); + return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.EmailValidation, returnUrl }); } case OpenAuthenticationStatus.AutoRegisteredAdminApproval: { - return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.AdminApproval }); + return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.AdminApproval, returnUrl }); } case OpenAuthenticationStatus.AutoRegisteredStandard: { - return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.Standard }); + return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.Standard, returnUrl }); } default: break; diff --git a/src/Plugins/SmartStore.FacebookAuth/Core/FacebookOAuth2Client.cs b/src/Plugins/SmartStore.FacebookAuth/Core/FacebookOAuth2Client.cs index df2287b070..e11239ef4f 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Core/FacebookOAuth2Client.cs +++ b/src/Plugins/SmartStore.FacebookAuth/Core/FacebookOAuth2Client.cs @@ -123,8 +123,7 @@ public override AuthenticationResult VerifyAuthentication(HttpContextBase contex // add the access token to the user data dictionary just in case page developers want to use it userData["accesstoken"] = accessToken; - return new AuthenticationResult( - isSuccessful: true, provider: this.ProviderName, providerUserId: id, userName: name, extraData: userData); + return new AuthenticationResult(isSuccessful: true, provider: ProviderName, providerUserId: id, userName: name, extraData: userData); } protected override Uri GetServiceLoginUrl(Uri returnUrl) @@ -182,21 +181,24 @@ protected override string QueryAccessToken(Uri returnUrl, string authorizationCo var webRequest = (HttpWebRequest)WebRequest.Create(uri); string accessToken = null; - HttpWebResponse response = (HttpWebResponse)webRequest.GetResponse(); - // handle response from FB - // this will not be a url with params like the first request to get the 'code' - Encoding rEncoding = Encoding.GetEncoding(response.CharacterSet); - - using (StreamReader sr = new StreamReader(response.GetResponseStream(), rEncoding)) + using (var response = (HttpWebResponse)webRequest.GetResponse()) { - var serializer = new JavaScriptSerializer(); - var jsonObject = serializer.DeserializeObject(sr.ReadToEnd()); - var jConvert = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(jsonObject)); + // handle response from FB + // this will not be a url with params like the first request to get the 'code' + Encoding rEncoding = Encoding.GetEncoding(response.CharacterSet); + + using (var sr = new StreamReader(response.GetResponseStream(), rEncoding)) + { + var serializer = new JavaScriptSerializer(); + var jsonObject = serializer.DeserializeObject(sr.ReadToEnd()); + var jConvert = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(jsonObject)); - Dictionary desirializedJsonObject = JsonConvert.DeserializeObject>(jConvert.ToString()); - accessToken = desirializedJsonObject["access_token"].ToString(); + Dictionary desirializedJsonObject = JsonConvert.DeserializeObject>(jConvert.ToString()); + accessToken = desirializedJsonObject["access_token"].ToString(); + } } + return accessToken; } diff --git a/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs b/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs index fe65c7bf72..6f987af313 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs +++ b/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs @@ -11,6 +11,7 @@ using DotNetOpenAuth.AspNet; using Newtonsoft.Json.Linq; using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Logging; using SmartStore.Services; using SmartStore.Services.Authentication.External; @@ -38,12 +39,16 @@ public FacebookProviderAuthorizer(IExternalAuthorizer authorizer, HttpContextBase httpContext, ICommonServices services) { - this._authorizer = authorizer; - this._openAuthenticationService = openAuthenticationService; - this._externalAuthenticationSettings = externalAuthenticationSettings; - this._httpContext = httpContext; - this._services = services; - } + _authorizer = authorizer; + _openAuthenticationService = openAuthenticationService; + _externalAuthenticationSettings = externalAuthenticationSettings; + _httpContext = httpContext; + _services = services; + + Logger = NullLogger.Instance; + } + + public ILogger Logger { get; set; } #endregion @@ -66,9 +71,34 @@ private FacebookOAuth2Client FacebookApplication private AuthorizeState VerifyAuthentication(string returnUrl) { - var authResult = this.FacebookApplication.VerifyAuthentication(_httpContext, GenerateLocalCallbackUri()); + string error = null; + AuthenticationResult authResult = null; + + try + { + authResult = this.FacebookApplication.VerifyAuthentication(_httpContext, GenerateLocalCallbackUri()); + } + catch (WebException wexc) + { + using (var response = wexc.Response as HttpWebResponse) + { + error = response.StatusDescription; + + var enc = Encoding.GetEncoding(response.CharacterSet); + using (var reader = new StreamReader(response.GetResponseStream(), enc)) + { + var rawResponse = reader.ReadToEnd(); + Logger.Log(LogLevel.Error, new Exception(rawResponse), response.StatusDescription, null); + } + } + } + catch (Exception exception) + { + error = exception.ToString(); + Logger.Log(LogLevel.Error, exception, null, null); + } - if (authResult.IsSuccessful) + if (authResult != null && authResult.IsSuccessful) { if (!authResult.ExtraData.ContainsKey("id")) throw new Exception("Authentication result does not contain id data"); @@ -76,7 +106,7 @@ private AuthorizeState VerifyAuthentication(string returnUrl) if (!authResult.ExtraData.ContainsKey("accesstoken")) throw new Exception("Authentication result does not contain accesstoken data"); - var parameters = new OAuthAuthenticationParameters(Provider.SystemName) + var parameters = new OAuthAuthenticationParameters(FacebookExternalAuthMethod.SystemName) { ExternalIdentifier = authResult.ProviderUserId, OAuthToken = authResult.ExtraData["accesstoken"], @@ -91,11 +121,17 @@ private AuthorizeState VerifyAuthentication(string returnUrl) return new AuthorizeState(returnUrl, result); } - var state = new AuthorizeState(returnUrl, OpenAuthenticationStatus.Error); + if (error.IsEmpty() && authResult != null && authResult.Error != null) + { + error = authResult.Error.Message; + } + if (error.IsEmpty()) + { + error = _services.Localization.GetResource("Admin.Common.UnknownError"); + } - state.AddError(authResult.Error != null - ? authResult.Error.Message - : _services.Localization.GetResource("Admin.Common.UnknownError")); + var state = new AuthorizeState(returnUrl, OpenAuthenticationStatus.Error); + state.AddError(error); return state; } diff --git a/src/Plugins/SmartStore.FacebookAuth/Core/Provider.cs b/src/Plugins/SmartStore.FacebookAuth/Core/Provider.cs deleted file mode 100644 index 61ed3cde9c..0000000000 --- a/src/Plugins/SmartStore.FacebookAuth/Core/Provider.cs +++ /dev/null @@ -1,14 +0,0 @@ - -namespace SmartStore.FacebookAuth.Core -{ - public static class Provider - { - public static string SystemName - { - get - { - return "SmartStore.FacebookAuth"; - } - } - } -} \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/FacebookExternalAuthMethod.cs b/src/Plugins/SmartStore.FacebookAuth/FacebookExternalAuthMethod.cs index d3a0ea7b26..86ed1ab677 100644 --- a/src/Plugins/SmartStore.FacebookAuth/FacebookExternalAuthMethod.cs +++ b/src/Plugins/SmartStore.FacebookAuth/FacebookExternalAuthMethod.cs @@ -6,41 +6,33 @@ namespace SmartStore.FacebookAuth { - /// - /// Facebook externalAuth processor - /// - public class FacebookExternalAuthMethod : BasePlugin, IExternalAuthenticationMethod, IConfigurable + /// + /// Facebook externalAuth processor + /// + public class FacebookExternalAuthMethod : BasePlugin, IExternalAuthenticationMethod, IConfigurable { - #region Fields - private readonly FacebookExternalAuthSettings _facebookExternalAuthSettings; private readonly ILocalizationService _localizationService; - #endregion - - #region Ctor - public FacebookExternalAuthMethod(FacebookExternalAuthSettings facebookExternalAuthSettings, ILocalizationService localizationService) { - this._facebookExternalAuthSettings = facebookExternalAuthSettings; + _facebookExternalAuthSettings = facebookExternalAuthSettings; _localizationService = localizationService; } - #endregion + public static string SystemName => "SmartStore.FacebookAuth"; - #region Methods - - /// - /// Gets a route for provider configuration - /// - /// Action name - /// Controller name - /// Route values - public void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) + /// + /// Gets a route for provider configuration + /// + /// Action name + /// Controller name + /// Route values + public void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) { actionName = "Configure"; controllerName = "ExternalAuthFacebook"; - routeValues = new RouteValueDictionary(new { Namespaces = "SmartStore.FacebookAuth.Controllers", area = Provider.SystemName }); + routeValues = new RouteValueDictionary(new { Namespaces = "SmartStore.FacebookAuth.Controllers", area = SystemName }); } /// @@ -53,7 +45,7 @@ public void GetPublicInfoRoute(out string actionName, out string controllerName, { actionName = "PublicInfo"; controllerName = "ExternalAuthFacebook"; - routeValues = new RouteValueDictionary(new { Namespaces = "SmartStore.FacebookAuth.Controllers", area = Provider.SystemName }); + routeValues = new RouteValueDictionary(new { Namespaces = "SmartStore.FacebookAuth.Controllers", area = SystemName }); } /// @@ -61,21 +53,16 @@ public void GetPublicInfoRoute(out string actionName, out string controllerName, /// public override void Install() { - //locales - _localizationService.ImportPluginResourcesFromXml(this.PluginDescriptor); + _localizationService.ImportPluginResourcesFromXml(PluginDescriptor); base.Install(); } public override void Uninstall() { - //locales - _localizationService.DeleteLocaleStringResources(this.PluginDescriptor.ResourceRootKey); + _localizationService.DeleteLocaleStringResources(PluginDescriptor.ResourceRootKey); base.Uninstall(); - } - - #endregion - + } } } diff --git a/src/Plugins/SmartStore.FacebookAuth/Localization/resources.de-de.xml b/src/Plugins/SmartStore.FacebookAuth/Localization/resources.de-de.xml index 34d1ab5e11..6839a4255c 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.FacebookAuth/Localization/resources.de-de.xml @@ -1,28 +1,35 @@  - - Facebook - + + Facebook + - -
                            • Die Facebook Zugangsdaten (s.u.) erhalten Sie, indem Sie bei Facebook unter Entwickler eine Anwendung mit der Option Facebook-Anmeldung erstellen.
                            • -
                            • Aktivieren Sie in den Kundeneinstellungen die automatische Registrierung, falls extern autorisierte Besucher automatisch registriert werden sollen.
                            • -
                            ]]> + So richten Sie die Facebook Authentifizierungen ein:

                              +
                            • Erstellen Sie bei Facebook eine neue App vom Typ Facebook Login. Deren Zugangsdaten tragen Sie bitte unten in das Formular ein.
                            • +
                            • Unter Facebook Login > Einstellungen > Gültige OAuth Redirect URIs wird die unten aufgeführte URL eingetragen.
                            • +
                            • Aktivieren Sie in den Kundeneinstellungen des Shops die automatische Registrierung, falls extern autorisierte Besucher automatisch registriert werden sollen.
                            • +
                            ]]>
                            - Mit Facebook anmelden - - - Benutzer-Schlüssel-Identifikator (App ID) - - - Geben Sie hier Ihren Benutzer-Schlüssel-Identifikator (App ID) an. - - - Benutzer-Sicherheitsschlüssel (App Secret) - - - Geben Sie hier Ihren Benutzer-Sicherheitsschlüssel (App Secret) an. - + Mit Facebook anmelden + + + Benutzer-Schlüssel-Identifikator (App ID) + + + Geben Sie hier Ihren Benutzer-Schlüssel-Identifikator (App ID) an. + + + Benutzer-Sicherheitsschlüssel (App Secret) + + + Geben Sie hier Ihren Benutzer-Sicherheitsschlüssel (App Secret) an. + + + Weiterleitungs URL (OAuth Redirect URI) + + + Die bei Facebook einzutragende Weiterleitungs URL (OAuth Redirect URI). + \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/Localization/resources.en-us.xml b/src/Plugins/SmartStore.FacebookAuth/Localization/resources.en-us.xml index e1594d213b..fdf0710bfa 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.FacebookAuth/Localization/resources.en-us.xml @@ -1,28 +1,35 @@  - - Facebook - + + Facebook + - -
                          • Create an application with the facebook login option in the developer area at facebook to get the facebook access data (see below).
                          • -
                          • Activate auto registration in the customer settings if you want externally authorized visitors to be registered automatically.
                          • -
                          ]]> + How to set up Facebook authentication:

                            +
                          • Create a new app on Facebook of type Facebook Login. Please enter their access data in the form below.
                          • +
                          • Enter the URL listed below under Facebook Login > Settings > Valid OAuth Redirect URIs.
                          • +
                          • Activate automatic registration in the shop's customer settings if you want to register externally authorized visitors automatically.
                          • +
                          ]]>
                          - - Sign in with Facebook - - - Client key identifier (App ID) - - - Enter your client key identifier (App ID) here. - - - Client secret (App Secret) - - - Enter your client secret (App Secret) here. - + + Sign in with Facebook + + + Client key identifier (App ID) + + + Enter your client key identifier (App ID) here. + + + Client secret (App Secret) + + + Enter your client secret (App Secret) here. + + + OAuth Redirect URI + + + The OAuth Redirect URI to be entered on Facebook. + \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/Models/ConfigurationModel.cs b/src/Plugins/SmartStore.FacebookAuth/Models/ConfigurationModel.cs index 928feab5b2..dc0a098627 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Models/ConfigurationModel.cs +++ b/src/Plugins/SmartStore.FacebookAuth/Models/ConfigurationModel.cs @@ -10,5 +10,8 @@ public class ConfigurationModel : ModelBase [SmartResourceDisplayName("Plugins.ExternalAuth.Facebook.ClientSecret")] public string ClientSecret { get; set; } - } + + [SmartResourceDisplayName("Plugins.ExternalAuth.Facebook.RedirectUri")] + public string RedirectUrl { get; set; } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/RouteProvider.cs b/src/Plugins/SmartStore.FacebookAuth/RouteProvider.cs index 90465eb41a..10f1f4c1ac 100644 --- a/src/Plugins/SmartStore.FacebookAuth/RouteProvider.cs +++ b/src/Plugins/SmartStore.FacebookAuth/RouteProvider.cs @@ -1,11 +1,10 @@ using System.Web.Mvc; using System.Web.Routing; -using SmartStore.FacebookAuth.Core; using SmartStore.Web.Framework.Routing; namespace SmartStore.FacebookAuth { - public partial class RouteProvider : IRouteProvider + public partial class RouteProvider : IRouteProvider { public void RegisterRoutes(RouteCollection routes) { @@ -14,7 +13,7 @@ public void RegisterRoutes(RouteCollection routes) new { controller = "ExternalAuthFacebook" }, new[] { "SmartStore.FacebookAuth.Controllers" } ) - .DataTokens["area"] = Provider.SystemName; + .DataTokens["area"] = FacebookExternalAuthMethod.SystemName; } public int Priority { diff --git a/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj b/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj index 3fee320bb8..41e2ae7d49 100644 --- a/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj +++ b/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj @@ -20,7 +20,7 @@ Properties SmartStore.FacebookAuth SmartStore.FacebookAuth - v4.5.2 + v4.6.1 512 @@ -45,6 +45,7 @@ + true @@ -183,7 +184,6 @@ - diff --git a/src/Plugins/SmartStore.FacebookAuth/Views/ExternalAuthFacebook/Configure.cshtml b/src/Plugins/SmartStore.FacebookAuth/Views/ExternalAuthFacebook/Configure.cshtml index 8bbf05261a..1658e3b419 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Views/ExternalAuthFacebook/Configure.cshtml +++ b/src/Plugins/SmartStore.FacebookAuth/Views/ExternalAuthFacebook/Configure.cshtml @@ -1,16 +1,14 @@ -@{ - Layout = ""; -} -@model SmartStore.FacebookAuth.Models.ConfigurationModel +@model SmartStore.FacebookAuth.Models.ConfigurationModel @using SmartStore.Web.Framework; +@{ + Layout = ""; +} -
                          - +
                          @Html.Raw(T("Plugins.ExternalAuth.Facebook.AdminInstruction")) +
                          -
                          - @{ Html.RenderAction("StoreScopeConfiguration", "Setting", new { area = "Admin" }); } @Html.ValidationSummary(false) @@ -18,8 +16,9 @@ @using (Html.BeginForm()) {
                          -
                          @@ -41,5 +40,13 @@ @Html.ValidationMessageFor(model => model.ClientSecret) + + + +
                          + @Html.SmartLabelFor(model => model.RedirectUrl) + + @Html.TextBoxFor(model => model.RedirectUrl, new { @readonly = "readonly", @class = "form-control-plaintext" }) +
                          } \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/web.config b/src/Plugins/SmartStore.FacebookAuth/web.config index 0a90ea0459..3c54ff640a 100644 --- a/src/Plugins/SmartStore.FacebookAuth/web.config +++ b/src/Plugins/SmartStore.FacebookAuth/web.config @@ -5,8 +5,16 @@ + - + @@ -128,13 +136,10 @@ + + + + - - - - - - - - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleAnalytics/Controllers/WidgetsGoogleAnalyticsController.cs b/src/Plugins/SmartStore.GoogleAnalytics/Controllers/WidgetsGoogleAnalyticsController.cs index 9534d76730..df4c49af25 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/Controllers/WidgetsGoogleAnalyticsController.cs +++ b/src/Plugins/SmartStore.GoogleAnalytics/Controllers/WidgetsGoogleAnalyticsController.cs @@ -16,10 +16,10 @@ using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.Settings; +using SmartStore.ComponentModel; namespace SmartStore.GoogleAnalytics.Controllers { - public class WidgetsGoogleAnalyticsController : SmartController { private readonly IWorkContext _workContext; @@ -42,55 +42,42 @@ public WidgetsGoogleAnalyticsController(IWorkContext workContext, this._categoryService = categoryService; } - [AdminAuthorize] - [ChildActionOnly] - public ActionResult Configure() + [AdminAuthorize, ChildActionOnly, LoadSetting] + public ActionResult Configure(GoogleAnalyticsSettings settings) { - //load settings for a chosen store scope - var storeScope = this.GetActiveStoreScopeConfiguration(_storeService, _workContext); - var googleAnalyticsSettings = _settingService.LoadSetting(storeScope); var model = new ConfigurationModel(); - model.GoogleId = googleAnalyticsSettings.GoogleId; - model.TrackingScript = googleAnalyticsSettings.TrackingScript; - model.EcommerceScript = googleAnalyticsSettings.EcommerceScript; - model.EcommerceDetailScript = googleAnalyticsSettings.EcommerceDetailScript; + MiniMapper.Map(settings, model); - model.ZoneId = googleAnalyticsSettings.WidgetZone; + model.ZoneId = settings.WidgetZone; model.AvailableZones.Add(new SelectListItem() { Text = " HTML tag", Value = "head_html_tag"}); model.AvailableZones.Add(new SelectListItem() { Text = "Before end HTML tag", Value = "body_end_html_tag_before" }); - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - storeDependingSettingHelper.GetOverrideKeys(googleAnalyticsSettings, model, storeScope, _settingService); - return View(model); } - [HttpPost] - [AdminAuthorize] - [ChildActionOnly] - [ValidateInput(false)] + [HttpPost, AdminAuthorize, ChildActionOnly, ValidateInput(false)] public ActionResult Configure(ConfigurationModel model, FormCollection form) { + var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); + ModelState.Clear(); - //load settings for a chosen store scope - var storeScope = this.GetActiveStoreScopeConfiguration(_storeService, _workContext); - var googleAnalyticsSettings = _settingService.LoadSetting(storeScope); - googleAnalyticsSettings.GoogleId = model.GoogleId; - googleAnalyticsSettings.TrackingScript = model.TrackingScript; - googleAnalyticsSettings.EcommerceScript = model.EcommerceScript; - googleAnalyticsSettings.EcommerceDetailScript = model.EcommerceDetailScript; - googleAnalyticsSettings.WidgetZone = model.ZoneId; + MiniMapper.Map(model, settings); + settings.WidgetZone = model.ZoneId; - using (_settingService.BeginScope()) + using (Services.Settings.BeginScope()) { - _settingService.SaveSetting(googleAnalyticsSettings, x => x.WidgetZone, 0, false); + storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, Services.Settings); + } - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - storeDependingSettingHelper.UpdateSettings(googleAnalyticsSettings, form, storeScope, _settingService); + using (Services.Settings.BeginScope()) + { + _settingService.SaveSetting(settings, x => x.WidgetZone, 0, false); } - return Configure(); + return RedirectToConfiguration("SmartStore.GoogleAnalytics"); } [ChildActionOnly] diff --git a/src/Plugins/SmartStore.GoogleAnalytics/SmartStore.GoogleAnalytics.csproj b/src/Plugins/SmartStore.GoogleAnalytics/SmartStore.GoogleAnalytics.csproj index 57e4920966..cb8fa2c279 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/SmartStore.GoogleAnalytics.csproj +++ b/src/Plugins/SmartStore.GoogleAnalytics/SmartStore.GoogleAnalytics.csproj @@ -20,7 +20,7 @@ Properties SmartStore.GoogleAnalytics SmartStore.GoogleAnalytics - v4.5.2 + v4.6.1 512 diff --git a/src/Plugins/SmartStore.GoogleAnalytics/Views/WidgetsGoogleAnalytics/Configure.cshtml b/src/Plugins/SmartStore.GoogleAnalytics/Views/WidgetsGoogleAnalytics/Configure.cshtml index ab4ac217aa..bf49159e0f 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/Views/WidgetsGoogleAnalytics/Configure.cshtml +++ b/src/Plugins/SmartStore.GoogleAnalytics/Views/WidgetsGoogleAnalytics/Configure.cshtml @@ -1,9 +1,8 @@ -@{ - Layout = ""; -} -@model SmartStore.GoogleAnalytics.Models.ConfigurationModel +@model SmartStore.GoogleAnalytics.Models.ConfigurationModel @using SmartStore.Web.Framework; - +@{ + Layout = ""; +}
                          @Html.Raw(@T("Plugins.Widgets.GoogleAnalytics.AdminInstruction"))
                          @@ -11,62 +10,62 @@ @{ Html.RenderAction("StoreScopeConfiguration", "Setting", new { area = "Admin" }); }
                          -
                          @using (Html.BeginForm()) { - - - - - - - - - - - - - - - - - - - - - -
                          - @Html.SmartLabelFor(model => model.ZoneId) - - @Html.DropDownListFor(model => model.ZoneId, Model.AvailableZones) - @Html.ValidationMessageFor(model => model.ZoneId) -
                          - @Html.SmartLabelFor(model => model.GoogleId) - - @Html.SettingEditorFor(model => model.GoogleId) - @Html.ValidationMessageFor(model => model.GoogleId) -
                          - @Html.SmartLabelFor(model => model.TrackingScript) - - @Html.SettingOverrideCheckbox(model => model.TrackingScript) - @Html.TextAreaFor(model => model.TrackingScript, new { style = "Width: 600px; Height: 200px;" }) - @Html.ValidationMessageFor(model => model.TrackingScript) -
                          - @Html.SmartLabelFor(model => model.EcommerceScript) - - @Html.SettingOverrideCheckbox(model => model.EcommerceScript) - @Html.TextAreaFor(model => model.EcommerceScript, new { style = "Width: 600px; Height: 50px;" }) - @Html.ValidationMessageFor(model => model.EcommerceScript) -
                          - @Html.SmartLabelFor(model => model.EcommerceDetailScript) - - @Html.SettingOverrideCheckbox(model => model.EcommerceDetailScript) - @Html.TextAreaFor(model => model.EcommerceDetailScript, new { style = "Width: 600px; Height: 50px;" }) - @Html.ValidationMessageFor(model => model.EcommerceDetailScript) -
                          - + + + + + + + + + + + + + + + + + + + + + +
                          + @Html.SmartLabelFor(model => model.ZoneId) + + @Html.DropDownListFor(model => model.ZoneId, Model.AvailableZones) + @Html.ValidationMessageFor(model => model.ZoneId) +
                          + @Html.SmartLabelFor(model => model.GoogleId) + + @Html.SettingEditorFor(model => model.GoogleId) + @Html.ValidationMessageFor(model => model.GoogleId) +
                          + @Html.SmartLabelFor(model => model.TrackingScript) + + @Html.SettingEditorFor(model => model.TrackingScript, + Html.TextAreaFor(model => model.TrackingScript, new { style = "height: 200px;" })) + @Html.ValidationMessageFor(model => model.TrackingScript) +
                          + @Html.SmartLabelFor(model => model.EcommerceScript) + + @Html.SettingEditorFor(model => model.EcommerceScript, + Html.TextAreaFor(model => model.EcommerceScript, new { style = "height: 50px;" })) + @Html.ValidationMessageFor(model => model.EcommerceScript) +
                          + @Html.SmartLabelFor(model => model.EcommerceDetailScript) + + @Html.SettingEditorFor(model => model.EcommerceDetailScript, + Html.TextAreaFor(model => model.EcommerceDetailScript, new { style = "height: 200px;" })) + @Html.ValidationMessageFor(model => model.EcommerceDetailScript) +
                          } \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleAnalytics/web.config b/src/Plugins/SmartStore.GoogleAnalytics/web.config index 275d035d5a..5e59e8cb6a 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/web.config +++ b/src/Plugins/SmartStore.GoogleAnalytics/web.config @@ -4,8 +4,16 @@ + - + @@ -119,13 +127,10 @@ + + + + - - - - - - - - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Content/smartstore.gmc.css b/src/Plugins/SmartStore.GoogleMerchantCenter/Content/smartstore.gmc.css deleted file mode 100644 index 4a8bfa9c72..0000000000 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Content/smartstore.gmc.css +++ /dev/null @@ -1,6 +0,0 @@ -.edit-taxonomy { - min-width: 460px; -} -.gmc-product-search { - margin-bottom: 8px !important; -} \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedGoogleMerchantCenterController.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedGoogleMerchantCenterController.cs index 6c37461c87..ed8533af16 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedGoogleMerchantCenterController.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedGoogleMerchantCenterController.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Web.Mvc; @@ -18,13 +19,13 @@ namespace SmartStore.GoogleMerchantCenter.Controllers public class FeedGoogleMerchantCenterController : PluginControllerBase { private readonly IGoogleFeedService _googleFeedService; - private readonly AdminAreaSettings _adminAreaSettings; - private readonly IExportProfileService _exportService; + private readonly Lazy _adminAreaSettings; + private readonly Lazy _exportService; public FeedGoogleMerchantCenterController( IGoogleFeedService googleFeedService, - AdminAreaSettings adminAreaSettings, - IExportProfileService exportService) + Lazy adminAreaSettings, + Lazy exportService) { _googleFeedService = googleFeedService; _adminAreaSettings = adminAreaSettings; @@ -75,9 +76,10 @@ public ActionResult ProductEditTab(int productId) ViewBag.DefaultIsBundle = ""; ViewBag.DefaultEnergyEfficiencyClass = notSpecified; ViewBag.DefaultCustomLabel = ""; + ViewBag.LanguageSeoCode = Services.WorkContext.WorkingLanguage.UniqueSeoCode.EmptyNull().ToLower(); - // we do not have export profile context here, so we simply use the first profile - var profile = _exportService.GetExportProfilesBySystemName(GmcXmlExportProvider.SystemName).FirstOrDefault(); + // We do not have export profile context here, so we simply use the first profile. + var profile = _exportService.Value.GetExportProfilesBySystemName(GmcXmlExportProvider.SystemName).FirstOrDefault(); if (profile != null) { var config = XmlHelper.Deserialize(profile.ProviderConfigData, typeof(ProfileConfigurationModel)) as ProfileConfigurationModel; @@ -99,7 +101,11 @@ public ActionResult ProductEditTab(int productId) ViewBag.DefaultAgeGroup = T("Plugins.Feed.Froogle.AgeGroup" + culture.TextInfo.ToTitleCase(config.AgeGroup)); } } - } + } + + ViewBag.AvailableCategories = model.Taxonomy.HasValue() + ? new List { new SelectListItem { Text = model.Taxonomy, Value = model.Taxonomy, Selected = true } } + : new List(); ViewBag.AvailableGenders = new List { @@ -124,37 +130,61 @@ public ActionResult ProductEditTab(int productId) return result; } - public ActionResult GoogleCategories() - { - var categories = _googleFeedService.GetTaxonomyList(); - return Json(categories, JsonRequestBehavior.AllowGet); - } - public ActionResult Configure() { var model = new FeedGoogleMerchantCenterModel(); - model.GridPageSize = _adminAreaSettings.GridPageSize; - model.AvailableGoogleCategories = _googleFeedService.GetTaxonomyList(); + model.GridPageSize = _adminAreaSettings.Value.GridPageSize; + model.LanguageSeoCode = Services.WorkContext.WorkingLanguage.UniqueSeoCode.EmptyNull().ToLower(); model.EnergyEfficiencyClasses = T("Plugins.Feed.Froogle.EnergyEfficiencyClasses").Text.SplitSafe(","); return View(model); } + [HttpPost, GridAction(EnableCustomBinding = true)] + public ActionResult GoogleProductList(GridCommand command, string searchProductName, string touched) + { + return new JsonResult + { + Data = _googleFeedService.GetGridModel(command, searchProductName, touched) + }; + } + [HttpPost] public ActionResult GoogleProductEdit(int pk, string name, string value) { _googleFeedService.Upsert(pk, name, value); - return this.Content(""); + return new EmptyResult(); } - [HttpPost, GridAction(EnableCustomBinding = true)] - public ActionResult GoogleProductList(GridCommand command, string searchProductName, string touched) + public JsonResult GetGoogleCategories(string search, int? page) { + page = page ?? 1; + const int take = 100; + var skip = (page.Value - 1) * take; + var categories = _googleFeedService.GetTaxonomyList(search); + var hasMoreItems = (page.Value * take) < categories.Count; + //$"{skip}\\{categories.Length} {hasMoreItems}: {search.NaIfEmpty()}".Dump(); + + var items = categories.Select(x => new + { + id = x, + text = x + }) + .Skip(skip) + .Take(take) + .ToList(); + return new JsonResult { - Data = _googleFeedService.GetGridModel(command, searchProductName, touched) + Data = new + { + hasMoreItems, + results = items + }, + MaxJsonLength = int.MaxValue, + JsonRequestBehavior = JsonRequestBehavior.AllowGet }; } } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs index 10b5732247..62ec60a9a6 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs @@ -16,7 +16,7 @@ public class Events : public Events(IGoogleFeedService googleService) { - this._googleService = googleService; + _googleService = googleService; } public void HandleEvent(TabStripCreated eventMessage) @@ -56,7 +56,6 @@ public void HandleEvent(ModelBoundEvent eventMessage) }; } - // map objects entity.AgeGroup = model.AgeGroup; entity.Color = model.Color; entity.Gender = model.Gender; diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Extensions/MiscExtensions.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Extensions/MiscExtensions.cs index 5a8dd0168d..e3a3702bf1 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Extensions/MiscExtensions.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Extensions/MiscExtensions.cs @@ -1,27 +1,9 @@ -using System.Web.Mvc; -using SmartStore.GoogleMerchantCenter.Domain; +using SmartStore.GoogleMerchantCenter.Domain; namespace SmartStore.GoogleMerchantCenter { public static class MiscExtensions { - public static string XEditableLink(this HtmlHelper hlp, string fieldName, string type) - { - string displayText = null; - - if (fieldName == "Gender" || fieldName == "AgeGroup" || fieldName == "Export2" || fieldName == "IsBundle" || fieldName == "IsAdult") - displayText = "<#= {0}Localize #>".FormatInvariant(fieldName); - else - displayText = "<#= {0} #>".FormatInvariant(fieldName); - - string skeleton = - "\" class=\"edit-link-{1}\"" + - " data-pk=\"<#= ProductId #>\" data-name=\"{0}\" data-value=\"<#= {0} #>\" data-inputclass=\"edit-{1}\" data-type=\"{2}\">" + - "{3}"; - - return skeleton.FormatInvariant(fieldName, fieldName.ToLower(), type, displayText); - } - public static bool IsTouched(this GoogleProductRecord p) { if (p != null) @@ -32,6 +14,7 @@ public static bool IsTouched(this GoogleProductRecord p) !p.Export || p.Multipack != 0 || p.IsBundle.HasValue || p.IsAdult.HasValue || p.EnergyEfficiencyClass.HasValue() || p.CustomLabel0.HasValue() || p.CustomLabel1.HasValue() || p.CustomLabel2.HasValue() || p.CustomLabel3.HasValue() || p.CustomLabel4.HasValue(); } + return false; } } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.de-de.xml b/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.de-de.xml index 9f3988d6ff..dff51b0ba2 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.de-de.xml @@ -15,13 +15,21 @@ -
                        • Für die Produktidentifizierung muss entweder die GTIN (z.B. als UPC, EAN etc.) oder der Hersteller samt Hersteller-Artikelnummer (MPN) hinterlegt sein. Google empfiehlt die Angabe aller drei Informationen.
                        • -
                        • Standard Steuer- und Versanddaten sind in den Einstellungen Ihres Google-Merchant-Center-Kontos zu hinterlegen.
                        • -
                        • Mehr Informationen zu benötigten Feldern finden Sie im Artikel Produkt-Feed-Spezifikationen.
                        • -
                        • Eine Liste mit allen gültigen Google-Produkt-Kategorie finden Sie hier.
                        • -
                        ]]> +
                      • Für die Produktidentifizierung muss entweder die GTIN (z.B. als UPC, EAN etc.) oder der Hersteller samt Hersteller-Artikelnummer (MPN) hinterlegt sein. Google empfiehlt die Angabe aller drei Informationen.
                      • +
                      • Standard Steuer- und Versanddaten sind in den Einstellungen Ihres Google-Merchant-Center-Kontos zu hinterlegen.
                      • +
                      • Mehr Informationen zu benötigten Feldern finden Sie im Artikel Produkt-Feed-Spezifikationen.
                      • +
                      • Eine Liste mit allen gültigen Google-Produkt-Kategorie finden Sie hier.
                      • +
                      ]]>
                      + + + Produktdaten + + + Export-Profile + + Allgemeine Einstellungen @@ -164,7 +172,7 @@ Noch nicht bearbeitet
                      - Verfällt in Tagen + Verfällt in Anzahl der Tage, nach dem die Artikel verfallen bzw. nicht mehr angezeigt werden sollen. Der Wert 0 bewirkt, dass kein Verfallsdatum exportiert wird. diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.en-us.xml b/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.en-us.xml index d24bea0de0..ae006a421d 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.en-us.xml @@ -15,13 +15,20 @@ -
                    • Either the GTIN (such as UPC, EAN, etc.) or manufacturer and manufacturer part number (MPN) are required for product identification. Google recommends that all three pieces of information be specified.
                    • -
                    • Specify default tax and shipping values in your Google Merchant Center account settings.
                    • -
                    • In order to get more info about required fields look at the following article Product feed specification.
                    • -
                    • You can find a list of all Google categories here.
                    • -
                    ]]> +
                  • Either the GTIN (such as UPC, EAN, etc.) or manufacturer and manufacturer part number (MPN) are required for product identification. Google recommends that all three pieces of information be specified.
                  • +
                  • Specify default tax and shipping values in your Google Merchant Center account settings.
                  • +
                  • In order to get more info about required fields look at the following article Product feed specification.
                  • +
                  • You can find a list of all Google categories here.
                  • +
                  ]]>
                  + + + Product data + + + Export profiles + General settings @@ -164,7 +171,7 @@ Not edited yet
                  - Expires in days + Expires in Number of days after products should expire or no longer appear. A value of 0 causes that no expiry date will be exported. diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Models/FeedGoogleMerchantCenterModel.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Models/FeedGoogleMerchantCenterModel.cs index 36c5f02799..973d44f295 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Models/FeedGoogleMerchantCenterModel.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Models/FeedGoogleMerchantCenterModel.cs @@ -1,7 +1,5 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using System.Text; -using Newtonsoft.Json; using SmartStore.Core.Domain.Catalog; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Modelling; @@ -11,18 +9,7 @@ namespace SmartStore.GoogleMerchantCenter.Models public class FeedGoogleMerchantCenterModel { public int GridPageSize { get; set; } - - public string[] AvailableGoogleCategories { get; set; } - public string AvailableGoogleCategoriesAsJson - { - get - { - if (AvailableGoogleCategories != null && AvailableGoogleCategories.Length > 0) - return JsonConvert.SerializeObject(AvailableGoogleCategories); - return "[ ]"; - } - } - + public string LanguageSeoCode { get; set; } public string[] EnergyEfficiencyClasses { get; set; } [SmartResourceDisplayName("Plugins.Feed.Froogle.SearchProductName")] @@ -36,7 +23,7 @@ public class GoogleProductModel : ModelBase { public int TotalCount { get; set; } - //this attribute is required to disable editing + // This attribute is required to disable editing. [ScaffoldColumn(false)] public int ProductId { @@ -45,7 +32,7 @@ public int ProductId } public int Id { get; set; } - //this attribute is required to disable editing + // This attribute is required to disable editing. [ReadOnly(true)] [ScaffoldColumn(false)] [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.ProductName")] @@ -62,7 +49,7 @@ public string ProductTypeLabelHint switch (ProductType) { case ProductType.SimpleProduct: - return "smnet-hide"; + return "secondary d-none"; case ProductType.GroupedProduct: return "success"; case ProductType.BundledProduct: diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Models/ProfileConfigurationModel.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Models/ProfileConfigurationModel.cs index 69d81d50dd..8213e76f08 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Models/ProfileConfigurationModel.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Models/ProfileConfigurationModel.cs @@ -1,7 +1,8 @@ using System; +using System.Collections.Generic; +using System.Web.Mvc; using System.Xml.Serialization; using FluentValidation.Attributes; -using Newtonsoft.Json; using SmartStore.GoogleMerchantCenter.Validators; using SmartStore.Web.Framework; @@ -18,22 +19,14 @@ public ProfileConfigurationModel() SpecialPrice = true; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.DefaultGoogleCategory")] - public string DefaultGoogleCategory { get; set; } - [XmlIgnore] - public string[] AvailableGoogleCategories { get; set; } + public string LanguageSeoCode { get; set; } [XmlIgnore] - public string AvailableGoogleCategoriesAsJson - { - get - { - if (AvailableGoogleCategories != null && AvailableGoogleCategories.Length > 0) - return JsonConvert.SerializeObject(AvailableGoogleCategories); - return ""; - } - } + public List AvailableCategories { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.DefaultGoogleCategory")] + public string DefaultGoogleCategory { get; set; } [SmartResourceDisplayName("Plugins.Feed.Froogle.AdditionalImages")] public bool AdditionalImages { get; set; } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs index e17c82ac07..5b81770617 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Web.Mvc; using System.Xml; +using SmartStore.Collections; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Domain.Directory; @@ -10,9 +12,11 @@ using SmartStore.Core.Plugins; using SmartStore.GoogleMerchantCenter.Models; using SmartStore.GoogleMerchantCenter.Services; +using SmartStore.Services; using SmartStore.Services.Catalog; using SmartStore.Services.DataExchange.Export; using SmartStore.Services.Directory; +using SmartStore.Services.Localization; namespace SmartStore.GoogleMerchantCenter.Providers { @@ -35,15 +39,22 @@ public class GmcXmlExportProvider : ExportProviderBase private readonly IGoogleFeedService _googleFeedService; private readonly IMeasureService _measureService; + private readonly ICommonServices _services; + private readonly IProductAttributeService _productAttributeService; private readonly MeasureSettings _measureSettings; + private Multimap _attributeMappings; public GmcXmlExportProvider( IGoogleFeedService googleFeedService, IMeasureService measureService, + ICommonServices services, + IProductAttributeService productAttributeService, MeasureSettings measureSettings) { _googleFeedService = googleFeedService; _measureService = measureService; + _services = services; + _productAttributeService = productAttributeService; _measureSettings = measureSettings; T = NullLocalizer.Instance; @@ -51,6 +62,19 @@ public GmcXmlExportProvider( public Localizer T { get; set; } + private Multimap AttributeMappings + { + get + { + if (_attributeMappings == null) + { + _attributeMappings = _productAttributeService.GetExportFieldMappings("gmc"); + } + + return _attributeMappings; + } + } + private string BasePriceUnits(string value) { const string defaultValue = "kg"; @@ -131,42 +155,74 @@ private void WriteString(XmlWriter writer, string fieldName, string value) } } - private void WriteString( - XmlWriter writer, - Dictionary mappedValues, + private string GetAttributeValue( + Multimap attributeValues, string fieldName, - string value) + int languageId, + string productEditTabValue, + string defaultValue) { - // TODO - if (mappedValues == null) + // 1. attribute export mapping. + if (attributeValues != null && AttributeMappings.ContainsKey(fieldName)) { - // regular product - WriteString(writer, fieldName, value); - } - else - { - // export attribute combination - if (mappedValues.ContainsKey(fieldName)) + foreach (var attributeId in AttributeMappings[fieldName]) { - WriteString(writer, fieldName, mappedValues[fieldName].EmptyNull()); - } - else - { - WriteString(writer, fieldName, value); + if (attributeValues.ContainsKey(attributeId)) + { + var attributeValue = attributeValues[attributeId].FirstOrDefault(x => x.ProductVariantAttribute.ProductAttributeId == attributeId); + if (attributeValue != null) + { + return attributeValue.GetLocalized(x => x.Name, languageId, true, false).EmptyNull(); + } + } } } - } - public static string SystemName - { - get { return "Feeds.GoogleMerchantCenterProductXml"; } + // 2. explicit set to unspecified. + if (defaultValue.IsCaseInsensitiveEqual(Unspecified)) + { + return string.Empty; + } + + // 3. product edit tab value. + if (productEditTabValue.HasValue()) + { + return productEditTabValue; + } + + return defaultValue.EmptyNull(); } - public static string Unspecified + private string GetBaseMeasureWeight() { - get { return "__nospec__"; } + var measureWeightEntity = _measureService.GetMeasureWeightById(_measureSettings.BaseWeightId); + var measureWeight = measureWeightEntity != null + ? measureWeightEntity.SystemKeyword.EmptyNull().ToLower() + : string.Empty; + + switch (measureWeight) + { + case "gram": + case "gramme": + return "g"; + case "mg": + case "milligramme": + case "milligram": + return "mg"; + case "lb": + return "lb"; + case "ounce": + case "oz": + return "oz"; + default: + return "kg"; + } } + public static string SystemName => "Feeds.GoogleMerchantCenterProductXml"; + + public static string Unspecified => "__nospec__"; + public override ExportConfigurationInfo ConfigurationInfo { get @@ -179,30 +235,47 @@ public override ExportConfigurationInfo ConfigurationInfo { var model = (obj as ProfileConfigurationModel); - model.AvailableGoogleCategories = _googleFeedService.GetTaxonomyList(); + model.LanguageSeoCode = _services.WorkContext.WorkingLanguage.UniqueSeoCode.EmptyNull().ToLower(); + + model.AvailableCategories = model.DefaultGoogleCategory.HasValue() + ? new List { new SelectListItem { Text = model.DefaultGoogleCategory, Value = model.DefaultGoogleCategory, Selected = true } } + : new List(); } }; } } - public override string FileExtension - { - get { return "XML"; } - } + public override string FileExtension => "XML"; protected override void Export(ExportExecuteContext context) { - dynamic currency = context.Currency; - var languageId = (int)context.Language.Id; - var measureWeightSystemKey = ""; + Currency currency = context.Currency.Entity; + var languageId = context.Projection.LanguageId ?? 0; var dateFormat = "yyyy-MM-ddTHH:mmZ"; + var defaultCondition = "new"; + var defaultAvailability = "in stock"; + var measureWeight = GetBaseMeasureWeight(); - var measureWeight = _measureService.GetMeasureWeightById(_measureSettings.BaseWeightId); + var config = (context.ConfigurationData as ProfileConfigurationModel) ?? new ProfileConfigurationModel(); + + if (config.Condition.IsCaseInsensitiveEqual(Unspecified)) + { + defaultCondition = string.Empty; + } + else if (config.Condition.HasValue()) + { + defaultCondition = config.Condition; + } - if (measureWeight != null) - measureWeightSystemKey = measureWeight.SystemKeyword; + if (config.Availability.IsCaseInsensitiveEqual(Unspecified)) + { + defaultAvailability = string.Empty; + } + else if (config.Availability.HasValue()) + { + defaultAvailability = config.Availability; + } - var config = (context.ConfigurationData as ProfileConfigurationModel) ?? new ProfileConfigurationModel(); using (var writer = XmlWriter.Create(context.DataStream, ExportXmlHelper.DefaultSettings)) { @@ -242,14 +315,15 @@ protected override void Export(ExportExecuteContext context) string mainImageUrl = product._MainPictureUrl; var price = (decimal)product.Price; var uniqueId = (string)product._UniqueId; + var isParent = (bool)product._IsParent; string brand = product._Brand; string gtin = product.Gtin; string mpn = product.ManufacturerPartNumber; - string condition = "new"; - string availability = "in stock"; + var availability = defaultAvailability; - var combinationValues = product._AttributeCombinationValues as IList; - var mappedValues = (combinationValues != null ? combinationValues.GetMappedValuesFromAlias("gmc", languageId) : null); + var attributeValues = !isParent && product._AttributeCombinationValues != null + ? ((ICollection)product._AttributeCombinationValues).ToMultimap(x => x.ProductVariantAttribute.ProductAttributeId, x => x) + : new Multimap(); var specialPrice = product._FutureSpecialPrice as decimal?; if (!specialPrice.HasValue) @@ -261,32 +335,12 @@ protected override void Export(ExportExecuteContext context) if (category.IsEmpty()) context.Log.Error(T("Plugins.Feed.Froogle.MissingDefaultCategory")); - if (config.Condition.IsCaseInsensitiveEqual(Unspecified)) - { - condition = ""; - } - else if (config.Condition.HasValue()) + if (entity.ManageInventoryMethod == ManageInventoryMethod.ManageStock && entity.StockQuantity <= 0) { - condition = config.Condition; - } - - if (config.Availability.IsCaseInsensitiveEqual(Unspecified)) - { - availability = ""; - } - else if (config.Availability.HasValue()) - { - availability = config.Availability; - } - else - { - if (entity.ManageInventoryMethod == ManageInventoryMethod.ManageStock && entity.StockQuantity <= 0) - { - if (entity.BackorderMode == BackorderMode.NoBackorders) - availability = "out of stock"; - else if (entity.BackorderMode == BackorderMode.AllowQtyBelow0 || entity.BackorderMode == BackorderMode.AllowQtyBelow0AndNotifyCustomer) - availability = (entity.AvailableForPreOrder ? "preorder" : "out of stock"); - } + if (entity.BackorderMode == BackorderMode.NoBackorders) + availability = "out of stock"; + else if (entity.BackorderMode == BackorderMode.AllowQtyBelow0 || entity.BackorderMode == BackorderMode.AllowQtyBelow0AndNotifyCustomer) + availability = entity.AvailableForPreOrder ? "preorder" : "out of stock"; } WriteString(writer, "id", uniqueId); @@ -330,7 +384,9 @@ protected override void Export(ExportExecuteContext context) } } + var condition = GetAttributeValue(attributeValues, "condition", languageId, null, defaultCondition); WriteString(writer, "condition", condition); + WriteString(writer, "availability", availability); if (availability == "preorder" && entity.AvailableStartDateTimeUtc.HasValue && entity.AvailableStartDateTimeUtc.Value > DateTime.UtcNow) @@ -342,7 +398,7 @@ protected override void Export(ExportExecuteContext context) if (config.SpecialPrice && specialPrice.HasValue) { - WriteString(writer, "sale_price", specialPrice.Value.FormatInvariant() + " " + (string)currency.CurrencyCode); + WriteString(writer, "sale_price", string.Concat(specialPrice.Value.FormatInvariant(), " ", currency.CurrencyCode)); if (entity.SpecialPriceStartDateTimeUtc.HasValue && entity.SpecialPriceEndDateTimeUtc.HasValue) { @@ -355,7 +411,7 @@ protected override void Export(ExportExecuteContext context) price = (product._RegularPrice as decimal?) ?? price; } - WriteString(writer, "price", price.FormatInvariant() + " " + (string)currency.CurrencyCode); + WriteString(writer, "price", string.Concat(price.FormatInvariant(), " ", currency.CurrencyCode)); WriteString(writer, "gtin", gtin); WriteString(writer, "brand", brand); @@ -364,21 +420,29 @@ protected override void Export(ExportExecuteContext context) var identifierExists = brand.HasValue() && (gtin.HasValue() || mpn.HasValue()); WriteString(writer, "identifier_exists", identifierExists ? "yes" : "no"); - if (config.Gender.IsCaseInsensitiveEqual(Unspecified)) - WriteString(writer, "gender", ""); - else - WriteString(writer, "gender", gmc != null && gmc.Gender.HasValue() ? gmc.Gender : config.Gender); + var gender = GetAttributeValue(attributeValues, "gender", languageId, gmc?.Gender, config.Gender); + WriteString(writer, "gender", gender); - if (config.AgeGroup.IsCaseInsensitiveEqual(Unspecified)) - WriteString(writer, "age_group", ""); - else - WriteString(writer, "age_group", gmc != null && gmc.AgeGroup.HasValue() ? gmc.AgeGroup : config.AgeGroup); + var ageGroup = GetAttributeValue(attributeValues, "age_group", languageId, gmc?.AgeGroup, config.AgeGroup); + WriteString(writer, "age_group", ageGroup); - WriteString(writer, "color", gmc != null && gmc.Color.HasValue() ? gmc.Color : config.Color); - WriteString(writer, "size", gmc != null && gmc.Size.HasValue() ? gmc.Size : config.Size); - WriteString(writer, "material", gmc != null && gmc.Material.HasValue() ? gmc.Material : config.Material); - WriteString(writer, "pattern", gmc != null && gmc.Pattern.HasValue() ? gmc.Pattern : config.Pattern); - WriteString(writer, "item_group_id", gmc != null && gmc.ItemGroupId.HasValue() ? gmc.ItemGroupId : ""); + var color = GetAttributeValue(attributeValues, "color", languageId, gmc?.Color, config.Color); + WriteString(writer, "color", color); + + var size = GetAttributeValue(attributeValues, "size", languageId, gmc?.Size, config.Size); + WriteString(writer, "size", size); + + var material = GetAttributeValue(attributeValues, "material", languageId, gmc?.Material, config.Material); + WriteString(writer, "material", material); + + var pattern = GetAttributeValue(attributeValues, "pattern", languageId, gmc?.Pattern, config.Pattern); + WriteString(writer, "pattern", pattern); + + var itemGroupId = gmc != null && gmc.ItemGroupId.HasValue() ? gmc.ItemGroupId : string.Empty; + if (itemGroupId.HasValue()) + { + WriteString(writer, "item_group_id", itemGroupId); + } if (config.ExpirationDays > 0) { @@ -387,19 +451,8 @@ protected override void Export(ExportExecuteContext context) if (config.ExportShipping) { - string weightInfo; - var weight = ((decimal)product.Weight).FormatInvariant(); - - if (measureWeightSystemKey.IsCaseInsensitiveEqual("gram")) - weightInfo = weight + " g"; - else if (measureWeightSystemKey.IsCaseInsensitiveEqual("lb")) - weightInfo = weight + " lb"; - else if (measureWeightSystemKey.IsCaseInsensitiveEqual("ounce")) - weightInfo = weight + " oz"; - else - weightInfo = weight + " kg"; - - WriteString(writer, "shipping_weight", weightInfo); + var weight = string.Concat(((decimal)product.Weight).FormatInvariant(), " ", measureWeight); + WriteString(writer, "shipping_weight", weight); } if (config.ExportBasePrice && entity.BasePriceHasValue) @@ -422,14 +475,14 @@ protected override void Export(ExportExecuteContext context) WriteString(writer, "is_bundle", gmc.IsBundle.HasValue ? (gmc.IsBundle.Value ? "yes" : "no") : null); WriteString(writer, "adult", gmc.IsAdult.HasValue ? (gmc.IsAdult.Value ? "yes" : "no") : null); WriteString(writer, "energy_efficiency_class", gmc.EnergyEfficiencyClass.HasValue() ? gmc.EnergyEfficiencyClass : null); - - WriteString(writer, "custom_label_0", gmc.CustomLabel0.HasValue() ? gmc.CustomLabel0 : null); - WriteString(writer, "custom_label_1", gmc.CustomLabel1.HasValue() ? gmc.CustomLabel1 : null); - WriteString(writer, "custom_label_2", gmc.CustomLabel2.HasValue() ? gmc.CustomLabel2 : null); - WriteString(writer, "custom_label_3", gmc.CustomLabel3.HasValue() ? gmc.CustomLabel3 : null); - WriteString(writer, "custom_label_4", gmc.CustomLabel4.HasValue() ? gmc.CustomLabel4 : null); } + 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); + ++context.RecordsSucceeded; } catch (Exception exception) diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs index b5fab3af04..764edb7f2b 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs @@ -7,6 +7,7 @@ using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Localization; +using SmartStore.Core.Logging; using SmartStore.Core.Plugins; using SmartStore.GoogleMerchantCenter.Domain; using SmartStore.GoogleMerchantCenter.Models; @@ -15,22 +16,25 @@ namespace SmartStore.GoogleMerchantCenter.Services { - public partial class GoogleFeedService : IGoogleFeedService + public partial class GoogleFeedService : IGoogleFeedService { private const string _googleNamespace = "http://base.google.com/ns/1.0"; private readonly IRepository _gpRepository; private readonly ICommonServices _services; private readonly IPluginFinder _pluginFinder; + private readonly ILogger _logger; public GoogleFeedService( IRepository gpRepository, ICommonServices services, - IPluginFinder pluginFinder) + IPluginFinder pluginFinder, + ILogger logger) { _gpRepository = gpRepository; _services = services; _pluginFinder = pluginFinder; + _logger = logger; T = NullLocalizer.Instance; } @@ -90,7 +94,7 @@ public void Upsert(int pk, string name, string value) return; var product = GetGoogleProductRecord(pk); - bool insert = (product == null); + var insert = (product == null); var utcNow = DateTime.UtcNow; if (product == null) @@ -291,29 +295,47 @@ public GridModel GetGridModel(GridCommand command, string se return model; } - public string[] GetTaxonomyList() + public List GetTaxonomyList(string searchTerm) { + var result = new List(); + try { var descriptor = _pluginFinder.GetPluginDescriptorBySystemName(GoogleMerchantCenterFeedPlugin.SystemName); - var fileDir = Path.Combine(descriptor.OriginalAssemblyFile.Directory.FullName, "Files"); - var fileName = "taxonomy.{0}.txt".FormatWith(_services.WorkContext.WorkingLanguage.LanguageCulture ?? "de-DE"); + var fileName = "taxonomy.{0}.txt".FormatInvariant(_services.WorkContext.WorkingLanguage.LanguageCulture ?? "de-DE"); var path = Path.Combine(fileDir, fileName); + var filter = searchTerm.HasValue(); + string line; if (!File.Exists(path)) + { path = Path.Combine(fileDir, "taxonomy.en-US.txt"); + } - string[] lines = File.ReadAllLines(path, Encoding.UTF8); - - return lines; + using (var file = new StreamReader(path, Encoding.UTF8)) + { + while ((line = file.ReadLine()) != null) + { + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + if (filter && line.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) < 0) + { + continue; + } + + result.Add(line); + } + } } catch (Exception exc) { - exc.Dump(); + _logger.Error(exc); } - return new string[] { }; + return result; } } } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Services/IGoogleFeedService.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Services/IGoogleFeedService.cs index 6b89e81213..cfd2bc5f06 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Services/IGoogleFeedService.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Services/IGoogleFeedService.cs @@ -5,7 +5,7 @@ namespace SmartStore.GoogleMerchantCenter.Services { - public partial interface IGoogleFeedService + public partial interface IGoogleFeedService { GoogleProductRecord GetGoogleProductRecord(int productId); @@ -21,6 +21,6 @@ public partial interface IGoogleFeedService GridModel GetGridModel(GridCommand command, string searchProductName = null, string touched = null); - string[] GetTaxonomyList(); + List GetTaxonomyList(string searchTerm); } } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj b/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj index edd0b394d5..3fbb85e2ab 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj @@ -20,7 +20,7 @@ Properties SmartStore.GoogleMerchantCenter SmartStore.GoogleMerchantCenter - v4.5.2 + v4.6.1 512 @@ -43,6 +43,9 @@ + + + true @@ -89,12 +92,12 @@ ..\..\packages\Autofac.Mvc5.4.0.2\lib\net45\Autofac.Integration.Mvc.dll - False - ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll + ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll + False - False - ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll + ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll + False ..\..\packages\FluentValidation.6.4.1\lib\Net45\FluentValidation.dll @@ -214,9 +217,6 @@ 201403112356126_Initial.cs - - PreserveNewest - PreserveNewest diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml index e6035e93e9..c3cc5f24dd 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml @@ -1,27 +1,37 @@ -@model FeedGoogleMerchantCenterModel -@using SmartStore.GoogleMerchantCenter; +@using SmartStore.GoogleMerchantCenter; @using SmartStore.GoogleMerchantCenter.Models; @using SmartStore.GoogleMerchantCenter.Providers; @using SmartStore.Web.Framework; @using Telerik.Web.Mvc.UI; @using SmartStore.Web.Framework.UI; - +@model FeedGoogleMerchantCenterModel @{ - Layout = ""; + Layout = ""; - Html.AddCssFileParts(true, "~/Content/x-editable/bootstrap-editable.css"); - Html.AddCssFileParts(true, "~/Plugins/SmartStore.GoogleMerchantCenter/Content/smartstore.gmc.css"); - Html.AppendScriptParts(true, "~/Content/x-editable/bootstrap-editable.js"); + Html.AddCssFileParts(true, "~/Content/vendors/x-editable/bootstrap-editable.css"); + Html.AppendScriptParts(true, "~/Content/vendors/x-editable/bootstrap-editable.js"); } + +
                  -
                  - +
                  @Html.Raw(@T("Plugins.Feed.Froogle.AdminInstruction")) -
                  -
                  - @{ Html.RenderAction("InfoProfile", "Export", new { systemName = GmcXmlExportProvider.SystemName, returnUrl = Request.RawUrl, area = "admin" }); } +
                  @@ -31,133 +41,263 @@
                  -

                   

                  - -
                  - - - - - - - -
                  - - - - - - - - - - - - - - - - -
                  - @(Html.Telerik().Grid() - .Name("gmc-products-grid") - .DataKeys(keys => - { - keys.Add(x => x.ProductId).RouteKey("ProductId"); - }) - .Columns(c => - { - c.Bound(x => x.ProductId) - .ReadOnly() - .Visible(false); - c.Bound(x => x.Name) - .ReadOnly().Visible(true) - .Template(x => @Html.LabeledProductName(x.ProductId, x.Name, x.ProductTypeName, x.ProductTypeLabelHint)) - .ClientTemplate(@Html.LabeledProductName("ProductId", "Name")); - c.Bound(x => x.SKU) - .ReadOnly() - .Visible(true); - c.Bound(x => x.Export2) - .ClientTemplate(Html.XEditableLink("Export2", "select2")); - c.Bound(x => x.Taxonomy) - .ClientTemplate(Html.XEditableLink("Taxonomy", "typeahead")); - c.Bound(x => x.Gender) - .ClientTemplate(Html.XEditableLink("Gender", "select2")); - c.Bound(x => x.AgeGroup) - .ClientTemplate(Html.XEditableLink("AgeGroup", "select2")); - c.Bound(x => x.IsAdult) - .ClientTemplate(Html.XEditableLink("IsAdult", "select2")); - c.Bound(x => x.Color) - .ClientTemplate(Html.XEditableLink("Color", "text")); - c.Bound(x => x.Size) - .ClientTemplate(Html.XEditableLink("Size", "text")); - c.Bound(x => x.Material) - .ClientTemplate(Html.XEditableLink("Material", "text")); - c.Bound(x => x.Pattern) - .ClientTemplate(Html.XEditableLink("Pattern", "text")); - c.Bound(x => x.Multipack2) - .ClientTemplate(Html.XEditableLink("Multipack2", "text")); - c.Bound(x => x.IsBundle) - .ClientTemplate(Html.XEditableLink("IsBundle", "select2")); - c.Bound(x => x.EnergyEfficiencyClass) - .ClientTemplate(Html.XEditableLink("EnergyEfficiencyClass", "select2")); - c.Bound(x => x.CustomLabel0) - .ClientTemplate(Html.XEditableLink("CustomLabel0", "text")); - c.Bound(x => x.CustomLabel1) - .ClientTemplate(Html.XEditableLink("CustomLabel1", "text")); - c.Bound(x => x.CustomLabel2) - .ClientTemplate(Html.XEditableLink("CustomLabel2", "text")); - c.Bound(x => x.CustomLabel3) - .ClientTemplate(Html.XEditableLink("CustomLabel3", "text")); - c.Bound(x => x.CustomLabel4) - .ClientTemplate(Html.XEditableLink("CustomLabel4", "text")); - }) - .ClientEvents(e => - { - e.OnDataBound("OnGridDataBound"); - e.OnDataBinding("OnGridDataBinding"); - e.OnError("OnGridError"); - }) - .DataBinding(dataBinding => - { - dataBinding.Ajax().Select("GoogleProductList", "FeedGoogleMerchantCenter"); - }) - .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) - .PreserveGridState() - .EnableCustomBinding(true) - ) -
                  -
                  - \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProductEditTab.cshtml b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProductEditTab.cshtml index c20ef9acd7..af40550606 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProductEditTab.cshtml +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProductEditTab.cshtml @@ -1,38 +1,11 @@ -@model SmartStore.GoogleMerchantCenter.Models.GoogleProductModel - +@using SmartStore.GoogleMerchantCenter; +@model SmartStore.GoogleMerchantCenter.Models.GoogleProductModel @{ Layout = ""; } - - @* VERY IMPORTANT for proper model binding *@ @Html.Hidden("__Type__", Model.GetType().AssemblyQualifiedName) - @Html.HiddenFor(m => m.ProductId) @@ -49,8 +22,8 @@ - @@ -189,4 +162,44 @@ @Html.ValidationMessageFor(m => m.CustomLabel4) -
                  @Html.SmartLabelFor(m => m.Taxonomy) - @Html.TextBoxFor(m => m.Taxonomy, new { data_provide = "typeahead", placeholder = ViewBag.DefaultCategory, style = "width: 97%", autocomplete = "off" }) + + @Html.DropDownListFor(model => model.Taxonomy, (IEnumerable)ViewBag.AvailableCategories, new { autocomplete = "off" }) @Html.ValidationMessageFor(m => m.Taxonomy)
                  \ No newline at end of file + + + diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProfileConfiguration.cshtml b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProfileConfiguration.cshtml index 110c3b670d..d3352b5bc6 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProfileConfiguration.cshtml +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProfileConfiguration.cshtml @@ -1,4 +1,5 @@ -@using SmartStore.GoogleMerchantCenter.Providers +@using SmartStore.GoogleMerchantCenter; +@using SmartStore.GoogleMerchantCenter.Providers @using SmartStore.GoogleMerchantCenter.Models; @model ProfileConfigurationModel @{ @@ -53,9 +54,9 @@
                  @T("Plugins.Feed.Froogle.DefaultValueSettings")
                  -
                  - +
                  @T("Plugins.Feed.Froogle.DefaultValueSettingsNote") +
                  @@ -64,9 +65,8 @@ @Html.SmartLabelFor(m => m.DefaultGoogleCategory) - - @Html.TextBoxFor(m => m.DefaultGoogleCategory, new { data_provide = "typeahead", placeholder = Model.DefaultGoogleCategory, style = "width: 98%", autocomplete = "off", - data_items = 18, data_source = Model.AvailableGoogleCategoriesAsJson }) + + @Html.DropDownListFor(model => model.DefaultGoogleCategory, Model.AvailableCategories, new { autocomplete = "off" }) @Html.ValidationMessageFor(m => m.DefaultGoogleCategory) @@ -75,7 +75,7 @@ @Html.SmartLabelFor(m => m.ExpirationDays) - @Html.EditorFor(m => m.ExpirationDays) + @Html.EditorFor(m => m.ExpirationDays, new { postfix = T("Time.Days").Text }) @Html.ValidationMessageFor(m => m.ExpirationDays) @@ -178,4 +178,44 @@ @Html.ValidationMessageFor(m => m.Pattern) - \ No newline at end of file + + + diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/packages.config b/src/Plugins/SmartStore.GoogleMerchantCenter/packages.config index f3c0ad1910..3c918560a7 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/packages.config +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/packages.config @@ -2,7 +2,7 @@ - + diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/web.config b/src/Plugins/SmartStore.GoogleMerchantCenter/web.config index bf9944de8a..bb33bef252 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/web.config +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/web.config @@ -5,8 +5,16 @@ + - + @@ -120,13 +128,10 @@ + + + + - - - - - - - - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/Controllers/OfflinePaymentController.cs b/src/Plugins/SmartStore.OfflinePayment/Controllers/OfflinePaymentController.cs index 490f3aba34..7c6eaa38f5 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Controllers/OfflinePaymentController.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Controllers/OfflinePaymentController.cs @@ -14,6 +14,8 @@ using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.Settings; +using SmartStore.Services.Media; +using SmartStore.Web.Framework.Theming; namespace SmartStore.OfflinePayment.Controllers { @@ -22,14 +24,17 @@ public class OfflinePaymentController : PaymentControllerBase { private readonly IComponentContext _ctx; private readonly HttpContextBase _httpContext; + private readonly IPictureService _pictureService; public OfflinePaymentController( HttpContextBase httpContext, - IComponentContext ctx) + IComponentContext ctx, + IPictureService pictureService) { _httpContext = httpContext; _ctx = ctx; - } + _pictureService = pictureService; + } #region Global @@ -52,10 +57,15 @@ private TModel ConfigureGet(Action fn = null { var model = new TModel(); - int storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); var settings = Services.Settings.LoadSetting(storeScope); + var store = storeScope == 0 + ? Services.StoreContext.CurrentStore + : Services.StoreService.GetStoreById(storeScope); + model.PrimaryStoreCurrencyCode = store.PrimaryStoreCurrency.CurrencyCode; model.DescriptionText = settings.DescriptionText; + model.PaymentMethodLogo = settings.ThumbnailPictureId; model.AdditionalFee = settings.AdditionalFee; model.AdditionalFeePercentage = settings.AdditionalFeePercentage; @@ -82,6 +92,7 @@ private void ConfigurePost(TModel model, FormCollection form, var settings = Services.Settings.LoadSetting(storeScope); settings.DescriptionText = model.DescriptionText; + settings.ThumbnailPictureId = model.PaymentMethodLogo; settings.AdditionalFee = model.AdditionalFee; settings.AdditionalFeePercentage = model.AdditionalFeePercentage; @@ -90,9 +101,12 @@ private void ConfigurePost(TModel model, FormCollection form, fn(settings); } - storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, Services.Settings); + using (Services.Settings.BeginScope()) + { + storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, Services.Settings); + } - NotifySuccess(Services.Localization.GetResource("Admin.Common.DataSuccessfullySaved")); + NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); } [NonAction] @@ -103,6 +117,7 @@ private TModel PaymentInfoGet(Action fn = nu var settings = _ctx.Resolve(); var model = new TModel(); model.DescriptionText = GetLocalizedText(settings.DescriptionText); + model.ThumbnailUrl = _pictureService.GetUrl(settings.ThumbnailPictureId, 120, false); if (fn != null) { @@ -184,8 +199,8 @@ public override ProcessPaymentRequest GetPaymentInfo(FormCollection form) paymentInfo.CreditCardType = form["CreditCardType"]; paymentInfo.CreditCardName = form["CardholderName"]; paymentInfo.CreditCardNumber = form["CardNumber"]; - paymentInfo.CreditCardExpireMonth = int.Parse(form["ExpireMonth"]); - paymentInfo.CreditCardExpireYear = int.Parse(form["ExpireYear"]); + paymentInfo.CreditCardExpireMonth = int.Parse(form["ExpireMonth"].SplitSafe(",")[0]); + paymentInfo.CreditCardExpireYear = int.Parse(form["ExpireYear"].SplitSafe(",")[0]); paymentInfo.CreditCardCvv2 = form["CardCode"]; } else if (type == "DirectDebit") @@ -250,11 +265,10 @@ public override string GetPaymentSummary(FormCollection form) } #endregion - - + #region CashOnDelivery - [AdminAuthorize, ChildActionOnly] + [AdminAuthorize, AdminThemed, ChildActionOnly] public ActionResult CashOnDeliveryConfigure() { var model = ConfigureGet(); @@ -262,7 +276,7 @@ public ActionResult CashOnDeliveryConfigure() return View("GenericConfigure", model); } - [HttpPost, AdminAuthorize, ChildActionOnly, ValidateInput(false)] + [HttpPost, AdminAuthorize, AdminThemed, ChildActionOnly, ValidateInput(false)] public ActionResult CashOnDeliveryConfigure(CashOnDeliveryConfigurationModel model, FormCollection form) { if (!ModelState.IsValid) @@ -280,11 +294,10 @@ public ActionResult CashOnDeliveryPaymentInfo() } #endregion - - + #region Invoice - [ChildActionOnly, AdminAuthorize] + [ChildActionOnly, AdminThemed, AdminAuthorize] public ActionResult InvoiceConfigure() { var model = ConfigureGet(); @@ -292,7 +305,7 @@ public ActionResult InvoiceConfigure() return View("GenericConfigure", model); } - [HttpPost, AdminAuthorize, ChildActionOnly, ValidateInput(false)] + [HttpPost, AdminAuthorize, AdminThemed, ChildActionOnly, ValidateInput(false)] public ActionResult InvoiceConfigure(InvoiceConfigurationModel model, FormCollection form) { if (!ModelState.IsValid) @@ -310,11 +323,10 @@ public ActionResult InvoicePaymentInfo() } #endregion - - + #region PayInStore - [ChildActionOnly, AdminAuthorize] + [ChildActionOnly, AdminThemed, AdminAuthorize] public ActionResult PayInStoreConfigure() { var model = ConfigureGet(); @@ -322,7 +334,7 @@ public ActionResult PayInStoreConfigure() return View("GenericConfigure", model); } - [HttpPost, AdminAuthorize, ChildActionOnly, ValidateInput(false)] + [HttpPost, AdminAuthorize, AdminThemed, ChildActionOnly, ValidateInput(false)] public ActionResult PayInStoreConfigure(PayInStoreConfigurationModel model, FormCollection form) { if (!ModelState.IsValid) @@ -340,11 +352,10 @@ public ActionResult PayInStorePaymentInfo() } #endregion - - + #region Prepayment - [AdminAuthorize, ChildActionOnly] + [AdminAuthorize, AdminThemed, ChildActionOnly] public ActionResult PrepaymentConfigure() { var model = ConfigureGet(); @@ -352,7 +363,7 @@ public ActionResult PrepaymentConfigure() return View("GenericConfigure", model); } - [HttpPost, AdminAuthorize, ChildActionOnly, ValidateInput(false)] + [HttpPost, AdminAuthorize, AdminThemed, ChildActionOnly, ValidateInput(false)] public ActionResult PrepaymentConfigure(PrepaymentConfigurationModel model, FormCollection form) { if (!ModelState.IsValid) @@ -370,11 +381,10 @@ public ActionResult PrepaymentPaymentInfo() } #endregion - - + #region DirectDebit - [AdminAuthorize, ChildActionOnly] + [AdminAuthorize, AdminThemed, ChildActionOnly] public ActionResult DirectDebitConfigure() { var model = ConfigureGet(); @@ -382,7 +392,7 @@ public ActionResult DirectDebitConfigure() return View("GenericConfigure", model); } - [HttpPost, AdminAuthorize, ChildActionOnly, ValidateInput(false)] + [HttpPost, AdminAuthorize, AdminThemed, ChildActionOnly, ValidateInput(false)] public ActionResult DirectDebitConfigure(DirectDebitConfigurationModel model, FormCollection form) { if (!ModelState.IsValid) @@ -410,11 +420,10 @@ public ActionResult DirectDebitPaymentInfo() } #endregion - - + #region Manual - [AdminAuthorize, ChildActionOnly] + [AdminAuthorize, AdminThemed, ChildActionOnly] public ActionResult ManualConfigure() { var model = ConfigureGet((m, s) => @@ -436,7 +445,7 @@ public ActionResult ManualConfigure() return View(model); } - [HttpPost, AdminAuthorize, ChildActionOnly, ValidateInput(false)] + [HttpPost, AdminAuthorize, AdminThemed, ChildActionOnly, ValidateInput(false)] public ActionResult ManualConfigure(ManualConfigurationModel model, FormCollection form) { if (!ModelState.IsValid) @@ -512,8 +521,7 @@ public ActionResult ManualPaymentInfo() #region PurchaseOrderNumber - [AdminAuthorize] - [ChildActionOnly] + [AdminAuthorize, AdminThemed, ChildActionOnly] public ActionResult PurchaseOrderNumberConfigure() { var model = ConfigureGet(); @@ -521,13 +529,13 @@ public ActionResult PurchaseOrderNumberConfigure() return View("GenericConfigure", model); } - [HttpPost, AdminAuthorize, ChildActionOnly, ValidateInput(false)] + [HttpPost, AdminAuthorize, AdminThemed, ChildActionOnly, ValidateInput(false)] public ActionResult PurchaseOrderNumberConfigure(PurchaseOrderNumberConfigurationModel model, FormCollection form) { if (!ModelState.IsValid) - return InvoiceConfigure(); + return PurchaseOrderNumberConfigure(); - ConfigurePost(model, form); + ConfigurePost(model, form); return PurchaseOrderNumberConfigure(); } diff --git a/src/Plugins/SmartStore.OfflinePayment/Localization/resources.de-de.xml b/src/Plugins/SmartStore.OfflinePayment/Localization/resources.de-de.xml index a9f2235ba1..f954ac09f1 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.OfflinePayment/Localization/resources.de-de.xml @@ -13,17 +13,11 @@ Geben Sie hier die Beschreibung an, die dem Kunden im Bestellprozess angezeigt wird. - - Zusätzliche Gebühr + + Logo - - Bestimmt die Gebühr, die dem Kunden für die Nutzung dieser Zahlart berechnet wird. - - - Zusätzliche Gebühren (prozentual) - - - Zusätzliche prozentuale Gebühr zum Gesamtbetrag. Ein fester Wert wird verwendet, falls diese Option nicht aktiviert ist. + + Laden Sie hier eine Grafik hoch, welche auf der Zahlartauswahlseite als Thumbnail der Zahlart angezeigt wird. @@ -121,6 +115,13 @@ + + + + + Zahlungsstatus nach Bestellabschluss diff --git a/src/Plugins/SmartStore.OfflinePayment/Localization/resources.en-us.xml b/src/Plugins/SmartStore.OfflinePayment/Localization/resources.en-us.xml index af3d0cc477..e9376b5596 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.OfflinePayment/Localization/resources.en-us.xml @@ -1,22 +1,23 @@  + + Offline payment methods + + + Provides offline payment methods, e.g. direct debit, credit card, invoice, prepayment, cash on delivery etc. + + Description Enter info that will be shown to customers during checkout. - - Additional fee - - - Determines the additional fee. + + Logo - - Additional fee. Use percentage - - - Determines whether to apply a percentage additional fee to the order total. If not enabled, a fixed value is used. + + Upload an image which will be displayed as thumbnail of the payment method on the payment selection page. @@ -114,6 +115,13 @@ + + + + + Payment status after order completion diff --git a/src/Plugins/SmartStore.OfflinePayment/Models/ConfigurationModel.cs b/src/Plugins/SmartStore.OfflinePayment/Models/ConfigurationModel.cs index ba59935fdb..f69436a624 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Models/ConfigurationModel.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Models/ConfigurationModel.cs @@ -3,21 +3,28 @@ using SmartStore.OfflinePayment.Settings; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Modelling; +using System.ComponentModel.DataAnnotations; namespace SmartStore.OfflinePayment.Models { public abstract class ConfigurationModelBase : ModelBase { + public string PrimaryStoreCurrencyCode { get; set; } + [AllowHtml] [SmartResourceDisplayName("Plugins.SmartStore.OfflinePayment.DescriptionText")] public string DescriptionText { get; set; } - [SmartResourceDisplayName("Plugins.SmartStore.OfflinePayment.AdditionalFee")] + [SmartResourceDisplayName("Admin.Configuration.Payment.Methods.AdditionalFee")] public decimal AdditionalFee { get; set; } - [SmartResourceDisplayName("Plugins.SmartStore.OfflinePayment.AdditionalFeePercentage")] + [SmartResourceDisplayName("Admin.Configuration.Payment.Methods.AdditionalFeePercentage")] public bool AdditionalFeePercentage { get; set; } - } + + [SmartResourceDisplayName("Plugins.SmartStore.OfflinePayment.PaymentMethodLogo")] + [UIHint("Picture")] + public int PaymentMethodLogo { get; set; } + } public class CashOnDeliveryConfigurationModel : ConfigurationModelBase { diff --git a/src/Plugins/SmartStore.OfflinePayment/Models/PaymentInfoModel.cs b/src/Plugins/SmartStore.OfflinePayment/Models/PaymentInfoModel.cs index b13dd98de0..6909d4cdd7 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Models/PaymentInfoModel.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Models/PaymentInfoModel.cs @@ -8,7 +8,8 @@ namespace SmartStore.OfflinePayment.Models public abstract class PaymentInfoModelBase : ModelBase { public string DescriptionText { get; set; } - } + public string ThumbnailUrl { get; set; } + } public class CashOnDeliveryPaymentInfoModel : PaymentInfoModelBase { diff --git a/src/Plugins/SmartStore.OfflinePayment/Settings/OfflinePaymentSettings.cs b/src/Plugins/SmartStore.OfflinePayment/Settings/OfflinePaymentSettings.cs index 52fbe9bc18..89c409a2b8 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Settings/OfflinePaymentSettings.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Settings/OfflinePaymentSettings.cs @@ -6,6 +6,7 @@ namespace SmartStore.OfflinePayment.Settings public abstract class PaymentSettingsBase : ISettings { public string DescriptionText { get; set; } + public int ThumbnailPictureId { get; set; } public decimal AdditionalFee { get; set; } public bool AdditionalFeePercentage { get; set; } } diff --git a/src/Plugins/SmartStore.OfflinePayment/SmartStore.OfflinePayment.csproj b/src/Plugins/SmartStore.OfflinePayment/SmartStore.OfflinePayment.csproj index ee2626c779..317ea9ef7d 100644 --- a/src/Plugins/SmartStore.OfflinePayment/SmartStore.OfflinePayment.csproj +++ b/src/Plugins/SmartStore.OfflinePayment/SmartStore.OfflinePayment.csproj @@ -20,7 +20,7 @@ Properties SmartStore.OfflinePayment SmartStore.OfflinePayment - v4.5.2 + v4.6.1 512 @@ -43,6 +43,7 @@ + true @@ -95,6 +96,7 @@ ..\..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll + diff --git a/src/Plugins/SmartStore.OfflinePayment/Validators/ManualPaymentInfoValidator.cs b/src/Plugins/SmartStore.OfflinePayment/Validators/ManualPaymentInfoValidator.cs index 5fa9e03a2f..ef4d0e125b 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Validators/ManualPaymentInfoValidator.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Validators/ManualPaymentInfoValidator.cs @@ -16,5 +16,6 @@ public ManualPaymentInfoValidator(ILocalizationService localizationService) RuleFor(x => x.CardholderName).NotEmpty().WithMessage(localizationService.GetResource("Payment.CardholderName.Required")); RuleFor(x => x.CardNumber).IsCreditCard().WithMessage(localizationService.GetResource("Payment.CardNumber.Wrong")); RuleFor(x => x.CardCode).Matches(@"^[0-9]{3,4}$").WithMessage(localizationService.GetResource("Payment.CardCode.Wrong")); - }} + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/DirectDebitPaymentInfo.cshtml b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/DirectDebitPaymentInfo.cshtml index 695f25cf62..6893c04136 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/DirectDebitPaymentInfo.cshtml +++ b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/DirectDebitPaymentInfo.cshtml @@ -4,28 +4,38 @@ Layout = ""; } -

                  - @Html.Raw(Model.DescriptionText) -

                  - @Html.Hidden("OfflinePaymentMethodType", "DirectDebit") +@if (Model.ThumbnailUrl.HasValue()) +{ +
                  + +
                  + @Html.Raw(Model.DescriptionText) +
                  +
                  +} +else +{ +
                  + @Html.Raw(Model.DescriptionText) +
                  +} +
                  @Html.LabelFor(model => model.EnterIBAN, new { @class = "col-md-3 col-form-label" })
                  -
                  - -
                  -
                  - -
                  +
                  +
                  + + +
                  +
                  + + +
                  +
                  diff --git a/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/GenericConfigure.cshtml b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/GenericConfigure.cshtml index 798df77929..742bd29fb7 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/GenericConfigure.cshtml +++ b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/GenericConfigure.cshtml @@ -1,6 +1,6 @@ @model SmartStore.OfflinePayment.Models.ConfigurationModelBase @using SmartStore.Web.Framework; - +@using SmartStore.Web.Framework.UI; @{ Layout = ""; } @@ -9,23 +9,38 @@ @using (Html.BeginForm()) { +
                  + +
                  + + + + + @@ -38,14 +53,5 @@ @Html.ValidationMessageFor(model => model.AdditionalFeePercentage) - - - -
                  @Html.SmartLabelFor(model => model.DescriptionText) - @Html.SettingOverrideCheckbox(model => model.DescriptionText) - @Html.TextAreaFor(model => model.DescriptionText, new { style = "width: 550px; height: 100px;" }) + @Html.SettingEditorFor(model => model.DescriptionText, Html.TextAreaFor(model => model.DescriptionText, new { style = "height: 100px;" })) @Html.ValidationMessageFor(model => model.DescriptionText)
                  + @Html.SmartLabelFor(model => model.PaymentMethodLogo) + + @Html.SettingEditorFor(model => model.PaymentMethodLogo, Html.EditorFor(m => m.PaymentMethodLogo, "Picture", new { transientUpload = true, validate = true })) + @Html.ValidationMessageFor(model => model.PaymentMethodLogo) +
                  @Html.SmartLabelFor(model => model.AdditionalFee) - @Html.SettingEditorFor(model => model.AdditionalFee) + @Html.SettingEditorFor(model => model.AdditionalFee, null, new { postfix = Model.PrimaryStoreCurrencyCode }) @Html.ValidationMessageFor(model => model.AdditionalFee)
                    - -
                  } \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/GenericPaymentInfo.cshtml b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/GenericPaymentInfo.cshtml index ceb5ed67e3..427c71560c 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/GenericPaymentInfo.cshtml +++ b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/GenericPaymentInfo.cshtml @@ -4,6 +4,18 @@ Layout = ""; } -
                  - @Html.Raw(Model.DescriptionText) -
                  \ No newline at end of file +@if (Model.ThumbnailUrl.HasValue()) +{ +
                  + +
                  + @Html.Raw(Model.DescriptionText) +
                  +
                  +} +else +{ +
                  + @Html.Raw(Model.DescriptionText) +
                  +} diff --git a/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/ManualConfigure.cshtml b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/ManualConfigure.cshtml index ff31e3b5e0..d30cf66665 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/ManualConfigure.cshtml +++ b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/ManualConfigure.cshtml @@ -8,14 +8,38 @@ @using (Html.BeginForm()) { +
                  + +
                  + + + + + + + + + @@ -23,8 +47,8 @@ @Html.SmartLabelFor(model => model.ExcludedCreditCards) @@ -33,7 +57,7 @@ @Html.SmartLabelFor(model => model.AdditionalFee) @@ -46,13 +70,5 @@ @Html.ValidationMessageFor(model => model.AdditionalFeePercentage) - - - -
                  + @Html.SmartLabelFor(model => model.DescriptionText) + + @Html.SettingEditorFor(model => model.DescriptionText, Html.TextAreaFor(model => model.DescriptionText, new { style = "height: 100px;" })) + @Html.ValidationMessageFor(model => model.DescriptionText) +
                  + @Html.SmartLabelFor(model => model.PaymentMethodLogo) + + @Html.SettingEditorFor(model => model.PaymentMethodLogo, Html.EditorFor(m => m.PaymentMethodLogo, "Picture", new { transientUpload = true, validate = true })) + @Html.ValidationMessageFor(model => model.PaymentMethodLogo) +
                  @Html.SmartLabelFor(model => model.TransactMode) - @Html.SettingOverrideCheckbox(model => model.TransactMode) - @Html.DropDownListFor(model => model.TransactMode, Model.TransactModeValues, new { @class = "form-control noskin" }) + @Html.SettingEditorFor(model => model.TransactMode, Html.DropDownListFor(model => model.TransactMode, Model.TransactModeValues))
                  - @Html.SettingOverrideCheckbox(model => model.ExcludedCreditCards) - @Html.ListBoxFor(x => x.ExcludedCreditCards, new MultiSelectList(Model.AvailableCreditCards, "Value", "Text"), new { multiple = "multiple" }) + @Html.SettingEditorFor(model => model.ExcludedCreditCards, + Html.ListBoxFor(model => model.ExcludedCreditCards, Model.AvailableCreditCards, new { multiple = "multiple", data_tags = "true" })) @Html.ValidationMessageFor(model => model.ExcludedCreditCards)
                  - @Html.SettingEditorFor(model => model.AdditionalFee) + @Html.SettingEditorFor(model => model.AdditionalFee, null, new { postfix = Model.PrimaryStoreCurrencyCode }) @Html.ValidationMessageFor(model => model.AdditionalFee)
                    - -
                  } \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/ManualPaymentInfo.cshtml b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/ManualPaymentInfo.cshtml index df6ae6e681..0a4fab6d28 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/ManualPaymentInfo.cshtml +++ b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/ManualPaymentInfo.cshtml @@ -6,6 +6,23 @@ @Html.Hidden("OfflinePaymentMethodType", "Manual") +@if (Model.ThumbnailUrl.HasValue()) +{ +
                  + +
                  + @Html.Raw(Model.DescriptionText) +
                  +
                  +} +else +{ +
                  + @Html.Raw(Model.DescriptionText) +
                  +} + +
                  @Html.LabelFor(model => model.CreditCardTypes, new { @class = "col-md-3 col-form-label required" }) diff --git a/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/PurchaseOrderNumberPaymentInfo.cshtml b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/PurchaseOrderNumberPaymentInfo.cshtml index 07345c2ae0..44a83f82e6 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/PurchaseOrderNumberPaymentInfo.cshtml +++ b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/PurchaseOrderNumberPaymentInfo.cshtml @@ -7,6 +7,22 @@ @Html.Hidden("OfflinePaymentMethodType", "PurchaseOrderNumber") +@if (Model.ThumbnailUrl.HasValue()) +{ +
                  + +
                  + @Html.Raw(Model.DescriptionText) +
                  +
                  +} +else +{ +
                  + @Html.Raw(Model.DescriptionText) +
                  +} +
                  @Html.ControlGroupFor(model => model.PurchaseOrderNumber, required: true, breakpoint: "md")
                  \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/web.config b/src/Plugins/SmartStore.OfflinePayment/web.config index bf9944de8a..bb33bef252 100644 --- a/src/Plugins/SmartStore.OfflinePayment/web.config +++ b/src/Plugins/SmartStore.OfflinePayment/web.config @@ -5,8 +5,16 @@ + - + @@ -120,13 +128,10 @@ + + + + - - - - - - - - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Content/branding.png b/src/Plugins/SmartStore.PayPal/Content/branding.png index bcd99dc6b2..9fba00608a 100644 Binary files a/src/Plugins/SmartStore.PayPal/Content/branding.png and b/src/Plugins/SmartStore.PayPal/Content/branding.png differ diff --git a/src/Plugins/SmartStore.PayPal/Content/icon.png b/src/Plugins/SmartStore.PayPal/Content/icon.png index 1a2423cef2..04beef7c77 100644 Binary files a/src/Plugins/SmartStore.PayPal/Content/icon.png and b/src/Plugins/SmartStore.PayPal/Content/icon.png differ diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalControllerBase.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalControllerBase.cs index 8db1218d7d..2fa248cf27 100644 --- a/src/Plugins/SmartStore.PayPal/Controllers/PayPalControllerBase.cs +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalControllerBase.cs @@ -10,6 +10,7 @@ using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; using SmartStore.Core.Logging; +using SmartStore.PayPal.Models; using SmartStore.PayPal.Settings; using SmartStore.Services.Orders; using SmartStore.Services.Payments; @@ -17,7 +18,19 @@ namespace SmartStore.PayPal.Controllers { - public abstract class PayPalControllerBase : PaymentControllerBase where TSetting : PayPalSettingsBase, ISettings, new() + public abstract class PayPalPaymentControllerBase : PaymentControllerBase + { + protected void PrepareConfigurationModel(ApiConfigurationModel model, int storeScope) + { + var store = storeScope == 0 + ? Services.StoreContext.CurrentStore + : Services.StoreService.GetStoreById(storeScope); + + model.PrimaryStoreCurrencyCode = store.PrimaryStoreCurrency.CurrencyCode; + } + } + + public abstract class PayPalControllerBase : PayPalPaymentControllerBase where TSetting : PayPalSettingsBase, ISettings, new() { public PayPalControllerBase( string systemName, diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalDirectController.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalDirectController.cs index af74d004d8..79015a43c8 100644 --- a/src/Plugins/SmartStore.PayPal/Controllers/PayPalDirectController.cs +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalDirectController.cs @@ -5,7 +5,6 @@ using System.Web.Mvc; using SmartStore.Core.Domain.Payments; using SmartStore.PayPal.Models; -using SmartStore.PayPal.Services; using SmartStore.PayPal.Settings; using SmartStore.PayPal.Validators; using SmartStore.Services.Orders; @@ -34,33 +33,13 @@ public PayPalDirectController( _httpContext = httpContext; } - private SelectList TransactModeValues(TransactMode selected) - { - return new SelectList(new List - { - new { ID = (int)TransactMode.Authorize, Name = T("Plugins.Payments.PayPalDirect.ModeAuth") }, - new { ID = (int)TransactMode.AuthorizeAndCapture, Name = T("Plugins.Payments.PayPalDirect.ModeAuthAndCapture") } - }, - "ID", "Name", (int)selected); - } - - [AdminAuthorize, ChildActionOnly] - public ActionResult Configure() + [LoadSetting, AdminAuthorize, ChildActionOnly] + public ActionResult Configure(PayPalDirectPaymentSettings settings, int storeScope) { var model = new PayPalDirectConfigurationModel(); - int storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); - var settings = Services.Settings.LoadSetting(storeScope); - model.Copy(settings, true); - model.TransactModeValues = TransactModeValues(settings.TransactMode); - - model.AvailableSecurityProtocols = PayPalService.GetSecurityProtocols() - .Select(x => new SelectListItem { Value = ((int)x.Key).ToString(), Text = x.Value }) - .ToList(); - - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - storeDependingSettingHelper.GetOverrideKeys(settings, model, storeScope, Services.Settings); + PrepareConfigurationModel(model, storeScope); return View(model); } @@ -68,35 +47,39 @@ public ActionResult Configure() [HttpPost, AdminAuthorize, ChildActionOnly] public ActionResult Configure(PayPalDirectConfigurationModel model, FormCollection form) { - if (!ModelState.IsValid) - return Configure(); - - ModelState.Clear(); - - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - int storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); var settings = Services.Settings.LoadSetting(storeScope); - model.Copy(settings, false); + if (!ModelState.IsValid) + { + return Configure(settings, storeScope); + } + + ModelState.Clear(); + model.Copy(settings, false); using (Services.Settings.BeginScope()) { storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, Services.Settings); + } - // multistore context not possible, see IPN handling + using (Services.Settings.BeginScope()) + { + // Multistore context not possible, see IPN handling. Services.Settings.SaveSetting(settings, x => x.UseSandbox, 0, false); } - NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); + NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); - return Configure(); + return RedirectToConfiguration(PayPalDirectProvider.SystemName, false); } public ActionResult PaymentInfo() { var model = new PayPalDirectPaymentInfoModel(); - //CC types + // Credit card types. model.CreditCardTypes.Add(new SelectListItem { Text = "Visa", @@ -118,7 +101,7 @@ public ActionResult PaymentInfo() Value = "Amex", }); - //years + // Years. for (int i = 0; i < 15; i++) { string year = Convert.ToString(DateTime.Now.Year + i); @@ -129,7 +112,7 @@ public ActionResult PaymentInfo() }); } - //months + // Months. for (int i = 1; i <= 12; i++) { string text = (i < 10) ? "0" + i.ToString() : i.ToString(); @@ -140,7 +123,7 @@ public ActionResult PaymentInfo() }); } - //set postback values + // Set postback values. var paymentData = _httpContext.GetCheckoutState().PaymentData; model.CardholderName = (string)paymentData.Get("CardholderName"); model.CardNumber = (string)paymentData.Get("CardNumber"); diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalExpressController.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalExpressController.cs index 9f6b5f7ab7..ec94a5667b 100644 --- a/src/Plugins/SmartStore.PayPal/Controllers/PayPalExpressController.cs +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalExpressController.cs @@ -1,18 +1,14 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Net; using System.Text; using System.Web.Mvc; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Discounts; -using SmartStore.Core.Domain.Logging; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Logging; using SmartStore.PayPal.Models; using SmartStore.PayPal.PayPalSvc; -using SmartStore.PayPal.Services; using SmartStore.PayPal.Settings; using SmartStore.PayPal.Validators; using SmartStore.Services.Common; @@ -55,16 +51,6 @@ public PayPalExpressController( _genericAttributeService = genericAttributeService; } - private SelectList TransactModeValues(TransactMode selected) - { - return new SelectList(new List - { - new { ID = (int)TransactMode.Authorize, Name = T("Plugins.Payments.PayPalExpress.ModeAuth") }, - new { ID = (int)TransactMode.AuthorizeAndCapture, Name = T("Plugins.Payments.PayPalExpress.ModeAuthAndCapture") } - }, - "ID", "Name", (int)selected); - } - private string GetCheckoutButtonUrl(PayPalExpressPaymentSettings settings) { var expressCheckoutButton = "~/Plugins/SmartStore.PayPal/Content/checkout-button-default.png"; @@ -83,23 +69,13 @@ private string GetCheckoutButtonUrl(PayPalExpressPaymentSettings settings) } - [AdminAuthorize, ChildActionOnly] - public ActionResult Configure() + [AdminAuthorize, ChildActionOnly, LoadSetting] + public ActionResult Configure(PayPalExpressPaymentSettings settings, int storeScope) { var model = new PayPalExpressConfigurationModel(); - int storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); - var settings = Services.Settings.LoadSetting(storeScope); - model.Copy(settings, true); - model.TransactModeValues = TransactModeValues(settings.TransactMode); - - model.AvailableSecurityProtocols = PayPalService.GetSecurityProtocols() - .Select(x => new SelectListItem { Value = ((int)x.Key).ToString(), Text = x.Value }) - .ToList(); - - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - storeDependingSettingHelper.GetOverrideKeys(settings, model, storeScope, Services.Settings); + PrepareConfigurationModel(model, storeScope); return View(model); } @@ -107,28 +83,32 @@ public ActionResult Configure() [HttpPost, AdminAuthorize, ChildActionOnly] public ActionResult Configure(PayPalExpressConfigurationModel model, FormCollection form) { + var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); + if (!ModelState.IsValid) - return Configure(); + { + return Configure(settings, storeScope); + } ModelState.Clear(); - - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - int storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); - var settings = Services.Settings.LoadSetting(storeScope); - - model.Copy(settings, false); + model.Copy(settings, false); using (Services.Settings.BeginScope()) { storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, Services.Settings); + } - // multistore context not possible, see IPN handling + using (Services.Settings.BeginScope()) + { + // Multistore context not possible, see IPN handling. Services.Settings.SaveSetting(settings, x => x.UseSandbox, 0, false); } - NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); + NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); - return Configure(); + return RedirectToConfiguration(PayPalExpressProvider.SystemName, false); } public ActionResult PaymentInfo() @@ -242,6 +222,8 @@ public ActionResult SubmitButton() if (resp.Ack == AckCodeType.Success) { + // Note: If Token is null and an empty page with "No token passed" is displyed, then this is caused by a broken + // Web References/PayPalSvc/Reference.cs file. To fix it, check git history of the file and revert changes. processPaymentRequest.PaypalToken = resp.Token; processPaymentRequest.OrderGuid = new Guid(); processPaymentRequest.IsShippingMethodSet = ControllerContext.RouteData.IsRouteEqual("ShoppingCart", "Cart"); diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalPlusController.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalPlusController.cs index 4ce44e30bf..ad831ae6da 100644 --- a/src/Plugins/SmartStore.PayPal/Controllers/PayPalPlusController.cs +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalPlusController.cs @@ -153,22 +153,15 @@ public override ProcessPaymentRequest GetPaymentInfo(FormCollection form) return paymentInfo; } - [AdminAuthorize, ChildActionOnly] - public ActionResult Configure() + [LoadSetting, AdminAuthorize, ChildActionOnly] + public ActionResult Configure(PayPalPlusPaymentSettings settings, int storeScope) { - var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); - var settings = Services.Settings.LoadSetting(storeScope); - var model = new PayPalPlusConfigurationModel { ConfigGroups = T("Plugins.SmartStore.PayPal.ConfigGroups").Text.SplitSafe(";") }; - model.AvailableSecurityProtocols = PayPal.Services.PayPalService.GetSecurityProtocols() - .Select(x => new SelectListItem { Value = ((int)x.Key).ToString(), Text = x.Value }) - .ToList(); - - // it's better to also offer inactive methods here but filter them out in frontend + // It's better to also offer inactive methods here but filter them out in frontend. var methods = _paymentService.LoadAllPaymentMethods(storeScope); model.AvailableThirdPartyPaymentMethods = methods @@ -179,11 +172,8 @@ public ActionResult Configure() .Select(x => new SelectListItem { Value = x.Metadata.SystemName, Text = GetPaymentMethodName(x) }) .ToList(); - model.Copy(settings, true); - - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - storeDependingSettingHelper.GetOverrideKeys(settings, model, storeScope, Services.Settings); + PrepareConfigurationModel(model, storeScope); return View(model); } @@ -194,6 +184,7 @@ public ActionResult Configure(PayPalPlusConfigurationModel model, FormCollection var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); var settings = Services.Settings.LoadSetting(storeScope); + var oldClientId = settings.ClientId; var oldSecret = settings.Secret; var oldProfileId = settings.ExperienceProfileId; @@ -206,13 +197,14 @@ public ActionResult Configure(PayPalPlusConfigurationModel model, FormCollection validator.Validate(model, ModelState); if (!ModelState.IsValid) - return Configure(); + { + return Configure(settings, storeScope); + } ModelState.Clear(); - model.Copy(settings, false); - // credentials changed: reset profile and webhook id to avoid errors + // Credentials changed: reset profile and webhook id to avoid errors. if (!oldClientId.IsCaseInsensitiveEqual(settings.ClientId) || !oldSecret.IsCaseInsensitiveEqual(settings.Secret)) { if (oldProfileId.IsCaseInsensitiveEqual(settings.ExperienceProfileId)) @@ -224,12 +216,17 @@ public ActionResult Configure(PayPalPlusConfigurationModel model, FormCollection using (Services.Settings.BeginScope()) { storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, Services.Settings); + } + + using (Services.Settings.BeginScope()) + { + // Multistore context not possible, see IPN handling. Services.Settings.SaveSetting(settings, x => x.UseSandbox, 0, false); } NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); - return Configure(); + return RedirectToConfiguration(PayPalPlusProvider.SystemName, false); } public ActionResult PaymentInfo() diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalRestApiControllerBase.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalRestApiControllerBase.cs index d80693337b..c28bb24fcd 100644 --- a/src/Plugins/SmartStore.PayPal/Controllers/PayPalRestApiControllerBase.cs +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalRestApiControllerBase.cs @@ -11,7 +11,7 @@ namespace SmartStore.PayPal.Controllers { - public abstract class PayPalRestApiControllerBase : PaymentControllerBase where TSetting : PayPalApiSettingsBase, ISettings, new() + public abstract class PayPalRestApiControllerBase : PayPalPaymentControllerBase where TSetting : PayPalApiSettingsBase, ISettings, new() { public PayPalRestApiControllerBase( string systemName, diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalStandardController.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalStandardController.cs index ae56e6e215..0d4d6a7605 100644 --- a/src/Plugins/SmartStore.PayPal/Controllers/PayPalStandardController.cs +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalStandardController.cs @@ -3,11 +3,9 @@ using System.Globalization; using System.Linq; using System.Web.Mvc; -using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; using SmartStore.Core.Logging; using SmartStore.PayPal.Models; -using SmartStore.PayPal.Services; using SmartStore.PayPal.Settings; using SmartStore.Services.Orders; using SmartStore.Services.Payments; @@ -30,50 +28,46 @@ public PayPalStandardController( { } - [AdminAuthorize, ChildActionOnly] - public ActionResult Configure() + [AdminAuthorize, ChildActionOnly, LoadSetting] + public ActionResult Configure(PayPalStandardPaymentSettings settings, int storeScope) { var model = new PayPalStandardConfigurationModel(); - int storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); - var settings = Services.Settings.LoadSetting(storeScope); - model.Copy(settings, true); - model.AvailableSecurityProtocols = PayPalService.GetSecurityProtocols() - .Select(x => new SelectListItem { Value = ((int)x.Key).ToString(), Text = x.Value }) - .ToList(); - - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - storeDependingSettingHelper.GetOverrideKeys(settings, model, storeScope, Services.Settings); + PrepareConfigurationModel(model, storeScope); return View(model); } [HttpPost, AdminAuthorize, ChildActionOnly] - public ActionResult Configure(PayPalStandardConfigurationModel model, FormCollection form) + public ActionResult Configure(PayPalStandardConfigurationModel model, FormCollection form) { - if (!ModelState.IsValid) - return Configure(); - - ModelState.Clear(); + var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - int storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); - var settings = Services.Settings.LoadSetting(storeScope); + if (!ModelState.IsValid) + { + return Configure(settings, storeScope); + } - model.Copy(settings, false); + ModelState.Clear(); + model.Copy(settings, false); using (Services.Settings.BeginScope()) { storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, Services.Settings); + } - // multistore context not possible, see IPN handling + using (Services.Settings.BeginScope()) + { + // Multistore context not possible, see IPN handling. Services.Settings.SaveSetting(settings, x => x.UseSandbox, 0, false); } - NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); + NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); - return Configure(); + return RedirectToConfiguration(PayPalStandardProvider.SystemName, false); } public ActionResult PaymentInfo() diff --git a/src/Plugins/SmartStore.PayPal/Description.txt b/src/Plugins/SmartStore.PayPal/Description.txt index 4800123320..9bb5fc6f74 100644 --- a/src/Plugins/SmartStore.PayPal/Description.txt +++ b/src/Plugins/SmartStore.PayPal/Description.txt @@ -2,7 +2,7 @@ Description: Provides the PayPal payment methods PayPal Standard, PayPal Direct, PayPal Express and PayPal PLUS. SystemName: SmartStore.PayPal Group: Payment -Version: 3.0.3 +Version: 3.0.3.1 MinAppVersion: 3.0.0 DisplayOrder: 1 FileName: SmartStore.PayPal.dll diff --git a/src/Plugins/SmartStore.PayPal/Extensions/MiscExtensions.cs b/src/Plugins/SmartStore.PayPal/Extensions/MiscExtensions.cs index 1bc07950a4..0444d1c9cc 100644 --- a/src/Plugins/SmartStore.PayPal/Extensions/MiscExtensions.cs +++ b/src/Plugins/SmartStore.PayPal/Extensions/MiscExtensions.cs @@ -17,10 +17,7 @@ public static string GetPayPalUrl(this PayPalSettingsBase settings) public static HttpWebRequest GetPayPalWebRequest(this PayPalSettingsBase settings) { - if (settings.SecurityProtocol.HasValue) - { - ServicePointManager.SecurityProtocol = settings.SecurityProtocol.Value; - } + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; var request = (HttpWebRequest)WebRequest.Create(GetPayPalUrl(settings)); return request; diff --git a/src/Plugins/SmartStore.PayPal/Filters/PayPalPlusCheckoutFilter.cs b/src/Plugins/SmartStore.PayPal/Filters/PayPalPlusCheckoutFilter.cs index b5d118ed35..a86570516d 100644 --- a/src/Plugins/SmartStore.PayPal/Filters/PayPalPlusCheckoutFilter.cs +++ b/src/Plugins/SmartStore.PayPal/Filters/PayPalPlusCheckoutFilter.cs @@ -62,7 +62,7 @@ public void OnActionExecuting(ActionExecutingContext filterContext) } // Skip payment if the cart total is zero. PayPal would return an error "Amount cannot be zero". - var cartTotal = _orderTotalCalculationService.Value.GetShoppingCartTotal(cart, true); + decimal? cartTotal = _orderTotalCalculationService.Value.GetShoppingCartTotal(cart, true); if (cartTotal.HasValue && cartTotal.Value == decimal.Zero) { var urlHelper = new UrlHelper(filterContext.HttpContext.Request.RequestContext); diff --git a/src/Plugins/SmartStore.PayPal/Localization/resources.de-de.xml b/src/Plugins/SmartStore.PayPal/Localization/resources.de-de.xml index e77aab5b4b..18140115e2 100644 --- a/src/Plugins/SmartStore.PayPal/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.PayPal/Localization/resources.de-de.xml @@ -68,9 +68,6 @@ Legt die Experience Profil-ID fest. Das Profil beinhaltet globale Daten wie z.B. Shop-Name und -Logo. Ein Profil braucht nur einmalig angelegt werden. Über den Button können Sie ein neues Profil erstellen oder ein vorhandenes aktualisieren. - - Experience Profil-ID - Webhook-ID @@ -98,12 +95,6 @@ Bestimmen Sie den Transaktionsmodus. - - Sicherheitsprotokoll - - - Legt das mit der PayPal-API zu verwendende Sicherheitsprotokoll fest. - API Benutzername @@ -122,18 +113,6 @@ Geben Sie Ihre Signatur ein. - - Zusätzliche Gebühren - - - Zusätzliche Gebühren, die dem Kunden berechnet werden sollen. - - - Zusätzliche Gebühren (prozentual) - - - Zusätzliche prozentuale Gebühr zum Gesamtbetrag. Es wird ein fester Wert verwendet, falls diese Option nicht aktiviert ist. - PayPal Adresse anzeigen @@ -149,9 +128,6 @@ Die Zahlart ist nicht aktiv ({0}). - - PayPal IPN. Wiederkehrende Zahlung. - PayPal IPN. Bestellung konnte nicht gefunden werden. @@ -196,7 +172,7 @@
                  1. In Ihr Premier- oder Business-Konto einloggen.
                  2. Auf den Register Mein Profil klicken.
                  3. -
                  4. Unter Sprach-Kodierung > Weitere Einstellungen wählen Sie bitte UTF-8.
                  5. +
                  6. Unter Sprach-Kodierung > Weitere Einstellungen wählen Sie bitte UTF-8.
                  7. Sofortige Zahlungsbestätigung klicken.
                  8. Einstellungen für sofortige Zahlungsbestätigungen wählen klicken.
                  9. Bei Benachrichtigungs-URL die URL Ihres IPN-Handlers (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalExpress/IPNHandler) eingeben.
                  10. @@ -270,7 +246,7 @@
                    1. In Ihr Premier- oder Business-Konto einloggen.
                    2. Auf den Register Mein Profil klicken.
                    3. -
                    4. Unter Sprach-Kodierung > Weitere Einstellungen wählen Sie bitte UTF-8.
                    5. +
                    6. Unter Sprach-Kodierung > Weitere Einstellungen wählen Sie bitte UTF-8.
                    7. Sofortige Zahlungsbestätigung klicken.
                    8. Einstellungen für sofortige Zahlungsbestätigungen wählen klicken.
                    9. Bei Benachrichtigungs-URL die URL Ihres IPN-Handlers (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalDirect/IPNHandler) eingeben.
                    10. @@ -323,7 +299,7 @@
                      1. In Ihr Premier- oder Business-Konto einloggen.
                      2. Auf den Register Mein Profil klicken.
                      3. -
                      4. Unter Sprach-Kodierung > Weitere Einstellungen wählen Sie bitte UTF-8.
                      5. +
                      6. Unter Sprach-Kodierung > Weitere Einstellungen wählen Sie bitte UTF-8.
                      7. Zurück zu Mein Profil und auf Sofortige Zahlungsbestätigung klicken.
                      8. Einstellungen für sofortige Zahlungsbestätigungen wählen klicken.
                      9. Bei Benachrichtigungs-URL die URL Ihres IPN-Handlers (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalStandard/IPNHandler) eingeben.
                      10. diff --git a/src/Plugins/SmartStore.PayPal/Localization/resources.en-us.xml b/src/Plugins/SmartStore.PayPal/Localization/resources.en-us.xml index 70f7133bab..d0a439de43 100644 --- a/src/Plugins/SmartStore.PayPal/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.PayPal/Localization/resources.en-us.xml @@ -98,12 +98,6 @@ Specify the payment transaction mode. - - Security protocol - - - Specifies the security protocol to use with the PayPal API. - API Account Name @@ -122,18 +116,6 @@ Enter the signature. - - Additional fee - - - Enter additional fee to charge your customers. - - - Additional fee. Use percentage. - - - Specifies whether to apply a percentage additional fee to the order total. A fixed value is used if not enabled. - Show PayPal address diff --git a/src/Plugins/SmartStore.PayPal/Models/ApiConfigurationModels.cs b/src/Plugins/SmartStore.PayPal/Models/ApiConfigurationModels.cs index be7d0a64f1..ad1af7b03c 100644 --- a/src/Plugins/SmartStore.PayPal/Models/ApiConfigurationModels.cs +++ b/src/Plugins/SmartStore.PayPal/Models/ApiConfigurationModels.cs @@ -1,18 +1,17 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Net; using System.Web.Mvc; -using SmartStore.PayPal.Services; +using SmartStore.ComponentModel; using SmartStore.PayPal.Settings; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Modelling; namespace SmartStore.PayPal.Models { - public abstract class ApiConfigurationModel: ModelBase + public abstract class ApiConfigurationModel : ModelBase { public string[] ConfigGroups { get; set; } + public string PrimaryStoreCurrencyCode { get; set; } [SmartResourceDisplayName("Plugins.Payments.PayPal.UseSandbox")] public bool UseSandbox { get; set; } @@ -21,12 +20,7 @@ public abstract class ApiConfigurationModel: ModelBase public bool IpnChangesPaymentStatus { get; set; } [SmartResourceDisplayName("Plugins.Payments.PayPal.TransactMode")] - public int TransactMode { get; set; } - public SelectList TransactModeValues { get; set; } - - [SmartResourceDisplayName("Plugins.Payments.PayPal.SecurityProtocol")] - public SecurityProtocolType? SecurityProtocol { get; set; } - public List AvailableSecurityProtocols { get; set; } + public TransactMode TransactMode { get; set; } [SmartResourceDisplayName("Plugins.Payments.PayPal.ApiAccountName")] public string ApiAccountName { get; set; } @@ -50,10 +44,10 @@ public abstract class ApiConfigurationModel: ModelBase [SmartResourceDisplayName("Plugins.SmartStore.PayPal.WebhookId")] public string WebhookId { get; set; } - [SmartResourceDisplayName("Plugins.Payments.PayPal.AdditionalFee")] + [SmartResourceDisplayName("Admin.Configuration.Payment.Methods.AdditionalFee")] public decimal AdditionalFee { get; set; } - [SmartResourceDisplayName("Plugins.Payments.PayPal.AdditionalFeePercentage")] + [SmartResourceDisplayName("Admin.Configuration.Payment.Methods.AdditionalFeePercentage")] public bool AdditionalFeePercentage { get; set; } } @@ -62,29 +56,9 @@ public class PayPalDirectConfigurationModel : ApiConfigurationModel public void Copy(PayPalDirectPaymentSettings settings, bool fromSettings) { if (fromSettings) - { - SecurityProtocol = settings.SecurityProtocol; - UseSandbox = settings.UseSandbox; - IpnChangesPaymentStatus = settings.IpnChangesPaymentStatus; - TransactMode = Convert.ToInt32(settings.TransactMode); - ApiAccountName = settings.ApiAccountName; - ApiAccountPassword = settings.ApiAccountPassword; - Signature = settings.Signature; - AdditionalFee = settings.AdditionalFee; - AdditionalFeePercentage = settings.AdditionalFeePercentage; - } + MiniMapper.Map(settings, this); else - { - settings.SecurityProtocol = SecurityProtocol; - settings.UseSandbox = UseSandbox; - settings.IpnChangesPaymentStatus = IpnChangesPaymentStatus; - settings.TransactMode = (TransactMode)TransactMode; - settings.ApiAccountName = ApiAccountName; - settings.ApiAccountPassword = ApiAccountPassword; - settings.Signature = Signature; - settings.AdditionalFee = AdditionalFee; - settings.AdditionalFeePercentage = AdditionalFeePercentage; - } + MiniMapper.Map(this, settings); } } @@ -108,45 +82,20 @@ public class PayPalExpressConfigurationModel : ApiConfigurationModel public void Copy(PayPalExpressPaymentSettings settings, bool fromSettings) { if (fromSettings) - { - SecurityProtocol = settings.SecurityProtocol; - UseSandbox = settings.UseSandbox; - IpnChangesPaymentStatus = settings.IpnChangesPaymentStatus; - TransactMode = Convert.ToInt32(settings.TransactMode); - ApiAccountName = settings.ApiAccountName; - ApiAccountPassword = settings.ApiAccountPassword; - Signature = settings.Signature; - AdditionalFee = settings.AdditionalFee; - AdditionalFeePercentage = settings.AdditionalFeePercentage; - ShowButtonInMiniShoppingCart = settings.ShowButtonInMiniShoppingCart; - ConfirmedShipment = settings.ConfirmedShipment; - NoShipmentAddress = settings.NoShipmentAddress; - CallbackTimeout = settings.CallbackTimeout; - DefaultShippingPrice = settings.DefaultShippingPrice; - } + MiniMapper.Map(settings, this); else - { - settings.SecurityProtocol = SecurityProtocol; - settings.UseSandbox = UseSandbox; - settings.IpnChangesPaymentStatus = IpnChangesPaymentStatus; - settings.TransactMode = (TransactMode)TransactMode; - settings.ApiAccountName = ApiAccountName; - settings.ApiAccountPassword = ApiAccountPassword; - settings.Signature = Signature; - settings.AdditionalFee = AdditionalFee; - settings.AdditionalFeePercentage = AdditionalFeePercentage; - settings.ShowButtonInMiniShoppingCart = ShowButtonInMiniShoppingCart; - settings.ConfirmedShipment = ConfirmedShipment; - settings.NoShipmentAddress = NoShipmentAddress; - settings.CallbackTimeout = CallbackTimeout; - settings.DefaultShippingPrice = DefaultShippingPrice; - } + MiniMapper.Map(this, settings); } } public class PayPalPlusConfigurationModel : ApiConfigurationModel { + public PayPalPlusConfigurationModel() + { + TransactMode = TransactMode.AuthorizeAndCapture; + } + [SmartResourceDisplayName("Plugins.Payments.PayPalPlus.ThirdPartyPaymentMethods")] public List ThirdPartyPaymentMethods { get; set; } public List AvailableThirdPartyPaymentMethods { get; set; } @@ -162,35 +111,11 @@ public void Copy(PayPalPlusPaymentSettings settings, bool fromSettings) { if (fromSettings) { - SecurityProtocol = settings.SecurityProtocol; - UseSandbox = settings.UseSandbox; - TransactMode = (int)Settings.TransactMode.AuthorizeAndCapture; - AdditionalFee = settings.AdditionalFee; - AdditionalFeePercentage = settings.AdditionalFeePercentage; - - ClientId = settings.ClientId; - Secret = settings.Secret; - ExperienceProfileId = settings.ExperienceProfileId; - WebhookId = settings.WebhookId; - ThirdPartyPaymentMethods = settings.ThirdPartyPaymentMethods; - DisplayPaymentMethodLogo = settings.DisplayPaymentMethodLogo; - DisplayPaymentMethodDescription = settings.DisplayPaymentMethodDescription; + MiniMapper.Map(settings, this); } else { - settings.SecurityProtocol = SecurityProtocol; - settings.UseSandbox = UseSandbox; - settings.TransactMode = Settings.TransactMode.AuthorizeAndCapture; - settings.AdditionalFee = AdditionalFee; - settings.AdditionalFeePercentage = AdditionalFeePercentage; - - settings.ClientId = ClientId; - settings.Secret = Secret; - settings.ExperienceProfileId = ExperienceProfileId; - settings.WebhookId = WebhookId; - settings.ThirdPartyPaymentMethods = ThirdPartyPaymentMethods; - settings.DisplayPaymentMethodLogo = DisplayPaymentMethodLogo; - settings.DisplayPaymentMethodDescription = DisplayPaymentMethodDescription; + MiniMapper.Map(this, settings); } } } diff --git a/src/Plugins/SmartStore.PayPal/Models/PayPalStandardConfigurationModel.cs b/src/Plugins/SmartStore.PayPal/Models/PayPalStandardConfigurationModel.cs index 017f939bf9..15510ada7a 100644 --- a/src/Plugins/SmartStore.PayPal/Models/PayPalStandardConfigurationModel.cs +++ b/src/Plugins/SmartStore.PayPal/Models/PayPalStandardConfigurationModel.cs @@ -1,24 +1,11 @@ -using System.Collections.Generic; -using System.Net; -using System.Web.Mvc; +using SmartStore.ComponentModel; using SmartStore.PayPal.Settings; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Modelling; namespace SmartStore.PayPal.Models { - public class PayPalStandardConfigurationModel : ModelBase + public class PayPalStandardConfigurationModel : ApiConfigurationModel { - [SmartResourceDisplayName("Plugins.Payments.PayPal.SecurityProtocol")] - public SecurityProtocolType? SecurityProtocol { get; set; } - public List AvailableSecurityProtocols { get; set; } - - [SmartResourceDisplayName("Plugins.Payments.PayPal.UseSandbox")] - public bool UseSandbox { get; set; } - - [SmartResourceDisplayName("Plugins.Payments.PayPal.IpnChangesPaymentStatus")] - public bool IpnChangesPaymentStatus { get; set; } - [SmartResourceDisplayName("Plugins.Payments.PayPalStandard.Fields.BusinessEmail")] public string BusinessEmail { get; set; } @@ -31,12 +18,6 @@ public class PayPalStandardConfigurationModel : ModelBase [SmartResourceDisplayName("Plugins.Payments.PayPalStandard.Fields.PdtValidateOnlyWarn")] public bool PdtValidateOnlyWarn { get; set; } - [SmartResourceDisplayName("Plugins.Payments.PayPal.AdditionalFee")] - public decimal AdditionalFee { get; set; } - - [SmartResourceDisplayName("Plugins.Payments.PayPal.AdditionalFeePercentage")] - public bool AdditionalFeePercentage { get; set; } - [SmartResourceDisplayName("Plugins.Payments.PayPal.IsShippingAddressRequired")] public bool IsShippingAddressRequired { get; set; } @@ -55,39 +36,9 @@ public class PayPalStandardConfigurationModel : ModelBase public void Copy(PayPalStandardPaymentSettings settings, bool fromSettings) { if (fromSettings) - { - SecurityProtocol = settings.SecurityProtocol; - UseSandbox = settings.UseSandbox; - IpnChangesPaymentStatus = settings.IpnChangesPaymentStatus; - BusinessEmail = settings.BusinessEmail; - PdtToken = settings.PdtToken; - PdtValidateOrderTotal = settings.PdtValidateOrderTotal; - PdtValidateOnlyWarn = settings.PdtValidateOnlyWarn; - AdditionalFee = settings.AdditionalFee; - AdditionalFeePercentage = settings.AdditionalFeePercentage; - IsShippingAddressRequired = settings.IsShippingAddressRequired; - UsePayPalAddress = settings.UsePayPalAddress; - PassProductNamesAndTotals = settings.PassProductNamesAndTotals; - EnableIpn = settings.EnableIpn; - IpnUrl = settings.IpnUrl; - } + MiniMapper.Map(settings, this); else - { - settings.SecurityProtocol = SecurityProtocol; - settings.UseSandbox = UseSandbox; - settings.IpnChangesPaymentStatus = IpnChangesPaymentStatus; - settings.BusinessEmail = BusinessEmail; - settings.PdtToken = PdtToken; - settings.PdtValidateOrderTotal = PdtValidateOrderTotal; - settings.PdtValidateOnlyWarn = PdtValidateOnlyWarn; - settings.AdditionalFee = AdditionalFee; - settings.AdditionalFeePercentage = AdditionalFeePercentage; - settings.IsShippingAddressRequired = IsShippingAddressRequired; - settings.UsePayPalAddress = UsePayPalAddress; - settings.PassProductNamesAndTotals = PassProductNamesAndTotals; - settings.EnableIpn = EnableIpn; - settings.IpnUrl = IpnUrl; - } + MiniMapper.Map(this, settings); } } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Properties/Settings.Designer.cs b/src/Plugins/SmartStore.PayPal/Properties/Settings.Designer.cs index 68e1a2dbfb..919a54792a 100644 --- a/src/Plugins/SmartStore.PayPal/Properties/Settings.Designer.cs +++ b/src/Plugins/SmartStore.PayPal/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace SmartStore.PayPal.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.0.1.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); diff --git a/src/Plugins/SmartStore.PayPal/Providers/PayPalExpressProvider.cs b/src/Plugins/SmartStore.PayPal/Providers/PayPalExpressProvider.cs index a3c3842b81..bcf2d1aa58 100644 --- a/src/Plugins/SmartStore.PayPal/Providers/PayPalExpressProvider.cs +++ b/src/Plugins/SmartStore.PayPal/Providers/PayPalExpressProvider.cs @@ -27,7 +27,7 @@ namespace SmartStore.PayPal { [SystemName("Payments.PayPalExpress")] [FriendlyName("PayPal Express")] - [DisplayOrder(0)] + [DisplayOrder(1)] public partial class PayPalExpressProvider : PayPalProviderBase { private readonly ICurrencyService _currencyService; diff --git a/src/Plugins/SmartStore.PayPal/Providers/PayPalProviderBase.cs b/src/Plugins/SmartStore.PayPal/Providers/PayPalProviderBase.cs index 1c65bd39dd..1c3469dae1 100644 --- a/src/Plugins/SmartStore.PayPal/Providers/PayPalProviderBase.cs +++ b/src/Plugins/SmartStore.PayPal/Providers/PayPalProviderBase.cs @@ -59,10 +59,7 @@ public override bool SupportVoid protected PayPalAPIAASoapBinding GetApiAaService(TSetting settings) { - if (settings.SecurityProtocol.HasValue) - { - ServicePointManager.SecurityProtocol = settings.SecurityProtocol.Value; - } + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; var service = new PayPalAPIAASoapBinding(); @@ -75,10 +72,7 @@ protected PayPalAPIAASoapBinding GetApiAaService(TSetting settings) protected PayPalAPISoapBinding GetApiService(TSetting settings) { - if (settings.SecurityProtocol.HasValue) - { - ServicePointManager.SecurityProtocol = settings.SecurityProtocol.Value; - } + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; var service = new PayPalAPISoapBinding(); diff --git a/src/Plugins/SmartStore.PayPal/Providers/PayPalStandardProvider.cs b/src/Plugins/SmartStore.PayPal/Providers/PayPalStandardProvider.cs index 454431845f..56a27c54ef 100644 --- a/src/Plugins/SmartStore.PayPal/Providers/PayPalStandardProvider.cs +++ b/src/Plugins/SmartStore.PayPal/Providers/PayPalStandardProvider.cs @@ -24,7 +24,7 @@ namespace SmartStore.PayPal { [SystemName("Payments.PayPalStandard")] [FriendlyName("PayPal Standard")] - [DisplayOrder(2)] + [DisplayOrder(1)] public partial class PayPalStandardProvider : PaymentPluginBase, IConfigurable { private readonly IOrderTotalCalculationService _orderTotalCalculationService; @@ -411,7 +411,7 @@ public List GetLineItems(PostProcessPaymentRequest postProcessPa var order = postProcessPaymentRequest.Order; var lst = new List(); - // order items... checkout attributes are included in order total + // Order items... checkout attributes are included in order total foreach (var orderItem in order.OrderItems) { var item = new PayPalLineItem @@ -426,7 +426,20 @@ public List GetLineItems(PostProcessPaymentRequest postProcessPa cartTotal += orderItem.PriceExclTax; } - // shipping + // Rounding + if (order.OrderTotalRounding != decimal.Zero) + { + var item = new PayPalLineItem + { + Type = PayPalItemType.Rounding, + Name = T("ShoppingCart.Totals.Rounding").Text, + Quantity = 1, + Amount = order.OrderTotalRounding + }; + lst.Add(item); + } + + // Shipping if (order.OrderShippingExclTax > decimal.Zero) { var item = new PayPalLineItem @@ -441,7 +454,7 @@ public List GetLineItems(PostProcessPaymentRequest postProcessPa cartTotal += order.OrderShippingExclTax; } - // payment fee + // Payment fee if (order.PaymentMethodAdditionalFeeExclTax > decimal.Zero) { var item = new PayPalLineItem @@ -456,7 +469,7 @@ public List GetLineItems(PostProcessPaymentRequest postProcessPa cartTotal += order.PaymentMethodAdditionalFeeExclTax; } - // tax + // Tax if (order.OrderTax > decimal.Zero) { var item = new PayPalLineItem @@ -610,6 +623,7 @@ public enum PayPalItemType CartItem = 0, Shipping, PaymentFee, - Tax + Tax, + Rounding } } diff --git a/src/Plugins/SmartStore.PayPal/Services/PayPalService.cs b/src/Plugins/SmartStore.PayPal/Services/PayPalService.cs index 68574190e1..9279692281 100644 --- a/src/Plugins/SmartStore.PayPal/Services/PayPalService.cs +++ b/src/Plugins/SmartStore.PayPal/Services/PayPalService.cs @@ -12,7 +12,6 @@ using SmartStore.Core.Data; using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Discounts; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; using SmartStore.Core.Domain.Stores; @@ -125,17 +124,11 @@ private Dictionary CreateAmount( var currency = _services.WorkContext.WorkingCurrency; var currencyCode = store.PrimaryStoreCurrency.CurrencyCode; var includingTax = (_services.WorkContext.GetTaxDisplayTypeFor(customer, store.Id) == TaxDisplayType.IncludingTax); - - Discount orderAppliedDiscount; - List appliedGiftCards; - int redeemedRewardPoints = 0; - decimal redeemedRewardPointsAmount; - decimal orderDiscountInclTax; - decimal totalOrderItems = decimal.Zero; + var totalOrderItems = decimal.Zero; var taxTotal = decimal.Zero; - var total = Math.Round(_orderTotalCalculationService.GetShoppingCartTotal(cart, out orderDiscountInclTax, out orderAppliedDiscount, out appliedGiftCards, - out redeemedRewardPoints, out redeemedRewardPointsAmount) ?? decimal.Zero, 2); + var cartTotal = _orderTotalCalculationService.GetShoppingCartTotal(cart); + var total = Math.Round(cartTotal.TotalAmount ?? decimal.Zero, 2); if (total == decimal.Zero) { @@ -169,7 +162,23 @@ private Dictionary CreateAmount( totalOrderItems += (Math.Round(productPrice, 2) * item.Item.Quantity); } - if (items != null && paymentFee != decimal.Zero) + // Rounding. + if (cartTotal.RoundingAmount != decimal.Zero) + { + if (items != null) + { + var line = new Dictionary(); + line.Add("quantity", "1"); + line.Add("name", T("ShoppingCart.Totals.Rounding").Text.Truncate(127)); + line.Add("price", cartTotal.RoundingAmount.FormatInvariant()); + line.Add("currency", currencyCode); + items.Add(line); + } + + totalOrderItems += Math.Round(cartTotal.RoundingAmount, 2); + } + + if (items != null && paymentFee != decimal.Zero) { var line = new Dictionary(); line.Add("quantity", "1"); @@ -269,37 +278,6 @@ public static string GetApiUrl(bool sandbox) return sandbox ? "https://api.sandbox.paypal.com" : "https://api.paypal.com"; } - public static Dictionary GetSecurityProtocols() - { - var dic = new Dictionary(); - - foreach (SecurityProtocolType protocol in Enum.GetValues(typeof(SecurityProtocolType))) - { - string friendlyName = null; - switch (protocol) - { - case SecurityProtocolType.Ssl3: - friendlyName = "SSL 3.0"; - break; - case SecurityProtocolType.Tls: - friendlyName = "TLS 1.0"; - break; - case SecurityProtocolType.Tls11: - friendlyName = "TLS 1.1"; - break; - case SecurityProtocolType.Tls12: - friendlyName = "TLS 1.2"; - break; - default: - friendlyName = protocol.ToString().ToUpper(); - break; - } - - dic.Add(protocol, friendlyName); - } - return dic; - } - public void AddOrderNote(PayPalSettingsBase settings, Order order, string anyString, bool isIpn = false) { try @@ -507,8 +485,7 @@ public PayPalResponse CallApi(string method, string path, string accessToken, Pa if (method.IsCaseInsensitiveEqual("GET") && data.HasValue()) url = url.EnsureEndsWith("?") + data; - if (settings.SecurityProtocol.HasValue) - ServicePointManager.SecurityProtocol = settings.SecurityProtocol.Value; + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; var request = (HttpWebRequest)WebRequest.Create(url); request.Method = method; @@ -966,9 +943,9 @@ public PayPalResponse UpsertCheckoutExperience(PayPalApiSettingsBase settings, P presentation.Add("brand_name", name); presentation.Add("locale_code", _services.WorkContext.WorkingLanguage.UniqueSeoCode.EmptyNull().ToUpper()); - + if (logo != null) - presentation.Add("logo_image", _pictureService.Value.GetPictureUrl(logo, showDefaultPicture: false, storeLocation: store.Url)); + presentation.Add("logo_image", _pictureService.Value.GetUrl(logo, 0, false, _services.StoreService.GetHost(store))); inpuFields.Add("allow_note", false); inpuFields.Add("no_shipping", 0); diff --git a/src/Plugins/SmartStore.PayPal/Settings/PayPalSettings.cs b/src/Plugins/SmartStore.PayPal/Settings/PayPalSettings.cs index 1d92e8070b..3fb6bc563f 100644 --- a/src/Plugins/SmartStore.PayPal/Settings/PayPalSettings.cs +++ b/src/Plugins/SmartStore.PayPal/Settings/PayPalSettings.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Net; using SmartStore.Core.Configuration; namespace SmartStore.PayPal.Settings @@ -8,13 +7,10 @@ public abstract class PayPalSettingsBase { public PayPalSettingsBase() { - SecurityProtocol = SecurityProtocolType.Tls12; IpnChangesPaymentStatus = true; AddOrderNotes = true; } - public SecurityProtocolType? SecurityProtocol { get; set; } - public bool UseSandbox { get; set; } public bool AddOrderNotes { get; set; } diff --git a/src/Plugins/SmartStore.PayPal/SmartStore.PayPal.csproj b/src/Plugins/SmartStore.PayPal/SmartStore.PayPal.csproj index 8dfd552676..e75e60050d 100644 --- a/src/Plugins/SmartStore.PayPal/SmartStore.PayPal.csproj +++ b/src/Plugins/SmartStore.PayPal/SmartStore.PayPal.csproj @@ -1,5 +1,6 @@  + @@ -20,7 +21,7 @@ Properties SmartStore.PayPal SmartStore.PayPal - v4.5.2 + v4.6.1 512 ..\..\ @@ -35,6 +36,9 @@ + + + true @@ -498,6 +502,12 @@ + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + - + @@ -148,13 +156,10 @@ + + + + - - - - - - - - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs b/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs index 5c5e03f8ab..e55f1cdd0a 100644 --- a/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs +++ b/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs @@ -30,12 +30,12 @@ public ByTotalController( AdminAreaSettings adminAreaSettings, ICommonServices services) { - this._shippingService = shippingService; - this._shippingByTotalService = shippingByTotalService; - this._shippingByTotalSettings = shippingByTotalSettings; - this._countryService = countryService; - this._adminAreaSettings = adminAreaSettings; - this._services = services; + _shippingService = shippingService; + _shippingByTotalService = shippingByTotalService; + _shippingByTotalSettings = shippingByTotalSettings; + _countryService = countryService; + _adminAreaSettings = adminAreaSettings; + _services = services; } public ActionResult Configure() @@ -148,22 +148,26 @@ public ActionResult AddShippingRate(ByTotalListModel model) BaseCharge = model.AddBaseCharge, MaxCharge = model.AddMaxCharge }; + _shippingByTotalService.InsertShippingByTotalRecord(shippingByTotalRecord); - return Json(new { Result = true }); + NotifySuccess(T("Plugins.Shipping.ByTotal.AddNewRecord.Success")); + + return Json(new { Result = true }); } [HttpPost] - public ActionResult SaveGeneralSettings(ByTotalListModel model) + public ActionResult Configure(ByTotalListModel model) { - //save settings _shippingByTotalSettings.LimitMethodsToCreated = model.LimitMethodsToCreated; _shippingByTotalSettings.SmallQuantityThreshold = model.SmallQuantityThreshold; _shippingByTotalSettings.SmallQuantitySurcharge = model.SmallQuantitySurcharge; _services.Settings.SaveSetting(_shippingByTotalSettings); - return Json(new { Result = true }); - } + NotifySuccess(T("Admin.Configuration.Updated")); + + return Configure(); + } } } diff --git a/src/Plugins/SmartStore.Shipping/Localization/resources.de-de.xml b/src/Plugins/SmartStore.Shipping/Localization/resources.de-de.xml index 259ba4ecd4..c5d47af01d 100644 --- a/src/Plugins/SmartStore.Shipping/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.Shipping/Localization/resources.de-de.xml @@ -15,10 +15,15 @@ Neue Versandbedingung hinzufügen - + + Die neue Versandbedingung wurde erfolgreich zugefügt. + + Optionen - + + Versandbedingungen + Land diff --git a/src/Plugins/SmartStore.Shipping/Localization/resources.en-us.xml b/src/Plugins/SmartStore.Shipping/Localization/resources.en-us.xml index e229ab4d46..16cfa0bb5f 100644 --- a/src/Plugins/SmartStore.Shipping/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.Shipping/Localization/resources.en-us.xml @@ -16,10 +16,15 @@ Add new record - - Settings - - + + The new record was successfully added. + + + Options + + + Shipping records + Country diff --git a/src/Plugins/SmartStore.Shipping/Providers/ByTotalProvider.cs b/src/Plugins/SmartStore.Shipping/Providers/ByTotalProvider.cs index d50e660045..05bd66b8cc 100644 --- a/src/Plugins/SmartStore.Shipping/Providers/ByTotalProvider.cs +++ b/src/Plugins/SmartStore.Shipping/Providers/ByTotalProvider.cs @@ -198,7 +198,7 @@ public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest get decimal sqThreshold = _shippingByTotalSettings.SmallQuantityThreshold; decimal sqSurcharge = _shippingByTotalSettings.SmallQuantitySurcharge; - var shippingMethods = _shippingService.GetAllShippingMethods(getShippingOptionRequest); + var shippingMethods = _shippingService.GetAllShippingMethods(getShippingOptionRequest, storeId); foreach (var shippingMethod in shippingMethods) { decimal? rate = GetRate(subTotal, shippingMethod.Id, storeId, countryId, stateProvinceId, zip); diff --git a/src/Plugins/SmartStore.Shipping/Providers/FixedRateProvider.cs b/src/Plugins/SmartStore.Shipping/Providers/FixedRateProvider.cs index cc303cee6e..3612ec78e1 100644 --- a/src/Plugins/SmartStore.Shipping/Providers/FixedRateProvider.cs +++ b/src/Plugins/SmartStore.Shipping/Providers/FixedRateProvider.cs @@ -58,7 +58,7 @@ public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest get return response; } - var shippingMethods = this._shippingService.GetAllShippingMethods(getShippingOptionRequest); + var shippingMethods = this._shippingService.GetAllShippingMethods(getShippingOptionRequest, getShippingOptionRequest.StoreId); foreach (var shippingMethod in shippingMethods) { var shippingOption = new ShippingOption(); @@ -82,7 +82,7 @@ public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest get if (getShippingOptionRequest == null) throw new ArgumentNullException("getShippingOptionRequest"); - var shippingMethods = this._shippingService.GetAllShippingMethods(getShippingOptionRequest); + var shippingMethods = _shippingService.GetAllShippingMethods(getShippingOptionRequest, getShippingOptionRequest.StoreId); var rates = new List(); foreach (var shippingMethod in shippingMethods) diff --git a/src/Plugins/SmartStore.Shipping/Services/ShippingByTotalService.cs b/src/Plugins/SmartStore.Shipping/Services/ShippingByTotalService.cs index a24143413c..8d2e5b8ceb 100644 --- a/src/Plugins/SmartStore.Shipping/Services/ShippingByTotalService.cs +++ b/src/Plugins/SmartStore.Shipping/Services/ShippingByTotalService.cs @@ -221,7 +221,7 @@ private bool ZipMatches(string zip, string pattern) { foreach (var entry in patterns) { - var wildcard = new Wildcard(entry); + var wildcard = new Wildcard(entry, true); if (wildcard.IsMatch(zip)) return true; } diff --git a/src/Plugins/SmartStore.Shipping/SmartStore.Shipping.csproj b/src/Plugins/SmartStore.Shipping/SmartStore.Shipping.csproj index 7d51819e8e..73b0d51d96 100644 --- a/src/Plugins/SmartStore.Shipping/SmartStore.Shipping.csproj +++ b/src/Plugins/SmartStore.Shipping/SmartStore.Shipping.csproj @@ -20,7 +20,7 @@ Properties SmartStore.Shipping SmartStore.Shipping - v4.5.2 + v4.6.1 512 @@ -89,12 +89,12 @@ ..\..\packages\Autofac.Mvc5.4.0.2\lib\net45\Autofac.Integration.Mvc.dll - False - ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll + ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll + True - False - ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll + ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll + True ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll diff --git a/src/Plugins/SmartStore.Shipping/Views/ByTotal/Configure.cshtml b/src/Plugins/SmartStore.Shipping/Views/ByTotal/Configure.cshtml index ae05d59b64..4e138a8fc7 100644 --- a/src/Plugins/SmartStore.Shipping/Views/ByTotal/Configure.cshtml +++ b/src/Plugins/SmartStore.Shipping/Views/ByTotal/Configure.cshtml @@ -1,103 +1,140 @@ -@{ - Layout = ""; -} -@model ByTotalListModel - +@model ByTotalListModel @using SmartStore.Shipping.Models; @using SmartStore.Web.Framework; @using Telerik.Web.Mvc.UI; @using SmartStore.Web.Framework.UI +@{ + Layout = ""; +} - - - @T("Plugins.Shipping.ByTotal.AddNewRecordTitle") - +
                        + +
                        - - - - -
                        - @(Html.Telerik().Grid() - .Name("shipping-bytotal-grid") - .DataKeys(keys => keys.Add(x => x.Id).RouteKey("Id")) - .DataBinding(dataBinding => dataBinding - .Ajax() - .Select("RatesList", "ByTotal", new RouteValueDictionary() { { "area", "SmartStore.Shipping" } }) - .Update("RateUpdate", "ByTotal", new RouteValueDictionary() { { "area", "SmartStore.Shipping" } }) - .Delete("RateDelete", "ByTotal", new RouteValueDictionary() { { "area", "SmartStore.Shipping" } }) - ) - .Columns(columns => - { - columns.Bound(x => x.StoreName).ReadOnly(); - columns.Bound(x => x.CountryName).ReadOnly(); - columns.Bound(x => x.StateProvinceName).ReadOnly(); - columns.Bound(x => x.Zip); - columns.Bound(x => x.ShippingMethodName).ReadOnly(); - columns.Bound(x => x.From) - .Format("{0:0.00}"); - columns.Bound(x => x.To) - .Format("{0:0.00}"); - columns.Bound(x => x.UsePercentage) - .Centered() - .Template(item => @Html.SymbolForBool(item.UsePercentage)) - .ClientTemplate(@Html.SymbolForBool("UsePercentage")); - columns.Bound(x => x.ShippingChargePercentage) - .Format("{0:0.00}"); - columns.Bound(x => x.ShippingChargeAmount) - .Format("{0:0.00}"); - columns.Bound(x => x.BaseCharge) - .Format("{0:0.00}"); - columns.Bound(x => x.MaxCharge) - .Format("{0:0.00}"); - columns.Command(commands => - { - commands.Edit().Localize(T); - commands.Delete().Localize(T); - }).Width(190); - }) - .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) - .Editable(x => x.Mode(GridEditMode.InLine)) - .PreserveGridState() - .EnableCustomBinding(true)) -
                        +@using (Html.BeginForm()) +{ + @Html.SmartStore().TabStrip().Name("shipping-by-weight-edit").Style(TabsStyle.Material).Items(x => + { + x.Add().Text(T("Plugins.Shipping.ByTotal.TabTitleGrid").Text).Content(TabGrid()).Selected(true); + x.Add().Text(T("Plugins.Shipping.ByTotal.TabTitleSettings").Text).Content(TabSettings()); + }) +} - +@helper ShippingByTotalGridCommands(Grid grid) +{ + + + @T("Plugins.Shipping.ByTotal.AddNewRecordTitle") + +} + + - - {Html.SmartStore().Window() - .Name("addrecord-window") - .Title(T("Plugins.Shipping.ByTotal.AddNewRecordTitle")) - .Content( - @ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                        - @Html.SmartLabelFor(model => model.AddStoreId) - - @Html.DropDownListFor(model => model.AddStoreId, Model.AvailableStores) - @Html.ValidationMessageFor(model => model.AddStoreId) -
                        - @Html.SmartLabelFor(model => model.AddCountryId) - - @Html.DropDownListFor(model => model.AddCountryId, Model.AvailableCountries, "*") - @Html.ValidationMessageFor(model => model.AddCountryId) -
                        - @Html.SmartLabelFor(model => model.AddStateProvinceId) - - @Html.DropDownListFor(model => model.AddStateProvinceId, Model.AvailableStates, "*") - @Html.ValidationMessageFor(model => model.AddStateProvinceId) -
                        - @Html.SmartLabelFor(model => model.AddZip) - - @Html.EditorFor(model => model.AddZip) - @Html.ValidationMessageFor(model => model.AddZip) -
                        - @Html.SmartLabelFor(model => model.AddShippingMethodId) - - @Html.DropDownListFor(model => model.AddShippingMethodId, Model.AvailableShippingMethods) - @Html.ValidationMessageFor(model => model.AddShippingMethodId) -
                        - @Html.SmartLabelFor(model => model.AddFrom) - - @Html.EditorFor(model => model.AddFrom) [@Model.PrimaryStoreCurrencyCode] - @Html.ValidationMessageFor(model => model.AddFrom) -
                        - @Html.SmartLabelFor(model => model.AddTo) - - @Html.EditorFor(model => model.AddTo) [@Model.PrimaryStoreCurrencyCode] - @Html.ValidationMessageFor(model => model.AddTo) -
                        - @Html.SmartLabelFor(model => model.AddUsePercentage) - - @Html.EditorFor(model => model.AddUsePercentage) - @Html.ValidationMessageFor(model => model.AddUsePercentage) -
                        - @Html.SmartLabelFor(model => model.AddShippingChargePercentage) - - @Html.EditorFor(model => model.AddShippingChargePercentage) - @Html.ValidationMessageFor(model => model.AddShippingChargePercentage) -
                        - @Html.SmartLabelFor(model => model.AddBaseCharge) - - @Html.EditorFor(model => model.AddBaseCharge) [@Model.PrimaryStoreCurrencyCode] - @Html.ValidationMessageFor(model => model.AddBaseCharge) -
                        - @Html.SmartLabelFor(model => model.AddMaxCharge) - - @Html.EditorFor(model => model.AddMaxCharge) [@Model.PrimaryStoreCurrencyCode] - @Html.ValidationMessageFor(model => model.AddMaxCharge) -
                        - @Html.SmartLabelFor(model => model.AddShippingChargeAmount) - - @Html.EditorFor(model => model.AddShippingChargeAmount) [@Model.PrimaryStoreCurrencyCode] - @Html.ValidationMessageFor(model => model.AddShippingChargeAmount) -
                        -
                        ) - .FooterContent(@ - - - ) - .Modal(true) - .Visible(false) - .Render(); - } - -
                        + }); + -
                        - @T("Plugins.Shipping.ByTotal.SettingsTitle") - - - - - - - - - - - - - - - - - -
                        - @Html.SmartLabelFor(model => model.SmallQuantityThreshold) - - @Html.EditorFor(model => model.SmallQuantityThreshold) [@Model.PrimaryStoreCurrencyCode] - @Html.ValidationMessageFor(model => model.SmallQuantityThreshold) -
                        - @Html.SmartLabelFor(model => model.SmallQuantitySurcharge) - - @Html.EditorFor(model => model.SmallQuantitySurcharge) [@Model.PrimaryStoreCurrencyCode] - @Html.ValidationMessageFor(model => model.SmallQuantitySurcharge) -
                        - @Html.SmartLabelFor(model => model.LimitMethodsToCreated) - - @Html.EditorFor(model => model.LimitMethodsToCreated) - @Html.ValidationMessageFor(model => model.LimitMethodsToCreated) -
                        -   - - -
                        -
                        +@{Html.SmartStore().Window() + .Name("addrecord-window") + .Size(WindowSize.Large) + .Title(T("Plugins.Shipping.ByTotal.AddNewRecordTitle")) + .Content( + @ +
                        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                        + @Html.SmartLabelFor(model => model.AddStoreId) + + @Html.DropDownListFor(model => model.AddStoreId, Model.AvailableStores) + @Html.ValidationMessageFor(model => model.AddStoreId) +
                        + @Html.SmartLabelFor(model => model.AddCountryId) + + @Html.DropDownList("CountryId", Model.AvailableCountries, "*", + new + { + @class = "form-control country-input country-selector", + data_region_control_selector = "#AddStateProvinceId", + data_states_ajax_url = Url.Action("GetStatesByCountryId", "Country", new RouteValueDictionary() { { "area", "Admin" } }), + data_addAsterisk = "true", + data_addEmptyStateIfRequired = "false" + }) + + @Html.ValidationMessageFor(model => model.AddCountryId) +
                        + @Html.SmartLabelFor(model => model.AddStateProvinceId) + + @Html.DropDownListFor(model => model.AddStateProvinceId, Model.AvailableStates, "*") + @Html.ValidationMessageFor(model => model.AddStateProvinceId) +
                        + @Html.SmartLabelFor(model => model.AddZip) + + @Html.EditorFor(model => model.AddZip) + @Html.ValidationMessageFor(model => model.AddZip) +
                        + @Html.SmartLabelFor(model => model.AddShippingMethodId) + + @Html.DropDownListFor(model => model.AddShippingMethodId, Model.AvailableShippingMethods) + @Html.ValidationMessageFor(model => model.AddShippingMethodId) +
                        + @Html.SmartLabelFor(model => model.AddFrom) + + @Html.EditorFor(model => model.AddFrom, new { postfix = Model.PrimaryStoreCurrencyCode }) + @Html.ValidationMessageFor(model => model.AddFrom) +
                        + @Html.SmartLabelFor(model => model.AddTo) + + @Html.EditorFor(model => model.AddTo, new { postfix = Model.PrimaryStoreCurrencyCode }) + @Html.ValidationMessageFor(model => model.AddTo) +
                        + @Html.SmartLabelFor(model => model.AddUsePercentage) + + @Html.EditorFor(model => model.AddUsePercentage) + @Html.ValidationMessageFor(model => model.AddUsePercentage) +
                        + @Html.SmartLabelFor(model => model.AddShippingChargePercentage) + + @Html.EditorFor(model => model.AddShippingChargePercentage) + @Html.ValidationMessageFor(model => model.AddShippingChargePercentage) +
                        + @Html.SmartLabelFor(model => model.AddBaseCharge) + + @Html.EditorFor(model => model.AddBaseCharge, new { postfix = Model.PrimaryStoreCurrencyCode }) + @Html.ValidationMessageFor(model => model.AddBaseCharge) +
                        + @Html.SmartLabelFor(model => model.AddMaxCharge) + + @Html.EditorFor(model => model.AddMaxCharge, new { postfix = Model.PrimaryStoreCurrencyCode }) + @Html.ValidationMessageFor(model => model.AddMaxCharge) +
                        + @Html.SmartLabelFor(model => model.AddShippingChargeAmount) + + @Html.EditorFor(model => model.AddShippingChargeAmount, new { postfix = Model.PrimaryStoreCurrencyCode }) + @Html.ValidationMessageFor(model => model.AddShippingChargeAmount) +
                        +
                        +
                        ) + .FooterContent(@ + + + ) + .Render(); } \ No newline at end of file diff --git a/src/Plugins/SmartStore.Shipping/Views/FixedRate/Configure.cshtml b/src/Plugins/SmartStore.Shipping/Views/FixedRate/Configure.cshtml index c7b0e5c6f1..04876565d0 100644 --- a/src/Plugins/SmartStore.Shipping/Views/FixedRate/Configure.cshtml +++ b/src/Plugins/SmartStore.Shipping/Views/FixedRate/Configure.cshtml @@ -1,39 +1,34 @@ -@{ - Layout = ""; -} -@model Telerik.Web.Mvc.GridModel +@model Telerik.Web.Mvc.GridModel @using Telerik.Web.Mvc.UI; @using SmartStore.Shipping.Models; +@{ + Layout = ""; +} - - - - -
                        - @(Html.Telerik().Grid(Model.Data) - .Name("Grid") - .DataKeys(keys => keys.Add(x => x.ShippingMethodId).RouteKey("ShippingMethodId")) - .Columns(columns => - { - columns.Bound(x => x.ShippingMethodName) - .ReadOnly(); - columns.Bound(x => x.Rate) - .Format("{0:0.00}") - .Width(180); - columns.Command(commands => - { - commands.Edit(); - }).Width(180); - }) - .Editable(x => - { - x.Mode(GridEditMode.InLine); - }) - .DataBinding(dataBinding => - { - dataBinding.Ajax() - .Select("Configure", "FixedRate", new RouteValueDictionary() { { "Namespaces", "SmartStore.Shipping.Controllers" }, { "area", "SmartStore.Shipping" } }) - .Update("ShippingRateUpdate", "FixedRate", new RouteValueDictionary() { { "Namespaces", "SmartStore.Shipping.Controllers" }, { "area", "SmartStore.Shipping" } }); - }) - .EnableCustomBinding(true)) -
                        +
                        + @(Html.Telerik().Grid(Model.Data) + .Name("Grid") + .DataKeys(keys => keys.Add(x => x.ShippingMethodId).RouteKey("ShippingMethodId")) + .Columns(columns => + { + columns.Bound(x => x.ShippingMethodName) + .ReadOnly() + .Width("80%"); ; + columns.Bound(x => x.Rate) + .Format("{0:0.00}") + .Width("8%"); + columns.Command(commands => + { + commands.Edit(); + }).Width("12%") + .HtmlAttributes(new { align = "right", @class = "omega" }); + }) + .DataBinding(dataBinding => + { + dataBinding.Ajax() + .Select("Configure", "FixedRate", new RouteValueDictionary() { { "Namespaces", "SmartStore.Shipping.Controllers" }, { "area", "SmartStore.Shipping" } }) + .Update("ShippingRateUpdate", "FixedRate", new RouteValueDictionary() { { "Namespaces", "SmartStore.Shipping.Controllers" }, { "area", "SmartStore.Shipping" } }); + }) + .Editable(x => { x.Mode(GridEditMode.InLine); }) + .EnableCustomBinding(true)) +
                        diff --git a/src/Plugins/SmartStore.Shipping/packages.config b/src/Plugins/SmartStore.Shipping/packages.config index 9fc69c653d..79c5c7d2ef 100644 --- a/src/Plugins/SmartStore.Shipping/packages.config +++ b/src/Plugins/SmartStore.Shipping/packages.config @@ -2,7 +2,7 @@ - + diff --git a/src/Plugins/SmartStore.Shipping/web.config b/src/Plugins/SmartStore.Shipping/web.config index 275d035d5a..5e59e8cb6a 100644 --- a/src/Plugins/SmartStore.Shipping/web.config +++ b/src/Plugins/SmartStore.Shipping/web.config @@ -4,8 +4,16 @@ + - + @@ -119,13 +127,10 @@ + + + + - - - - - - - - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.ShippingByWeight/ByWeightShippingComputationMethod.cs b/src/Plugins/SmartStore.ShippingByWeight/ByWeightShippingComputationMethod.cs index 3da4c6919b..680abbbe79 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/ByWeightShippingComputationMethod.cs +++ b/src/Plugins/SmartStore.ShippingByWeight/ByWeightShippingComputationMethod.cs @@ -14,6 +14,7 @@ using SmartStore.ShippingByWeight.Data; using SmartStore.ShippingByWeight.Data.Migrations; using SmartStore.ShippingByWeight.Services; +using SmartStore.Core.Domain.Tax; namespace SmartStore.ShippingByWeight { @@ -137,7 +138,9 @@ public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest req int storeId = request.StoreId > 0 ? request.StoreId : _storeContext.CurrentStore.Id; var taxRate = decimal.Zero; - decimal subTotal = decimal.Zero; + decimal subTotalInclTax = decimal.Zero; + decimal subTotalExclTax = decimal.Zero; + decimal currentSubTotal = decimal.Zero; int countryId = 0; string zip = null; @@ -155,25 +158,30 @@ public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest req } var itemSubTotal = _priceCalculationService.GetSubTotal(shoppingCartItem, true); + var itemSubTotalInclTax = _taxService.GetProductPrice(shoppingCartItem.Item.Product, itemSubTotal, true, request.Customer, out taxRate); - subTotal += itemSubTotalInclTax; - } + subTotalInclTax += itemSubTotalInclTax; + + var itemSubTotalExclTax = _taxService.GetProductPrice(shoppingCartItem.Item.Product, itemSubTotal, false, request.Customer, out taxRate); + subTotalExclTax += itemSubTotalExclTax; + } var weight = _shippingService.GetShoppingCartTotalWeight(request.Items, _shippingByWeightSettings.IncludeWeightOfFreeShippingProducts); + var shippingMethods = _shippingService.GetAllShippingMethods(request, storeId); + currentSubTotal = _services.WorkContext.TaxDisplayType == TaxDisplayType.ExcludingTax ? subTotalExclTax : subTotalInclTax; - var shippingMethods = _shippingService.GetAllShippingMethods(request); foreach (var shippingMethod in shippingMethods) { var record = _shippingByWeightService.FindRecord(shippingMethod.Id, storeId, countryId, weight, zip); - decimal? rate = GetRate(subTotal, weight, shippingMethod.Id, storeId, countryId, zip); + decimal? rate = GetRate(subTotalInclTax, weight, shippingMethod.Id, storeId, countryId, zip); if (rate.HasValue) { var shippingOption = new ShippingOption(); shippingOption.ShippingMethodId = shippingMethod.Id; shippingOption.Name = shippingMethod.GetLocalized(x => x.Name); - if (record != null && record.SmallQuantityThreshold > subTotal) + if (record != null && record.SmallQuantityThreshold > currentSubTotal) { shippingOption.Description = shippingMethod.GetLocalized(x => x.Description) + _localizationService.GetResource("Plugin.Shipping.ByWeight.SmallQuantitySurchargeNotReached").FormatWith( diff --git a/src/Plugins/SmartStore.ShippingByWeight/Controllers/ShippingByWeightController.cs b/src/Plugins/SmartStore.ShippingByWeight/Controllers/ShippingByWeightController.cs index 3bb5247ae0..d68d99d695 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Controllers/ShippingByWeightController.cs +++ b/src/Plugins/SmartStore.ShippingByWeight/Controllers/ShippingByWeightController.cs @@ -8,13 +8,11 @@ using SmartStore.ShippingByWeight.Models; using SmartStore.ShippingByWeight.Services; using SmartStore.Web.Framework.Controllers; -using SmartStore.Web.Framework.Filters; using SmartStore.Web.Framework.Security; using Telerik.Web.Mvc; namespace SmartStore.ShippingByWeight.Controllers { - [AdminAuthorize] public class ShippingByWeightController : PluginControllerBase { @@ -131,15 +129,9 @@ public ActionResult RateDelete(int id, GridCommand command) return RatesList(command); } - [HttpPost, ActionName("Configure")] - [FormValueRequired("addshippingbyweightrecord")] + [HttpPost] public ActionResult AddShippingByWeightRecord(ShippingByWeightListModel model) { - if (!ModelState.IsValid) - { - return Configure(); - } - var sbw = new ShippingByWeightRecord() { StoreId = model.AddStoreId, @@ -155,24 +147,26 @@ public ActionResult AddShippingByWeightRecord(ShippingByWeightListModel model) SmallQuantitySurcharge = model.SmallQuantitySurcharge, SmallQuantityThreshold = model.SmallQuantityThreshold, }; + _shippingByWeightService.InsertShippingByWeightRecord(sbw); - return Configure(); - } + NotifySuccess(T("Plugins.Shipping.ByWeight.AddNewRecord.Success")); - [HttpPost, ActionName("Configure")] - [FormValueRequired("savegeneralsettings")] - public ActionResult SaveGeneralSettings(ShippingByWeightListModel model) + return Json(new { Result = true }); + } + + [HttpPost] + public ActionResult Configure(ShippingByWeightListModel model) { - //save settings _shippingByWeightSettings.LimitMethodsToCreated = model.LimitMethodsToCreated; _shippingByWeightSettings.CalculatePerWeightUnit = model.CalculatePerWeightUnit; _shippingByWeightSettings.IncludeWeightOfFreeShippingProducts = model.IncludeWeightOfFreeShippingProducts; _services.Settings.SaveSetting(_shippingByWeightSettings); - - return Configure(); - } - + + NotifySuccess(T("Admin.Configuration.Updated")); + + return Configure(); + } } } diff --git a/src/Plugins/SmartStore.ShippingByWeight/Localization/resources.de-de.xml b/src/Plugins/SmartStore.ShippingByWeight/Localization/resources.de-de.xml index 3e61637b0f..a81cc70122 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.ShippingByWeight/Localization/resources.de-de.xml @@ -87,14 +87,22 @@ Warenwert, bis zu dem ein Mindermengenzuschlag erhoben werden soll. Der Zuschlag wird ignoriert, wenn keine Versandkosten anfallen. Verwenden Sie "0", wenn kein Zuschlag erhoben werden soll. - - Es wird ein Mindermengenzuschlag von {0} erhoben, da Ihr Bestellwert unter {1} liegt.]]> + + + Sie werden mit einem Zuschlag von {0} belastet, weil die Summe Ihrer Bestellung nicht {1} erreicht hat.]]> + Neue Versandbedingung hinzufügen - + + Die neue Versandbedingung wurde erfolgreich zugefügt. + + Optionen + + Versandbedingungen + \ No newline at end of file diff --git a/src/Plugins/SmartStore.ShippingByWeight/Localization/resources.en-us.xml b/src/Plugins/SmartStore.ShippingByWeight/Localization/resources.en-us.xml index e4b05ee854..a3097a2517 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.ShippingByWeight/Localization/resources.en-us.xml @@ -87,16 +87,21 @@ Subtotal up to which a "small quantity surcharge" should be added. The surcharge will be ignored if no shipping fee is applied. Use "0" if no fee will be charged. - + You're charged with a surcharge of {0} because the total of your order hasn't reached {1}.]]> - - + Add new record - - Settings + + The new record was successfully added. + + + Options + + + Shipping records \ No newline at end of file diff --git a/src/Plugins/SmartStore.ShippingByWeight/Services/ShippingByWeightService.cs b/src/Plugins/SmartStore.ShippingByWeight/Services/ShippingByWeightService.cs index f60212f80c..62545474f0 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Services/ShippingByWeightService.cs +++ b/src/Plugins/SmartStore.ShippingByWeight/Services/ShippingByWeightService.cs @@ -210,7 +210,7 @@ private bool ZipMatches(string zip, string pattern) { foreach (var entry in patterns) { - var wildcard = new Wildcard(entry); + var wildcard = new Wildcard(entry, true); if (wildcard.IsMatch(zip)) return true; } diff --git a/src/Plugins/SmartStore.ShippingByWeight/SmartStore.ShippingByWeight.csproj b/src/Plugins/SmartStore.ShippingByWeight/SmartStore.ShippingByWeight.csproj index 9f7312abac..eca63edcc2 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/SmartStore.ShippingByWeight.csproj +++ b/src/Plugins/SmartStore.ShippingByWeight/SmartStore.ShippingByWeight.csproj @@ -20,7 +20,7 @@ Properties SmartStore.ShippingByWeight SmartStore.ShippingByWeight - v4.5.2 + v4.6.1 512 @@ -89,12 +89,12 @@ ..\..\packages\Autofac.Mvc5.4.0.2\lib\net45\Autofac.Integration.Mvc.dll
                        - False - ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll + ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll + True - False - ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll + ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll + True ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll diff --git a/src/Plugins/SmartStore.ShippingByWeight/Views/ShippingByWeight/Configure.cshtml b/src/Plugins/SmartStore.ShippingByWeight/Views/ShippingByWeight/Configure.cshtml index df4014b1ad..ccb6c906ee 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Views/ShippingByWeight/Configure.cshtml +++ b/src/Plugins/SmartStore.ShippingByWeight/Views/ShippingByWeight/Configure.cshtml @@ -6,211 +6,25 @@ Layout = ""; } - - - - -
                        - @(Html.Telerik().Grid() - .Name("Grid") - .DataKeys(keys => keys.Add(x => x.Id).RouteKey("Id")) - .Columns(columns => - { - columns.Bound(x => x.StoreName).ReadOnly(); - columns.Bound(x => x.CountryName).ReadOnly(); - columns.Bound(x => x.Zip); - columns.Bound(x => x.ShippingMethodName).ReadOnly(); - columns.Bound(x => x.From) - .Format("{0:0.00}"); - columns.Bound(x => x.To) - .Format("{0:0.00}"); - columns.Bound(x => x.UsePercentage) - .Centered() - .Template(item => @Html.SymbolForBool(item.UsePercentage)) - .ClientTemplate(@Html.SymbolForBool("UsePercentage")); - columns.Bound(x => x.ShippingChargePercentage) - .Format("{0:0.00}"); - columns.Bound(x => x.ShippingChargeAmount) - .Format("{0:0.00}"); - columns.Bound(x => x.SmallQuantitySurcharge) - .Format("{0:0.00}"); - columns.Bound(x => x.SmallQuantityThreshold) - .Format("{0:0.00}"); - columns.Command(commands => - { - commands.Edit(); - commands.Delete(); - }).Width(180); - - }) - .Editable(x => - { - x.Mode(GridEditMode.InLine); - }) - .DataBinding(dataBinding => - { - dataBinding.Ajax() - .Select("RatesList", "ShippingByWeight", new RouteValueDictionary() { { "area", "SmartStore.ShippingByWeight" } }) - .Update("RateUpdate", "ShippingByWeight", new RouteValueDictionary() { { "area", "SmartStore.ShippingByWeight" } }) - .Delete("RateDelete", "ShippingByWeight", new RouteValueDictionary() { { "area", "SmartStore.ShippingByWeight" } }); - }) - .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) - .PreserveGridState() - .EnableCustomBinding(true)) -
                        -

                        +
                        + +
                        @using (Html.BeginForm()) -{ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                        -
                        -
                        @T("Plugins.Shipping.ByWeight.AddNewRecordTitle")
                        -
                        -
                        - @Html.SmartLabelFor(model => model.AddStoreId) - - @Html.DropDownListFor(model => model.AddStoreId, Model.AvailableStores) - @Html.ValidationMessageFor(model => model.AddStoreId) -
                        - @Html.SmartLabelFor(model => model.AddCountryId) - - @Html.DropDownListFor(model => model.AddCountryId, Model.AvailableCountries) - @Html.ValidationMessageFor(model => model.AddCountryId) -
                        - @Html.SmartLabelFor(model => model.AddZip) - - @Html.EditorFor(model => model.AddZip) - @Html.ValidationMessageFor(model => model.AddZip) -
                        - @Html.SmartLabelFor(model => model.AddShippingMethodId) - - @Html.DropDownListFor(model => model.AddShippingMethodId, Model.AvailableShippingMethods) - @Html.ValidationMessageFor(model => model.AddShippingMethodId) -
                        - @Html.SmartLabelFor(model => model.AddFrom) - - @Html.EditorFor(model => model.AddFrom) [@Model.BaseWeightIn] - @Html.ValidationMessageFor(model => model.AddFrom) -
                        - @Html.SmartLabelFor(model => model.AddTo) - - @Html.EditorFor(model => model.AddTo) [@Model.BaseWeightIn] - @Html.ValidationMessageFor(model => model.AddTo) -
                        - @Html.SmartLabelFor(model => model.AddUsePercentage) - - @Html.EditorFor(model => model.AddUsePercentage) - @Html.ValidationMessageFor(model => model.AddUsePercentage) -
                        - @Html.SmartLabelFor(model => model.AddShippingChargePercentage) - - @Html.EditorFor(model => model.AddShippingChargePercentage) - @Html.ValidationMessageFor(model => model.AddShippingChargePercentage) -
                        - @Html.SmartLabelFor(model => model.AddShippingChargeAmount) - - @Html.EditorFor(model => model.AddShippingChargeAmount) [@Model.PrimaryStoreCurrencyCode] - @Html.ValidationMessageFor(model => model.AddShippingChargeAmount) -
                        - @Html.SmartLabelFor(model => model.SmallQuantitySurcharge) - - @Html.EditorFor(model => model.SmallQuantitySurcharge) [@Model.PrimaryStoreCurrencyCode] - @Html.ValidationMessageFor(model => model.SmallQuantitySurcharge) -
                        - @Html.SmartLabelFor(model => model.SmallQuantityThreshold) - - @Html.EditorFor(model => model.SmallQuantityThreshold) - @Html.ValidationMessageFor(model => model.SmallQuantityThreshold) -
                        -   - - -
                        +{ + @Html.SmartStore().TabStrip().Name("shipping-by-weight-edit").Style(TabsStyle.Material).Items(x => + { + x.Add().Text(T("Plugins.Shipping.ByWeight.TabTitleGrid").Text).Content(TabGrid()).Selected(true); + x.Add().Text(T("Plugins.Shipping.ByWeight.TabTitleSettings").Text).Content(TabSettings()); + }) +} - - - - +@helper TabSettings() +{ +
                        -
                        -
                        @T("Plugins.Shipping.ByWeight.SettingsTitle")
                        -
                        -
                        - - - -
                        @Html.SmartLabelFor(model => model.CalculatePerWeightUnit) @@ -238,15 +52,243 @@ @Html.ValidationMessageFor(model => model.IncludeWeightOfFreeShippingProducts)
                        -   - - -
                        +} + +@helper TabGrid() +{ +
                        + @(Html.Telerik().Grid() + .Name("shipping-byweight-grid") + .DataKeys(keys => keys.Add(x => x.Id).RouteKey("Id")) + .Columns(columns => + { + columns.Bound(x => x.StoreName) + .Width("8%") + .ReadOnly(); + columns.Bound(x => x.CountryName) + .Width("8%") + .ReadOnly(); + columns.Bound(x => x.Zip) + .Width("8%"); + columns.Bound(x => x.ShippingMethodName) + .Width("8%") + .ReadOnly(); + columns.Bound(x => x.From) + .Width("8%") + .Format("{0:0.00}"); + columns.Bound(x => x.To) + .Width("8%") + .Format("{0:0.00}"); + columns.Bound(x => x.UsePercentage) + .Width("8%") + .Centered() + .Template(item => @Html.SymbolForBool(item.UsePercentage)) + .ClientTemplate(@Html.SymbolForBool("UsePercentage")); + columns.Bound(x => x.ShippingChargePercentage) + .Width("8%") + .Format("{0:0.00}"); + columns.Bound(x => x.ShippingChargeAmount) + .Width("8%") + .Format("{0:0.00}"); + columns.Bound(x => x.SmallQuantitySurcharge) + .Width("8%") + .Format("{0:0.00}"); + columns.Bound(x => x.SmallQuantityThreshold) + .Width("8%") + .Format("{0:0.00}"); + columns.Command(commands => + { + commands.Edit(); + commands.Delete(); + }).Width("12%").HtmlAttributes(new { align = "right" }); + + }) + .DataBinding(dataBinding => + { + dataBinding.Ajax() + .Select("RatesList", "ShippingByWeight", new RouteValueDictionary() { { "area", "SmartStore.ShippingByWeight" } }) + .Update("RateUpdate", "ShippingByWeight", new RouteValueDictionary() { { "area", "SmartStore.ShippingByWeight" } }) + .Delete("RateDelete", "ShippingByWeight", new RouteValueDictionary() { { "area", "SmartStore.ShippingByWeight" } }); + }) + .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) + .Editable(x => x.Mode(GridEditMode.InLine)) + .ToolBar(commands => commands.Template(ShippingByWeightGridCommands)) + .PreserveGridState() + .EnableCustomBinding(true)) +
                        +} + +@helper ShippingByWeightGridCommands(Grid grid) +{ + + + @T("Plugins.Shipping.ByTotal.AddNewRecordTitle") + +} + + + +@{Html.SmartStore().Window() + .Name("addrecord-window") + .Size(WindowSize.Large) + .Title(T("Plugins.Shipping.ByWeight.AddNewRecordTitle")) + .Content( + @ +
                        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                        + @Html.SmartLabelFor(model => model.AddStoreId) + + @Html.DropDownListFor(model => model.AddStoreId, Model.AvailableStores) + @Html.ValidationMessageFor(model => model.AddStoreId) +
                        + @Html.SmartLabelFor(model => model.AddCountryId) + + @Html.DropDownListFor(model => model.AddCountryId, Model.AvailableCountries) + @Html.ValidationMessageFor(model => model.AddCountryId) +
                        + @Html.SmartLabelFor(model => model.AddZip) + + @Html.EditorFor(model => model.AddZip) + @Html.ValidationMessageFor(model => model.AddZip) +
                        + @Html.SmartLabelFor(model => model.AddShippingMethodId) + + @Html.DropDownListFor(model => model.AddShippingMethodId, Model.AvailableShippingMethods) + @Html.ValidationMessageFor(model => model.AddShippingMethodId) +
                        + @Html.SmartLabelFor(model => model.AddFrom) + + @Html.EditorFor(model => model.AddFrom, new { postfix = Model.BaseWeightIn }) + @Html.ValidationMessageFor(model => model.AddFrom) +
                        + @Html.SmartLabelFor(model => model.AddTo) + + @Html.EditorFor(model => model.AddTo, new { postfix = Model.BaseWeightIn }) + @Html.ValidationMessageFor(model => model.AddTo) +
                        + @Html.SmartLabelFor(model => model.AddUsePercentage) + + @Html.EditorFor(model => model.AddUsePercentage) + @Html.ValidationMessageFor(model => model.AddUsePercentage) +
                        + @Html.SmartLabelFor(model => model.AddShippingChargePercentage) + + @Html.EditorFor(model => model.AddShippingChargePercentage) + @Html.ValidationMessageFor(model => model.AddShippingChargePercentage) +
                        + @Html.SmartLabelFor(model => model.AddShippingChargeAmount) + + @Html.EditorFor(model => model.AddShippingChargeAmount, new { postfix = Model.PrimaryStoreCurrencyCode }) + @Html.ValidationMessageFor(model => model.AddShippingChargeAmount) +
                        + @Html.SmartLabelFor(model => model.SmallQuantitySurcharge) + + @Html.EditorFor(model => model.SmallQuantitySurcharge, new { postfix = Model.PrimaryStoreCurrencyCode }) + @Html.ValidationMessageFor(model => model.SmallQuantitySurcharge) +
                        + @Html.SmartLabelFor(model => model.SmallQuantityThreshold) + + @Html.EditorFor(model => model.SmallQuantityThreshold) + @Html.ValidationMessageFor(model => model.SmallQuantityThreshold) +
                        +
                        +
                        ) + .FooterContent(@ + + + ) + .Render(); } \ No newline at end of file diff --git a/src/Plugins/SmartStore.ShippingByWeight/packages.config b/src/Plugins/SmartStore.ShippingByWeight/packages.config index 9fc69c653d..42df152c4d 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/packages.config +++ b/src/Plugins/SmartStore.ShippingByWeight/packages.config @@ -2,7 +2,7 @@ - + diff --git a/src/Plugins/SmartStore.ShippingByWeight/web.config b/src/Plugins/SmartStore.ShippingByWeight/web.config index 275d035d5a..5e59e8cb6a 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/web.config +++ b/src/Plugins/SmartStore.ShippingByWeight/web.config @@ -4,8 +4,16 @@ + - + @@ -119,13 +127,10 @@ + + + + - - - - - - - - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.Tax/Controllers/TaxByRegionController.cs b/src/Plugins/SmartStore.Tax/Controllers/TaxByRegionController.cs index ca43d31f14..138b5a5fd2 100644 --- a/src/Plugins/SmartStore.Tax/Controllers/TaxByRegionController.cs +++ b/src/Plugins/SmartStore.Tax/Controllers/TaxByRegionController.cs @@ -15,7 +15,6 @@ namespace SmartStore.Tax.Controllers { - [AdminAuthorize] public class TaxByRegionController : PluginControllerBase { @@ -58,7 +57,7 @@ public ActionResult Configure() model.TaxRates = _taxRateService.GetAllTaxRates() .Select(x => { - var m = new ByRegionTaxRateModel() + var m = new ByRegionTaxRateModel { Id = x.Id, TaxCategoryId = x.TaxCategoryId, @@ -138,9 +137,14 @@ public ActionResult RateDelete(int id, GridCommand command) return RatesList(command); } - [HttpPost, ActionName("Configure")] - [FormValueRequired("addtaxrate")] - public ActionResult AddTaxRate(ByRegionTaxRateListModel model) + [HttpPost] + public ActionResult Configure(ByRegionTaxRateListModel model) + { + return Configure(); + } + + [HttpPost] + public ActionResult AddTaxByRegionRecord(ByRegionTaxRateListModel model) { if (!ModelState.IsValid) { @@ -155,10 +159,12 @@ public ActionResult AddTaxRate(ByRegionTaxRateListModel model) Zip = model.AddZip, Percentage = model.AddPercentage }; + _taxRateService.InsertTaxRate(taxRate); - return Configure(); - } + NotifySuccess(T("Plugins.Tax.CountryStateZip.AddNewRecord.Success")); + return Json(new { Result = true }); + } } } diff --git a/src/Plugins/SmartStore.Tax/Controllers/TaxFixedRateController.cs b/src/Plugins/SmartStore.Tax/Controllers/TaxFixedRateController.cs index 1340565989..3d7f1eb680 100644 --- a/src/Plugins/SmartStore.Tax/Controllers/TaxFixedRateController.cs +++ b/src/Plugins/SmartStore.Tax/Controllers/TaxFixedRateController.cs @@ -13,7 +13,6 @@ namespace SmartStore.Tax.Controllers { - [AdminAuthorize] public class TaxFixedRateController : PluginControllerBase { diff --git a/src/Plugins/SmartStore.Tax/Localization/resources.de-de.xml b/src/Plugins/SmartStore.Tax/Localization/resources.de-de.xml index 1229c24787..f56155a6cc 100644 --- a/src/Plugins/SmartStore.Tax/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.Tax/Localization/resources.de-de.xml @@ -56,6 +56,9 @@ Neuen Steuersatz hinzufügen + + Der neue Steuersatz wurde erfolgreich zugefügt. + diff --git a/src/Plugins/SmartStore.Tax/Localization/resources.en-us.xml b/src/Plugins/SmartStore.Tax/Localization/resources.en-us.xml index 86bde57c04..455af19357 100644 --- a/src/Plugins/SmartStore.Tax/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.Tax/Localization/resources.en-us.xml @@ -57,6 +57,9 @@ Adding a new tax rate + + The new tax rate was successfully added. + diff --git a/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj b/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj index 4d05252a93..2eb774d77e 100644 --- a/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj +++ b/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj @@ -20,7 +20,7 @@ Properties SmartStore.Tax SmartStore.Tax - v4.5.2 + v4.6.1 512 @@ -86,12 +86,12 @@ ..\..\packages\Autofac.4.5.0\lib\net45\Autofac.dll
                        - False - ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll + ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll + True - False - ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll + ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll + True ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll diff --git a/src/Plugins/SmartStore.Tax/Views/TaxByRegion/Configure.cshtml b/src/Plugins/SmartStore.Tax/Views/TaxByRegion/Configure.cshtml index 1bce0727b9..d4bc7bbf34 100644 --- a/src/Plugins/SmartStore.Tax/Views/TaxByRegion/Configure.cshtml +++ b/src/Plugins/SmartStore.Tax/Views/TaxByRegion/Configure.cshtml @@ -1,144 +1,161 @@ -@using SmartStore.Web.Framework; +@model ByRegionTaxRateListModel +@using SmartStore.Web.Framework; @using Telerik.Web.Mvc.UI; @using SmartStore.Tax.Models; - -@model ByRegionTaxRateListModel @{ Layout = ""; } - - - - -
                        - @(Html.Telerik().Grid(Model.TaxRates) - .Name("Grid") - .DataKeys(keys => keys.Add(x => x.Id).RouteKey("Id")) - .Columns(columns => - { - columns.Bound(x => x.CountryName) - .ReadOnly(); - columns.Bound(x => x.StateProvinceName) - .ReadOnly(); - columns.Bound(x => x.Zip) - .Width(140); - columns.Bound(x => x.TaxCategoryName) - .ReadOnly(); - columns.Bound(x => x.Percentage) - .Width(140) - .Format("{0:0.00}"); - columns.Command(commands => - { - commands.Edit(); - commands.Delete(); - }).Width(180); - - }) - .Editable(x => - { - x.Mode(GridEditMode.InLine); - }) - .DataBinding(dataBinding => - { - dataBinding.Ajax() - .Select("RatesList", "TaxByRegion", new RouteValueDictionary() { { "area", "SmartStore.Tax" } }) - .Update("RateUpdate", "TaxByRegion", new RouteValueDictionary() { { "area", "SmartStore.Tax" } }) - .Delete("RateDelete", "TaxByRegion", new RouteValueDictionary() { { "area", "SmartStore.Tax" } }); - }) - .EnableCustomBinding(true)) -
                        +
                        + @(Html.Telerik().Grid(Model.TaxRates) + .Name("tax-byregion-grid") + .DataKeys(keys => keys.Add(x => x.Id).RouteKey("Id")) + .Columns(columns => + { + columns.Bound(x => x.CountryName) + .Width("30%") + .ReadOnly(); + columns.Bound(x => x.StateProvinceName) + .Width("30%") + .ReadOnly(); + columns.Bound(x => x.Zip) + .Width("10%"); + columns.Bound(x => x.TaxCategoryName) + .Width("10%") + .ReadOnly(); + columns.Bound(x => x.Percentage) + .Width("8%") + .Format("{0:0.00}"); + columns.Command(commands => + { + commands.Edit(); + commands.Delete(); + }).Width("12%") + .HtmlAttributes(new { align = "right", @class = "omega" }); + }) + .DataBinding(dataBinding => + { + dataBinding.Ajax() + .Select("RatesList", "TaxByRegion", new RouteValueDictionary() { { "area", "SmartStore.Tax" } }) + .Update("RateUpdate", "TaxByRegion", new RouteValueDictionary() { { "area", "SmartStore.Tax" } }) + .Delete("RateDelete", "TaxByRegion", new RouteValueDictionary() { { "area", "SmartStore.Tax" } }); + }) + .Editable(x => { x.Mode(GridEditMode.InLine); }) + .ToolBar(commands => commands.Template(TaxByRegionGridCommands)) + .EnableCustomBinding(true)) +
                        -

                        +@helper TaxByRegionGridCommands(Grid grid) +{ + + + @T("Plugins.Tax.CountryStateZip.AddRecord.Hint") + +} -@using (Html.BeginForm()) -{ - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                        -
                        -
                        @T("Plugins.Tax.CountryStateZip.AddRecord.Hint")
                        -
                        -
                        - @Html.SmartLabelFor(model => model.AddCountryId) - - @Html.DropDownListFor(model => model.AddCountryId, Model.AvailableCountries) - @Html.ValidationMessageFor(model => model.AddCountryId) -
                        - @Html.SmartLabelFor(model => model.AddStateProvinceId) - - @Html.DropDownListFor(model => model.AddStateProvinceId, Model.AvailableStates) - @Html.ValidationMessageFor(model => model.AddStateProvinceId) -
                        - @Html.SmartLabelFor(model => model.AddZip) - - @Html.EditorFor(model => model.AddZip) - @Html.ValidationMessageFor(model => model.AddZip) -
                        - @Html.SmartLabelFor(model => model.AddTaxCategoryId) - - @Html.DropDownListFor(model => model.AddTaxCategoryId, Model.AvailableTaxCategories) - @Html.ValidationMessageFor(model => model.AddTaxCategoryId) -
                        - @Html.SmartLabelFor(model => model.AddPercentage) - - @Html.EditorFor(model => model.AddPercentage) - @Html.ValidationMessageFor(model => model.AddPercentage) -
                        -   - - -
                        -} \ No newline at end of file + +@{Html.SmartStore().Window() + .Name("addrecord-window") + .Size(WindowSize.Large) + .Title(T("Plugins.Tax.CountryStateZip.AddRecord.Hint")) + .Content( + @ +
                        + + + + + + + + + + + + + + + + + + + + + +
                        + @Html.SmartLabelFor(model => model.AddCountryId) + + @Html.DropDownList("CountryId", Model.AvailableCountries, + new + { + @class = "form-control country-input country-selector", + data_region_control_selector = "#AddStateProvinceId", + data_states_ajax_url = Url.Action("GetStatesByCountryId", "Country", new RouteValueDictionary() { { "area", "Admin" } }), + data_addAsterisk = "true", + data_addEmptyStateIfRequired = "false" + }) + @Html.ValidationMessageFor(model => model.AddCountryId) +
                        + @Html.SmartLabelFor(model => model.AddStateProvinceId) + + @Html.DropDownListFor(model => model.AddStateProvinceId, Model.AvailableStates) + @Html.ValidationMessageFor(model => model.AddStateProvinceId) +
                        + @Html.SmartLabelFor(model => model.AddZip) + + @Html.EditorFor(model => model.AddZip) + @Html.ValidationMessageFor(model => model.AddZip) +
                        + @Html.SmartLabelFor(model => model.AddTaxCategoryId) + + @Html.DropDownListFor(model => model.AddTaxCategoryId, Model.AvailableTaxCategories) + @Html.ValidationMessageFor(model => model.AddTaxCategoryId) +
                        + @Html.SmartLabelFor(model => model.AddPercentage) + + @Html.EditorFor(model => model.AddPercentage) + @Html.ValidationMessageFor(model => model.AddPercentage) +
                        +
                        +
                        ) + .FooterContent(@ + + + ) + .Render(); +} diff --git a/src/Plugins/SmartStore.Tax/Views/TaxFixedRate/Configure.cshtml b/src/Plugins/SmartStore.Tax/Views/TaxFixedRate/Configure.cshtml index ae249fc208..88fa0fd009 100644 --- a/src/Plugins/SmartStore.Tax/Views/TaxFixedRate/Configure.cshtml +++ b/src/Plugins/SmartStore.Tax/Views/TaxFixedRate/Configure.cshtml @@ -1,41 +1,33 @@ -@{ - Layout = ""; -} - -@model Telerik.Web.Mvc.GridModel +@model Telerik.Web.Mvc.GridModel @using Telerik.Web.Mvc.UI; @using SmartStore.Tax.Models; +@{ + Layout = ""; +} - - - - -
                        - @(Html.Telerik().Grid(Model.Data) - .Name("Grid") - .DataKeys(keys => keys.Add(x => x.TaxCategoryId).RouteKey("TaxCategoryId")) - .Columns(columns => - { - columns.Bound(x => x.TaxCategoryName) - .ReadOnly(); - columns.Bound(x => x.Rate) - .Width(400) - .Format("{0:0.00}"); - columns.Command(commands => - { - commands.Edit(); - }).Width(120); - - }) - .Editable(x => - { - x.Mode(GridEditMode.InLine); - }) - .DataBinding(dataBinding => - { - dataBinding.Ajax() - .Select("Configure", "TaxFixedRate", new RouteValueDictionary() { { "area", "SmartStore.Tax" } }) - .Update("TaxRateUpdate", "TaxFixedRate", new RouteValueDictionary() { { "area", "SmartStore.Tax" } }); - }) - .EnableCustomBinding(true)) -
                        +
                        + @(Html.Telerik().Grid(Model.Data) + .Name("Grid") + .DataKeys(keys => keys.Add(x => x.TaxCategoryId).RouteKey("TaxCategoryId")) + .Columns(columns => + { + columns.Bound(x => x.TaxCategoryName) + .Width("70%") + .ReadOnly(); + columns.Bound(x => x.Rate) + .Width("18%") + .Format("{0:0.00}"); + columns.Command(commands => + { + commands.Edit(); + }).Width("12%").HtmlAttributes(new { align = "right", @class = "omega" }); + }) + .DataBinding(dataBinding => + { + dataBinding.Ajax() + .Select("Configure", "TaxFixedRate", new RouteValueDictionary() { { "area", "SmartStore.Tax" } }) + .Update("TaxRateUpdate", "TaxFixedRate", new RouteValueDictionary() { { "area", "SmartStore.Tax" } }); + }) + .Editable(x => { x.Mode(GridEditMode.InLine); }) + .EnableCustomBinding(true)) +
                        diff --git a/src/Plugins/SmartStore.Tax/packages.config b/src/Plugins/SmartStore.Tax/packages.config index ef566823a5..5ea7037968 100644 --- a/src/Plugins/SmartStore.Tax/packages.config +++ b/src/Plugins/SmartStore.Tax/packages.config @@ -1,7 +1,7 @@  - + diff --git a/src/Plugins/SmartStore.Tax/web.config b/src/Plugins/SmartStore.Tax/web.config index bf9944de8a..bb33bef252 100644 --- a/src/Plugins/SmartStore.Tax/web.config +++ b/src/Plugins/SmartStore.Tax/web.config @@ -5,8 +5,16 @@ + - + @@ -120,13 +128,10 @@ + + + + - - - - - - - - + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Controllers/ContollerExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Controllers/ContollerExtensions.cs index 1517db8245..bec016843f 100644 --- a/src/Presentation/SmartStore.Web.Framework/Controllers/ContollerExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Controllers/ContollerExtensions.cs @@ -183,7 +183,7 @@ private static void ThrowIfViewNotFound(ViewEngineResult viewResult, string view /// Store service /// Work context /// Store ID; 0 if we are in a shared mode - public static int GetActiveStoreScopeConfiguration(this Controller controller, IStoreService storeService, IWorkContext workContext) + public static int GetActiveStoreScopeConfiguration(this IController controller, IStoreService storeService, IWorkContext workContext) { //ensure that we have 2 (or more) stores if (storeService.GetAllStores().Count < 2) diff --git a/src/Presentation/SmartStore.Web.Framework/Controllers/SmartController.cs b/src/Presentation/SmartStore.Web.Framework/Controllers/SmartController.cs index e604894982..28a487f255 100644 --- a/src/Presentation/SmartStore.Web.Framework/Controllers/SmartController.cs +++ b/src/Presentation/SmartStore.Web.Framework/Controllers/SmartController.cs @@ -176,6 +176,28 @@ protected virtual ActionResult RedirectToReferrer(string referrer, Func + /// Redirects to the configuration page of a plugin or a provider. + /// + /// The system name of the plugin or the provider. + /// true plugin configuration, false provider configuration. + protected virtual ActionResult RedirectToConfiguration(string systemName, bool isPlugin = true) + { + Guard.NotEmpty(systemName, nameof(systemName)); + + var actionName = isPlugin ? "ConfigurePlugin" : "ConfigureProvider"; + + if (ControllerContext.IsChildAction) + { + var url = Url.Action(actionName, "Plugin", new { systemName, area = "Admin" }); + Response.Redirect(url); + + return new EmptyResult(); + } + + return RedirectToAction(actionName, "Plugin", new { systemName, area = "Admin" }); + } + /// /// On exception /// diff --git a/src/Presentation/SmartStore.Web.Framework/DependencyRegistrar.cs b/src/Presentation/SmartStore.Web.Framework/DependencyRegistrar.cs index 34ebc6cb28..b656f49340 100644 --- a/src/Presentation/SmartStore.Web.Framework/DependencyRegistrar.cs +++ b/src/Presentation/SmartStore.Web.Framework/DependencyRegistrar.cs @@ -77,8 +77,11 @@ using SmartStore.Services.Tax; using SmartStore.Services.Themes; using SmartStore.Services.Topics; +using SmartStore.Templating; +using SmartStore.Templating.Liquid; using SmartStore.Utilities; using SmartStore.Web.Framework.Bundling; +using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Framework.Filters; using SmartStore.Web.Framework.Localization; using SmartStore.Web.Framework.Plugins; @@ -217,7 +220,7 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); - builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); @@ -393,7 +396,7 @@ protected override void Load(ContainerBuilder builder) m.For(em => em.HookedType, hookedType); m.For(em => em.ImplType, hook); m.For(em => em.IsLoadHook, typeof(IDbLoadHook).IsAssignableFrom(hook)); - m.For(em => em.Important, hookedType.HasAttribute(false)); + m.For(em => em.Important, hook.HasAttribute(false)); }); } @@ -404,7 +407,19 @@ protected override void Load(ContainerBuilder builder) } else { - builder.Register(c => new SmartObjectContext(DataSettings.Current.DataConnectionString)) + builder.Register(c => + { + try + { + return new SmartObjectContext(DataSettings.Current.DataConnectionString); + } + catch + { + //return new SmartObjectContext(); + return null; + } + + }) .PropertiesAutowired(PropertyWiringOptions.AllowCircularDependencies) .InstancePerRequest(); } @@ -440,8 +455,7 @@ protected override void AttachToComponentRegistration(IComponentRegistry compone { if (DataSettings.DatabaseIsInstalled()) { - var prop = e.Component.Metadata.Get("Property.DbQuerySettings") as FastProperty; - if (prop != null) + if (e.Component.Metadata.Get("Property.DbQuerySettings") is FastProperty prop) { var querySettings = e.Context.Resolve(); prop.SetValue(e.Instance, querySettings); @@ -475,7 +489,10 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); + builder.Register(c => c.Resolve().Get).InstancePerRequest(); + builder.Register(c => c.Resolve().GetEx).InstancePerRequest(); + builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); } @@ -492,13 +509,21 @@ protected override void AttachToComponentRegistration(IComponentRegistry compone { if (DataSettings.DatabaseIsInstalled() && e.Context.Resolve().IsFullyInitialized) { - var prop = e.Component.Metadata.Get("Property.T") as FastProperty; - if (prop != null) + if (e.Component.Metadata.Get("Property.T") is FastProperty prop) { try { - Localizer localizer = e.Context.Resolve().Get; - prop.SetValue(e.Instance, localizer); + var iText = e.Context.Resolve(); + if (prop.Property.PropertyType == typeof(Localizer)) + { + Localizer localizer = e.Context.Resolve().Get; + prop.SetValue(e.Instance, localizer); + } + else + { + LocalizerEx localizerEx = e.Context.Resolve().GetEx; + prop.SetValue(e.Instance, localizerEx); + } } catch { } } @@ -508,7 +533,7 @@ protected override void AttachToComponentRegistration(IComponentRegistry compone private static PropertyInfo FindUserProperty(Type type) { - return type.GetProperty("T", typeof(Localizer)); + return type.GetProperty("T", typeof(Localizer)) ?? type.GetProperty("T", typeof(LocalizerEx)); } } @@ -611,15 +636,19 @@ public class MessagingModule : Module { protected override void Load(ContainerBuilder builder) { + // Templating + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + + builder.RegisterType().As().InstancePerRequest(); + builder.RegisterType().As().InstancePerRequest(); + builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); - builder.RegisterType().As().InstancePerRequest(); - builder.RegisterType().As().InstancePerRequest(); - builder.RegisterType().As().InstancePerRequest(); - builder.RegisterType().As().SingleInstance(); // xxx (http) + builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().SingleInstance(); } } @@ -661,7 +690,7 @@ protected override void Load(ContainerBuilder builder) if (DataSettings.DatabaseIsInstalled()) { pageHelperRegistration.PropertiesAutowired(PropertyWiringOptions.None); - builder.RegisterType().AsActionFilterFor(-100); + builder.RegisterType().AsActionFilterFor(-100); } } @@ -723,9 +752,8 @@ protected override void AttachToComponentRegistration(IComponentRegistry compone var baseType = typeof(WebApiEntityController<,>); var type = registration.Activator.LimitType; - Type implementingType; - if (!type.IsSubClass(baseType, out implementingType)) + if (!type.IsSubClass(baseType, out var implementingType)) return; var repoProperty = FindRepositoryProperty(type, implementingType.GetGenericArguments()[0]); @@ -1148,8 +1176,7 @@ public IEnumerable RegistrationsFor( Service service, Func> registrations) { - var ts = service as TypedService; - if (ts != null && typeof(ISettings).IsAssignableFrom(ts.ServiceType)) + if (service is TypedService ts && typeof(ISettings).IsAssignableFrom(ts.ServiceType)) { var buildMethod = BuildMethod.MakeGenericMethod(ts.ServiceType); yield return (IComponentRegistration)buildMethod.Invoke(null, null); @@ -1162,11 +1189,9 @@ public IEnumerable RegistrationsFor( .ForDelegate((c, p) => { int currentStoreId = 0; - IStoreContext storeContext; - try { - if (c.TryResolve(out storeContext)) + if (c.TryResolve(out IStoreContext storeContext)) { currentStoreId = storeContext.CurrentStore.Id; //uncomment the code below if you want load settings per store only when you have two stores installed. @@ -1243,8 +1268,7 @@ static IComponentRegistration CreateMetaRegistration(Service providedService, var workValues = scope.Resolve>(); - T value; - if (!workValues.Values.TryGetValue(w, out value)) + if (!workValues.Values.TryGetValue(w, out T value)) { value = (T)workValues.ComponentContext.ResolveComponent(valueRegistration, p); workValues.Values[w] = value; diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs index 792019ac68..eaf24b73ff 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlExtensions.cs @@ -19,7 +19,6 @@ using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Framework.Settings; using SmartStore.Web.Framework.UI; -using SmartStore.Web.Framework.Theming; namespace SmartStore.Web.Framework { @@ -54,9 +53,7 @@ public static MvcHtmlString Hint(this HtmlHelper helper, string value) return MvcHtmlString.Create(a.ToString()); } - public static HelperResult LocalizedEditor(this HtmlHelper helper, string name, - Func localizedTemplate, - Func standardTemplate) + public static HelperResult LocalizedEditor(this HtmlHelper helper, string name, Func localizedTemplate, Func standardTemplate) where T : ILocalizedModel where TLocalizedModelLocal : ILocalizedModelLocal { @@ -64,8 +61,8 @@ public static HelperResult LocalizedEditor(this HtmlHel { if (helper.ViewData.Model.Locales.Count > 1) { - writer.Write("
                        "); - var tabStrip = helper.SmartStore().TabStrip().Name(name).SmartTabSelection(false).Style(TabsStyle.Pills).Items(x => + writer.Write("
                        "); + var tabStrip = helper.SmartStore().TabStrip().Name(name).SmartTabSelection(false).Style(TabsStyle.Tabs).AddCssClass("nav-locales").Items(x => { if (standardTemplate != null) { @@ -78,7 +75,7 @@ public static HelperResult LocalizedEditor(this HtmlHel var language = EngineContext.Current.Resolve().GetLanguageById(locale.LanguageId); x.Add().Text(language.Name) - .Content(localizedTemplate(i).ToHtmlString()) + .Content(localizedTemplate(i)) .ImageUrl("~/Content/images/flags/" + language.FlagImageFileName) .Selected(i == 0 && standardTemplate == null); } @@ -98,20 +95,21 @@ public static MvcHtmlString DeleteConfirmation(this HtmlHelper helper, str return DeleteConfirmation(helper, "", buttonsSelector); } - // Adds an action name parameter for using other delete action names - public static MvcHtmlString DeleteConfirmation(this HtmlHelper helper, string actionName, string buttonsSelector = null) where T : EntityModelBase + /// + /// Adds an action name parameter for using other delete action names + /// + /// + /// + /// + /// + /// + public static MvcHtmlString DeleteConfirmation(this HtmlHelper helper, string actionName, string buttonsSelector = null) where T : EntityModelBase { if (String.IsNullOrEmpty(actionName)) actionName = "Delete"; var modalId = MvcHtmlString.Create(helper.ViewData.ModelMetadata.ModelType.Name.ToLower() + "-delete-confirmation").ToHtmlString(); - string script = ""; - if (!string.IsNullOrEmpty(buttonsSelector)) - { - script = "\n"; - } - var deleteConfirmationModel = new DeleteConfirmationModel { Id = helper.ViewData.Model.Id, @@ -122,21 +120,29 @@ public static MvcHtmlString DeleteConfirmation(this HtmlHelper helper, str EntityType = buttonsSelector.Replace("-delete", "") }; - var window = helper.SmartStore().Window().Name(modalId) - .Title(EngineContext.Current.Resolve().GetResource("Admin.Common.AreYouSure")) - .Modal(true) - .Visible(false) - .Content(helper.Partial("Delete", deleteConfirmationModel).ToHtmlString()) - .ToHtmlString(); + var script = string.Empty; + if (buttonsSelector.HasValue()) + { + script = "\n"; + } + + helper.SmartStore().Window().Name(modalId) + .Title(EngineContext.Current.Resolve().GetResource("Admin.Common.AreYouSure")) + .Content(helper.Partial("Delete", deleteConfirmationModel).ToHtmlString()) + .Show(false) + .Render(); - return MvcHtmlString.Create(script + window); + return new MvcHtmlString(script); } public static MvcHtmlString SmartLabel(this HtmlHelper helper, string expression, string labelText, string hint = null, object htmlAttributes = null) { var result = new StringBuilder(); - var label = helper.Label(expression, labelText, htmlAttributes); + var labelAttrs = new RouteValueDictionary(htmlAttributes); + //labelAttrs.AppendCssClass("col-form-label"); + + var label = helper.Label(expression, labelText, labelAttrs); result.Append("
                        "); { @@ -158,8 +164,7 @@ public static MvcHtmlString SmartLabelFor( object htmlAttributes = null) { var metadata = ModelMetadata.FromLambdaExpression(expression, helper.ViewData); - object resourceDisplayName = null; - metadata.AdditionalValues.TryGetValue("SmartResourceDisplayName", out resourceDisplayName); + metadata.AdditionalValues.TryGetValue("SmartResourceDisplayName", out object resourceDisplayName); return SmartLabelFor(helper, expression, resourceDisplayName as SmartResourceDisplayName, metadata, displayHint, htmlAttributes); } @@ -214,7 +219,10 @@ private static MvcHtmlString SmartLabelFor( labelText = metadata.PropertyName.SplitPascalCase(); } - var label = helper.LabelFor(expression, labelText, htmlAttributes); + var labelAttrs = new RouteValueDictionary(htmlAttributes); + //labelAttrs.AppendCssClass("col-form-label"); + + var label = helper.LabelFor(expression, labelText, labelAttrs); if (displayHint) { @@ -496,7 +504,8 @@ public static MvcHtmlString ControlGroupFor( string inputHtml = ""; var htmlAttributes = new RouteValueDictionary(); - var dataTypeName = ModelMetadata.FromLambdaExpression(expression, html.ViewData).DataTypeName.EmptyNull(); + var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData); + var dataTypeName = metadata.DataTypeName.EmptyNull(); var groupClass = "form-group row"; var labelClass = "col-{0}-3 col-form-label".FormatInvariant(breakpoint.NullEmpty() ?? "md"); var controlsClass = "col-{0}-9".FormatInvariant(breakpoint.NullEmpty() ?? "md"); @@ -531,9 +540,11 @@ public static MvcHtmlString ControlGroupFor( switch (editorType) { case InputEditorType.Checkbox: - inputHtml = string.Format("
                        ", - html.EditorFor(expression).ToString(), - ModelMetadata.FromLambdaExpression(expression, html.ViewData).DisplayName); // TBD: ist das OK so? + CommonHelper.TryConvert(metadata.Model, out var isChecked); + inputHtml = string.Format("
                        {0}
                        ", + html.CheckBox(ExpressionHelper.GetExpressionText(expression), isChecked, new { @class = "form-check-input" }).ToString(), + html.IdFor(expression), + metadata.DisplayName); break; case InputEditorType.Password: inputHtml = html.PasswordFor(expression, htmlAttributes).ToString(); @@ -569,17 +580,17 @@ public static MvcHtmlString ColorBox(this HtmlHelper html, string name, string c defaultColor = defaultColor.EmptyNull(); var isDefault = color.IsCaseInsensitiveEqual(defaultColor); - sb.Append("
                        "); + sb.Append("
                        ".FormatInvariant(defaultColor)); - sb.AppendFormat(html.TextBox(name, isDefault ? "" : color, new { @class = "form-control", placeholder = defaultColor }).ToHtmlString()); - sb.AppendFormat("", defaultColor.HasValue() ? "background-color: " + defaultColor : ""); + sb.AppendFormat(html.TextBox(name, isDefault ? "" : color, new { @class = "form-control colorval", placeholder = defaultColor }).ToHtmlString()); + sb.AppendFormat("
                         
                        ", defaultColor.HasValue() ? "background-color: " + defaultColor : ""); sb.Append("
                        "); - var bootstrapJsRoot = "~/Content/bootstrap/js/"; - html.AppendScriptParts(false, - bootstrapJsRoot + "custom/bootstrap-colorpicker.js", - bootstrapJsRoot + "custom/bootstrap-colorpicker-globalinit.js"); + var scriptRoot = "~/Content/vendors/colorpicker/js/"; + html.AppendScriptParts(true, + scriptRoot + "bootstrap-colorpicker.js", + scriptRoot + "bootstrap-colorpicker-globalinit.js"); return MvcHtmlString.Create(sb.ToString()); } @@ -610,16 +621,99 @@ public static MvcHtmlString TableFormattedVariantAttributes(this HtmlHelper help return MvcHtmlString.Create(sb.ToString()); } - public static MvcHtmlString SettingOverrideCheckbox( + //public static MvcHtmlString SettingEditorFor( + // this HtmlHelper helper, + // Expression> expression, + // string parentSelector = null, + // object additionalViewData = null) + //{ + // var editor = helper.EditorFor(expression, additionalViewData); + + // var data = helper.ViewData[StoreDependingSettingHelper.ViewDataKey] as StoreDependingSettingData; + // if (data == null || data.ActiveStoreScopeConfiguration <= 0) + // return editor; // CONTROL + + // var sb = new StringBuilder("
                        "); + // sb.Append("
                        "); + // sb.Append(helper.SettingOverrideCheckboxInternal(expression, data, parentSelector)); // CHECK + // sb.Append("
                        "); + // sb.Append("
                        "); + // sb.Append(editor); // CONTROL + // sb.Append("
                        "); + + // return MvcHtmlString.Create(sb.ToString()); + //} + + public static MvcHtmlString SettingEditorFor( + this HtmlHelper helper, + Expression> expression, + string parentSelector = null, + object additionalViewData = null) + { + return SettingEditorFor( + helper, + expression, + helper.EditorFor(expression, additionalViewData), + parentSelector); + } + + public static MvcHtmlString SettingEditorFor( this HtmlHelper helper, Expression> expression, + Func editor, string parentSelector = null) { - var data = helper.ViewData[StoreDependingSettingHelper.ViewDataKey] as StoreDependingSettingData; + return SettingEditorFor( + helper, + expression, + new MvcHtmlString(editor(helper.ViewData.Model).ToHtmlString()), + parentSelector); + } + + public static MvcHtmlString EnumSettingEditorFor( + this HtmlHelper helper, + Expression> expression, + string parentSelector = null, + object htmlAttributes = null, + string optionLabel = null) where TValue : struct + { + return SettingEditorFor( + helper, + expression, + helper.DropDownListForEnum(expression, htmlAttributes, optionLabel), + parentSelector); + } + + public static MvcHtmlString SettingEditorFor( + this HtmlHelper helper, + Expression> expression, + MvcHtmlString editor, + string parentSelector = null) + { + Guard.NotNull(expression, nameof(expression)); + Guard.NotNull(editor, nameof(editor)); + var data = helper.ViewData[StoreDependingSettingHelper.ViewDataKey] as StoreDependingSettingData; if (data == null || data.ActiveStoreScopeConfiguration <= 0) - return MvcHtmlString.Empty; + return editor; // CONTROL + + var sb = new StringBuilder("
                        "); + sb.Append("
                        "); + sb.Append(helper.SettingOverrideCheckboxInternal(expression, data, parentSelector)); // CHECK + sb.Append("
                        "); + sb.Append("
                        "); + sb.Append(editor.ToHtmlString()); // CONTROL + sb.Append("
                        "); + + return MvcHtmlString.Create(sb.ToString()); + } + private static MvcHtmlString SettingOverrideCheckboxInternal( + this HtmlHelper helper, + Expression> expression, + StoreDependingSettingData data, + string parentSelector = null) + { var fieldPrefix = helper.ViewData.TemplateInfo.HtmlFieldPrefix; var settingKey = ExpressionHelper.GetExpressionText(expression); var localizeService = EngineContext.Current.Resolve(); @@ -633,47 +727,22 @@ public static MvcHtmlString SettingOverrideCheckbox( var fieldId = settingKey + (settingKey.EndsWith("_OverrideForStore") ? "" : "_OverrideForStore"); var sb = new StringBuilder(); - sb.Append(""); // Controls are not floating, so line-break prevents different distances between them. sb.Append("\r\n"); return MvcHtmlString.Create(sb.ToString()); } - public static MvcHtmlString SettingEditorFor( - this HtmlHelper helper, - Expression> expression, - string parentSelector = null, - object additionalViewData = null) - { - var checkbox = helper.SettingOverrideCheckbox(expression, parentSelector); - var editor = helper.EditorFor(expression, additionalViewData); - - return MvcHtmlString.Create(checkbox.ToString() + editor.ToString()); - } - - public static MvcHtmlString EnumSettingEditorFor( - this HtmlHelper helper, - Expression> expression, - string parentSelector = null, - object htmlAttributes = null, - string optionLabel = null) where TValue : struct - { - var checkbox = helper.SettingOverrideCheckbox(expression, parentSelector); - var editor = helper.DropDownListForEnum(expression, htmlAttributes, optionLabel); - - return MvcHtmlString.Create(checkbox.ToString() + editor.ToString()); - } - public static MvcHtmlString CollapsedText(this HtmlHelper helper, string text) { if (text.IsEmpty()) @@ -800,11 +869,11 @@ public static MvcHtmlString IconForFileExtension(this HtmlHelper helper, string { if (ext.IsEmpty()) { - result = "{0}".FormatInvariant("".NaIfEmpty()); + result = "{0}".FormatInvariant("".NaIfEmpty()); } else { - result = result + "{0}".FormatInvariant(label); + result = result + "{0}".FormatInvariant(label); } } diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlSelectListExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlSelectListExtensions.cs index 33455155a2..930805f5d4 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlSelectListExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlSelectListExtensions.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using System.Web.Mvc; using SmartStore.Core; +using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Stores; using SmartStore.Core.Infrastructure; using SmartStore.Services.Localization; @@ -32,21 +33,43 @@ public static SelectList ToSelectList(this TEnum enumObj, bool markCurren } /// - /// Get a list of all stores + /// Get a select list of all stores /// - public static IList ToSelectListItems(this IEnumerable stores) + public static IList ToSelectListItems(this IEnumerable stores, params int[] selectedStoreIds) { - var lst = new List(); + var list = new List(); foreach (var store in stores) { - lst.Add(new SelectListItem + list.Add(new SelectListItem { Text = store.Name, - Value = store.Id.ToString() + Value = store.Id.ToString(), + Selected = selectedStoreIds != null && selectedStoreIds.Contains(store.Id) }); } - return lst; + + return list; + } + + /// + /// Get a select list of all customer roles + /// + public static IList ToSelectListItems(this IEnumerable roles, params int[] selectedCustomerRoleIds) + { + var list = new List(); + + foreach (var role in roles) + { + list.Add(new SelectListItem + { + Text = role.Name, + Value = role.Id.ToString(), + Selected = selectedCustomerRoleIds != null && selectedCustomerRoleIds.Contains(role.Id) + }); + } + + return list; } public static void SelectValue(this List lst, string value, string defaultValue = null) diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlZoneExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlZoneExtensions.cs index 125a010a73..b74766934a 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlZoneExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/HtmlZoneExtensions.cs @@ -104,7 +104,7 @@ public static IDisposable BeginZoneContent(this HtmlHelper helper, ZoneInjectMode injectMode = ZoneInjectMode.Append, string key = null) { - if (key.HasValue() && DocumentZone.HasUniqueKey(key)) + if ((key.HasValue() && DocumentZone.HasUniqueKey(key)) || helper.ViewContext.HttpContext.Request.IsAjaxRequest()) { return ActionDisposable.Empty; } diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/HttpExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/HttpExtensions.cs index 00ddeaceac..65b716686a 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/HttpExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/HttpExtensions.cs @@ -133,8 +133,7 @@ public static RouteData GetRouteData(this HttpContextBase httpContext) { Guard.NotNull(httpContext, nameof(httpContext)); - var handler = httpContext.Handler as MvcHandler; - if (handler != null && handler.RequestContext != null) + if (httpContext.Handler is MvcHandler handler && handler.RequestContext != null) { return handler.RequestContext.RouteData; } @@ -193,9 +192,11 @@ internal static void SetUserThemeChoiceInCookie(this HttpContextBase context, st if (value.HasValue() && cookie == null) { - cookie = new HttpCookie("sm.UserThemeChoice"); - cookie.HttpOnly = true; - cookie.Expires = DateTime.UtcNow.AddYears(1); + cookie = new HttpCookie("sm.UserThemeChoice") + { + HttpOnly = true, + Expires = DateTime.UtcNow.AddYears(1) + }; } if (value.HasValue()) @@ -224,8 +225,7 @@ internal static HttpCookie GetPreviewModeCookie(this HttpContextBase context, bo if (cookie == null && createIfMissing) { - cookie = new HttpCookie("sm.PreviewModeOverrides"); - cookie.HttpOnly = true; + cookie = new HttpCookie("sm.PreviewModeOverrides") { HttpOnly = true }; context.Request.Cookies.Set(cookie); } diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/UrlHelperExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/UrlHelperExtensions.cs index cc1426f8fc..8e5d409fc6 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/UrlHelperExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/UrlHelperExtensions.cs @@ -1,4 +1,7 @@ using System.Web.Mvc; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.Infrastructure; +using SmartStore.Services.Media; namespace SmartStore.Web.Framework { @@ -8,6 +11,7 @@ public static string LogOn(this UrlHelper urlHelper, string returnUrl) { if (!string.IsNullOrEmpty(returnUrl)) return urlHelper.Action("Login", "Customer", new { ReturnUrl = returnUrl, area = "" }); + return urlHelper.Action("Login", "Customer", new { area = "" }); } @@ -15,6 +19,7 @@ public static string LogOff(this UrlHelper urlHelper, string returnUrl) { if (!string.IsNullOrEmpty(returnUrl)) return urlHelper.Action("Logout", "Customer", new { ReturnUrl = returnUrl, area = "" }); + return urlHelper.Action("Logout", "Customer", new { area = "" }); } @@ -28,5 +33,17 @@ public static string Referrer(this UrlHelper urlHelper, string fallbackUrl = "") return fallbackUrl; } - } + + public static string Picture(this UrlHelper urlHelper, int? pictureId, int targetSize = 0, FallbackPictureType fallbackType = FallbackPictureType.Entity, string host = null) + { + var pictureService = EngineContext.Current.Resolve(); + return pictureService.GetUrl(pictureId.GetValueOrDefault(), targetSize, fallbackType, host); + } + + public static string Picture(this UrlHelper urlHelper, Picture picture, int targetSize = 0, FallbackPictureType fallbackType = FallbackPictureType.Entity, string host = null) + { + var pictureService = EngineContext.Current.Resolve(); + return pictureService.GetUrl(picture, targetSize, fallbackType, host); + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/HandleExceptionFilter.cs b/src/Presentation/SmartStore.Web.Framework/Filters/HandleExceptionFilter.cs index c4ee621424..081c754738 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/HandleExceptionFilter.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/HandleExceptionFilter.cs @@ -110,7 +110,7 @@ public virtual void OnActionExecuted(ActionExecutedContext filterContext) // handle not found (404) from within the MVC pipeline (only called when HttpNotFoundResult is returned from actions) var requestContext = filterContext.RequestContext; var url = requestContext.HttpContext.Request.RawUrl; - + filterContext.Result = new ViewResult { ViewName = "NotFound", diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/NotifyAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/NotifyAttribute.cs index dfde57dfd9..ec44b5bb1d 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/NotifyAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/NotifyAttribute.cs @@ -39,15 +39,15 @@ private void Persist(IDictionary bag, IEnumerable s if (!source.Any()) return; - var existing = (bag[NotificationsKey] ?? new List()) as List; + var existing = (bag[NotificationsKey] ?? new HashSet()) as HashSet; source.Each(x => { - if (x.Message.Text.HasValue() && !existing.Contains(x)) + if (x.Message.Text.HasValue()) existing.Add(x); }); - bag[NotificationsKey] = existing; + bag[NotificationsKey] = TrimSet(existing); } private void HandleAjaxRequest(NotifyEntry entry, HttpResponseBase response) @@ -58,6 +58,16 @@ private void HandleAjaxRequest(NotifyEntry entry, HttpResponseBase response) response.AddHeader("X-Message-Type", entry.Type.ToString().ToLower()); response.AddHeader("X-Message", entry.Message.Text); } + + private HashSet TrimSet(HashSet entries) + { + if (entries.Count <= 20) + { + return entries; + } + + return new HashSet(entries.Skip(entries.Count - 20)); + } } } diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/PublicStoreAllowNavigationAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/PublicStoreAllowNavigationAttribute.cs index 9a03d5f25c..a9790ec54a 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/PublicStoreAllowNavigationAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/PublicStoreAllowNavigationAttribute.cs @@ -17,7 +17,8 @@ public class PublicStoreAllowNavigationAttribute : FilterAttribute, IActionFilte new Tuple("SmartStore.Web.Controllers.CustomerController", "PasswordRecovery"), new Tuple("SmartStore.Web.Controllers.CustomerController", "PasswordRecoveryConfirm"), new Tuple("SmartStore.Web.Controllers.CustomerController", "AccountActivation"), - new Tuple("SmartStore.Web.Controllers.CustomerController", "CheckUsernameAvailability") + new Tuple("SmartStore.Web.Controllers.CustomerController", "CheckUsernameAvailability"), + new Tuple("SmartStore.Web.Controllers.CatalogController", "OffCanvasMenu") }; public Lazy PermissionService { get; set; } diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs index 50cc9f7139..987f3eb683 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs @@ -2,6 +2,7 @@ using System.Web.Mvc; using SmartStore.Core; using SmartStore.Core.Data; +using SmartStore.Core.Domain.Customers; using SmartStore.Services.Customers; namespace SmartStore.Web.Framework.Filters @@ -11,7 +12,8 @@ public class StoreIpAddressAttribute : FilterAttribute, IActionFilter public Lazy WebHelper { get; set; } public Lazy WorkContext { get; set; } public Lazy CustomerService { get; set; } - + public Lazy CustomerSettings { get; set; } + public virtual void OnActionExecuting(ActionExecutingContext filterContext) { if (!DataSettings.DatabaseIsInstalled()) @@ -20,24 +22,24 @@ public virtual void OnActionExecuting(ActionExecutingContext filterContext) if (filterContext == null || filterContext.HttpContext == null || filterContext.HttpContext.Request == null) return; - //don't apply filter to child methods + // Don't apply filter to child methods. if (filterContext.IsChildAction) return; - //only GET requests + // Only GET requests. if (!String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) return; - var webHelper = this.WebHelper.Value; + if (!CustomerSettings.Value.StoreLastIpAddress) + return; - //update IP address - string currentIpAddress = webHelper.GetCurrentIpAddress(); + // Update IP address. + var webHelper = WebHelper.Value; + var currentIpAddress = webHelper.GetCurrentIpAddress(); if (!String.IsNullOrEmpty(currentIpAddress)) { - var workContext = WorkContext.Value; - var customer = workContext.CurrentCustomer; - - if (!currentIpAddress.Equals(customer.LastIpAddress, StringComparison.InvariantCultureIgnoreCase)) + var customer = WorkContext.Value.CurrentCustomer; + if (customer != null && !currentIpAddress.Equals(customer.LastIpAddress, StringComparison.InvariantCultureIgnoreCase)) { var customerService = CustomerService.Value; customer.LastIpAddress = currentIpAddress; diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/StoreLastVisitedPageAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/StoreLastVisitedPageAttribute.cs index 5a192e1eca..fbbc9b9dca 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/StoreLastVisitedPageAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/StoreLastVisitedPageAttribute.cs @@ -9,7 +9,6 @@ namespace SmartStore.Web.Framework.Filters { public class StoreLastVisitedPageAttribute : FilterAttribute, IActionFilter { - public Lazy WebHelper { get; set; } public Lazy WorkContext { get; set; } public Lazy CustomerSettings { get; set; } diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/UnitOfWorkAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/UnitOfWorkAttribute.cs index 9a060a6294..2721a44f65 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/UnitOfWorkAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/UnitOfWorkAttribute.cs @@ -7,7 +7,6 @@ namespace SmartStore.Web.Framework.Filters [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] public class UnitOfWorkAttribute : ActionFilterAttribute { - public UnitOfWorkAttribute() : this(null, int.MaxValue) { diff --git a/src/Presentation/SmartStore.Web.Framework/FrameworkCacheConsumer.cs b/src/Presentation/SmartStore.Web.Framework/FrameworkCacheConsumer.cs index 05829750e6..cf42696204 100644 --- a/src/Presentation/SmartStore.Web.Framework/FrameworkCacheConsumer.cs +++ b/src/Presentation/SmartStore.Web.Framework/FrameworkCacheConsumer.cs @@ -1,6 +1,10 @@ using System; +using System.Collections.Generic; using System.Web; +using SmartStore.Core; using SmartStore.Core.Caching; +using SmartStore.Core.Data; +using SmartStore.Core.Data.Hooks; using SmartStore.Core.Domain.Configuration; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Themes; @@ -10,17 +14,8 @@ namespace SmartStore.Web.Framework { - public class FrameworkCacheConsumer : - IConsumer>, - IConsumer>, - IConsumer>, - IConsumer>, - IConsumer>, - IConsumer>, - IConsumer>, - IConsumer + public partial class FrameworkCacheConsumer : DbSaveHook, IConsumer { - /// /// Key for ThemeVariables caching /// @@ -29,8 +24,7 @@ public class FrameworkCacheConsumer : /// {1} : store identifier /// public const string THEMEVARS_KEY = "pres:themevars-{0}-{1}"; - public const string THEMEVARS_THEME_KEY = "pres:themevars-{0}"; - + public const string THEMEVARS_THEME_KEY = "pres:themevars-{0}"; /// /// Key for tax display type caching @@ -40,61 +34,77 @@ public class FrameworkCacheConsumer : /// {1} : store identifier /// public const string CUSTOMERROLES_TAX_DISPLAY_TYPES_KEY = "fw:customerroles:taxdisplaytypes-{0}-{1}"; - public const string CUSTOMERROLES_TAX_DISPLAY_TYPES_PATTERN_KEY = "fw:customerroles:taxdisplaytypes"; + public const string CUSTOMERROLES_TAX_DISPLAY_TYPES_PATTERN_KEY = "fw:customerroles:taxdisplaytypes*"; private readonly ICacheManager _cacheManager; private readonly IAssetCache _assetCache; + // Item1 = ThemeName, Item2 = StoreId + private HashSet> _themeScopes; + public FrameworkCacheConsumer(ICacheManager cacheManager, IAssetCache assetCache) { _cacheManager = cacheManager; _assetCache = assetCache; } - public void HandleEvent(EntityInserted eventMessage) - { - HttpRuntime.Cache.Remove(BuildThemeVarsCacheKey(eventMessage.Entity)); - } - - public void HandleEvent(EntityUpdated eventMessage) - { - HttpRuntime.Cache.Remove(BuildThemeVarsCacheKey(eventMessage.Entity)); - } - - public void HandleEvent(EntityDeleted eventMessage) - { - HttpRuntime.Cache.Remove(BuildThemeVarsCacheKey(eventMessage.Entity)); - } - public void HandleEvent(ThemeTouchedEvent eventMessage) { var cacheKey = BuildThemeVarsCacheKey(eventMessage.ThemeName, 0); HttpRuntime.Cache.RemoveByPattern(cacheKey); } + public override void OnAfterSave(IHookedEntity entry) + { + if (entry.Entity is ThemeVariable) + { + var themeVar = entry.Entity as ThemeVariable; + AddEvictableThemeScope(themeVar.Theme, themeVar.StoreId); + } + else if (entry.Entity is CustomerRole) + { + _cacheManager.RemoveByPattern(CUSTOMERROLES_TAX_DISPLAY_TYPES_PATTERN_KEY); + } + else if (entry.Entity is Setting && entry.InitialState == EntityState.Modified) + { + var setting = entry.Entity as Setting; + if (setting.Name.IsCaseInsensitiveEqual("TaxSettings.TaxDisplayType")) + { + _cacheManager.RemoveByPattern(CUSTOMERROLES_TAX_DISPLAY_TYPES_PATTERN_KEY); // depends on TaxSettings.TaxDisplayType + } + } + else + { + throw new NotImplementedException(); + } + } - public void HandleEvent(EntityDeleted eventMessage) - { - _cacheManager.RemoveByPattern(CUSTOMERROLES_TAX_DISPLAY_TYPES_PATTERN_KEY); - } + public override void OnAfterSaveCompleted() + { + FlushThemeVarsCacheEviction(); + } - public void HandleEvent(EntityUpdated eventMessage) - { - _cacheManager.RemoveByPattern(CUSTOMERROLES_TAX_DISPLAY_TYPES_PATTERN_KEY); - } + #region Helpers - public void HandleEvent(EntityInserted eventMessage) - { - _cacheManager.RemoveByPattern(CUSTOMERROLES_TAX_DISPLAY_TYPES_PATTERN_KEY); - } + private void AddEvictableThemeScope(string themeName, int storeId) + { + if (_themeScopes == null) + _themeScopes = new HashSet>(); + _themeScopes.Add(new Tuple(themeName, storeId)); + } - public void HandleEvent(EntityUpdated eventMessage) - { - // clear models which depend on settings - _cacheManager.RemoveByPattern(CUSTOMERROLES_TAX_DISPLAY_TYPES_PATTERN_KEY); // depends on TaxSettings.TaxDisplayType - } + private void FlushThemeVarsCacheEviction() + { + if (_themeScopes == null || _themeScopes.Count == 0) + return; - #region Helpers + foreach (var scope in _themeScopes) + { + HttpRuntime.Cache.Remove(BuildThemeVarsCacheKey(scope.Item1 /* ThemeName */, scope.Item2 /* StoreId */)); + } + + _themeScopes.Clear(); + } private static string BuildThemeVarsCacheKey(ThemeVariable entity) { @@ -112,7 +122,5 @@ internal static string BuildThemeVarsCacheKey(string themeName, int storeId) } #endregion - } - } diff --git a/src/Presentation/SmartStore.Web.Framework/Localization/ILocalizedModel.cs b/src/Presentation/SmartStore.Web.Framework/Localization/ILocalizedModel.cs index abde521279..d91f9f2ad8 100644 --- a/src/Presentation/SmartStore.Web.Framework/Localization/ILocalizedModel.cs +++ b/src/Presentation/SmartStore.Web.Framework/Localization/ILocalizedModel.cs @@ -6,8 +6,13 @@ public interface ILocalizedModel { } - public interface ILocalizedModel : ILocalizedModel + public interface ILocalizedModel : ILocalizedModel where T : ILocalizedModelLocal { - IList Locales { get; set; } + IList Locales { get; set; } } + + public interface ILocalizedModelLocal + { + int LanguageId { get; set; } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Localization/ILocalizedModelLocal.cs b/src/Presentation/SmartStore.Web.Framework/Localization/ILocalizedModelLocal.cs deleted file mode 100644 index 35c850c80d..0000000000 --- a/src/Presentation/SmartStore.Web.Framework/Localization/ILocalizedModelLocal.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace SmartStore.Web.Framework.Localization -{ - public interface ILocalizedModelLocal - { - int LanguageId { get; set; } - } -} diff --git a/src/Presentation/SmartStore.Web.Framework/Localization/IText.cs b/src/Presentation/SmartStore.Web.Framework/Localization/IText.cs index aa60884b0a..ec648c3377 100644 --- a/src/Presentation/SmartStore.Web.Framework/Localization/IText.cs +++ b/src/Presentation/SmartStore.Web.Framework/Localization/IText.cs @@ -6,5 +6,6 @@ namespace SmartStore.Web.Framework.Localization public interface IText { LocalizedString Get(string key, params object[] args); + LocalizedString GetEx(string key, int languageId, params object[] args); } } diff --git a/src/Presentation/SmartStore.Web.Framework/Localization/LocalizedRoute.cs b/src/Presentation/SmartStore.Web.Framework/Localization/LocalizedRoute.cs index ff4948b41d..6ab96f7117 100644 --- a/src/Presentation/SmartStore.Web.Framework/Localization/LocalizedRoute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Localization/LocalizedRoute.cs @@ -129,7 +129,7 @@ public override VirtualPathData GetVirtualPath(RequestContext requestContext, Ro { if (!requestContext.RouteData.Values.ContainsKey("StripInvalidSeoCode")) { - data.VirtualPath = String.Concat(cultureCode, "/", data.VirtualPath); + data.VirtualPath = String.Concat(cultureCode, "/", data.VirtualPath).TrimEnd('/'); } } } diff --git a/src/Presentation/SmartStore.Web.Framework/Localization/SetWorkingCultureAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Localization/SetWorkingCultureAttribute.cs index c460525c0d..61903e4d72 100644 --- a/src/Presentation/SmartStore.Web.Framework/Localization/SetWorkingCultureAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Localization/SetWorkingCultureAttribute.cs @@ -1,30 +1,31 @@ using System; +using System.Collections.Generic; using System.Globalization; +using System.Text; using System.Threading; using System.Web; using System.Web.Mvc; +using Newtonsoft.Json; using SmartStore.Core; using SmartStore.Core.Data; +using SmartStore.Web.Framework.UI; namespace SmartStore.Web.Framework.Localization { /// - /// Attribute which determines and sets the working culture + /// Attribute which determines and sets working culture and globalization scripts /// - public class SetWorkingCultureAttribute : FilterAttribute, IAuthorizationFilter + public class SetWorkingCultureAttribute : FilterAttribute, IAuthorizationFilter, IActionFilter { public Lazy WorkContext { get; set; } + public Lazy AssetBuilder { get; set; } - public void OnAuthorization(AuthorizationContext filterContext) + public void OnAuthorization(AuthorizationContext filterContext) { - if (filterContext == null || filterContext.HttpContext == null) - return; - - HttpRequestBase request = filterContext.HttpContext.Request; + var request = filterContext?.HttpContext?.Request; if (request == null) return; - // don't apply filter to child methods if (filterContext.IsChildAction) return; @@ -32,21 +33,135 @@ public void OnAuthorization(AuthorizationContext filterContext) return; var workContext = WorkContext.Value; - var workingLanguage = workContext.WorkingLanguage; - - CultureInfo culture; - if (workContext.CurrentCustomer != null && workingLanguage != null) - { - culture = new CultureInfo(workingLanguage.LanguageCulture); - } - else - { - culture = new CultureInfo("en-US"); - } + + CultureInfo culture = workContext.CurrentCustomer != null && workContext.WorkingLanguage != null + ? new CultureInfo(workContext.WorkingLanguage.LanguageCulture) + : new CultureInfo("en-US"); Thread.CurrentThread.CurrentCulture = culture; Thread.CurrentThread.CurrentUICulture = culture; } - } + public void OnActionExecuting(ActionExecutingContext filterContext) + { + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { + if (filterContext.IsChildAction) + return; + + if (!DataSettings.DatabaseIsInstalled()) + return; + + if (!(filterContext.Result is ViewResult)) + return; + + var culture = Thread.CurrentThread.CurrentUICulture; + if (culture.Name == "en-US") + return; + + var builder = AssetBuilder.Value; + var json = CreateCultureJson(culture); + + var sb = new StringBuilder(); + sb.Append(""); + + var script = sb.ToString(); + + builder.AppendCustomHeadParts(script); + } + + private string CreateCultureJson(CultureInfo ci) + { + var nf = ci.NumberFormat; + var df = ci.DateTimeFormat; + + var dict = new Dictionary + { + { "name", ci.Name }, + { "englishName", ci.EnglishName }, + { "nativeName", ci.NativeName }, + { "isRTL", ci.TextInfo.IsRightToLeft }, + { "language", ci.TwoLetterISOLanguageName }, + { "numberFormat", new Dictionary + { + { ",", nf.NumberGroupSeparator }, + { ".", nf.NumberDecimalSeparator }, + { "pattern", new[] { nf.NumberNegativePattern } }, + { "decimals", nf.NumberDecimalDigits }, + { "groupSizes", nf.NumberGroupSizes }, + { "+", nf.PositiveSign }, + { "-", nf.NegativeSign }, + { "NaN", nf.NaNSymbol }, + { "negativeInfinity", nf.NegativeInfinitySymbol }, + { "positiveInfinity", nf.PositiveInfinitySymbol }, + { "percent", new Dictionary + { + { ",", nf.PercentGroupSeparator }, + { ".", nf.PercentDecimalSeparator }, + { "pattern", new[] { nf.PercentNegativePattern, nf.PercentPositivePattern } }, + { "decimals", nf.PercentDecimalDigits }, + { "groupSizes", nf.PercentGroupSizes }, + { "symbol", nf.PercentSymbol } + } }, + { "currency", new Dictionary + { + { ",", nf.CurrencyGroupSeparator }, + { ".", nf.CurrencyDecimalSeparator }, + { "pattern", new[] { nf.CurrencyNegativePattern, nf.CurrencyPositivePattern } }, + { "decimals", nf.CurrencyDecimalDigits }, + { "groupSizes", nf.CurrencyGroupSizes }, + { "symbol", nf.CurrencySymbol } + } }, + } }, + { "dateTimeFormat", new Dictionary + { + { "calendarName", df.NativeCalendarName }, + { "/", df.DateSeparator }, + { ":", df.TimeSeparator }, + { "firstDay", (int)df.FirstDayOfWeek }, + { "twoDigitYearMax", ci.Calendar.TwoDigitYearMax }, + { "AM", df.AMDesignator.IsEmpty() ? null : new[] { df.AMDesignator, df.AMDesignator.ToLower(), df.AMDesignator.ToUpper() } }, + { "PM", df.PMDesignator.IsEmpty() ? null : new[] { df.PMDesignator, df.PMDesignator.ToLower(), df.PMDesignator.ToUpper() } }, + { "days", new Dictionary + { + { "names", df.DayNames }, + { "namesAbbr", df.AbbreviatedDayNames }, + { "namesShort", df.ShortestDayNames }, + } }, + { "months", new Dictionary + { + { "names", df.MonthNames }, + { "namesAbbr", df.AbbreviatedMonthNames }, + } }, + { "patterns", new Dictionary + { + { "d", df.ShortDatePattern }, + { "D", df.LongDatePattern }, + { "t", df.ShortTimePattern }, + { "T", df.LongTimePattern }, + { "g", df.ShortDatePattern + " " + df.ShortTimePattern }, + { "G", df.ShortDatePattern + " " + df.LongTimePattern }, + { "f", df.FullDateTimePattern }, // TODO: (mc) find it actually + { "F", df.FullDateTimePattern }, + { "M", df.MonthDayPattern }, + { "Y", df.YearMonthPattern }, + { "u", df.UniversalSortableDateTimePattern }, + } } + } } + }; + + var json = JsonConvert.SerializeObject(dict, new JsonSerializerSettings + { + Formatting = Formatting.None + }); + + return json; + } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/Localization/Text.cs b/src/Presentation/SmartStore.Web.Framework/Localization/Text.cs index f96004f6f3..e3ace14f69 100644 --- a/src/Presentation/SmartStore.Web.Framework/Localization/Text.cs +++ b/src/Presentation/SmartStore.Web.Framework/Localization/Text.cs @@ -1,4 +1,5 @@ -using SmartStore.Core.Localization; +using System; +using SmartStore.Core.Localization; using SmartStore.Services.Localization; namespace SmartStore.Web.Framework.Localization @@ -13,10 +14,15 @@ public Text(ILocalizationService localizationService) } public LocalizedString Get(string key, params object[] args) + { + return GetEx(key, 0, args); + } + + public LocalizedString GetEx(string key, int languageId, params object[] args) { try { - var value = _localizationService.GetResource(key); + var value = _localizationService.GetResource(key, languageId); if (string.IsNullOrEmpty(value)) { diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/IAclSelector.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/IAclSelector.cs new file mode 100644 index 0000000000..5dfddbe817 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/IAclSelector.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Web.Mvc; + +namespace SmartStore.Web.Framework.Modelling +{ + public interface IAclSelector + { + [SmartResourceDisplayName("Admin.Common.Acl.SubjectTo")] + bool SubjectToAcl { get; } + + [SmartResourceDisplayName("Admin.Common.Acl.AvailableFor")] + IEnumerable AvailableCustomerRoles { get; } + + int[] SelectedCustomerRoleIds { get; } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/IStoreSelector.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/IStoreSelector.cs new file mode 100644 index 0000000000..cb124b943e --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/IStoreSelector.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Web.Mvc; + +namespace SmartStore.Web.Framework.Modelling +{ + public interface IStoreSelector + { + [SmartResourceDisplayName("Admin.Common.Store.LimitedTo")] + bool LimitedToStores { get; } + + [SmartResourceDisplayName("Admin.Common.Store.AvailableFor")] + IEnumerable AvailableStores { get; } + + int[] SelectedStoreIds { get; } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfPartialViewContent.cs b/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfPartialViewContent.cs index fb266aa64e..eef903e167 100644 --- a/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfPartialViewContent.cs +++ b/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfPartialViewContent.cs @@ -4,7 +4,6 @@ namespace SmartStore.Web.Framework.Pdf { - public class PdfPartialViewContent : PdfHtmlContent { public PdfPartialViewContent(string partialViewName, object model, ControllerContext controllerContext) @@ -12,5 +11,4 @@ public PdfPartialViewContent(string partialViewName, object model, ControllerCon { } } - } diff --git a/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfRouteContent.cs b/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfRouteContent.cs index 2bd47f6ae5..7b931a690f 100644 --- a/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfRouteContent.cs +++ b/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfRouteContent.cs @@ -5,8 +5,7 @@ using SmartStore.Services.Pdf; namespace SmartStore.Web.Framework.Pdf -{ - +{ public class PdfRouteContent : PdfUrlContent { public PdfRouteContent(string routeName, ControllerContext controllerContext) @@ -39,8 +38,5 @@ protected PdfRouteContent(string routeName, string action, string controller, Ro // throw Error.InvalidOperation("Either 'routeName' or 'action' must be set"); //} } - - } - } diff --git a/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfViewContent.cs b/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfViewContent.cs index 70901633b9..a96f90c4b5 100644 --- a/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfViewContent.cs +++ b/src/Presentation/SmartStore.Web.Framework/Pdf/Content/PdfViewContent.cs @@ -6,11 +6,9 @@ using SmartStore.Web.Framework.Controllers; namespace SmartStore.Web.Framework.Pdf -{ - +{ public class PdfViewContent : PdfHtmlContent { - public PdfViewContent(string viewName, object model, ControllerContext controllerContext) : this(viewName, null, model, controllerContext) { @@ -48,5 +46,4 @@ protected internal static string ViewToString(string viewName, string masterName return html; } } - } diff --git a/src/Presentation/SmartStore.Web.Framework/Pdf/PdfReceiptHeaderFooterModel.cs b/src/Presentation/SmartStore.Web.Framework/Pdf/PdfReceiptHeaderFooterModel.cs index 8ccb73199e..67a69d0b5a 100644 --- a/src/Presentation/SmartStore.Web.Framework/Pdf/PdfReceiptHeaderFooterModel.cs +++ b/src/Presentation/SmartStore.Web.Framework/Pdf/PdfReceiptHeaderFooterModel.cs @@ -16,6 +16,8 @@ public partial class PdfReceiptHeaderFooterModel : ModelBase public BankConnectionSettings MerchantBankAccount { get; set; } public ContactDataSettings MerchantContactData { get; set; } + public string MerchantFormattedAddress { get; set; } + public PdfHeaderFooterVariables Variables { get; set; } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Plugins/PluginMediator.cs b/src/Presentation/SmartStore.Web.Framework/Plugins/PluginMediator.cs index 23f036d5a2..c498275972 100644 --- a/src/Presentation/SmartStore.Web.Framework/Plugins/PluginMediator.cs +++ b/src/Presentation/SmartStore.Web.Framework/Plugins/PluginMediator.cs @@ -168,10 +168,8 @@ public TModel ToProviderModel(Provider provider, b { var routeInfo = _routesCache.GetOrAdd(model.SystemName, (key) => { - string actionName, controllerName; - RouteValueDictionary routeValues; var configurable = (IConfigurable)provider.Value; - configurable.GetConfigurationRoute(out actionName, out controllerName, out routeValues); + configurable.GetConfigurationRoute(out var actionName, out var controllerName, out var routeValues); if (actionName.IsEmpty()) { diff --git a/src/Presentation/SmartStore.Web.Framework/Routing/GuidConstraint.cs b/src/Presentation/SmartStore.Web.Framework/Routing/GuidConstraint.cs index 105fceb0b1..97f1550d1b 100644 --- a/src/Presentation/SmartStore.Web.Framework/Routing/GuidConstraint.cs +++ b/src/Presentation/SmartStore.Web.Framework/Routing/GuidConstraint.cs @@ -20,9 +20,7 @@ public bool Match(HttpContextBase httpContext, Route route, string parameterName if (!string.IsNullOrEmpty(stringValue)) { - Guid guidValue; - - return Guid.TryParse(stringValue, out guidValue) && + return Guid.TryParse(stringValue, out var guidValue) && (_allowEmpty || guidValue != Guid.Empty); } } diff --git a/src/Presentation/SmartStore.Web.Framework/Security/AdminValidateIpAddressAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Security/AdminValidateIpAddressAttribute.cs index 7de9ba99c8..aca5a27830 100644 --- a/src/Presentation/SmartStore.Web.Framework/Security/AdminValidateIpAddressAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Security/AdminValidateIpAddressAttribute.cs @@ -21,17 +21,22 @@ public virtual void OnAuthorization(AuthorizationContext filterContext) if (request == null) return; - //don't apply filter to child methods + // don't apply filter to child methods if (filterContext.IsChildAction) return; + // Prevent lockout + if (filterContext.HttpContext.Request.IsLocal) + return; + bool ok = false; var ipAddresses = SecuritySettings.Value.AdminAreaAllowedIpAddresses; if (ipAddresses != null && ipAddresses.Count > 0) { var webHelper = WebHelper.Value; + var curIpAddress = webHelper.GetCurrentIpAddress(); foreach (string ip in ipAddresses) - if (ip.Equals(webHelper.GetCurrentIpAddress(), StringComparison.InvariantCultureIgnoreCase)) + if (ip.Equals(curIpAddress, StringComparison.InvariantCultureIgnoreCase)) { ok = true; break; diff --git a/src/Presentation/SmartStore.Web.Framework/Security/FilePermissionHelper.cs b/src/Presentation/SmartStore.Web.Framework/Security/FilePermissionHelper.cs index a3ace37425..35b5720dd3 100644 --- a/src/Presentation/SmartStore.Web.Framework/Security/FilePermissionHelper.cs +++ b/src/Presentation/SmartStore.Web.Framework/Security/FilePermissionHelper.cs @@ -157,10 +157,10 @@ public static IEnumerable GetDirectoriesWrite(IWebHelper webHelper) var dirsToCheck = new List(); dirsToCheck.Add(Path.Combine(rootDir, "App_Data")); dirsToCheck.Add(Path.Combine(rootDir, "App_Data", "Tenants", DataSettings.Current.TenantName)); + dirsToCheck.Add(Path.Combine(rootDir, "App_Data", "Tenants", DataSettings.Current.TenantName, "Media")); dirsToCheck.Add(Path.Combine(rootDir, "bin")); dirsToCheck.Add(Path.Combine(rootDir, "content")); dirsToCheck.Add(Path.Combine(rootDir, "Exchange")); - dirsToCheck.Add(Path.Combine(rootDir, "Media")); dirsToCheck.Add(Path.Combine(rootDir, "plugins")); dirsToCheck.Add(Path.Combine(rootDir, "plugins\\bin")); return dirsToCheck; diff --git a/src/Presentation/SmartStore.Web.Framework/Security/RequireHttpsByConfigAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Security/RequireHttpsByConfigAttribute.cs index 2e28de7a9c..20ca8336ee 100644 --- a/src/Presentation/SmartStore.Web.Framework/Security/RequireHttpsByConfigAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Security/RequireHttpsByConfigAttribute.cs @@ -3,7 +3,6 @@ using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Domain.Security; -using SmartStore.Core.Infrastructure; namespace SmartStore.Web.Framework.Security { @@ -33,8 +32,9 @@ public virtual void OnAuthorization(AuthorizationContext filterContext) return; var securitySettings = SecuritySettings.Value; + var isLocalRequest = filterContext.HttpContext.Request.IsLocal; - if (!securitySettings.UseSslOnLocalhost && filterContext.HttpContext.Request.IsLocal) + if (!securitySettings.UseSslOnLocalhost && isLocalRequest) return; var webHelper = WebHelper.Value; @@ -62,7 +62,7 @@ public virtual void OnAuthorization(AuthorizationContext filterContext) // string url = "https://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl; string url = webHelper.GetThisPageUrl(true, true); - filterContext.Result = new RedirectResult(url, true); + filterContext.Result = new RedirectResult(url, !isLocalRequest); } } } @@ -74,7 +74,7 @@ public virtual void OnAuthorization(AuthorizationContext filterContext) // redirect to HTTP version of page // string url = "http://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl; string url = webHelper.GetThisPageUrl(true, false); - filterContext.Result = new RedirectResult(url, true); + filterContext.Result = new RedirectResult(url, !isLocalRequest); } } break; diff --git a/src/Presentation/SmartStore.Web.Framework/Settings/LoadSettingAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Settings/LoadSettingAttribute.cs new file mode 100644 index 0000000000..df6e25159f --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Settings/LoadSettingAttribute.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.Mvc; +using SmartStore.Core.Configuration; +using SmartStore.Services; +using SmartStore.Web.Framework.Controllers; + +namespace SmartStore.Web.Framework.Settings +{ + [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + public class LoadSettingAttribute : FilterAttribute, IActionFilter + { + public sealed class SettingParam + { + public ISettings Instance { get; set; } + public ParameterDescriptor Parameter { get; set; } + } + + protected int _storeId; + protected SettingParam[] _settingParams; + + public LoadSettingAttribute() + : this(true) + { + } + + public LoadSettingAttribute(bool updateParameterFromStore) + { + UpdateParameterFromStore = updateParameterFromStore; + } + + public bool UpdateParameterFromStore { get; set; } + public ICommonServices Services { get; set; } + + public virtual void OnActionExecuting(ActionExecutingContext filterContext) + { + // Get the current configured store id + _storeId = filterContext.Controller.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + Func predicate = (x) => new[] { "storescope", "storeid" }.Contains(x.ParameterName, StringComparer.OrdinalIgnoreCase); + var storeScopeParam = FindActionParameters(filterContext.ActionDescriptor, false, false, predicate).FirstOrDefault(); + if (storeScopeParam != null) + { + // We found an action param named storeScope with type int. Assign our storeId to it. + filterContext.ActionParameters[storeScopeParam.ParameterName] = _storeId; + } + + // Find the required ISettings concrete types in ActionDescriptor.GetParameters() + _settingParams = FindActionParameters(filterContext.ActionDescriptor) + .Select(x => + { + // Load settings for the settings type obtained with FindActionParameters() + var settings = UpdateParameterFromStore + ? Services.Settings.LoadSetting(x.ParameterType, _storeId) + : filterContext.ActionParameters[x.ParameterName] as ISettings; + + if (settings == null) + { + throw new InvalidOperationException($"Could not load settings for type '{x.ParameterType.FullName}'."); + } + + // Replace settings from action parameters with our loaded settings + if (UpdateParameterFromStore) + { + filterContext.ActionParameters[x.ParameterName] = settings; + } + + return new SettingParam + { + Instance = settings, + Parameter = x + }; + }) + .ToArray(); + } + + public virtual void OnActionExecuted(ActionExecutedContext filterContext) + { + var viewResult = filterContext.Result as ViewResultBase; + + if (viewResult != null) + { + var model = viewResult.Model; + + if (model == null) + { + // Nothing to override. E.g. insufficient permission. + return; + } + + var settingsHelper = new StoreDependingSettingHelper(filterContext.Controller.ViewData); + + foreach (var param in _settingParams) + { + settingsHelper.GetOverrideKeys(param.Instance, model, _storeId, Services.Settings); + } + } + } + + protected IEnumerable FindActionParameters( + ActionDescriptor actionDescriptor, + bool requireDefaultConstructor = true, + bool throwIfNotFound = true, + Func predicate = null) + { + Guard.NotNull(actionDescriptor, nameof(actionDescriptor)); + + var t = typeof(T); + + var query = actionDescriptor + .GetParameters() + .Where(x => t.IsAssignableFrom(x.ParameterType)); + + if (predicate != null) + { + query = query.Where(predicate); + } + + if (throwIfNotFound && !query.Any()) + { + throw new InvalidOperationException( + $"A controller action method with a '{this.GetType().Name}' attribute requires an action parameter of type '{t.Name}' in order to execute properly."); + } + + if (requireDefaultConstructor) + { + foreach (var param in query) + { + if (!param.ParameterType.HasDefaultConstructor()) + { + throw new InvalidOperationException($"The parameter '{param.ParameterName}' must have a default parameterless constructor."); + } + } + } + + return query; + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Settings/SaveSettingAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Settings/SaveSettingAttribute.cs new file mode 100644 index 0000000000..278679dbde --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Settings/SaveSettingAttribute.cs @@ -0,0 +1,93 @@ +using System; +using System.Linq; +using System.Web.Mvc; + +namespace SmartStore.Web.Framework.Settings +{ + [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + public sealed class SaveSettingAttribute : LoadSettingAttribute + { + private FormCollection _form; + private IDisposable _settingsWriteBatch; + + public SaveSettingAttribute() + : base(true) + { + } + + public SaveSettingAttribute(bool updateParameterFromStore) + : base(updateParameterFromStore) + { + } + + public override void OnActionExecuting(ActionExecutingContext filterContext) + { + base.OnActionExecuting(filterContext); + + if (!filterContext.Controller.ViewData.ModelState.IsValid) + { + return; + } + + // Find the required FormCollection parameter in ActionDescriptor.GetParameters() + var formParam = FindActionParameters(filterContext.ActionDescriptor, requireDefaultConstructor: false, throwIfNotFound: false).FirstOrDefault(); + _form = formParam != null + ? (FormCollection)filterContext.ActionParameters[formParam.ParameterName] + : BindFormCollection(filterContext.Controller.ControllerContext); + + + _settingsWriteBatch = Services.Settings.BeginScope(); + } + + public override void OnActionExecuted(ActionExecutedContext filterContext) + { + if (filterContext.Controller.ViewData.ModelState.IsValid) + { + var updateSettings = true; + var redirectResult = filterContext.Result as RedirectToRouteResult; + if (redirectResult != null) + { + var controllerName = redirectResult.RouteValues["controller"] as string; + var areaName = redirectResult.RouteValues["area"] as string; + if (controllerName.IsCaseInsensitiveEqual("security") && areaName.IsCaseInsensitiveEqual("admin")) + { + // Insufficient permission. We must not save because the action did not run. + updateSettings = false; + } + } + + if (updateSettings) + { + var settingHelper = new StoreDependingSettingHelper(filterContext.Controller.ViewData); + + foreach (var param in _settingParams) + { + settingHelper.UpdateSettings(param.Instance, _form, _storeId, Services.Settings); + } + } + } + + if (_settingsWriteBatch != null) + { + _settingsWriteBatch.Dispose(); + _settingsWriteBatch = null; + } + + base.OnActionExecuted(filterContext); + } + + private FormCollection BindFormCollection(ControllerContext controllerContext) + { + var bindingContext = new ModelBindingContext + { + ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(FormCollection)), + ModelState = controllerContext.Controller.ViewData.ModelState, + ValueProvider = controllerContext.Controller.ValueProvider + }; + + var modelBinder = ModelBinders.Binders.GetBinder(typeof(FormCollection)); + + return (FormCollection)modelBinder.BindModel(controllerContext, bindingContext); + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Settings/StoreDependingSettingHelper.cs b/src/Presentation/SmartStore.Web.Framework/Settings/StoreDependingSettingHelper.cs index 0ccfd8a656..7605dd7934 100644 --- a/src/Presentation/SmartStore.Web.Framework/Settings/StoreDependingSettingHelper.cs +++ b/src/Presentation/SmartStore.Web.Framework/Settings/StoreDependingSettingHelper.cs @@ -18,7 +18,7 @@ public StoreDependingSettingHelper(ViewDataDictionary viewData) _viewData = viewData; } - public static string ViewDataKey { get { return "StoreDependingSettingData"; } } + public static string ViewDataKey => "StoreDependingSettingData"; public StoreDependingSettingData Data { @@ -48,13 +48,18 @@ public bool IsOverrideChecked(object settings, string name, FormCollection form) public void AddOverrideKey(object settings, string name) { + if (Data == null) + { + throw new SmartException("You must call GetOverrideKeys or CreateViewDataObject before AddOverrideKey."); + } + var key = settings.GetType().Name + "." + name; Data.OverrideSettingKeys.Add(key); } public void CreateViewDataObject(int activeStoreScopeConfiguration, string rootSettingClass = null) { - _viewData[ViewDataKey] = new StoreDependingSettingData() + _viewData[ViewDataKey] = new StoreDependingSettingData { ActiveStoreScopeConfiguration = activeStoreScopeConfiguration, RootSettingClass = rootSettingClass @@ -94,25 +99,31 @@ public void GetOverrideKeys( continue; } - var key = string.Empty; - var setting = string.Empty; - - if (localized == null) - { - key = settingName + "." + name; - setting = settingService.GetSettingByKey(key, storeId: storeId); - } - else - { - key = string.Concat("Locales[", index.ToString(), "].", name); - setting = localizedEntityService.GetLocalizedValue(localized.LanguageId, 0, settingName, name); - } - - if (!string.IsNullOrEmpty(setting)) + string key = null; + + if (localized == null) + { + key = settingName + "." + name; + + if (settingService.GetSettingByKey(key, storeId: storeId) == null) + { + key = null; + } + } + else + { + var value = localizedEntityService.GetLocalizedValue(localized.LanguageId, 0, settingName, name); + if (!string.IsNullOrEmpty(value)) + { + key = string.Concat("Locales[", index.ToString(), "].", name); + } + } + + if (key != null) { data.OverrideSettingKeys.Add(key); } - } + } if (isRootModel) { @@ -138,13 +149,14 @@ public void GetOverrideKey( return; } - var data = Data ?? new StoreDependingSettingData(); - var setting = string.Empty; + var key = formKey; if (localized == null) { - var key = string.Concat(settings.GetType().Name, ".", settingName); - setting = settingService.GetSettingByKey(key, storeId: storeId); + if (settingService.GetSettingByKey(string.Concat(settings.GetType().Name, ".", settingName), storeId: storeId) == null) + { + key = null; + } } else { @@ -152,33 +164,57 @@ public void GetOverrideKey( throw new ArgumentException("Localized override key not supported yet."); } - if (!string.IsNullOrEmpty(setting)) + if (key != null) { - data.OverrideSettingKeys.Add(formKey); + var data = Data ?? new StoreDependingSettingData(); + data.OverrideSettingKeys.Add(key); } } - public void UpdateSettings(object settings, FormCollection form, int storeId, ISettingService settingService, ILocalizedModelLocal localized = null) + /// + /// Updates settings for a store. + /// + /// Settings class instance. + /// Form value collection. + /// Store identifier. + /// Setting service. + /// Localized model. + /// Function to map property names. Return null to skip a property. + public void UpdateSettings( + object settings, + FormCollection form, + int storeId, + ISettingService settingService, + ILocalizedModelLocal localized = null, + Func propertyNameMapper = null) { var settingName = settings.GetType().Name; var properties = FastProperty.GetProperties(localized == null ? settings.GetType() : localized.GetType()).Values; - using (settingService.BeginScope()) + foreach (var prop in properties) { - foreach (var prop in properties) + var name = prop.Name; + + if (propertyNameMapper != null) { - var name = prop.Name; - var key = settingName + "." + name; + name = propertyNameMapper(name); + } - if (storeId == 0 || IsOverrideChecked(key, form)) - { - dynamic value = prop.GetValue(localized == null ? settings : localized); - settingService.SetSetting(key, value == null ? "" : value, storeId, false); - } - else if (storeId > 0) - { - settingService.DeleteSetting(key, storeId); - } + if (string.IsNullOrWhiteSpace(name)) + { + continue; + } + + var key = string.Concat(settingName, ".", name); + + if (storeId == 0 || IsOverrideChecked(key, form)) + { + dynamic value = prop.GetValue(localized == null ? settings : localized); + settingService.SetSetting(key, value == null ? "" : value, storeId, false); + } + else if (storeId > 0) + { + settingService.DeleteSetting(key, storeId); } } } diff --git a/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj b/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj index dfed75deb2..229c4ccd1f 100644 --- a/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj +++ b/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj @@ -10,7 +10,7 @@ Properties SmartStore.Web.Framework SmartStore.Web.Framework - v4.5.2 + v4.6.1 512 @@ -92,21 +92,20 @@ ..\..\packages\BundleTransformer.Core.1.9.152\lib\net40\BundleTransformer.Core.dll True - - ..\..\packages\BundleTransformer.Less.1.9.143\lib\net40\BundleTransformer.Less.dll - True - ..\..\packages\BundleTransformer.SassAndScss.1.9.154\lib\net40\BundleTransformer.SassAndScss.dll True + + ..\..\packages\DotLiquid.2.0.254\lib\net45\DotLiquid.dll + - False - ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll + ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll + True - False - ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll + ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll + True ..\..\packages\FluentValidation.6.4.1\lib\Net45\FluentValidation.dll @@ -161,6 +160,9 @@ ..\..\packages\System.Spatial.5.8.2\lib\net40\System.Spatial.dll True + + ..\..\packages\System.ValueTuple.4.4.0\lib\net461\System.ValueTuple.dll + @@ -236,9 +238,25 @@ + + + + + + + + + + + + + + + + @@ -293,6 +311,10 @@ + + + + @@ -318,7 +340,6 @@ - @@ -349,7 +370,6 @@ - @@ -372,12 +392,10 @@ - - diff --git a/src/Presentation/SmartStore.Web.Framework/SmartUrlRoutingModule.cs b/src/Presentation/SmartStore.Web.Framework/SmartUrlRoutingModule.cs index badedf689f..65a3189792 100644 --- a/src/Presentation/SmartStore.Web.Framework/SmartUrlRoutingModule.cs +++ b/src/Presentation/SmartStore.Web.Framework/SmartUrlRoutingModule.cs @@ -11,34 +11,58 @@ using System.Web.Hosting; using System.Reflection; using SmartStore.Web.Framework.Theming; +using SmartStore.Core.Infrastructure; +using SmartStore.Core.Events; +using SmartStore.Core.Data; namespace SmartStore.Web.Framework { + public class HttpModuleInitializedEvent + { + public HttpModuleInitializedEvent(HttpApplication application) + { + Application = application; + } + + public HttpApplication Application { get; private set; } + } + + /// + /// Request event sequence: + /// - BeginRequest + /// - AuthenticateRequest + /// - PostAuthenticateRequest + /// - AuthorizeRequest + /// - PostAuthorizeRequest + /// - ResolveRequestCache + /// - PostResolveRequestCache + /// - MapRequestHandler + /// - PostMapRequestHandler + /// - AcquireRequestState + /// - PostAcquireRequestState + /// - PreRequestHandlerExecute + /// - PostRequestHandlerExecute + /// - ReleaseRequestState + /// - PostReleaseRequestState + /// - UpdateRequestCache + /// - PostUpdateRequestCache + /// - LogRequest + /// - PostLogRequest + /// - EndRequest + /// - PreSendRequestHeaders + /// - PreSendRequestContent + /// public class SmartUrlRoutingModule : IHttpModule { private static readonly object _contextKey = new object(); private static readonly ConcurrentBag _routes = new ConcurrentBag(); - public void Init(HttpApplication application) + static SmartUrlRoutingModule() { - if (application.Context.Items[_contextKey] == null) - { - application.Context.Items[_contextKey] = _contextKey; - - if (CommonHelper.IsDevEnvironment && HttpContext.Current.IsDebuggingEnabled) - { - // Handle plugin static file in DevMode - application.PostAuthorizeRequest += (s, e) => PostAuthorizeRequest(new HttpContextWrapper(((HttpApplication)s).Context)); - application.PreSendRequestHeaders += (s, e) => PreSendRequestHeaders(new HttpContextWrapper(((HttpApplication)s).Context)); - } - - application.PostResolveRequestCache += (s, e) => PostResolveRequestCache(new HttpContextWrapper(((HttpApplication)s).Context)); - - StopSubDirMonitoring(); - } + StopSubDirMonitoring(); } - private void StopSubDirMonitoring() + private static void StopSubDirMonitoring() { try { @@ -54,6 +78,29 @@ private void StopSubDirMonitoring() catch { } } + public void Init(HttpApplication application) + { + if (!DataSettings.DatabaseIsInstalled()) + return; + + if (application.Context.Items[_contextKey] == null) + { + application.Context.Items[_contextKey] = _contextKey; + + if (CommonHelper.IsDevEnvironment && HttpContext.Current.IsDebuggingEnabled) + { + // Handle plugin static file in DevMode + application.PostAuthorizeRequest += (s, e) => PostAuthorizeRequest(new HttpContextWrapper(((HttpApplication)s).Context)); + application.PreSendRequestHeaders += (s, e) => PreSendRequestHeaders(new HttpContextWrapper(((HttpApplication)s).Context)); + } + + application.PostResolveRequestCache += (s, e) => PostResolveRequestCache(new HttpContextWrapper(((HttpApplication)s).Context)); + + // Publish event to give plugins the chance to register custom event handlers for the request lifecycle. + EngineContext.Current.Resolve().Publish(new HttpModuleInitializedEvent(application)); + } + } + /// /// Registers a path pattern which should be handled by the /// diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Events.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Events.cs new file mode 100644 index 0000000000..7754edbf54 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Events.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using DotLiquid; + +namespace SmartStore.Templating +{ + /// + /// Published when a template zone is about to be rendered. + /// By subscribing to this event, implementors can inject custom + /// content to specific template zones. + /// + public sealed class ZoneRenderingEvent + { + private IList _snippets; + + public ZoneRenderingEvent(string zoneName, IDictionary model) + { + ZoneName = zoneName; + Model = model; + } + + internal Context LiquidContext { get; set; } + + /// + /// The name of the rendered template. + /// + public string TemplateName + { + get => Evaluate("Context.TemplateName") as string; + } + + /// + /// The name of the zone which is rendered. + /// + public string ZoneName { get; private set; } + + /// + /// The template model + /// + public IDictionary Model { get; private set; } + + /// + /// Evaluates an expression - e.g. Product.Sku - and returns it's value. + /// + /// + /// + public object Evaluate(string expression) + { + return LiquidContext[expression, false]; + } + + /// + /// Specifies the custom content which the template engine should parse and inject. + /// + /// The content + public void InjectContent(string content) + { + if (content.HasValue()) + { + AddSnippet(new Snippet { Content = content, Parse = true }); + } + } + + /// + /// Specifies the custom content to inject. + /// + /// The content + /// This should be true if the content contains template syntax. + public void InjectContent(string content, bool parse) + { + if (content.HasValue()) + { + AddSnippet(new Snippet { Content = content, Parse = parse }); + } + } + + private void AddSnippet(Snippet snippet) + { + if (_snippets == null) + _snippets = new List(); + + _snippets.Add(snippet); + } + + internal IList Snippets + { + get => _snippets; + } + + internal class Snippet + { + public string Content { get; set; } + public bool Parse { get; set; } + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/DictionaryDrop.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/DictionaryDrop.cs new file mode 100644 index 0000000000..e23556ddab --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/DictionaryDrop.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; + +namespace SmartStore.Templating.Liquid +{ + internal class DictionaryDrop : SafeDropBase + { + private readonly IDictionary _inner; + + public DictionaryDrop(IDictionary data) + { + Guard.NotNull(data, nameof(data)); + + _inner = data; + } + + public override bool ContainsKey(object key) + { + return (key is string s) + ? _inner.ContainsKey(s) + : false; + } + + protected override object InvokeMember(string name) + { + return _inner.Get(name); + } + + public override object GetWrappedObject() + { + return _inner; + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/EnumerableWrapper.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/EnumerableWrapper.cs new file mode 100644 index 0000000000..721e73d533 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/EnumerableWrapper.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using DotLiquid; + +namespace SmartStore.Templating.Liquid +{ + internal class EnumerableWrapper : IEnumerable, ISafeObject + { + private readonly IEnumerable _enumerable; + + public EnumerableWrapper(IEnumerable enumerable) + { + Guard.NotNull(enumerable, nameof(enumerable)); + _enumerable = enumerable; + } + + public IEnumerator GetEnumerator() + { + return GetEnumeratorInternal(); + } + + public object GetWrappedObject() => _enumerable; + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumeratorInternal(); + } + + private IEnumerator GetEnumeratorInternal() + { + return _enumerable + .Cast() + .Select(x => LiquidUtil.CreateSafeObject(x)) + .OfType() + .GetEnumerator(); + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/ObjectDrop.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/ObjectDrop.cs new file mode 100644 index 0000000000..f729518602 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/ObjectDrop.cs @@ -0,0 +1,56 @@ +using System; +using System.Reflection; +using SmartStore.ComponentModel; +using SmartStore.Core; + +namespace SmartStore.Templating.Liquid +{ + internal class ObjectDrop : SafeDropBase + { + private readonly object _data; + private readonly Type _type; + + public ObjectDrop(object data) + { + Guard.NotNull(data, nameof(data)); + + _data = data; + + if (data is BaseEntity be) + { + _type = be.GetUnproxiedType(); + } + else + { + _type = data.GetType(); + } + } + + public override bool ContainsKey(object key) + { + return true; + } + + protected override object InvokeMember(string name) + { + var prop = FastProperty.GetProperty(_type, name); + if (prop != null) + { + return prop.GetValue(_data); + } + + var method = _type.GetMethod(name, BindingFlags.Instance | BindingFlags.Public); + if (method != null && method.GetParameters().Length == 0) + { + return method.Invoke(_data, null); + } + + return null; + } + + public override object GetWrappedObject() + { + return _data; + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/SafeDropBase.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/SafeDropBase.cs new file mode 100644 index 0000000000..2974c3cacf --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/SafeDropBase.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using DotLiquid; + +namespace SmartStore.Templating.Liquid +{ + internal interface ISafeObject + { + object GetWrappedObject(); + } + + internal abstract class SafeDropBase : ILiquidizable, IIndexable, ISafeObject + { + private readonly IDictionary _safeObjects = new Dictionary(); + + protected object GetOrCreateSafeObject(string name) + { + if (!_safeObjects.TryGetValue(name, out var safeObject)) + { + safeObject = LiquidUtil.CreateSafeObject(InvokeMember(name)); + if (safeObject is ISafeObject) + { + _safeObjects[name] = safeObject; + } + } + + return safeObject; + } + + public abstract bool ContainsKey(object key); + + public object this[object key] + { + get + { + return (key is string s) ? GetOrCreateSafeObject(s) : null; + } + } + + protected abstract object InvokeMember(string name); + + public object ToLiquid() + { + return this; + } + + public abstract object GetWrappedObject(); + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/TestDrop.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/TestDrop.cs new file mode 100644 index 0000000000..0c02cfc4c2 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Drops/TestDrop.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using DotLiquid; +using SmartStore.ComponentModel; +using SmartStore.Core; + +namespace SmartStore.Templating.Liquid +{ + internal class TestDrop : ITestModel, ILiquidizable, IIndexable, ISafeObject + { + private readonly BaseEntity _entity; + private readonly Type _type; + private readonly string _modelPrefix; + + public TestDrop(BaseEntity entity, string modelPrefix) + { + _entity = entity; + _type = entity.GetUnproxiedType(); + + if (modelPrefix.HasValue()) + { + _modelPrefix = modelPrefix.EnsureEndsWith("."); + } + + _modelPrefix = _modelPrefix ?? string.Empty; + } + + public string ModelName + { + get => _type.Name; + } + + public object GetWrappedObject() + { + return _entity; + } + + public bool ContainsKey(object key) + { + return true; + } + + public object this[object key] + { + get + { + object value = null; + + if (key is string name) + { + var modelPrefix = _modelPrefix + name; + var fastProp = FastProperty.GetProperty(_type, name); + var pi = fastProp?.Property; + + if (pi == null) + { + value = "{{ " + modelPrefix + " }}"; + } + else if (pi.PropertyType.IsPredefinedType()) + { + value = "{{ " + modelPrefix + " }}"; + } + else if (fastProp.IsSequenceType) + { + var seqType = pi.PropertyType.GetGenericArguments()[0]; + if (typeof(BaseEntity).IsAssignableFrom(seqType)) + { + var testObj1 = new TestDrop((BaseEntity)Activator.CreateInstance(seqType), "it"); + var testObj2 = new TestDrop((BaseEntity)Activator.CreateInstance(seqType), "it"); + var list = Activator.CreateInstance(typeof(List)) as List; + list.Add(testObj1); + list.Add(testObj2); + value = list; + } + } + else if (typeof(BaseEntity).IsAssignableFrom(pi.PropertyType)) + { + value = new TestDrop((BaseEntity)Activator.CreateInstance(pi.PropertyType), modelPrefix); + } + + //if (value is string s) + //{ + // value = "{1}".FormatInvariant(invalid ? " invalid" : "", value); + //} + } + + return value; + } + } + + public object ToLiquid() + { + return this; + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Filters/AdditionalFilters.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Filters/AdditionalFilters.cs new file mode 100644 index 0000000000..a723d5975d --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Filters/AdditionalFilters.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using DotLiquid; +using Newtonsoft.Json; +using SmartStore.Utilities; +using SmartStore.Services; +using SmartStore.Services.Directory; +using SmartStore.Core.Domain.Directory; +using SmartStore.Services.Common; + +namespace SmartStore.Templating.Liquid +{ + public static class AdditionalFilters + { + private static LiquidTemplateEngine GetEngine() + { + return (LiquidTemplateEngine)Template.FileSystem; + } + + #region Common Filters + + public static object Default(object input, object value) + { + return input ?? value; + } + + public static string Json(object input) + { + if (input == null) + return null; + + return JsonConvert.SerializeObject(input, new JsonSerializerSettings + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore + }); + } + + public static string FormatAddress(Context context, object input) + { + if (input == null) + return null; + + var engine = GetEngine(); + var countryService = engine.Services.Resolve(); + var addressService = engine.Services.Resolve(); + + Country country = null; + + // We know that we converted Address entity to a dictionary. + + if (input is IDictionary dict) + { + country = countryService.GetCountryById(dict.Get("CountryId").Convert().GetValueOrDefault()); + } + else if (input is IIndexable lq) + { + country = countryService.GetCountryById(lq["CountryId"].Convert().GetValueOrDefault()); + } + + return addressService.FormatAddress(input, country?.AddressFormat, context.FormatProvider); + } + + #endregion + + #region Array Filters + + public static IEnumerable Uniq(IEnumerable input) + { + if (input == null) + return null; + + return input.Cast().Distinct(); + } + + public static IEnumerable Compact(IEnumerable input) + { + if (input == null) + return null; + + return input.Cast().Where(x => x != null); + } + + public static IEnumerable Reverse(IEnumerable input) + { + if (input == null) + return null; + + return input.Cast().Reverse(); + } + + #endregion + + #region Number Filters + + public static object Ceil(object input) + { + if (input == null) + return null; + + return CommonHelper.TryConvert(input, out var d) + ? Math.Ceiling(d) + : 0; + } + + public static object Floor(object input) + { + if (input == null) + return null; + + return CommonHelper.TryConvert(input, out var d) + ? Math.Floor(d) + : 0; + } + + public static object Abs(object input) + { + if (input == null) + return null; + + return CommonHelper.TryConvert(input, out var d) + ? Math.Abs(d) + : 0; + } + + public static object AtMost(object input, object operand) + { + if (input == null || operand == null) + return null; + + return CommonHelper.TryConvert(input, out var d) && CommonHelper.TryConvert(operand, out var o) + ? Math.Min(d, o) + : 0; + } + + public static object AtLeast(object input, object operand) + { + if (input == null || operand == null) + return null; + + return CommonHelper.TryConvert(input, out var d) && CommonHelper.TryConvert(operand, out var o) + ? Math.Max(d, o) + : 0; + } + + #endregion + + #region String Filters + + public static string Prettify(object input, bool allowSpace) + { + if (CommonHelper.TryConvert(input, out var l)) + { + return Prettifier.BytesToString(l); + } + else if (input is string s) + { + return s.Prettify(allowSpace); + } + + return null; + } + + public static string SanitizeHtmlId(string input) + { + return input?.SanitizeHtmlId(); + } + + public static string Md5(string input) + { + return input?.Hash(Encoding.UTF8); + } + + public static string Sha1(string input) + { + return input?.Sha(Encoding.UTF8); + } + + public static string UrlDecode(string input) + { + return input?.UrlDecode(); + } + + public static string Handleize(string input) + { + return Inflector.Handleize(input.EmptyNull()); + } + + public static string Pluralize(string input) + { + return Inflector.Pluralize(input.EmptyNull()); + } + + #endregion + + #region Localization Filters + + public static string T(Context context, string key, object arg1 = null, object arg2 = null, object arg3 = null, object arg4 = null) + { + var engine = GetEngine(); + + int languageId = 0; + + if (context["Context.LanguageId"] is int lid) + { + languageId = lid; + } + + var args = (new object[] { arg1, arg2, arg3, arg4 }).ToArray(); + + return engine.T(key, languageId, args); + } + + #endregion + + #region Url Filters + + #endregion + + #region Html Filters + + public static string ScriptTag(string input) + { + return String.Format("", input); + } + + public static string StylesheetTag(string input) + { + return String.Format("", input); + } + + public static string ImgTag(string input, string alt = "", string css = "") + { + return input == null ? null : GetImageTag(input, alt, css); + } + + private static string GetImageTag(string src, string alt, string css) + { + return String.Format("\"{1}\"", src, alt, css); + } + + #endregion + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/LiquidTemplate.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/LiquidTemplate.cs new file mode 100644 index 0000000000..c02037c9be --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/LiquidTemplate.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Web.Hosting; +using DotLiquid; +using SmartStore.ComponentModel; + +namespace SmartStore.Templating.Liquid +{ + internal class LiquidTemplate : ITemplate + { + public LiquidTemplate(Template template, string source) + { + Guard.NotNull(template, nameof(template)); + Guard.NotNull(source, nameof(source)); + + Template = template; + Source = source; + } + + public string Source + { + get; + internal set; + } + + public Template Template + { + get; + internal set; + } + + public string Render(object model, IFormatProvider formatProvider) + { + Guard.NotNull(model, nameof(model)); + Guard.NotNull(formatProvider, nameof(formatProvider)); + + var p = CreateParameters(model, formatProvider); + return Template.Render(p); + } + + private RenderParameters CreateParameters(object data, IFormatProvider formatProvider) + { + var p = new RenderParameters(formatProvider); + + Hash hash = null; + + if (data is ISafeObject so) + { + if (so.GetWrappedObject() is IDictionary soDict) + { + hash = Hash.FromDictionary(soDict); + } + else + { + data = so.GetWrappedObject(); + } + } + + if (hash == null) + { + hash = new Hash(); + + if (data is IDictionary dict) + { + foreach (var kvp in dict) + { + hash[kvp.Key] = LiquidUtil.CreateSafeObject(kvp.Value); + } + } + else + { + var props = FastProperty.GetProperties(data); + foreach (var prop in props) + { + hash[prop.Key] = LiquidUtil.CreateSafeObject(prop.Value.GetValue(data)); + } + } + } + + p.LocalVariables = hash; + p.ErrorsOutputMode = HostingEnvironment.IsHosted ? ErrorsOutputMode.Display : ErrorsOutputMode.Rethrow; + + return p; + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/LiquidTemplateEngine.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/LiquidTemplateEngine.cs new file mode 100644 index 0000000000..7175b83841 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/LiquidTemplateEngine.cs @@ -0,0 +1,160 @@ +using System; +using System.IO; +using System.Web; +using System.Web.Hosting; +using DotLiquid; +using DotLiquid.FileSystems; +using DotLiquid.NamingConventions; +using SmartStore.Core; +using SmartStore.Core.Events; +using SmartStore.Core.Infrastructure.DependencyManagement; +using SmartStore.Core.IO; +using SmartStore.Core.Localization; +using SmartStore.Core.Themes; +using SmartStore.Services; + +namespace SmartStore.Templating.Liquid +{ + public partial class LiquidTemplateEngine : ITemplateEngine, ITemplateFileSystem + { + private readonly Work _services; + private readonly Work _localizer; + private readonly Work _themeContext; + private readonly IVirtualPathProvider _vpp; + + public LiquidTemplateEngine( + Work services, + IVirtualPathProvider vpp, + Work themeContext, + Work localizer) + { + _services = services; + _vpp = vpp; + _localizer = localizer; + _themeContext = themeContext; + + // Register tag "zone" + Template.RegisterTagFactory(new ZoneTagFactory(_services.Value.EventPublisher)); + + // Register Filters + Template.RegisterFilter(typeof(AdditionalFilters)); + + Template.NamingConvention = new CSharpNamingConvention(); + Template.FileSystem = this; + } + + #region Services + + public ICommonServices Services => _services.Value; + + public LocalizedString T(string key, int languageId, params object[] args) + { + return _localizer.Value(key, languageId, args); + } + + #endregion + + #region ITemplateEngine + + public ITemplate Compile(string source) + { + Guard.NotNull(source, nameof(source)); + + return new LiquidTemplate(Template.Parse(source), source); + } + + public string Render(string source, object model, IFormatProvider formatProvider) + { + Guard.NotNull(source, nameof(source)); + Guard.NotNull(model, nameof(model)); + Guard.NotNull(formatProvider, nameof(formatProvider)); + + return Compile(source).Render(model, formatProvider); + } + + public ITestModel CreateTestModelFor(BaseEntity entity, string modelPrefix) + { + Guard.NotNull(entity, nameof(entity)); + + return new TestDrop(entity, modelPrefix); + } + + #endregion + + #region ITemplateFileSystem + + public Template GetTemplate(Context context, string templateName) + { + var virtualPath = ResolveVirtualPath(context, templateName); + + if (virtualPath.IsEmpty()) + { + return null; + } + + var cacheKey = HttpRuntime.Cache.BuildScopedKey("LiquidPartial://" + virtualPath); + var cachedTemplate = HttpRuntime.Cache.Get(cacheKey); + + if (cachedTemplate == null) + { + // Read from file, compile and put to cache with file dependeny + var source = ReadTemplateFileInternal(virtualPath); + cachedTemplate = Template.Parse(source); + var cacheDependency = _vpp.GetCacheDependency(virtualPath, DateTime.UtcNow); + HttpRuntime.Cache.Insert(cacheKey, cachedTemplate, cacheDependency); + } + + return (Template)cachedTemplate; + } + + public string ReadTemplateFile(Context context, string templateName) + { + var virtualPath = ResolveVirtualPath(context, templateName); + + return ReadTemplateFileInternal(virtualPath); + } + + private string ReadTemplateFileInternal(string virtualPath) + { + if (virtualPath.IsEmpty()) + { + return string.Empty; + } + + if (!_vpp.FileExists(virtualPath)) + { + throw new FileNotFoundException($"Include file '{virtualPath}' does not exist."); + } + + using (var stream = _vpp.OpenFile(virtualPath)) + { + return stream.AsString(); + } + } + + private string ResolveVirtualPath(Context context, string templateName) + { + var path = ((string)context[templateName]).NullEmpty() ?? templateName; + + if (path.IsEmpty()) + return string.Empty; + + path = path.EnsureEndsWith(".liquid"); + + string virtualPath = null; + + if (!path.StartsWith("~/")) + { + var currentTheme = _themeContext.Value.CurrentTheme; + virtualPath = _vpp.Combine(currentTheme.Location, currentTheme.ThemeName, "Views/Shared/EmailTemplates", path); + } + else + { + virtualPath = VirtualPathUtility.ToAppRelative(path); + } + return virtualPath; + } + + #endregion + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/LiquidUtil.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/LiquidUtil.cs new file mode 100644 index 0000000000..541af87685 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/LiquidUtil.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace SmartStore.Templating.Liquid +{ + internal static class LiquidUtil + { + private static readonly IDictionary> _typeWrapperCache + = new Dictionary>(); + + internal static object CreateSafeObject(object value) + { + if (value == null) + { + return null; + } + + if (value is TestDrop) + { + return value; + } + + var valueType = value.GetType(); + + if (!_typeWrapperCache.TryGetValue(valueType, out var fn)) + { + if (value is IDictionary dict) + { + fn = x => new DictionaryDrop((IDictionary)x); + } + else if (valueType.IsSequenceType()) + { + var genericArgs = valueType.GetGenericArguments(); + var isEnumerable = genericArgs.Length == 1 && valueType.IsSubClass(typeof(IEnumerable<>)); + if (isEnumerable) + { + var seqType = genericArgs[0]; + if (!IsSafeType(seqType)) + { + fn = x => new EnumerableWrapper((IEnumerable)x); + } + } + } + else if (valueType.IsPlainObjectType()) + { + fn = x => new ObjectDrop(x); + } + + _typeWrapperCache[valueType] = fn; + } + + return fn?.Invoke(value) ?? value; + } + + public static bool IsSafeType(Type type) + { + return type.IsPredefinedType(); + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Tags/Zone.cs b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Tags/Zone.cs new file mode 100644 index 0000000000..ed1113cf1c --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Templating/Liquid/Tags/Zone.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using DotLiquid; +using DotLiquid.Exceptions; +using DotLiquid.Util; +using SmartStore.Core.Events; + +namespace SmartStore.Templating.Liquid +{ + internal sealed class ZoneTagFactory : ITagFactory + { + private readonly IEventPublisher _eventPublisher; + + public ZoneTagFactory(IEventPublisher eventPublisher) + { + _eventPublisher = eventPublisher; + } + + public string TagName => "zone"; + + public Tag Create() + { + return new Zone { EventPublisher = _eventPublisher }; + } + + class Zone : Tag + { + private static readonly Regex Syntax = R.B(@"^({0})", DotLiquid.Liquid.QuotedFragment); + + private string _zoneName; + + public IEventPublisher EventPublisher { get; set; } + + public override void Initialize(string tagName, string markup, List tokens) + { + Match syntaxMatch = Syntax.Match(markup); + + if (syntaxMatch.Success) + { + _zoneName = syntaxMatch.Groups[1].Value; + } + else + { + throw new SyntaxException("Syntax Error in 'zone' tag - Valid syntax: zone '[ZoneName]'."); + } + + base.Initialize(tagName, markup, tokens); + } + + public override void Render(Context context, TextWriter result) + { + var zoneName = (string)context[_zoneName] ?? _zoneName; + + if (zoneName.IsEmpty()) + return; + + var model = context.Environments.First(); + var evt = new ZoneRenderingEvent(zoneName, model); + + evt.LiquidContext = context; + + EventPublisher.Publish(evt); + + if (evt.Snippets != null && evt.Snippets.Count > 0) + { + foreach (var snippet in evt.Snippets) + { + if (snippet.Parse) + { + Template.Parse(snippet.Content) + .Render(result, new RenderParameters(context.FormatProvider) { LocalVariables = model }); + } + else + { + result.Write(snippet.Content); + } + } + } + } + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/AssetTranslator.cs b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/AssetTranslator.cs index 8ebc50a0d2..1e3e3fbd6c 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/AssetTranslator.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/AssetTranslator.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; -using BundleTransformer.Core; using BundleTransformer.Core.Assets; using BundleTransformer.Core.Constants; -using BundleTransformer.Core.Transformers; using BundleTransformer.Core.Translators; using BundleTransformer.SassAndScss.Translators; using SmartStore.Core.Infrastructure; @@ -152,12 +150,4 @@ protected override string[] ValidTypeCodes get { return new[] { "sass", "scss" }; } } } - - public sealed class LessTranslator : AssetTranslator - { - protected override string[] ValidTypeCodes - { - get { return new[] { "less" }; } - } - } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/BundlingVirtualPathProvider.cs b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/BundlingVirtualPathProvider.cs index 5aff8064f7..69bf40ad2f 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/BundlingVirtualPathProvider.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/BundlingVirtualPathProvider.cs @@ -87,7 +87,7 @@ public override CacheDependency GetCacheDependency(string virtualPath, IEnumerab // invalidate the cache when variables change cacheKey = FrameworkCacheConsumer.BuildThemeVarsCacheKey(theme.ThemeName, storeId); - if ((styleResult.IsSass || styleResult.IsLess) && (ThemeHelper.IsStyleValidationRequest())) + if (styleResult.IsSass && (ThemeHelper.IsStyleValidationRequest())) { // Special case: ensure that cached validation result gets nuked in a while, // when ThemeVariableService publishes the entity changed messages. diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/ModuleImportsVirtualFile.cs b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/ModuleImportsVirtualFile.cs index fb59f5e743..720b884ee4 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/ModuleImportsVirtualFile.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/ModuleImportsVirtualFile.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Web.Hosting; using SmartStore.Core.Plugins; +using SmartStore.Core.Data; namespace SmartStore.Web.Framework.Theming.Assets { @@ -17,7 +18,10 @@ static ModuleImportsVirtualFile() _adminImports = new HashSet(); _publicImports = new HashSet(); - CollectModuleImports(); + if (DataSettings.DatabaseIsInstalled()) + { + CollectModuleImports(); + } } private static void CollectModuleImports() diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/CssHttpHandler.cs b/src/Presentation/SmartStore.Web.Framework/Theming/CssHttpHandler.cs index 8cd3c243de..c7a96144a9 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/CssHttpHandler.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/CssHttpHandler.cs @@ -1,8 +1,6 @@ using System; -using System.Threading.Tasks; using System.Web; using System.Web.Caching; -using System.Web.Optimization; using BundleTransformer.Core; using BundleTransformer.Core.Assets; using BundleTransformer.Core.Configuration; @@ -12,6 +10,7 @@ using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Infrastructure; +using SmartStore.Core.Themes; using SmartStore.Web.Framework.Theming.Assets; namespace SmartStore.Web.Framework.Theming @@ -24,14 +23,6 @@ protected override IAsset TranslateAssetCore(IAsset asset, ITransformer transfor } } - public class LessCssHttpHandler : CssHttpHandlerBase - { - protected override IAsset TranslateAssetCore(IAsset asset, ITransformer transformer, bool isDebugMode) - { - return InnerTranslateAsset("LessTranslator", asset, transformer, isDebugMode); - } - } - public abstract class CssHttpHandlerBase : BundleTransformer.Core.HttpHandlers.StyleAssetHandlerBase { protected CssHttpHandlerBase() diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/DefaultThemeFileResolver.cs b/src/Presentation/SmartStore.Web.Framework/Theming/DefaultThemeFileResolver.cs index 348a3926cf..591aa577e1 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/DefaultThemeFileResolver.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/DefaultThemeFileResolver.cs @@ -135,17 +135,13 @@ public InheritedThemeFileResult Resolve(string virtualPath) bool isExplicit = false; - string requestedThemeName; - string relativePath; - string query; - - virtualPath = ThemeHelper.TokenizePath(virtualPath, out requestedThemeName, out relativePath, out query); + virtualPath = ThemeHelper.TokenizePath(virtualPath, out var requestedThemeName, out var relativePath, out var query); Func nullOrFile = () => { if (isExplicit) { - return new InheritedThemeFileResult { IsExplicit = true, OriginalVirtualPath = virtualPath }; + return new InheritedThemeFileResult { IsExplicit = true, OriginalVirtualPath = virtualPath, Query = query }; } return null; }; @@ -177,9 +173,7 @@ public InheritedThemeFileResult Resolve(string virtualPath) using (_rwLock.GetWriteLock()) { // ALWAYS begin the search with the current working theme's location! - string resultVirtualPath; - string resultPhysicalPath; - string actualLocation = LocateFile(currentTheme.ThemeName, relativePath, out resultVirtualPath, out resultPhysicalPath); + string actualLocation = LocateFile(currentTheme.ThemeName, relativePath, out var resultVirtualPath, out var resultPhysicalPath); if (actualLocation != null) { @@ -190,7 +184,8 @@ public InheritedThemeFileResult Resolve(string virtualPath) ResultVirtualPath = resultVirtualPath, ResultPhysicalPath = resultPhysicalPath, OriginalThemeName = requestedThemeName, - ResultThemeName = actualLocation + ResultThemeName = actualLocation, + Query = query }; } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/SmartVirtualPathProvider.cs b/src/Presentation/SmartStore.Web.Framework/Theming/SmartVirtualPathProvider.cs index db01c3e0b1..88d06542ba 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/SmartVirtualPathProvider.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/SmartVirtualPathProvider.cs @@ -43,11 +43,9 @@ protected string ResolveDebugFilePath(string virtualPath) // Two-Level caching: RequestCache > AppCache var d = _requestState.GetState(); - string debugPath; - if (!d.TryGetValue(virtualPath, out debugPath)) + if (!d.TryGetValue(virtualPath, out var debugPath)) { - string appRelativePath; - if (!IsPluginPath(virtualPath, out appRelativePath)) + if (!IsPluginPath(virtualPath, out var appRelativePath)) { // don't query again in this request d[virtualPath] = null; diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHelper.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHelper.cs index e6045dab69..9e5474c48e 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHelper.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHelper.cs @@ -26,9 +26,9 @@ static ThemeHelper() { ThemesBasePath = CommonHelper.GetAppSetting("sm:ThemesBasePath", "~/Themes/").EnsureEndsWith("/"); - var pattern = @"^{0}(.*)/(.+)(\.)(png|gif|jpg|jpeg|css|scss|less|js|cshtml|svg|json)$".FormatInvariant(ThemesBasePath); + var pattern = @"^{0}(.*)/(.+)(\.)(png|gif|jpg|jpeg|css|scss|js|cshtml|svg|json|liquid)(\?explicit)*$".FormatInvariant(ThemesBasePath); s_inheritableThemeFilePattern = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); - s_themeVarsPattern = new Regex(@"\.(db|app)/themevars(.scss|.less)$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); + s_themeVarsPattern = new Regex(@"\.(db|app)/themevars(.scss)$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); s_moduleImportsPattern = new Regex(@"\.app/moduleimports.scss$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); s_extensionlessPathPattern = new Regex(@"~/(.+)/([^/.]*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); } @@ -37,7 +37,7 @@ internal static IEnumerable RemoveVirtualImports(IEnumerable vir { Guard.NotNull(virtualPathDependencies, nameof(virtualPathDependencies)); - // determine the virtual themevars.(scss|less) import reference + // determine the virtual themevarsscss import reference var themeVarsFile = virtualPathDependencies.Where(x => ThemeHelper.PathIsThemeVars(x)).FirstOrDefault(); var moduleImportsFile = virtualPathDependencies.Where(x => ThemeHelper.PathIsModuleImports(x)).FirstOrDefault(); @@ -131,10 +131,6 @@ internal static IsStyleSheetResult IsStyleSheet(string path) { return new IsStyleSheetResult { Path = path, IsSass = true }; } - else if (extension == ".less") - { - return new IsStyleSheetResult { Path = path, IsLess = true }; - } else if (extension.IsEmpty()) { // StyleBundles are extension-less, so we have to ask 'BundleTable' @@ -148,6 +144,15 @@ internal static IsStyleSheetResult IsStyleSheet(string path) } } } + else if (extension.EndsWith("?explicit")) + { + // Handle virtual Sass imports with '?explicit' query + // TBD: (mc) other query params could exist + + // Process again, this time without query + var pathWithoutQuery = path.Substring(0, path.IndexOf('?')); + return IsStyleSheet(pathWithoutQuery); + } return null; } @@ -187,7 +192,6 @@ internal class IsStyleSheetResult { public string Path { get; set; } public bool IsCss { get; set; } - public bool IsLess { get; set; } public bool IsSass { get; set; } public bool IsBundle { get; set; } @@ -197,8 +201,6 @@ public string Extension { if (IsSass) return ".scss"; - else if (IsLess) - return ".less"; else if (IsBundle) return ""; @@ -208,7 +210,7 @@ public string Extension public bool IsPreprocessor { - get { return IsLess || IsSass; } + get { return IsSass; } } public bool IsThemeVars diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHtmlExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHtmlExtensions.cs index 6ad545d6e1..39195b6f37 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHtmlExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHtmlExtensions.cs @@ -19,7 +19,7 @@ public static class ThemeHtmlExtensions #region ThemeVars - public static MvcHtmlString ThemeVarLabel(this HtmlHelper html, ThemeVariableInfo info) + public static MvcHtmlString ThemeVarLabel(this HtmlHelper html, ThemeVariableInfo info, string hint = null) { Guard.NotNull(info, "info"); @@ -30,15 +30,14 @@ public static MvcHtmlString ThemeVarLabel(this HtmlHelper html, ThemeVariableInf var displayName = locService.GetResource(resKey, langId, false, "", true); - string hint = null; - if (displayName.HasValue()) + if (displayName.HasValue() && hint.IsEmpty()) { hint = locService.GetResource(resKey + ".Hint", langId, false, "", true); hint = "${0}{1}".FormatInvariant(info.Name, hint.HasValue() ? "\n" + hint : ""); } result.Append("
                        "); - result.Append(html.Label(html.NameForThemeVar(info), displayName.NullEmpty() ?? "$" + info.Name)); + result.Append(html.Label(html.NameForThemeVar(info), displayName.NullEmpty() ?? "$" + info.Name, new { @class = "col-form-label" })); if (hint.HasValue()) { result.Append(html.Hint(hint).ToHtmlString()); @@ -66,7 +65,6 @@ public static MvcHtmlString ThemeVarEditor(this HtmlHelper html, ThemeVariableIn strValue = value.ToString(); } - var currentTheme = ThemeHelper.ResolveCurrentTheme(); var isDefault = strValue.IsCaseInsensitiveEqual(info.DefaultValue); var isValidColor = info.Type == ThemeVariableType.Color && ((strValue.HasValue() && ThemeVarsRepository.IsValidColor(strValue)) || (strValue.IsEmpty() && ThemeVarsRepository.IsValidColor(info.DefaultValue))); @@ -97,19 +95,26 @@ public static MvcHtmlString ThemeVarEditor(this HtmlHelper html, ThemeVariableIn control = html.TextBox(expression, isDefault ? "" : strValue, new { placeholder = info.DefaultValue, @class = "form-control" }); } + return control; + } + + public static MvcHtmlString ThemeVarChainInfo(this HtmlHelper html, ThemeVariableInfo info) + { + Guard.NotNull(info, "info"); + + var currentTheme = ThemeHelper.ResolveCurrentTheme(); + if (currentTheme != info.Manifest) { // the variable is inherited from a base theme: display an info badge var chainInfo = " {0}".FormatCurrent(info.Manifest.ThemeName); - return MvcHtmlString.Create(control.ToString() + chainInfo); + return MvcHtmlString.Create(chainInfo); } - else - { - return control; - } - } - private static MvcHtmlString ThemeVarSelectEditor(HtmlHelper html, ThemeVariableInfo info, string expression, string value) + return MvcHtmlString.Empty; + } + + private static MvcHtmlString ThemeVarSelectEditor(HtmlHelper html, ThemeVariableInfo info, string expression, string value) { var manifest = info.Manifest; diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeVarsRepository.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeVarsRepository.cs index 48f1d0ff63..82fd16985e 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeVarsRepository.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeVarsRepository.cs @@ -18,7 +18,6 @@ internal class ThemeVarsRepository private static readonly Regex s_valueLessVars = new Regex(@"[@][a-zA-Z0-9_-]+", RegexOptions.Compiled); //private static readonly Regex s_valueWhitelist = new Regex(@"^[#@]?[a-zA-Z0-9""' _\.,-]*$"); - const string LessVarPrefix = "@var_"; const string SassVarPrefix = "$"; public string GetPreprocessorCss(string extension, string themeName, int storeId) @@ -27,9 +26,8 @@ public string GetPreprocessorCss(string extension, string themeName, int storeId Guard.IsPositive(storeId, nameof(storeId)); var variables = GetVariables(themeName, storeId); + var css = Transform(variables); - var isLess = extension.IsCaseInsensitiveEqual(".less"); - var css = Transform(variables, isLess); return css; } @@ -87,35 +85,17 @@ private ExpandoObject GetRawVariablesCore(string themeName, int storeId) return themeVarService.GetThemeVariables(themeName, storeId) ?? new ExpandoObject(); } - private string Transform(IDictionary parameters, bool toLess) + private string Transform(IDictionary parameters) { if (parameters.Count == 0) return string.Empty; - var prefix = toLess ? LessVarPrefix : SassVarPrefix; + var prefix = SassVarPrefix; var sb = new StringBuilder(); foreach (var parameter in parameters.Where(kvp => kvp.Value.HasValue())) { - var value = parameter.Value; - if (toLess) - { - value = s_valueLessVars.Replace(value, match => - { - // Replaces all occurences of @varname with @var_varname (in case of LESS). - // The LESS compiler would throw exceptions otherwise, because the main variables file - // is not loaded yet at this stage. - var refVar = match.Value; - if (!refVar.StartsWith(prefix)) - { - refVar = "{0}{1}".FormatInvariant(prefix, refVar.Substring(1)); - } - - return refVar; - }); - } - - sb.AppendFormat("{0}{1}: {2};\n", prefix, parameter.Key, value); + sb.AppendFormat("{0}{1}: {2};\n", prefix, parameter.Key, parameter.Value); } return sb.ToString(); diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableVirtualPathProviderViewEngine.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableVirtualPathProviderViewEngine.cs index 3068df7402..9df61823c7 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableVirtualPathProviderViewEngine.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeableVirtualPathProviderViewEngine.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.Linq; using System.Web; @@ -8,6 +7,7 @@ using System.Web.WebPages; using SmartStore.Core.Infrastructure; using SmartStore.Core.Logging; +using SmartStore.Core.Themes; using SmartStore.Services.Common; using SmartStore.Utilities; @@ -82,9 +82,6 @@ public override ViewEngineResult FindView(ControllerContext controllerContext, s var chronometer = EngineContext.Current.Resolve(); using (chronometer.Step("Find view '{0}'".FormatInvariant(viewName))) { - string[] viewLocationsSearched; - string[] masterLocationsSearched; - var themeName = GetCurrentThemeName(controllerContext); var controllerName = controllerContext.RouteData.GetRequiredString("controller"); var areaName = controllerContext.RouteData.GetAreaName(); @@ -100,7 +97,7 @@ public override ViewEngineResult FindView(ControllerContext controllerContext, s themeName, "View", useCache, - out viewLocationsSearched); + out var viewLocationsSearched); var masterPath = ResolveViewPath( controllerContext, areaName, @@ -112,7 +109,7 @@ public override ViewEngineResult FindView(ControllerContext controllerContext, s themeName, "Master", useCache, - out masterLocationsSearched); + out var masterLocationsSearched); if (!string.IsNullOrEmpty(viewPath) && (!string.IsNullOrEmpty(masterPath) || string.IsNullOrEmpty(masterName))) { diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemingVirtualPathProvider.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemingVirtualPathProvider.cs index c40580242f..b61e5a2890 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemingVirtualPathProvider.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemingVirtualPathProvider.cs @@ -8,8 +8,6 @@ using System.Web.Hosting; using SmartStore.Core.Infrastructure; using SmartStore.Core.Themes; -using SmartStore.Utilities; -using SmartStore.Web.Framework.Plugins; namespace SmartStore.Web.Framework.Theming { @@ -39,7 +37,16 @@ public override bool FileExists(string virtualPath) } else { - virtualPath = result.OriginalVirtualPath; + if (result.Query.HasValue() && result.Query.IndexOf('.') >= 0) + { + // libSass tries to locate files by appending .[s]css extension to our querystring. Prevent this shit! + return false; + } + else + { + // Let system VPP check for this file + virtualPath = result.OriginalVirtualPath; + } } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs b/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs index 682ed4e8ad..1f67537e13 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPage.cs @@ -285,6 +285,32 @@ public string GenerateHelpUrl(string path) return SmartStoreVersion.GenerateHelpUrl(seoCode, path); } + + /// + /// Tries to find a matching localization file for a given culture in the following order + /// (assuming is 'de-DE', is 'lang-*.js' and is 'en-US'): + /// + /// Exact match > lang-de-DE.js + /// Neutral culture > lang-de.js + /// Any region for language > lang-de-CH.js + /// Exact match for fallback culture > lang-en-US.js + /// Neutral fallback culture > lang-en.js + /// Any region for fallback language > lang-en-GB.js + /// + /// + /// The ISO culture code to get a localization file for, e.g. 'de-DE' + /// The virtual path to search in + /// The pattern to match, e.g. 'lang-*.js'. The wildcard char MUST exist. + /// Optional. + /// Result + public LocalizationFileResolveResult ResolveLocalizationFile( + string culture, + string virtualPath, + string pattern, + string fallbackCulture = "en") + { + return _helper.LocalizationFileResolver.Resolve(culture, virtualPath, pattern, true, fallbackCulture); + } } public abstract class WebViewPage : WebViewPage diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPageHelper.cs b/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPageHelper.cs index 269478f827..75ea3db180 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPageHelper.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/WebViewPageHelper.cs @@ -19,7 +19,7 @@ public class WebViewPageHelper private bool _initialized; private ControllerContext _controllerContext; private ExpandoObject _themeVars; - private IList _internalNotifications; + private ICollection _internalNotifications; private int? _currentCategoryId; private int? _currentManufacturerId; @@ -35,6 +35,7 @@ public WebViewPageHelper() } public Localizer T { get; set; } + public ILocalizationFileResolver LocalizationFileResolver { get; set; } public ICommonServices Services { get; set; } public IThemeRegistry ThemeRegistry { get; set; } public IThemeContext ThemeContext { get; set; } @@ -163,12 +164,12 @@ public IEnumerable ResolveNotifications(NotifyType? type) if (_internalNotifications == null) { string key = NotifyAttribute.NotificationsKey; - IList entries; + ICollection entries; var tempData = _controllerContext.Controller.TempData; if (tempData.ContainsKey(key)) { - entries = tempData[key] as IList; + entries = tempData[key] as ICollection; if (entries != null) { result = result.Concat(entries); @@ -178,14 +179,14 @@ public IEnumerable ResolveNotifications(NotifyType? type) var viewData = _controllerContext.Controller.ViewData; if (viewData.ContainsKey(key)) { - entries = viewData[key] as IList; + entries = viewData[key] as ICollection; if (entries != null) { result = result.Concat(entries); } } - _internalNotifications = new List(result); + _internalNotifications = new HashSet(result); } if (type == null) diff --git a/src/Presentation/SmartStore.Web.Framework/UI/BundleBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/BundleBuilder.cs index 8190735894..824fbb0b8f 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/BundleBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/BundleBuilder.cs @@ -8,9 +8,9 @@ using BundleTransformer.Core.Orderers; using BundleTransformer.Core.Bundles; using SmartStore.Core; -using SmartStore.Web.Framework.Theming; using SmartStore.Services.Seo; using SmartStore.Web.Framework.Theming.Assets; +using SmartStore.Core.Themes; namespace SmartStore.Web.Framework.UI { diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Button/Button.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Button/Button.cs deleted file mode 100644 index ac1947139f..0000000000 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Button/Button.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Web.Mvc; -using System.Web; - -namespace SmartStore.Web.Framework.UI { - - public class Button : IHtmlString { - - TagBuilder _builder; - - public Button(string tagName = "a", string text = "Mein Text") { - _builder = new TagBuilder(tagName); - this.Text = text; - } - - public string Text { get; set; } - - public virtual string ToHtmlString() { - _builder.AddCssClass("btn"); - _builder.InnerHtml = this.Text; - _builder.MergeAttribute("href", "#"); - return _builder.ToString(TagRenderMode.Normal); - } - - public override string ToString() { - return this.ToHtmlString(); - } - - } - -} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Component.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Component.cs index 0eafebfde4..f4f366f912 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Component.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Component.cs @@ -1,10 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Web.Mvc; -using System.Web.UI; -using System.IO; using System.Web.Routing; namespace SmartStore.Web.Framework.UI @@ -14,7 +9,7 @@ public abstract class Component : IUiComponent protected Component() { this.HtmlAttributes = new RouteValueDictionary(); - this.ComponentVersion = BootstrapVersion.V2; + this.ComponentVersion = BootstrapVersion.V4; } public string Id @@ -59,5 +54,4 @@ public BootstrapVersion ComponentVersion set; } } - -} +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentBuilder.cs index 004f8bbcf3..4e5ceef345 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentBuilder.cs @@ -1,22 +1,19 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Web; -using System.Web.UI; using SmartStore.Utilities; using SmartStore.Core.Infrastructure; using System.Web.Mvc; namespace SmartStore.Web.Framework.UI { - public abstract class ComponentBuilder : IHtmlString, IHideObjectMembers + public abstract class ComponentBuilder : IHtmlString, IHideObjectMembers where TComponent : Component - where TBuilder : ComponentBuilder + where TBuilder : ComponentBuilder { private ComponentRenderer _renderer; - protected ComponentBuilder(TComponent component, HtmlHelper htmlHelper) + protected ComponentBuilder(TComponent component, HtmlHelper htmlHelper) { Guard.NotNull(component, nameof(component)); Guard.NotNull(htmlHelper, nameof(htmlHelper)); @@ -25,7 +22,7 @@ protected ComponentBuilder(TComponent component, HtmlHelper htmlHelper) this.HtmlHelper = htmlHelper; } - protected internal HtmlHelper HtmlHelper + protected internal HtmlHelper HtmlHelper { get; private set; @@ -130,7 +127,24 @@ public virtual TBuilder HtmlAttributes(IDictionary attributes) return this as TBuilder; } - public string ToHtmlString() + public virtual TBuilder HtmlAttribute(string name, object value) + { + Guard.NotEmpty(name, nameof(name)); + Guard.NotNull(value, nameof(value)); + + this.Component.HtmlAttributes[name] = value; + return this as TBuilder; + } + + public virtual TBuilder AddCssClass(string cssClass) + { + Guard.NotEmpty(cssClass, nameof(cssClass)); + + this.Component.HtmlAttributes.AppendCssClass(cssClass); + return this as TBuilder; + } + + public string ToHtmlString() { return this.Renderer.ToHtmlString(); } @@ -145,11 +159,9 @@ public virtual void Render() this.Renderer.Render(); } - public static implicit operator TComponent(ComponentBuilder builder) + public static implicit operator TComponent(ComponentBuilder builder) { return builder.ToComponent(); } - } - } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentFactory.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentFactory.cs index b4fa8f1bf2..43b2d6dcfc 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentFactory.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentFactory.cs @@ -1,24 +1,19 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Web.Mvc; using System.ComponentModel; using SmartStore.Core; namespace SmartStore.Web.Framework.UI -{ - - public class ComponentFactory : IHideObjectMembers +{ + public class ComponentFactory : IHideObjectMembers { - - public ComponentFactory(HtmlHelper helper) + public ComponentFactory(HtmlHelper helper) { this.HtmlHelper = helper; } [EditorBrowsable(EditorBrowsableState.Never)] - public HtmlHelper HtmlHelper + public HtmlHelper HtmlHelper { get; set; @@ -26,48 +21,55 @@ public HtmlHelper HtmlHelper #region Builders - public virtual TabStripBuilder TabStrip() + public virtual TabStripBuilder TabStrip() { - return new TabStripBuilder(new TabStrip(), this.HtmlHelper); + return new TabStripBuilder(new TabStrip(), this.HtmlHelper); } - public virtual WindowBuilder Window() + public virtual WindowBuilder Window() { - return new WindowBuilder(new Window(), this.HtmlHelper); + return new WindowBuilder(new Window(), this.HtmlHelper); } - public virtual PagerBuilder Pager(string viewDataKey) + public virtual PagerBuilder Pager(string viewDataKey) { var dataSource = this.HtmlHelper.ViewContext.ViewData.Eval(viewDataKey) as IPageable; if (dataSource == null) { - throw new InvalidOperationException(string.Format("Item in ViewData with key '{0}' is not an IPageable.", - viewDataKey)); + throw new InvalidOperationException(string.Format("Item in ViewData with key '{0}' is not an IPageable.", viewDataKey)); } return Pager(dataSource); } - public virtual PagerBuilder Pager(int pageIndex, int pageSize, int totalItemsCount) + public virtual PagerBuilder Pager(int pageIndex, int pageSize, int totalItemsCount) { return Pager(new PagedList(pageIndex, pageSize, totalItemsCount)); } - public virtual PagerBuilder Pager(int pageCount) + public virtual PagerBuilder Pager(int pageCount) { // for simple pagers without active state (e.g. forum topic mini pager) return Pager(new PagedList(0, 1, pageCount)); } - public virtual PagerBuilder Pager(IPageable model) + public virtual PagerBuilder Pager(IPageable model) { Guard.NotNull(model, "model"); - return new PagerBuilder(new Pager(), this.HtmlHelper).Model(model); + return new PagerBuilder(new Pager(), this.HtmlHelper).Model(model); } - #endregion + public virtual EntityPickerBuilder EntityPicker() + { + return new EntityPickerBuilder(new EntityPicker(), this.HtmlHelper); + } - } + public virtual FileUploaderBuilder FileUploader() + { + return new FileUploaderBuilder(new FileUploader(), this.HtmlHelper); + } + #endregion + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRenderer.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRenderer.cs index 0559f16d10..1bef5d85ee 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRenderer.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRenderer.cs @@ -66,24 +66,22 @@ protected virtual void WriteHtmlCore(HtmlTextWriter writer) public virtual void Render() { - using (HtmlTextWriter htmlTextWriter = new HtmlTextWriter(this.ViewContext.Writer)) - { - this.WriteHtml(htmlTextWriter); - } - } + var output = ((System.Web.Mvc.WebViewPage)this.HtmlHelper.ViewDataContainer).Output; + var str = ToHtmlString(); + output.Write(str); + } public virtual string ToHtmlString() { - string str; using (var stringWriter = new StringWriter()) { using (var htmlWriter = new HtmlTextWriter(stringWriter)) { this.WriteHtml(htmlWriter); - str = stringWriter.ToString(); + var str = stringWriter.ToString(); + return str; } } - return str; } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRendererUtils.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRendererUtils.cs index 68994f30b4..fdcc8c62e1 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRendererUtils.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/ComponentRendererUtils.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; namespace SmartStore.Web.Framework.UI { @@ -17,5 +15,4 @@ public static void PrependCssClass(this IDictionary attributes, attributes.PrependInValue("class", " ", @class); } } - } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPicker.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPicker.cs new file mode 100644 index 0000000000..161f88beca --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPicker.cs @@ -0,0 +1,49 @@ +using System; +namespace SmartStore.Web.Framework.UI +{ + public class EntityPicker : Component + { + public EntityPicker() + { + EntityType = "product"; + IconCssClass = "fa fa-search"; + HtmlAttributes["type"] = "button"; + HtmlAttributes.AppendCssClass("btn btn-secondary"); + HighlightSearchTerm = true; + AppendMode = true; + Delimiter = ","; + FieldName = "id"; + } + + public string EntityType { get; set; } + + public string TargetInputSelector + { + get { return HtmlAttributes["data-target"] as string; } + set { HtmlAttributes["data-target"] = value; } + } + + public string Caption { get; set; } + public string IconCssClass { get; set; } + + public string DialogTitle { get; set; } + public string DialogUrl { get; set; } + + public bool DisableGroupedProducts { get; set; } + public bool DisableBundleProducts { get; set; } + public int[] DisabledEntityIds { get; set; } + public int[] SelectedEntityIds { get; set; } + + public bool EnableThumbZoomer { get; set; } + public bool HighlightSearchTerm { get; set; } + + public int MaxItems { get; set; } + public bool AppendMode { get; set; } + public string Delimiter { get; set; } + public string FieldName { get; set; } + + public string OnDialogLoadingHandlerName { get; set; } + public string OnDialogLoadedHandlerName { get; set; } + public string OnSelectionCompletedHandlerName { 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 new file mode 100644 index 0000000000..a17551a51e --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/EntityPicker/EntityPickerBuilder.cs @@ -0,0 +1,161 @@ +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 +{ + public class EntityPickerBuilder : ComponentBuilder, TModel> + { + public EntityPickerBuilder(EntityPicker component, HtmlHelper htmlHelper) + : base(component, htmlHelper) + { + WithRenderer(new ViewBasedComponentRenderer("EntityPicker")); + DialogUrl(UrlHelper.GenerateUrl( + null, + "Picker", + "Entity", + new RouteValueDictionary { { "area", "" } }, + RouteTable.Routes, + htmlHelper.ViewContext.RequestContext, + false)); + } + + public EntityPickerBuilder EntityType(string value) + { + base.Component.EntityType = value; + return this; + } + + public EntityPickerBuilder Caption(string value) + { + base.Component.Caption = value; + return this; + } + + public EntityPickerBuilder IconCssClass(string value) + { + base.Component.IconCssClass = value; + return this; + } + + public EntityPickerBuilder Tooltip(string value) + { + base.Component.HtmlAttributes["title"] = value; + return this; + } + + public EntityPickerBuilder DialogTitle(string value) + { + base.Component.DialogTitle = value; + return this; + } + + public EntityPickerBuilder DialogUrl(string value) + { + base.Component.DialogUrl = value; + return this; + } + + + public EntityPickerBuilder For(Expression> expression) + { + Guard.NotNull(expression, nameof(expression)); + + return For(ExpressionHelper.GetExpressionText(expression)); + } + + public EntityPickerBuilder For(string expression) + { + Guard.NotEmpty(expression, nameof(expression)); + + base.Component.TargetInputSelector = "#" + this.HtmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(expression); + return this; + } + + + public EntityPickerBuilder DisableGroupedProducts(bool value) + { + base.Component.DisableGroupedProducts = value; + return this; + } + + public EntityPickerBuilder DisableBundleProducts(bool value) + { + base.Component.DisableBundleProducts = value; + return this; + } + + public EntityPickerBuilder DisabledEntityIds(params int[] values) + { + base.Component.DisabledEntityIds = values; + return this; + } + + public EntityPickerBuilder SelectedEntityIds(params int[] values) + { + base.Component.SelectedEntityIds = values; + return this; + } + + public EntityPickerBuilder EnableThumbZoomer(bool value) + { + base.Component.EnableThumbZoomer = value; + return this; + } + + public EntityPickerBuilder HighlightSearchTerm(bool value) + { + base.Component.HighlightSearchTerm = value; + return this; + } + + + public EntityPickerBuilder MaxItems(int value) + { + base.Component.MaxItems = value; + return this; + } + + public EntityPickerBuilder AppendMode(bool value) + { + base.Component.AppendMode = value; + return this; + } + + public EntityPickerBuilder Delimiter(string value) + { + base.Component.Delimiter = value; + return this; + } + + public EntityPickerBuilder FieldName(string value) + { + base.Component.FieldName = value; + return this; + } + + + public EntityPickerBuilder OnDialogLoading(string handlerName) + { + base.Component.OnDialogLoadingHandlerName = handlerName; + return this; + } + + public EntityPickerBuilder OnDialogLoaded(string handlerName) + { + base.Component.OnDialogLoadedHandlerName = handlerName; + return this; + } + + public EntityPickerBuilder OnSelectionCompleted(string handlerName) + { + base.Component.OnSelectionCompletedHandlerName = handlerName; + return this; + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Enums.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Enums.cs index 4906bd5cb5..b6cc612798 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Enums.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Enums.cs @@ -4,13 +4,28 @@ namespace SmartStore.Web.Framework.UI { public enum BadgeStyle { - Default, + Secondary, Primary, Success, Info, Warning, - Danger - } + Danger, + Light, + Dark + } + + public enum ButtonStyle + { + Secondary, + Primary, + Success, + Info, + Warning, + Danger, + Light, + Dark, + Link + } public enum BootstrapVersion { diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/FileUploader/FileUploader.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/FileUploader/FileUploader.cs new file mode 100644 index 0000000000..0616d36231 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/FileUploader/FileUploader.cs @@ -0,0 +1,52 @@ +using System; +using SmartStore.Core.Infrastructure; +using SmartStore.Core.Localization; + +namespace SmartStore.Web.Framework.UI +{ + public class FileUploader : Component + { + public FileUploader() + : this(EngineContext.Current.Resolve()) + { + } + + public FileUploader(Localizer localizer) + { + HtmlAttributes.AppendCssClass("fileupload form-row align-items-center"); + HtmlAttributes.Add("data-accept", "gif|jpe?g|png"); + HtmlAttributes.Add("data-show-remove-after-upload", "false"); + IconCssClass = "fa fa-upload"; + ButtonStyle = ButtonStyle.Secondary; + + if (localizer != null) + { + CancelText = localizer("Common.Fileuploader.Cancel"); + RemoveText = localizer("Common.Remove"); + UploadText = localizer("Common.Fileuploader.Upload"); + } + } + + public string UploadUrl + { + get { return HtmlAttributes["data-upload-url"] as string; } + set { HtmlAttributes["data-upload-url"] = value; } + } + + public string IconCssClass { get; set; } + public ButtonStyle ButtonStyle { get; set; } + public bool ButtonOutlineStyle { get; set; } + public bool ShowRemoveButton { get; set; } + + public string CancelText { get; set; } + public string RemoveText { get; set; } + public string UploadText { get; set; } + + public string OnUploadingHandlerName { get; set; } + public string OnUploadCompletedHandlerName { get; set; } + public string OnErrorHandlerName { get; set; } + public string OnFileRemoveHandlerName { get; set; } + public string OnAbortedHandlerName { get; set; } + public string OnCompletedHandlerName { get; set; } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/FileUploader/FileUploaderBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/FileUploader/FileUploaderBuilder.cs new file mode 100644 index 0000000000..761a51bcf1 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/FileUploader/FileUploaderBuilder.cs @@ -0,0 +1,125 @@ +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 +{ + public class FileUploaderBuilder : ComponentBuilder, TModel> + { + public FileUploaderBuilder(FileUploader component, HtmlHelper htmlHelper) + : base(component, htmlHelper) + { + WithRenderer(new ViewBasedComponentRenderer("FileUploader")); + } + + public FileUploaderBuilder UploadUrl(string value) + { + base.Component.UploadUrl = value; + return this; + } + + public FileUploaderBuilder IconCssClass(string value) + { + base.Component.IconCssClass = value; + return this; + } + + public FileUploaderBuilder ButtonStyle(ButtonStyle value) + { + base.Component.ButtonStyle = value; + return this; + } + + public FileUploaderBuilder ButtonOutlineStyle(bool value) + { + base.Component.ButtonOutlineStyle = value; + return this; + } + + public FileUploaderBuilder ShowRemoveButton(bool value) + { + base.Component.ShowRemoveButton = value; + return this; + } + + public FileUploaderBuilder ShowRemoveButtonAfterUpload(bool value) + { + base.Component.HtmlAttributes["data-show-remove-after-upload"] = value.ToString().ToLower(); + return this; + } + + public FileUploaderBuilder AcceptedFileTypes(string value) + { + if (value.IsEmpty()) + { + if (base.Component.HtmlAttributes.ContainsKey("data-accept")) + base.Component.HtmlAttributes.Remove("data-accept"); + } + else + { + base.Component.HtmlAttributes["data-accept"] = value; + } + + return this; + } + + public FileUploaderBuilder CancelText(string value) + { + base.Component.CancelText = value; + return this; + } + + public FileUploaderBuilder RemoveText(string value) + { + base.Component.RemoveText = value; + return this; + } + + public FileUploaderBuilder UploadText(string value) + { + base.Component.UploadText = value; + return this; + } + + public FileUploaderBuilder OnUploadingHandlerName(string handlerName) + { + base.Component.OnUploadingHandlerName = handlerName; + return this; + } + + public FileUploaderBuilder OnUploadCompletedHandlerName(string handlerName) + { + base.Component.OnUploadCompletedHandlerName = handlerName; + return this; + } + + public FileUploaderBuilder OnErrorHandlerName(string handlerName) + { + base.Component.OnErrorHandlerName = handlerName; + return this; + } + + public FileUploaderBuilder OnFileRemoveHandlerName(string handlerName) + { + base.Component.OnFileRemoveHandlerName = handlerName; + return this; + } + + public FileUploaderBuilder OnAbortedHandlerName(string handlerName) + { + base.Component.OnAbortedHandlerName = handlerName; + return this; + } + + public FileUploaderBuilder OnCompletedHandlerName(string handlerName) + { + base.Component.OnCompletedHandlerName = handlerName; + return this; + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/HtmlHelperExtensions.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/HtmlHelperExtensions.cs index d69c730832..b9382d6e5b 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/HtmlHelperExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/HtmlHelperExtensions.cs @@ -9,9 +9,9 @@ namespace SmartStore.Web.Framework.UI { public static class HtmlHelperExtensions { - public static ComponentFactory SmartStore(this HtmlHelper helper) + public static ComponentFactory SmartStore(this HtmlHelper helper) { - return new ComponentFactory(helper); + return new ComponentFactory(helper); } public static IHtmlString Attrs(this HtmlHelper html, IDictionary attrs) diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/IHtmlAttributesContainer.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/IHtmlAttributesContainer.cs index cc7aec1fc1..31974274b4 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/IHtmlAttributesContainer.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/IHtmlAttributesContainer.cs @@ -3,15 +3,11 @@ namespace SmartStore.Web.Framework.UI { - public interface IHtmlAttributesContainer { - IDictionary HtmlAttributes { get; } - } - } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/AdminMenuProvider.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/AdminMenuProvider.cs index 0689436a61..ccbc2fbbd6 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/AdminMenuProvider.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/AdminMenuProvider.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using SmartStore.Collections; namespace SmartStore.Web.Framework.UI @@ -10,7 +7,7 @@ public abstract class AdminMenuProvider : IMenuProvider { public void BuildMenu(TreeNode rootNode) { - var pluginsNode = rootNode.Children.FirstOrDefault(x => x.Value.Id == "plugins"); + var pluginsNode = rootNode.SelectNodeById("plugins"); BuildMenuCore(pluginsNode); } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuItem.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuItem.cs index dacbc68cb9..09786612db 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuItem.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuItem.cs @@ -1,8 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Web.Routing; -using SmartStore.Collections; namespace SmartStore.Web.Framework.UI { diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigatableComponentBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigatableComponentBuilder.cs index 44be9592b6..5f6a13e8aa 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigatableComponentBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigatableComponentBuilder.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Web.Mvc; using System.Web.Routing; using System.Web.WebPages; @@ -10,13 +8,12 @@ namespace SmartStore.Web.Framework.UI { - - public abstract class NavigatableComponentBuilder : ComponentBuilder + public abstract class NavigatableComponentBuilder : ComponentBuilder where TComponent : Component, INavigatable - where TBuilder : ComponentBuilder + where TBuilder : ComponentBuilder { - public NavigatableComponentBuilder(TComponent component, HtmlHelper htmlHelper) + public NavigatableComponentBuilder(TComponent component, HtmlHelper htmlHelper) : base(component, htmlHelper) { } @@ -75,12 +72,12 @@ public TBuilder Url(string value) } - public abstract class NavigatableComponentWithContentBuilder : NavigatableComponentBuilder + public abstract class NavigatableComponentWithContentBuilder : NavigatableComponentBuilder where TComponent : Component, INavigatable, IContentContainer - where TBuilder : ComponentBuilder + where TBuilder : ComponentBuilder { - public NavigatableComponentWithContentBuilder(TComponent component, HtmlHelper htmlHelper) + public NavigatableComponentWithContentBuilder(TComponent component, HtmlHelper htmlHelper) : base(component, htmlHelper) { } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItem.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItem.cs index fe8dad2d7d..abccb95769 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItem.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItem.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Web.Routing; using System.Web.WebPages; using Newtonsoft.Json; @@ -35,7 +33,9 @@ public NavigationItem() public string ImageUrl { get; set; } - public string Icon { get; set; } + public int? ImageId { get; set; } + + public string Icon { get; set; } public string Text { get; set; } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItemBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItemBuilder.cs index 3bf8a3f511..1671e86478 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItemBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/NavigationItemBuilder.cs @@ -127,7 +127,13 @@ public TBuilder ImageUrl(string value) return (this as TBuilder); } - public TBuilder Icon(string value) + public TBuilder ImageId(int? value) + { + this.Item.ImageId = value; + return (this as TBuilder); + } + + public TBuilder Icon(string value) { this.Item.Icon = value; return (this as TBuilder); @@ -145,7 +151,7 @@ public TBuilder Summary(string value) return (this as TBuilder); } - public TBuilder Badge(string value, BadgeStyle style = BadgeStyle.Default, bool condition = true) + public TBuilder Badge(string value, BadgeStyle style = BadgeStyle.Secondary, bool condition = true) { if (condition) { diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/Pager.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/Pager.cs index 1aa29e6cbc..dfaa5d4603 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/Pager.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/Pager.cs @@ -233,7 +233,5 @@ public ModifiedParameter ModifiedParam get; private set; } - } - } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/PagerBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/PagerBuilder.cs index 262dae68b1..1164c9fa3c 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/PagerBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Pager/PagerBuilder.cs @@ -4,117 +4,116 @@ namespace SmartStore.Web.Framework.UI { - public class PagerBuilder : NavigatableComponentBuilder + public class PagerBuilder : NavigatableComponentBuilder, TModel> { - - public PagerBuilder(Pager component, HtmlHelper htmlHelper) + public PagerBuilder(Pager component, HtmlHelper htmlHelper) : base(component, htmlHelper) { } - public PagerBuilder Model(IPageable value) + public PagerBuilder Model(IPageable value) { base.Component.Model = value; return this; } - public PagerBuilder Alignment(PagerAlignment value) + public PagerBuilder Alignment(PagerAlignment value) { base.Component.Alignment = value; return this; } - public PagerBuilder Size(PagerSize value) + public PagerBuilder Size(PagerSize value) { base.Component.Size = value; return this; } - public PagerBuilder Style(PagerStyle value) + public PagerBuilder Style(PagerStyle value) { base.Component.Style = value; return this; } - public PagerBuilder ShowFirst(bool value) + public PagerBuilder ShowFirst(bool value) { base.Component.ShowFirst = value; return this; } - public PagerBuilder ShowLast(bool value) + public PagerBuilder ShowLast(bool value) { base.Component.ShowLast = value; return this; } - public PagerBuilder ShowNext(bool value) + public PagerBuilder ShowNext(bool value) { base.Component.ShowNext = value; return this; } - public PagerBuilder ShowPrevious(bool value) + public PagerBuilder ShowPrevious(bool value) { base.Component.ShowPrevious = value; return this; } - public PagerBuilder ShowSummary(bool value) + public PagerBuilder ShowSummary(bool value) { base.Component.ShowSummary = value; return this; } - public PagerBuilder ShowPaginator(bool value) + public PagerBuilder ShowPaginator(bool value) { base.Component.ShowPaginator = value; return this; } - public PagerBuilder MaxPagesToDisplay(int value) + public PagerBuilder MaxPagesToDisplay(int value) { base.Component.MaxPagesToDisplay = value; return this; } - public PagerBuilder SkipActiveState(bool value) + public PagerBuilder SkipActiveState(bool value) { base.Component.SkipActiveState = value; return this; } - public PagerBuilder FirstButtonText(string value) + public PagerBuilder FirstButtonText(string value) { base.Component.FirstButtonText = value; return this; } - public PagerBuilder LastButtonText(string value) + public PagerBuilder LastButtonText(string value) { base.Component.LastButtonText = value; return this; } - public PagerBuilder NextButtonText(string value) + public PagerBuilder NextButtonText(string value) { base.Component.NextButtonText = value; return this; } - public PagerBuilder PreviousButtonText(string value) + public PagerBuilder PreviousButtonText(string value) { base.Component.PreviousButtonText = value; return this; } - public PagerBuilder CurrentPageText(string value) + public PagerBuilder CurrentPageText(string value) { base.Component.CurrentPageText = value; return this; } - public PagerBuilder ItemTitleFormatString(string value) + public PagerBuilder ItemTitleFormatString(string value) { base.Component.ItemTitleFormatString = value; return this; diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStrip.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStrip.cs index 7f0999ee8a..7ab56a8bdc 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStrip.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStrip.cs @@ -67,12 +67,6 @@ public TabsStyle Style set; } - public bool Stacked - { - get; - set; - } - public bool Fade { get; diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripBuilder.cs index 4282edd2e6..390a7732d4 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripBuilder.cs @@ -4,28 +4,28 @@ namespace SmartStore.Web.Framework.UI { - public class TabStripBuilder : ComponentBuilder + public class TabStripBuilder : ComponentBuilder, TModel> { - public TabStripBuilder(TabStrip Component, HtmlHelper htmlHelper) + public TabStripBuilder(TabStrip Component, HtmlHelper htmlHelper) : base(Component, htmlHelper) { } - public TabStripBuilder Items(Action addAction) + public TabStripBuilder Items(Action addAction) { var factory = new TabFactory(base.Component.Items, this.HtmlHelper); addAction(factory); return this; } - public TabStripBuilder Responsive(bool value, string breakpoint = " Responsive(bool value, string breakpoint = " TabContentHeaderContent(string value) { if (value.IsEmpty()) { @@ -36,66 +36,60 @@ public TabStripBuilder TabContentHeaderContent(string value) return this.TabContentHeaderContent(x => new HelperResult(writer => writer.Write(value))); } - public TabStripBuilder TabContentHeaderContent(Func value) + public TabStripBuilder TabContentHeaderContent(Func value) { return this.TabContentHeaderContent(value(null)); } - public TabStripBuilder TabContentHeaderContent(HelperResult value) + public TabStripBuilder TabContentHeaderContent(HelperResult value) { base.Component.TabContentHeaderContent = value; return this; } - public TabStripBuilder Position(TabsPosition value) + public TabStripBuilder Position(TabsPosition value) { base.Component.Position = value; return this; } - public TabStripBuilder Style(TabsStyle value) + public TabStripBuilder Style(TabsStyle value) { base.Component.Style = value; return this; } - public TabStripBuilder Stacked(bool value) - { - base.Component.Stacked = value; - return this; - } - - public TabStripBuilder Fade(bool value) + public TabStripBuilder Fade(bool value) { base.Component.Fade = value; return this; } - public TabStripBuilder SmartTabSelection(bool value) + public TabStripBuilder SmartTabSelection(bool value) { base.Component.SmartTabSelection = value; return this; } - public TabStripBuilder OnAjaxBegin(string value) + public TabStripBuilder OnAjaxBegin(string value) { base.Component.OnAjaxBegin = value; return this; } - public TabStripBuilder OnAjaxSuccess(string value) + public TabStripBuilder OnAjaxSuccess(string value) { base.Component.OnAjaxSuccess = value; return this; } - public TabStripBuilder OnAjaxFailure(string value) + public TabStripBuilder OnAjaxFailure(string value) { base.Component.OnAjaxFailure = value; return this; } - public TabStripBuilder OnAjaxComplete(string value) + public TabStripBuilder OnAjaxComplete(string value) { base.Component.OnAjaxComplete = value; return this; 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 9a29725583..6c72f76581 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripRenderer.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/TabStrip/TabStripRenderer.cs @@ -28,6 +28,7 @@ protected override void WriteHtmlCore(HtmlTextWriter writer) var hasContent = tab.Items.Any(x => x.Content != null || x.Ajax); var isTabbable = tab.Position != TabsPosition.Top; + var isStacked = tab.Position == TabsPosition.Left || tab.Position == TabsPosition.Right; var urlHelper = new UrlHelper(this.ViewContext.RequestContext); if (tab.Items.Count == 0) @@ -46,6 +47,11 @@ protected override void WriteHtmlCore(HtmlTextWriter writer) tab.HtmlAttributes.Add("data-tabselector-href", urlHelper.Action("SetSelectedTab", "Common", new { area = "admin" })); } + if (isStacked) + { + tab.HtmlAttributes.AppendCssClass("row"); + } + if (tab.OnAjaxBegin.HasValue()) { tab.HtmlAttributes.Add("data-ajax-onbegin", tab.OnAjaxBegin); @@ -79,6 +85,12 @@ protected override void WriteHtmlCore(HtmlTextWriter writer) if (tab.Position == TabsPosition.Below && hasContent) RenderTabContent(writer); + if (isStacked) + { + writer.AddAttribute("class", "col-lg-auto nav-aside"); + writer.RenderBeginTag("aside"); // opening left/right tabs col + } + // Tabs var ulAttrs = new Dictionary(); ulAttrs.AppendCssClass("nav"); @@ -96,9 +108,9 @@ protected override void WriteHtmlCore(HtmlTextWriter writer) ulAttrs.AppendCssClass("nav-tabs nav-tabs-line"); } - if (tab.Stacked) + if (isStacked) { - ulAttrs.AppendCssClass("nav-stacked flex-column"); + ulAttrs.AppendCssClass("flex-row flex-lg-column"); } writer.AddAttributes(ulAttrs); @@ -126,16 +138,35 @@ protected override void WriteHtmlCore(HtmlTextWriter writer) writer.RenderEndTag(); // ul } + if (isStacked) + { + writer.RenderEndTag(); // closing left/right tabs col + } + + // TODO: (mc) render right positioned tabs also if (tab.Position != TabsPosition.Below && hasContent) + { + if (isStacked) + { + writer.AddAttribute("class", "col-lg nav-content"); + writer.RenderBeginTag("div"); // opening left/right content col + } + RenderTabContent(writer); + if (isStacked) + { + writer.RenderEndTag(); // closing left/right content col + } + } + if (selector != null) { writer.WriteLine( @"".FormatInvariant(selector)); @@ -203,7 +234,7 @@ private string TrySelectRememberedTab() if (tabToSelect.Ajax && tabToSelect.Content == null) { - return ".nav a[data-ajax-url][href=#{0}]".FormatInvariant(rememberedTab.TabId); + return ".nav a[data-ajax-url][href='#{0}']".FormatInvariant(rememberedTab.TabId); } } } @@ -401,6 +432,7 @@ protected virtual void RenderItemContent(HtmlTextWriter writer, Tab item, int in { //
                        {content}
                        item.ContentHtmlAttributes.AppendCssClass("tab-pane"); + item.ContentHtmlAttributes.Add("role", "tabpanel"); if (base.Component.Fade) { item.ContentHtmlAttributes.AppendCssClass("fade"); @@ -409,7 +441,7 @@ protected virtual void RenderItemContent(HtmlTextWriter writer, Tab item, int in { if (base.Component.Fade) { - item.ContentHtmlAttributes.AppendCssClass("show in"); // .in for BS2, .show for BS4 + item.ContentHtmlAttributes.AppendCssClass("show"); } item.ContentHtmlAttributes.AppendCssClass("active"); } @@ -433,8 +465,5 @@ private string BuildItemId(Tab item, int index) } return "{0}-{1}".FormatInvariant(this.Component.Id, index); } - - } - } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/Window.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/Window.cs index 4df56f3852..207b56d65f 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/Window.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/Window.cs @@ -1,23 +1,30 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Web.WebPages; namespace SmartStore.Web.Framework.UI { - - public class Window : Component + public enum WindowSize + { + Small, + Medium, + Large, + Flex, + FlexSmall + } + + public class Window : Component { - public Window() { - this.Fade = true; - this.Modal = true; - this.Visible = true; + this.Size = WindowSize.Medium; + this.Fade = true; + this.Focus = true; this.BackDrop = true; this.ShowClose = true; + this.Show = true; this.CloseOnEscapePress = true; + this.CloseOnBackdropClick = true; + this.RenderAtPageEnd = true; } public override bool NameIsRequired @@ -25,7 +32,9 @@ public override bool NameIsRequired get { return true; } } - public string Title { get; set; } + public WindowSize Size { get; set; } + + public string Title { get; set; } public HelperResult Content { get; set; } @@ -33,22 +42,22 @@ public override bool NameIsRequired public HelperResult FooterContent { get; set; } - public bool Modal { get; set; } - public bool Fade { get; set; } - public bool BackDrop { get; set; } + public bool Focus { get; set; } - public bool Visible { get; set; } + public bool Show { get; set; } - public bool ShowClose { get; set; } + public bool BackDrop { get; set; } - public bool CloseOnEscapePress { get; set; } + public bool ShowClose { get; set; } - public int? Width { get; set; } + public bool CenterVertically { get; set; } - public int? Height { get; set; } + public bool CloseOnEscapePress { get; set; } - } + public bool CloseOnBackdropClick { get; set; } + public bool RenderAtPageEnd { get; set; } + } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/WindowBuilder.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/WindowBuilder.cs index b4b0b8271f..c6d4c667c3 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/WindowBuilder.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/WindowBuilder.cs @@ -1,113 +1,118 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Web.Mvc; using System.Web.WebPages; namespace SmartStore.Web.Framework.UI { - - public class WindowBuilder : ComponentBuilder + public class WindowBuilder : ComponentBuilder, TModel> { - - public WindowBuilder(Window Component, HtmlHelper htmlHelper) + public WindowBuilder(Window Component, HtmlHelper htmlHelper) : base(Component, htmlHelper) { } - public WindowBuilder Title(string value) + public WindowBuilder Size(WindowSize value) + { + base.Component.Size = value; + return this; + } + + public WindowBuilder Title(string value) { base.Component.Title = value; return this; } - public WindowBuilder LoadContentFrom(string url) + public WindowBuilder LoadContentFrom(string url) { base.Component.ContentUrl = url; return this; } - public WindowBuilder Content(string value) + public WindowBuilder Content(string value) { return this.Content(x => new HelperResult(writer => writer.Write(value))); } - public WindowBuilder Content(Func value) + public WindowBuilder Content(Func value) { return this.Content(value(null)); } - public WindowBuilder Content(HelperResult value) + public WindowBuilder Content(HelperResult value) { this.Component.Content = value; return this; } - public WindowBuilder FooterContent(string value) + public WindowBuilder FooterContent(string value) { return this.FooterContent(x => new HelperResult(writer => writer.Write(value))); } - public WindowBuilder FooterContent(Func value) + public WindowBuilder FooterContent(Func value) { return this.FooterContent(value(null)); } - public WindowBuilder FooterContent(HelperResult value) + public WindowBuilder FooterContent(HelperResult value) { this.Component.FooterContent = value; return this; } - public WindowBuilder Modal(bool value) - { - base.Component.Modal = value; - return this; - } - - public WindowBuilder Fade(bool value) + public WindowBuilder Fade(bool value) { base.Component.Fade = value; return this; } - public WindowBuilder BackDrop(bool value) + public WindowBuilder BackDrop(bool value) { base.Component.BackDrop = value; return this; } - public WindowBuilder Visible(bool value) - { - base.Component.Visible = value; - return this; - } - - public WindowBuilder ShowClose(bool value) + public WindowBuilder ShowClose(bool value) { base.Component.ShowClose = value; return this; } - public WindowBuilder CloseOnEscapePress(bool value) - { - base.Component.CloseOnEscapePress = value; - return this; - } + public WindowBuilder Focus(bool value) + { + base.Component.Focus = value; + return this; + } - public WindowBuilder Width(int value) - { - base.Component.Width = value; - return this; - } + public WindowBuilder CenterVertically(bool value) + { + base.Component.CenterVertically = value; + return this; + } - public WindowBuilder Height(int value) + public WindowBuilder Show(bool value) + { + base.Component.Show = value; + return this; + } + + public WindowBuilder CloseOnEscapePress(bool value) { - base.Component.Height = value; + base.Component.CloseOnEscapePress = value; return this; } - } + public WindowBuilder CloseOnBackdropClick(bool value) + { + base.Component.CloseOnBackdropClick = value; + return this; + } -} + public WindowBuilder RenderAtPageEnd(bool value) + { + base.Component.RenderAtPageEnd = value; + return this; + } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/WindowRenderer.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/WindowRenderer.cs index a702513387..71507fd1af 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/WindowRenderer.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Window/WindowRenderer.cs @@ -6,82 +6,120 @@ namespace SmartStore.Web.Framework.UI // TODO: (mc) make modal window renderer BS4 ready (after backend has been updated to BS4) public class WindowRenderer : ComponentRenderer { - protected override void WriteHtmlCore(HtmlTextWriter writer) + public override void Render() + { + if (Component.RenderAtPageEnd) + { + using (this.HtmlHelper.BeginZoneContent("end")) + { + base.Render(); + } + } + else + { + base.Render(); + } + } + + protected override void WriteHtmlCore(HtmlTextWriter writer) { var win = base.Component; win.HtmlAttributes.AppendCssClass("modal"); - win.HtmlAttributes["role"] = "dialog"; - win.HtmlAttributes["tabindex"] = "-1"; - win.HtmlAttributes["aria-labelledby"] = win.Id + "Label"; + win.HtmlAttributes["role"] = "dialog"; + win.HtmlAttributes["tabindex"] = "-1"; + win.HtmlAttributes["aria-hidden"] = "true"; + win.HtmlAttributes["aria-labelledby"] = win.Id + "Label"; - if (win.Width.GetValueOrDefault() > 0) - { - win.HtmlAttributes["style"] = "width:{0}px; margin-left:-{1}px".FormatInvariant(win.Width.Value, Math.Ceiling((double)(win.Width.Value / 2))); - } - - if (!win.Visible) - { - win.HtmlAttributes["aria-hidden"] = "true"; - win.HtmlAttributes.AppendCssClass("hide"); - } - else - { - win.HtmlAttributes["aria-hidden"] = "false"; - } - - if (win.Fade) + if (win.Fade) { win.HtmlAttributes.AppendCssClass("fade"); - if (win.Visible) - win.HtmlAttributes.AppendCssClass("in"); } - // other options - win.HtmlAttributes["data-backdrop"] = win.BackDrop.ToString().ToLower(); - win.HtmlAttributes["data-keyboard"] = win.CloseOnEscapePress.ToString().ToLower(); - //win.HtmlAttributes["data-show"] = win.Visible.ToString().ToLower(); - if (win.ContentUrl.HasValue()) - { - win.HtmlAttributes["data-remote"] = win.ContentUrl; - } + // Other options + win.HtmlAttributes["data-keyboard"] = win.CloseOnEscapePress.ToString().ToLower(); + win.HtmlAttributes["data-show"] = win.Show.ToString().ToLower(); + win.HtmlAttributes["data-focus"] = win.Focus.ToString().ToLower(); + win.HtmlAttributes["data-backdrop"] = win.BackDrop + ? (win.CloseOnBackdropClick ? "true" : "static") + : "false"; - writer.AddAttributes(win.HtmlAttributes); - writer.RenderBeginTag("div"); // root div - - // HEADER - if (win.ShowClose && win.Title.HasValue()) + if (win.ContentUrl.HasValue()) { - this.RenderHeader(writer); + // TBD: (BS4) does this still work? + win.HtmlAttributes["data-remote"] = win.ContentUrl; } - // BODY - this.RenderBody(writer); - - // FOOTER - if (win.FooterContent != null) - { - this.RenderFooter(writer); - } + writer.AddAttributes(win.HtmlAttributes); + writer.RenderBeginTag("div"); // div.modal + { + var className = "modal-dialog"; + switch (win.Size) + { + case WindowSize.Small: + className += " modal-sm"; + break; + case WindowSize.Large: + className += " modal-lg"; + break; + case WindowSize.Flex: + className += " modal-flex"; + break; + case WindowSize.FlexSmall: + className += " modal-flex modal-flex-sm"; + break; + } + + if (win.CenterVertically) + { + className += " modal-dialog-centered"; + } + + writer.AddAttribute("class", className); + win.HtmlAttributes["role"] = "document"; + writer.RenderBeginTag("div"); // div.modal-dialog + { + writer.AddAttribute("class", "modal-content"); + writer.RenderBeginTag("div"); // div.modal-content + { + // HEADER + if (win.ShowClose && win.Title.HasValue()) + { + this.RenderHeader(writer); + } + + // BODY + this.RenderBody(writer); + + // FOOTER + if (win.FooterContent != null) + { + this.RenderFooter(writer); + } + } + writer.RenderEndTag(); // div.modal-content + } + writer.RenderEndTag(); // div.modal-dialog + } writer.RenderEndTag(); // div.modal } protected virtual void RenderHeader(HtmlTextWriter writer) { var win = base.Component; - + writer.AddAttribute("class", "modal-header"); writer.RenderBeginTag("div"); - if (win.ShowClose) - { - writer.Write(""); - } + if (win.Title.HasValue()) + { + writer.Write("".FormatCurrent(win.Id + "Label", win.Title)); + } - if (win.Title.HasValue()) + if (win.ShowClose) { - writer.Write("

                        {1}

                        ".FormatCurrent(win.Id + "Label", win.Title)); + writer.Write(""); } writer.RenderEndTag(); // div.modal-header @@ -92,10 +130,6 @@ protected virtual void RenderBody(HtmlTextWriter writer) var win = base.Component; writer.AddAttribute("class", "modal-body"); - if (win.Height.GetValueOrDefault() > 0) - { - writer.AddAttribute("style", "max-height:{0}px;".FormatInvariant(win.Height.Value)); - } writer.RenderBeginTag("div"); if (win.ContentUrl.IsEmpty() && win.Content != null) diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Extensions/LayoutExtensions.cs b/src/Presentation/SmartStore.Web.Framework/UI/Extensions/LayoutExtensions.cs index a2077c1211..d64d1442a7 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Extensions/LayoutExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Extensions/LayoutExtensions.cs @@ -3,6 +3,7 @@ using System.Web.Routing; using SmartStore.Core; using SmartStore.Core.Infrastructure; +using SmartStore.Core.Localization; namespace SmartStore.Web.Framework.UI { @@ -137,6 +138,38 @@ public static MvcHtmlString SmartScripts(this HtmlHelper html, UrlHelper urlHelp return MvcHtmlString.Create(pageAssetsBuilder.GenerateScripts(urlHelper, location, enableBundling)); } + /// + /// Tries to find a matching localization file for a given culture in the following order + /// (assuming is 'de-DE', is 'lang-*.js' and is 'en-US'): + /// + /// Exact match > lang-de-DE.js + /// Neutral culture > lang-de.js + /// Any region for language > lang-de-CH.js + /// Exact match for fallback culture > lang-en-US.js + /// Neutral fallback culture > lang-en.js + /// Any region for fallback language > lang-en-GB.js + /// + /// + /// The ISO culture code to get a localization file for, e.g. 'de-DE' + /// The virtual path to search in + /// The pattern to match, e.g. 'lang-*.js'. The wildcard char MUST exist. + /// Optional. + /// + public static MvcHtmlString LocalizationScript(this HtmlHelper html, string culture, string virtualPath, string pattern, string fallbackCulture = "en") + { + var fileResolver = EngineContext.Current.Resolve(); + var result = fileResolver.Resolve(culture, virtualPath, pattern, true, fallbackCulture); + + if (result != null) + { + return MvcHtmlString.Create("".FormatInvariant(result.VirtualPath)); + } + else + { + return MvcHtmlString.Empty; + } + } + #endregion #region CssFileParts diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Extensions/ScaffoldExtensions.cs b/src/Presentation/SmartStore.Web.Framework/UI/Extensions/ScaffoldExtensions.cs index 8425004c81..dd848ff90f 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Extensions/ScaffoldExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Extensions/ScaffoldExtensions.cs @@ -1,4 +1,5 @@ -using System.Text; +using System; +using System.Text; using System.Web.Mvc; using System.Web.WebPages; using SmartStore.Core.Domain.Common; @@ -15,6 +16,7 @@ public static string SymbolForBool(this HtmlHelper helper, string boolFiel { return "".FormatInvariant(boolFieldName); } + public static HelperResult SymbolForBool(this HtmlHelper helper, bool value) { return new HelperResult(writer => writer.Write("".FormatInvariant(value.ToString().ToLower()))); @@ -27,7 +29,6 @@ public static string LabeledProductName(this HtmlHelper helper, string id, if (id.HasValue()) { string url = UrlHelper.GenerateContentUrl("~/Admin/Product/Edit/", helper.ViewContext.RequestContext.HttpContext); - namePart = "\"><#= {2} #>".FormatInvariant(url, id, name); } else @@ -35,7 +36,7 @@ public static string LabeledProductName(this HtmlHelper helper, string id, namePart = "<#= {0} #>".FormatInvariant(name); } - string result = "<#= {1} #>{2}".FormatInvariant(typeLabelHint, typeName, namePart); + string result = "<#= {1} #>{2}".FormatInvariant(typeLabelHint, typeName, namePart); return "<# if({0} && {0}.length > 0) {{ #>{1}<# }} #>".FormatInvariant(name, result); } @@ -57,7 +58,7 @@ public static HelperResult LabeledProductName(this HtmlHelper helper, int namePart = "{0}".FormatInvariant(helper.Encode(name)); } - return new HelperResult(writer => writer.Write("{1}{2}".FormatInvariant(typeLabelHint, typeName, namePart))); + return new HelperResult(writer => writer.Write("{1}{2}".FormatInvariant(typeLabelHint, typeName, namePart))); } public static string LabeledOrderNumber(this HtmlHelper helper) @@ -67,7 +68,7 @@ public static string LabeledOrderNumber(this HtmlHelper helper) string link = "\"><#= OrderNumber #>".FormatInvariant(url); - string label = "{1}".FormatInvariant( + string label = "{1}".FormatInvariant( localize.GetResource("Admin.Orders.Payments.NewIpn.Hint"), localize.GetResource("Admin.Orders.Payments.NewIpn")); @@ -81,12 +82,12 @@ public static HelperResult LabeledCurrencyName(this HtmlHelper helper, int if (isPrimaryStoreCurrency) { - sb.AppendFormat("{0}", localize.GetResource("Admin.Configuration.Currencies.Fields.IsPrimaryStoreCurrency")); + sb.AppendFormat("{0}", localize.GetResource("Admin.Configuration.Currencies.Fields.IsPrimaryStoreCurrency")); } if (isPrimaryExchangeRateCurrency) { - sb.AppendFormat("{0}", localize.GetResource("Admin.Configuration.Currencies.Fields.IsPrimaryExchangeRateCurrency")); + sb.AppendFormat("{0}", localize.GetResource("Admin.Configuration.Currencies.Fields.IsPrimaryExchangeRateCurrency")); } string url = UrlHelper.GenerateContentUrl("~/Admin/Currency/Edit/", helper.ViewContext.RequestContext.HttpContext); @@ -96,9 +97,10 @@ public static HelperResult LabeledCurrencyName(this HtmlHelper helper, int return new HelperResult(writer => writer.Write(sb.ToString())); } + [Obsolete] public static string RichEditorFlavor(this HtmlHelper helper) { - return EngineContext.Current.Resolve().RichEditorFlavor.NullEmpty() ?? "RichEditor"; + return "Html"; } public static GridEditActionCommandBuilder Localize(this GridEditActionCommandBuilder builder, Localizer T) diff --git a/src/Presentation/SmartStore.Web.Framework/UI/INavigatable.cs b/src/Presentation/SmartStore.Web.Framework/UI/INavigatable.cs index b6b4591baf..2619738c49 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/INavigatable.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/INavigatable.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Web.Routing; namespace SmartStore.Web.Framework.UI @@ -65,8 +63,5 @@ string Url get; set; } - - } - } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/ISiteMap.cs b/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/ISiteMap.cs index 1d0cf12912..00f95bf721 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/ISiteMap.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/ISiteMap.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using SmartStore.Collections; namespace SmartStore.Web.Framework.UI @@ -27,6 +28,18 @@ public interface ISiteMap /// false resolves counts for direct children of only, true traverses the whole sub-tree void ResolveElementCounts(TreeNode curNode, bool deep = false); + /// + /// Gets all cached trees from the underlying cache storage + /// + /// A dictionary of trees (Key: cache key, Value: tree instance) + /// + /// Multiple trees are created per sitemap depending + /// on language, customer-(roles), store and other parameters. + /// This method does not create anything, but returns all + /// previously processed and cached sitemap variations. + /// + IDictionary> GetAllCachedTrees(); + /// /// Removes the sitemap from the application cache /// diff --git a/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/SiteMapBase.cs b/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/SiteMapBase.cs index 955466b864..8bbfa4ac73 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/SiteMapBase.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/SiteMapBase.cs @@ -3,6 +3,7 @@ using SmartStore.Core.Logging; using SmartStore.Collections; using SmartStore.Services; +using System.Collections.Generic; namespace SmartStore.Web.Framework.UI { @@ -15,8 +16,8 @@ public abstract class SiteMapBase : ISiteMap /// {0} : sitemap name /// {1} : sitemap specific key suffix /// - const string SITEMAP_KEY = "pres:sitemap:{0}-{1}"; - const string SITEMAP_PATTERN_KEY = "pres:sitemap:{0}"; + internal const string SITEMAP_KEY = "pres:sitemap:{0}-{1}"; + internal const string SITEMAP_PATTERN_KEY = "pres:sitemap:{0}*"; public abstract string Name { get; } @@ -104,6 +105,25 @@ public virtual void ResolveElementCounts(TreeNode curNode, bool deep = { } + public IDictionary> GetAllCachedTrees() + { + var cache = Services.Cache; + var keys = cache.Keys(SITEMAP_PATTERN_KEY.FormatInvariant(this.Name)); + + var trees = new Dictionary>(keys.Count()); + + foreach (var key in keys) + { + var tree = cache.Get>(key); + if (tree != null) + { + trees[key] = tree; + } + } + + return trees; + } + public void ClearCache() { Services.Cache.RemoveByPattern(SITEMAP_PATTERN_KEY.FormatInvariant(this.Name)); diff --git a/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/SiteMapService.cs b/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/SiteMapService.cs index 37fcc9ec10..2a0ee4f191 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/SiteMapService.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/SiteMap/SiteMapService.cs @@ -31,7 +31,7 @@ public TreeNode GetRootNode(string mapName) public TreeNode GetCurrentNode(string mapName, ControllerContext controllerContext) { - if (_currentNode != null) + if (_currentNode == null) { var map = GetSiteMap(mapName); _currentNode = map.Root.SelectNode(x => x.Value.IsCurrent(controllerContext), true) ?? map.Root; diff --git a/src/Presentation/SmartStore.Web.Framework/WebApi/Security/HmacAuthentication.cs b/src/Presentation/SmartStore.Web.Framework/WebApi/Security/HmacAuthentication.cs index 8c83b68688..5e503bc7cc 100644 --- a/src/Presentation/SmartStore.Web.Framework/WebApi/Security/HmacAuthentication.cs +++ b/src/Presentation/SmartStore.Web.Framework/WebApi/Security/HmacAuthentication.cs @@ -6,10 +6,11 @@ namespace SmartStore.Web.Framework.WebApi.Security { - public class HmacAuthentication + public class HmacAuthentication { - private static readonly string _delimiterRepresentation = "\n"; - private static readonly string _scheme = "SmNetHmac"; + protected static readonly string[] _dateFormats = new string[] { "o", "yyyy-MM-ddTHH:mm:ss.FFFFFFFZ" }; + protected static readonly string _delimiterRepresentation = "\n"; + protected static readonly string _scheme = "SmNetHmac"; public static string Scheme1 { get { return _scheme + "1"; } } public static string SignatureMethod { get { return "HMAC-SHA256"; } } @@ -134,14 +135,17 @@ public string GetWwwAuthenticateScheme(string schemeConsumer) return Scheme1; // fallback to first version } - /// Parse ISO-8601 UTC timestamp including milliseconds. - public bool ParseTimestamp(string timestamp, out DateTime time) + /// + /// Parse ISO-8601 UTC timestamp including optional milliseconds. + /// Examples: 2013-11-09T11:37:21.1918793Z, 2013-11-09T11:37:21.191Z, 2013-11-09T11:37:21Z. + /// + public bool ParseTimestamp(string timestamp, out DateTime time) { - if (DateTime.TryParseExact(timestamp, "o", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out time)) - return true; - - if (DateTime.TryParseExact(timestamp, "yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out time)) - return true; + foreach (var format in _dateFormats) + { + if (DateTime.TryParseExact(timestamp, format, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out time)) + return true; + } time = new DateTime(); return false; diff --git a/src/Presentation/SmartStore.Web.Framework/WebApi/Security/WebApiAuthenticateAttribute.cs b/src/Presentation/SmartStore.Web.Framework/WebApi/Security/WebApiAuthenticateAttribute.cs index 7c334db152..1393e39364 100644 --- a/src/Presentation/SmartStore.Web.Framework/WebApi/Security/WebApiAuthenticateAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/WebApi/Security/WebApiAuthenticateAttribute.cs @@ -228,12 +228,13 @@ public override void OnAuthorization(HttpActionContext actionContext) if (result == HmacResult.Success) { - // inform core about the authentication. note you cannot use IWorkContext.set_CurrentCustomer here. + // Inform core about the authentication. Note, you cannot use IWorkContext.set_CurrentCustomer here. HttpContext.Current.User = new SmartStorePrincipal(customer, HmacAuthentication.Scheme1); var response = HttpContext.Current.Response; - response.AddHeader(WebApiGlobal.Header.Version, controllingData.Version); + response.AddHeader(WebApiGlobal.Header.AppVersion, SmartStoreVersion.CurrentFullVersion); + response.AddHeader(WebApiGlobal.Header.Version, controllingData.Version); response.AddHeader(WebApiGlobal.Header.MaxTop, controllingData.MaxTop.ToString()); response.AddHeader(WebApiGlobal.Header.Date, utcNow.ToString("o")); response.AddHeader(WebApiGlobal.Header.CustomerId, customer.Id.ToString()); @@ -247,11 +248,12 @@ public override void OnAuthorization(HttpActionContext actionContext) var headers = actionContext.Response.Headers; var authorization = actionContext.Request.Headers.Authorization; - // see RFC-2616 + // See RFC-2616 var scheme = _hmac.GetWwwAuthenticateScheme(authorization != null ? authorization.Scheme : null); headers.WwwAuthenticate.Add(new AuthenticationHeaderValue(scheme)); - headers.Add(WebApiGlobal.Header.Version, controllingData.Version); + headers.Add(WebApiGlobal.Header.AppVersion, SmartStoreVersion.CurrentFullVersion); + headers.Add(WebApiGlobal.Header.Version, controllingData.Version); headers.Add(WebApiGlobal.Header.MaxTop, controllingData.MaxTop.ToString()); headers.Add(WebApiGlobal.Header.Date, utcNow.ToString("o")); headers.Add(WebApiGlobal.Header.HmacResultId, ((int)result).ToString()); diff --git a/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiCore.cs b/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiCore.cs index 01bb18c678..96066d3bf2 100644 --- a/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiCore.cs +++ b/src/Presentation/SmartStore.Web.Framework/WebApi/WebApiCore.cs @@ -22,7 +22,8 @@ public static class Header public static string Date { get { return Prefix + "Date"; } } public static string PublicKey { get { return Prefix + "PublicKey"; } } public static string MaxTop { get { return Prefix + "MaxTop"; } } - public static string Version { get { return Prefix + "Version"; } } + public static string AppVersion { get { return Prefix + "AppVersion"; } } + public static string Version { get { return Prefix + "Version"; } } public static string CustomerId { get { return Prefix + "CustomerId"; } } public static string HmacResultId { get { return Prefix + "HmacResultId"; } } public static string HmacResultDescription { get { return Prefix + "HmacResultDesc"; } } diff --git a/src/Presentation/SmartStore.Web.Framework/WebWorkContext.cs b/src/Presentation/SmartStore.Web.Framework/WebWorkContext.cs index 3c7388473d..b13f87e6bd 100644 --- a/src/Presentation/SmartStore.Web.Framework/WebWorkContext.cs +++ b/src/Presentation/SmartStore.Web.Framework/WebWorkContext.cs @@ -13,7 +13,6 @@ using SmartStore.Services.Customers; using SmartStore.Services.Directory; using SmartStore.Services.Localization; -using SmartStore.Services.Stores; using SmartStore.Services.Tax; namespace SmartStore.Web.Framework @@ -32,7 +31,6 @@ public partial class WebWorkContext : IWorkContext private readonly TaxSettings _taxSettings; private readonly LocalizationSettings _localizationSettings; private readonly ICacheManager _cacheManager; - private readonly IStoreService _storeService; private readonly Lazy _taxService; private readonly IUserAgent _userAgent; @@ -55,7 +53,6 @@ public WebWorkContext( TaxSettings taxSettings, LocalizationSettings localizationSettings, Lazy taxService, - IStoreService storeService, IUserAgent userAgent) { _cacheManager = cacheManager; @@ -69,7 +66,6 @@ public WebWorkContext( _taxSettings = taxSettings; _taxService = taxService; _localizationSettings = localizationSettings; - _storeService = storeService; _userAgent = userAgent; } @@ -195,6 +191,7 @@ protected virtual Customer GetGuestCustomer() { visitorCookie = new HttpCookie(VisitorCookieName); visitorCookie.HttpOnly = true; + //visitorCookie.Secure = true; visitorCookie.Value = customer.CustomerGuid.ToString(); if (customer.CustomerGuid == Guid.Empty) { diff --git a/src/Presentation/SmartStore.Web.Framework/app.config b/src/Presentation/SmartStore.Web.Framework/app.config index 0fea6ea0f3..1300116038 100644 --- a/src/Presentation/SmartStore.Web.Framework/app.config +++ b/src/Presentation/SmartStore.Web.Framework/app.config @@ -58,6 +58,10 @@ + + + + - + diff --git a/src/Presentation/SmartStore.Web.Framework/packages.config b/src/Presentation/SmartStore.Web.Framework/packages.config index 77f5b63ad2..f5a042c77d 100644 --- a/src/Presentation/SmartStore.Web.Framework/packages.config +++ b/src/Presentation/SmartStore.Web.Framework/packages.config @@ -7,10 +7,10 @@ - - + + @@ -34,5 +34,6 @@ + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Content/TODO.txt b/src/Presentation/SmartStore.Web/Administration/Content/TODO.txt new file mode 100644 index 0000000000..c79f519920 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Content/TODO.txt @@ -0,0 +1,52 @@ +* OK Update to latest Bootstrap +* OK Unify resource files between Flex and Admin (js, css etc.) +* Merge Flex _core.scss with Admin shared/_utils.scss +* Remove globalize.js entirely and all moment.js locale files +* OK Apply .wide to parent tr of td.adminData with html editor or any other "wide" control (hide the label also) +* Find replacement for bootstrap-typeahead.js +* OK bootstrapper-datetimepicker skinning is missing +* OK Update bootstrap-editable.js +* OK Update popper.js +* OK Select2: multiple or taggable selects are not required to be hidden fields anymore > refactor. +* OK Enclose all button labels in , when an icon is available +* OK Readonly controls in .adminData: input[type=text][readonly].form-control-plaintext (formerly static labels) +* OK Pass "postfix" additional viewdata to relevant numerictextbox instances +* OK Refactor control toggling: 'data-toggler-for' instead of redundant toggle[...]() calls in every view +* Dropdown Groups: mehr als 2 Ebenen gehen nicht, daher bspw. EditorTemplates/ModelTree.cshtml umgestalten +* OK window.openPopup > check whether 'fluid = true' should be passed +* OK Locale Editors: Move hidden fields from td to another place to avoid space +* OK Fix color picker control +* IStoreSelector, IAclSelector > refactor models & views and remove obsolete locale resources. +* OK Validation styling for form controls +* OK Shared\EditorTemplates\ButtonType.cshtml > Type names +* select2: .input-lg & .input-sm styling +* OK All tables (.table) must be wrapped in div.table-responsive +* Apply .omega to all (last) table cells which contain action buttons to enable visibility toggling on hover. Apply .active-row to parent div to disable toggling, but keep other styling. +* OK Add .alert-dismissable with × closer to all .alert elements +* OK .form-check > .form-check-input + .form-check-label +* OK Replace Html.SettingOverrideCheckbox () with new Html.SettingEditorFor() + + +TODOs Telerik grid: +==================== +* OK In ClientEvents.OnEdit: select2 change triggering only in editmode. +* OK Grid inline editing: when a grid is editable, assign width (either px or %) to all columns except the last buttons col (otherwise: disco!) +* OK .t-grid .t-last with command buttons: add *.HtmlAttributes(new { align = "right" }) to column (see ProductEdit > Promotion tab) +* OK Move custom grid action buttons below or above a grid to the grid toolbar (see ProductEdit > Promotion tab > CrossSellingGridCommands(...) etc.) +* OK .omega for last tds in Non-AJAX grids + + +TODOs BS4 Migration: +==================== +* OK Split Dropdown Buttons: .btn-dropwdown-split +* OK Input Groups: .input-group-append/prepend & .input-group-text + + +TODOs Assets: +================ +* select2 localization files + + +TODOs Frontend: +================ +* .modal.modal-flex >> .modal-dialog.modal-flex \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Content/_admin.scss b/src/Presentation/SmartStore.Web/Administration/Content/_admin.scss new file mode 100644 index 0000000000..792fbdbaf6 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Content/_admin.scss @@ -0,0 +1,1455 @@ +/// +/// +/// +/// + +// +// Legacy BS elements +// -------------------------------------------------- + +.well { + background-color: $gray-100; + padding: 1rem; + margin-bottom: $alert-margin-bottom; +} + +pre { + background: $gray-100; + padding: 1rem; + border: 1px solid rgba(#000, 0.1); + + &.prettyprint { + padding: 1rem !important; + border: 1px solid rgba(#000, 0.1) !important; + } +} + +.badge-secondary { + background-color: $gray-600; + color: $yiq-text-light; +} + + +// Typo +// ----------------------------------------------------- + +.text-light { + color: $gray-400 !important; +} + +.text-primary-darkest { + color: $sm-violet !important; +} + + +// +// Buttons: +// btn-(outline-)(secondary|light) +// look really shitty in BS4, style it decently. +// ----------------------------------------------------- + +.btn-outline-secondary, +.btn-outline-light { + color: $yiq-text-dark; + border-color: $input-border-color; + + &:not([disabled]):not(.disabled):hover, + &:not([disabled]):not(.disabled).hover { + border-color: darken($input-border-color, 8%); + } + + &.disabled, + &[disabled] { + color: $yiq-text-dark; + border-color: $input-border-color; + } + + &:not([disabled]):not(.disabled):active, + &:not([disabled]):not(.disabled).active, + .show > &.dropdown-toggle { + background-color: darken($secondary, 5%); + border-color: darken($input-border-color, 5%); + } +} + +.btn-outline-secondary { + color: $yiq-text-dark; +} + +.btn-outline-light { + color: $gray-600; +} + + + +// +// Layout +// ----------------------------------------------------- + +html { + height: 100%; +} + +body { + background-color: $gray-200; + &.popup { + background-color: #fff; + } +} + +#page { + position: relative; + margin: 0; + padding: 0; +} + +header { + @include gradient-bg($indigo); + padding-bottom: 50px; + + .popup & { + display: none; + } +} + +.cph { + padding-top: 55px; +} + +#content { + position: relative; + height: auto; + margin: 0 15px; + margin-top: -50px; + background-color: #fff; + padding: $content-padding-y ($content-padding-x / 2); + padding-bottom: $content-padding-x * 2; + box-shadow: 0 0 6px rgba(0,0,0, 0.2); + border-top-left-radius: $border-radius; + border-top-right-radius: $border-radius; + + @include media-breakpoint-up(lg) { + margin-left: 30px; + margin-right: 30px; + padding-left: $content-padding-x; + padding-right: $content-padding-x; + } +} + +.popup #content { + margin: 0; + border: 0; + box-shadow: none; + border-radius: 0; +} + +.popup.bare { + height: 100%; + overflow-y: hidden; + + #page, + #content, + .cph { + height: 100%; + padding: 0; + } +} + +.section-title { + border-bottom: solid 3px $gray-300; + padding-bottom: 1px; + margin-bottom: 10px; + color: #009FFF; + font-size: 14px; + font-weight: 700; + vertical-align: bottom; +} + +.section-title img { + vertical-align: middle; + padding-bottom: 2px; +} + +.section-header { + position: absolute; + left: 0; + top: 0; + right: 0; + padding: 0.75rem ($content-padding-x / 2); + transition: background-color 0.1s ease-in-out, padding 0.1s ease-in-out; + + @include make-row(); + flex-wrap: nowrap; + margin-left: 0; + margin-right: 0; + + .title, .options { + @include make-col-ready(); + /*padding-left: 0; + padding-right: 0;*/ + } + + .title { + font-family: $headings-font-family; + font-weight: 400; + color: $headings-color; + font-size: $h5-font-size; + line-height: $input-height; + min-height: $input-height; + vertical-align: middle; + @include text-truncate(); + padding-left: 0; + padding-right: 5px; + color: $text-muted; + // mimic .col + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + + > i { + font-size: 28px; + vertical-align: text-top; + margin-right: 0.5rem; + color: $text-muted; + } + + > img { + max-height: $input-height; + max-width: 120px; + margin-right: 5px; + vertical-align: top; + } + + a:not(.btn) { + text-decoration: none; + font-family: $font-family-sans-serif; + font-size: 12px; + font-weight: normal; + } + } + // .title + .options { + // mimic .col-auto + flex: 0 0 auto; + width: auto; + max-width: none; // Reset earlier grid tiers + padding-left: 5px; + padding-right: 0; + + > * { + vertical-align: middle; + } + + &.btn-toolbar { + margin: 0; + } + + @media (max-width: 768px) { + // hide action button labels on small screens + .btn > span:not(:only-child) { + display: none; + } + } + } + + &.sticky { + position: fixed; + z-index: 600; + left: $content-padding-x / 2; + right: $content-padding-x / 2; + margin-left: 0; + margin-right: 0; + background: $gray-100; + background: #fff; + border-bottom: 1px solid rgba(0,0,0, 0.08); + + .modal-open & { + // add the killed scrollbar width + margin-right: 17px; + } + } + + @include media-breakpoint-up(lg) { + padding-left: $content-padding-x; + padding-right: $content-padding-x; + + &.sticky { + left: $content-padding-x; + right: $content-padding-x; + } + } +} + +.popup .section-header.sticky { + left: 0; + right: 0; + top: 0; +} + + +// +// Main Menu +// -------------------------------------------------------------- + +#navbar { + padding: 0 30px; + + .dropdown-toggle:after { + display: none; + } + + .navbar-brand { + //margin-right: 1.75rem; + + > .navbar-img { + height: 28px; + width: auto; + } + } + + .dropdown-menu { + margin-top: -2px; + opacity: 0; + transform: translateY(-10px); + display: block; + visibility: hidden; + transition: opacity 0.28s ease, transform 0.28s ease-out; + + &.show { + visibility: visible; + transform: translateY(0); + opacity: 1; + } + } + + #navbar-menu .nav-item > .dropdown-menu { + @include bubble("top", "near", 8, #fff, $dropdown-border-color, 10px); + } + + .dropdown-item > i, + .dropdown-item > img { + display: inline-block; + /*margin-left: -8px; + margin-right: 0.6rem;*/ + font-size: inherit; + //vertical-align: bottom; + } + + .dropdown-header { + font-size: $font-size-xs; + text-transform: uppercase; + } + + .nav-item { + .nav-link { + max-width: 100px; + padding: 14px 8px !important; + text-align: center; + color: #fff; + background: rgba(#fff, 0); + opacity: 0.8; + transition: opacity 0.1s linear, background-color 0.1s linear; + } + + .nav-link:hover, + &.show .nav-link { + background: rgba(#fff, .15); + opacity: 1; + } + + .navbar-icon { + position: relative; + text-align: center; + vertical-align: middle; + font-size: 20px; + height: 22px; + line-height: 22px; + color: #fff; + } + + .navbar-label { + @include text-truncate(); + text-align: center; + font-size: 11px; + padding: 0; + padding-top: 0.25rem; + } + + .navbar-tool { + padding-left: 0.625rem !important; + padding-right: 0.625rem !important; + height: 72px; + display: flex; + + > * { + align-self: center; + } + } + } + + .reddot { + position: absolute; + width: 10px; + height: 10px; + border-radius: 50%; + background-color: $red; + color: #fff; + } +} + + + +// +// Forms +// -------------------------------------------------- + +legend { + font-family: $headings-font-family; + font-weight: $headings-font-weight; + font-size: 1.25rem; +} + +.col-form-label { + font-weight: $font-weight-medium; +} + +.ctl-label { + position: relative; + padding-right: 2.5rem; + display: inline-block; + + > label { + position: relative; + padding-right: 0; + margin-bottom: 0.4rem; + font-weight: $font-weight-medium; + } + + > .hint { + $hintColor: $gray-600; + display: block; + position: absolute; + cursor: default; + width: 2.5rem; + right: 0; + top: 0; + text-decoration: none; + outline: 0; + color: $hintColor; + transition: all .1s ease-in-out; + opacity: 0; + text-align: center; + /*@extend .col-form-label;*/ + + &:hover { + color: $gray-600; + } + + > i { + font-size: 1.25rem; + } + } + + &:hover > .hint { + opacity: 1; + } +} + + +// +// Multi-store settings +// -------------------------------------------------- + +.multi-store-setting-control > input[type=checkbox] { + position: relative; + margin: 0; + margin-top: $input-padding-y; +} + + +// +// AJAX Busy +// -------------------------------------------------- + +@keyframes loading { + from {left: 50%; width: 0; z-index:100;} + 50% {left: 0; width: 100%; z-index: 10;} + to {left: 0; width: 100%;} +} + +@keyframes loadingbar { + from {left: -200px; width: 30%;} + 50% {width: 30%;} + 70% {width: 70%;} + 80% { left: 50%;} + 95% {left: 120%;} + to {left: 100%;} +} + +#ajax-busy { + position: fixed; + opacity: 0; + left: 0; + right: 0; + top: 0; + width: 100%; + height: 3px; + z-index: 999; + background-color: #ffab40; + transition: opacity .05s ease-in; + + &.busy { + opacity: 1; + } + + &.busy > .bar { + content: ""; + display: inline; + position: absolute; + width: 0; + height: 100%; + left: 50%; + text-align: center; + animation: loading 1.5s cubic-bezier(0.445, 0.05, 0.55, 0.95) infinite; + } + > .bar:nth-child(1) { + background-color: #fff; + } + > .bar:nth-child(2) { + background-color: #3F51B5; + animation-delay: 0.5s; + } + > .bar:nth-child(3) { + background-color: #ffab40; + animation-delay: 1s; + } +} + +.loading-bar { + height: 4px; + width: 100%; + position: relative; + overflow: hidden; + background-color: #ddd; + + &:before { + display: block; + position: absolute; + content: ""; + left: -200px; + width: 200px; + height: 4px; + background-color: #2980b9; + animation: loadingbar 2s linear infinite; + } +} + + +// +// Apply grid layout to editor tables (.adminContent) +// -------------------------------------------------- + +.adminData { + select, + input, + textarea, + .uneditable-input, + .input-append, + .input-prepend { + margin-bottom: 0 !important; + } +} + +.adminContent { + display: block; + width: 100%; + margin-right: auto; + margin-left: auto; + padding-left: 0; + padding-right: 0; + border-collapse: initial; + + > tbody { + display: flex; + flex-flow: column; + border-collapse: initial; + } + + > tbody > tr, + > tr, + .adminRow { + display: flex; + flex-wrap: wrap; + margin-top: 0.4rem; + margin-bottom: 0.4rem; + margin-left: 0 !important; + margin-right: 0 !important; + } + + div.adminRow { + // div type rows do margin collapse, fix it. + margin-top: 0.8rem; + margin-bottom: 0.8rem; + } + + .adminTitle, + .adminData { + @include make-col-ready(); + padding-right: 0; + padding-left: 0; + } + + .adminTitle { + margin-bottom: 0; + } + + .adminData { + > .switch, + .multi-store-setting-control > .switch { + padding-top: calc(#{$input-padding-y} + #{$input-border-width}); + padding-bottom: calc(#{$input-padding-y} + #{$input-border-width}); + } + + > .switch { + margin-bottom: 0; + line-height: $input-line-height; + } + } + + > tbody > tr > td:only-child:not(.adminData):not(.adminTitle), + > tr > td:only-child:not(.adminData):not(.adminTitle) { + flex-grow: 1; + max-width: 100%; + } + + &.adminContent-boxed { + .adminTitle, .adminData { + padding-left: $grid-gutter-width / 2; + padding-right: $grid-gutter-width / 2; + } + } + + &:not(.adminContent-vertical) > tbody > tr:not(.wide), + &:not(.adminContent-vertical) > tr:not(.wide), + &:not(.adminContent-vertical) .adminRow:not(.wide) { + @include media-breakpoint-up(md) { + flex-wrap: nowrap; + + > .adminTitle { + flex: 0 0 33.333333%; + max-width: 33.333333%; + text-align: right; + padding-right: 0 !important; + + .ctl-label > label, + .ctl-label > .hint { + // actually is '.col-form-label', @extend is not possible here. + font-size: inherit; + line-height: $input-line-height; + padding-top: calc(#{$input-padding-y} + #{$input-border-width}); + padding-bottom: calc(#{$input-padding-y} + #{$input-border-width}); + margin-bottom: 0; + } + } + + > .adminData { + flex: 0 0 66.666667%; + max-width: 66.666667%; + padding-left: 0 !important; + + &:not(.wide) { + padding-right: 0 !important; + } + } + } + + @include media-breakpoint-up(lg) { + > .adminTitle { + flex-basis: 400px; + max-width: 40%; + } + + > .adminData { + flex-basis: 600px; + max-width: 60%; + + &.wide { + flex-grow: 1; + flex-basis: 0; + max-width: 100%; + } + } + } + } +} + +.adminSeparator { + width: 100%; +} + +.ie11 { + .adminContent, + .adminTitle, + .adminData { + display: table; + } + .adminTitle { + width: 25%; + } +} + +// +// Tables +// -------------------------------------------------- + +.admin-table { + $tbcolor: darken($table-border-color, 5%); + border: 1px solid $tbcolor; + + thead { + background-color: $table-head-bg; + } + + thead th { + line-height: 1.75rem; + border-bottom-width: 1px; + border-top-color: $tbcolor; + } + + td, th { + vertical-align: middle; + } + + .disabled { + color: $text-muted; + } + + .progress-info { + min-width: 260px; + } + + select, input { + margin-bottom: 0; + } +} + +// Handle action button toggling in tables & grids +.table tbody, +.t-grid > table tbody, +.t-grid > form > table tbody { + .t-last { + @include text-truncate(); + } + + .omega { + text-align: right; + } + + tr:not(.t-grid-edit-row):not(.active-row) { + > .t-last, + > .omega { + .t-button, .btn { + visibility: hidden; + } + } + + &:hover > .t-last, + &:hover > .omega { + .t-button, .btn { + visibility: visible; + } + } + } +} + + +// +// Locale Editor +// -------------------------------------------------- + +.locale-editor { + $border-color: $card-border-color; + + background-color: #fff; + border: 1px solid $border-color; + margin-bottom: 1rem; + border-radius: $border-radius; + + .nav-locales > .nav { + padding: 0.25rem 0.25rem 0 0.25rem; + border-bottom-color: $border-color; + background-color: $card-cap-bg; + border-top-left-radius: $border-radius; + border-top-right-radius: $border-radius; + + .nav-link { + color: #777; + font-weight: 600; + padding-top: 0.75rem; + padding-bottom: 0.75rem; + + > img { + opacity: 0.8; + } + } + + .nav-link:hover, + .nav-link.active { + color: inherit; + border-color: transparent; + + > img { + opacity: 1; + } + } + + .nav-link.active { + background-color: #fff; + border-color: $border-color; + border-bottom-color: #fff; + } + } + + > .nav-locales > .tab-content > .tab-pane > .adminContent { + @extend .adminContent-boxed; + } +} + + +// +// Thumb Zoomer +// -------------------------------------------------- + +.zoomable-thumb-container { + width: 50px; + height: 32px; + line-height: 32px; + display: inline-block; + text-align: center; + vertical-align: middle; + + > img { + max-height: 100%; + max-width: 100%; + } +} + +.zoomable-thumb { + max-width: 50px; + max-height: 32px; +} + +.zoomable-thumb-clone { + border: 1px solid #bbb; + background-color: #fff; + z-index: 100; +} + + +// +// Adapt class-less form elements to bs4 +// -------------------------------------------------- + +input[type=text]:not([class*="form-control"]), +input[type=password]:not([class*="form-control"]), +input[type=email]:not([class*="form-control"]), +input[type=number]:not([class*="form-control"]), +input[type=tel]:not([class*="form-control"]), +.t-numerictextbox > .t-input, +select:not([class*="form-control"]), +textarea:not([class*="form-control"]) { + @extend .form-control; +} + +// Reset BS' check/radio positioning in adminData-columns +.adminData > .form-check-input { + position: relative; + margin-left: 0; +} + + +// +// admin-config-group +// -------------------------------------------------- + +.admin-config-group { + display: block; + width: 100%; + + .title, + .small-title, + .head { + font-family: $headings-font-family; + font-weight: $headings-font-weight; + color: inherit; + border-bottom: 1px solid $gray-300; + } + .sub-title { + font-family: $font-family-base; + font-size: $font-size-base; + font-weight: 100; + } + .title, + .head { + font-size: $h5-font-size; + padding: 12px 0; + } + .head { + margin-bottom: 12px; + } + .title { + margin: 12px 0; + } + .small-title { + margin: 2px 0; + padding: 2px 0; + } +} + + +// +// Themes +// -------------------------------------------------- + +#theme-list { + .theme-thumbnail { + height: 220px; + background-size: cover; + background-repeat: no-repeat; + border-bottom: 1px solid rgba(#000, 0.1); + } + + .theme-broken-badge { + position: absolute; + background-color: $danger; + color: #fff; + right: 0; + bottom: 0; + padding: 0.25rem 0.75rem; + font-size: 1rem; + } +} + +.theme-editor { + .themevar-chain-info { + color: $gray-600; + margin-left: 0.75rem; + } +} + + +// +// Datetimepicker +// -------------------------------------------------- + +.datetimepicker-group { + .input-group-text { + padding-left: 0 !important; + padding-right: 0 !important; + width: $input-height; + + > i { + width: 100%; + } + } + + &.input-group-sm .input-group-text { + width: $input-height-sm; + } + + &.input-group-lg .input-group-text { + width: $input-height-lg; + } +} + + +// +// Dashboard +// -------------------------------------------------- + +.stats-today { + .stats-today-item { + padding: 0 1.5rem; + text-align: left; + border-right: 1px solid $gray-300; + + &:last-child { + padding-right: $grid-gutter-width/2; + border-right: none + } + } + + .stats-today-item-label { + font-weight: 400; + font-size: $font-size-xs; + margin-bottom: 0.75rem; + color: $gray-600; + } + + .stats-today-item-value { + font-size: 2rem; + //font-weight: 600; + line-height: 2rem; + color: $gray-500; + + &.active { + color: $gray-700; + } + } + + .stats-today-title { + line-height: 1; + } +} + +.marketplace-feed { + .marketplace-feed-title { + text-transform: uppercase; + font-size: 18px; + margin-bottom: 30px; + + i { + font-size: 20px; + } + } + + .marketplace-feed-item { + margin-bottom: 15px; + padding-bottom: 15px; + border-bottom: 1px solid #ddd; + + &:last-child { + border-bottom: none; + } + } + + .marketplace-feed-item-title { + margin-bottom: 4px; + } +} + + +// Plugins & Providers +// -------------------------------------------------------------- + +.sortable-grip { + left: 0; + width: 9px; + z-index: 1000; +} + +.module-list { + margin-bottom: 2rem; + margin-left: -$content-padding-x; + margin-right: -$content-padding-x; +} + +.module-list, +.module-item { + position: relative; +} + +.module-list > h3 { + color: $text-muted; + font-size: $h5-font-size; + margin-bottom: 1rem; + padding: 0 $content-padding-x; +} + +.module-item { + display: flex; + flex-wrap: wrap; + padding: 1rem 0; + border-top: 1px solid rgba(#000, 0.1); +} + +.module-item.inactive { + background-color: $gray-100; +} + +.module-item.inactive .module-icon { + opacity: 0.5; +} + +.module-icon { + width: 93px; + text-align: center; + flex: 0 0 auto; + max-width: none; + padding: 15px; + padding-left: $content-padding-x; + + .icon, i { + max-width: 48px; + } +} + +.module-data { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + padding: 0 15px; +} + +.module-item:last-child .module-data { + border-bottom: none; +} + +.module-col { +} + +.module-heading { + display: flex; + flex-wrap: nowrap; +} + +.module-title { + display: flex; + flex-wrap: nowrap; +} + +.module-name { + font-weight: $font-weight-medium; + font-size: $font-size-lg; +} + +.module-badges { + margin-left: 0.4rem; +} + +.module-info { + margin-top: 0.2rem; + font-size: $font-size-sm; +} + +.module-actions { + margin-left: auto; + transition: opacity 0.1s ease-in-out; + + &:not(.show) { + opacity: 0; + } + + .module-item:hover & { + opacity: 1; + } +} + +.module-info .attr:after { + display: inline-block; + content: '\00b7'; + padding: 0 5px; +} + +.module-info .attr:last-child:after { + display: none; +} + +.module-info .attr-value { + display: inline-block; + color: $text-muted; + padding-left: 0.15rem +} + +.module-description { + margin-bottom: 0.25rem; + color: $gray-600; +} + +table.payment-method-features td { + padding: 2px 4px; + font-size: $font-size-sm; +} + + +// +// Plugin configuration +// -------------------------------------------------- + +.section-header .options .plugin-actions { + display: inline-block; +} + +.plugin-config-container .plugin-actions { + display: none; +} + +.plugin-configuration { + display: flex; + flex-wrap: nowrap; + margin-top: 1rem; + + > div:first-child { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + } + > div:last-child { + flex: 0 0 auto; + width: auto; + max-width: 300px; + padding-left: 2rem; + } + + img { + max-width: 100%; + height: auto; + } +} + + +// Special icons +// -------------------------------------------------------------- + +.icon-active-true { + color: $green; + text-shadow: 0 -1px 0 #fff; + &::before { content: '\f00c'; } +} + +.icon-active-false { + color: $gray-400; + text-shadow: 0 -1px 0 #fff; + &::before { content: '\f068'; } +} + + +// Attributes +// -------------------------------------------------------------- + + +#order-form .checkout-attributes { + table { + //margin-left: auto + } + .attr-caption { + font-weight: $font-weight-medium; + text-align: right; + padding-right: 10px; + &:after { + content: ': ' + } + } + .attr-value { + text-align: left; + } +} + +/* Table formatted variant attributes */ +.product-attribute-table { + .column-name { + min-width: 80px; + padding: 2px 10px 2px 0; + vertical-align: top; + font-weight: $font-weight-medium; + } + + .column-value { + padding: 2px 0 2px 0; + vertical-align: top; + } +} + + + +// Warning panel message +// -------------------------------------------------------------- + +.warning-panel-message { + $bgColor: darken($primary, 15%); + z-index: 1031; + position: fixed; + bottom: 0; + left: 0; + right: 0; + margin: 0; + padding: 10px 20px; + @include gradient-y($bgColor, darken($bgColor, 12%)); + color: #fff; + text-shadow: 0px 1px 0px mix(#000, $bgColor, 30%); + border-top: 1px solid darken($bgColor, 10%); + text-align: center; + a { + color: inherit; + text-decoration: underline; + } + + .popup & { + display: none !important; + } +} + + +// +// Import column mapping +// -------------------------------------------------------------- + +@media (max-width: 1280px) { + #ImportColumnMappings .right-label, + .column-mapping .mapping-button-label, + .mapping-list-item .right-label { + display: none; + } +} + +.column-mapping .right-label, +.mapping-list-item .right-label { + float: right; + font-size: 0.9em; + padding-top: 2px; +} +.mapping-list-item { + @include text-truncate(); +} + +.column-mapping { + .column-one, .column-two { + width: 42%; + } + + .select2-container .select2-choice span { + margin-right: 12px; + white-space: nowrap; + } + + .mapping-edit { + white-space: nowrap; + + .select-column, .select-property { + width: 84%; + } + .select2-container { + float: left; + display: inline-block; + min-width: 220px; + margin-right: 4px; + } + } + + .mapping-item { + .item-inner { + white-space: nowrap; + cursor: pointer; + padding: 7px 0; + + .property-icon { + display: inline-block; + margin-right: 0; + } + .left-label { + display: inline-block; + } + .right-label { + margin-right: 20px; + } + } + .select2-selection__clear { + margin: 0 0 0 3px; + } + } +} + + +// +// Misc +// -------------------------------------------------------------- + +.color-container { + display: inline-block; + background-color: transparent; + padding-right: 0.5rem; + line-height: 1; + + .color { + width: $font-size-base; + height: $font-size-base; + vertical-align: bottom; + display: inline-block; + border: 1px solid #646464; + } +} + + +// +// HTML editors +// -------------------------------------------------------------- + +.cke_chrome.cke_focus { + border-color: $input-focus-border-color; + box-shadow: none; +} + +.note-editor-preview { + border: 1px solid $input-border-color; + min-height: 100px; + max-height: 350px; + overflow-x: hidden; + overflow-y: auto; + padding: $input-padding-y $input-padding-x; + cursor: default; + + &.empty { + min-height: initial; + //color: $text-muted; + } +} + +.note-editor { + border-color: $input-border-color !important; + border-radius: $input-border-radius !important; + + .modal-dialog { + @extend .modal-lg; + } + + &.focus { + border-color: $input-focus-border-color !important; + } + + .note-toolbar { + display: flex; + flex-wrap: wrap; + + > .note-btn-group { + border-right: 1px solid rgba(#000, 0.12); + padding-right: 5px; + } + + > .note-btn-group:last-of-type { + border-right-width: 0; + padding-right: 0; + //margin-left: auto !important; + } + + .dropdown-toggle:after { + margin-left: 0.5em; + opacity: 0.6; + } + + .dropdown-toggle:empty:after { + margin-left: 0; + } + + .note-para > .btn-group:last-child .dropdown-menu.show { + // Fix: the para popover button groups should not wrap + display: flex !important; + } + + .dropdown-style > .dropdown-item > * { + // Fix: Summernote does not remove the BS4 bottom margin of para style dropdown items + margin-bottom: 0; + } + + .note-color + .btn-group > .btn { + // Fix: the fontsize button width should not change (toolbar flickers) + padding-left: 0.5rem; + padding-right: 0.5rem; + min-width: 50px; + } + + /*.note-current-fontname, + .note-current-fontsize { + font-weight: $font-weight-normal; + }*/ + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Content/_telerik.scss b/src/Presentation/SmartStore.Web/Administration/Content/_telerik.scss new file mode 100644 index 0000000000..460e2fecf9 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Content/_telerik.scss @@ -0,0 +1,739 @@ +/// + +/* SmartStore theme for Telerik MVC Extensions */ + + +/* Variables +-------------------------------------------------------------- */ + +$grid-pager-bg: #f9fafa; + + +/* Mixins +-------------------------------------------------------------- */ + +@mixin custom-t-icon() { + position: relative; + background-clip: content-box; + box-sizing: content-box; + vertical-align: middle; + text-align: center; + font-size: $font-size-base; + line-height: $line-height-base; + background-image: none; + text-indent: 0; + color: transparent; + margin: 0; + padding: 0; + + &:hover:before { + color: $body-color; + } + + &:before { + position: absolute; + display: inline-block; + text-indent: 0; + @include fontawesome("\f013"); // cog + text-align: center; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + color: $gray-700; + } +} + + +/* Common +-------------------------------------------------------------- */ + +.t-button { + @extend .btn; +} + +.t-no-data { + color: $text-muted; +} + +.t-widget, .t-input { + border-color: $input-border-color; /*#a4abb2*/ + background-color: #fff; +} +.t-header, +.t-grid-header, +.t-toolbar, +.t-grouping-header, +.t-tooltip, +.t-grid-pager { + color: $gray-800; + background-color: $table-head-bg; + border-color: $table-border-color; +} +.t-menu-vertical, + .t-editor, +.t-tooltip, +.t-tabstrip { + background-position: 0 -48px; +} +.t-icon { + background-image: url('telerik/img/sprite.png'); +} +.t-editor .t-tool-icon { + background-image: url('telerik/img/editor.png'); +} +.t-picker-wrap { + padding-right: 23px; +} +.t-loading, +.t-widget .t-loading { + background: transparent url('telerik/img/loading.gif') no-repeat 0 0; +} +.t-widget, +.t-link:link, +.t-link:active, +.t-link:visited, +.t-popup, +.t-grid .t-header { + color: #3b3b3b; +} +.t-group { + background-color: #f1f2f3; + border-color: $dropdown-border-color; + &.t-popup { + background-color: #fff; + } +} +.t-popup, .t-menu .t-group { + box-shadow: $dropdown-box-shadow; +} +.t-content, +.t-editable-area { + border-color: #a4abb2; + background-color: #fff; +} +.t-separator { + border-color: #a4abb2; + background-color: #fff; +} +.t-alt { + //background-color: $table-accent-bg /* #f1f2f3 */; + background-color: none; +} +.t-state-default { + border-color: #a4abb2; +} +.t-active-filter { + background-color: #fff; +} +.t-state-hover, +.t-state-hover:hover { + background-color: #fae185; + border-color: #f3d64a; + color: #000; +} +.t-state-active, +.t-header .t-state-active { + background-color: #fff; + border-color: #a4abb2; +} +.t-state-selected { + background-color: rgba(#000, 0.05); +} +.t-state-focused { + background-color: #d3d6da; +} +.t-state-hover, .t-state-selected { + color: $gray-900; +} +.t-state-error, +.t-widget.input-validation-error, +.t-widget .input-validation-error { + border-color: #ff7c7c; + background-color: #ffe4e4; + color: #f20000; +} +.t-autocomplete { + background-color: #fff; +} +.t-toolbar, +.t-grouping-header, +.t-grid-pager, +.t-group-footer td, +.t-grid-footer, +.t-footer-template td, +.t-widget .t-status { + border-color: $input-border-color /*#a4abb2*/; + background-color: #f5f5f5; //#f1f2f3; +} +.t-widget .t-status { + background-color: transparent; +} +.t-grouping-row td { + background: url('telerik/img/sprite.png') repeat-x 0 -48px; +} +.t-grouping-header .t-group-indicator { + border-color: #a4abb2; +} +.t-grouping-dropclue { + background: url('telerik/img/sprite.png') no-repeat -48px -288px; +} +.t-grouping-row .t-group-cell, +.t-grouping-row p { + background: #fff; + font-weight: bold; + color: $gray-700; +} +.t-treeview .t-drop-clue { + background-image: url('telerik/img/sprite.png'); + background-position: 0 -358px; +} +.t-treeview .t-state-selected { + border-color: #f4b914; +} +.t-panelbar .t-link, +.t-panelbar .t-group, +.t-panelbar .t-content { + border-color: #a4abb2; +} +.t-other-month .t-link { + color: #a4abb2; +} + +.t-link:hover { + text-decoration: none !important; +} + + +body .t-content.t-state-active { + padding: 10px !important; +} + +.form-actions { + overflow: hidden; + padding-top: 1em; +} +.form-actions .t-button, +.form-actions .t-button button { + float: right; + margin-left: 10px; + padding: 2px 15px; +} + +.dialog-content-container { + margin: 6px 0 2px 0; + padding: 5px; +} + + +td.adminData .t-combobox { + width: 306px; +} + + +/* Buttons +-------------------------------------------------------------- */ + +button.t-button.t-state-disabled:hover, +a.t-button.t-state-disabled:hover, +.t-state-disabled .t-button:hover { + background: #d2d5d9; +} + +.t-state-disabled, +.t-state-disabled:hover, +.t-state-disabled .t-link, +.t-state-disabled .t-button { + border-color: #a4abb2; + color: #a4abb2; +} + + + +/* Numeric textbox +-------------------------------------------------------------- */ + +.t-numerictextbox { + display: block; + overflow: hidden; + position: relative; + width: 100%; + + &:after { + // mimic input group addon + position: absolute; + z-index: 2; + content: ' '; + display: block; + background: $input-group-addon-bg; + border-left: 1px solid $input-group-addon-border-color; + top: 1px; + right: 1px; + bottom: 1px; + left: auto; + width: $input-height; + + .t-grid-edit-row & { + width: 1.5rem; + } + } + + .t-input { + position: relative; + z-index: 1; + padding-right: $input-height-inner + 1rem !important; + + .t-grid-edit-row & { + padding-left: 0.5rem !important; + padding-right: 1.75rem !important; + } + } + + .t-formatted-value { + position: absolute; + overflow: hidden; + z-index: 2; + padding: $input-btn-padding-y $input-btn-padding-x; + font-size: $font-size-base; // Match inputs + font-weight: $font-weight-normal; + line-height: $input-btn-line-height; + color: $input-color; + + .t-grid-edit-row & { + padding-left: 0.5rem !important; + } + + &.t-state-empty { + color: $gray-600 !important; + } + } + + .t-input { + ~ .t-icon { + @include custom-t-icon(); + position: absolute; + display: block; + z-index: 3; + top: 0; + right: 0; + height: 50%; + width: calc(#{$input-height}); + + .t-grid-edit-row & { + width: 1.5rem; + } + + &:before { + content: "\f077"; + font-size: 10px; + } + + &.t-arrow-down:before { + content: "\f078"; // chevron-down + } + + &.t-arrow-down { + top: auto; + bottom: 0; + } + } + + &:disabled ~ .t-icon { + opacity: 0.5; + } + + &:not(:disabled) ~ .t-icon { + &:hover { + background-color: rgba(#000, 0.05); + } + + &:active { + background-color: rgba(#000, 0.08); + } + } + } +} + +.numerictextbox-group { + display: block; + position: relative; + + .numerictextbox-postfix { + display: block; + position: absolute; + z-index: 2; + top: 0; + color: $gray-600; + right: $input-height; + padding: $input-btn-padding-y $input-btn-padding-x; + font-size: $font-size-base; // Match inputs + line-height: $input-btn-line-height; + cursor: default; + user-select: none; + } +} + + + +/* Grid +-------------------------------------------------------------- */ + +.t-grid { + box-shadow: 1px 1px 3px rgba(#000, 0.1); + + .t-status-text { + color: $gray-600; + font-size: $font-size-sm; + } + + .t-button .fa { + margin-top: 0; + } + + .t-grid-header-wrap, .t-grid-footer-wrap { + border-color: #a4abb2; + } + + tbody .t-button { + @extend .btn-sm; + min-width: 92px; + + &.t-grid-update, &.t-grid-insert { + text-transform: capitalize; + @extend .btn-warning; + } + + &.t-grid-delete { + @extend .btn-danger; + } + + &:not(.t-grid-update):not(.t-grid-delete):not(.t-grid-insert) { + @extend .btn-secondary; + } + } + + form.t-grid-actions { + margin: 0; + } + + .text-box.single-line { + /* ... */ + } + + .t-filter-options { + border-radius: $border-radius; + padding: 0.5rem; + box-sizing: border-box; + //width: 200px; + min-width: 200px; + + .t-filter-help-text { + font-size: 12px; + padding: 0.25rem 0; + } + + .t-button { + @extend .btn-sm; + @extend .btn-secondary; + } + + .t-filter-button { + margin-top: 1rem; + } + + input[type=text], .t-filter-operator { + width: 100%; + padding: $input-padding-y-sm $input-padding-x-sm !important; + font-size: $font-size-sm !important; + line-height: $input-line-height-sm !important; + height: $input-height-sm !important; + @include border-radius($input-border-radius-sm); + } + } +} + +.t-grid > table, +.t-grid > form > table { + @extend .table; + margin-bottom: 0; + + td[align=center] { + text-align: center; + } + + .btn { + padding: $input-btn-padding-y-sm $input-btn-padding-x-sm; + font-size: $font-size-sm; + + .fa { + font-size: $font-size-sm; + } + } + + td, th { + vertical-align: middle; + padding-left: 0.4rem; + padding-right: 0.4rem; + border-left-width: 0 !important; + border-right-width: 0 !important; + border-color: $table-border-color; + line-height: $line-height-base; + + &:first-child { + padding-left: 0.75rem; + } + + &:last-child { + padding-left: 0.75rem; + } + } + + .t-grid-edit-row td { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + + .form-check-input { + position: relative; + margin: 0; + padding: 0; + } + } +} + + +.t-grid-edit-row { + .text-box, + .t-numerictextbox { + // overwrite telerik defaults + width: 100%; + min-width: 100%; + max-width: 100%; + margin: 0; + } +} + + +.t-grid-edit-cell { + padding: 6px; +} + +.t-grouping-header { + text-shadow: none; + padding: 0.6em; +} + + +.t-grid-toolbar { + padding: 0; + background: $grid-pager-bg; + position: relative; + display: flex; + + &:empty { + border-width: 0 !important; + } + + .btn, + .t-button { + display: block; + border-radius: 0 !important; + margin: 0; + border: 0; + border-left: 1px solid rgba(#000, 0.1); + padding-top: 1rem; + padding-bottom: 1rem; + background-color: transparent; + color: #515151; + font-size: $font-size-sm; + transition: $btn-transition; + box-shadow: none; + + &:first-child { + border-left-width: 0; + } + + &:hover { + background-color: $gray-200; + } + + &:active { + background-color: $gray-300; + box-shadow: $btn-active-box-shadow !important; + } + + &.t-grid-add, + &.t-button-primary { + &:hover { + background-color: darken($primary, 7.5%); + color: #fff; + } + + &:active { + background-color: darken($primary, 10%); + color: #fff; + } + } + + &.t-grid-save-changes { + &:hover { + background-color: darken($warning, 7.5%); + color: #fff; + } + + &:active { + background-color: darken($warning, 10%); + color: #fff; + } + } + + &.t-grid-cancel-changes { + &:hover { + background-color: $danger; + color: #fff; + } + + &:active { + background-color: darken($danger, 10%); + color: #fff; + } + } + } + /*.t-button, + .btn:not(.btn-primary):not(.btn-warning):not(.btn-danger):not(.btn-secondary) { + @include button-variant($warning, $warning); + }*/ +} + +.t-grid-filter { + position: absolute; + display: flex; + right: 0; + top: 0; + height: 100%; + width: 28px; + margin: 0; + padding: $gray-100 0; + + .t-icon { + @include custom-t-icon(); + + &:before { + content: "\f0b0"; + } + } +} + +.t-grid-header .t-header { + font-weight: 600 !important; + padding: 0 !important; + position: relative; + + .t-link { + margin: 0; + padding: 0.75em 0.4rem; + height: auto; + line-height: 1.75rem; + } + + &:first-child .t-link { + padding-left: 0.75em; + } + + &:last-child .t-link { + padding-right: 0.75em; + } + + a.t-link:hover { + background-color: #fff2cf; + } + + .t-arrow-up, + .t-arrow-down { + margin-left: 0.25rem; + } +} + + +/* Grid pager +-------------------------------------------------------------- */ + +.t-grid-pager { + background: $grid-pager-bg; + padding-top: 0; + padding-bottom: 0; + line-height: $line-height-base; + + .t-numeric, + .t-status .t-icon { + margin: 0; + } + + .t-status { + margin: 0; + padding: 0; + padding-right: 0.6em; + padding-bottom: 7px; + padding-top: 9px; + height: auto; + } + + .t-refresh { + height: 16px; + } + + .t-pager .t-link span { + vertical-align: sub; + } + + .t-refresh, + .t-link, + .t-status-text, + .t-state-active { + padding-top: 0.6rem; + padding-bottom: 0.6rem; + font-size: $font-size-sm; + line-height: $line-height-base; + background-color: transparent; + text-shadow: none !important; + vertical-align: middle !important; + margin: 0; + } + + .t-refresh, + .t-link, + .t-state-active { + padding-left: 0.4rem; + padding-right: 0.4rem; + transition: background-color 0.06s ease-in-out; + border-radius: 0; + color: inherit; + } + + .t-link, + .t-state-active { + min-width: 1.2rem; + text-align: center; + border: 1px solid transparent; + border-top-width: 0; + border-bottom-width: 0; + } + + .t-link.t-state-hover, + .t-link.t-state-hover:hover, + .t-link:hover { + background-color: rgba(#000, 0.08); + //border-width: 0; + color: inherit; + } + + .t-state-active { + /*background-color: rgba(#000, 0.5); + color: #fff;*/ + background-color: #fff; + border-color: rgba(#000, 0.15); + //box-shadow: inset 1px 1px 2px rgba(0, 0, 0, 0.06); + } + + .t-status-text { + padding-right: 0.75rem; + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Content/_variables.scss b/src/Presentation/SmartStore.Web/Administration/Content/_variables.scss new file mode 100644 index 0000000000..db5222eea3 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Content/_variables.scss @@ -0,0 +1,193 @@ +/// + +// +// SmartStore CI colors +// -------------------------------------------------- + +$sm-orange1: #ee9B00; +$sm-orange2: #e77c00; +$sm-green1: #44b284; +$sm-bluegreen: #2c8d8a; +$sm-red1: #ee1111; +$sm-red2: #dc3000; +$sm-red3: #db004f; +$sm-blue: #37a0e6; +$sm-blue2: #18509f; +$sm-violet: #55237d; +$sm-gray1: #dadada; +$sm-gray2: #bcbcbc; +$sm-gray3: #878787; +$sm-gray4: #4a4a49; + + +// +// Backend specific variables +// -------------------------------------------------- + +$content-padding-x: 30px; +$content-padding-y: 15px; + + +// +// Shared variables +// -------------------------------------------------- + +$alert-info-is-primary: true; +$enable-lightbtn-tweak: true; +$enable-social-buttons: false; + + +// +// BS4 variables +// -------------------------------------------------- + +$enable-shadows: true; +$enable-gradients: false; + +$gray-100: #f8f9fa; +$gray-200: #e9ecef; +$gray-300: #dee2e6; +$gray-400: #ced4da; +$gray-500: #b0bac3; +$gray-600: #8d9ba9; +$gray-700: #596167; +$gray-800: #393f46; +$gray-900: #22262a; + +$text-muted: $gray-600; + +$blue: $sm-blue; +$indigo: $sm-blue2; +$purple: $sm-violet; +$pink: #e91e63; // Pink 500 +$red: $sm-red2; +$orange: $sm-orange1; +$yellow: #fdd835; // Yellow 600 +$green: $sm-green1; +$teal: #009688; // Teal 500 +$cyan: #00BCD4; // Cyan 500 +$bluegrey: #78909c; // Bluegrey 500 + +$primary: $blue; +$secondary: lighten($gray-200, 2%); +$success: $green; +$info: $sm-bluegreen; +$warning: $orange; +$danger: $red; +$light: $gray-100; +$dark: $sm-red3; + +$theme-colors: ( + "gray": $gray-700, + "primary-dark": $sm-blue2, + "warning-dark": $sm-orange2, + "danger-dark": $sm-red2 +); + +$body-color: $gray-800; +$body-bg: #fff; +$link-color: desaturate(darken($primary, 12%), 0%); //#0277bb; + +// Set a specific jump point for requesting color jumps +$theme-color-interval: 8%; + +// The yiq lightness value that determines when the lightness of color changes from "dark" to "light". Acceptable values are between 0 and 255. +$yiq-contrasted-threshold: 164; + +// Customize the light and dark text colors for use in our YIQ color contrast function. +$yiq-text-dark: $gray-800; +$yiq-text-light: #fff; + +$font-size-base: 0.875rem; +$font-size-lg: 1rem; +$font-size-sm: 0.8125rem; + +$h1-font-size: 2.5rem; +$h2-font-size: 2rem; +$h3-font-size: 1.75rem; +$h4-font-size: 1.5rem; +$h5-font-size: 1.25rem; +$h6-font-size: 1rem; + +$small-font-size: 90%; + +$headings-font-weight: 400; + +$table-cell-padding: .75rem; +$table-cell-padding-sm: .3rem; +$table-border-color: $gray-300; +$table-head-bg: #f6f8fa; +$table-head-color: $gray-700; +$table-accent-bg: rgba(#000, .015); + +$input-btn-focus-width: .25rem; +$input-btn-focus-color: rgba($sm-blue2, .25); +$input-btn-focus-box-shadow: 0 0 0 $input-btn-focus-width $input-btn-focus-color; +$input-btn-padding-x: 1rem; +$input-btn-padding-y: 0.5625rem; +$input-btn-padding-x-sm: 0.75rem; +$input-btn-padding-y-sm: 0.375rem; +$input-btn-padding-x-lg: 1.5rem; +$input-btn-padding-y-lg: 0.75rem; +$input-btn-line-height: 1.5; +$input-box-shadow: none; +$input-focus-border-color: lighten($sm-blue2, 30%); +$input-focus-box-shadow: none; +$input-border-radius: 0; +$input-border-radius-lg: 0; +$input-border-radius-sm: 0; +$input-transition: none; +$input-group-addon-bg: $gray-100; + +$btn-font-weight: 600; +$btn-focus-width: 0; //$input-btn-focus-width; +$btn-box-shadow: inset 0 0 0 rgba(#fff, 0.15), 0 0 0 rgba(#000, 0.075); +$btn-focus-box-shadow: $input-btn-focus-box-shadow; +//$btn-active-box-shadow: inset 0 2px 3px rgba(#000, .125); +$btn-transition: background-color .05s ease-in-out, border-color .05s ease-in-out, box-shadow .05s ease-in-out; +$btn-border-radius: 0; //.125rem; +$btn-border-radius-lg: 0; //.1875rem; +$btn-border-radius-sm: 0; //.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-link-color: $body-color; //$gray-800; +$dropdown-link-hover-color: #fff; //darken($gray-800, 5%); +$dropdown-link-hover-bg: $indigo; +$dropdown-link-active-color: $dropdown-link-hover-color; +$dropdown-link-active-bg: lighten($dropdown-link-hover-bg, 5%); +$dropdown-item-padding-y: 0.625rem; // 0.5rem; +$dropdown-item-padding-x: 1.5rem; +$dropdown-header-color: $text-muted; +$dropdown-link-disabled-color: $gray-500; + +$popover-box-shadow: $dropdown-box-shadow; +$popover-border-color: $dropdown-border-color; + +$alert-padding-y: 1rem; +$alert-border-radius: 0; + +$nav-tabs-border-color: #e1e4e8; +$nav-tabs-link-active-color: $gray-900; + +$component-active-bg: lighten($primary, 36%); +$component-active-color: inherit; + + +/*$custom-control-indicator-bg: $gray-200; +$custom-control-indicator-disabled-bg: $gray-100; +$custom-control-indicator-checked-color: #fff; +$custom-control-indicator-checked-bg: $primary; +$custom-control-indicator-active-color: #fff; +$custom-control-indicator-active-bg: $component-active-bg; +$custom-checkbox-indicator-indeterminate-bg:$primary;*/ + +$custom-control-indicator-bg: $gray-200; +$custom-control-indicator-disabled-bg: $gray-100; +$custom-control-indicator-checked-color: #fff; +$custom-control-indicator-checked-bg: $warning; +$custom-control-indicator-active-color: #fff; +$custom-control-indicator-active-bg: lighten($warning, 35%); +$custom-checkbox-indicator-indeterminate-bg: $warning; +$custom-control-indicator-focus-box-shadow: 0 0 0 1px $body-bg, 0 0 0 $input-btn-focus-width rgba($warning, .25); \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Content/admin.less b/src/Presentation/SmartStore.Web/Administration/Content/admin.less deleted file mode 100644 index 09acd137e0..0000000000 --- a/src/Presentation/SmartStore.Web/Administration/Content/admin.less +++ /dev/null @@ -1,1818 +0,0 @@ - -/* General --------------------------------------------------------------- */ - -a:active, -a:hover { - outline: 0; -} - -.btn { - text-transform: uppercase; -} - -.dropdown-menu .fa, -.btn > .fa { - font-size: 14px; -} - -.dropdown-menu { - .border-radius(0) !important; - text-shadow: none; -} - -.dropdown-menu li > a { - padding: 8px 30px 8px 20px; - text-shadow: none; -} - -.dropdown-menu li > a > .fa { - color: #6d6d6d; -} - -.dropdown-menu li > a:hover > .fa { - color: #444; -} - -.dropdown-menu li > a:hover, -.dropdown-menu li > a:focus, -.dropdown-submenu:hover > a { - background-image: none; - filter: initial; - background-color: @dropdownLinkBackgroundHover; -} - -.alert { - padding-top: 14px; - padding-bottom: 14px; -} - -.fw-600 { - font-weight: 600; -} - -.fw-bold { - font-weight: bold; -} - -/* Page --------------------------------------------------------------- */ - -#page { - position: relative; - margin: 0; - padding: 0; -} - -body:not(.popup) #page { - min-width: 900px; -} - - - -/* Warning panel message --------------------------------------------------------------- */ -.warning-panel-message { - @bgColor: darken(@btnWarningBackground, 15%); - z-index: 1031; - position: fixed; - bottom: 0; - left: 0; - right: 0; - margin: 0; - padding: 10px 20px; - #gradient > .vertical(@bgColor, darken(@bgColor, 12%)); - color: #fff; - text-shadow: 0px 1px 0px mix(@black, @bgColor, 30%); - border-top: 1px solid darken(@bgColor, 10%); - text-align: center; - a { - color: inherit; - text-decoration: underline; - } - - .popup & { - display: none !important; - } -} - - -/* Header --------------------------------------------------------------- */ - -header { - background-color: @navbarInverseBackground; - height: 70px; - padding-bottom: 50px; -} - -.popup header { - display: none; -} - - - -/* Navbar overrides --------------------------------------------------------------- */ -.navbar .nav { - font-family: Tahoma, @baseFontFamily; -} - -.navbar.navbar-inverse .navbar-inner { - margin: 0; - padding: 0; - border: 0; - background: @navbarInverseBackground; - //background-image: none; - .box-shadow(none); - .border-radius(0); -} - -.navbar.navbar-inverse .navbar-inner .brand { - margin: 0; - padding-left: 30px; -} - -#navbar-tools { - - .active-tool() { - .box-shadow(~'inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05)'); - } - - position: absolute; - right: 30px; - top: 10px; - margin: 0; - - .btn, - .btn-group { - margin-top: 0; - } - - .open .navbar-tool { - .active-tool(); - } - - .toppad { - margin-top: 8px; - } - - .navbar-tool { - color: rgba(255,255,255, .87); - border: none; - background: transparent; - .box-shadow(none); - padding: 8px 6px; - .transition(color 0.1s linear); - - &:hover { - color: #fff; - } - - &:active { - .active-tool(); - } - - > .fa { - font-size: 20px; - } - - span { - display: inline-block; - //padding-bottom: 10px; - margin-left: 3px; - vertical-align: top; - } - - } -} - -#current-user { - margin-left: 20px; -} - -#current-user span { - .text-overflow(); - max-width: 100px; - display: inline-block; - vertical-align: text-top; -} - - -/* Menu --------------------------------------------------------------- */ - -#navbar .dropdown-menu { - margin-top: -2px; - font-family: @baseFontFamily; -} - -#navbar .dropdown-menu .fa { - margin-right: 6px; - font-size: 17px; - vertical-align: bottom; -} - -#navbar .nav-header { - text-shadow: none; - padding-left: 20px; -} - -#navbar .dropdown-submenu > a:after { - margin-right: -20px; -} - -#navbar .dropdown-submenu:hover > a:after { - border-left-color: #ccc; -} - -#navbar .navbar-nav > li > a { - max-height: @navbarHeight - 12; - min-width: 40px; - max-width: 80px; - height: auto; - padding: 12px 6px !important; - text-align: center; - background: transparent; - background: rgba(255,255,255, 0); - .opacity(90); - .transition(~'opacity 0.1s linear, background-color 0.1s linear'); - - .fa { - position: relative; - text-align: center; - vertical-align: middle; - width: 22px; - height: 22px; - font-size: 19px; - line-height: 22px; - color: #fff; - padding: 2px 0; - } - - .navbar-label { - display: block; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - text-align: center; - font-size: 11px; - line-height: 20px; - margin: 0; - } -} - -#navbar .navbar-nav > li:hover > a { - background: rgba(255,255,255, .15); - .opacity(100); -} - -.reddot { - position: absolute; - width: 10px; - height: 10px; - border-radius: 50%; - background-color: @red; - color: #fff; -} - -/* SM additions --------------------------------------------------------------- */ - -#ajax-busy { - position: fixed; - opacity: 0; - left: 0; - right: 0; - top: 0; - width: 100%; - height: 3px; - z-index: 20; - background-color: #ffab40; - .transition(opacity .05s ease-in); -} -#ajax-busy.busy { - opacity: 1; -} -#ajax-busy.busy > .bar { - content: ""; - display: inline; - position: absolute; - width: 0; - height: 100%; - left: 50%; - text-align: center; - animation: loading 1.5s cubic-bezier(0.445, 0.05, 0.55, 0.95) infinite; - -webkit-animation: loading 1.5s cubic-bezier(0.445, 0.05, 0.55, 0.95) infinite; -} -#ajax-busy > .bar:nth-child(1) { - background-color: #fff; -} -#ajax-busy > .bar:nth-child(2) { - background-color: #3F51B5; - animation-delay: 0.5s; - -webkit-animation-delay: 0.5s; -} -#ajax-busy > .bar:nth-child(3) { - background-color: #ffab40; - animation-delay: 1s; - -webkit-animation-delay: 1s; -} -@keyframes loading { - from {left: 50%; width: 0; z-index:100;} - 50% {left: 0; width: 100%; z-index: 10;} - to {left: 0; width: 100%;} -} -@-webkit-keyframes loading { - from {left: 50%; width: 0; z-index:100;} - 50% {left: 0; width: 100%; z-index: 10;} - to {left: 0; width: 100%;} -} - -.loading-bar { - height: 4px; - width: 100%; - position: relative; - overflow: hidden; - background-color: #ddd; -} -.loading-bar:before { - display: block; - position: absolute; - content: ""; - left: -200px; - width: 200px; - height: 4px; - background-color: #2980b9; - animation: loadingbar 2s linear infinite; - -webkit-animation: loadingbar 2s linear infinite; -} -@keyframes loadingbar { - from {left: -200px; width: 30%;} - 50% {width: 30%;} - 70% {width: 70%;} - 80% { left: 50%;} - 95% {left: 120%;} - to {left: 100%;} -} -@-webkit-keyframes loadingbar { - from {left: -200px; width: 30%;} - 50% {width: 30%;} - 70% {width: 70%;} - 80% { left: 50%;} - 95% {left: 120%;} - to {left: 100%;} -} - -#content { - position: relative; - height: auto; - margin: 0 30px; - margin-top: -50px; - background-color: #fff; - padding: 15px 30px 60px 30px; - .box-shadow(~'0 0 6px rgba(0,0,0, 0.2)'); -} - -.popup #content { - margin: 0; - border: 0; - .box-shadow(0); -} - -td.adminData { - select, - input, - textarea, - .uneditable-input, - .input-append, - .input-prepend { - margin-bottom: 0 !important; - } -} - -td.adminData > input[type="radio"], -td.adminData > input[type="checkbox"] { - margin-top: -1px; -} - -.admin-config-group { - .title, - .small-title, - .head { - font-family: @headingsFontFamily; - font-weight: @headingsFontWeight; - color: inherit; - border-bottom: 1px solid #ddd; - } - .sub-title { - font-family: @baseFontFamily; - font-size: @baseFontSize; - font-weight: normal; - } - .title, - .head { - font-size: @fontSizeLarge; - padding: 12px 0; - } - .head { - margin-bottom: 12px; - } - .title { - margin: 12px 0; - } - .small-title { - margin: 2px 0; - padding: 2px 0; - } -} - - -/* "select2" ui tweaks for admin --------------------------------------------------------------- */ - -.select2-container:not(.autowidth) { - min-width: 314px; /* ensures more clean look & feel */ -} - -.select2-image-item { - img { - margin-bottom: 3px; - } - span:not(.select2-icon) { - padding-left: 5px; - } - .icon-container { - float: left; - display: inline-block; - width: 16px; - min-width: 16px; - max-width: 16px; - } -} - -/* select2 with large, templated items --------------------------------------------------------------- */ -.large-select2 { - .select2-choice { - height: 42px; - min-height: 42px; - padding-top: 4px; - - > div > b { - padding-top: 10px; - } - } -} - -.large-select2-item { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - height: 38px; - - img { - margin-right: 12px; - vertical-align: middle; - text-align: center; - max-height: 100%; - } - > div { - margin-right: 0px !important; - display: inline-block; - vertical-align: top; - line-height: normal; - } - > div > span { - display: block; - padding-bottom: 1px; - } -} - - -/* SM Telerik tweaks & overrides --------------------------------------------------------------- */ - -.t-button { - .btn(); - &:hover { border-color: rgba(0,0,0, 0.2) } -} - -.t-grid .t-status-text { - color: #aaa; -} - -.t-grid .table { - margin-bottom: 0; -} -.t-grid .table td[align=center] { - text-align: center; -} - -.t-grid *:not(.btn-group) > .btn { - margin: 0.2em; -} - -.t-grid .t-button, -.t-edit-form .t-button, -.t-filter-options { - padding: 2px 4px; - font-size: @fontSizeSmall; - .border-radius(2px); -} - -.t-filter-options { - font-size: @fontSizeSmall; -} - -.t-grid .btn { - padding: @paddingSmall; -} -.t-grid .btn, -.t-grid .btn .fa { - font-size: @fontSizeSmall; -} -.t-grid .t-button .fa { - margin-top: 0; -} - -.t-grid-toolbar .btn { - text-shadow: none; - text-decoration: none; - font-size: 13px; -} - -.t-grid-filter { - margin: 0; - padding: 0; - position: absolute; - right: 4px; - top: 50%; - margin-top: -8px; -} - -.t-link:hover { - text-decoration: none !important; -} - -.t-grid-header .t-header { - font-size: 12px; - text-transform: uppercase; - font-weight: bold !important; - padding: 0 !important; - position: relative; -} - -.t-grid-header .t-header .t-link { - margin: 0; - padding: 0.6em; -} - -.t-grid-pager, -.t-grid-toolbar { - background: #f7f7f7; - text-shadow: none !important; - padding-top: .4em !important; - padding-bottom: .4em !important; -} - -.t-grid tbody .t-last { - .text-overflow(); -} - -.t-grid form.t-grid-actions { - margin: 0; -} - -.t-grid-edit-row .t-numerictextbox .t-arrow-up { - margin-left: -16px; -} - -body .t-content.t-state-active { - padding: 10px !important; -} - -.form-actions { - overflow: hidden; - padding-top: 1em; -} -.form-actions .t-button, .form-actions .t-button button { - float: right; - margin-left: 10px; - padding: 2px 15px; -} - -.dialog-content-container { - margin: 6px 0 2px 0; - padding: 5px; -} - -table .multiline-grid { - td { - vertical-align: top !important; - } -} - - - -/* HTML editor (tiny mce) --------------------------------------------------------------- */ - -.mce_pdw_toggle.mceButtonActive img { - .rotate(180deg); -} - - -body { - //background-attachment: fixed; - //#gradient > .vertical-three-colors(@infoBackground, @infoBackground, 50%, #fff); - background-color: #f5f5f5; -} -hr { - border-bottom: 1px solid #CCC; - color: #FFF; -} -label.forcheckbox { - margin: 0 0 0 .4em; - display: inline; -} - -/* SECTION HEADERS */ -.section-title { - border-bottom: solid 3px #dfdfdf; - padding-bottom: 1px; - margin-bottom: 10px; - color: #009FFF; - font-size: 14px; - font-weight: 700; - vertical-align: bottom; -} -.section-title img { - vertical-align: middle; - padding-bottom: 2px; -} -.section-header { - position: absolute; - left: 0; - top: 0; - right: 0; - padding: 10px 30px; - .transition(background-color 0.1s linear); - - - .title { - position: relative; - float: left; - left: 5px; - font-family: @pageTitleFontFamily; - font-weight: @pageTitleFontWeight; - color: @pageTitleColor; - font-size: 20px; - line-height: 32px; - height: 32px; - /*vertical-align: bottom;*/ - vertical-align: middle; - .text-overflow(); - text-shadow: none; - - .fa { - font-size: 28px; - //font-size: 32px; - vertical-align: text-top; - } - - img { - max-height: 32px; - max-width: 120px; - margin-right: 5px; - vertical-align: top; - } - - a { - text-decoration: none; - font-family: @sansFontFamily; - font-size: 12px; - font-weight: normal; - } - } // .title - - .options { - position: absolute; - right: 30px; - top: 10px; - &.btn-toolbar { margin: 0; } - } - - &.sticky { - position: fixed; - z-index: 10; - left: 30px; - right: 30px; - top: 0; - background: #f7f7f7; - - text-shadow: none; - border-bottom: 1px solid #ccc; - - .title { - text-shadow: none; - left: 5px; - } - - .options { - right: 30px; - } - } -} - -/* ABOUT PAGE */ -.about-page { - max-width: 1000px; - //text-align: center; - - .work-list { - list-style-type: none; - padding: 0; - margin: 20px 0 0 0; - - li { - line-height: normal; - margin: 10px 0 15px 0; - - .name, .name a:hover { - font-weight: bold; - } - } - } -} - -.popup .section-header.sticky { - left: 0; - right: 0; - top: 0; - .options { right: 30px } -} - -/* MULTI-STORE */ -*.multi-store-scope { - label.checkbox { - margin: 4px 0 1px 3px; - - .hint { - .smaller(); - } - } -} - -/* TABLES */ -td { - vertical-align: middle; -} -td.fieldname { - font-size: 12px; -} -.table-container { -} -.table-container .row { -} -.table-container .row .item-name { - padding: 3px 10px 0 0; - display: block; - white-space: nowrap; - padding-right: 10px; - vertical-align: middle; -} -.table-container .row .item-value { - padding: 3px 0 0; - vertical-align: middle; -} -table.adminContent { - border-collapse: collapse; - margin: 0; - width: 100%; - vertical-align: middle; - text-align: left; -} -td.adminTitle { - padding: 4px; - width: 300px; -} -.modal td.adminTitle { - width: auto; -} -td.adminData { - padding: 4px; -} -td.adminTitle > .ctl-label { - position: relative; - text-align: right; - vertical-align: middle; - - label { - display: inline-block; - padding-right: 20px; - margin-bottom: 0; - } - - a.hint { - @hintColor: #999; //#21759b - position: absolute; - width: 16px; - height: 16px; - right: 0; - top: 50%; - margin-top: -8px; - text-decoration: none; - outline: 0; - color: @hintColor; - .transition(all .1s linear); - .opacity(0); - &:hover { - color: darken(@hintColor, 25%) - } - .fa { - font-size: 16px; - } - } -} -tr.adminGroup { - > td { - padding-top: 18px; - } - - label { - padding-bottom: 3px; - text-decoration: underline; - } -} - -td.adminTitle:hover a.hint { - .opacity(100); -} - -.well td.adminTitle { - width: 280px; -} -.well.well-small td.adminTitle { - width: 290px; -} - -td.adminData { - text-align: left; - vertical-align: middle; -} -td.adminData .text-box.single-line { - width: 300px; -} -td.adminData input[type=text].input-large, -td.adminData textarea.input-large, -td.adminData .control-large { - width: 500px; -} -td.adminData input[type=text].input-xlarge, -td.adminData textarea.input-xlarge, -td.adminData .control-xlarge { - width: 800px; -} - - -.t-grid .text-box.single-line { - /* ... */ -} -td.adminData textarea { - width: 300px; -} -td.adminData .t-combobox { - width: 306px; -} - -tr.adminSeparator hr, -td.adminSeparator hr { - border: medium none; - background-color: #ddd; - color: #ddd; - height: 1px; - margin-top: 12px; - margin-bottom: 12px; - padding: 0; -} - -.section-header .options .plugin-actions { - display: inline-block; -} - -.plugin-config-container .plugin-actions { - display: none; -} - -.plugin-configuration { - display: table; - width: 100%; - clear: both; - - > div { - display: table-cell; - vertical-align: top; - } - > div:first-child { - width: 80%; - } - > div:last-child { - float: right; - padding-left: 50px; - } -} - -.admin-table { - thead th { - text-transform: uppercase; - } - th, td { - padding: 12px; - } - .disabled { - .muted(); - } - .progress-info { - min-width: 260px; - } - select, input { - margin-bottom: 0; - } - .control-large { - width: 500px; - min-width: 500px; - } -} - -/* SERVER CONTROLS */ -label { - padding-left: 3px; -} - -/* MISC. CLASSES */ -.clear { - border: medium none; - clear: both; - float: none; - font-size: 0; - height: 0; - line-height: 0; -} - -.mr8 { margin-right: 8px; } -.ml4 { margin-left: 4px; } - -/* MASTER HEADER */ -.header-menu, -.status-bar { - display: none; -} - -.header { - height: 70px; - width: 100%; - @headerColor: #2486C3; - #gradient > .vertical(@headerColor, lighten(@headerColor, 16%)); - position: relative; - border-top: 3px solid darken(@headerColor, 4%); - border-bottom: 3px solid lighten(@headerColor, 13%); - .box-shadow(0 1px 0 #fff); - - & a { - text-decoration: none; - } -} - -a img { - border: 0; -} - -legend { - font-family: @headingsFontFamily; - font-weight: @headingsFontWeight; - font-size: 18px; -} - -/* MASTER CONTENT */ -.cph { - padding-top: 55px; -} - -/* WIDGETS */ -.widget { - position: relative; - clear: both; - width: auto; - margin-bottom: 20px; - overflow: hidden; -} - -.widget-table { - .box-shadow(1px 1px 2px rgba(0,0,0, 0.07)); -} - -.widget-header { - position: relative; - height: 40px; - line-height: 40px; - border: 1px solid #d5d5d5; - .background-clip(padding-box); - #gradient > .vertical(#fafafa, #e9e9e9); - - .fa { - display: inline-block; - margin-left: 13px; - margin-right: -2px; - font-size: 16px; - vertical-align: middle; - } - - h3 { - position: relative; - top: 2px; - left: 10px; - display: inline-block; - margin-right: 3em; - - font-family: inherit; - color: #444; - font-size: 14px; - line-height: 18px; - font-weight: 400; - text-transform: uppercase; - } -} - -.widget-content { - padding: 20px 15px 15px; - background: #fff; - border: 1px solid #d5d5d5; - position: relative; - zoom: 1; -} - -.widget-header + .widget-content { - border-top: none; -} - -.widget-nopad .widget-content { - padding: 0; -} - -/* Widget Content Clearfix */ -.widget-content:before, -.widget-content:after { - content:""; - display:table; -} - -.widget-content:after { - clear:both; -} - -/* Widget Table */ -.widget-table .widget-content { - padding: 0; -} - -.widget-table .table { - margin-bottom: 0; - border: none; - table-layout: fixed; -} - -.widget-table .table td, -.widget-table .table th { - .text-overflow(); -} - -.widget-table .table tr td:first-child { - border-left: none; -} - -.widget-table .table tr th:first-child { - border-left: none; - width: 50%; -} - - -/* DASHBOARD */ - - -.stats-today .widget-content { - background: transparent; - border: none; - padding: 0; -} - -.stats-today { - position: relative; -} - -.stats-today-title, -.stats-today-item-value { - font-family: inherit; - font-weight: 400; - color: inherit; - font-size: 30px; - line-height: 36px; -} - -.stats-today-title { - position: absolute; - font-size: 50px; - font-family: @pageTitleFontFamily; - font-weight: @pageTitleFontWeight; -} - -.stats-today-item { - float: right; - padding: 0 24px; - text-align: center; - border-right: 1px solid #f2f2f2; - &.omega { padding-right: 0; border-right: none } -} - -.stats-today-item-label { - font-weight: bold; - font-size: 12px; - margin-bottom: 8px; - color: #aaa; -} - -.stats-today-item-value { - color: #333; -} - -.marketplace-feed-title { - text-transform: uppercase; - font-size: 18px; - margin-bottom: 30px; - .fa { font-size: 20px; } -} - -.marketplace-feed-item { - margin-bottom: 15px; - padding-bottom: 15px; - border-bottom: 1px solid #ddd; - &:last-child { border-bottom: none; } -} - -.marketplace-feed-item-title { - margin-bottom: 4px; -} - - -/* ORDER EDITING */ -table.order-edit { - width: 100%; - border: solid 1px #ccc; - margin-top: 4px; -} -table.order-edit td { - padding: 5px 5px; - border: medium none; - .smaller(); -} - -.bundle-items-container { - padding: 10px 0 0 60px; - - .bundle-item { - margin-top: 5px; - } - - .bundle-item-attribute-info { - font-size: 0.85em; - line-height: 1.45em; - margin-top: 2px; - } -} - - -/* WARNINGS */ -.system-warnings { - font-size: 1.2em; -} - -.system-warnings li { - margin-bottom: 0.8em; -} - - -/* DATA LISTS */ - -.data-list .thumbnail .caption { padding: 4px } -.data-list-grid .data-list-row, -.data-list .data-list-item { margin-bottom: 12px; } - -.item-box { - border: 1px solid #ddd; - position: relative; - .small(); - .box-shadow(0 1px 3px rgba(0,0,0,.055)); - .transition(~'box-shadow .12s linear, border-color .12s linear'); - &:hover { - border: 1px solid @orange; - .box-shadow(0px 0px 8px fade(@orange, 60%)); - } -} - - -/* Plugins & Providers --------------------------------------------------------------- */ - -.sortable-grip { - left: 67px; - width: 9px; -} - -.module-list, .module-item { - position: relative; -} - -.module-list > h3 { - font-family: @headingsFontFamily; - font-weight: @headingsFontWeight; - color: inherit; - font-size: 18px; - margin-top: 30px; -} - -.module-item { - padding: 12px 0; - border-top: 1px solid #ddd; -} -.module-item.inactive { - background-color: #fafafa; -} -.module-item.inactive .module-icon { - .opacity(50); -} - -.module-icon { - float: left; - width: 48px; - margin-left: 10px; - text-align: center; - - .icon, .fa { - max-width: 48px; - } -} - -.module-data { - box-sizing: border-box; - margin-left: 90px; -} -.module-item:last-child .module-data { - border-bottom: none; -} - -.module-col { - float: left; - display: block; - box-sizing: border-box; - min-height: 1px; - position: relative; - padding: 0 15px; - - &:first-child { - padding-left: 0; - } -} - -.module-data-cols-1 .module-col { width: 100%; } -.module-data-cols-2 .module-col { width: 50%; } -.module-data-cols-3 .module-col { width: 33.3332%; } -.module-data-cols-4 .module-col { width: 25%; } - -.module-name { - font-weight: bold; - margin-right: 5px; -} - -.module-info { - margin-top: 3px; -} -.module-info .attr:after { - display: inline-block; - content: '\00b7'; - padding: 0 5px; -} -.module-info .attr:last-child:after { - display: none; -} -.module-info .attr-value { - color: #aaa; - //padding-right: 5px; -} - -.module-description { - margin: 4px 0; -} - -.module-actions { - margin-top: 8px; -} - -.module-actions .btn { - text-transform: none; -} - -table.payment-method-features td { - padding: 2px 4px; - font-size: 12px; -} - - -/* Special icons */ -.icon-active-true { - color: @green; - text-shadow: 0 -1px 0 #fff; - &::before { content: '\f00c'; } -} - -.icon-active-false { - color: #c5c5c5; - text-shadow: 0 -1px 0 #fff; - &::before { content: '\f068'; } -} - - -#order-form .checkout-attributes { - table { - //margin-left: auto - } - .attr-caption { - font-weight: bold; - text-align: right; - padding-right: 10px; - &:after { - content: ': ' - } - } - .attr-value { - text-align: left; - } -} - -/* Table formatted variant attributes */ -.product-attribute-table { - .column-name { - width: 140px; - padding: 1px 10px 1px 0; - vertical-align: top; - } - .column-value { - font-weight: bold; - padding: 1px 0 1px 0; - vertical-align: top; - } -} - -/* Themes */ -#theme-list { - min-width: 820px; // 3 items in a row - max-width: 1360px; // 5 items - - .theme-item { - position: relative; - float: left; - width: 250px; - box-sizing: border-box; - margin: 0 20px 20px 0; - border: 1px solid #ccc; - } - .theme-thumbnail-wrapper, - .theme-data { - padding: 8px; - } - .theme-thumbnail { - height: 220px; - background-size: cover; - } - .theme-data h4 { - margin-top: 0; - color: #333; - } - .theme-data h4 > small { - .smaller(); - font-weight: normal; - } - .theme-info { - font-size: 12px; - line-height: 16px; - } - .theme-buttons { - margin-top: 15px; - } - .theme-broken-badge { - position: absolute; - background-color: @red; - color: #fff; - right: 0; - bottom: 0; - padding: 8px 12px; - font-size: 20px; - } -} - -.theme-editor { - .form-control { - width: 400px; - } - - .colorpicker-component .form-control { - width: 372px; - } -} - -.themevar-chain-info { - color: #bbb; - margin-left: 10px; - vertical-align: middle; -} - - -.slides-title { - position: relative; - .headline { - font-size: 18px; - font-weight: 100; - } -} - -.color-container { - background-color: transparent; - padding-right: 6px; - .color { - width: 10px; - height: 10px; - line-height: 10px; - display: inline-block; - border: 1px solid #646464; - } -} - - -/* On/Off switch button --------------------------------------------------------------- */ - -.switch { - position: relative; - display: inline-block; - vertical-align: middle; - margin: 0; - padding: 0; - user-select: none; - - > .switch-toggle { - position: relative; - display: inline-block; - vertical-align: middle; - width: 45px; - height: 20px; - border-radius: 20px; - background-color: #f2f2f2; - border: 1px solid rgba(0,0,0, 0.15); - box-sizing: border-box; - cursor: pointer; - transition: background-color 0.4s cubic-bezier(.54,1.85,.5,1); - - &:before { - position: absolute; - left: 1px; - top: 1px; - content: ' '; - width: 16px; - height: 16px; - border-radius: 50%; - background-color: #fff; - box-shadow: 0 0 3px rgba(0, 0, 0, 0.2); - transition: left 0.4s cubic-bezier(.54,1.85,.5,1); - } - - &:after { - position: absolute; - display: inline-block; - box-sizing: border-box; - width: 27px; - height: 100%; - padding: 3px; - vertical-align: middle; - text-transform: lowercase; - font-size: 10px; - line-height: 100%; - text-align: center; - - content: 'Aus'; - content: attr(data-off); - color: #bbb; - left: auto; - right: 0; - } - } - - > input[type=checkbox] { - position: absolute; - z-index: 0; - opacity: 0; - } - - > input[type=checkbox]:checked ~ .switch-toggle { - background-color: @orange; - - &:before { - left: 26px; - } - - &:after { - content: 'An'; - content: attr(data-on); - color: rgba(255,255,255, 0.75); - left: 0; - right: auto; - } - } -} - -.multi-store-override-switch { - margin-right: 8px; -} - - - -#pnlAllFlags a.flag { - min-width: 100px; - display: inline-block; -} -#pnlAllFlags a.flag:hover { - text-decoration: none; -} - - -/* Download editor --------------------------------------------------------------- */ - -.download-editor { - .panel-switcher-icon { - width: 13px; - } - - .filename { - line-height: 32px; - vertical-align: middle; - padding-right: 10px; - font-weight: bold; - max-width: 300px; - display: inline-block; - .text-overflow(); - } -} - - -/* Scheduled tasks --------------------------------------------------------------- */ - -.minimal-task-widget { - min-height: 26px; -} -.minimal-task-widget .task-progress { - width: 400px; -} -.minimal-task-widget .btn-cancel-task { - margin-left: 10px -} -.minimal-task-widget .btn-actions { - display: inline-block; - margin-left: 10px; -} - - - - -/* Data exchange --------------------------------------------------------------- */ -.profile-list { - margin-bottom: 10px; - border-bottom: 1px solid #ddd; -} - -.info-profile td { - padding-top: 20px; -} - -/* Import column mapping --------------------------------------------------------------- */ -@media (max-width: 1280px) { - #ImportColumnMappings .right-label, - .column-mapping .mapping-button-label, - .mapping-list-item .right-label { - display: none; - } -} - -.column-mapping .right-label, -.mapping-list-item .right-label { - float: right; - font-size: 0.9em; -} -.mapping-list-item { - .text-overflow(); -} - -.column-mapping { - .column-one, .column-two { - width: 42%; - } - - .select-column, .select-property, .input-default { - width: 98%; - } - - th, td, .mapping-delete, .mapping-add, .mapping-apply, .mapping-cancel { - white-space: nowrap; - padding-left: 8px; - padding-right: 8px; - } - - .select2-container .select2-choice span { - margin-right: 12px; - white-space: nowrap; - } - - .mapping-edit { - white-space: nowrap; - - .select-column, .select-property { - width: 84%; - } - .select2-container { - float: left; - display: inline-block; - min-width: 220px; - margin-right: 4px; - } - } - - .mapping-item { - .item-inner { - white-space: nowrap; - cursor: pointer; - padding: 7px 0; - - .property-icon { - display: inline-block; - margin-right: 0; - } - .left-label { - display: inline-block; - } - .right-label { - margin-right: 20px; - } - } - } -} - -// -// Choice Boxes -// TODO: (mc) DELETE this stuff after SASSifying the backend theme, -// 'cause choice-boxes are already contained in shared scss files. -// ================================================================ - -.choice-box-group { - padding: 0; - list-style-type: none; - position: relative; - margin: 0 -0.4rem; - box-sizing: border-box; - - * { box-sizing: border-box } -} - -.choice-box-group::after { - content: ""; - display: table; - clear: both; -} - -.choice-box-group .choice-box { - position: relative; - float: left; - display: block; - padding: 0.2rem; -} - -.choice-box-group .choice-box-label { - margin: 0; -} - -.choice-box-group .choice-box-content { - position: relative; - display: block; - cursor: pointer; -} - -.choice-box-group .choice-box-element { - display: inline-block; - border: 1px solid rgba(0, 0, 0, 0.3); - background-color: #fff; - -webkit-transition: border-color 0.1s linear; - transition: border-color 0.1s linear; - height: 2.8rem; - line-height: 2.8rem; - min-width: 2.8rem; - vertical-align: middle; - text-align: center; - overflow: hidden; - font-weight: 600; -} - -.choice-box-group .choice-box-element:hover { - border-color: rgba(0, 0, 0, 0.7); -} - -.choice-box-group .choice-box-element .choice-box-img { - display: block; - position: relative; - max-width: ~"calc(2.8rem - 4px)"; - max-height: ~"calc(100% - 4px)"; - margin: auto; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); -} - -.choice-box-group .choice-box-element .choice-box-text { - padding: 0 0.4rem; -} - -.choice-box-control-native:checked + .choice-box-content:after { - box-sizing: content-box; - position: absolute; - z-index: 1; - display: block; - right: -6px; - top: -6px; - background-color: #ff9800; - border-radius: 50%; - border: 1px solid #fff; - width: 16px; - height: 16px; - line-height: 16px; - text-align: center; - vertical-align: middle; - font-family: FontAwesome; - content: "\f00c"; - color: #fff; - font-size: 10px; -} - -.choice-box-control-native:checked + .choice-box-content .choice-box-element { - border-color: rgba(0, 0, 0, 0.7); -} - -.choice-box-group .choice-box-control-native { - display: none !important; -} - -.choice-box-group.choice-box-group-xl .choice-box-element { - height: 6rem; - line-height: 6rem; - min-width: 6rem; - .choice-box-img { max-width: ~"calc(6rem - 8px)"; } -} - -.choice-box-group.choice-box-group-lg .choice-box-element { - height: 4rem; - line-height: 4rem; - min-width: 4rem; - .choice-box-img { max-width: ~"calc(4rem - 6px)"; } -} - -.choice-box-group.choice-box-group-sm .choice-box-element { - height: 1.5rem; - line-height: 1.5rem; - min-width: 1.5rem; - .choice-box-img { max-width: ~"calc(1.5rem - 4px)"; } -} - - - -/* Bootstrap 4 fixes --------------------------------------------------------------- */ -.bootstrap-datetimepicker-widget th.switch { - display: table-cell; -} - - -/* CKEditor skinning --------------------------------------------------------------- */ - -.cke_chrome.cke_focus { - border-color: @blue; - box-shadow: none; -} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/conf.json b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/conf.json index f2c19cad97..3142fe6dd1 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/conf.json +++ b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/conf.json @@ -1,35 +1,35 @@ { - "FILES_ROOT": "Uploaded", - "RETURN_URL_PREFIX": "", - "SESSION_PATH_KEY": "", - "THUMBS_VIEW_WIDTH": "140", - "THUMBS_VIEW_HEIGHT": "120", - "PREVIEW_THUMB_WIDTH": "300", - "PREVIEW_THUMB_HEIGHT": "200", - "MAX_IMAGE_WIDTH": "0", - "MAX_IMAGE_HEIGHT": "0", - "INTEGRATION": "ckeditor", - "DIRLIST": "../../../Admin/RoxyFileManager/ProcessRequest?a=DIRLIST", - "CREATEDIR": "../../../Admin/RoxyFileManager/ProcessRequest?a=CREATEDIR", - "DELETEDIR": "../../../Admin/RoxyFileManager/ProcessRequest?a=DELETEDIR", - "MOVEDIR": "../../../Admin/RoxyFileManager/ProcessRequest?a=MOVEDIR", - "COPYDIR": "../../../Admin/RoxyFileManager/ProcessRequest?a=COPYDIR", - "RENAMEDIR": "../../../Admin/RoxyFileManager/ProcessRequest?a=RENAMEDIR", - "FILESLIST": "../../../Admin/RoxyFileManager/ProcessRequest?a=FILESLIST", - "UPLOAD": "../../../Admin/RoxyFileManager/ProcessRequest?a=UPLOAD", - "DOWNLOAD": "../../../Admin/RoxyFileManager/ProcessRequest?a=DOWNLOAD", - "DOWNLOADDIR": "../../../Admin/RoxyFileManager/ProcessRequest?a=DOWNLOADDIR", - "DELETEFILE": "../../../Admin/RoxyFileManager/ProcessRequest?a=DELETEFILE", - "MOVEFILE": "../../../Admin/RoxyFileManager/ProcessRequest?a=MOVEFILE", - "COPYFILE": "../../../Admin/RoxyFileManager/ProcessRequest?a=COPYFILE", - "RENAMEFILE": "../../../Admin/RoxyFileManager/ProcessRequest?a=RENAMEFILE", - "GENERATETHUMB": "../../../Admin/RoxyFileManager/ProcessRequest?a=GENERATETHUMB", - "DEFAULTVIEW": "list", - "FORBIDDEN_UPLOADS": "zip js jsp jsb mhtml mht xhtml xht php phtml php3 php4 php5 phps shtml jhtml pl sh py cgi exe scr dll msi vbs bat com pif cmd vxd cpl htpasswd htaccess aspx", - "ALLOWED_UPLOADS": "", - "FILEPERMISSIONS": "0644", - "DIRPERMISSIONS": "0755", - "LANG": "auto", - "DATEFORMAT": "dd.MM.yyyy HH:mm", - "OPEN_LAST_DIR": "yes" + "FILES_ROOT": "Uploaded", + "RETURN_URL_PREFIX": "", + "SESSION_PATH_KEY": "", + "THUMBS_VIEW_WIDTH": "140", + "THUMBS_VIEW_HEIGHT": "120", + "PREVIEW_THUMB_WIDTH": "300", + "PREVIEW_THUMB_HEIGHT": "200", + "MAX_IMAGE_WIDTH": "0", + "MAX_IMAGE_HEIGHT": "0", + "INTEGRATION": "custom", + "DIRLIST": "ProcessRequest?a=DIRLIST", + "CREATEDIR": "ProcessRequest?a=CREATEDIR", + "DELETEDIR": "ProcessRequest?a=DELETEDIR", + "MOVEDIR": "ProcessRequest?a=MOVEDIR", + "COPYDIR": "ProcessRequest?a=COPYDIR", + "RENAMEDIR": "ProcessRequest?a=RENAMEDIR", + "FILESLIST": "ProcessRequest?a=FILESLIST", + "UPLOAD": "ProcessRequest?a=UPLOAD", + "DOWNLOAD": "ProcessRequest?a=DOWNLOAD", + "DOWNLOADDIR": "ProcessRequest?a=DOWNLOADDIR", + "DELETEFILE": "ProcessRequest?a=DELETEFILE", + "MOVEFILE": "ProcessRequest?a=MOVEFILE", + "COPYFILE": "ProcessRequest?a=COPYFILE", + "RENAMEFILE": "ProcessRequest?a=RENAMEFILE", + "GENERATETHUMB": true, + "DEFAULTVIEW": "list", + "FORBIDDEN_UPLOADS": "zip js jsp jsb mhtml mht xhtml xht php phtml php3 php4 php5 phps shtml jhtml pl sh py cgi exe scr dll msi vbs bat com pif cmd vxd cpl htpasswd htaccess aspx", + "ALLOWED_UPLOADS": "", + "FILEPERMISSIONS": "0644", + "DIRPERMISSIONS": "0755", + "LANG": "auto", + "DATEFORMAT": "dd.MM.yyyy HH:mm", + "OPEN_LAST_DIR": "yes" } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_flat_0_aaaaaa_40x100.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_flat_0_aaaaaa_40x100.png deleted file mode 100644 index e1a114b4e2..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_flat_0_aaaaaa_40x100.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_flat_75_ffffff_40x100.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_flat_75_ffffff_40x100.png deleted file mode 100644 index 399d1ba7dd..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_flat_75_ffffff_40x100.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_55_fbf9ee_1x400.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_55_fbf9ee_1x400.png deleted file mode 100644 index 9267153440..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_55_fbf9ee_1x400.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_65_ffffff_1x400.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_65_ffffff_1x400.png deleted file mode 100644 index 12902e2006..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_65_ffffff_1x400.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_75_dadada_1x400.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_75_dadada_1x400.png deleted file mode 100644 index c6af0909f1..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_75_dadada_1x400.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_75_e6e6e6_1x400.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_75_e6e6e6_1x400.png deleted file mode 100644 index 7e2dc85982..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_75_e6e6e6_1x400.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_95_fef1ec_1x400.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_95_fef1ec_1x400.png deleted file mode 100644 index be40bcf3c3..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_glass_95_fef1ec_1x400.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png deleted file mode 100644 index 57d294c986..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_222222_256x240.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_222222_256x240.png deleted file mode 100644 index c1cb1170c8..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_222222_256x240.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_2e83ff_256x240.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_2e83ff_256x240.png deleted file mode 100644 index 84b601bf0f..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_2e83ff_256x240.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_444444_256x240.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_444444_256x240.png new file mode 100644 index 0000000000..a802263b58 Binary files /dev/null and b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_444444_256x240.png differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_454545_256x240.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_454545_256x240.png deleted file mode 100644 index b6db1acdd4..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_454545_256x240.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_555555_256x240.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_555555_256x240.png new file mode 100644 index 0000000000..7009bf752f Binary files /dev/null and b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_555555_256x240.png differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_777620_256x240.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_777620_256x240.png new file mode 100644 index 0000000000..e0a1fdfdc0 Binary files /dev/null and b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_777620_256x240.png differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_777777_256x240.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_777777_256x240.png new file mode 100644 index 0000000000..8e26ee4fd8 Binary files /dev/null and b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_777777_256x240.png differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_888888_256x240.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_888888_256x240.png deleted file mode 100644 index feea0e2026..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_888888_256x240.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_cc0000_256x240.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_cc0000_256x240.png new file mode 100644 index 0000000000..28154300a6 Binary files /dev/null and b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_cc0000_256x240.png differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_cd0a0a_256x240.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_cd0a0a_256x240.png deleted file mode 100644 index ed5b6b0930..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_cd0a0a_256x240.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_ffffff_256x240.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_ffffff_256x240.png new file mode 100644 index 0000000000..4d66f596e5 Binary files /dev/null and b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/images/ui-icons_ffffff_256x240.png differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/jquery-ui-1.10.4.custom.css b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/jquery-ui-1.10.4.custom.css index 4827019a8a..48cf598fbd 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/jquery-ui-1.10.4.custom.css +++ b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/jquery-ui-1.10.4.custom.css @@ -1,8 +1,8 @@ -/*! jQuery UI - v1.10.4 - 2014-01-21 +/*! jQuery UI - v1.10.4 - 2018-02-27 * http://jqueryui.com * Includes: jquery.ui.core.css, jquery.ui.resizable.css, jquery.ui.selectable.css, jquery.ui.button.css, jquery.ui.dialog.css, jquery.ui.tooltip.css, jquery.ui.theme.css -* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px -* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */ +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=-apple-system%2C%20BlinkMacSystemFont%2C%20%22Segoe%20UI%22%2C%20Roboto%2C%20%22Helvetica%20Neue%22%2C%20Arial%2C%20sans-serif%2C%20%22Apple%20Color%20Emoji%22%2C%20%22Segoe%20UI%20Emoji%22%2C%20%22Segoe%20UI%20Symbol%22&fsDefault=14px&fwDefault=normal&cornerRadius=3px&bgColorHeader=%23e9e9e9&bgTextureHeader=flat&borderColorHeader=%23dddddd&fcHeader=%23333333&iconColorHeader=%23444444&bgColorContent=%23ffffff&bgTextureContent=flat&borderColorContent=%23dddddd&fcContent=%23333333&iconColorContent=%23444444&bgColorDefault=%23f6f6f6&bgTextureDefault=flat&borderColorDefault=%23c5c5c5&fcDefault=%23454545&iconColorDefault=%23777777&bgColorHover=%23ededed&bgTextureHover=flat&borderColorHover=%23cccccc&fcHover=%232b2b2b&iconColorHover=%23555555&bgColorActive=%23007fff&bgTextureActive=flat&borderColorActive=%23003eff&fcActive=%23ffffff&iconColorActive=%23ffffff&bgColorHighlight=%23fffa90&bgTextureHighlight=flat&borderColorHighlight=%23dad55e&fcHighlight=%23777620&iconColorHighlight=%23777620&bgColorError=%23fddfdf&bgTextureError=flat&borderColorError=%23f1a899&fcError=%235f3f3f&iconColorError=%23cc0000&bgColorOverlay=%23aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=%23666666&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=5px&offsetTopShadow=0px&offsetLeftShadow=0px&cornerRadiusShadow=8px&bgImgOpacityHeader=&bgImgOpacityContent=&bgImgOpacityDefault=&bgImgOpacityHover=&bgImgOpacityActive=&bgImgOpacityHighlight=&bgImgOpacityError= +* Copyright jQuery Foundation and other contributors; Licensed MIT */ /* Layout helpers ----------------------------------*/ @@ -338,8 +338,8 @@ body .ui-tooltip { /* Component containers ----------------------------------*/ .ui-widget { - font-family: Verdana,Arial,sans-serif; - font-size: 1.1em; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-size: 14px; } .ui-widget .ui-widget { font-size: 1em; @@ -348,25 +348,25 @@ body .ui-tooltip { .ui-widget select, .ui-widget textarea, .ui-widget button { - font-family: Verdana,Arial,sans-serif; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 1em; } .ui-widget-content { - border: 1px solid #aaaaaa; - background: #ffffff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x; - color: #222222; + border: 1px solid #dddddd; + background: #ffffff; + color: #333333; } .ui-widget-content a { - color: #222222; + color: #333333; } .ui-widget-header { - border: 1px solid #aaaaaa; - background: #cccccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x; - color: #222222; + border: 1px solid #dddddd; + background: #e9e9e9; + color: #333333; font-weight: bold; } .ui-widget-header a { - color: #222222; + color: #333333; } /* Interaction states @@ -374,15 +374,15 @@ body .ui-tooltip { .ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { - border: 1px solid #d3d3d3; - background: #e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x; + border: 1px solid #c5c5c5; + background: #f6f6f6; font-weight: normal; - color: #555555; + color: #454545; } .ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { - color: #555555; + color: #454545; text-decoration: none; } .ui-state-hover, @@ -391,10 +391,10 @@ body .ui-tooltip { .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { - border: 1px solid #999999; - background: #dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x; + border: 1px solid #cccccc; + background: #ededed; font-weight: normal; - color: #212121; + color: #2b2b2b; } .ui-state-hover a, .ui-state-hover a:hover, @@ -404,21 +404,21 @@ body .ui-tooltip { .ui-state-focus a:hover, .ui-state-focus a:link, .ui-state-focus a:visited { - color: #212121; + color: #2b2b2b; text-decoration: none; } .ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { - border: 1px solid #aaaaaa; - background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x; + border: 1px solid #003eff; + background: #007fff; font-weight: normal; - color: #212121; + color: #ffffff; } .ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { - color: #212121; + color: #ffffff; text-decoration: none; } @@ -427,31 +427,31 @@ body .ui-tooltip { .ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight { - border: 1px solid #fcefa1; - background: #fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x; - color: #363636; + border: 1px solid #dad55e; + background: #fffa90; + color: #777620; } .ui-state-highlight a, .ui-widget-content .ui-state-highlight a, .ui-widget-header .ui-state-highlight a { - color: #363636; + color: #777620; } .ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error { - border: 1px solid #cd0a0a; - background: #fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x; - color: #cd0a0a; + border: 1px solid #f1a899; + background: #fddfdf; + color: #5f3f3f; } .ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { - color: #cd0a0a; + color: #5f3f3f; } .ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { - color: #cd0a0a; + color: #5f3f3f; } .ui-priority-primary, .ui-widget-content .ui-priority-primary, @@ -486,27 +486,27 @@ body .ui-tooltip { } .ui-icon, .ui-widget-content .ui-icon { - background-image: url(images/ui-icons_222222_256x240.png); + background-image: url("images/ui-icons_444444_256x240.png"); } .ui-widget-header .ui-icon { - background-image: url(images/ui-icons_222222_256x240.png); + background-image: url("images/ui-icons_444444_256x240.png"); } .ui-state-default .ui-icon { - background-image: url(images/ui-icons_888888_256x240.png); + background-image: url("images/ui-icons_777777_256x240.png"); } .ui-state-hover .ui-icon, .ui-state-focus .ui-icon { - background-image: url(images/ui-icons_454545_256x240.png); + background-image: url("images/ui-icons_555555_256x240.png"); } .ui-state-active .ui-icon { - background-image: url(images/ui-icons_454545_256x240.png); + background-image: url("images/ui-icons_ffffff_256x240.png"); } .ui-state-highlight .ui-icon { - background-image: url(images/ui-icons_2e83ff_256x240.png); + background-image: url("images/ui-icons_777620_256x240.png"); } .ui-state-error .ui-icon, .ui-state-error-text .ui-icon { - background-image: url(images/ui-icons_cd0a0a_256x240.png); + background-image: url("images/ui-icons_cc0000_256x240.png"); } /* positioning */ @@ -696,37 +696,37 @@ body .ui-tooltip { .ui-corner-top, .ui-corner-left, .ui-corner-tl { - border-top-left-radius: 4px; + border-top-left-radius: 3px; } .ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { - border-top-right-radius: 4px; + border-top-right-radius: 3px; } .ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { - border-bottom-left-radius: 4px; + border-bottom-left-radius: 3px; } .ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { - border-bottom-right-radius: 4px; + border-bottom-right-radius: 3px; } /* Overlays */ .ui-widget-overlay { - background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; + background: #aaaaaa; opacity: .3; filter: Alpha(Opacity=30); } .ui-widget-shadow { - margin: -8px 0 0 -8px; - padding: 8px; - background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; + margin: 0px 0 0 0px; + padding: 5px; + background: #666666; opacity: .3; filter: Alpha(Opacity=30); border-radius: 8px; diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/jquery-ui-1.10.4.custom.min.css b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/jquery-ui-1.10.4.custom.min.css index 0418dfd016..be713c57cc 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/jquery-ui-1.10.4.custom.min.css +++ b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/jquery-ui-1.10.4.custom.min.css @@ -1,7 +1,7 @@ -/*! jQuery UI - v1.10.4 - 2014-01-21 +/*! jQuery UI - v1.10.4 - 2018-02-27 * http://jqueryui.com * Includes: jquery.ui.core.css, jquery.ui.resizable.css, jquery.ui.selectable.css, jquery.ui.button.css, jquery.ui.dialog.css, jquery.ui.tooltip.css, jquery.ui.theme.css -* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px -* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */ +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=-apple-system%2C%20BlinkMacSystemFont%2C%20%22Segoe%20UI%22%2C%20Roboto%2C%20%22Helvetica%20Neue%22%2C%20Arial%2C%20sans-serif%2C%20%22Apple%20Color%20Emoji%22%2C%20%22Segoe%20UI%20Emoji%22%2C%20%22Segoe%20UI%20Symbol%22&fsDefault=14px&fwDefault=normal&cornerRadius=3px&bgColorHeader=%23e9e9e9&bgTextureHeader=flat&borderColorHeader=%23dddddd&fcHeader=%23333333&iconColorHeader=%23444444&bgColorContent=%23ffffff&bgTextureContent=flat&borderColorContent=%23dddddd&fcContent=%23333333&iconColorContent=%23444444&bgColorDefault=%23f6f6f6&bgTextureDefault=flat&borderColorDefault=%23c5c5c5&fcDefault=%23454545&iconColorDefault=%23777777&bgColorHover=%23ededed&bgTextureHover=flat&borderColorHover=%23cccccc&fcHover=%232b2b2b&iconColorHover=%23555555&bgColorActive=%23007fff&bgTextureActive=flat&borderColorActive=%23003eff&fcActive=%23ffffff&iconColorActive=%23ffffff&bgColorHighlight=%23fffa90&bgTextureHighlight=flat&borderColorHighlight=%23dad55e&fcHighlight=%23777620&iconColorHighlight=%23777620&bgColorError=%23fddfdf&bgTextureError=flat&borderColorError=%23f1a899&fcError=%235f3f3f&iconColorError=%23cc0000&bgColorOverlay=%23aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=%23666666&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=5px&offsetTopShadow=0px&offsetLeftShadow=0px&cornerRadiusShadow=8px&bgImgOpacityHeader=&bgImgOpacityContent=&bgImgOpacityDefault=&bgImgOpacityHover=&bgImgOpacityActive=&bgImgOpacityHighlight=&bgImgOpacityError= +* Copyright jQuery Foundation and other contributors; Licensed MIT */ -.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-button{display:inline-block;position:relative;padding:0;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2.2em}button.ui-button-icon-only{width:2.4em}.ui-button-icons-only{width:3.4em}button.ui-button-icons-only{width:3.7em}.ui-button .ui-button-text{display:block;line-height:normal}.ui-button-text-only .ui-button-text{padding:.4em 1em}.ui-button-icon-only .ui-button-text,.ui-button-icons-only .ui-button-text{padding:.4em;text-indent:-9999999px}.ui-button-text-icon-primary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 1em .4em 2.1em}.ui-button-text-icon-secondary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 2.1em .4em 1em}.ui-button-text-icons .ui-button-text{padding-left:2.1em;padding-right:2.1em}input.ui-button{padding:.4em 1em}.ui-button-icon-only .ui-icon,.ui-button-text-icon-primary .ui-icon,.ui-button-text-icon-secondary .ui-icon,.ui-button-text-icons .ui-icon,.ui-button-icons-only .ui-icon{position:absolute;top:50%;margin-top:-8px}.ui-button-icon-only .ui-icon{left:50%;margin-left:-8px}.ui-button-text-icon-primary .ui-button-icon-primary,.ui-button-text-icons .ui-button-icon-primary,.ui-button-icons-only .ui-button-icon-primary{left:.5em}.ui-button-text-icon-secondary .ui-button-icon-secondary,.ui-button-text-icons .ui-button-icon-secondary,.ui-button-icons-only .ui-button-icon-secondary{right:.5em}.ui-buttonset{margin-right:7px}.ui-buttonset .ui-button{margin-left:0;margin-right:-.3em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-dialog{overflow:hidden;position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-se{width:12px;height:12px;right:-5px;bottom:-5px;background-position:16px 16px}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px;-webkit-box-shadow:0 0 5px #aaa;box-shadow:0 0 5px #aaa}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #aaa;background:#fff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x;color:#222}.ui-widget-content a{color:#222}.ui-widget-header{border:1px solid #aaa;background:#ccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x;color:#222;font-weight:bold}.ui-widget-header a{color:#222}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #d3d3d3;background:#e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x;font-weight:normal;color:#555}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#555;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #999;background:#dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited{color:#212121;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #aaa;background:#fff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#212121;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fcefa1;background:#fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x;color:#cd0a0a}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#cd0a0a}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#cd0a0a}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-widget-header .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-state-default .ui-icon{background-image:url(images/ui-icons_888888_256x240.png)}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url(images/ui-icons_454545_256x240.png)}.ui-state-active .ui-icon{background-image:url(images/ui-icons_454545_256x240.png)}.ui-state-highlight .ui-icon{background-image:url(images/ui-icons_2e83ff_256x240.png)}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url(images/ui-icons_cd0a0a_256x240.png)}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{margin:-8px 0 0 -8px;padding:8px;background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30);border-radius:8px} \ No newline at end of file +.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-button{display:inline-block;position:relative;padding:0;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2.2em}button.ui-button-icon-only{width:2.4em}.ui-button-icons-only{width:3.4em}button.ui-button-icons-only{width:3.7em}.ui-button .ui-button-text{display:block;line-height:normal}.ui-button-text-only .ui-button-text{padding:.4em 1em}.ui-button-icon-only .ui-button-text,.ui-button-icons-only .ui-button-text{padding:.4em;text-indent:-9999999px}.ui-button-text-icon-primary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 1em .4em 2.1em}.ui-button-text-icon-secondary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 2.1em .4em 1em}.ui-button-text-icons .ui-button-text{padding-left:2.1em;padding-right:2.1em}input.ui-button{padding:.4em 1em}.ui-button-icon-only .ui-icon,.ui-button-text-icon-primary .ui-icon,.ui-button-text-icon-secondary .ui-icon,.ui-button-text-icons .ui-icon,.ui-button-icons-only .ui-icon{position:absolute;top:50%;margin-top:-8px}.ui-button-icon-only .ui-icon{left:50%;margin-left:-8px}.ui-button-text-icon-primary .ui-button-icon-primary,.ui-button-text-icons .ui-button-icon-primary,.ui-button-icons-only .ui-button-icon-primary{left:.5em}.ui-button-text-icon-secondary .ui-button-icon-secondary,.ui-button-text-icons .ui-button-icon-secondary,.ui-button-icons-only .ui-button-icon-secondary{right:.5em}.ui-buttonset{margin-right:7px}.ui-buttonset .ui-button{margin-left:0;margin-right:-.3em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-dialog{overflow:hidden;position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-se{width:12px;height:12px;right:-5px;bottom:-5px;background-position:16px 16px}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px;-webkit-box-shadow:0 0 5px #aaa;box-shadow:0 0 5px #aaa}body .ui-tooltip{border-width:2px}.ui-widget{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:14px}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1em}.ui-widget-content{border:1px solid #ddd;background:#fff;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #ddd;background:#e9e9e9;color:#333;font-weight:bold}.ui-widget-header a{color:#333}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #c5c5c5;background:#f6f6f6;font-weight:normal;color:#454545}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#454545;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #ccc;background:#ededed;font-weight:normal;color:#2b2b2b}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited{color:#2b2b2b;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #003eff;background:#007fff;font-weight:normal;color:#fff}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#fff;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #dad55e;background:#fffa90;color:#777620}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#777620}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #f1a899;background:#fddfdf;color:#5f3f3f}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#5f3f3f}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#5f3f3f}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-state-default .ui-icon{background-image:url("images/ui-icons_777777_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url("images/ui-icons_555555_256x240.png")}.ui-state-active .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-highlight .ui-icon{background-image:url("images/ui-icons_777620_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cc0000_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:3px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:3px}.ui-widget-overlay{background:#aaa;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{margin:0 0 0 0;padding:5px;background:#666;opacity:.3;filter:Alpha(Opacity=30);border-radius:8px} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/main.css b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/main.css deleted file mode 100644 index 0f81662ef6..0000000000 --- a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/main.css +++ /dev/null @@ -1,308 +0,0 @@ -/* - RoxyFileman - web based file manager. Ready to use with CKEditor, TinyMCE. - Can be easily integrated with any other WYSIWYG editor or CMS. - - Copyright (C) 2013, RoxyFileman.com - Lyubomir Arsov. All rights reserved. - For licensing, see LICENSE.txt or http://RoxyFileman.com/license - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - - Contact: Lyubomir Arsov, liubo (at) web-lobby.com -*/ -body{ - font-family: Verdana; - font-size: 11px; - padding:0; - margin:0; - background:#FFF; - height:100%; - width:100%; -} -a{text-decoration:none;color:#DD7700;} -ul{ - list-style:none; - margin:0; - padding:0; -} -#wraper{ - width:100%; - height:100%; -} -#wraper,#dlgNewDir,#dlgFileProp,#dlgAddFile,#menuDir, -#menuFile,#pnlRenameFile,#pnlLoadingDirs, -#pnlDirName,#pnlLoading,#pnlEmptyDir, -#pnlSearchNoFiles,#pnlFileProp, #pnlDirList ul{ - display:none; -} -.contextMenu a:hover, -#pnlFileList .selected,#pnlFileList .selected:hover, -#menuFile a:hover{ - background-color: #000099 !important; - color:#FFF; -} -#pnlDirList .selected,#pnlDirList div.selected:hover{background-color: #DFD !important;} -.dialog input{width:97%;} -.filesize{font-weight:bold;margin-top:4px;display:inline-block;} -.pnlDragFile,.pnlDragDir{ - background:#FFF; - opacity:0.8; - float:left; - width:auto !important; -} -#pnlStatus{ - padding:5px; -} -.scrollPane{ - height:400px; - overflow: auto; -} -.bottomLine{background:#DDD;border-top:1px solid #CCC;} -.pale{opacity:0.5;} -/* Search and order fields */ -#txtSearch,#ddlOrder{ - border:1px solid #CCC; - border-radius:3px; -} -#txtSearch{ - padding:0 22px 0 3px; - background:url(../images/sprite.png) no-repeat 87px -314px; - width:80px; - height:20px; -} -input[type=text]::-ms-clear { display: none; } -/* END OF Search and order fields */ - -/* Directory tree */ -.pnlDirs{ - width:260px; - overflow:auto; - border-right:1px solid #DDD; -} -#pnlDirList{margin-left:4px;} -#pnlDirList ul{padding:0 0 0 20px;} -#pnlDirList .dir{ - line-height:30px; - float:left; - margin-right:5px; -} -#pnlDirList .dirPlus{ - float:left; - margin: 3px 3px 0 0; -} -#pnlDirList li,#pnlDirList div{ - float:left; - clear:both; - width:98%; -} -#pnlDirList div{padding:3px;cursor:pointer;} -#pnlLoadingDirs{margin-left:5px;} -/* END OF Directory tree */ - -/* File list */ -.pnlFiles{padding-left:4px;} -#pnlFileList .icon{float:left;margin-right:3px;} -#pnlFileList li{float:left;padding:3px;clear:both;width:98%;cursor:pointer;} -#pnlFileList li:hover,#pnlDirList li div:hover{background:#FFFFCC !important;} -#pnlFileList .size{float:right;margin-left:20px;} -#pnlFileList .time{float:right;margin-left:15px;width:130px;} -.imgPreview{max-width:300px;} -/* END OF File list */ - -/* Context menus */ -.contextMenu{ - position:absolute; - background-color:#DDD; - border:1px solid #BBB; - min-width:100px; - font-weight: bold; - z-index:1000 !important; -} -.contextMenu a{ - background-repeat:no-repeat; - background-position: 4px 6px; - padding: 5px 25px 5px 20px; - display:block; - color:#000; - text-decoration:none; -} -hr{margin:0;color:#AAA;} -/* END OF Context menus */ - -/* Buttons */ -.actions{padding:3px 0 10px 5px;} -.actions input{ - background-repeat:no-repeat; - background-position: 3px center; - padding:0 3px 0 18px; - height:22px; - font-size: 11px; - border:1px solid #CCC; - background-color: #EEE; - border-radius:3px; - margin-bottom: 3px; -} -.actions input[type=button]:hover,.actions input.selected{background-color: #DFD;cursor:pointer;} -#btnAddDir{background-image:url(../images/sprite.png);background-position: 4px -213px;} -#mnuCreateDir{background-image:url(../images/sprite.png);background-position: 4px -210px;} -#mnuDownloadDir{background-image:url(../images/sprite.png);background-position: 4px 7px;} -#btnDeleteDir{background-image:url(../images/sprite.png);background-position: 4px -237px;} -#mnuDeleteDir{background-image:url(../images/sprite.png);background-position: 4px -235px;} -#btnAddFile{background-image:url(../images/sprite.png);background-position: 4px -81px;} -#btnDeleteFile{background-image:url(../images/sprite.png);background-position: 4px -104px;} -#mnuDeleteFile{background-image:url(../images/sprite.png);background-position: 4px -102px;} -#btnDeleteFile{margin-right:15px;} -#btnDownloadFile{background-image:url(../images/sprite.png);background-position: 4px -128px;} -#mnuDownload{background-image:url(../images/sprite.png);background-position: 4px -126px;} -#btnPreviewFile{background-image:url(../images/sprite.png);background-position: 4px -285px;} -#mnuPreview{background-image:url(../images/sprite.png);background-position: 4px -283px;} -#btnSelectFile{background-image:url(../images/sprite.png);background-position: 4px -399px;} -#mnuSelectFile{background-image:url(../images/sprite.png);background-position: 4px -397px;} -#btnThumbView{background-image:url(../images/sprite.png);background-position: center -447px;} -#btnListView{background-image:url(../images/sprite.png);background-position: center -483px;} -#btnRenameFile,#btnRenameDir{background-image:url(../images/sprite.png);background-position: 4px 4px;} -#mnuRenameDir,#mnuRenameFile{background-image:url(../images/sprite.png);background-position: 4px 7px;} -#mnuDirCut,#mnuFileCut{background-image:url(../images/sprite.png);background-position: 4px -47px;} -#mnuDirCopy,#mnuFileCopy{background-image:url(../images/sprite.png);background-position: 4px -20px;} -#mnuDirPaste,#mnuFilePaste{background-image:url(../images/sprite.png);background-position: 4px -370px;} - -/* END OF Buttons */ -/* File list views */ -ul#pnlFileList.thumbView li{ - /* - width:140px; - height: 140px; - */ - float:left !important; - clear:none; - margin:0 6px 6px 0; - overflow:hidden; - padding:10px; - text-align:center; - vertical-align:middle; - border: 1px solid #EEE; - border-radius: 5px; - position:relative; - background:#EEE; -} -ul#pnlFileList.thumbView .name,ul#pnlFileList.thumbView .time{display:none;} -ul#pnlFileList.thumbView .icon{ - clear:both; - float:none; - margin:0 !important; - text-align:center; - vertical-align:middle; - background-position:center center; - background-repeat:no-repeat; - /* - width:140px; - height:120px; - */ -} -ul#pnlFileList.thumbView .size{ - display:block !important; - width:95% !important; - position:absolute; - bottom:5px; - margin:0; -} -/* END OF File list views */ -#fileUploads{width:100%;} -.uploadFilesList{ - width:100%; - height:300px; - overflow:auto; -} -#uploadResult{ - -} -div.ui-tooltip { - max-width: 90%; -} -/* Upload dialog */ -.uploadFilesList{ - font-size: 11px; -} -.fileUpload{ - margin: 8px 0 0 0; - position: relative; - border:1px solid #EEE; -} -.fileUpload .error{font-weight:bold;} -.fileName{ - z-index:2; - line-height: 20px; - height:20px; - overflow: hidden; - padding:0 13px 0 5px; -} -.progressPercent{z-index:2;} -.stripes{ - background: url('../images/stripes.gif') repeat-x; - -moz-opacity: 0.40; - -khtml-opacity: 0.40; - opacity: 0.40; - -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40); - filter: progid:DXImageTransform.Microsoft.Alpha(opacity=40); - filter: alpha(opacity=40); - width:100%; - height:100%; - z-index:-1; - position: relative; -} -.uploadProgress{ - position: absolute; - width:0; - height:100%; - top:0; - left:0; - z-index:-1; - background: #d3edff; - background: -moz-linear-gradient(top, #d3edff 0%, #cbebff 47%, #a1dbff 100%); - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#d3edff), color-stop(47%,#cbebff), color-stop(100%,#a1dbff)); - background: -webkit-linear-gradient(top, #d3edff 0%,#cbebff 47%,#a1dbff 100%); - background: -o-linear-gradient(top, #d3edff 0%,#cbebff 47%,#a1dbff 100%); - background: -ms-linear-gradient(top, #d3edff 0%,#cbebff 47%,#a1dbff 100%); - background: linear-gradient(to bottom, #d3edff 0%,#cbebff 47%,#a1dbff 100%); - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#d3edff', endColorstr='#a1dbff',GradientType=0 ); -} -.uploadError{ - background: #ff8787; - background: -moz-linear-gradient(top, #ff8787 0%, #ffa3a3 45%, #ff5e5e 100%); - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ff8787), color-stop(45%,#ffa3a3), color-stop(100%,#ff5e5e)); - background: -webkit-linear-gradient(top, #ff8787 0%,#ffa3a3 45%,#ff5e5e 100%); - background: -o-linear-gradient(top, #ff8787 0%,#ffa3a3 45%,#ff5e5e 100%); - background: -ms-linear-gradient(top, #ff8787 0%,#ffa3a3 45%,#ff5e5e 100%); - background: linear-gradient(to bottom, #ff8787 0%,#ffa3a3 45%,#ff5e5e 100%); - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ff8787', endColorstr='#ff5e5e',GradientType=0 ); -} -.uploadComplete{ - background: #dbff87; - background: -moz-linear-gradient(top, #dbff87 0%, #e0f49c 45%, #bddd49 100%); - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#dbff87), color-stop(45%,#e0f49c), color-stop(100%,#bddd49)); - background: -webkit-linear-gradient(top, #dbff87 0%,#e0f49c 45%,#bddd49 100%); - background: -o-linear-gradient(top, #dbff87 0%,#e0f49c 45%,#bddd49 100%); - background: -ms-linear-gradient(top, #dbff87 0%,#e0f49c 45%,#bddd49 100%); - background: linear-gradient(to bottom, #dbff87 0%,#e0f49c 45%,#bddd49 100%); - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#dbff87', endColorstr='#bddd49',GradientType=0 ); -} -.uploadError .stripes,.uploadComplete .stripes{display:none;} -.removeUpload{ - background:url(../images/sprite.png) no-repeat 6px -421px; - position:absolute; - right:0; - top:0; - height: 100%; - width:20px; - cursor:pointer; -} -/* END OF Upload dialog */ \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/main.min.css b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/main.min.css deleted file mode 100644 index 98c23d28ed..0000000000 --- a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/main.min.css +++ /dev/null @@ -1 +0,0 @@ -body{font-family:Verdana;font-size:11px;padding:0;margin:0;background:#FFF;height:100%;width:100%}a{text-decoration:none;color:#d70}ul{list-style:none;margin:0;padding:0}#wraper{width:100%;height:100%}#wraper,#dlgNewDir,#dlgFileProp,#dlgAddFile,#menuDir,#menuFile,#pnlRenameFile,#pnlLoadingDirs,#pnlDirName,#pnlLoading,#pnlEmptyDir,#pnlSearchNoFiles,#pnlFileProp,#pnlDirList ul{display:none}.contextMenu a:hover,#pnlFileList .selected,#pnlFileList .selected:hover,#menuFile a:hover{background-color:#009!important;color:#FFF}#pnlDirList .selected,#pnlDirList div.selected:hover{background-color:#DFD!important}.dialog input{width:97%}.filesize{font-weight:bold;margin-top:4px;display:inline-block}.pnlDragFile,.pnlDragDir{background:#FFF;opacity:.8;float:left;width:auto!important}#pnlStatus{padding:5px}.scrollPane{height:400px;overflow:auto}.bottomLine{background:#DDD;border-top:1px solid #CCC}.pale{opacity:.5}#txtSearch,#ddlOrder{border:1px solid #CCC;border-radius:3px}#txtSearch{padding:0 22px 0 3px;background:url(../images/sprite.png) no-repeat 87px -314px;width:80px;height:20px}input[type=text]::-ms-clear{display:none}.pnlDirs{width:260px;overflow:auto;border-right:1px solid #DDD}#pnlDirList{margin-left:4px}#pnlDirList ul{padding:0 0 0 20px}#pnlDirList .dir{line-height:30px;float:left;margin-right:5px}#pnlDirList .dirPlus{float:left;margin:3px 3px 0 0}#pnlDirList li,#pnlDirList div{float:left;clear:both;width:98%}#pnlDirList div{padding:3px;cursor:pointer}#pnlLoadingDirs{margin-left:5px}.pnlFiles{padding-left:4px}#pnlFileList .icon{float:left;margin-right:3px}#pnlFileList li{float:left;padding:3px;clear:both;width:98%;cursor:pointer}#pnlFileList li:hover,#pnlDirList li div:hover{background:#ffc!important}#pnlFileList .size{float:right;margin-left:20px}#pnlFileList .time{float:right;margin-left:15px;width:130px}.imgPreview{max-width:300px}.contextMenu{position:absolute;background-color:#DDD;border:1px solid #BBB;min-width:100px;font-weight:bold;z-index:1000!important}.contextMenu a{background-repeat:no-repeat;background-position:4px 6px;padding:5px 25px 5px 20px;display:block;color:#000;text-decoration:none}hr{margin:0;color:#AAA}.actions{padding:3px 0 10px 5px}.actions input{background-repeat:no-repeat;background-position:3px center;padding:0 3px 0 18px;height:22px;font-size:11px;border:1px solid #CCC;background-color:#EEE;border-radius:3px;margin-bottom:3px}.actions input[type=button]:hover,.actions input.selected{background-color:#DFD;cursor:pointer}#btnAddDir{background-image:url(../images/sprite.png);background-position:4px -213px}#mnuCreateDir{background-image:url(../images/sprite.png);background-position:4px -210px}#mnuDownloadDir{background-image:url(../images/sprite.png);background-position:4px 7px}#btnDeleteDir{background-image:url(../images/sprite.png);background-position:4px -237px}#mnuDeleteDir{background-image:url(../images/sprite.png);background-position:4px -235px}#btnAddFile{background-image:url(../images/sprite.png);background-position:4px -81px}#btnDeleteFile{background-image:url(../images/sprite.png);background-position:4px -104px}#mnuDeleteFile{background-image:url(../images/sprite.png);background-position:4px -102px}#btnDeleteFile{margin-right:15px}#btnDownloadFile{background-image:url(../images/sprite.png);background-position:4px -128px}#mnuDownload{background-image:url(../images/sprite.png);background-position:4px -126px}#btnPreviewFile{background-image:url(../images/sprite.png);background-position:4px -285px}#mnuPreview{background-image:url(../images/sprite.png);background-position:4px -283px}#btnSelectFile{background-image:url(../images/sprite.png);background-position:4px -399px}#mnuSelectFile{background-image:url(../images/sprite.png);background-position:4px -397px}#btnThumbView{background-image:url(../images/sprite.png);background-position:center -447px}#btnListView{background-image:url(../images/sprite.png);background-position:center -483px}#btnRenameFile,#btnRenameDir{background-image:url(../images/sprite.png);background-position:4px 4px}#mnuRenameDir,#mnuRenameFile{background-image:url(../images/sprite.png);background-position:4px 7px}#mnuDirCut,#mnuFileCut{background-image:url(../images/sprite.png);background-position:4px -47px}#mnuDirCopy,#mnuFileCopy{background-image:url(../images/sprite.png);background-position:4px -20px}#mnuDirPaste,#mnuFilePaste{background-image:url(../images/sprite.png);background-position:4px -370px}ul#pnlFileList.thumbView li{float:left!important;clear:none;margin:0 6px 6px 0;overflow:hidden;padding:10px;text-align:center;vertical-align:middle;border:1px solid #EEE;border-radius:5px;position:relative;background:#EEE}ul#pnlFileList.thumbView .name,ul#pnlFileList.thumbView .time{display:none}ul#pnlFileList.thumbView .icon{clear:both;float:none;margin:0!important;text-align:center;vertical-align:middle;background-position:center center;background-repeat:no-repeat}ul#pnlFileList.thumbView .size{display:block!important;width:95%!important;position:absolute;bottom:5px;margin:0}#fileUploads{width:100%}.uploadFilesList{width:100%;height:300px;overflow:auto}div.ui-tooltip{max-width:90%}.uploadFilesList{font-size:11px}.fileUpload{margin:8px 0 0 0;position:relative;border:1px solid #EEE}.fileUpload .error{font-weight:bold}.fileName{z-index:2;line-height:20px;height:20px;overflow:hidden;padding:0 13px 0 5px}.progressPercent{z-index:2}.stripes{background:url('../images/stripes.gif') repeat-x;-moz-opacity:.40;-khtml-opacity:.40;opacity:.40;-ms-filter:alpha(opacity=40);filter:alpha(opacity=40);filter:alpha(opacity=40);width:100%;height:100%;z-index:-1;position:relative}.uploadProgress{position:absolute;width:0;height:100%;top:0;left:0;z-index:-1;background:#d3edff;background:-moz-linear-gradient(top,#d3edff 0,#cbebff 47%,#a1dbff 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#d3edff),color-stop(47%,#cbebff),color-stop(100%,#a1dbff));background:-webkit-linear-gradient(top,#d3edff 0,#cbebff 47%,#a1dbff 100%);background:-o-linear-gradient(top,#d3edff 0,#cbebff 47%,#a1dbff 100%);background:-ms-linear-gradient(top,#d3edff 0,#cbebff 47%,#a1dbff 100%);background:linear-gradient(to bottom,#d3edff 0,#cbebff 47%,#a1dbff 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#d3edff',endColorstr='#a1dbff',GradientType=0)}.uploadError{background:#ff8787;background:-moz-linear-gradient(top,#ff8787 0,#ffa3a3 45%,#ff5e5e 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#ff8787),color-stop(45%,#ffa3a3),color-stop(100%,#ff5e5e));background:-webkit-linear-gradient(top,#ff8787 0,#ffa3a3 45%,#ff5e5e 100%);background:-o-linear-gradient(top,#ff8787 0,#ffa3a3 45%,#ff5e5e 100%);background:-ms-linear-gradient(top,#ff8787 0,#ffa3a3 45%,#ff5e5e 100%);background:linear-gradient(to bottom,#ff8787 0,#ffa3a3 45%,#ff5e5e 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff8787',endColorstr='#ff5e5e',GradientType=0)}.uploadComplete{background:#dbff87;background:-moz-linear-gradient(top,#dbff87 0,#e0f49c 45%,#bddd49 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#dbff87),color-stop(45%,#e0f49c),color-stop(100%,#bddd49));background:-webkit-linear-gradient(top,#dbff87 0,#e0f49c 45%,#bddd49 100%);background:-o-linear-gradient(top,#dbff87 0,#e0f49c 45%,#bddd49 100%);background:-ms-linear-gradient(top,#dbff87 0,#e0f49c 45%,#bddd49 100%);background:linear-gradient(to bottom,#dbff87 0,#e0f49c 45%,#bddd49 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#dbff87',endColorstr='#bddd49',GradientType=0)}.uploadError .stripes,.uploadComplete .stripes{display:none}.removeUpload{background:url(../images/sprite.png) no-repeat 6px -421px;position:absolute;right:0;top:0;height:100%;width:20px;cursor:pointer} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/main.scss b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/main.scss new file mode 100644 index 0000000000..30227cab61 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/css/main.scss @@ -0,0 +1,406 @@ +@import '../../_variables.scss'; +@import '../../../../Content/bs4/scss/_functions.scss'; +@import '../../../../Content/bs4/scss/_variables.scss'; + +$fi-selected-bg: $gray-400; +$fi-selected-bg: $component-active-bg; + +body { + background-color: #fff; +} + +ul { + list-style: none; + margin: 0; + padding: 0; +} + +#wraper { + width: 100%; + height: 100%; +} + +#wraper, +#dlgNewDir, +#dlgFileProp, +#dlgAddFile, +#menuDir, +#menuFile, +#pnlRenameFile, +#pnlLoadingDirs, +#pnlDirName, +#pnlLoading, +#pnlEmptyDir, +#pnlSearchNoFiles, +#pnlFileProp, +#pnlDirList ul { + display: none; +} + +#pnlFileList { + .selected, + .selected:hover { + background-color: $fi-selected-bg !important; + } + + .icon { + float: left; + margin-right: 4px; + + > .thumb { + display: none; + } + } + + li { + float: left; + padding: 4px 4px 4px 8px; + clear: both; + width: 99%; + cursor: pointer; + } + + .size { + float: right; + margin-left: 22px; + white-space: nowrap; + } + + .time { + float: right; + margin-left: 22px; + width: 130px; + white-space: nowrap; + } + + &.thumbView { + padding: 3px; + + li { + //width:140px; + //height: 140px; + float: left !important; + clear: none; + margin: 3px; + overflow: hidden; + padding: 10px; + text-align: center; + vertical-align: middle; + border: 1px solid transparent; + position: relative; + + &:hover { + background-color: $gray-200; + } + + &:hover, + &.selected { + border-color: rgba(#000, 0.08); + } + + .file-icon { + font-size: 84px; + } + + &.file-image { + .file-icon { + display: none; + } + + .thumb { + display: inline-block; + } + } + } + + .time, + .size { + display: none; + } + + .icon { + display: flex; + align-items: center; + justify-content: center; + width: 140px; + height: 120px; + margin: 0; + padding: 0; + position: relative; + float: none; + + > img { + max-width: 100%; + height: auto; + } + + > img.thumb { + box-shadow: 2px 2px 6px rgba(#000, 0.25); + } + } + + .name { + display: block !important; + font-size: 12px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + //width: 100% !important; + position: absolute; + left: 4px; + right: 4px; + bottom: 5px; + margin: 0; + } + } +} + +.filesize { + font-weight: 600; + margin-top: 4px; + display: inline-block; +} + +.pnlDragFile, +.pnlDragDir { + background: #fff; + opacity: 0.8; + float: left; + width: auto !important; + z-index: 999999; + position: absolute; +} + +#pnlStatus { + padding: 5px; +} + +.scrollPane { + width: 100%; + height: 400px; + overflow: auto; + position: relative; +} + +.bottomLine { + background: $gray-100; + border-top: 1px solid rgba(#000, 0.12); + height: 36px; +} + +input[type=text]::-ms-clear { + display: none; +} + + +td.pnlDirs { + width: 260px; + border-right: 1px solid rgba(0,0,0, 0.12); + + .scrollPane { + width: 260px; + } +} + +#pnlDirList { + //width: initial; + position: relative; + + .dir-item { + padding: 3px 0.4rem; + cursor: pointer; + position: relative; + float: left; + min-width: 100%; + + .dirPlus { + font-size: 10px; + width: 12px; + height: 10px; + margin-top: 1px; + margin-right: 8px; + text-align: center; + } + + .dir { + padding-top: 2px; + } + + .name { + white-space: nowrap; + display: block; + } + + &:hover { + background-color: $gray-200; + } + + &.selected, + &.selected:hover { + background-color: $fi-selected-bg !important; + } + + &.indeterm { + background-color: $gray-200 !important; + } + + &.dragover { + background-color: $gray-200 !important; + } + } + + ul .dir-item { + padding-left: 1.4rem; + } + + ul ul .dir-item { + padding-left: 2.4rem; + } + + ul ul ul .dir-item { + padding-left: 3.4rem; + } + + ul ul ul ul .dir-item { + padding-left: 4.4rem; + } + + ul ul ul ul ul .dir-item { + padding-left: 5.4rem; + } + + ul ul ul ul ul ul .dir-item { + padding-left: 6.4rem; + } + + ul ul ul ul ul ul ul .dir-item { + padding-left: 7.4rem; + } + + ul ul ul ul ul ul ul ul .dir-item { + padding-left: 8.4rem; + } + + ul ul ul ul ul ul ul ul ul .dir-item { + padding-left: 9.4rem; + } +} + +#pnlLoadingDirs { + margin-left: 5px; +} + + +.imgPreview { + max-width: 300px; +} + + +.actions { + padding: 3px 0 10px 5px; + background-color: $gray-100; + border-bottom: 1px solid rgba(0,0,0, 0.1); + + > .btn-group { + padding-right: 5px; + margin-right: 5px; + border-right: 1px solid rgba(0, 0, 0, 0.12); + } + + > .btn-group:last-child { + padding-right: 0; + margin-right: 0; + border-right: none; + } +} + + +#fileUploads { + width: 100%; +} + +.uploadFilesList { + width: 100%; + height: 300px; + overflow: auto; +} + +#uploadResult { +} + +div.ui-tooltip { + max-width: 90%; +} + +/* Upload dialog */ +.uploadFilesList { + font-size: 12px; +} + +.fileUpload { + margin: 8px 0 0 0; + position: relative; + border: 1px solid #EEE; + + .error { + font-weight: 600; + } +} + + + +.fileName { + z-index: 2; + line-height: 20px; + height: 20px; + overflow: hidden; + padding: 0 13px 0 5px; +} + +.progressPercent { + z-index: 2; +} + +.stripes { + background: url('../images/stripes.gif') repeat-x; + opacity: 0.40; + width: 100%; + height: 100%; + z-index: -1; + position: relative; +} + +.uploadProgress { + position: absolute; + width: 0; + height: 100%; + top: 0; + left: 0; + z-index: -1; + background: #d3edff; + background: linear-gradient(to bottom, #d3edff 0%,#cbebff 47%,#a1dbff 100%); +} + +.uploadError { + background: #ff8787; + background: linear-gradient(to bottom, #ff8787 0%,#ffa3a3 45%,#ff5e5e 100%); +} + +.uploadComplete { + background: #dbff87; + background: linear-gradient(to bottom, #dbff87 0%,#e0f49c 45%,#bddd49 100%); +} + +.uploadError .stripes, .uploadComplete .stripes { + display: none; +} + +.removeUpload { + background: url(../images/sprite.png) no-repeat 6px -421px; + position: absolute; + right: 0; + top: 0; + height: 100%; + width: 20px; + cursor: pointer; +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/dev.html b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/dev.html deleted file mode 100644 index 6dc6cdd12b..0000000000 --- a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/dev.html +++ /dev/null @@ -1,153 +0,0 @@ - - - - - -Roxy file manager - - - - - - - - - - - - - - - - - - - - - - - -
                        -
                        - - - -
                        -
                        - Loading directories...
                        - -
                        -
                        -
                          -
                          -
                          - - -
                          - - - - - - -
                          - Order by: -    - -    - -
                          -
                          -
                          -
                          - Loading files...
                          - -
                          -
                          - This folder is empty -
                          -
                          - No files found -
                          -
                            -
                            -
                            -
                            -    © 2013 - RoxyFileman - -
                            Status bar
                            -
                            - - - -
                            -
                            - -

                            - -
                            -
                            -
                            -
                            -
                            -
                            -
                            - - -
                            -
                            - -
                            -
                            -
                            - -
                            - - - diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/action-folder-paste.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/action-folder-paste.png deleted file mode 100644 index 438a639c8e..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/action-folder-paste.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/action-folder-rename.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/action-folder-rename.png deleted file mode 100644 index 34ec67629a..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/action-folder-rename.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/arrow_down.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/arrow_down.png deleted file mode 100644 index 691f6e0c7c..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/arrow_down.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/arrow_up.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/arrow_up.png deleted file mode 100644 index 30d005f256..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/arrow_up.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/copy.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/copy.png deleted file mode 100644 index 249dcc510a..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/copy.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/cut.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/cut.png deleted file mode 100644 index d2576bd7ae..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/cut.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/dir-minus.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/dir-minus.png deleted file mode 100644 index 61b95f8cb7..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/dir-minus.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/dir-plus.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/dir-plus.png deleted file mode 100644 index 2f08324416..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/dir-plus.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-add.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-add.png deleted file mode 100644 index 4fb28bf723..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-add.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-delete.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-delete.png deleted file mode 100644 index dd1c0a4f8d..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-delete.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-download.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-download.png deleted file mode 100644 index e01eabbb10..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-download.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-duplicate.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-duplicate.png deleted file mode 100644 index 34106e0c08..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/file-duplicate.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_3gp.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_3gp.png deleted file mode 100644 index 35a05dd0a4..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_3gp.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_7z.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_7z.png deleted file mode 100644 index 5ed205bb95..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_7z.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ace.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ace.png deleted file mode 100644 index 799604d967..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ace.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ai.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ai.png deleted file mode 100644 index 078057f6f9..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ai.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_aif.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_aif.png deleted file mode 100644 index 02ba441724..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_aif.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_aiff.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_aiff.png deleted file mode 100644 index 45f6c27ef5..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_aiff.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_amr.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_amr.png deleted file mode 100644 index 4c30c8ce26..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_amr.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_asf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_asf.png deleted file mode 100644 index f65286f422..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_asf.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_asx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_asx.png deleted file mode 100644 index 9ac440b4c8..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_asx.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bat.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bat.png deleted file mode 100644 index ba72c7f896..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bat.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bin.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bin.png deleted file mode 100644 index adc7af36c7..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bin.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bmp.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bmp.png deleted file mode 100644 index 485cde8032..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bmp.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bup.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bup.png deleted file mode 100644 index 5e25354d13..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_bup.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cab.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cab.png deleted file mode 100644 index 0e19a97312..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cab.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cbr.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cbr.png deleted file mode 100644 index 37d886aa04..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cbr.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cda.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cda.png deleted file mode 100644 index c50b7519c8..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cda.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cdl.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cdl.png deleted file mode 100644 index cb57905683..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cdl.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cdr.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cdr.png deleted file mode 100644 index d6def9e36c..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_cdr.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_chm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_chm.png deleted file mode 100644 index 7a993614b7..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_chm.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dat.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dat.png deleted file mode 100644 index 9567f6af54..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dat.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_divx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_divx.png deleted file mode 100644 index 99cb983eca..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_divx.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dll.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dll.png deleted file mode 100644 index 7ac35c9846..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dll.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dmg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dmg.png deleted file mode 100644 index a2c644bddc..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dmg.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_doc.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_doc.png deleted file mode 100644 index 8738d2eb21..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_doc.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dss.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dss.png deleted file mode 100644 index d51df3c293..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dss.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dvf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dvf.png deleted file mode 100644 index 62bbb95aa4..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dvf.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dwg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dwg.png deleted file mode 100644 index 0199681774..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_dwg.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_eml.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_eml.png deleted file mode 100644 index 6c973fcde8..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_eml.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_eps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_eps.png deleted file mode 100644 index 009582ced9..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_eps.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_exe.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_exe.png deleted file mode 100644 index c9cec75704..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_exe.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_fla.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_fla.png deleted file mode 100644 index 648b1d0735..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_fla.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_flv.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_flv.png deleted file mode 100644 index ccc1eb7f31..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_flv.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_gif.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_gif.png deleted file mode 100644 index b1aa6c3d10..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_gif.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_gz.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_gz.png deleted file mode 100644 index d4517e1c16..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_gz.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_hqx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_hqx.png deleted file mode 100644 index ae7cc0620d..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_hqx.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_htm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_htm.png deleted file mode 100644 index 061ff46943..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_htm.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_html.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_html.png deleted file mode 100644 index d86548cd52..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_html.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ifo.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ifo.png deleted file mode 100644 index 89b0166a4c..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ifo.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_indd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_indd.png deleted file mode 100644 index 0cbaadc72b..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_indd.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_iso.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_iso.png deleted file mode 100644 index e8df06db98..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_iso.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_jar.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_jar.png deleted file mode 100644 index 383aea4fa1..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_jar.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_jpeg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_jpeg.png deleted file mode 100644 index 68e38ab252..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_jpeg.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_jpg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_jpg.png deleted file mode 100644 index 39be8180d7..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_jpg.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_lnk.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_lnk.png deleted file mode 100644 index 2b05f43030..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_lnk.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_log.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_log.png deleted file mode 100644 index bc99e85cf4..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_log.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4a.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4a.png deleted file mode 100644 index d7c86c3c7d..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4a.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4b.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4b.png deleted file mode 100644 index 8a73d4e5aa..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4b.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4p.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4p.png deleted file mode 100644 index f9d90b924c..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4p.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4v.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4v.png deleted file mode 100644 index c7b0b1f7e9..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_m4v.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mcd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mcd.png deleted file mode 100644 index c268b87dff..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mcd.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mdb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mdb.png deleted file mode 100644 index 7b7b83611d..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mdb.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mid.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mid.png deleted file mode 100644 index 4d3e482836..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mid.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mov.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mov.png deleted file mode 100644 index 6a9186516f..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mov.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mp2.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mp2.png deleted file mode 100644 index bbc5f049c6..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mp2.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mp3.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mp3.png deleted file mode 100644 index 137afabfff..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mp3.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mp4.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mp4.png deleted file mode 100644 index caa154cea3..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mp4.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mpeg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mpeg.png deleted file mode 100644 index 81994a291a..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mpeg.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mpg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mpg.png deleted file mode 100644 index 948b643180..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mpg.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_msi.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_msi.png deleted file mode 100644 index 97a8a3b191..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_msi.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mswmm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mswmm.png deleted file mode 100644 index d70aaa75ba..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_mswmm.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ogg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ogg.png deleted file mode 100644 index a6b55f6cc2..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ogg.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pdf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pdf.png deleted file mode 100644 index 04423b4965..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pdf.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_png.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_png.png deleted file mode 100644 index 76230d3060..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_png.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pps.png deleted file mode 100644 index 44a2d2c7e8..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pps.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ps.png deleted file mode 100644 index 0e4b20ae0f..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ps.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_psd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_psd.png deleted file mode 100644 index b98ff86015..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_psd.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pst.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pst.png deleted file mode 100644 index 4f5f61f424..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pst.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ptb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ptb.png deleted file mode 100644 index a3568dd4d5..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ptb.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pub.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pub.png deleted file mode 100644 index 4a71c01b60..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_pub.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_qbb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_qbb.png deleted file mode 100644 index 24fc0ae534..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_qbb.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_qbw.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_qbw.png deleted file mode 100644 index 162b0fb9b5..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_qbw.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_qxd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_qxd.png deleted file mode 100644 index f5e46cff8a..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_qxd.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ram.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ram.png deleted file mode 100644 index a55ba848a1..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ram.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rar.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rar.png deleted file mode 100644 index 934f18247f..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rar.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rm.png deleted file mode 100644 index 639e180215..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rm.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rmvb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rmvb.png deleted file mode 100644 index 362ffdfce1..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rmvb.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rtf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rtf.png deleted file mode 100644 index cae2c95cff..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_rtf.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_sea.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_sea.png deleted file mode 100644 index d9906e2e0d..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_sea.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ses.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ses.png deleted file mode 100644 index b62459b768..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ses.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_sit.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_sit.png deleted file mode 100644 index 629270d3f1..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_sit.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_sitx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_sitx.png deleted file mode 100644 index 4c7a0855e9..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_sitx.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ss.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ss.png deleted file mode 100644 index a3a1dbcf73..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ss.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_swf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_swf.png deleted file mode 100644 index 3de371311f..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_swf.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_tgz.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_tgz.png deleted file mode 100644 index b896b27673..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_tgz.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_thm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_thm.png deleted file mode 100644 index 0f6bbae201..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_thm.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_tif.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_tif.png deleted file mode 100644 index c7d4da88f7..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_tif.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_tmp.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_tmp.png deleted file mode 100644 index 75e014ee90..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_tmp.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_torrent.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_torrent.png deleted file mode 100644 index 6e8003c424..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_torrent.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ttf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ttf.png deleted file mode 100644 index dda399e3df..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_ttf.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_txt.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_txt.png deleted file mode 100644 index 1e7c12f801..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_txt.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_vcd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_vcd.png deleted file mode 100644 index d066ecbbeb..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_vcd.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_vob.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_vob.png deleted file mode 100644 index 2de5bed7d3..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_vob.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wav.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wav.png deleted file mode 100644 index a8d7b142d7..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wav.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wma.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wma.png deleted file mode 100644 index e699f0baac..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wma.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wmv.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wmv.png deleted file mode 100644 index 98001f5451..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wmv.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wps.png deleted file mode 100644 index 0e7cbc05cc..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_wps.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_xls.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_xls.png deleted file mode 100644 index 4a394e527d..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_xls.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_xpi.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_xpi.png deleted file mode 100644 index 4ff58d7e42..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_xpi.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_zip.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_zip.png deleted file mode 100644 index 3b1b54fd45..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/file_extension_zip.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/unknown.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/unknown.png deleted file mode 100644 index 098859c245..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/big/unknown.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_3gp.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_3gp.png deleted file mode 100644 index 4065bdfd90..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_3gp.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_7z.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_7z.png deleted file mode 100644 index 81e33ebe16..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_7z.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ace.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ace.png deleted file mode 100644 index 912abbd9be..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ace.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ai.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ai.png deleted file mode 100644 index 762346076e..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ai.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_aif.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_aif.png deleted file mode 100644 index 9edd1c0f4f..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_aif.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_aiff.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_aiff.png deleted file mode 100644 index 6bd4ab7a60..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_aiff.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_amr.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_amr.png deleted file mode 100644 index 9fa593557f..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_amr.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_asf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_asf.png deleted file mode 100644 index 2ff894edd6..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_asf.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_asx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_asx.png deleted file mode 100644 index 28f610a6df..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_asx.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bat.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bat.png deleted file mode 100644 index 1edba7688f..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bat.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bin.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bin.png deleted file mode 100644 index 4c5411efb8..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bin.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bmp.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bmp.png deleted file mode 100644 index 42aa0026f2..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bmp.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bup.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bup.png deleted file mode 100644 index ce04201323..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_bup.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cab.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cab.png deleted file mode 100644 index 74aef831b7..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cab.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cbr.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cbr.png deleted file mode 100644 index 9b79766cc3..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cbr.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cda.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cda.png deleted file mode 100644 index e9045442ff..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cda.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cdl.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cdl.png deleted file mode 100644 index e52bc641f1..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cdl.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cdr.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cdr.png deleted file mode 100644 index 277b23d0e5..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_cdr.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_chm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_chm.png deleted file mode 100644 index 3d7b07c515..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_chm.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dat.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dat.png deleted file mode 100644 index 758a0e1c1a..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dat.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_divx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_divx.png deleted file mode 100644 index 204c9d0c8b..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_divx.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dll.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dll.png deleted file mode 100644 index 49111a90be..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dll.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dmg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dmg.png deleted file mode 100644 index d8d6a81c16..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dmg.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_doc.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_doc.png deleted file mode 100644 index dfd46f9ce9..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_doc.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dss.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dss.png deleted file mode 100644 index 4752a952c4..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dss.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dvf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dvf.png deleted file mode 100644 index 27e4c2358b..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dvf.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dwg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dwg.png deleted file mode 100644 index 9dd4c903fb..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_dwg.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_eml.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_eml.png deleted file mode 100644 index e6f3174fb0..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_eml.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_eps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_eps.png deleted file mode 100644 index 919089b353..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_eps.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_exe.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_exe.png deleted file mode 100644 index 09bbde7eaf..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_exe.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_fla.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_fla.png deleted file mode 100644 index 81f80e9a41..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_fla.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_flv.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_flv.png deleted file mode 100644 index 043623c4d8..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_flv.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_gif.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_gif.png deleted file mode 100644 index efa8206090..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_gif.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_gz.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_gz.png deleted file mode 100644 index f391025d7e..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_gz.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_hqx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_hqx.png deleted file mode 100644 index e1fe0bee23..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_hqx.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_htm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_htm.png deleted file mode 100644 index bcd6f0e15c..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_htm.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_html.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_html.png deleted file mode 100644 index a78b68e37b..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_html.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ifo.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ifo.png deleted file mode 100644 index 541c14efc0..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ifo.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_indd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_indd.png deleted file mode 100644 index 812e3c012b..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_indd.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_iso.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_iso.png deleted file mode 100644 index f1e060e539..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_iso.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_jar.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_jar.png deleted file mode 100644 index f77a21c38f..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_jar.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_jpeg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_jpeg.png deleted file mode 100644 index a69dff9948..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_jpeg.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_jpg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_jpg.png deleted file mode 100644 index 6ec08d7d49..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_jpg.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_lnk.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_lnk.png deleted file mode 100644 index 8306dbbfcf..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_lnk.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_log.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_log.png deleted file mode 100644 index ae294cfaa0..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_log.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4a.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4a.png deleted file mode 100644 index 9518f75d47..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4a.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4b.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4b.png deleted file mode 100644 index f0888c7393..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4b.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4p.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4p.png deleted file mode 100644 index a7d89042a3..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4p.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4v.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4v.png deleted file mode 100644 index cf0f2cfed9..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_m4v.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mcd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mcd.png deleted file mode 100644 index 403ecad8be..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mcd.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mdb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mdb.png deleted file mode 100644 index a74b16d60b..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mdb.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mid.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mid.png deleted file mode 100644 index 07887a064b..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mid.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mov.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mov.png deleted file mode 100644 index 75075c7819..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mov.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mp2.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mp2.png deleted file mode 100644 index 704a347e04..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mp2.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mp3.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mp3.png deleted file mode 100644 index d2624de030..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mp3.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mp4.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mp4.png deleted file mode 100644 index ecc3d28342..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mp4.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mpeg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mpeg.png deleted file mode 100644 index 6518619a22..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mpeg.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mpg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mpg.png deleted file mode 100644 index d01440fe28..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mpg.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_msi.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_msi.png deleted file mode 100644 index 3f53d37854..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_msi.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mswmm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mswmm.png deleted file mode 100644 index f8c4fc8268..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_mswmm.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ogg.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ogg.png deleted file mode 100644 index 874915f710..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ogg.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pdf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pdf.png deleted file mode 100644 index ef52e6a4d8..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pdf.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_png.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_png.png deleted file mode 100644 index 812e3c012b..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_png.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pps.png deleted file mode 100644 index 3964c49235..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pps.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ps.png deleted file mode 100644 index 9bd9e03df4..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ps.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_psd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_psd.png deleted file mode 100644 index d3f6ec562b..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_psd.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pst.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pst.png deleted file mode 100644 index 5da647e5d0..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pst.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ptb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ptb.png deleted file mode 100644 index 8250def1cf..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ptb.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pub.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pub.png deleted file mode 100644 index 806b8ba37a..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_pub.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_qbb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_qbb.png deleted file mode 100644 index 5e4d56b863..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_qbb.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_qbw.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_qbw.png deleted file mode 100644 index 7e20ff067f..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_qbw.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_qxd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_qxd.png deleted file mode 100644 index 577f6efd0d..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_qxd.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ram.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ram.png deleted file mode 100644 index 18a73cd5c0..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ram.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rar.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rar.png deleted file mode 100644 index 6a94dd89cd..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rar.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rm.png deleted file mode 100644 index ca0983bbd9..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rm.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rmvb.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rmvb.png deleted file mode 100644 index 9c533a0505..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rmvb.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rtf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rtf.png deleted file mode 100644 index a7efed7e31..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_rtf.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_sea.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_sea.png deleted file mode 100644 index 03f87f879a..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_sea.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ses.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ses.png deleted file mode 100644 index a85638623e..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ses.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_sit.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_sit.png deleted file mode 100644 index 98206fc2e3..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_sit.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_sitx.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_sitx.png deleted file mode 100644 index 3c3bb4c44e..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_sitx.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ss.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ss.png deleted file mode 100644 index 7d056d0241..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ss.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_swf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_swf.png deleted file mode 100644 index 5650971b09..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_swf.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_tgz.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_tgz.png deleted file mode 100644 index 5253aab3d0..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_tgz.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_thm.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_thm.png deleted file mode 100644 index b3acbb1c8c..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_thm.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_tif.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_tif.png deleted file mode 100644 index a284f79764..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_tif.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_tmp.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_tmp.png deleted file mode 100644 index 80c165b033..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_tmp.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_torrent.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_torrent.png deleted file mode 100644 index 09de7ab1d4..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_torrent.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ttf.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ttf.png deleted file mode 100644 index 51a0bbb6ef..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_ttf.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_txt.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_txt.png deleted file mode 100644 index e3bed85703..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_txt.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_vcd.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_vcd.png deleted file mode 100644 index 5380d08d00..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_vcd.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_vob.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_vob.png deleted file mode 100644 index 5a5dde849b..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_vob.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wav.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wav.png deleted file mode 100644 index 6897534d98..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wav.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wma.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wma.png deleted file mode 100644 index 63f5d343b5..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wma.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wmv.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wmv.png deleted file mode 100644 index 4017f86712..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wmv.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wps.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wps.png deleted file mode 100644 index 69154a0218..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_wps.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_xls.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_xls.png deleted file mode 100644 index a5cb228dde..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_xls.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_xpi.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_xpi.png deleted file mode 100644 index dea5e195ad..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_xpi.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_zip.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_zip.png deleted file mode 100644 index f0756cd29c..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/file_extension_zip.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/unknown.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/unknown.png deleted file mode 100644 index 3ea3d5d6b5..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/filetypes/unknown.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/find.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/find.png deleted file mode 100644 index 1d6f4f13fc..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/find.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-add.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-add.png deleted file mode 100644 index 537184e705..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-add.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-delete.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-delete.png deleted file mode 100644 index ae496495d1..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-delete.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-download.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-download.png deleted file mode 100644 index e307dec1ca..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/folder-download.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/paste.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/paste.png deleted file mode 100644 index 468189e950..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/paste.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/preview.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/preview.png deleted file mode 100644 index ef29588dc6..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/preview.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/remove-upload - Copy.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/remove-upload - Copy.png deleted file mode 100644 index a93bdf82c1..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/remove-upload - Copy.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/rename.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/rename.png deleted file mode 100644 index 34ec67629a..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/rename.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/search.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/search.png deleted file mode 100644 index b81b18de12..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/search.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/select.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/select.png deleted file mode 100644 index dc951d454a..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/select.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/view-list.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/view-list.png deleted file mode 100644 index 8a08b5224d..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/view-list.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/view-tile.png b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/view-tile.png deleted file mode 100644 index 4a0c64e663..0000000000 Binary files a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/images/view-tile.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/index.html b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/index.html deleted file mode 100644 index 101c734e29..0000000000 --- a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/index.html +++ /dev/null @@ -1,147 +0,0 @@ - - - - - -Roxy file manager - - - - - - - - - - - - - - - - - - -
                            -
                            - - - -
                            -
                            - Loading directories...
                            - -
                            -
                            -
                              -
                              -
                              - - -
                              - - - - - - -
                              - Order by: -    - -    - -
                              -
                              -
                              -
                              - Loading files...
                              - -
                              -
                              - This folder is empty -
                              -
                              - No files found -
                              -
                                -
                                -
                                -
                                -    © 2013 - RoxyFileman - -
                                Status bar
                                -
                                - - - -
                                -
                                - -

                                - -
                                -
                                -
                                -
                                -
                                -
                                -
                                - - -
                                -
                                - -
                                -
                                -
                                - -
                                - - - \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/custom.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/custom.js index 366bf704dd..2a00689549 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/custom.js +++ b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/custom.js @@ -19,27 +19,51 @@ Contact: Lyubomir Arsov, liubo (at) web-lobby.com */ -function FileSelected(file){ - /** - * file is an object containing following properties: - * - * fullPath - path to the file - absolute from your site root - * path - directory in which the file is located - absolute from your site root - * size - size of the file in bytes - * time - timestamo of last modification - * name - file name - * ext - file extension - * width - if the file is image, this will be the width of the original image, 0 otherwise - * height - if the file is image, this will be the height of the original image, 0 otherwise - * - */ - alert('"' + file.fullPath + "\" selected.\n To integrate with CKEditor or TinyMCE change INTEGRATION setting in conf.json. For more details see the Installation instructions at http://www.roxyfileman.com/install."); -} -function GetSelectedValue(){ - /** - * This function is called to retrieve selected value when custom integration is used. - * Url parameter selected will override this value. - */ - - return ""; +function FileSelected(file) { + /** + * file is an object containing following properties: + * + * fullPath - path to the file - absolute from your site root + * path - directory in which the file is located - absolute from your site root + * size - size of the file in bytes + * time - timestamp of last modification + * name - file name + * ext - file extension + * width - if the file is image, this will be the width of the original image, 0 otherwise + * height - if the file is image, this will be the height of the original image, 0 otherwise + * + */ + + var p = (window.opener || window.parent); + + // Set the value of field sent to Fileman via URL param "field". + var fieldId = RoxyUtils.GetUrlParam('field'); + //opener.document.getElementById(fieldId).value = file.fullPath; + p.window.jQuery('#' + fieldId).val(file.fullPath).trigger('change'); + + //// Set the source of an image which id is sent to Fileman via URL param "img". + // opener.document.getElementById(RoxyUtils.GetUrlParam('img')).src = file.fullPath; + + // Close file manager if it's opened in separate window. + if (window.opener) { + self.close(); + } + else { + // We put the modal dialog's ID in "mid" + p.window.closePopup(RoxyUtils.GetUrlParam('mid')); + } } + +function GetSelectedValue() { + /** + * This function is called to retrieve selected value when custom integration is used. + * Url parameter selected will override this value. + */ + + var p = (window.opener || window.parent); + var fieldId = RoxyUtils.GetUrlParam('field'); + + if (fieldId) { + return p.window.jQuery('#' + fieldId).val(); + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/directory.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/directory.js index 46c29faf62..0dd0b780c3 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/directory.js +++ b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/directory.js @@ -19,520 +19,590 @@ Contact: Lyubomir Arsov, liubo (at) web-lobby.com */ -function Directory(fullPath, numDirs, numFiles){ - if(!fullPath) fullPath = ''; - this.fullPath = fullPath; - this.name = RoxyUtils.GetFilename(fullPath); - if(!this.name) - this.name = 'My files'; - this.path = RoxyUtils.GetPath(fullPath); - this.dirs = (numDirs?numDirs:0); - this.files = (numFiles?numFiles:0); - this.filesList = new Array(); - this.Show = function(){ - var html = this.GetHtml(); - var el = null; - el = $('li[data-path="'+this.path+'"]'); - if(el.length == 0) - el = $('#pnlDirList'); - else{ - if(el.children('ul').length == 0) - el.append('
                                  '); - el = el.children('ul'); - } - if(el){ - el.append(html); - this.SetEvents(); - } - }; - this.SetEvents = function(){ - var el = this.GetElement(); - if(RoxyFilemanConf.MOVEDIR){ - el.draggable({helper:makeDragDir,start:startDragDir,cursorAt: { left: 10 ,top:10},delay:200}); - } - el = el.children('div'); - el.click(function(e){ - selectDir(this); - }); +$(function () { + $('#pnlDirList').on('contextmenu', '.dir-item', function (e) { + e.stopPropagation(); + e.preventDefault(); + closeMenus('file'); + selectDir(this, true); + var t = e.pageY; + var menuEnd = t + $('#menuDir').height() + 30; + if (menuEnd > $(window).height()) { + offset = menuEnd - $(window).height() + 30; + t -= offset; + } + if (t < 0) + t = 0; + $('#menuDir').css({ + top: t + 'px', + left: e.pageX + 'px' + }).show(); - el.bind('contextmenu', function(e) { - e.stopPropagation(); - e.preventDefault(); - closeMenus('file'); - selectDir(this); - var t = e.pageY - $('#menuDir').height(); - if(t < 0) - t = 0; - $('#menuDir').css({ - top: t+'px', - left: e.pageX+'px' - }).show(); + return false; + }); - return false; - }); + $('#pnlDirList').on('click', '.dir-item', function (e) { + e.preventDefault(); + selectDir(this); + }); - el.droppable({drop:moveObject,over:dragFileOver,out:dragFileOut}); - el = el.children('.dirPlus'); - el.click(function(e){ - e.stopPropagation(); - var d = Directory.Parse($(this).closest('li').attr('data-path')); - d.Expand(); - }); - }; - this.GetHtml = function(){ - var html = '
                                • '; - html += '
                                  '; - html += ''+this.name+' ('+this.files+')
                                  '; - html += '
                                • '; + $('#pnlDirList').on('click', '.dirPlus', function (e) { + e.stopPropagation(); + e.preventDefault(); + var d = Directory.Parse($(this).closest('li').attr('data-path')); + d.Expand(); + }); +}); - return html; - }; - this.SetStatusBar = function(){ - $('#pnlStatus').html(this.files+' '+(this.files == 1?t('file'):t('files'))); - }; - this.SetSelectedFile = function(path){ - if(path){ - var f = File.Parse(path); - if(f){ - selectFile(f.GetElement()); - } - } - }; - this.Select = function(selectedFile){ - var el = this.GetElement(); - el.children('div').addClass('selected'); - $('#pnlDirList li[data-path!="'+this.fullPath+'"] > div').removeClass('selected'); - el.children('img.dir').prop('src', 'images/folder.png'); - this.SetStatusBar(); - var p = this.GetParent(); - while(p){ - p.Expand(true); - p = p.GetParent(); - } - this.Expand(true); - this.ListFiles(true, selectedFile); - setLastDir(this.fullPath); - }; - this.GetElement = function(){ - return $('li[data-path="'+this.fullPath+'"]'); - }; - this.IsExpanded = function(){ - var el = this.GetElement().children('ul'); - return (el && el.is(":visible")); - }; - this.IsListed = function(){ - if($('#hdDir').val() == this.fullPath) - return true; - return false; - }; - this.GetExpanded = function(el){ - var ret = new Array(); - if(!el) - el = $('#pnlDirList'); - el.children('li').each(function(){ - var path = $(this).attr('data-path'); - var d = new Directory(path); - if(d){ - if(d.IsExpanded() && path) - ret.push(path); - ret = ret.concat(d.GetExpanded(d.GetElement().children('ul'))); - } - }); +function Directory(fullPath, numDirs, numFiles) { + if (!fullPath) fullPath = ''; + this.fullPath = fullPath; + this.name = RoxyUtils.GetFilename(fullPath); + if (!this.name) + this.name = 'My files'; + this.path = RoxyUtils.GetPath(fullPath); + this.dirs = (numDirs ? numDirs : 0); + this.files = (numFiles ? numFiles : 0); + this.filesList = []; - return ret; - }; - this.RestoreExpanded = function(expandedDirs){ - for(i = 0; i < expandedDirs.length; i++){ - var d = Directory.Parse(expandedDirs[i]); - if(d) - d.Expand(true); - } - }; - this.GetParent = function(){ - return Directory.Parse(this.path); - }; - this.SetOpened = function(){ - var li = this.GetElement(); - if(li.find('li').length < 1) - li.children('div').children('.dirPlus').prop('src', 'images/blank.gif'); - else if(this.IsExpanded()) - li.children('div').children('.dirPlus').prop('src', 'images/dir-minus.png'); - else - li.children('div').children('.dirPlus').prop('src', 'images/dir-plus.png'); - }; - this.Update = function(newPath){ - var el = this.GetElement(); - if(newPath){ - this.fullPath = newPath; - this.name = RoxyUtils.GetFilename(newPath); - if(!this.name) - this.name = 'My files'; - this.path = RoxyUtils.GetPath(newPath); - } - el.attr('data-path', this.fullPath); - el.attr('data-dirs', this.dirs); - el.attr('data-files', this.files); - el.children('div').children('.name').html(this.name+' ('+this.files+')'); - this.SetOpened(); - }; - this.LoadAll = function(selectedDir){ - var expanded = this.GetExpanded(); - var dirListURL = RoxyFilemanConf.DIRLIST; - if(!dirListURL){ - alert(t('E_ActionDisabled')); - return; - } - $('#pnlLoadingDirs').show(); - $('#pnlDirList').hide(); - dirListURL = RoxyUtils.AddParam(dirListURL, 'type', RoxyUtils.GetUrlParam('type')); + this.Show = function () { + var html = this.GetHtml(); + var el = null; + el = $('li[data-path="' + this.path + '"]'); + if (el.length == 0) + el = $('#pnlDirList'); + else { + if (el.children('ul').length == 0) + el.append('
                                    '); + el = el.children('ul'); + } + if (el) { + el.append(html); + this.SetEvents(); + } + }; + this.SetEvents = function () { + var el = this.GetElement(); + el.draggable({ + helper: makeDragDir, + start: startDragDir, + cursorAt: { + left: 10, + top: 10 + }, + delay: 200 + }); + el.find('> .dir-item').droppable({ + drop: moveObject, + over: dragFileOver, + out: dragFileOut + }); + }; + this.GetHtml = function () { + var dirClass = (this.dirs > 0 ? "" : " invisible"); - var dir = this; - $.ajax({ - url: dirListURL, - type:'POST', - dataType: 'json', - async: false, - cache: false, - success: function(dirs){ - $('#pnlDirList').children('li').remove(); - for(i = 0; i < dirs.length; i++){ - var d = new Directory(dirs[i].p, dirs[i].d, dirs[i].f); - d.Show(); - } - $('#pnlLoadingDirs').hide(); - $('#pnlDirList').show(); - dir.RestoreExpanded(expanded); - var d = Directory.Parse(selectedDir); - if(d) - d.Select(); - }, - error: function(data){ - $('#pnlLoadingDirs').hide(); - $('#pnlDirList').show(); - alert(t('E_LoadingAjax')+' '+RoxyFilemanConf.DIRLIST); - } - }); - }; - this.Expand = function(show){ - var li = this.GetElement(); - var el = li.children('ul'); - if(this.IsExpanded() && !show) - el.hide(); - else - el.show(); + var html = '
                                  • '; + html += '
                                    '; + html += '' + this.name + (parseInt(this.files) ? ' (' + this.files + ')' : '') + '
                                    '; + html += '
                                  • '; + + return html; + }; + this.SetStatusBar = function () { + $('#pnlStatus').html(this.files + ' ' + (this.files == 1 ? t('file') : t('files'))); + }; + this.SetSelectedFile = function (path) { + if (path) { + var f = File.Parse(path); + if (f) { + selectFile(f.GetElement()); + } + } + }; + this.Select = function (selectedFile, indeterm) { + var li = this.GetElement(); + var dir = li.find('> .dir-item'); + var currentSelected = getSelectedDir(); - this.SetOpened(); - }; - this.Create = function(newName){ - if(!newName) - return false; - else if(!RoxyFilemanConf.CREATEDIR){ - alert(t('E_ActionDisabled')); - return; - } - var url = RoxyUtils.AddParam(RoxyFilemanConf.CREATEDIR, 'd', this.fullPath); - url = RoxyUtils.AddParam(url, 'n', newName); - var item = this; - var ret = false; - $.ajax({ - url: url, - type: 'POST', - data: {d: this.fullPath, n: newName}, - dataType: 'json', - async:false, - cache: false, - success: function(data){ - if(data.res.toLowerCase() == 'ok'){ - item.LoadAll(RoxyUtils.MakePath(item.fullPath, newName)); - ret = true; - } - else{ - alert(data.msg); - } - }, - error: function(data){ - alert(t('E_LoadingAjax')+' '+item.name); - } - }); - return ret; - }; - this.Delete = function(){ - if(!RoxyFilemanConf.DELETEDIR){ - alert(t('E_ActionDisabled')); - return; - } - var url = RoxyUtils.AddParam(RoxyFilemanConf.DELETEDIR, 'd', this.fullPath); - var item = this; - var ret = false; - $.ajax({ - url: url, - type: 'POST', - data: {d: this.fullPath}, - dataType: 'json', - async:false, - cache: false, - success: function(data){ - if(data.res.toLowerCase() == 'ok'){ - var parent = item.GetParent(); - parent.dirs--; - parent.Update(); - parent.Select(); - item.GetElement().remove(); - ret = true; - } - if(data.msg) - alert(data.msg); - }, - error: function(data){ - alert(t('E_LoadingAjax')+' '+item.name); - } - }); - return ret; - }; - this.Rename = function(newName){ - if(!newName) - return false; - else if(!RoxyFilemanConf.RENAMEDIR){ - alert(t('E_ActionDisabled')); - return; - } - var url = RoxyUtils.AddParam(RoxyFilemanConf.RENAMEDIR, 'd', this.fullPath); - url = RoxyUtils.AddParam(url, 'n', newName); - var item = this; - var ret = false; - $.ajax({ - url: url, - type: 'POST', - data: {d: this.fullPath, n: newName}, - dataType: 'json', - async:false, - cache: false, - success: function(data){ - if(data.res.toLowerCase() == 'ok'){ - var newPath = RoxyUtils.MakePath(item.path, newName); - item.Update(newPath); - item.Select(); - ret = true; - } - if(data.msg) - alert(data.msg); - }, - error: function(data){ - alert(t('E_LoadingAjax')+' '+item.name); - } - }); - return ret; - }; - this.Copy = function(newPath){ - if(!RoxyFilemanConf.COPYDIR){ - alert(t('E_ActionDisabled')); - return; - } - var url = RoxyUtils.AddParam(RoxyFilemanConf.COPYDIR, 'd', this.fullPath); - url = RoxyUtils.AddParam(url, 'n', newPath); - var item = this; - var ret = false; - $.ajax({ - url: url, - type: 'POST', - data: {d: this.fullPath, n: newPath}, - dataType: 'json', - async:false, - cache: false, - success: function(data){ - if(data.res.toLowerCase() == 'ok'){ - var d = Directory.Parse(newPath); - if(d){ - d.LoadAll(d.fullPath); - } - ret = true; - } - if(data.msg) - alert(data.msg); - }, - error: function(data){ - alert(t('E_LoadingAjax')+' '+url); - } - }); - return ret; - }; - this.Move = function(newPath){ - if(!newPath) - return false; - else if(!RoxyFilemanConf.MOVEDIR){ - alert(t('E_ActionDisabled')); - return; - } - var url = RoxyUtils.AddParam(RoxyFilemanConf.MOVEDIR, 'd', this.fullPath); - url = RoxyUtils.AddParam(url, 'n', newPath); - var item = this; - var ret = false; - $.ajax({ - url: url, - type: 'POST', - data: {d: this.fullPath, n: newPath}, - dataType: 'json', - async:false, - cache: false, - success: function(data){ - if(data.res.toLowerCase() == 'ok'){ - item.LoadAll(RoxyUtils.MakePath(newPath, item.name)); - ret = true; - } - if(data.msg) - alert(data.msg); - }, - error: function(data){ - alert(t('E_LoadingAjax')+' '+item.name); - } - }); - return ret; - }; - this.ListFiles = function(refresh, selectedFile){ - $('#pnlLoading').show(); - $('#pnlEmptyDir').hide(); - $('#pnlFileList').hide(); - $('#pnlSearchNoFiles').hide(); - this.LoadFiles(refresh, selectedFile); - }; - this.FilesLoaded = function(filesList, selectedFile){ - filesList = this.SortFiles(filesList); - $('#pnlFileList').html(''); - for(i = 0; i < filesList.length; i++){ - var f = filesList[i]; - f.Show(); - } - $('#hdDir').val(this.fullPath); - $('#pnlLoading').hide(); - if($('#pnlFileList').children('li').length == 0) - $('#pnlEmptyDir').show(); - this.files = $('#pnlFileList').children('li').length; - this.Update(); - this.SetStatusBar(); - filterFiles(); - switchView(); - $('#pnlFileList').show(); - this.SetSelectedFile(selectedFile); - }; - this.LoadFiles = function(refresh, selectedFile){ - if(!RoxyFilemanConf.FILESLIST){ - alert(t('E_ActionDisabled')); - return; - } - var ret = new Array(); - var fileURL = RoxyFilemanConf.FILESLIST; - fileURL = RoxyUtils.AddParam(fileURL, 'd', this.fullPath); - fileURL = RoxyUtils.AddParam(fileURL, 'type', RoxyUtils.GetUrlParam('type')); - var item = this; - if(!this.IsListed() || refresh){ + if (indeterm && currentSelected) { + if (currentSelected.fullPath != li.data('path')) { + $('#pnlDirList').data('indeterm', dir); + $('#pnlDirList .indeterm').removeClass('indeterm'); + dir.addClass('indeterm'); + } + } + else { + dir.addClass('selected'); + $('#pnlDirList li[data-path!="' + this.fullPath + '"] > .dir-item').removeClass('selected'); + this.SetStatusBar(); - $.ajax({ - url: fileURL, - type: 'POST', - data: {d: this.fullPath, type: RoxyUtils.GetUrlParam('type')}, - dataType: 'json', - async:true, - cache: false, - success: function(files){ - for(i = 0; i < files.length; i++){ - ret.push(new File(files[i].p, files[i].s, files[i].t, files[i].w, files[i].h)); - } - item.FilesLoaded(ret, selectedFile); - }, - error: function(data){ - alert(t('E_LoadingAjax')+' '+fileURL); - } - }); - } - else{ - $('#pnlFileList li').each(function(){ - ret.push(new File($(this).attr('data-path'), $(this).attr('data-size'), $(this).attr('data-time'), $(this).attr('data-w'), $(this).attr('data-h'))); - }); - item.FilesLoaded(ret, selectedFile); - } + var p = this.GetParent(); + while (p) { + p.Expand(true); + p = p.GetParent(); + } + this.ListFiles(true, selectedFile); + setLastDir(this.fullPath); + } + }; + this.GetElement = function () { + return $('li[data-path="' + this.fullPath + '"]'); + }; + this.IsExpanded = function () { + var el = this.GetElement().children('ul'); + return (el && el.is(":visible")); + }; + this.IsIndeterm = function () { + var el = this.GetElement().find('> .dir-item'); + return el.is(".indeterm"); + }; + this.IsListed = function () { + if ($('#hdDir').val() == this.fullPath) + return true; + return false; + }; + this.GetExpanded = function (el) { + var ret = new Array(); + if (!el) + el = $('#pnlDirList'); + el.children('li').each(function () { + var path = $(this).attr('data-path'); + var d = new Directory(path); + if (d) { + if (d.IsExpanded() && path) + ret.push(path); + ret = ret.concat(d.GetExpanded(d.GetElement().children('ul'))); + } + }); - return ret; - }; + return ret; + }; + this.RestoreExpanded = function (expandedDirs) { + for (i = 0; i < expandedDirs.length; i++) { + var d = Directory.Parse(expandedDirs[i]); + if (d) + d.Expand(true); + } + }; + this.GetParent = function () { + return Directory.Parse(this.path); + }; + this.SetOpened = function () { + var li = this.GetElement(); + var chevrons = li.children('div').children('.dirPlus'); + if (li.find('li').length < 1) + chevrons.addClass('invisible'); + else if (this.IsExpanded()) + chevrons.removeClass('invisible fa-chevron-right').addClass("fa-chevron-down"); + else + chevrons.removeClass('invisible fa-chevron-down').addClass("fa-chevron-right"); + }; + this.Update = function (newPath) { + var el = this.GetElement(); + if (newPath) { + this.fullPath = newPath; + this.name = RoxyUtils.GetFilename(newPath); + if (!this.name) + this.name = 'My files'; + this.path = RoxyUtils.GetPath(newPath); + } + el.data('path', this.fullPath); + el.data('dirs', this.dirs); + el.data('files', this.files); + el.children('div').children('.name').html(this.name + ' (' + this.files + ')'); + this.SetOpened(); + }; + this.LoadAll = function (selectedDir) { + var expanded = this.GetExpanded(); + var dirListURL = RoxyUtils.GetRootPath(RoxyFilemanConf.DIRLIST); + if (!dirListURL) { + alert(t('E_ActionDisabled')); + return; + } + $('#pnlLoadingDirs').show(); + $('#pnlDirList').hide(); + dirListURL = RoxyUtils.AddParam(dirListURL, 'type', RoxyUtils.GetUrlParam('type')); - this.SortByName = function(files, order){ - files.sort(function(a, b){ - var x = (order == 'desc'?0:2) - a = a.name.toLowerCase(); - b = b.name.toLowerCase(); - if(a > b) - return -1 + x; - else if(a < b) - return 1 - x; - else - return 0; - }); + var dir = this; + $.ajax({ + url: dirListURL, + type: 'POST', + dataType: 'json', + async: false, + cache: false, + success: function (dirs) { + $('#pnlDirList').children('li').remove(); + var d; + for (i = 0; i < dirs.length; i++) { + d = new Directory(dirs[i].p, dirs[i].d, dirs[i].f); + d.Show(); + } + $('#pnlLoadingDirs').hide(); + $('#pnlDirList').show(); + dir.RestoreExpanded(expanded); + d = Directory.Parse(selectedDir); + if (d) d.Select(); + }, + error: function (data) { + $('#pnlLoadingDirs').hide(); + $('#pnlDirList').show(); + alert(t('E_LoadingAjax') + ' ' + RoxyFilemanConf.DIRLIST); + } + }); + }; + this.Expand = function (show) { + var li = this.GetElement(); + var el = li.children('ul'); + if (this.IsExpanded() && !show) + el.hide(); + else + el.show(); - return files; - }; - this.SortBySize = function(files, order){ - files.sort(function(a, b){ - var x = (order == 'desc'?0:2) - a = parseInt(a.size); - b = parseInt(b.size); - if(a > b) - return -1 + x; - else if(a < b) - return 1 - x; - else - return 0; - }); + this.SetOpened(); + }; + this.Create = function (newName) { + if (!newName) + return false; + else if (!RoxyFilemanConf.CREATEDIR) { + alert(t('E_ActionDisabled')); + return; + } + var url = RoxyUtils.AddParam(RoxyUtils.GetRootPath(RoxyFilemanConf.CREATEDIR), 'd', this.fullPath); + url = RoxyUtils.AddParam(url, 'n', newName); + var item = this; + var ret = false; + $.ajax({ + url: url, + type: 'POST', + data: { + d: this.fullPath, + n: newName + }, + dataType: 'json', + async: false, + cache: false, + success: function (data) { + if (data.res.toLowerCase() == 'ok') { + item.LoadAll(RoxyUtils.MakePath(item.fullPath, newName)); + ret = true; + } else { + alert(data.msg); + } + }, + error: function (data) { + alert(t('E_LoadingAjax') + ' ' + item.name); + } + }); + return ret; + }; + this.Delete = function () { + if (!RoxyFilemanConf.DELETEDIR) { + alert(t('E_ActionDisabled')); + return; + } + var url = RoxyUtils.AddParam(RoxyUtils.GetRootPath(RoxyFilemanConf.DELETEDIR), 'd', this.fullPath); + var item = this; + var ret = false; + $.ajax({ + url: url, + type: 'POST', + data: { + d: this.fullPath + }, + dataType: 'json', + async: false, + cache: false, + success: function (data) { + if (data.res.toLowerCase() == 'ok') { + var parent = item.GetParent(); + parent.dirs--; + parent.Update(); + parent.Select(); + item.GetElement().remove(); + ret = true; + } + if (data.msg) + alert(data.msg); + }, + error: function (data) { + alert(t('E_LoadingAjax') + ' ' + item.name); + } + }); + return ret; + }; + this.Rename = function (newName) { + if (!newName) + return false; + else if (!RoxyFilemanConf.RENAMEDIR) { + alert(t('E_ActionDisabled')); + return; + } + var url = RoxyUtils.AddParam(RoxyUtils.GetRootPath(RoxyFilemanConf.RENAMEDIR), 'd', this.fullPath); + url = RoxyUtils.AddParam(url, 'n', newName); + var item = this; + var ret = false; + $.ajax({ + url: url, + type: 'POST', + data: { + d: this.fullPath, + n: newName + }, + dataType: 'json', + async: false, + cache: false, + success: function (data) { + if (data.res.toLowerCase() == 'ok') { + var newPath = RoxyUtils.MakePath(item.path, newName); + item.Update(newPath); + item.Select(); + ret = true; + } + if (data.msg) + alert(data.msg); + }, + error: function (data) { + alert(t('E_LoadingAjax') + ' ' + item.name); + } + }); + return ret; + }; + this.Copy = function (newPath) { + if (!RoxyFilemanConf.COPYDIR) { + alert(t('E_ActionDisabled')); + return; + } + var url = RoxyUtils.AddParam(RoxyUtils.GetRootPath(RoxyFilemanConf.COPYDIR), 'd', this.fullPath); + url = RoxyUtils.AddParam(url, 'n', newPath); + var item = this; + var ret = false; + $.ajax({ + url: url, + type: 'POST', + data: { + d: this.fullPath, + n: newPath + }, + dataType: 'json', + async: false, + cache: false, + success: function (data) { + if (data.res.toLowerCase() == 'ok') { + var d = Directory.Parse(newPath); + if (d) { + d.LoadAll(d.fullPath); + } + ret = true; + } + if (data.msg) + alert(data.msg); + }, + error: function (data) { + alert(t('E_LoadingAjax') + ' ' + url); + } + }); + return ret; + }; + this.Move = function (newPath) { + if (!newPath) + return false; + else if (!RoxyFilemanConf.MOVEDIR) { + alert(t('E_ActionDisabled')); + return; + } + var url = RoxyUtils.AddParam(RoxyUtils.GetRootPath(RoxyFilemanConf.MOVEDIR), 'd', this.fullPath); + url = RoxyUtils.AddParam(url, 'n', newPath); + var item = this; + var ret = false; + $.ajax({ + url: url, + type: 'POST', + data: { + d: this.fullPath, + n: newPath + }, + dataType: 'json', + async: false, + cache: false, + success: function (data) { + if (data.res.toLowerCase() == 'ok') { + item.LoadAll(RoxyUtils.MakePath(newPath, item.name)); + ret = true; + } + if (data.msg) + alert(data.msg); + }, + error: function (data) { + alert(t('E_LoadingAjax') + ' ' + item.name); + } + }); + return ret; + }; + this.ListFiles = function (refresh, selectedFile) { + $('#pnlLoading').show(); + $('#pnlEmptyDir').hide(); + $('#pnlFileList').hide(); + $('#pnlSearchNoFiles').hide(); + this.LoadFiles(refresh, selectedFile); + }; + this.FilesLoaded = function (filesList, selectedFile) { + var list = $('#pnlFileList'); + filesList = this.SortFiles(filesList); + + var html = []; + for (i = 0; i < filesList.length; i++) { + var f = filesList[i]; + html.push(f.GenerateHtml()); + } - return files; - }; - this.SortByTime = function(files, order){ - files.sort(function(a, b){ - var x = (order == 'desc'?0:2) - a = parseInt(a.time); - b = parseInt(b.time); - if(a > b) - return -1 + x; - else if(a < b) - return 1 - x; - else - return 0; - }); + // Set Html + list.html(html.join("")); - return files; - }; - this.SortFiles = function(files){ - var order = $('#ddlOrder').val(); - if(!order) - order = 'name'; + // Bind events + list.find('.file-item').tooltip({ + show: { + delay: 700, + duration: 100 + }, + hide: 200, + track: true, + content: tooltipContent + }); - switch(order){ - case 'size': - files = this.SortBySize(files, 'asc'); - break; - case 'size_desc': - files = this.SortBySize(files, 'desc'); - break; - case 'time': - files = this.SortByTime(files, 'asc'); - break; - case 'time_desc': - files = this.SortByTime(files, 'desc'); - break; - case 'name_desc': - files = this.SortByName(files, 'desc'); - break; - default: - files = this.SortByName(files, 'asc'); - } + $('#hdDir').val(this.fullPath); + $('#pnlLoading').hide(); + var liLen = list.children('li').length; + if (liLen == 0) + $('#pnlEmptyDir').show(); + this.files = liLen; + this.Update(); + this.SetStatusBar(); + filterFiles(); + switchView(); + list.show(); + this.SetSelectedFile(selectedFile); + }; + this.LoadFiles = function (refresh, selectedFile) { + if (!RoxyFilemanConf.FILESLIST) { + alert(t('E_ActionDisabled')); + return; + } + var ret = new Array(); + var fileURL = RoxyUtils.GetRootPath(RoxyFilemanConf.FILESLIST); + fileURL = RoxyUtils.AddParam(fileURL, 'd', this.fullPath); + fileURL = RoxyUtils.AddParam(fileURL, 'type', RoxyUtils.GetUrlParam('type')); + var item = this; + if (!this.IsListed() || refresh) { + $.ajax({ + url: fileURL, + type: 'POST', + data: { + d: this.fullPath, + type: RoxyUtils.GetUrlParam('type') + }, + dataType: 'json', + async: true, + cache: false, + success: function (files) { + for (i = 0; i < files.length; i++) { + var f = files[i]; + ret.push(new File(f.p, f.s, f.t, f.w, f.h, f.m)); + } + item.FilesLoaded(ret, selectedFile); + }, + error: function (data) { + alert(t('E_LoadingAjax') + ' ' + fileURL); + } + }); + } else { + $('#pnlFileList li').each(function () { + ret.push(new File($(this).attr('data-path'), $(this).attr('data-size'), $(this).attr('data-time'), $(this).attr('data-w'), $(this).attr('data-h'))); + }); + item.FilesLoaded(ret, selectedFile); + } - return files; - }; + return ret; + }; + + this.SortByName = function (files, order) { + files.sort(function (a, b) { + var x = (order == 'desc' ? 0 : 2) + a = a.name.toLowerCase(); + b = b.name.toLowerCase(); + if (a > b) + return -1 + x; + else if (a < b) + return 1 - x; + else + return 0; + }); + + return files; + }; + this.SortBySize = function (files, order) { + files.sort(function (a, b) { + var x = (order == 'desc' ? 0 : 2) + a = parseInt(a.size); + b = parseInt(b.size); + if (a > b) + return -1 + x; + else if (a < b) + return 1 - x; + else + return 0; + }); + + return files; + }; + this.SortByTime = function (files, order) { + files.sort(function (a, b) { + var x = (order == 'desc' ? 0 : 2) + a = parseInt(a.time); + b = parseInt(b.time); + if (a > b) + return -1 + x; + else if (a < b) + return 1 - x; + else + return 0; + }); + + return files; + }; + this.SortFiles = function (files) { + var order = $('#ddlOrder').val(); + if (!order) + order = 'name'; + + switch (order) { + case 'size': + files = this.SortBySize(files, 'asc'); + break; + case 'size_desc': + files = this.SortBySize(files, 'desc'); + break; + case 'time': + files = this.SortByTime(files, 'asc'); + break; + case 'time_desc': + files = this.SortByTime(files, 'desc'); + break; + case 'name_desc': + files = this.SortByName(files, 'desc'); + break; + default: + files = this.SortByName(files, 'asc'); + } + + return files; + }; } -Directory.Parse = function(path){ - var ret = false; - var li = $('#pnlDirList').find('li[data-path="'+path+'"]'); - if(li.length > 0) - ret = new Directory(li.attr('data-path'), li.attr('data-dirs'), li.attr('data-files')); +Directory.Parse = function (path) { + var ret = false; + var li = $('#pnlDirList').find('li[data-path="' + path + '"]'); + if (li.length > 0) + ret = new Directory(li.attr('data-path'), li.attr('data-dirs'), li.attr('data-files')); - return ret; -}; + return ret; +}; \ No newline at end of file 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 337faca3bb..9d27380ad6 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/file.js +++ b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/file.js @@ -19,212 +19,266 @@ Contact: Lyubomir Arsov, liubo (at) web-lobby.com */ -function File(filePath, fileSize, modTime, w, h){ - this.fullPath = filePath; - this.type = RoxyUtils.GetFileType(filePath); - this.name = RoxyUtils.GetFilename(filePath); - this.ext = RoxyUtils.GetFileExt(filePath); - this.path = RoxyUtils.GetPath(filePath); - this.icon = RoxyUtils.GetFileIcon(filePath); - this.bigIcon = this.icon.replace('filetypes', 'filetypes/big'); - this.image = filePath; - this.size = (fileSize?fileSize:RoxyUtils.GetFileSize(filePath)); - this.time = modTime; - this.width = (w? w: 0); - this.height = (h? h: 0); - this.Show = function(){ - html = '
                                  • '; - html += ''; - html += ''+RoxyUtils.FormatDate(new Date(this.time * 1000))+''; - html += ''+this.name+''; - html += ''+RoxyUtils.FormatFileSize(this.size)+''; - html += '
                                  • '; - $('#pnlFileList').append(html); - var li = $("#pnlFileList li:last"); - if(RoxyFilemanConf.MOVEFILE){ - li.draggable({helper:makeDragFile,start:startDragFile,cursorAt: { left: 10 ,top:10},delay:200}); - } - li.click(function(e){ - selectFile(this); - }); - li.dblclick(function(e){ - selectFile(this); - setFile(); - }); - li.tooltip({show:{delay:700},track: true, content:tooltipContent}); - - li.bind('contextmenu', function(e) { - e.stopPropagation(); - e.preventDefault(); - closeMenus('dir'); - selectFile(this); - $(this).tooltip('close'); - var t = e.pageY - $('#menuFile').height(); - if(t < 0) - t = 0; - $('#menuFile').css({ - top: t+'px', - left: e.pageX+'px' - }).show(); - - return false; - }); - }; - this.GetElement = function(){ - return $('li[data-path="'+this.fullPath+'"]'); - }; - this.IsImage = function(){ - var ret = false; - if(this.type == 'image') - ret = true; - return ret; - }; - this.Delete = function(){ - if(!RoxyFilemanConf.DELETEFILE){ - alert(t('E_ActionDisabled')); - return; - } - var deleteUrl = RoxyUtils.AddParam(RoxyFilemanConf.DELETEFILE, 'f', this.fullPath); - var item = this; - $.ajax({ - url: deleteUrl, - type: 'POST', - data: {f: this.fullPath}, - dataType: 'json', - async:false, - success: function(data){ - if(data.res.toLowerCase() == 'ok'){ - $('li[data-path="'+item.fullPath+'"]').remove(); - var d = Directory.Parse(item.path); - if(d){ - d.files--; - d.Update(); - d.SetStatusBar(); - } - } - else{ - alert(data.msg); - } - }, - error: function(data){ - alert(t('E_LoadingAjax')+' '+deleteUrl); - } - }); - }; - this.Rename = function(newName){ - if(!RoxyFilemanConf.RENAMEFILE){ - alert(t('E_ActionDisabled')); - return false; - } - if(!newName) - return false; - var url = RoxyUtils.AddParam(RoxyFilemanConf.RENAMEFILE, 'f', this.fullPath); - url = RoxyUtils.AddParam(url, 'n', newName); - var item = this; - var ret = false; - $.ajax({ - url: url, - type: 'POST', - data: {f: this.fullPath, n: newName}, - dataType: 'json', - async:false, - success: function(data){ - if(data.res.toLowerCase() == 'ok'){ - var newPath = RoxyUtils.MakePath(this.path, newName); - $('li[data-path="'+item.fullPath+'"] .icon').attr('src', RoxyUtils.GetFileIcon(newName)); - $('li[data-path="'+item.fullPath+'"] .name').text(newName); - $('li[data-path="'+newPath+'"]').attr('data-path', newPath); - ret = true; - } - if(data.msg) - alert(data.msg); - }, - error: function(data){ - alert(t('E_LoadingAjax')+' '+url); - } - }); - return ret; - }; - this.Copy = function(newPath){ - if(!RoxyFilemanConf.COPYFILE){ - alert(t('E_ActionDisabled')); - return; - } - var url = RoxyUtils.AddParam(RoxyFilemanConf.COPYFILE, 'f', this.fullPath); - url = RoxyUtils.AddParam(url, 'n', newPath); - var item = this; - var ret = false; - $.ajax({ - url: url, - type: 'POST', - data: {f: this.fullPath, n: newPath}, - dataType: 'json', - async:false, - success: function(data){ - if(data.res.toLowerCase() == 'ok'){ - var d = Directory.Parse(newPath); - if(d){ - d.files++; - d.Update(); - d.SetStatusBar(); - d.ListFiles(true); - } - ret = true; - } - if(data.msg) - alert(data.msg); - }, - error: function(data){ - alert(t('E_LoadingAjax')+' '+url); - } - }); - return ret; - }; - this.Move = function(newPath){ - if(!RoxyFilemanConf.MOVEFILE){ - alert(t('E_ActionDisabled')); - return; - } - newFullPath = RoxyUtils.MakePath(newPath, this.name); - var url = RoxyUtils.AddParam(RoxyFilemanConf.MOVEFILE, 'f', this.fullPath); - url = RoxyUtils.AddParam(url, 'n', newFullPath); - var item = this; - var ret = false; - $.ajax({ - url: url, - type: 'POST', - data: {f: this.fullPath, n: newFullPath}, - dataType: 'json', - async:false, - success: function(data){ - if(data.res.toLowerCase() == 'ok'){ - $('li[data-path="'+item.fullPath+'"]').remove(); - var d = Directory.Parse(item.path); - if(d){ - d.files--; - d.Update(); - d.SetStatusBar(); - d = Directory.Parse(newPath); - d.files++; - d.Update(); - } - ret = true; - } - if(data.msg) - alert(data.msg); - }, - error: function(data){ - alert(t('E_LoadingAjax')+' '+url); - } - }); - return ret; - }; + +$(function () { + $(document).on('dblclick', '.file-item', function (e) { + e.stopPropagation(); + e.preventDefault(); + selectFile(this); + setFile(); + }); + + $('#pnlFileList').on('contextmenu', '.file-item', function (e) { + e.stopPropagation(); + e.preventDefault(); + closeMenus('dir'); + selectFile(this); + $(this).tooltip('close'); + var t = e.pageY; + var menuEnd = t + $('#menuFile').height() + 30; + if (menuEnd > $(window).height()) { + offset = menuEnd - $(window).height() + 30; + t -= offset; + } + $('#menuFile').css({ + top: t + 'px', + left: e.pageX + 'px' + }).show(); + + return false; + }); + + $(document).on('click', '.file-item', function (e) { + e.stopPropagation(); + e.preventDefault(); + + selectFile(this); + }); + + $(document).on('mouseenter', '.file-item', function (e) { + var li = $(this); + + // create draggable + if (!li.data('ui-draggable')) { + li.draggable({ + helper: makeDragFile, + start: startDragFile, + addClasses: false, + appendTo: 'body', + cursorAt: { + left: 10, + top: 10 + }, + delay: 200 + }); + } + }); +}); + +function File(filePath, fileSize, modTime, w, h, mime) { + this.fullPath = filePath; + this.mime = mime; + this.type = RoxyUtils.GetFileType(filePath, mime); + this.icon = RoxyIconHints[this.type]; + this.name = RoxyUtils.GetFilename(filePath); + this.ext = RoxyUtils.GetFileExt(filePath); + this.path = RoxyUtils.GetPath(filePath); + this.image = filePath; + this.size = (fileSize ? fileSize : RoxyUtils.GetFileSize(filePath)); + this.time = modTime; + this.width = (w ? w : 0); + this.height = (h ? h : 0); + this.thumb = this.type === 'image' ? filePath : RoxyUtils.GetAssetPath("images/blank.gif"); + this.GenerateHtml = function () { + var attrs = [ + 'data-mime="' + this.mime + '"', + 'data-path="' + this.fullPath + '"', + 'data-time="' + this.time + '"', + 'data-w="' + this.width + '"', + 'data-h="' + this.height + '"', + 'data-size="' + this.size + '"', + 'title="' + this.name + '"' + ]; + var html = [ + '
                                  • ', + '
                                    ', + '' + RoxyUtils.FormatDate(new Date(this.time * 1000)) + '', + '' + this.name + '', + '' + RoxyUtils.FormatFileSize(this.size) + '', + '
                                  • ' + ].join(""); + + return html; + }; + this.GetElement = function () { + return $('li[data-path="' + this.fullPath + '"]'); + }; + this.IsImage = function () { + return this.type === 'image'; + }; + this.Delete = function () { + if (!RoxyFilemanConf.DELETEFILE) { + alert(t('E_ActionDisabled')); + return; + } + var deleteUrl = RoxyUtils.AddParam(RoxyUtils.GetRootPath(RoxyFilemanConf.DELETEFILE), 'f', this.fullPath); + var item = this; + $.ajax({ + url: deleteUrl, + type: 'POST', + data: { + f: this.fullPath + }, + dataType: 'json', + async: false, + success: function (data) { + if (data.res.toLowerCase() == 'ok') { + $('li[data-path="' + item.fullPath + '"]').remove(); + var d = Directory.Parse(item.path); + if (d) { + d.files--; + d.Update(); + d.SetStatusBar(); + } + } else { + alert(data.msg); + } + }, + error: function (data) { + alert(t('E_LoadingAjax') + ' ' + deleteUrl); + } + }); + }; + this.Rename = function (newName) { + if (!RoxyFilemanConf.RENAMEFILE) { + alert(t('E_ActionDisabled')); + return false; + } + if (!newName) + return false; + var url = RoxyUtils.AddParam(RoxyUtils.GetRootPath(RoxyFilemanConf.RENAMEFILE), 'f', this.fullPath); + url = RoxyUtils.AddParam(url, 'n', newName); + var item = this; + var ret = false; + $.ajax({ + url: url, + type: 'POST', + data: { + f: this.fullPath, + n: newName + }, + dataType: 'json', + async: false, + success: function (data) { + if (data.res.toLowerCase() == 'ok') { + var newPath = RoxyUtils.MakePath(this.path, newName); + var fileType = RoxyUtils.GetFileIcon(newName); + 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); + $('.name', li).text(newName); + $('li[data-path="' + newPath + '"]').attr('data-path', newPath); + ret = true; + } + if (data.msg) + alert(data.msg); + }, + error: function (data) { + alert(t('E_LoadingAjax') + ' ' + url); + } + }); + return ret; + }; + this.Copy = function (newPath) { + if (!RoxyFilemanConf.COPYFILE) { + alert(t('E_ActionDisabled')); + return; + } + var url = RoxyUtils.AddParam(RoxyUtils.GetRootPath(RoxyFilemanConf.COPYFILE), 'f', this.fullPath); + url = RoxyUtils.AddParam(url, 'n', newPath); + var item = this; + var ret = false; + $.ajax({ + url: url, + type: 'POST', + data: { + f: this.fullPath, + n: newPath + }, + dataType: 'json', + async: false, + success: function (data) { + if (data.res.toLowerCase() == 'ok') { + var d = Directory.Parse(newPath); + if (d) { + d.files++; + d.Update(); + d.SetStatusBar(); + if (!$("#pnlDirList").data("indeterm")) { + d.ListFiles(true); + } + } + ret = true; + } + if (data.msg) + alert(data.msg); + }, + error: function (data) { + alert(t('E_LoadingAjax') + ' ' + url); + } + }); + return ret; + }; + this.Move = function (newPath) { + if (!RoxyFilemanConf.MOVEFILE) { + alert(t('E_ActionDisabled')); + return; + } + newFullPath = RoxyUtils.MakePath(newPath, this.name); + var url = RoxyUtils.AddParam(RoxyUtils.GetRootPath(RoxyFilemanConf.MOVEFILE), 'f', this.fullPath); + url = RoxyUtils.AddParam(url, 'n', newFullPath); + var item = this; + var ret = false; + $.ajax({ + url: url, + type: 'POST', + data: { + f: this.fullPath, + n: newFullPath + }, + dataType: 'json', + async: false, + success: function (data) { + if (data.res.toLowerCase() == 'ok') { + $('li[data-path="' + item.fullPath + '"]').remove(); + var d = Directory.Parse(item.path); + if (d) { + d.files--; + d.Update(); + d.SetStatusBar(); + d = Directory.Parse(newPath); + d.files++; + d.Update(); + } + ret = true; + } + if (data.msg) + alert(data.msg); + }, + error: function (data) { + alert(t('E_LoadingAjax') + ' ' + url); + } + }); + return ret; + }; } -File.Parse = function(path){ - var ret = false; - var li = $('#pnlFileList').find('li[data-path="'+path+'"]'); - if(li.length > 0) - ret = new File(li.attr('data-path'), li.attr('data-size'), li.attr('data-time'), li.attr('data-w'), li.attr('data-h')); - return ret; +File.Parse = function (path) { + var ret = false; + var li = $('#pnlFileList').find('li[data-path="' + path + '"]'); + if (li.length > 0) + ret = new File(li.data('path'), li.data('size'), li.data('time'), li.data('w'), li.data('h')); + + return ret; }; \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/filetypes.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/filetypes.js index 209d66f01a..48d5a1092b 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/filetypes.js +++ b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/filetypes.js @@ -118,4 +118,4 @@ fileTypeIcons['wmv'] = 'file_extension_wmv.png'; fileTypeIcons['wps'] = 'file_extension_wps.png'; fileTypeIcons['xls'] = 'file_extension_xls.png'; fileTypeIcons['xpi'] = 'file_extension_xpi.png'; -fileTypeIcons['zip'] = 'file_extension_zip.png'; +fileTypeIcons['zip'] = 'file_extension_zip.png'; \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/jquery-1.10.2.min.js b/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/jquery-1.10.2.min.js deleted file mode 100644 index da4170647d..0000000000 --- a/src/Presentation/SmartStore.Web/Administration/Content/filemanager/js/jquery-1.10.2.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! jQuery v1.10.2 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license -//@ sourceMappingURL=jquery-1.10.2.min.map -*/ -(function(e,t){var n,r,i=typeof t,o=e.location,a=e.document,s=a.documentElement,l=e.jQuery,u=e.$,c={},p=[],f="1.10.2",d=p.concat,h=p.push,g=p.slice,m=p.indexOf,y=c.toString,v=c.hasOwnProperty,b=f.trim,x=function(e,t){return new x.fn.init(e,t,r)},w=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=/\S+/g,C=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,N=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,k=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,E=/^[\],:{}\s]*$/,S=/(?:^|:|,)(?:\s*\[)+/g,A=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,j=/"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,D=/^-ms-/,L=/-([\da-z])/gi,H=function(e,t){return t.toUpperCase()},q=function(e){(a.addEventListener||"load"===e.type||"complete"===a.readyState)&&(_(),x.ready())},_=function(){a.addEventListener?(a.removeEventListener("DOMContentLoaded",q,!1),e.removeEventListener("load",q,!1)):(a.detachEvent("onreadystatechange",q),e.detachEvent("onload",q))};x.fn=x.prototype={jquery:f,constructor:x,init:function(e,n,r){var i,o;if(!e)return this;if("string"==typeof e){if(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:N.exec(e),!i||!i[1]&&n)return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e);if(i[1]){if(n=n instanceof x?n[0]:n,x.merge(this,x.parseHTML(i[1],n&&n.nodeType?n.ownerDocument||n:a,!0)),k.test(i[1])&&x.isPlainObject(n))for(i in n)x.isFunction(this[i])?this[i](n[i]):this.attr(i,n[i]);return this}if(o=a.getElementById(i[2]),o&&o.parentNode){if(o.id!==i[2])return r.find(e);this.length=1,this[0]=o}return this.context=a,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return g.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(g.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,n,r,i,o,a,s=arguments[0]||{},l=1,u=arguments.length,c=!1;for("boolean"==typeof s&&(c=s,s=arguments[1]||{},l=2),"object"==typeof s||x.isFunction(s)||(s={}),u===l&&(s=this,--l);u>l;l++)if(null!=(o=arguments[l]))for(i in o)e=s[i],r=o[i],s!==r&&(c&&r&&(x.isPlainObject(r)||(n=x.isArray(r)))?(n?(n=!1,a=e&&x.isArray(e)?e:[]):a=e&&x.isPlainObject(e)?e:{},s[i]=x.extend(c,a,r)):r!==t&&(s[i]=r));return s},x.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=l),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){if(e===!0?!--x.readyWait:!x.isReady){if(!a.body)return setTimeout(x.ready);x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(a,[x]),x.fn.trigger&&x(a).trigger("ready").off("ready"))}},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray||function(e){return"array"===x.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?c[y.call(e)]||"object":typeof e},isPlainObject:function(e){var n;if(!e||"object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!v.call(e,"constructor")&&!v.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(r){return!1}if(x.support.ownLast)for(n in e)return v.call(e,n);for(n in e);return n===t||v.call(e,n)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||a;var r=k.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:function(n){return e.JSON&&e.JSON.parse?e.JSON.parse(n):null===n?n:"string"==typeof n&&(n=x.trim(n),n&&E.test(n.replace(A,"@").replace(j,"]").replace(S,"")))?Function("return "+n)():(x.error("Invalid JSON: "+n),t)},parseXML:function(n){var r,i;if(!n||"string"!=typeof n)return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(o){r=t}return r&&r.documentElement&&!r.getElementsByTagName("parsererror").length||x.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&x.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(D,"ms-").replace(L,H)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,a=M(e);if(n){if(a){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(a){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:b&&!b.call("\ufeff\u00a0")?function(e){return null==e?"":b.call(e)}:function(e){return null==e?"":(e+"").replace(C,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(M(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){var r;if(t){if(m)return m.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,o=0;if("number"==typeof r)for(;r>o;o++)e[i++]=n[o];else while(n[o]!==t)e[i++]=n[o++];return e.length=i,e},grep:function(e,t,n){var r,i=[],o=0,a=e.length;for(n=!!n;a>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,a=M(e),s=[];if(a)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(s[s.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(s[s.length]=r);return d.apply([],s)},guid:1,proxy:function(e,n){var r,i,o;return"string"==typeof n&&(o=e[n],n=e,e=o),x.isFunction(e)?(r=g.call(arguments,2),i=function(){return e.apply(n||this,r.concat(g.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):t},access:function(e,n,r,i,o,a,s){var l=0,u=e.length,c=null==r;if("object"===x.type(r)){o=!0;for(l in r)x.access(e,n,l,r[l],!0,a,s)}else if(i!==t&&(o=!0,x.isFunction(i)||(s=!0),c&&(s?(n.call(e,i),n=null):(c=n,n=function(e,t,n){return c.call(x(e),n)})),n))for(;u>l;l++)n(e[l],r,s?i:i.call(e[l],l,n(e[l],r)));return o?e:c?n.call(e):u?n(e[0],r):a},now:function(){return(new Date).getTime()},swap:function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i}}),x.ready.promise=function(t){if(!n)if(n=x.Deferred(),"complete"===a.readyState)setTimeout(x.ready);else if(a.addEventListener)a.addEventListener("DOMContentLoaded",q,!1),e.addEventListener("load",q,!1);else{a.attachEvent("onreadystatechange",q),e.attachEvent("onload",q);var r=!1;try{r=null==e.frameElement&&a.documentElement}catch(i){}r&&r.doScroll&&function o(){if(!x.isReady){try{r.doScroll("left")}catch(e){return setTimeout(o,50)}_(),x.ready()}}()}return n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){c["[object "+t+"]"]=t.toLowerCase()});function M(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}r=x(a),function(e,t){var n,r,i,o,a,s,l,u,c,p,f,d,h,g,m,y,v,b="sizzle"+-new Date,w=e.document,T=0,C=0,N=st(),k=st(),E=st(),S=!1,A=function(e,t){return e===t?(S=!0,0):0},j=typeof t,D=1<<31,L={}.hasOwnProperty,H=[],q=H.pop,_=H.push,M=H.push,O=H.slice,F=H.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},B="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",P="[\\x20\\t\\r\\n\\f]",R="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",W=R.replace("w","w#"),$="\\["+P+"*("+R+")"+P+"*(?:([*^$|!~]?=)"+P+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+W+")|)|)"+P+"*\\]",I=":("+R+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+$.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+P+"+|((?:^|[^\\\\])(?:\\\\.)*)"+P+"+$","g"),X=RegExp("^"+P+"*,"+P+"*"),U=RegExp("^"+P+"*([>+~]|"+P+")"+P+"*"),V=RegExp(P+"*[+~]"),Y=RegExp("="+P+"*([^\\]'\"]*)"+P+"*\\]","g"),J=RegExp(I),G=RegExp("^"+W+"$"),Q={ID:RegExp("^#("+R+")"),CLASS:RegExp("^\\.("+R+")"),TAG:RegExp("^("+R.replace("w","w*")+")"),ATTR:RegExp("^"+$),PSEUDO:RegExp("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+P+"*(even|odd|(([+-]|)(\\d*)n|)"+P+"*(?:([+-]|)"+P+"*(\\d+)|))"+P+"*\\)|)","i"),bool:RegExp("^(?:"+B+")$","i"),needsContext:RegExp("^"+P+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+P+"*((?:-\\d)?\\d*)"+P+"*\\)|)(?=[^-]|$)","i")},K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,et=/^(?:input|select|textarea|button)$/i,tt=/^h\d$/i,nt=/'|\\/g,rt=RegExp("\\\\([\\da-f]{1,6}"+P+"?|("+P+")|.)","ig"),it=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{M.apply(H=O.call(w.childNodes),w.childNodes),H[w.childNodes.length].nodeType}catch(ot){M={apply:H.length?function(e,t){_.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function at(e,t,n,i){var o,a,s,l,u,c,d,m,y,x;if((t?t.ownerDocument||t:w)!==f&&p(t),t=t||f,n=n||[],!e||"string"!=typeof e)return n;if(1!==(l=t.nodeType)&&9!==l)return[];if(h&&!i){if(o=Z.exec(e))if(s=o[1]){if(9===l){if(a=t.getElementById(s),!a||!a.parentNode)return n;if(a.id===s)return n.push(a),n}else if(t.ownerDocument&&(a=t.ownerDocument.getElementById(s))&&v(t,a)&&a.id===s)return n.push(a),n}else{if(o[2])return M.apply(n,t.getElementsByTagName(e)),n;if((s=o[3])&&r.getElementsByClassName&&t.getElementsByClassName)return M.apply(n,t.getElementsByClassName(s)),n}if(r.qsa&&(!g||!g.test(e))){if(m=d=b,y=t,x=9===l&&e,1===l&&"object"!==t.nodeName.toLowerCase()){c=mt(e),(d=t.getAttribute("id"))?m=d.replace(nt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",u=c.length;while(u--)c[u]=m+yt(c[u]);y=V.test(e)&&t.parentNode||t,x=c.join(",")}if(x)try{return M.apply(n,y.querySelectorAll(x)),n}catch(T){}finally{d||t.removeAttribute("id")}}}return kt(e.replace(z,"$1"),t,n,i)}function st(){var e=[];function t(n,r){return e.push(n+=" ")>o.cacheLength&&delete t[e.shift()],t[n]=r}return t}function lt(e){return e[b]=!0,e}function ut(e){var t=f.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ct(e,t){var n=e.split("|"),r=e.length;while(r--)o.attrHandle[n[r]]=t}function pt(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function ft(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function dt(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function ht(e){return lt(function(t){return t=+t,lt(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}s=at.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},r=at.support={},p=at.setDocument=function(e){var n=e?e.ownerDocument||e:w,i=n.defaultView;return n!==f&&9===n.nodeType&&n.documentElement?(f=n,d=n.documentElement,h=!s(n),i&&i.attachEvent&&i!==i.top&&i.attachEvent("onbeforeunload",function(){p()}),r.attributes=ut(function(e){return e.className="i",!e.getAttribute("className")}),r.getElementsByTagName=ut(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),r.getElementsByClassName=ut(function(e){return e.innerHTML="
                                    ",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),r.getById=ut(function(e){return d.appendChild(e).id=b,!n.getElementsByName||!n.getElementsByName(b).length}),r.getById?(o.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){return e.getAttribute("id")===t}}):(delete o.find.ID,o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),o.find.TAG=r.getElementsByTagName?function(e,n){return typeof n.getElementsByTagName!==j?n.getElementsByTagName(e):t}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},o.find.CLASS=r.getElementsByClassName&&function(e,n){return typeof n.getElementsByClassName!==j&&h?n.getElementsByClassName(e):t},m=[],g=[],(r.qsa=K.test(n.querySelectorAll))&&(ut(function(e){e.innerHTML="",e.querySelectorAll("[selected]").length||g.push("\\["+P+"*(?:value|"+B+")"),e.querySelectorAll(":checked").length||g.push(":checked")}),ut(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&g.push("[*^$]="+P+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||g.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),g.push(",.*:")})),(r.matchesSelector=K.test(y=d.webkitMatchesSelector||d.mozMatchesSelector||d.oMatchesSelector||d.msMatchesSelector))&&ut(function(e){r.disconnectedMatch=y.call(e,"div"),y.call(e,"[s!='']:x"),m.push("!=",I)}),g=g.length&&RegExp(g.join("|")),m=m.length&&RegExp(m.join("|")),v=K.test(d.contains)||d.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},A=d.compareDocumentPosition?function(e,t){if(e===t)return S=!0,0;var i=t.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(t);return i?1&i||!r.sortDetached&&t.compareDocumentPosition(e)===i?e===n||v(w,e)?-1:t===n||v(w,t)?1:c?F.call(c,e)-F.call(c,t):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,t){var r,i=0,o=e.parentNode,a=t.parentNode,s=[e],l=[t];if(e===t)return S=!0,0;if(!o||!a)return e===n?-1:t===n?1:o?-1:a?1:c?F.call(c,e)-F.call(c,t):0;if(o===a)return pt(e,t);r=e;while(r=r.parentNode)s.unshift(r);r=t;while(r=r.parentNode)l.unshift(r);while(s[i]===l[i])i++;return i?pt(s[i],l[i]):s[i]===w?-1:l[i]===w?1:0},n):f},at.matches=function(e,t){return at(e,null,null,t)},at.matchesSelector=function(e,t){if((e.ownerDocument||e)!==f&&p(e),t=t.replace(Y,"='$1']"),!(!r.matchesSelector||!h||m&&m.test(t)||g&&g.test(t)))try{var n=y.call(e,t);if(n||r.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(i){}return at(t,f,null,[e]).length>0},at.contains=function(e,t){return(e.ownerDocument||e)!==f&&p(e),v(e,t)},at.attr=function(e,n){(e.ownerDocument||e)!==f&&p(e);var i=o.attrHandle[n.toLowerCase()],a=i&&L.call(o.attrHandle,n.toLowerCase())?i(e,n,!h):t;return a===t?r.attributes||!h?e.getAttribute(n):(a=e.getAttributeNode(n))&&a.specified?a.value:null:a},at.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},at.uniqueSort=function(e){var t,n=[],i=0,o=0;if(S=!r.detectDuplicates,c=!r.sortStable&&e.slice(0),e.sort(A),S){while(t=e[o++])t===e[o]&&(i=n.push(o));while(i--)e.splice(n[i],1)}return e},a=at.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=a(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=a(t);return n},o=at.selectors={cacheLength:50,createPseudo:lt,match:Q,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(rt,it),e[3]=(e[4]||e[5]||"").replace(rt,it),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||at.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&at.error(e[0]),e},PSEUDO:function(e){var n,r=!e[5]&&e[2];return Q.CHILD.test(e[0])?null:(e[3]&&e[4]!==t?e[2]=e[4]:r&&J.test(r)&&(n=mt(r,!0))&&(n=r.indexOf(")",r.length-n)-r.length)&&(e[0]=e[0].slice(0,n),e[2]=r.slice(0,n)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(rt,it).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=N[e+" "];return t||(t=RegExp("(^|"+P+")"+e+"("+P+"|$)"))&&N(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=at.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,p,f,d,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,y=s&&t.nodeName.toLowerCase(),v=!l&&!s;if(m){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&v){c=m[b]||(m[b]={}),u=c[e]||[],d=u[0]===T&&u[1],f=u[0]===T&&u[2],p=d&&m.childNodes[d];while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[T,d,f];break}}else if(v&&(u=(t[b]||(t[b]={}))[e])&&u[0]===T)f=u[1];else while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(v&&((p[b]||(p[b]={}))[e]=[T,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=o.pseudos[e]||o.setFilters[e.toLowerCase()]||at.error("unsupported pseudo: "+e);return r[b]?r(t):r.length>1?(n=[e,e,"",t],o.setFilters.hasOwnProperty(e.toLowerCase())?lt(function(e,n){var i,o=r(e,t),a=o.length;while(a--)i=F.call(e,o[a]),e[i]=!(n[i]=o[a])}):function(e){return r(e,0,n)}):r}},pseudos:{not:lt(function(e){var t=[],n=[],r=l(e.replace(z,"$1"));return r[b]?lt(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:lt(function(e){return function(t){return at(e,t).length>0}}),contains:lt(function(e){return function(t){return(t.textContent||t.innerText||a(t)).indexOf(e)>-1}}),lang:lt(function(e){return G.test(e||"")||at.error("unsupported lang: "+e),e=e.replace(rt,it).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===d},focus:function(e){return e===f.activeElement&&(!f.hasFocus||f.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!o.pseudos.empty(e)},header:function(e){return tt.test(e.nodeName)},input:function(e){return et.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:ht(function(){return[0]}),last:ht(function(e,t){return[t-1]}),eq:ht(function(e,t,n){return[0>n?n+t:n]}),even:ht(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:ht(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:ht(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:ht(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}},o.pseudos.nth=o.pseudos.eq;for(n in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})o.pseudos[n]=ft(n);for(n in{submit:!0,reset:!0})o.pseudos[n]=dt(n);function gt(){}gt.prototype=o.filters=o.pseudos,o.setFilters=new gt;function mt(e,t){var n,r,i,a,s,l,u,c=k[e+" "];if(c)return t?0:c.slice(0);s=e,l=[],u=o.preFilter;while(s){(!n||(r=X.exec(s)))&&(r&&(s=s.slice(r[0].length)||s),l.push(i=[])),n=!1,(r=U.exec(s))&&(n=r.shift(),i.push({value:n,type:r[0].replace(z," ")}),s=s.slice(n.length));for(a in o.filter)!(r=Q[a].exec(s))||u[a]&&!(r=u[a](r))||(n=r.shift(),i.push({value:n,type:a,matches:r}),s=s.slice(n.length));if(!n)break}return t?s.length:s?at.error(e):k(e,l).slice(0)}function yt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function vt(e,t,n){var r=t.dir,o=n&&"parentNode"===r,a=C++;return t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||o)return e(t,n,i)}:function(t,n,s){var l,u,c,p=T+" "+a;if(s){while(t=t[r])if((1===t.nodeType||o)&&e(t,n,s))return!0}else while(t=t[r])if(1===t.nodeType||o)if(c=t[b]||(t[b]={}),(u=c[r])&&u[0]===p){if((l=u[1])===!0||l===i)return l===!0}else if(u=c[r]=[p],u[1]=e(t,n,s)||i,u[1]===!0)return!0}}function bt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function xt(e,t,n,r,i){var o,a=[],s=0,l=e.length,u=null!=t;for(;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function wt(e,t,n,r,i,o){return r&&!r[b]&&(r=wt(r)),i&&!i[b]&&(i=wt(i,o)),lt(function(o,a,s,l){var u,c,p,f=[],d=[],h=a.length,g=o||Nt(t||"*",s.nodeType?[s]:s,[]),m=!e||!o&&t?g:xt(g,f,e,s,l),y=n?i||(o?e:h||r)?[]:a:m;if(n&&n(m,y,s,l),r){u=xt(y,d),r(u,[],s,l),c=u.length;while(c--)(p=u[c])&&(y[d[c]]=!(m[d[c]]=p))}if(o){if(i||e){if(i){u=[],c=y.length;while(c--)(p=y[c])&&u.push(m[c]=p);i(null,y=[],u,l)}c=y.length;while(c--)(p=y[c])&&(u=i?F.call(o,p):f[c])>-1&&(o[u]=!(a[u]=p))}}else y=xt(y===a?y.splice(h,y.length):y),i?i(null,a,y,l):M.apply(a,y)})}function Tt(e){var t,n,r,i=e.length,a=o.relative[e[0].type],s=a||o.relative[" "],l=a?1:0,c=vt(function(e){return e===t},s,!0),p=vt(function(e){return F.call(t,e)>-1},s,!0),f=[function(e,n,r){return!a&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;i>l;l++)if(n=o.relative[e[l].type])f=[vt(bt(f),n)];else{if(n=o.filter[e[l].type].apply(null,e[l].matches),n[b]){for(r=++l;i>r;r++)if(o.relative[e[r].type])break;return wt(l>1&&bt(f),l>1&&yt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&Tt(e.slice(l,r)),i>r&&Tt(e=e.slice(r)),i>r&&yt(e))}f.push(n)}return bt(f)}function Ct(e,t){var n=0,r=t.length>0,a=e.length>0,s=function(s,l,c,p,d){var h,g,m,y=[],v=0,b="0",x=s&&[],w=null!=d,C=u,N=s||a&&o.find.TAG("*",d&&l.parentNode||l),k=T+=null==C?1:Math.random()||.1;for(w&&(u=l!==f&&l,i=n);null!=(h=N[b]);b++){if(a&&h){g=0;while(m=e[g++])if(m(h,l,c)){p.push(h);break}w&&(T=k,i=++n)}r&&((h=!m&&h)&&v--,s&&x.push(h))}if(v+=b,r&&b!==v){g=0;while(m=t[g++])m(x,y,l,c);if(s){if(v>0)while(b--)x[b]||y[b]||(y[b]=q.call(p));y=xt(y)}M.apply(p,y),w&&!s&&y.length>0&&v+t.length>1&&at.uniqueSort(p)}return w&&(T=k,u=C),x};return r?lt(s):s}l=at.compile=function(e,t){var n,r=[],i=[],o=E[e+" "];if(!o){t||(t=mt(e)),n=t.length;while(n--)o=Tt(t[n]),o[b]?r.push(o):i.push(o);o=E(e,Ct(i,r))}return o};function Nt(e,t,n){var r=0,i=t.length;for(;i>r;r++)at(e,t[r],n);return n}function kt(e,t,n,i){var a,s,u,c,p,f=mt(e);if(!i&&1===f.length){if(s=f[0]=f[0].slice(0),s.length>2&&"ID"===(u=s[0]).type&&r.getById&&9===t.nodeType&&h&&o.relative[s[1].type]){if(t=(o.find.ID(u.matches[0].replace(rt,it),t)||[])[0],!t)return n;e=e.slice(s.shift().value.length)}a=Q.needsContext.test(e)?0:s.length;while(a--){if(u=s[a],o.relative[c=u.type])break;if((p=o.find[c])&&(i=p(u.matches[0].replace(rt,it),V.test(s[0].type)&&t.parentNode||t))){if(s.splice(a,1),e=i.length&&yt(s),!e)return M.apply(n,i),n;break}}}return l(e,f)(i,t,!h,n,V.test(e)),n}r.sortStable=b.split("").sort(A).join("")===b,r.detectDuplicates=S,p(),r.sortDetached=ut(function(e){return 1&e.compareDocumentPosition(f.createElement("div"))}),ut(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||ct("type|href|height|width",function(e,n,r){return r?t:e.getAttribute(n,"type"===n.toLowerCase()?1:2)}),r.attributes&&ut(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||ct("value",function(e,n,r){return r||"input"!==e.nodeName.toLowerCase()?t:e.defaultValue}),ut(function(e){return null==e.getAttribute("disabled")})||ct(B,function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&i.specified?i.value:e[n]===!0?n.toLowerCase():null}),x.find=at,x.expr=at.selectors,x.expr[":"]=x.expr.pseudos,x.unique=at.uniqueSort,x.text=at.getText,x.isXMLDoc=at.isXML,x.contains=at.contains}(e);var O={};function F(e){var t=O[e]={};return x.each(e.match(T)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?O[e]||F(e):x.extend({},e);var n,r,i,o,a,s,l=[],u=!e.once&&[],c=function(t){for(r=e.memory&&t,i=!0,a=s||0,s=0,o=l.length,n=!0;l&&o>a;a++)if(l[a].apply(t[0],t[1])===!1&&e.stopOnFalse){r=!1;break}n=!1,l&&(u?u.length&&c(u.shift()):r?l=[]:p.disable())},p={add:function(){if(l){var t=l.length;(function i(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&p.has(n)||l.push(n):n&&n.length&&"string"!==r&&i(n)})})(arguments),n?o=l.length:r&&(s=t,c(r))}return this},remove:function(){return l&&x.each(arguments,function(e,t){var r;while((r=x.inArray(t,l,r))>-1)l.splice(r,1),n&&(o>=r&&o--,a>=r&&a--)}),this},has:function(e){return e?x.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],o=0,this},disable:function(){return l=u=r=t,this},disabled:function(){return!l},lock:function(){return u=t,r||p.disable(),this},locked:function(){return!u},fireWith:function(e,t){return!l||i&&!u||(t=t||[],t=[e,t.slice?t.slice():t],n?u.push(t):c(t)),this},fire:function(){return p.fireWith(this,arguments),this},fired:function(){return!!i}};return p},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var a=o[0],s=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=s&&s.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[a+"With"](this===r?n.promise():this,s?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=g.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),a=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?g.call(arguments):r,n===s?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},s,l,u;if(r>1)for(s=Array(r),l=Array(r),u=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(a(t,u,n)).fail(o.reject).progress(a(t,l,s)):--i;return i||o.resolveWith(u,n),o.promise()}}),x.support=function(t){var n,r,o,s,l,u,c,p,f,d=a.createElement("div");if(d.setAttribute("className","t"),d.innerHTML="
                                    a",n=d.getElementsByTagName("*")||[],r=d.getElementsByTagName("a")[0],!r||!r.style||!n.length)return t;s=a.createElement("select"),u=s.appendChild(a.createElement("option")),o=d.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t.getSetAttribute="t"!==d.className,t.leadingWhitespace=3===d.firstChild.nodeType,t.tbody=!d.getElementsByTagName("tbody").length,t.htmlSerialize=!!d.getElementsByTagName("link").length,t.style=/top/.test(r.getAttribute("style")),t.hrefNormalized="/a"===r.getAttribute("href"),t.opacity=/^0.5/.test(r.style.opacity),t.cssFloat=!!r.style.cssFloat,t.checkOn=!!o.value,t.optSelected=u.selected,t.enctype=!!a.createElement("form").enctype,t.html5Clone="<:nav>"!==a.createElement("nav").cloneNode(!0).outerHTML,t.inlineBlockNeedsLayout=!1,t.shrinkWrapBlocks=!1,t.pixelPosition=!1,t.deleteExpando=!0,t.noCloneEvent=!0,t.reliableMarginRight=!0,t.boxSizingReliable=!0,o.checked=!0,t.noCloneChecked=o.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!u.disabled;try{delete d.test}catch(h){t.deleteExpando=!1}o=a.createElement("input"),o.setAttribute("value",""),t.input=""===o.getAttribute("value"),o.value="t",o.setAttribute("type","radio"),t.radioValue="t"===o.value,o.setAttribute("checked","t"),o.setAttribute("name","t"),l=a.createDocumentFragment(),l.appendChild(o),t.appendChecked=o.checked,t.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,d.attachEvent&&(d.attachEvent("onclick",function(){t.noCloneEvent=!1}),d.cloneNode(!0).click());for(f in{submit:!0,change:!0,focusin:!0})d.setAttribute(c="on"+f,"t"),t[f+"Bubbles"]=c in e||d.attributes[c].expando===!1;d.style.backgroundClip="content-box",d.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===d.style.backgroundClip;for(f in x(t))break;return t.ownLast="0"!==f,x(function(){var n,r,o,s="padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",l=a.getElementsByTagName("body")[0];l&&(n=a.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",l.appendChild(n).appendChild(d),d.innerHTML="
                                    t
                                    ",o=d.getElementsByTagName("td"),o[0].style.cssText="padding:0;margin:0;border:0;display:none",p=0===o[0].offsetHeight,o[0].style.display="",o[1].style.display="none",t.reliableHiddenOffsets=p&&0===o[0].offsetHeight,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",x.swap(l,null!=l.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===d.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(d,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(d,null)||{width:"4px"}).width,r=d.appendChild(a.createElement("div")),r.style.cssText=d.style.cssText=s,r.style.marginRight=r.style.width="0",d.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),typeof d.style.zoom!==i&&(d.innerHTML="",d.style.cssText=s+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=3===d.offsetWidth,d.style.display="block",d.innerHTML="
                                    ",d.firstChild.style.width="5px",t.shrinkWrapBlocks=3!==d.offsetWidth,t.inlineBlockNeedsLayout&&(l.style.zoom=1)),l.removeChild(n),n=d=o=r=null)}),n=s=l=u=r=o=null,t -}({});var B=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;function R(e,n,r,i){if(x.acceptData(e)){var o,a,s=x.expando,l=e.nodeType,u=l?x.cache:e,c=l?e[s]:e[s]&&s;if(c&&u[c]&&(i||u[c].data)||r!==t||"string"!=typeof n)return c||(c=l?e[s]=p.pop()||x.guid++:s),u[c]||(u[c]=l?{}:{toJSON:x.noop}),("object"==typeof n||"function"==typeof n)&&(i?u[c]=x.extend(u[c],n):u[c].data=x.extend(u[c].data,n)),a=u[c],i||(a.data||(a.data={}),a=a.data),r!==t&&(a[x.camelCase(n)]=r),"string"==typeof n?(o=a[n],null==o&&(o=a[x.camelCase(n)])):o=a,o}}function W(e,t,n){if(x.acceptData(e)){var r,i,o=e.nodeType,a=o?x.cache:e,s=o?e[x.expando]:x.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){x.isArray(t)?t=t.concat(x.map(t,x.camelCase)):t in r?t=[t]:(t=x.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;while(i--)delete r[t[i]];if(n?!I(r):!x.isEmptyObject(r))return}(n||(delete a[s].data,I(a[s])))&&(o?x.cleanData([e],!0):x.support.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}x.extend({cache:{},noData:{applet:!0,embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?x.cache[e[x.expando]]:e[x.expando],!!e&&!I(e)},data:function(e,t,n){return R(e,t,n)},removeData:function(e,t){return W(e,t)},_data:function(e,t,n){return R(e,t,n,!0)},_removeData:function(e,t){return W(e,t,!0)},acceptData:function(e){if(e.nodeType&&1!==e.nodeType&&9!==e.nodeType)return!1;var t=e.nodeName&&x.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),x.fn.extend({data:function(e,n){var r,i,o=null,a=0,s=this[0];if(e===t){if(this.length&&(o=x.data(s),1===s.nodeType&&!x._data(s,"parsedAttrs"))){for(r=s.attributes;r.length>a;a++)i=r[a].name,0===i.indexOf("data-")&&(i=x.camelCase(i.slice(5)),$(s,i,o[i]));x._data(s,"parsedAttrs",!0)}return o}return"object"==typeof e?this.each(function(){x.data(this,e)}):arguments.length>1?this.each(function(){x.data(this,e,n)}):s?$(s,e,x.data(s,e)):null},removeData:function(e){return this.each(function(){x.removeData(this,e)})}});function $(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(P,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:B.test(r)?x.parseJSON(r):r}catch(o){}x.data(e,n,r)}else r=t}return r}function I(e){var t;for(t in e)if(("data"!==t||!x.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}x.extend({queue:function(e,n,r){var i;return e?(n=(n||"fx")+"queue",i=x._data(e,n),r&&(!i||x.isArray(r)?i=x._data(e,n,x.makeArray(r)):i.push(r)),i||[]):t},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),a=function(){x.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return x._data(e,n)||x._data(e,n,{empty:x.Callbacks("once memory").add(function(){x._removeData(e,t+"queue"),x._removeData(e,n)})})}}),x.fn.extend({queue:function(e,n){var r=2;return"string"!=typeof e&&(n=e,e="fx",r--),r>arguments.length?x.queue(this[0],e):n===t?this:this.each(function(){var t=x.queue(this,e,n);x._queueHooks(this,e),"fx"===e&&"inprogress"!==t[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,o=x.Deferred(),a=this,s=this.length,l=function(){--i||o.resolveWith(a,[a])};"string"!=typeof e&&(n=e,e=t),e=e||"fx";while(s--)r=x._data(a[s],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(l));return l(),o.promise(n)}});var z,X,U=/[\t\r\n\f]/g,V=/\r/g,Y=/^(?:input|select|textarea|button|object)$/i,J=/^(?:a|area)$/i,G=/^(?:checked|selected)$/i,Q=x.support.getSetAttribute,K=x.support.input;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return e=x.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,o,a=0,s=this.length,l="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,a=0,s=this.length,l=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var t,r=0,o=x(this),a=e.match(T)||[];while(t=a[r++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else(n===i||"boolean"===n)&&(this.className&&x._data(this,"__className__",this.className),this.className=this.className||e===!1?"":x._data(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(U," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,o=this[0];{if(arguments.length)return i=x.isFunction(e),this.each(function(n){var o;1===this.nodeType&&(o=i?e.call(this,n,x(this).val()):e,null==o?o="":"number"==typeof o?o+="":x.isArray(o)&&(o=x.map(o,function(e){return null==e?"":e+""})),r=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],r&&"set"in r&&r.set(this,o,"value")!==t||(this.value=o))});if(o)return r=x.valHooks[o.type]||x.valHooks[o.nodeName.toLowerCase()],r&&"get"in r&&(n=r.get(o,"value"))!==t?n:(n=o.value,"string"==typeof n?n.replace(V,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=x.find.attr(e,"value");return null!=t?t:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;for(;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;a.push(t)}return a},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),a=i.length;while(a--)r=i[a],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,n,r){var o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===i?x.prop(e,n,r):(1===s&&x.isXMLDoc(e)||(n=n.toLowerCase(),o=x.attrHooks[n]||(x.expr.match.bool.test(n)?X:z)),r===t?o&&"get"in o&&null!==(a=o.get(e,n))?a:(a=x.find.attr(e,n),null==a?t:a):null!==r?o&&"set"in o&&(a=o.set(e,r,n))!==t?a:(e.setAttribute(n,r+""),r):(x.removeAttr(e,n),t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(T);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)?K&&Q||!G.test(n)?e[r]=!1:e[x.camelCase("default-"+n)]=e[r]=!1:x.attr(e,n,""),e.removeAttribute(Q?n:r)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,n,r){var i,o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return a=1!==s||!x.isXMLDoc(e),a&&(n=x.propFix[n]||n,o=x.propHooks[n]),r!==t?o&&"set"in o&&(i=o.set(e,r,n))!==t?i:e[n]=r:o&&"get"in o&&null!==(i=o.get(e,n))?i:e[n]},propHooks:{tabIndex:{get:function(e){var t=x.find.attr(e,"tabindex");return t?parseInt(t,10):Y.test(e.nodeName)||J.test(e.nodeName)&&e.href?0:-1}}}}),X={set:function(e,t,n){return t===!1?x.removeAttr(e,n):K&&Q||!G.test(n)?e.setAttribute(!Q&&x.propFix[n]||n,n):e[x.camelCase("default-"+n)]=e[n]=!0,n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,n){var r=x.expr.attrHandle[n]||x.find.attr;x.expr.attrHandle[n]=K&&Q||!G.test(n)?function(e,n,i){var o=x.expr.attrHandle[n],a=i?t:(x.expr.attrHandle[n]=t)!=r(e,n,i)?n.toLowerCase():null;return x.expr.attrHandle[n]=o,a}:function(e,n,r){return r?t:e[x.camelCase("default-"+n)]?n.toLowerCase():null}}),K&&Q||(x.attrHooks.value={set:function(e,n,r){return x.nodeName(e,"input")?(e.defaultValue=n,t):z&&z.set(e,n,r)}}),Q||(z={set:function(e,n,r){var i=e.getAttributeNode(r);return i||e.setAttributeNode(i=e.ownerDocument.createAttribute(r)),i.value=n+="","value"===r||n===e.getAttribute(r)?n:t}},x.expr.attrHandle.id=x.expr.attrHandle.name=x.expr.attrHandle.coords=function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&""!==i.value?i.value:null},x.valHooks.button={get:function(e,n){var r=e.getAttributeNode(n);return r&&r.specified?r.value:t},set:z.set},x.attrHooks.contenteditable={set:function(e,t,n){z.set(e,""===t?!1:t,n)}},x.each(["width","height"],function(e,n){x.attrHooks[n]={set:function(e,r){return""===r?(e.setAttribute(n,"auto"),r):t}}})),x.support.hrefNormalized||x.each(["href","src"],function(e,t){x.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),x.support.style||(x.attrHooks.style={get:function(e){return e.style.cssText||t},set:function(e,t){return e.style.cssText=t+""}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.support.enctype||(x.propFix.enctype="encoding"),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,n){return x.isArray(n)?e.checked=x.inArray(x(e).val(),n)>=0:t}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Z=/^(?:input|select|textarea)$/i,et=/^key/,tt=/^(?:mouse|contextmenu)|click/,nt=/^(?:focusinfocus|focusoutblur)$/,rt=/^([^.]*)(?:\.(.+)|)$/;function it(){return!0}function ot(){return!1}function at(){try{return a.activeElement}catch(e){}}x.event={global:{},add:function(e,n,r,o,a){var s,l,u,c,p,f,d,h,g,m,y,v=x._data(e);if(v){r.handler&&(c=r,r=c.handler,a=c.selector),r.guid||(r.guid=x.guid++),(l=v.events)||(l=v.events={}),(f=v.handle)||(f=v.handle=function(e){return typeof x===i||e&&x.event.triggered===e.type?t:x.event.dispatch.apply(f.elem,arguments)},f.elem=e),n=(n||"").match(T)||[""],u=n.length;while(u--)s=rt.exec(n[u])||[],g=y=s[1],m=(s[2]||"").split(".").sort(),g&&(p=x.event.special[g]||{},g=(a?p.delegateType:p.bindType)||g,p=x.event.special[g]||{},d=x.extend({type:g,origType:y,data:o,handler:r,guid:r.guid,selector:a,needsContext:a&&x.expr.match.needsContext.test(a),namespace:m.join(".")},c),(h=l[g])||(h=l[g]=[],h.delegateCount=0,p.setup&&p.setup.call(e,o,m,f)!==!1||(e.addEventListener?e.addEventListener(g,f,!1):e.attachEvent&&e.attachEvent("on"+g,f))),p.add&&(p.add.call(e,d),d.handler.guid||(d.handler.guid=r.guid)),a?h.splice(h.delegateCount++,0,d):h.push(d),x.event.global[g]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,p,f,d,h,g,m=x.hasData(e)&&x._data(e);if(m&&(c=m.events)){t=(t||"").match(T)||[""],u=t.length;while(u--)if(s=rt.exec(t[u])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){p=x.event.special[d]||{},d=(r?p.delegateType:p.bindType)||d,f=c[d]||[],s=s[2]&&RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;while(o--)a=f[o],!i&&g!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,p.remove&&p.remove.call(e,a));l&&!f.length&&(p.teardown&&p.teardown.call(e,h,m.handle)!==!1||x.removeEvent(e,d,m.handle),delete c[d])}else for(d in c)x.event.remove(e,d+t[u],n,r,!0);x.isEmptyObject(c)&&(delete m.handle,x._removeData(e,"events"))}},trigger:function(n,r,i,o){var s,l,u,c,p,f,d,h=[i||a],g=v.call(n,"type")?n.type:n,m=v.call(n,"namespace")?n.namespace.split("."):[];if(u=f=i=i||a,3!==i.nodeType&&8!==i.nodeType&&!nt.test(g+x.event.triggered)&&(g.indexOf(".")>=0&&(m=g.split("."),g=m.shift(),m.sort()),l=0>g.indexOf(":")&&"on"+g,n=n[x.expando]?n:new x.Event(g,"object"==typeof n&&n),n.isTrigger=o?2:3,n.namespace=m.join("."),n.namespace_re=n.namespace?RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,n.result=t,n.target||(n.target=i),r=null==r?[n]:x.makeArray(r,[n]),p=x.event.special[g]||{},o||!p.trigger||p.trigger.apply(i,r)!==!1)){if(!o&&!p.noBubble&&!x.isWindow(i)){for(c=p.delegateType||g,nt.test(c+g)||(u=u.parentNode);u;u=u.parentNode)h.push(u),f=u;f===(i.ownerDocument||a)&&h.push(f.defaultView||f.parentWindow||e)}d=0;while((u=h[d++])&&!n.isPropagationStopped())n.type=d>1?c:p.bindType||g,s=(x._data(u,"events")||{})[n.type]&&x._data(u,"handle"),s&&s.apply(u,r),s=l&&u[l],s&&x.acceptData(u)&&s.apply&&s.apply(u,r)===!1&&n.preventDefault();if(n.type=g,!o&&!n.isDefaultPrevented()&&(!p._default||p._default.apply(h.pop(),r)===!1)&&x.acceptData(i)&&l&&i[g]&&!x.isWindow(i)){f=i[l],f&&(i[l]=null),x.event.triggered=g;try{i[g]()}catch(y){}x.event.triggered=t,f&&(i[l]=f)}return n.result}},dispatch:function(e){e=x.event.fix(e);var n,r,i,o,a,s=[],l=g.call(arguments),u=(x._data(this,"events")||{})[e.type]||[],c=x.event.special[e.type]||{};if(l[0]=e,e.delegateTarget=this,!c.preDispatch||c.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),n=0;while((o=s[n++])&&!e.isPropagationStopped()){e.currentTarget=o.elem,a=0;while((i=o.handlers[a++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(i.namespace))&&(e.handleObj=i,e.data=i.data,r=((x.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,l),r!==t&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,n){var r,i,o,a,s=[],l=n.delegateCount,u=e.target;if(l&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!=this;u=u.parentNode||this)if(1===u.nodeType&&(u.disabled!==!0||"click"!==e.type)){for(o=[],a=0;l>a;a++)i=n[a],r=i.selector+" ",o[r]===t&&(o[r]=i.needsContext?x(r,this).index(u)>=0:x.find(r,this,null,[u]).length),o[r]&&o.push(i);o.length&&s.push({elem:u,handlers:o})}return n.length>l&&s.push({elem:this,handlers:n.slice(l)}),s},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,o=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=tt.test(i)?this.mouseHooks:et.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new x.Event(o),t=r.length;while(t--)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||a),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,i,o,s=n.button,l=n.fromElement;return null==e.pageX&&null!=n.clientX&&(i=e.target.ownerDocument||a,o=i.documentElement,r=i.body,e.pageX=n.clientX+(o&&o.scrollLeft||r&&r.scrollLeft||0)-(o&&o.clientLeft||r&&r.clientLeft||0),e.pageY=n.clientY+(o&&o.scrollTop||r&&r.scrollTop||0)-(o&&o.clientTop||r&&r.clientTop||0)),!e.relatedTarget&&l&&(e.relatedTarget=l===e.target?n.toElement:l),e.which||s===t||(e.which=1&s?1:2&s?3:4&s?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==at()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===at()&&this.blur?(this.blur(),!1):t},delegateType:"focusout"},click:{trigger:function(){return x.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):t},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==t&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=a.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===i&&(e[r]=null),e.detachEvent(r,n))},x.Event=function(e,n){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?it:ot):this.type=e,n&&x.extend(this,n),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,t):new x.Event(e,n)},x.Event.prototype={isDefaultPrevented:ot,isPropagationStopped:ot,isImmediatePropagationStopped:ot,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=it,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=it,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=it,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.submitBubbles||(x.event.special.submit={setup:function(){return x.nodeName(this,"form")?!1:(x.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=x.nodeName(n,"input")||x.nodeName(n,"button")?n.form:t;r&&!x._data(r,"submitBubbles")&&(x.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),x._data(r,"submitBubbles",!0))}),t)},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&x.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return x.nodeName(this,"form")?!1:(x.event.remove(this,"._submit"),t)}}),x.support.changeBubbles||(x.event.special.change={setup:function(){return Z.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(x.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),x.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),x.event.simulate("change",this,e,!0)})),!1):(x.event.add(this,"beforeactivate._change",function(e){var t=e.target;Z.test(t.nodeName)&&!x._data(t,"changeBubbles")&&(x.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||x.event.simulate("change",this.parentNode,e,!0)}),x._data(t,"changeBubbles",!0))}),t)},handle:function(e){var n=e.target;return this!==n||e.isSimulated||e.isTrigger||"radio"!==n.type&&"checkbox"!==n.type?e.handleObj.handler.apply(this,arguments):t},teardown:function(){return x.event.remove(this,"._change"),!Z.test(this.nodeName)}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&a.addEventListener(e,r,!0)},teardown:function(){0===--n&&a.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,n,r,i,o){var a,s;if("object"==typeof e){"string"!=typeof n&&(r=r||n,n=t);for(a in e)this.on(a,n,r,e[a],o);return this}if(null==r&&null==i?(i=n,r=n=t):null==i&&("string"==typeof n?(i=r,r=t):(i=r,r=n,n=t)),i===!1)i=ot;else if(!i)return this;return 1===o&&(s=i,i=function(e){return x().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=x.guid++)),this.each(function(){x.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,o;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,x(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if("object"==typeof e){for(o in e)this.off(o,n,e[o]);return this}return(n===!1||"function"==typeof n)&&(r=n,n=t),r===!1&&(r=ot),this.each(function(){x.event.remove(this,e,r,n)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,n){var r=this[0];return r?x.event.trigger(e,n,r,!0):t}});var st=/^.[^:#\[\.,]*$/,lt=/^(?:parents|prev(?:Until|All))/,ut=x.expr.match.needsContext,ct={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t,n=x(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(x.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e||[],!0))},filter:function(e){return this.pushStack(ft(this,e||[],!1))},is:function(e){return!!ft(this,"string"==typeof e&&ut.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],a=ut.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(a?a.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?x.inArray(this[0],x(e)):x.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function pt(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return pt(e,"nextSibling")},prev:function(e){return pt(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return x.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(ct[e]||(i=x.unique(i)),lt.test(e)&&(i=i.reverse())),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,n,r){var i=[],o=e[n];while(o&&9!==o.nodeType&&(r===t||1!==o.nodeType||!x(o).is(r)))1===o.nodeType&&i.push(o),o=o[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function ft(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(st.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return x.inArray(e,t)>=0!==n})}function dt(e){var t=ht.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}var ht="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",gt=/ jQuery\d+="(?:null|\d+)"/g,mt=RegExp("<(?:"+ht+")[\\s/>]","i"),yt=/^\s+/,vt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bt=/<([\w:]+)/,xt=/\s*$/g,At={option:[1,""],legend:[1,"
                                    ","
                                    "],area:[1,"",""],param:[1,"",""],thead:[1,"","
                                    "],tr:[2,"","
                                    "],col:[2,"","
                                    "],td:[3,"","
                                    "],_default:x.support.htmlSerialize?[0,"",""]:[1,"X
                                    ","
                                    "]},jt=dt(a),Dt=jt.appendChild(a.createElement("div"));At.optgroup=At.option,At.tbody=At.tfoot=At.colgroup=At.caption=At.thead,At.th=At.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===t?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||a).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(Ft(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&_t(Ft(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++){1===e.nodeType&&x.cleanData(Ft(e,!1));while(e.firstChild)e.removeChild(e.firstChild);e.options&&x.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return 1===n.nodeType?n.innerHTML.replace(gt,""):t;if(!("string"!=typeof e||Tt.test(e)||!x.support.htmlSerialize&&mt.test(e)||!x.support.leadingWhitespace&&yt.test(e)||At[(bt.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(vt,"<$1>");try{for(;i>r;r++)n=this[r]||{},1===n.nodeType&&(x.cleanData(Ft(n,!1)),n.innerHTML=e);n=0}catch(o){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=d.apply([],e);var r,i,o,a,s,l,u=0,c=this.length,p=this,f=c-1,h=e[0],g=x.isFunction(h);if(g||!(1>=c||"string"!=typeof h||x.support.checkClone)&&Nt.test(h))return this.each(function(r){var i=p.eq(r);g&&(e[0]=h.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(l=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),r=l.firstChild,1===l.childNodes.length&&(l=r),r)){for(a=x.map(Ft(l,"script"),Ht),o=a.length;c>u;u++)i=l,u!==f&&(i=x.clone(i,!0,!0),o&&x.merge(a,Ft(i,"script"))),t.call(this[u],i,u);if(o)for(s=a[a.length-1].ownerDocument,x.map(a,qt),u=0;o>u;u++)i=a[u],kt.test(i.type||"")&&!x._data(i,"globalEval")&&x.contains(s,i)&&(i.src?x._evalUrl(i.src):x.globalEval((i.text||i.textContent||i.innerHTML||"").replace(St,"")));l=r=null}return this}});function Lt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function Ht(e){return e.type=(null!==x.find.attr(e,"type"))+"/"+e.type,e}function qt(e){var t=Et.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function _t(e,t){var n,r=0;for(;null!=(n=e[r]);r++)x._data(n,"globalEval",!t||x._data(t[r],"globalEval"))}function Mt(e,t){if(1===t.nodeType&&x.hasData(e)){var n,r,i,o=x._data(e),a=x._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)x.event.add(t,n,s[n][r])}a.data&&(a.data=x.extend({},a.data))}}function Ot(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!x.support.noCloneEvent&&t[x.expando]){i=x._data(t);for(r in i.events)x.removeEvent(t,r,i.handle);t.removeAttribute(x.expando)}"script"===n&&t.text!==e.text?(Ht(t).text=e.text,qt(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),x.support.html5Clone&&e.innerHTML&&!x.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Ct.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=0,i=[],o=x(e),a=o.length-1;for(;a>=r;r++)n=r===a?this:this.clone(!0),x(o[r])[t](n),h.apply(i,n.get());return this.pushStack(i)}});function Ft(e,n){var r,o,a=0,s=typeof e.getElementsByTagName!==i?e.getElementsByTagName(n||"*"):typeof e.querySelectorAll!==i?e.querySelectorAll(n||"*"):t;if(!s)for(s=[],r=e.childNodes||e;null!=(o=r[a]);a++)!n||x.nodeName(o,n)?s.push(o):x.merge(s,Ft(o,n));return n===t||n&&x.nodeName(e,n)?x.merge([e],s):s}function Bt(e){Ct.test(e.type)&&(e.defaultChecked=e.checked)}x.extend({clone:function(e,t,n){var r,i,o,a,s,l=x.contains(e.ownerDocument,e);if(x.support.html5Clone||x.isXMLDoc(e)||!mt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Dt.innerHTML=e.outerHTML,Dt.removeChild(o=Dt.firstChild)),!(x.support.noCloneEvent&&x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(r=Ft(o),s=Ft(e),a=0;null!=(i=s[a]);++a)r[a]&&Ot(i,r[a]);if(t)if(n)for(s=s||Ft(e),r=r||Ft(o),a=0;null!=(i=s[a]);a++)Mt(i,r[a]);else Mt(e,o);return r=Ft(o,"script"),r.length>0&&_t(r,!l&&Ft(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){var i,o,a,s,l,u,c,p=e.length,f=dt(t),d=[],h=0;for(;p>h;h++)if(o=e[h],o||0===o)if("object"===x.type(o))x.merge(d,o.nodeType?[o]:o);else if(wt.test(o)){s=s||f.appendChild(t.createElement("div")),l=(bt.exec(o)||["",""])[1].toLowerCase(),c=At[l]||At._default,s.innerHTML=c[1]+o.replace(vt,"<$1>")+c[2],i=c[0];while(i--)s=s.lastChild;if(!x.support.leadingWhitespace&&yt.test(o)&&d.push(t.createTextNode(yt.exec(o)[0])),!x.support.tbody){o="table"!==l||xt.test(o)?""!==c[1]||xt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;while(i--)x.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u)}x.merge(d,s.childNodes),s.textContent="";while(s.firstChild)s.removeChild(s.firstChild);s=f.lastChild}else d.push(t.createTextNode(o));s&&f.removeChild(s),x.support.appendChecked||x.grep(Ft(d,"input"),Bt),h=0;while(o=d[h++])if((!r||-1===x.inArray(o,r))&&(a=x.contains(o.ownerDocument,o),s=Ft(f.appendChild(o),"script"),a&&_t(s),n)){i=0;while(o=s[i++])kt.test(o.type||"")&&n.push(o)}return s=null,f},cleanData:function(e,t){var n,r,o,a,s=0,l=x.expando,u=x.cache,c=x.support.deleteExpando,f=x.event.special;for(;null!=(n=e[s]);s++)if((t||x.acceptData(n))&&(o=n[l],a=o&&u[o])){if(a.events)for(r in a.events)f[r]?x.event.remove(n,r):x.removeEvent(n,r,a.handle); -u[o]&&(delete u[o],c?delete n[l]:typeof n.removeAttribute!==i?n.removeAttribute(l):n[l]=null,p.push(o))}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}}),x.fn.extend({wrapAll:function(e){if(x.isFunction(e))return this.each(function(t){x(this).wrapAll(e.call(this,t))});if(this[0]){var t=x(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&1===e.firstChild.nodeType)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var Pt,Rt,Wt,$t=/alpha\([^)]*\)/i,It=/opacity\s*=\s*([^)]*)/,zt=/^(top|right|bottom|left)$/,Xt=/^(none|table(?!-c[ea]).+)/,Ut=/^margin/,Vt=RegExp("^("+w+")(.*)$","i"),Yt=RegExp("^("+w+")(?!px)[a-z%]+$","i"),Jt=RegExp("^([+-])=("+w+")","i"),Gt={BODY:"block"},Qt={position:"absolute",visibility:"hidden",display:"block"},Kt={letterSpacing:0,fontWeight:400},Zt=["Top","Right","Bottom","Left"],en=["Webkit","O","Moz","ms"];function tn(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=en.length;while(i--)if(t=en[i]+n,t in e)return t;return r}function nn(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function rn(e,t){var n,r,i,o=[],a=0,s=e.length;for(;s>a;a++)r=e[a],r.style&&(o[a]=x._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&nn(r)&&(o[a]=x._data(r,"olddisplay",ln(r.nodeName)))):o[a]||(i=nn(r),(n&&"none"!==n||!i)&&x._data(r,"olddisplay",i?n:x.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}x.fn.extend({css:function(e,n){return x.access(this,function(e,n,r){var i,o,a={},s=0;if(x.isArray(n)){for(o=Rt(e),i=n.length;i>s;s++)a[n[s]]=x.css(e,n[s],!1,o);return a}return r!==t?x.style(e,n,r):x.css(e,n)},e,n,arguments.length>1)},show:function(){return rn(this,!0)},hide:function(){return rn(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){nn(this)?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Wt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":x.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,a,s,l=x.camelCase(n),u=e.style;if(n=x.cssProps[l]||(x.cssProps[l]=tn(u,l)),s=x.cssHooks[n]||x.cssHooks[l],r===t)return s&&"get"in s&&(o=s.get(e,!1,i))!==t?o:u[n];if(a=typeof r,"string"===a&&(o=Jt.exec(r))&&(r=(o[1]+1)*o[2]+parseFloat(x.css(e,n)),a="number"),!(null==r||"number"===a&&isNaN(r)||("number"!==a||x.cssNumber[l]||(r+="px"),x.support.clearCloneStyle||""!==r||0!==n.indexOf("background")||(u[n]="inherit"),s&&"set"in s&&(r=s.set(e,r,i))===t)))try{u[n]=r}catch(c){}}},css:function(e,n,r,i){var o,a,s,l=x.camelCase(n);return n=x.cssProps[l]||(x.cssProps[l]=tn(e.style,l)),s=x.cssHooks[n]||x.cssHooks[l],s&&"get"in s&&(a=s.get(e,!0,r)),a===t&&(a=Wt(e,n,i)),"normal"===a&&n in Kt&&(a=Kt[n]),""===r||r?(o=parseFloat(a),r===!0||x.isNumeric(o)?o||0:a):a}}),e.getComputedStyle?(Rt=function(t){return e.getComputedStyle(t,null)},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s.getPropertyValue(n)||s[n]:t,u=e.style;return s&&(""!==l||x.contains(e.ownerDocument,e)||(l=x.style(e,n)),Yt.test(l)&&Ut.test(n)&&(i=u.width,o=u.minWidth,a=u.maxWidth,u.minWidth=u.maxWidth=u.width=l,l=s.width,u.width=i,u.minWidth=o,u.maxWidth=a)),l}):a.documentElement.currentStyle&&(Rt=function(e){return e.currentStyle},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s[n]:t,u=e.style;return null==l&&u&&u[n]&&(l=u[n]),Yt.test(l)&&!zt.test(n)&&(i=u.left,o=e.runtimeStyle,a=o&&o.left,a&&(o.left=e.currentStyle.left),u.left="fontSize"===n?"1em":l,l=u.pixelLeft+"px",u.left=i,a&&(o.left=a)),""===l?"auto":l});function on(e,t,n){var r=Vt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function an(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;for(;4>o;o+=2)"margin"===n&&(a+=x.css(e,n+Zt[o],!0,i)),r?("content"===n&&(a-=x.css(e,"padding"+Zt[o],!0,i)),"margin"!==n&&(a-=x.css(e,"border"+Zt[o]+"Width",!0,i))):(a+=x.css(e,"padding"+Zt[o],!0,i),"padding"!==n&&(a+=x.css(e,"border"+Zt[o]+"Width",!0,i)));return a}function sn(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Rt(e),a=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=Wt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Yt.test(i))return i;r=a&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+an(e,t,n||(a?"border":"content"),r,o)+"px"}function ln(e){var t=a,n=Gt[e];return n||(n=un(e,t),"none"!==n&&n||(Pt=(Pt||x(" + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/MessageTemplate/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/MessageTemplate/_CreateOrUpdate.cshtml index 2d1bca860b..9bb79b8299 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/MessageTemplate/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/MessageTemplate/_CreateOrUpdate.cshtml @@ -1,11 +1,25 @@ @model MessageTemplateModel -@using SmartStore.Web.Framework.UI; +@using SmartStore.Web.Framework.UI @Html.ValidationSummary(false) @Html.HiddenFor(model => model.Id) -@Html.SmartStore().TabStrip().Name("template-edit").Style(TabsStyle.Tabs).Position(TabsPosition.Top).Items(x => + + +@Html.SmartStore().TabStrip().Name("template-edit").Style(TabsStyle.Material).Position(TabsPosition.Top).Items(x => { x.Add().Text(T("Admin.Common.Info").Text).Content(TabInfo()).Selected(true); x.Add().Text(T("Admin.Common.Stores").Text).Content(TabStores()); @@ -15,22 +29,28 @@ }) @helper TabInfo() -{ -
                                    - - - - +{ + string modelTree = ViewBag.LastModelTreeJson; + string templateName = "MessageTemplate/" + Model.Name; + + if (modelTree.HasValue()) + { + + } + +
                                    - @Html.SmartLabelFor(model => model.TokensTree) - - @Html.EditorFor(model => model.TokensTree, "TokenSelector") -
                                    @@ -60,16 +80,37 @@ @Html.SmartLabelFor(model => model.Locales[item].EmailAccountId) + + + + + + + + @@ -78,7 +119,7 @@ @Html.SmartLabelFor(model => model.Locales[item].Subject) @@ -109,20 +150,12 @@ @Html.ValidationMessageFor(model => model.Locales[item].Attachment3FileId) - - + - - -
                                    @Html.SmartLabelFor(model => model.Name) - @Model.Name +
                                    + @Model.Name +
                                    @Html.HiddenFor(model => model.Name)
                                    + @*IMPORTANT: Do not delete, this hidden element contains the id to assign localized values to the corresponding language *@ + @Html.HiddenFor(model => model.Locales[item].LanguageId) + @Html.DropDownListFor(model => Model.Locales[item].EmailAccountId, new SelectList(Model.AvailableEmailAccounts, "Id", "DisplayName", Model.Locales[item].EmailAccountId)) @Html.ValidationMessageFor(model => model.Locales[item].EmailAccountId)
                                    + @Html.SmartLabelFor(model => model.Locales[item].To) + + @Html.TextBoxFor(model => Model.Locales[item].To) + @Html.ValidationMessageFor(model => model.Locales[item].To) +
                                    + @Html.SmartLabelFor(model => model.Locales[item].ReplyTo) + + @Html.TextBoxFor(model => Model.Locales[item].ReplyTo) + @Html.ValidationMessageFor(model => model.Locales[item].ReplyTo) +
                                    @Html.SmartLabelFor(model => model.Locales[item].BccEmailAddresses) - @Html.EditorFor(model => Model.Locales[item].BccEmailAddresses) + @Html.TextBoxFor(model => Model.Locales[item].BccEmailAddresses) @Html.ValidationMessageFor(model => model.Locales[item].BccEmailAddresses)
                                    - @Html.TextBoxFor(model => Model.Locales[item].Subject, new { @class = "input-large" }) + @Html.TextBoxFor(model => Model.Locales[item].Subject) @Html.ValidationMessageFor(model => model.Locales[item].Subject)
                                    - @Html.SmartLabelFor(model => model.Locales[item].Body) -
                                    - @Html.EditorFor(model => Model.Locales[item].Body, "RichEditor") + @Html.EditorFor(model => Model.Locales[item].Body, "Liquid", new { TemplateName = templateName }) @Html.ValidationMessageFor(model => model.Locales[item].Body)
                                    - @Html.HiddenFor(model => model.Locales[item].LanguageId) -
                                    , @ @@ -135,12 +168,30 @@ @Html.ValidationMessageFor(model => model.EmailAccountId) + + + + + + + + @@ -149,7 +200,7 @@ @Html.SmartLabelFor(model => model.Subject) @@ -180,12 +231,9 @@ @Html.ValidationMessageFor(model => model.Attachment3FileId) - - + @@ -195,54 +243,5 @@ @helper TabStores() { - -
                                    + @Html.SmartLabelFor(model => model.To) + + @Html.TextBoxFor(model => model.To) + @Html.ValidationMessageFor(model => model.To) +
                                    + @Html.SmartLabelFor(model => model.ReplyTo) + + @Html.TextBoxFor(model => model.ReplyTo) + @Html.ValidationMessageFor(model => model.ReplyTo) +
                                    @Html.SmartLabelFor(model => model.BccEmailAddresses) - @Html.EditorFor(model => model.BccEmailAddresses) + @Html.TextBoxFor(model => model.BccEmailAddresses) @Html.ValidationMessageFor(model => model.BccEmailAddresses)
                                    - @Html.TextBoxFor(model => model.Subject, new { @class = "input-large" }) + @Html.TextBoxFor(model => model.Subject) @Html.ValidationMessageFor(model => model.Subject)
                                    - @Html.SmartLabelFor(model => model.Body) -
                                    - @Html.EditorFor(model => model.Body, "RichEditor") + @Html.EditorFor(model => model.Body, "Liquid", new { TemplateName = templateName }) @Html.ValidationMessageFor(model => model.Body)
                                    - - - - - - - - -
                                    - @Html.SmartLabelFor(model => model.LimitedToStores) - - @Html.EditorFor(model => model.LimitedToStores) - @Html.ValidationMessageFor(model => model.LimitedToStores) -
                                    - @Html.SmartLabelFor(model => model.AvailableStores) - - @if (Model.AvailableStores != null && Model.AvailableStores.Count > 0) - { - foreach (var store in Model.AvailableStores) - { - - } - } - else - { -
                                    @T("Admin.Configuration.Stores.NoStoresDefined")
                                    - } -
                                    + @Html.Partial("StoreSelector", Model) } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/News/Comments.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/News/Comments.cshtml index 79347bed2a..5edd8cdda4 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/News/Comments.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/News/Comments.cshtml @@ -3,10 +3,8 @@ @{ var gridPageSize = EngineContext.Current.Resolve().GridPageSize; - int? filterByNewsItemId = ViewBag.FilterByNewsItemId; - //page title ViewBag.Title = T("Admin.ContentManagement.News.Comments").Text; }
                                    @@ -17,55 +15,54 @@
                                    - - - - -
                                    - @(Html.Telerik().Grid(Model.Data) - .Name("comments-grid") - .DataKeys(x => { - x.Add(y => y.Id).RouteKey("Id"); - }) - .Columns(columns => - { - columns.Bound(x => x.Id) - .Width(50); - columns.Bound(x => x.NewsItemTitle) - .Width(450) - .Template(x => Html.ActionLink(x.NewsItemTitle, "Edit", "News", new { id = x.NewsItemId }, new { })) - .ClientTemplate("\"><#= NewsItemTitle #>"); - columns.Bound(x => x.CustomerId) - .Width(200) - .Template(x => Html.ActionLink(x.CustomerName, "Edit", "Customer", new { id = x.CustomerId }, new { })) - .ClientTemplate("\"><#= CustomerName #>"); - columns.Bound(x => x.CommentTitle) - .Width(200); - columns.Bound(x => x.CommentText) - .Encoded(false); - columns.Bound(x => x.IpAddress) - .Width(100); - columns.Bound(x => x.CreatedOn) - .Width(150); - columns.Command(commands => - { - commands.Delete().Localize(T); - }).Width(90); - }) - .Pageable(settings => settings.Total(Model.Total).PageSize(gridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => { - var settingBuilder = dataBinding.Ajax(); - if (filterByNewsItemId.HasValue) - { - settingBuilder = settingBuilder.Select("Comments", "News", new { filterByNewsItemId = filterByNewsItemId.Value }); - settingBuilder = settingBuilder.Delete("CommentDelete", "News", new { filterByNewsItemId = filterByNewsItemId.Value }); - } - else - { - settingBuilder = settingBuilder.Select("Comments", "News"); - settingBuilder = settingBuilder.Delete("CommentDelete", "News"); - } - }) - .PreserveGridState() - .EnableCustomBinding(true)) -
                                    + +
                                    + @(Html.Telerik().Grid(Model.Data) + .Name("comments-grid") + .DataKeys(x => + { + x.Add(y => y.Id).RouteKey("Id"); + }) + .Columns(columns => + { + columns.Bound(x => x.Id) + .Width(50); + columns.Bound(x => x.NewsItemTitle) + .Width(450) + .Template(x => Html.ActionLink(x.NewsItemTitle, "Edit", "News", new { id = x.NewsItemId }, new { })) + .ClientTemplate("\"><#= NewsItemTitle #>"); + columns.Bound(x => x.CustomerId) + .Width(200) + .Template(x => Html.ActionLink(x.CustomerName, "Edit", "Customer", new { id = x.CustomerId }, new { })) + .ClientTemplate("\"><#= CustomerName #>"); + columns.Bound(x => x.CommentTitle) + .Width(200); + columns.Bound(x => x.CommentText) + .Encoded(false); + columns.Bound(x => x.IpAddress) + .Width(100); + columns.Bound(x => x.CreatedOn) + .Width(150); + columns.Command(commands => + { + commands.Delete().Localize(T); + }).Width(90); + }) + .Pageable(settings => settings.Total(Model.Total).PageSize(gridPageSize).Position(GridPagerPosition.Both)) + .DataBinding(dataBinding => + { + var settingBuilder = dataBinding.Ajax(); + if (filterByNewsItemId.HasValue) + { + settingBuilder = settingBuilder.Select("Comments", "News", new { filterByNewsItemId = filterByNewsItemId.Value }); + settingBuilder = settingBuilder.Delete("CommentDelete", "News", new { filterByNewsItemId = filterByNewsItemId.Value }); + } + else + { + settingBuilder = settingBuilder.Select("Comments", "News"); + settingBuilder = settingBuilder.Delete("CommentDelete", "News"); + } + }) + .PreserveGridState() + .EnableCustomBinding(true)) +
                                    \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/News/Create.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/News/Create.cshtml index d253fc2cd2..f3fc345949 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/News/Create.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/News/Create.cshtml @@ -1,6 +1,5 @@ @model NewsItemModel @{ - //page title ViewBag.Title = T("Admin.ContentManagement.News.NewsItems.AddNew").Text; } @using (Html.BeginForm()) @@ -10,8 +9,13 @@ @T("Admin.ContentManagement.News.NewsItems.AddNew") @Html.ActionLink("(" + T("Admin.ContentManagement.News.NewsItems.BackToList") + ")", "List")
                                    - - + +
                                    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/News/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/News/Edit.cshtml index 639b10fd04..06fcf0f6aa 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/News/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/News/Edit.cshtml @@ -11,9 +11,17 @@
                                    @{ Html.RenderWidget("admin_button_toolbar_before"); } - - - + + + @{ Html.RenderWidget("admin_button_toolbar_after"); }
                                    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/News/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/News/List.cshtml index 46f4322625..39a1707d01 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/News/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/News/List.cshtml @@ -15,87 +15,79 @@
                                    @{ Html.RenderWidget("admin_button_toolbar_before"); } + + + @T("Admin.Common.AddNew") + + -  @T("Admin.Common.AddNew") - @{ Html.RenderWidget("admin_button_toolbar_after"); }
                                    @if (Model.AvailableStores.Count > 1) { - - - - - - - - - -
                                    - @Html.SmartLabelFor(model => model.SearchStoreId) - - @Html.DropDownList("SearchStoreId", Model.AvailableStores, T("Admin.Common.All")) -
                                    -   - - -
                                    +
                                    +
                                    + @Html.SmartLabelFor(model => model.SearchStoreId) + @Html.DropDownListFor(model => model.SearchStoreId, Model.AvailableStores, T("Admin.Common.All"), new { @class = "form-control" }) +
                                    +
                                    + + +
                                    +
                                    } -

                                    - - - - - -
                                    - @(Html.Telerik().Grid() - .Name("newsitem-grid") - .Columns(columns => - { - columns.Bound(x => x.Id) - .ClientTemplate("") - .Title("") - .HtmlAttributes(new { style = "text-align:center" }) - .HeaderHtmlAttributes(new { style = "text-align:center" }); - columns.Bound(x => x.Title) - .Width(450) - .Template(x => Html.ActionLink(x.Title, "Edit", new { id = x.Id })) - .ClientTemplate("\"><#= Title #>"); - columns.Bound(x => x.Published) - .Template(item => @Html.SymbolForBool(item.Published)) - .ClientTemplate(@Html.SymbolForBool("Published")) - .Centered(); - columns.Bound(x => x.LanguageName); - columns.Bound(x => x.Comments) - .Template( - @
                                    - @Html.ActionLink(T("Admin.ContentManagement.News.NewsItems.Fields.Comments").Text + " - " + @item.Comments, "Comments", new { filterByNewsItemId = item.Id }) -
                                    - ) - .ClientTemplate(""); - columns.Bound(x => x.StartDate); - columns.Bound(x => x.EndDate); - columns.Bound(x => x.LimitedToStores) - .Template(item => @Html.SymbolForBool(item.LimitedToStores)) - .ClientTemplate(@Html.SymbolForBool("LimitedToStores")) - .Hidden(Model.AvailableStores.Count <= 1) - .Centered(); - columns.Bound(x => x.CreatedOn); - }) - .Pageable(settings => settings.PageSize(gridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("List", "News")) - .ClientEvents(events => events.OnDataBinding("onDataBinding").OnDataBound("onDataBound").OnRowDataBound("onRowDataBound")) - .PreserveGridState() - .EnableCustomBinding(true)) -
                                    +
                                    + @(Html.Telerik().Grid() + .Name("newsitem-grid") + .Columns(columns => + { + columns.Bound(x => x.Id) + .ClientTemplate("") + .Title("") + .HtmlAttributes(new { style = "text-align:center" }) + .HeaderHtmlAttributes(new { style = "text-align:center" }); + columns.Bound(x => x.Title) + .Width(450) + .Template(x => Html.ActionLink(x.Title, "Edit", new { id = x.Id })) + .ClientTemplate("\"><#= Title #>"); + columns.Bound(x => x.Published) + .Template(item => @Html.SymbolForBool(item.Published)) + .ClientTemplate(@Html.SymbolForBool("Published")) + .Centered(); + columns.Bound(x => x.LanguageName); + columns.Bound(x => x.Comments) + .Template( + @
                                    + @Html.ActionLink(T("Admin.ContentManagement.News.NewsItems.Fields.Comments").Text + " - " + @item.Comments, "Comments", new { filterByNewsItemId = item.Id }) +
                                    + ) + .ClientTemplate(""); + columns.Bound(x => x.StartDate); + columns.Bound(x => x.EndDate); + columns.Bound(x => x.LimitedToStores) + .Template(item => @Html.SymbolForBool(item.LimitedToStores)) + .ClientTemplate(@Html.SymbolForBool("LimitedToStores")) + .Hidden(Model.AvailableStores.Count <= 1) + .Centered(); + columns.Bound(x => x.CreatedOn); + }) + .Pageable(settings => settings.PageSize(gridPageSize).Position(GridPagerPosition.Both)) + .DataBinding(dataBinding => dataBinding.Ajax().Select("List", "News")) + .ClientEvents(events => events.OnDataBinding("onDataBinding").OnDataBound("onDataBound").OnRowDataBound("onRowDataBound")) + .PreserveGridState() + .EnableCustomBinding(true)) +
                                    + + - - - - - - - - - -
                                    - @Html.SmartLabelFor(model => model.LimitedToStores) - - @Html.EditorFor(model => model.LimitedToStores) - @Html.ValidationMessageFor(model => model.LimitedToStores) -
                                    - @Html.SmartLabelFor(model => model.AvailableStores) - - @if (Model.AvailableStores != null && Model.AvailableStores.Count > 0) - { - foreach (var store in Model.AvailableStores) - { - - } - } - else - { -
                                    @T("Admin.Configuration.Stores.NoStoresDefined")
                                    - }
                                    + @Html.Partial("StoreSelector", Model) } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/NewsLetterSubscription/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/NewsLetterSubscription/List.cshtml index 87f9e25b5e..5f1666018a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/NewsLetterSubscription/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/NewsLetterSubscription/List.cshtml @@ -11,87 +11,70 @@
                                    @{ Html.RenderWidget("admin_button_toolbar_before"); } - @{ Html.RenderWidget("admin_button_toolbar_after"); }
                                    - - - - - +
                                    +
                                    + @Html.SmartLabelFor(model => model.SearchEmail) + @Html.TextBoxFor(model => Model.SearchEmail, new { @class = "form-control" }) +
                                    @if (Model.AvailableStores.Count > 1) { -
                                    - - - +
                                    + @Html.SmartLabelFor(model => model.StoreId) + @Html.DropDownListFor(model => model.StoreId, Model.AvailableStores, T("Admin.Common.All"), new { @class = "form-control" }) +
                                    } - - - - -
                                    - @Html.SmartLabelFor(model => model.SearchEmail) - - @Html.EditorFor(model => Model.SearchEmail) -
                                    - @Html.SmartLabelFor(model => model.StoreId) - - @Html.DropDownList("StoreId", Model.AvailableStores, T("Admin.Common.All")) -
                                    -   - - -
                                    - -

                                    - - - - - -
                                    - @(Html.Telerik().Grid(Model.NewsLetterSubscriptions.Data) - .Name("newsLetterSubscriptions-grid") - .DataKeys(x => - { - x.Add(y => y.Id).RouteKey("Id"); - }) - .Columns(columns => - { - columns.Bound(x => x.Email) - .Width(500); - columns.Bound(x => x.Active) - .Template(item => @Html.SymbolForBool(item.Active)) - .ClientTemplate(@Html.SymbolForBool("Active")) - .Centered() - .Width(100); - columns.Bound(x => x.StoreName) - .Hidden(Model.AvailableStores.Count <= 1) - .ReadOnly(); - columns.Bound(x => x.CreatedOn) - .ReadOnly() - .Width(150); - columns.Command(commands => - { - commands.Edit().Localize(T); - commands.Delete().Localize(T); - }).Width(180); +
                                    + + +
                                    + - }) - .Pageable(settings => settings.Total(Model.NewsLetterSubscriptions.Total).PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax() - .Select("SubscriptionList", "NewsLetterSubscription") - .Update("SubscriptionUpdate", "NewsLetterSubscription") - .Delete("SubscriptionDelete", "NewsLetterSubscription")) - .ClientEvents(events => events.OnDataBinding("onDataBinding")) - .ClientEvents(x => x.OnError("grid_onError")) - .PreserveGridState() - .EnableCustomBinding(true)) -
                                    +
                                    + @(Html.Telerik().Grid(Model.NewsLetterSubscriptions.Data) + .Name("newsLetterSubscriptions-grid") + .DataKeys(x => + { + x.Add(y => y.Id).RouteKey("Id"); + }) + .Columns(columns => + { + columns.Bound(x => x.Email) + .Width(500); + columns.Bound(x => x.Active) + .Template(item => @Html.SymbolForBool(item.Active)) + .ClientTemplate(@Html.SymbolForBool("Active")) + .Centered() + .Width(100); + columns.Bound(x => x.StoreName) + .Hidden(Model.AvailableStores.Count <= 1) + .ReadOnly(); + columns.Bound(x => x.CreatedOn) + .ReadOnly() + .Width(150); + columns.Command(commands => + { + commands.Edit().Localize(T); + commands.Delete().Localize(T); + }) + .HtmlAttributes(new { align = "right", @class = "omega" }) + .Width(220); + }) + .Pageable(settings => settings.Total(Model.NewsLetterSubscriptions.Total).PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) + .DataBinding(dataBinding => dataBinding.Ajax() + .Select("SubscriptionList", "NewsLetterSubscription") + .Update("SubscriptionUpdate", "NewsLetterSubscription") + .Delete("SubscriptionDelete", "NewsLetterSubscription")) + .ClientEvents(events => events.OnDataBinding("onDataBinding")) + .ClientEvents(x => x.OnError("grid_onError")) + .PreserveGridState() + .EnableCustomBinding(true)) +
                                    - - @Html.Widget("order_edit_top") - - - - - - - - - - - - - - - - - - - @if (Model.AffiliateId != 0) - { - - - - - } - - - - - - - - - - - - @if (Model.RecurringPaymentId > 0) - { - - - - - } - @if (!String.IsNullOrEmpty(Model.VatNumber)) - { - - - - - } - @if (Model.AllowCustomersToSelectTaxDisplayType || Model.TaxDisplayType == TaxDisplayType.IncludingTax) - { - - - - - } - @if (Model.AllowCustomersToSelectTaxDisplayType || Model.TaxDisplayType == TaxDisplayType.ExcludingTax) - { - - - - - } - @if ((Model.AllowCustomersToSelectTaxDisplayType || Model.TaxDisplayType == TaxDisplayType.IncludingTax) && !String.IsNullOrEmpty(Model.OrderSubTotalDiscountInclTax)) - { - - - - - } - @if ((Model.AllowCustomersToSelectTaxDisplayType || Model.TaxDisplayType == TaxDisplayType.ExcludingTax) && !String.IsNullOrEmpty(Model.OrderSubTotalDiscountExclTax)) - { - - - - - } - @if (Model.AllowCustomersToSelectTaxDisplayType || Model.TaxDisplayType == TaxDisplayType.IncludingTax) - { - - - - - } - @if (Model.AllowCustomersToSelectTaxDisplayType || Model.TaxDisplayType == TaxDisplayType.ExcludingTax) - { - - - - - } - @if ((Model.AllowCustomersToSelectTaxDisplayType || Model.TaxDisplayType == TaxDisplayType.IncludingTax) && !String.IsNullOrEmpty(Model.PaymentMethodAdditionalFeeInclTax)) - { - - - - - } - @if ((Model.AllowCustomersToSelectTaxDisplayType || Model.TaxDisplayType == TaxDisplayType.ExcludingTax) && !String.IsNullOrEmpty(Model.PaymentMethodAdditionalFeeExclTax)) - { - - - - - } - @if (Model.DisplayTaxRates) - { - foreach (var tr in Model.TaxRates) - { - - - - - } - } - @if (Model.DisplayTax) - { - - - - - } - @if (!String.IsNullOrEmpty(Model.OrderTotalDiscount)) - { - - - - - } - @foreach (var gc in Model.GiftCards) - { - - - - - } - @if (Model.RedeemedRewardPoints > 0) - { - - - - - } - - - - - @if (!String.IsNullOrEmpty(Model.RefundedAmount)) - { - - - - - } - - - - - - - - - - @if (Model.AllowStoringCreditCardNumber) - { - - - - - } - @if (Model.AllowStoringCreditCardNumber) - { - - - - - } - @if (Model.AllowStoringCreditCardNumber || !String.IsNullOrEmpty(Model.CardNumber)) - { - - - - - } - @if (Model.AllowStoringCreditCardNumber) - { - - - - - } - @if (Model.AllowStoringCreditCardNumber) - { - - - - - } - @if (Model.AllowStoringCreditCardNumber) - { - - - - - } - @if (Model.AllowStoringCreditCardNumber) - { - - - - - - - } - @if (Model.AllowStoringDirectDebit) - { - - - - - } - @if (Model.AllowStoringDirectDebit) - { - - - - - } - @if (Model.AllowStoringDirectDebit) - { - - - - - } - @if (Model.AllowStoringDirectDebit) - { - - - - - } - @if (Model.AllowStoringDirectDebit) - { - - - - - } - @if (Model.AllowStoringDirectDebit) - { - - - - - } - @if (Model.AllowStoringDirectDebit) - { - - - - - } - @if (Model.AllowStoringDirectDebit) - { - - - - - - - } +@Html.DeleteConfirmation("order-delete") - @if (Model.DisplayCompletePaymentNote) - { - - - - } - @if (Model.DisplayPurchaseOrderNumber) - { - - - - - } - @if (!String.IsNullOrEmpty(Model.AuthorizationTransactionId)) - { - - - - - } - @if (!String.IsNullOrEmpty(Model.AuthorizationTransactionResult)) - { - - - - - } - @if (!String.IsNullOrEmpty(Model.CaptureTransactionId)) - { - - - - - } - @if (!String.IsNullOrEmpty(Model.CaptureTransactionResult)) - { - - - - - } - @if (!String.IsNullOrEmpty(Model.SubscriptionTransactionId)) - { - - - - - } - - - - - - - - - - - - - - - - - - - - - - - - - - -
                                    - @Html.SmartLabelFor(model => model.OrderStatus) - - @Model.OrderStatus -   - @if (Model.CanCancelOrder) - { - - } - - @if (Model.CanCompleteOrder) - { - - } -
                                    - @Html.SmartLabelFor(model => model.OrderNumber) - - @Model.OrderNumber -
                                    - @Html.SmartLabelFor(model => model.OrderGuid) - - @Model.OrderGuid -
                                    - @Html.SmartLabelFor(model => model.StoreName) - - @Model.StoreName -
                                    - @Html.SmartLabelFor(model => model.AffiliateId) - - @Model.AffiliateFullName -
                                    -
                                    -
                                    - @Html.SmartLabelFor(model => model.CustomerId) - - @Model.CustomerName -
                                    - @Html.SmartLabelFor(model => model.CustomerIp) - - @Model.CustomerIp -
                                    - @Html.SmartLabelFor(model => model.RecurringPaymentId) - - @T("Admin.Common.View") -
                                    - @Html.SmartLabelFor(model => model.VatNumber) - - @Model.VatNumber -
                                    - @Html.SmartLabelFor(model => model.OrderSubtotalInclTax) - - @Model.OrderSubtotalInclTax -
                                    - @Html.SmartLabelFor(model => model.OrderSubtotalExclTax) - - @Model.OrderSubtotalExclTax -
                                    - @Html.SmartLabelFor(model => model.OrderSubTotalDiscountInclTax) - - @Model.OrderSubTotalDiscountInclTax -
                                    - @Html.SmartLabelFor(model => model.OrderSubTotalDiscountExclTax) - - @Model.OrderSubTotalDiscountExclTax -
                                    - @Html.SmartLabelFor(model => model.OrderShippingInclTax) - - @Model.OrderShippingInclTax -
                                    - @Html.SmartLabelFor(model => model.OrderShippingExclTax) - - @Model.OrderShippingExclTax -
                                    - @Html.SmartLabelFor(model => model.PaymentMethodAdditionalFeeInclTax) - - @Model.PaymentMethodAdditionalFeeInclTax -
                                    - @Html.SmartLabelFor(model => model.PaymentMethodAdditionalFeeExclTax) - - @Model.PaymentMethodAdditionalFeeExclTax -
                                    - @Html.SmartLabelFor(model => model.Tax) - - @tr.Rate% - @tr.Value -
                                    - @Html.SmartLabelFor(model => model.Tax) - - @Model.Tax -
                                    - @Html.SmartLabelFor(model => model.OrderTotalDiscount) - - @Model.OrderTotalDiscount -
                                    - @Html.SmartLabelFor(model => model.GiftCards[0].CouponCode) (@(gc.CouponCode)) - - @gc.Amount -
                                    - @Html.SmartLabelFor(model => model.RedeemedRewardPoints) - - @Model.RedeemedRewardPoints @T("Admin.Orders.Fields.RedeemedRewardPoints.Points") - / - @Model.RedeemedRewardPointsAmount -
                                    - @Html.SmartLabelFor(model => model.OrderTotal) - - @Model.OrderTotal -
                                    - @Html.SmartLabelFor(model => model.RefundedAmount) - - @Model.RefundedAmount -
                                    - -
                                    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                                    - @Html.SmartLabelFor(model => model.OrderSubtotalInclTaxValue) - - @Html.EditorFor(model => model.OrderSubtotalInclTaxValue) - @T("Admin.Orders.Fields.Edit.InclTax") - @Html.EditorFor(model => model.OrderSubtotalExclTaxValue) - @T("Admin.Orders.Fields.Edit.ExclTax") -
                                    - @Html.SmartLabelFor(model => model.OrderSubTotalDiscountInclTaxValue) - - @Html.EditorFor(model => model.OrderSubTotalDiscountInclTaxValue) - @T("Admin.Orders.Fields.Edit.InclTax") - @Html.EditorFor(model => model.OrderSubTotalDiscountExclTaxValue) - @T("Admin.Orders.Fields.Edit.ExclTax") -
                                    - @Html.SmartLabelFor(model => model.OrderShippingInclTaxValue) - - @Html.EditorFor(model => model.OrderShippingInclTaxValue) - @T("Admin.Orders.Fields.Edit.InclTax") - @Html.EditorFor(model => model.OrderShippingExclTaxValue) - @T("Admin.Orders.Fields.Edit.ExclTax") -
                                    - @Html.SmartLabelFor(model => model.PaymentMethodAdditionalFeeInclTaxValue) - - @Html.EditorFor(model => model.PaymentMethodAdditionalFeeInclTaxValue) - @T("Admin.Orders.Fields.Edit.InclTax") - @Html.EditorFor(model => model.PaymentMethodAdditionalFeeExclTaxValue) - @T("Admin.Orders.Fields.Edit.ExclTax") -
                                    - @Html.SmartLabelFor(model => model.TaxRatesValue) - - @Html.EditorFor(model => model.TaxRatesValue) -
                                    - @Html.SmartLabelFor(model => model.TaxValue) - - @Html.EditorFor(model => model.TaxValue) -
                                    - @Html.SmartLabelFor(model => model.OrderTotalDiscountValue) - - @Html.EditorFor(model => model.OrderTotalDiscountValue) -
                                    - @Html.SmartLabelFor(model => model.OrderTotalValue) - - @Html.EditorFor(model => model.OrderTotalValue) -
                                    -
                                    - -
                                    - - - -
                                    -
                                    -
                                    - @Html.SmartLabelFor(model => model.CardType) - - @Model.CardType - @Html.EditorFor(model => model.CardType) -
                                    - @Html.SmartLabelFor(model => model.CardName) - - @Model.CardName - @Html.EditorFor(model => model.CardName) -
                                    - @Html.SmartLabelFor(model => model.CardNumber) - - @Model.CardNumber - @Html.EditorFor(model => model.CardNumber) -
                                    - @Html.SmartLabelFor(model => model.CardCvv2) - - @Model.CardCvv2 - @Html.EditorFor(model => model.CardCvv2) -
                                    - @Html.SmartLabelFor(model => model.CardExpirationMonth) - - @Model.CardExpirationMonth - @Html.EditorFor(model => model.CardExpirationMonth) -
                                    - @Html.SmartLabelFor(model => model.CardExpirationYear) - - @Model.CardExpirationYear - @Html.EditorFor(model => model.CardExpirationYear) -
                                    - - - -
                                    -
                                    -
                                    - @Html.SmartLabelFor(model => model.DirectDebitAccountHolder) - - @Model.DirectDebitAccountHolder - @Html.EditorFor(model => model.DirectDebitAccountHolder) -
                                    - @Html.SmartLabelFor(model => model.DirectDebitAccountNumber) - - @Model.DirectDebitAccountNumber - @Html.EditorFor(model => model.DirectDebitAccountNumber) -
                                    - @Html.SmartLabelFor(model => model.DirectDebitBankCode) - - @Model.DirectDebitBankCode - @Html.EditorFor(model => model.DirectDebitBankCode) -
                                    - @Html.SmartLabelFor(model => model.DirectDebitBankName) - - @Model.DirectDebitBankName - @Html.EditorFor(model => model.DirectDebitBankName) -
                                    - @Html.SmartLabelFor(model => model.DirectDebitBIC) - - @Model.DirectDebitBIC - @Html.EditorFor(model => model.DirectDebitBIC) -
                                    - @Html.SmartLabelFor(model => model.DirectDebitCountry) - - @Model.DirectDebitCountry - @Html.EditorFor(model => model.DirectDebitCountry) -
                                    - @Html.SmartLabelFor(model => model.DirectDebitIban) - - @Model.DirectDebitIban - @Html.EditorFor(model => model.DirectDebitIban) -
                                    - - - -
                                    -
                                    -
                                    -
                                    - @Html.Raw(T("Order.CompletePayment.AdminNote", Url.Action("Details", "Order", new { id = Model.Id, area = "" }))) -
                                    -
                                    - @Html.SmartLabelFor(model => model.PurchaseOrderNumber) - - @Model.PurchaseOrderNumber -
                                    - @Html.SmartLabelFor(model => model.AuthorizationTransactionId) - - @Model.AuthorizationTransactionId -
                                    - @Html.SmartLabelFor(model => model.AuthorizationTransactionResult) - - @Model.AuthorizationTransactionResult -
                                    - @Html.SmartLabelFor(model => model.CaptureTransactionId) - - @Model.CaptureTransactionId -
                                    - @Html.SmartLabelFor(model => model.CaptureTransactionResult) - - @Model.CaptureTransactionResult -
                                    - @Html.SmartLabelFor(model => model.SubscriptionTransactionId) - - @Model.SubscriptionTransactionId -
                                    - @Html.SmartLabelFor(model => model.PaymentMethod) - - @Model.PaymentMethod - @if (Model.PaymentMethodSystemName.HasValue()) - { - (@Model.PaymentMethodSystemName) - } -
                                    - @Html.SmartLabelFor(model => model.PaymentStatus) - - @Model.PaymentStatus -   - @if (Model.CanMarkOrderAsPaid) - { - -    - } - @if (Model.CanCapture) - { - - } - @if (Model.CanRefund) - { - - } - @if (Model.CanRefundOffline) - { - - } - @if (Model.CanPartiallyRefund) - { - - } - @if (Model.CanPartiallyRefundOffline) - { - - } - @if (Model.CanVoid) - { - - } - @if (Model.CanVoidOffline) - { - - } - -
                                    - @Html.SmartLabelFor(model => model.CreatedOn) - - @Html.DisplayFor(model => model.CreatedOn) -
                                    - @Html.SmartLabelFor(model => model.UpdatedOn) - - @Html.DisplayFor(model => model.UpdatedOn) -
                                    -
                                    -
                                    - @Html.SmartLabelFor(model => model.AcceptThirdPartyEmailHandOver) - - @(Model.AcceptThirdPartyEmailHandOver ? T("Common.Yes") : T("Common.No")) -
                                    - @if (Model.CustomerComment.HasValue()) - { -

                                    @T("Admin.Order.CustomerComment.Heading")

                                    -
                                    - @Model.CustomerComment -
                                    - } -
                                    -} -@helper TabBillingInfo() -{ - - - - - -
                                    - @Html.SmartLabelFor(model => model.BillingAddress) - -
                                    - - @if (Model.BillingAddress.FirstNameEnabled || Model.BillingAddress.LastNameEnabled) - { - - - - - } - @if (Model.BillingAddress.EmailEnabled) - { - - - - - } - @if (Model.BillingAddress.PhoneEnabled) - { - - - - - } - @if (Model.BillingAddress.FaxEnabled) - { - - - - - } - @if (Model.BillingAddress.CompanyEnabled) - { - - - - - } - @if (Model.BillingAddress.StreetAddressEnabled) - { - - - - - } - @if (Model.BillingAddress.StreetAddress2Enabled) - { - - - - - } - @if (Model.BillingAddress.CityEnabled) - { - - - - - } - @if (Model.BillingAddress.StateProvinceEnabled) - { - - - - - } - @if (Model.BillingAddress.ZipPostalCodeEnabled) - { - - - - - } - @if (Model.BillingAddress.CountryEnabled) - { - - - - - } - - - -
                                    - @T("Admin.Orders.Address.FullName") - - @Model.BillingAddress.FirstName @Model.BillingAddress.LastName -
                                    - @T("Admin.Orders.Address.Email") - - @Model.BillingAddress.Email -
                                    - @T("Admin.Orders.Address.Phone") - - @Model.BillingAddress.PhoneNumber -
                                    - @T("Admin.Orders.Address.Fax") - - @Model.BillingAddress.FaxNumber -
                                    - @T("Admin.Orders.Address.Company") - - @Model.BillingAddress.Company -
                                    - @T("Admin.Orders.Address.Address1") - - @Model.BillingAddress.Address1 -
                                    - @T("Admin.Orders.Address.Address2") - - @Model.BillingAddress.Address2 -
                                    - @T("Admin.Orders.Address.City") - - @Model.BillingAddress.City -
                                    - @T("Admin.Orders.Address.StateProvince") - - @Model.BillingAddress.StateProvinceName -
                                    - @T("Admin.Orders.Address.ZipPostalCode") - - @Model.BillingAddress.ZipPostalCode -
                                    - @T("Admin.Orders.Address.Country") - - @Model.BillingAddress.CountryName -
                                    -  @T("Admin.Common.Edit") -
                                    -
                                    -
                                    -} -@helper TabShippingInfo() +@helper TabInfo() { - if (Model.IsShippable && Model.ShippingAddress != null) - { - - - - - - - - - - - - - -
                                    - @Html.SmartLabelFor(model => model.ShippingAddress) - -
                                    - - @if (Model.ShippingAddress.FirstNameEnabled || Model.ShippingAddress.LastNameEnabled) - { - - - - - } - @if (Model.ShippingAddress.EmailEnabled) - { - - - - - } - @if (Model.ShippingAddress.PhoneEnabled) - { - - - - - } - @if (Model.ShippingAddress.FaxEnabled) - { - - - - - } - @if (Model.ShippingAddress.CompanyEnabled) - { - - - - - } - @if (Model.ShippingAddress.StreetAddressEnabled) - { - - - - - } - @if (Model.ShippingAddress.StreetAddress2Enabled) - { - - - - - } - @if (Model.ShippingAddress.CityEnabled) - { - - - - - } - @if (Model.ShippingAddress.StateProvinceEnabled) - { - - - - - } - @if (Model.ShippingAddress.ZipPostalCodeEnabled) - { - - - - - } - @if (Model.ShippingAddress.CountryEnabled) - { - - - - - } - - - -
                                    - @T("Admin.Orders.Address.FullName") - - @Model.ShippingAddress.FirstName @Model.ShippingAddress.LastName -
                                    - @T("Admin.Orders.Address.Email"): - - @Model.ShippingAddress.Email -
                                    - @T("Admin.Orders.Address.Phone") - - @Model.ShippingAddress.PhoneNumber -
                                    - @T("Admin.Orders.Address.Fax") - - @Model.ShippingAddress.FaxNumber -
                                    - @T("Admin.Orders.Address.Company") - - @Model.ShippingAddress.Company -
                                    - @T("Admin.Orders.Address.Address1") - - @Model.ShippingAddress.Address1 -
                                    - @T("Admin.Orders.Address.Address2") - - @Model.ShippingAddress.Address2 -
                                    - @T("Admin.Orders.Address.City") - - @Model.ShippingAddress.City -
                                    - @T("Admin.Orders.Address.StateProvince") - - @Model.ShippingAddress.StateProvinceName -
                                    - @T("Admin.Orders.Address.ZipPostalCode") - - @Model.ShippingAddress.ZipPostalCode -
                                    - @T("Admin.Orders.Address.Country") - - @Model.ShippingAddress.CountryName -
                                    -  @T("Admin.Common.Edit") -
                                    -
                                    - -
                                    - @Html.SmartLabelFor(model => model.ShippingMethod) - - @Model.ShippingMethod -
                                    - @Html.SmartLabelFor(model => model.ShippingStatus) - - @Model.ShippingStatus -
                                    - -
                                    -
                                    - -

                                    @T("Admin.Orders.Shipments")

                                    - -
                                    -
                                    - - - - - @if (Model.CanAddNewShipments) - { - - - - } -
                                    - @(Html.Telerik().Grid().Name("shipments-grid") - .DataBinding(binding => - { - binding.Ajax().Select("ShipmentsSelect", "Order", new { orderId = Model.Id }); - }) - .Columns(columns => - { - columns.Bound(x => x.Id) - .Template(x => Html.ActionLink(x.Id.ToString(), "ShipmentDetails", "Order", new { id = x.Id }, new { })) - .ClientTemplate("\"><#= Id #>"); - columns.Bound(x => x.TrackingNumber); - columns.Bound(x => x.TotalWeight); - columns.Bound(x => x.ShippedDate); - columns.Bound(x => x.DeliveryDate); - }) - .EnableCustomBinding(true)) - - @if (Model.CanAddNewShipments) - { -
                                    - -
                                    - } -
                                    - -
                                    - } - else - { - - - - - -
                                    - - @T("Admin.Orders.ShippingInfo.NotRequired") -
                                    - } + @Html.Partial("_Edit.Info") } -@helper BundleItemsInfo(OrderModel.OrderItemModel parentItem) +@helper TabBillingAndShipment() { - if (parentItem.BundleItems != null) - { -
                                    - @foreach (var item in parentItem.BundleItems.OrderBy(x => x.DisplayOrder)) - { -
                                    -
                                    - @Html.ActionLink(item.ProductName, "Edit", "Product", new { id = item.ProductId }, new { }) - @if (item.Quantity > 1) - { - - × @item.Quantity - - } -
                                    - @if (!String.IsNullOrWhiteSpace(item.PriceWithDiscount)) - { -
                                    - @Html.Raw(item.PriceWithDiscount) -
                                    - } - @if (!String.IsNullOrEmpty(item.AttributeInfo)) - { -
                                    - @Html.Raw(item.AttributeInfo) -
                                    - } -
                                    - } -
                                    - } + @Html.Partial("_Edit.BillingAndShipment") } @helper TabProducts() { - - - if (Model.AutoUpdateOrderItemInfo.HasValue()) - { -
                                    - - @Html.Raw(Model.AutoUpdateOrderItemInfo) -
                                    - } - - - - - - @if (!String.IsNullOrEmpty(Model.CheckoutAttributeInfo)) - { - - - - } - - - -
                                    - - - - - - @if (Model.HasDownloadableProducts) - { - - } - - - - - - - - - @foreach (var item in Model.Items) - { - - - @if (Model.HasDownloadableProducts) - { - - } - - - - - - - } - -
                                    - @T("Admin.Orders.Products.ProductName") - - @T("Admin.Orders.Products.Download") - - @T("Admin.Orders.Products.Price") - - @T("Admin.Orders.Products.Quantity") - - @T("Admin.Orders.Products.Discount") - - @T("Admin.Orders.Products.Total") - - @T("Admin.Common.Edit") -
                                    -
                                    - @Html.LabeledProductName(item.ProductId, item.ProductName, item.ProductTypeName, item.ProductTypeLabelHint) - - @if (!String.IsNullOrEmpty(item.AttributeInfo)) - { -
                                    - @Html.Raw(item.AttributeInfo) - } - @if (!String.IsNullOrEmpty(item.RecurringInfo)) - { -
                                    - @Html.Raw(item.RecurringInfo) - } - @if (!String.IsNullOrEmpty(item.Sku)) - { -
                                    - @T("Admin.Orders.Products.SKU"): - @item.Sku - } - @if (item.ProductType == ProductType.BundledProduct) - { -
                                    - @BundleItemsInfo(item) - } - @if (item.ReturnRequests.Count > 0) - { -
                                    -
                                    - for (int i = 0; i < item.ReturnRequests.Count; ++i) - { - var returnRequest = item.ReturnRequests[i]; - - @returnRequest.StatusString - - - @T("Admin.Orders.Products.ReturnRequest") @returnRequest.Id - , - - @T("Admin.ReturnRequests.Fields.Quantity") @returnRequest.Quantity - if (i != item.ReturnRequests.Count - 1) - { -
                                    - } - } - } - @if (item.PurchasedGiftCardIds.Count > 0) - { -
                                    -
                                    - @T("Admin.Orders.Products.GiftCards"): - for (int i = 0; i < item.PurchasedGiftCardIds.Count; i++) - { - @item.PurchasedGiftCardIds[i] - if (i != item.PurchasedGiftCardIds.Count - 1) - { - , - } - } - } -
                                    -
                                    - @if (item.IsDownload) - { -
                                    - @string.Format(T("Admin.Orders.Products.Download.DownloadCount").Text, item.DownloadCount) - -
                                    -
                                    - if (item.DownloadActivationType == DownloadActivationType.Manually) - { -
                                    - -
                                    -
                                    - } -
                                    - - @T("Admin.Orders.Products.License") - - - @if (item.LicenseDownloadId.HasValue && item.LicenseDownloadId.Value > 0) - { - @T("Admin.Orders.Products.License.DownloadLicense") -
                                    - } - -
                                    - } -
                                    - @if (Model.AllowCustomersToSelectTaxDisplayType) - { - @item.UnitPriceInclTax -
                                    - @item.UnitPriceExclTax - } - else - { - switch (Model.TaxDisplayType) - { - case TaxDisplayType.ExcludingTax: - { - @item.UnitPriceExclTax - } - break; - case TaxDisplayType.IncludingTax: - { - @item.UnitPriceInclTax - } - break; - default: - break; - } - } - -
                                    - - - - - - - - - - - - - -
                                    - @T("Admin.Orders.Products.Edit.InclTax") - - -
                                    - @T("Admin.Orders.Products.Edit.ExclTax") - - -
                                    - @T("Admin.Orders.Products.AddNew.TaxRate") - - -
                                    -
                                    -
                                    - @item.Quantity - -
                                    - - - - -
                                    - -
                                    -
                                    -
                                    - @if (Model.AllowCustomersToSelectTaxDisplayType) - { - @item.DiscountInclTax -
                                    - @item.DiscountExclTax - } - else - { - switch (Model.TaxDisplayType) - { - case TaxDisplayType.ExcludingTax: - { - @item.DiscountExclTax - } - break; - case TaxDisplayType.IncludingTax: - { - @item.DiscountInclTax - } - break; - default: - break; - } - } -
                                    -
                                    -
                                    - - - - - - - - - -
                                    - @T("Admin.Orders.Products.Edit.InclTax") - - -
                                    - @T("Admin.Orders.Products.Edit.ExclTax") - - -
                                    -
                                    -
                                    - @if (Model.AllowCustomersToSelectTaxDisplayType) - { - @item.SubTotalInclTax -
                                    - @item.SubTotalExclTax - } - else - { - switch (Model.TaxDisplayType) - { - case TaxDisplayType.ExcludingTax: - { - @item.SubTotalExclTax - } - break; - case TaxDisplayType.IncludingTax: - { - @item.SubTotalInclTax - } - break; - default: - break; - } - } -
                                    -
                                    -
                                    - - - - - - - - - -
                                    - @T("Admin.Orders.Products.Edit.InclTax") - - -
                                    - @T("Admin.Orders.Products.Edit.ExclTax") - - -
                                    -
                                    -
                                    - - @if (item.IsReturnRequestPossible) - { - - } - - - - - - - - - - -
                                    -
                                    -
                                    - @Html.Raw(Model.CheckoutAttributeInfo) -
                                    -
                                    - -
                                    + @Html.Partial("_Edit.Products") } + @helper TabOrderNotes() { - - - - -
                                    - @(Html.Telerik().Grid().Name("ordernotes-grid") - .DataKeys(keys => - { - keys.Add(x => x.Id).RouteKey("orderNoteId"); - keys.Add(x => x.OrderId).RouteKey("orderId"); - }) - .DataBinding(binding => - { - binding.Ajax().Select("OrderNotesSelect", "Order", new { orderId = Model.Id }) - .Delete("OrderNoteDelete", "Order"); - }) - .Columns(columns => - { - columns.Bound(x => x.CreatedOn).Width(140); - columns.Bound(x => x.Note).Encoded(false); - columns.Bound(x => x.DisplayToCustomer) - .Template(item => @Html.SymbolForBool(item.DisplayToCustomer)) - .ClientTemplate(@Html.SymbolForBool("DisplayToCustomer")) - .Centered() - .Width(140); - columns.Command(commands => - { - commands.Delete().Localize(T); - }).Width(140).Title(T("Admin.Common.Delete").Text); - }) - .EnableCustomBinding(true)) -
                                    - - - - - - - - - - - - - - - - - -
                                    -
                                    -
                                    @T("Admin.Orders.OrderNotes.AddTitle")
                                    -
                                    -
                                    - @Html.SmartLabelFor(model => model.AddOrderNoteMessage) - - @Html.TextAreaFor(model => model.AddOrderNoteMessage, new { @class = "control-xlarge", style = "height: 120px;" }) - @Html.ValidationMessageFor(model => model.AddOrderNoteMessage) -
                                    - @Html.SmartLabelFor(model => model.AddOrderNoteDisplayToCustomer) - - @Html.EditorFor(model => model.AddOrderNoteDisplayToCustomer) - @Html.ValidationMessageFor(model => model.AddOrderNoteDisplayToCustomer) -
                                    -   - - -
                                    - - + @Html.Partial("_Edit.OrderNotes") } @helper TabOrderAttributes() diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/List.cshtml index 186ac46110..e5137c9928 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/List.cshtml @@ -14,213 +14,162 @@ @T("Admin.Orders") -
                                    + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - +
                                    + @Html.SmartLabelFor(model => model.StoreId) + @Html.DropDownListFor(model => model.StoreId, Model.AvailableStores, allString, new { @class = "form-control" }) +
                                    + } + else + { +
                                    } - - - - - - - - - - - - - - - - -
                                    - @Html.SmartLabelFor(model => model.StartDate) - - @Html.EditorFor(model => model.StartDate) -
                                    - @Html.SmartLabelFor(model => model.EndDate) - - @Html.EditorFor(model => Model.EndDate) -
                                    - @Html.SmartLabelFor(model => model.CustomerName) - - @Html.EditorFor(model => Model.CustomerName) -
                                    - @Html.SmartLabelFor(model => model.CustomerEmail) - - @Html.EditorFor(model => Model.CustomerEmail) -
                                    - @Html.SmartLabelFor(model => model.OrderStatusIds) - - @Html.DropDownList("OrderStatusIds", Model.AvailableOrderStatuses, null, new { multiple = "multiple" }) -
                                    - @Html.SmartLabelFor(model => model.PaymentStatusIds) - - @Html.DropDownList("PaymentStatusIds", Model.AvailablePaymentStatuses, null, new { multiple = "multiple" }) -
                                    - @Html.SmartLabelFor(model => model.ShippingStatusIds) - - @Html.DropDownList("ShippingStatusIds", Model.AvailableShippingStatuses, null, new { multiple = "multiple" }) -
                                    - @Html.SmartLabelFor(model => model.StoreId) - - @Html.DropDownListFor(model => model.StoreId, Model.AvailableStores, allString) -
                                    - @Html.SmartLabelFor(model => model.OrderGuid) - - @Html.EditorFor(model => Model.OrderGuid) -
                                    - @Html.SmartLabelFor(model => model.OrderNumber) - - @Html.EditorFor(model => Model.OrderNumber) -
                                    - @Html.SmartLabelFor(model => model.GoDirectlyToNumber) - - @Html.EditorFor(model => Model.GoDirectlyToNumber) - -
                                    -   - - -
                                    +
                                    +
                                    + @Html.SmartLabelFor(model => model.OrderStatusIds) + @Html.DropDownList("OrderStatusIds", Model.AvailableOrderStatuses, null, new { multiple = "multiple", @class = "form-control" }) +
                                    +
                                    + @Html.SmartLabelFor(model => model.PaymentStatusIds) + @Html.DropDownList("PaymentStatusIds", Model.AvailablePaymentStatuses, null, new { multiple = "multiple", @class = "form-control" }) +
                                    -

                                    +
                                    + + +
                                    +
                                    - - - - -
                                    - @(Html.Telerik().Grid() - .Name("orders-grid") - .TableHtmlAttributes(new { @class = "multiline-grid" }) - .ClientEvents(events => events - .OnDataBinding("onDataBinding") - .OnDataBound("onDataBound") - .OnComplete("onComplete")) - .Columns(columns => - { - columns.Bound(x => x.Id) - .ClientTemplate("") - .Title("") - .Width(50) - .HtmlAttributes(new { style = "text-align:center" }) - .HeaderHtmlAttributes(new { style = "text-align:center" }); - @*TODO: number of products, product summary?*@ - columns.Bound(x => x.OrderNumber) - .Width(150) - .Title(T("Admin.Order")) - .ClientTemplate(@Html.LabeledOrderNumber()); - columns.Bound(x => x.CustomerName) - //.Width(300) - .Title(T("Admin.Orders.Fields.Customer")) - .ClientTemplate( - "
                                    <#= CustomerName #>
                                    " + - "
                                    <#= CustomerEmail #>
                                    "); - columns.Bound(x => x.ShippingStatus) - .Title(T("Admin.Orders.Shipment")) - .ClientTemplate( - "
                                    " + - "<# if(IsShippable){ #><#= ShippingAddressString #><# } #>" + - "<# if(IsShippable){ #>
                                    <#= ViaShippingMethod #>
                                    <# } #>" + - "<# if(!IsShippable){ #><#= ShippingStatus #><# } #>" + - "
                                    "); - columns.Bound(x => x.CreatedOn) - .Width(200) - .Title(T("Order.OrderDate")) - .ClientTemplate( - "
                                    <#= CreatedOnString #>
                                    " + - "
                                    <#= FromStore #>
                                    "); - columns.Bound(x => x.OrderStatus) - .Width(100) - .Title(T("Admin.Orders.Fields.OrderStatus")) - .ClientTemplate("<#= OrderStatus #>"); - columns.Bound(x => x.OrderTotal) - .Width(200) - .RightAlign() - .ClientTemplate( - "
                                    <#= OrderTotal #>
                                    " + - "<# if(HasPaymentMethod){ #>
                                    <#= WithPaymentMethod #>
                                    <# } #>") - .FooterTemplate( - string.Format("
                                    {0}:
                                    " + - (hideProfitReport ? "{1}" : "
                                    {1}:
                                    ") + - "
                                    {2}:
                                    " + - "
                                    {3}:
                                    ", - @T("Admin.Orders.Report.Summary").Text, - (hideProfitReport ? "" : @T("Admin.Orders.Report.Profit").Text), - @T("Admin.Orders.Report.Tax").Text, - @T("Admin.Orders.Report.Total").Text)); - }) - .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("OrderList", "Order")) - .PreserveGridState() - .EnableCustomBinding(true)) -
                                    - +
                                    + @(Html.Telerik().Grid() + .Name("orders-grid") + .TableHtmlAttributes(new { @class = "multiline-grid" }) + .ClientEvents(events => events + .OnDataBinding("onDataBinding") + .OnDataBound("onDataBound") + .OnComplete("onComplete")) + .Columns(columns => + { + columns.Bound(x => x.Id) + .ClientTemplate("") + .Title("") + .Width(50) + .HtmlAttributes(new { style = "text-align:center" }) + .HeaderHtmlAttributes(new { style = "text-align:center" }); + @*TODO: number of products, product summary?*@ + columns.Bound(x => x.OrderNumber) + .Width(150) + .Title(T("Admin.Order")) + .ClientTemplate(@Html.LabeledOrderNumber()); + columns.Bound(x => x.CustomerName) + //.Width(300) + .Title(T("Admin.Orders.Fields.Customer")) + .ClientTemplate( + "
                                    <#= CustomerName #>
                                    " + + "
                                    <#= CustomerEmail #>
                                    "); + columns.Bound(x => x.ShippingStatus) + .Title(T("Admin.Orders.Shipment")) + .ClientTemplate( + "
                                    " + + "<# if(IsShippable){ #><#= ShippingAddressString #><# } #>" + + "<# if(IsShippable){ #>
                                    <#= ViaShippingMethod #>
                                    <# } #>" + + "<# if(!IsShippable){ #><#= ShippingStatus #><# } #>" + + "
                                    "); + columns.Bound(x => x.CreatedOn) + .Width(200) + .Title(T("Order.OrderDate")) + .ClientTemplate( + "
                                    <#= CreatedOnString #>
                                    " + + "
                                    <#= FromStore #>
                                    "); + columns.Bound(x => x.OrderStatus) + .Width(100) + .Title(T("Admin.Orders.Fields.OrderStatus")) + .ClientTemplate("<#= OrderStatus #>"); + columns.Bound(x => x.OrderTotal) + .Width(200) + .RightAlign() + .ClientTemplate( + "
                                    <#= OrderTotal #>
                                    " + + "<# if(HasPaymentMethod){ #>
                                    <#= WithPaymentMethod #>
                                    <# } #>") + .FooterTemplate( + string.Format("
                                    {0}:
                                    " + + (hideProfitReport ? "{1}" : "
                                    {1}:
                                    ") + + "
                                    {2}:
                                    " + + "
                                    {3}:
                                    ", + @T("Admin.Orders.Report.Summary").Text, + (hideProfitReport ? "" : @T("Admin.Orders.Report.Profit").Text), + @T("Admin.Orders.Report.Tax").Text, + @T("Admin.Orders.Report.Total").Text)); + }) + .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) + .DataBinding(dataBinding => dataBinding.Ajax().Select("OrderList", "Order")) + .PreserveGridState() + .EnableCustomBinding(true)) +
                                    + - } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/OrderAverageReport.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/OrderAverageReport.cshtml index 2f804ca6bc..700351cd97 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/OrderAverageReport.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/OrderAverageReport.cshtml @@ -1,53 +1,42 @@ @model IList -@using Telerik.Web.Mvc; -@using Telerik.Web.Mvc.UI -
                                    -
                                    +
                                    +
                                    +

                                    @T("Admin.SalesReport.Average.SumTodayOrders")

                                    +
                                    -
                                    - @T("Admin.SalesReport.Average.SumTodayOrders") -
                                    - - @{ - var list = Model.Reverse().ToList(); - var length = Model.Count; - } - @for (int i = 0; i < list.Count(); ++i ) - { - var x = list[i]; -
                                    -
                                    @x.OrderStatus
                                    -
                                    @x.SumTodayOrders
                                    -
                                    - } - -
                                    + @for (int i = 0; i < Model.Count; ++i) + { + var x = Model[i]; +
                                    +
                                    @x.OrderStatus
                                    +
                                    @x.SumTodayOrders
                                    +
                                    + }
                                    -
                                    - -
                                    - -

                                    @T("Admin.SalesReport.Average")

                                    -
                                    - -
                                    - - - - - - - - - - - - @foreach (var x in Model) - { - - + + + + + } + +
                                    @T("Admin.SalesReport.Average.OrderStatus")@T("Admin.SalesReport.Average.SumThisWeekOrders")@T("Admin.SalesReport.Average.SumThisMonthOrders")@T("Admin.SalesReport.Average.SumThisYearOrders")@T("Admin.SalesReport.Average.SumAllTimeOrders")
                                    +
                                    +
                                    + + @T("Admin.SalesReport.Average") +
                                    + +
                                    + + + + + + + + + + + + @foreach (var x in Model) + { + + - - - - - - } - -
                                    @T("Admin.SalesReport.Average.OrderStatus")@T("Admin.SalesReport.Average.SumThisWeekOrders")@T("Admin.SalesReport.Average.SumThisMonthOrders")@T("Admin.SalesReport.Average.SumThisYearOrders")@T("Admin.SalesReport.Average.SumAllTimeOrders")
                                    @if (x.Url.HasValue()) { @x.OrderStatus @@ -57,15 +46,14 @@ @x.OrderStatus } @x.SumThisWeekOrders@x.SumThisMonthOrders@x.SumThisYearOrders@x.SumAllTimeOrders
                                    -
                                    - +
                                    @x.SumThisWeekOrders@x.SumThisMonthOrders@x.SumThisYearOrders@x.SumAllTimeOrders
                                    +
                                    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/OrderIncompleteReport.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/OrderIncompleteReport.cshtml index f248bbb257..afd9d45032 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/OrderIncompleteReport.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/OrderIncompleteReport.cshtml @@ -1,16 +1,13 @@ @model IList -@using Telerik.Web.Mvc; -@using Telerik.Web.Mvc.UI -
                                    - -
                                    - -

                                    @T("Admin.SalesReport.Incomplete")

                                    -
                                    - -
                                    - +
                                    +
                                    + + @T("Admin.SalesReport.Incomplete") +
                                    + +
                                    +
                                    @@ -44,8 +41,5 @@
                                    @T("Admin.SalesReport.Incomplete.Item")
                                    - - -
                                    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/PartiallyRefundOrderPopup.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/PartiallyRefundOrderPopup.cshtml index 74ecb81688..6eb5b973cc 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/PartiallyRefundOrderPopup.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/PartiallyRefundOrderPopup.cshtml @@ -1,8 +1,6 @@ -@{ +@model OrderModel +@{ Layout = "_AdminPopupLayout"; -} -@model OrderModel -@{ ViewBag.Title = T("Admin.Orders.Fields.PartialRefund").Text; } @@ -10,36 +8,33 @@ {
                                    - @T("Admin.Orders.Fields.PartialRefund") + @string.Format(T("Admin.Orders.Fields.PartialRefund.OrderInfo").Text, Model.Id)
                                    @Html.ValidationSummary(false) - -

                                    - @string.Format(T("Admin.Orders.Fields.PartialRefund.OrderInfo").Text, Model.Id) -

                                    - - + +
                                    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentDetails.Print.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentDetails.Print.cshtml index ffa5d77fa0..025d84801b 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentDetails.Print.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentDetails.Print.cshtml @@ -1,9 +1,5 @@ @model IEnumerable -@using SmartStore.Core.Domain.Orders; -@using SmartStore.Core.Domain.Catalog; -@using SmartStore.Admin.Models.Shipping; -@using SmartStore.Services.Localization; -@using SmartStore.Core.Html; + @{ Layout = "_Print"; Html.AddTitleParts(T("Admin.Orders.Shipments.ViewDetails").Text); @@ -48,34 +44,7 @@
                                    - @if (address.Company.HasValue()) - { -
                                    @address.Company
                                    - } -
                                    @JoinValues(new string[] { address.Salutation, address.Title, address.FirstName, address.LastName })
                                    - @if (address.Address1.HasValue()) - { -
                                    @address.Address1
                                    - } - @if (address.Address2.HasValue()) - { -
                                    @address.Address2
                                    - } - @if (address.ZipPostalCode.HasValue() || address.City.HasValue()) - { -
                                    @JoinValues(new string[] { address.ZipPostalCode, address.City })
                                    - } - @if (address.Country != null) - { - if (address.StateProvince != null) - { -
                                    @JoinValues(new string[] { address.Country.Name, address.StateProvince.Name }, ", ")
                                    - } - else - { -
                                    @JoinValues(new string[] { address.Country.Name }, ", ")
                                    - } - } + @Html.Raw(shipment.FormattedShippingAddress)
                                    @@ -118,29 +87,30 @@

                                    @T("PDFPackagingSlip.ProductListHeadline")

                                    -
                                    @Html.SmartLabelFor(model => model.AmountToRefund) - @Html.EditorFor(model => model.AmountToRefund)     - @string.Format(@T("Admin.Orders.Fields.PartialRefund.AmountToRefund.Max").Text, Model.MaxAmountToRefundFormatted, "") + @Html.EditorFor(model => model.AmountToRefund) +
                                    + @string.Format(@T("Admin.Orders.Fields.PartialRefund.AmountToRefund.Max").Text, Model.MaxAmountToRefundFormatted, "") +
                                    -   -
                                    - - - - - @if (showGtin) - { - - } - - - - - - @for (int i = 0; i < shipment.Items.Count; i++) - { - var item = shipment.Items[i]; - @ShipmentLine(item, showGtin) - } - -
                                    @T("PDFPackagingSlip.ProductName")@T("PDFPackagingSlip.SKU")@T("PDFPackagingSlip.Gtin")@T("PDFPackagingSlip.QTY")@T("PDFPackagingSlip.Weight")
                                    +
                                    + + + + + + @if (showGtin) + { + + } + + + + + + @for (int i = 0; i < shipment.Items.Count; i++) + { + var item = shipment.Items[i]; + @ShipmentLine(item, showGtin) + } + +
                                    @T("PDFPackagingSlip.ProductName")@T("PDFPackagingSlip.SKU")@T("PDFPackagingSlip.Gtin")@T("PDFPackagingSlip.QTY")@T("PDFPackagingSlip.Weight")
                                    +
                                    - } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentDetails.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentDetails.cshtml index d04d7c2880..8572c9fceb 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentDetails.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentDetails.cshtml @@ -1,7 +1,5 @@ @model ShipmentModel @using Telerik.Web.Mvc.UI; -@using SmartStore.Core.Domain.Tax; -@using SmartStore.Core.Domain.Catalog; @{ ViewBag.Title = T("Admin.Orders.Shipments.ViewDetails").Text; } @@ -14,12 +12,15 @@
                                    @if (Model.DisplayPdfPackagingSlip) { - + - @T("Admin.Orders.Shipments.PrintPackagingSlip") + @T("Admin.Orders.Shipments.PrintPackagingSlip") } - +
                                    @@ -31,11 +32,15 @@ @Html.SmartLabelFor(model => model.TrackingNumber) - @Html.EditorFor(model => model.TrackingNumber) - - +
                                    + @Html.EditorFor(model => model.TrackingNumber) + + + +
                                    @@ -43,7 +48,9 @@ @Html.SmartLabelFor(model => model.TotalWeight) - @Model.TotalWeight +
                                    + @Model.TotalWeight +
                                    @@ -51,14 +58,16 @@ @Html.SmartLabelFor(model => model.ShippedDate) - @Model.ShippedDate - @if (Model.CanShip) - { -   - - } +
                                    + @Model.ShippedDate + @if (Model.CanShip) + { + + } +
                                    @@ -66,68 +75,64 @@ @Html.SmartLabelFor(model => model.DeliveryDate) - @Model.DeliveryDate - @if (Model.CanDeliver) - { -   - - } +
                                    + @Model.DeliveryDate + @if (Model.CanDeliver) + { + + } +
                                    -

                                    @T("Admin.Orders.Shipments.Products")

                                    +
                                    @T("Admin.Orders.Shipments.Products")
                                    - - - + + + + } + +
                                    +
                                    + + + + + + + + + + @foreach (var item in Model.Items) + { + + - -
                                    + @T("Admin.Orders.Shipments.Products.ProductName") + + @T("Admin.Orders.Shipments.Products.SKU") + + @T("Admin.Orders.Shipments.Products.QtyShipped") +
                                    +
                                    + @Html.LabeledProductName(item.ProductId, item.ProductName, item.ProductTypeName, item.ProductTypeLabelHint) - - - - - - - - - - @foreach (var item in Model.Items) - { - - - - - - } - -
                                    - @T("Admin.Orders.Shipments.Products.ProductName") - - @T("Admin.Orders.Shipments.Products.SKU") - - @T("Admin.Orders.Shipments.Products.QtyShipped") -
                                    -
                                    - @Html.LabeledProductName(item.ProductId, item.ProductName, item.ProductTypeName, item.ProductTypeLabelHint) - - @if (!String.IsNullOrEmpty(item.AttributeInfo)) - { -
                                    - @Html.Raw(item.AttributeInfo) - } -
                                    -
                                    -
                                    - @item.Sku -
                                    -
                                    - @item.QuantityInThisShipment -
                                    - -
                                    + @if (!String.IsNullOrEmpty(item.AttributeInfo)) + { +
                                    + @Html.Raw(item.AttributeInfo) + } +
                                    +
                                    +
                                    + @item.Sku +
                                    +
                                    + @item.QuantityInThisShipment +
                                    + } @Html.DeleteConfirmation("DeleteShipment", "shipment-delete") \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentList.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentList.cshtml index 967a96ac86..61330f9f09 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentList.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/ShipmentList.cshtml @@ -1,11 +1,7 @@ @model ShipmentListModel - @using Telerik.Web.Mvc.UI - @{ var gridPageSize = EngineContext.Current.Resolve().GridPageSize; - - //page title ViewBag.Title = T("Admin.Orders.Shipments.List").Text; } @@ -19,92 +15,79 @@
                                    @if (Model.DisplayPdfPackagingSlip) { - -  @T("Admin.Orders.Shipments.PrintPackagingSlip.All") + + + @T("Admin.Orders.Shipments.PrintPackagingSlip.All") - }
                                    - - - - - - - - - - - - - - - - - -
                                    - @Html.SmartLabelFor(model => model.StartDate) - - @Html.EditorFor(model => model.StartDate) -
                                    - @Html.SmartLabelFor(model => model.EndDate) - - @Html.EditorFor(model => Model.EndDate) -
                                    - @Html.SmartLabelFor(model => model.TrackingNumber) - - @Html.EditorFor(model => Model.TrackingNumber) -
                                    -   - - -
                                    - -

                                    - - - - - -
                                    - @(Html.Telerik().Grid() - .Name("shipments-grid") - .ClientEvents(events => events - .OnDataBinding("onDataBinding") - .OnDataBound("onDataBound")) - .Columns(columns => - { - columns.Bound(x => x.Id) - .ClientTemplate("") - .Title("") - .Width(50) - .HtmlAttributes(new { style = "text-align:center" }) - .HeaderHtmlAttributes(new { style = "text-align:center" }); - columns.Bound(x => x.Id); - columns.Bound(x => x.OrderId) - .Width(80) - .Centered() - .Template(x => Html.ActionLink(T("Admin.Common.View").Text, "Edit", "Order", new { id = x.OrderId }, new { })) - .ClientTemplate("\"><#= OrderId #>"); - columns.Bound(x => x.TrackingNumber); - columns.Bound(x => x.TotalWeight); - columns.Bound(x => x.ShippedDate); - columns.Bound(x => x.DeliveryDate); - columns.Bound(x => x.Id) - .Template(x => Html.ActionLink(T("Admin.Common.View").Text, "ShipmentDetails", "Order", new { id = x.Id }, new { })) - .ClientTemplate("\">" + T("Admin.Common.View").Text + "") - .Width(50) - .Centered() - .HeaderTemplate("{0}".FormatWith(T("Admin.Common.View").Text)); - }) - .Pageable(settings => settings.PageSize(gridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("ShipmentListSelect", "Order")) - .PreserveGridState() - .EnableCustomBinding(true)) -
                                    + +
                                    +
                                    + @Html.SmartLabelFor(model => model.StartDate) + @Html.EditorFor(model => model.StartDate) +
                                    +
                                    + @Html.SmartLabelFor(model => model.EndDate) + @Html.EditorFor(model => model.EndDate) +
                                    + +
                                    + @Html.SmartLabelFor(model => model.TrackingNumber) + @Html.EditorFor(model => model.TrackingNumber) +
                                    + +
                                    + + +
                                    +
                                    + + +
                                    + @(Html.Telerik().Grid() + .Name("shipments-grid") + .ClientEvents(events => events + .OnDataBinding("onDataBinding") + .OnDataBound("onDataBound")) + .Columns(columns => + { + columns.Bound(x => x.Id) + .ClientTemplate("") + .Title("") + .Width(50) + .HtmlAttributes(new { style = "text-align:center" }) + .HeaderHtmlAttributes(new { style = "text-align:center" }); + columns.Bound(x => x.Id); + columns.Bound(x => x.OrderId) + .Width(80) + .Centered() + .Template(x => Html.ActionLink(T("Admin.Common.View").Text, "Edit", "Order", new { id = x.OrderId }, new { })) + .ClientTemplate("\"><#= OrderId #>"); + columns.Bound(x => x.TrackingNumber); + columns.Bound(x => x.TotalWeight); + columns.Bound(x => x.ShippedDate); + columns.Bound(x => x.DeliveryDate); + columns.Bound(x => x.Id) + .Template(x => Html.ActionLink(T("Admin.Common.View").Text, "ShipmentDetails", "Order", new { id = x.Id }, new { @class = "t-button" })) + .ClientTemplate("\" class=\"t-button\">" + T("Admin.Common.View").Text + "") + .Width(50) + .Centered() + .Title(String.Empty); + }) + .Pageable(settings => settings.PageSize(gridPageSize).Position(GridPagerPosition.Both)) + .DataBinding(dataBinding => dataBinding.Ajax().Select("ShipmentListSelect", "Order")) + .PreserveGridState() + .EnableCustomBinding(true)) +
                                    + + +@Html.Widget("order_edit_top") + + + + + + + + + + + + + + + + + + + @if (Model.AffiliateId != 0) + { + + + + + } + + + + + + + + + + + + @if (Model.RecurringPaymentId > 0) + { + + + + + } + @if (!String.IsNullOrEmpty(Model.VatNumber)) + { + + + + + } + @if (Model.AllowCustomersToSelectTaxDisplayType || Model.TaxDisplayType == TaxDisplayType.IncludingTax) + { + + + + + } + @if (Model.AllowCustomersToSelectTaxDisplayType || Model.TaxDisplayType == TaxDisplayType.ExcludingTax) + { + + + + + } + @if ((Model.AllowCustomersToSelectTaxDisplayType || Model.TaxDisplayType == TaxDisplayType.IncludingTax) && !String.IsNullOrEmpty(Model.OrderSubTotalDiscountInclTax)) + { + + + + + } + @if ((Model.AllowCustomersToSelectTaxDisplayType || Model.TaxDisplayType == TaxDisplayType.ExcludingTax) && !String.IsNullOrEmpty(Model.OrderSubTotalDiscountExclTax)) + { + + + + + } + @if (Model.AllowCustomersToSelectTaxDisplayType || Model.TaxDisplayType == TaxDisplayType.IncludingTax) + { + + + + + } + @if (Model.AllowCustomersToSelectTaxDisplayType || Model.TaxDisplayType == TaxDisplayType.ExcludingTax) + { + + + + + } + @if ((Model.AllowCustomersToSelectTaxDisplayType || Model.TaxDisplayType == TaxDisplayType.IncludingTax) && !String.IsNullOrEmpty(Model.PaymentMethodAdditionalFeeInclTax)) + { + + + + + } + @if ((Model.AllowCustomersToSelectTaxDisplayType || Model.TaxDisplayType == TaxDisplayType.ExcludingTax) && !String.IsNullOrEmpty(Model.PaymentMethodAdditionalFeeExclTax)) + { + + + + + } + @if (Model.DisplayTaxRates) + { + foreach (var tr in Model.TaxRates) + { + + + + + } + } + @if (Model.DisplayTax) + { + + + + + } + @if (!String.IsNullOrEmpty(Model.OrderTotalDiscount)) + { + + + + + } + @foreach (var gc in Model.GiftCards) + { + + + + + } + @if (Model.RedeemedRewardPoints > 0) + { + + + + + } + @if (Model.OrderTotalRounding.HasValue()) + { + + + + + } + + + + + @if (!String.IsNullOrEmpty(Model.RefundedAmount)) + { + + + + + } + + + + + + + + + + + + + @if (Model.AllowStoringCreditCardNumber) + { + + + + + + + + + } + @if (Model.AllowStoringCreditCardNumber || !String.IsNullOrEmpty(Model.CardNumber)) + { + + + + + } + @if (Model.AllowStoringCreditCardNumber) + { + + + + + + + + + + + + + + + + + + + } + + @if (Model.AllowStoringDirectDebit) + { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + } + + @if (Model.DisplayCompletePaymentNote) + { + + + + } + @if (Model.DisplayPurchaseOrderNumber) + { + + + + + } + @if (!String.IsNullOrEmpty(Model.AuthorizationTransactionId)) + { + + + + + } + @if (!String.IsNullOrEmpty(Model.AuthorizationTransactionResult)) + { + + + + + } + @if (!String.IsNullOrEmpty(Model.CaptureTransactionId)) + { + + + + + } + @if (!String.IsNullOrEmpty(Model.CaptureTransactionResult)) + { + + + + + } + @if (!String.IsNullOrEmpty(Model.SubscriptionTransactionId)) + { + + + + + } + + + + + + + + + + + + + + + + + + + + + + + + + + +
                                    + @Html.SmartLabelFor(model => model.OrderStatus) + +
                                    + @Model.OrderStatus + + @if (Model.CanCancelOrder) + { + + } + + @if (Model.CanCompleteOrder) + { + + } +
                                    +
                                    + @Html.SmartLabelFor(model => model.OrderNumber) + +
                                    + @Model.OrderNumber +
                                    +
                                    + @Html.SmartLabelFor(model => model.OrderGuid) + +
                                    + @Model.OrderGuid +
                                    +
                                    + @Html.SmartLabelFor(model => model.StoreName) + +
                                    + @Model.StoreName +
                                    +
                                    + @Html.SmartLabelFor(model => model.AffiliateId) + + +
                                    +
                                    +
                                    + @Html.SmartLabelFor(model => model.CustomerId) + + +
                                    + @Html.SmartLabelFor(model => model.CustomerIp) + +
                                    + @Model.CustomerIp +
                                    +
                                    + @Html.SmartLabelFor(model => model.RecurringPaymentId) + + +
                                    + @Html.SmartLabelFor(model => model.VatNumber) + +
                                    + @Model.VatNumber +
                                    +
                                    + @Html.SmartLabelFor(model => model.OrderSubtotalInclTax) + +
                                    + @Model.OrderSubtotalInclTax +
                                    +
                                    + @Html.SmartLabelFor(model => model.OrderSubtotalExclTax) + +
                                    + @Model.OrderSubtotalExclTax +
                                    +
                                    + @Html.SmartLabelFor(model => model.OrderSubTotalDiscountInclTax) + +
                                    + @Model.OrderSubTotalDiscountInclTax +
                                    +
                                    + @Html.SmartLabelFor(model => model.OrderSubTotalDiscountExclTax) + +
                                    + @Model.OrderSubTotalDiscountExclTax +
                                    +
                                    + @Html.SmartLabelFor(model => model.OrderShippingInclTax) + +
                                    + @Model.OrderShippingInclTax +
                                    +
                                    + @Html.SmartLabelFor(model => model.OrderShippingExclTax) + +
                                    + @Model.OrderShippingExclTax +
                                    +
                                    + @Html.SmartLabelFor(model => model.PaymentMethodAdditionalFeeInclTax) + +
                                    + @Model.PaymentMethodAdditionalFeeInclTax +
                                    +
                                    + @Html.SmartLabelFor(model => model.PaymentMethodAdditionalFeeExclTax) + +
                                    + @Model.PaymentMethodAdditionalFeeExclTax +
                                    +
                                    + @Html.SmartLabelFor(model => model.Tax) + +
                                    + @tr.Rate% - @tr.Value +
                                    +
                                    + @Html.SmartLabelFor(model => model.Tax) + +
                                    + @Model.Tax +
                                    +
                                    + @Html.SmartLabelFor(model => model.OrderTotalDiscount) + +
                                    + @Model.OrderTotalDiscount +
                                    +
                                    + @Html.SmartLabelFor(model => model.GiftCards[0].CouponCode) (@(gc.CouponCode)) + +
                                    + @gc.Amount +
                                    +
                                    + @Html.SmartLabelFor(model => model.RedeemedRewardPoints) + +
                                    + @Model.RedeemedRewardPoints @T("Admin.Orders.Fields.RedeemedRewardPoints.Points") + / + @Model.RedeemedRewardPointsAmount +
                                    +
                                    + @Html.SmartLabelFor(model => model.OrderTotalRounding) + +
                                    + @Model.OrderTotalRounding +
                                    +
                                    + @Html.SmartLabelFor(model => model.OrderTotal) + +
                                    + @Model.OrderTotal +
                                    +
                                    + @Html.SmartLabelFor(model => model.RefundedAmount) + +
                                    + @Model.RefundedAmount +
                                    +
                                    +
                                    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                                    + @Html.SmartLabelFor(model => model.OrderSubtotalInclTaxValue) + + @Html.EditorFor(model => model.OrderSubtotalInclTaxValue, new { postfix = T("Admin.Orders.Fields.Edit.InclTax").Text }) +
                                    + @Html.EditorFor(model => model.OrderSubtotalExclTaxValue, new { postfix = T("Admin.Orders.Fields.Edit.ExclTax").Text }) +
                                    +
                                    + @Html.SmartLabelFor(model => model.OrderSubTotalDiscountInclTaxValue) + + @Html.EditorFor(model => model.OrderSubTotalDiscountInclTaxValue, new { postfix = T("Admin.Orders.Fields.Edit.InclTax").Text }) +
                                    + @Html.EditorFor(model => model.OrderSubTotalDiscountExclTaxValue, new { postfix = T("Admin.Orders.Fields.Edit.ExclTax").Text }) +
                                    +
                                    + @Html.SmartLabelFor(model => model.OrderShippingInclTaxValue) + + @Html.EditorFor(model => model.OrderShippingInclTaxValue, new { postfix = T("Admin.Orders.Fields.Edit.InclTax").Text }) +
                                    + @Html.EditorFor(model => model.OrderShippingExclTaxValue, new { postfix = T("Admin.Orders.Fields.Edit.ExclTax").Text }) +
                                    +
                                    + @Html.SmartLabelFor(model => model.PaymentMethodAdditionalFeeInclTaxValue) + + @Html.EditorFor(model => model.PaymentMethodAdditionalFeeInclTaxValue, new { postfix = T("Admin.Orders.Fields.Edit.InclTax").Text }) +
                                    + @Html.EditorFor(model => model.PaymentMethodAdditionalFeeExclTaxValue, new { postfix = T("Admin.Orders.Fields.Edit.ExclTax").Text }) +
                                    +
                                    + @Html.SmartLabelFor(model => model.TaxRatesValue) + + @Html.EditorFor(model => model.TaxRatesValue) +
                                    + @Html.SmartLabelFor(model => model.TaxValue) + + @Html.EditorFor(model => model.TaxValue) +
                                    + @Html.SmartLabelFor(model => model.OrderTotalDiscountValue) + + @Html.EditorFor(model => model.OrderTotalDiscountValue) +
                                    + @Html.SmartLabelFor(model => model.OrderTotalRoundingValue) + + @Html.EditorFor(model => model.OrderTotalRoundingValue) +
                                    + @Html.SmartLabelFor(model => model.OrderTotalValue) + + @Html.EditorFor(model => model.OrderTotalValue) +
                                    +
                                    +
                                    + + + +
                                    +
                                    +
                                    + @Html.SmartLabelFor(model => model.CardType) + +
                                    + @Model.CardType +
                                    + @Html.EditorFor(model => model.CardType) +
                                    + @Html.SmartLabelFor(model => model.CardName) + +
                                    + @Model.CardName +
                                    + @Html.EditorFor(model => model.CardName) +
                                    + @Html.SmartLabelFor(model => model.CardNumber) + +
                                    + @Model.CardNumber +
                                    + @Html.EditorFor(model => model.CardNumber) +
                                    + @Html.SmartLabelFor(model => model.CardCvv2) + +
                                    + @Model.CardCvv2 +
                                    + @Html.EditorFor(model => model.CardCvv2) +
                                    + @Html.SmartLabelFor(model => model.CardExpirationMonth) + +
                                    + @Model.CardExpirationMonth +
                                    + @Html.EditorFor(model => model.CardExpirationMonth) +
                                    + @Html.SmartLabelFor(model => model.CardExpirationYear) + +
                                    + @Model.CardExpirationYear +
                                    + @Html.EditorFor(model => model.CardExpirationYear) +
                                    + + + +
                                    +
                                    +
                                    + @Html.SmartLabelFor(model => model.DirectDebitAccountHolder) + +
                                    + @Model.DirectDebitAccountHolder +
                                    + @Html.EditorFor(model => model.DirectDebitAccountHolder) +
                                    + @Html.SmartLabelFor(model => model.DirectDebitAccountNumber) + +
                                    + @Model.DirectDebitAccountNumber +
                                    + @Html.EditorFor(model => model.DirectDebitAccountNumber) +
                                    + @Html.SmartLabelFor(model => model.DirectDebitBankCode) + +
                                    + @Model.DirectDebitBankCode +
                                    + @Html.EditorFor(model => model.DirectDebitBankCode) +
                                    + @Html.SmartLabelFor(model => model.DirectDebitBankName) + +
                                    + @Model.DirectDebitBankName +
                                    + @Html.EditorFor(model => model.DirectDebitBankName) +
                                    + @Html.SmartLabelFor(model => model.DirectDebitBIC) + +
                                    + @Model.DirectDebitBIC +
                                    + @Html.EditorFor(model => model.DirectDebitBIC) +
                                    + @Html.SmartLabelFor(model => model.DirectDebitCountry) + +
                                    + @Model.DirectDebitCountry +
                                    + @Html.EditorFor(model => model.DirectDebitCountry) +
                                    + @Html.SmartLabelFor(model => model.DirectDebitIban) + +
                                    + @Model.DirectDebitIban +
                                    + @Html.EditorFor(model => model.DirectDebitIban) +
                                    + + + +
                                    +
                                    +
                                    +
                                    + @Html.Raw(T("Order.CompletePayment.AdminNote", Url.Action("Details", "Order", new { id = Model.Id, area = "" }))) +
                                    +
                                    + @Html.SmartLabelFor(model => model.PurchaseOrderNumber) + +
                                    + @Model.PurchaseOrderNumber +
                                    +
                                    + @Html.SmartLabelFor(model => model.AuthorizationTransactionId) + +
                                    + @Model.AuthorizationTransactionId +
                                    +
                                    + @Html.SmartLabelFor(model => model.AuthorizationTransactionResult) + +
                                    + @Model.AuthorizationTransactionResult +
                                    +
                                    + @Html.SmartLabelFor(model => model.CaptureTransactionId) + +
                                    + @Model.CaptureTransactionId +
                                    +
                                    + @Html.SmartLabelFor(model => model.CaptureTransactionResult) + +
                                    + @Model.CaptureTransactionResult +
                                    +
                                    + @Html.SmartLabelFor(model => model.SubscriptionTransactionId) + +
                                    + @Model.SubscriptionTransactionId +
                                    +
                                    + @Html.SmartLabelFor(model => model.PaymentMethod) + +
                                    + @Model.PaymentMethod + @if (Model.PaymentMethodSystemName.HasValue()) + { + (@Model.PaymentMethodSystemName) + } +
                                    +
                                    + @Html.SmartLabelFor(model => model.PaymentStatus) + + @Model.PaymentStatus + + @if (Model.CanMarkOrderAsPaid) + { + + } + @if (Model.CanCapture) + { + + } + @if (Model.CanRefund) + { + + } + @if (Model.CanRefundOffline) + { + + } + @if (Model.CanPartiallyRefund) + { + + } + @if (Model.CanPartiallyRefundOffline) + { + + } + @if (Model.CanVoid) + { + + } + @if (Model.CanVoidOffline) + { + + } +
                                    + @Html.SmartLabelFor(model => model.CreatedOn) + + @Html.TextBoxFor(model => model.CreatedOn, new { @readonly = "readonly", @class = "form-control-plaintext" }) +
                                    + @Html.SmartLabelFor(model => model.UpdatedOn) + + @Html.TextBoxFor(model => model.UpdatedOn, new { @readonly = "readonly", @class = "form-control-plaintext" }) +
                                    +
                                    +
                                    + @Html.SmartLabelFor(model => model.AcceptThirdPartyEmailHandOver) + +
                                    + @(Model.AcceptThirdPartyEmailHandOver ? T("Common.Yes") : T("Common.No")) +
                                    +
                                    + @if (Model.CustomerComment.HasValue()) + { +

                                    @T("Admin.Order.CustomerComment.Heading")

                                    +
                                    + @Model.CustomerComment +
                                    + } +
                                    \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/_Edit.OrderNotes.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/_Edit.OrderNotes.cshtml new file mode 100644 index 0000000000..8dfb77aa17 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/_Edit.OrderNotes.cshtml @@ -0,0 +1,107 @@ +@model OrderModel + +
                                    + @(Html.Telerik().Grid().Name("ordernotes-grid") + .DataKeys(keys => + { + keys.Add(x => x.Id).RouteKey("orderNoteId"); + keys.Add(x => x.OrderId).RouteKey("orderId"); + }) + .DataBinding(binding => + { + binding.Ajax().Select("OrderNotesSelect", "Order", new { orderId = Model.Id }) + .Delete("OrderNoteDelete", "Order"); + }) + .Columns(columns => + { + columns.Bound(x => x.CreatedOn).Width(140); + columns.Bound(x => x.Note).Encoded(false); + columns.Bound(x => x.DisplayToCustomer) + .Template(item => @Html.SymbolForBool(item.DisplayToCustomer)) + .ClientTemplate(@Html.SymbolForBool("DisplayToCustomer")) + .Centered() + .Width(140); + columns.Command(commands => + { + commands.Delete().Localize(T); + }).Width(140).HtmlAttributes(new { align = "right" }); + }) + .ToolBar(commands => commands.Template(OrderNotesGridCommands)) + .EnableCustomBinding(true)) +
                                    + +@{Html.SmartStore().Window() + .Name("addrecord-window") + .Size(WindowSize.Large) + .Title(T("Admin.Orders.OrderNotes.AddButton")) + .Content( + @ +
                                    + + + + + + + + + +
                                    + @Html.SmartLabelFor(model => model.AddOrderNoteMessage) + + @Html.TextAreaFor(model => model.AddOrderNoteMessage, new { style = "height: 120px;" }) + @Html.ValidationMessageFor(model => model.AddOrderNoteMessage) +
                                    + @Html.SmartLabelFor(model => model.AddOrderNoteDisplayToCustomer) + + @Html.EditorFor(model => model.AddOrderNoteDisplayToCustomer) + @Html.ValidationMessageFor(model => model.AddOrderNoteDisplayToCustomer) +
                                    +
                                    +
                                    ) + .FooterContent( + @ + + + ) + .Render(); +} + +@helper OrderNotesGridCommands(Grid grid) +{ + + + @T("Admin.Orders.OrderNotes.AddButton") + +} + + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/_Edit.Products.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/_Edit.Products.cshtml new file mode 100644 index 0000000000..a4a06be61b --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/_Edit.Products.cshtml @@ -0,0 +1,429 @@ +@model OrderModel +@using SmartStore.Core.Domain.Tax; +@using SmartStore.Core.Domain.Catalog; + + + + + +@if (Model.AutoUpdateOrderItemInfo.HasValue()) +{ +
                                    + + @Html.Raw(Model.AutoUpdateOrderItemInfo) +
                                    +} + +
                                    +
                                    + +
                                    +
                                    + + + + + @if (Model.HasDownloadableProducts) + { + + } + + + + + + + + + @foreach (var item in Model.Items) + { + + + + @if (Model.HasDownloadableProducts) + { + + } + + + + + + + + + } + +
                                    + @T("Admin.Orders.Products.ProductName") + + @T("Admin.Orders.Products.Download") + + @T("Admin.Orders.Products.Price") + + @T("Admin.Orders.Products.Quantity") + + @T("Admin.Orders.Products.Discount") + + @T("Admin.Orders.Products.Total") +
                                    +
                                    +
                                    + @Html.LabeledProductName(item.ProductId, item.ProductName, item.ProductTypeName, item.ProductTypeLabelHint) +
                                    + + @if (!String.IsNullOrEmpty(item.AttributeInfo)) + { + @Html.Raw(item.AttributeInfo) + } + @if (!String.IsNullOrEmpty(item.RecurringInfo)) + { +
                                    + @Html.Raw(item.RecurringInfo) + } + @if (!String.IsNullOrEmpty(item.Sku)) + { +
                                    + @T("Admin.Orders.Products.SKU"): + @item.Sku + } + @if (item.ProductType == ProductType.BundledProduct) + { +
                                    + @BundleItemsInfo(item) + } + @if (item.ReturnRequests.Count > 0) + { +
                                    +
                                    + for (int i = 0; i < item.ReturnRequests.Count; ++i) + { + var returnRequest = item.ReturnRequests[i]; + + @returnRequest.StatusString + + + @T("Admin.Orders.Products.ReturnRequest") @returnRequest.Id + + + , + + @T("Admin.ReturnRequests.Fields.Quantity") @returnRequest.Quantity + if (i != item.ReturnRequests.Count - 1) + { +
                                    + } + } + } + @if (item.PurchasedGiftCardIds.Count > 0) + { +
                                    +
                                    + @T("Admin.Orders.Products.GiftCards"): + + for (int i = 0; i < item.PurchasedGiftCardIds.Count; i++) + { + @item.PurchasedGiftCardIds[i] + if (i != item.PurchasedGiftCardIds.Count - 1) + { + , + } + } + } +
                                    +
                                    + @if (item.IsDownload) + { +
                                    + @string.Format(T("Admin.Orders.Products.Download.DownloadCount").Text, item.DownloadCount) + +
                                    + + if (item.DownloadActivationType == DownloadActivationType.Manually) + { +
                                    + +
                                    + } + +
                                    + @T("Admin.Orders.Products.License") + + @if (item.LicenseDownloadId.HasValue && item.LicenseDownloadId.Value > 0) + { + + @T("Admin.Orders.Products.License.DownloadLicense") + + } + + +
                                    + } +
                                    + +
                                    + @if (Model.AllowCustomersToSelectTaxDisplayType) + { + @item.UnitPriceInclTax +
                                    + @item.UnitPriceExclTax + } + else + { + @(Model.TaxDisplayType == TaxDisplayType.ExcludingTax ? item.UnitPriceExclTax : item.UnitPriceInclTax) + } +
                                    + +
                                    +
                                    +
                                    +
                                    + @T("Admin.Orders.Products.Edit.InclTax") +
                                    +
                                    + +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    + @T("Admin.Orders.Products.Edit.ExclTax") +
                                    +
                                    + +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    + @T("Admin.Orders.Products.AddNew.TaxRate") +
                                    +
                                    + +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    + @item.Quantity +
                                    + +
                                    +
                                    + +
                                    +
                                    +
                                    + +
                                    + @if (Model.AllowCustomersToSelectTaxDisplayType) + { + @item.DiscountInclTax +
                                    + @item.DiscountExclTax + } + else + { + @(Model.TaxDisplayType == TaxDisplayType.ExcludingTax ? item.DiscountExclTax : item.DiscountInclTax) + } +
                                    + +
                                    +
                                    +
                                    +
                                    + @T("Admin.Orders.Products.Edit.InclTax") +
                                    +
                                    + +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    + @T("Admin.Orders.Products.Edit.ExclTax") +
                                    +
                                    + +
                                    +
                                    +
                                    +
                                    +
                                    + +
                                    + @if (Model.AllowCustomersToSelectTaxDisplayType) + { + @item.SubTotalInclTax +
                                    + @item.SubTotalExclTax + } + else + { + @(Model.TaxDisplayType == TaxDisplayType.ExcludingTax ? item.SubTotalExclTax : item.SubTotalInclTax) + } +
                                    + +
                                    + +
                                    +
                                    +
                                    + @T("Admin.Orders.Products.Edit.InclTax") +
                                    +
                                    + +
                                    +
                                    +
                                    + +
                                    +
                                    +
                                    + @T("Admin.Orders.Products.Edit.ExclTax") +
                                    +
                                    + +
                                    +
                                    +
                                    +
                                    +
                                    + + @if (item.IsReturnRequestPossible) + { + + } + + + + + + + + +
                                    +
                                    +
                                    + +@if (!String.IsNullOrEmpty(Model.CheckoutAttributeInfo)) +{ +
                                    + @Html.Raw(Model.CheckoutAttributeInfo) +
                                    +} + +@helper BundleItemsInfo(OrderModel.OrderItemModel parentItem) +{ + if (parentItem.BundleItems != null) + { +
                                    + @foreach (var item in parentItem.BundleItems.OrderBy(x => x.DisplayOrder)) + { +
                                    +
                                    + @Html.ActionLink(item.ProductName, "Edit", "Product", new { id = item.ProductId }, new { }) + @if (item.Quantity > 1) + { + + × @item.Quantity + + } +
                                    + @if (!String.IsNullOrWhiteSpace(item.PriceWithDiscount)) + { +
                                    + @Html.Raw(item.PriceWithDiscount) +
                                    + } + @if (!String.IsNullOrEmpty(item.AttributeInfo)) + { +
                                    + @Html.Raw(item.AttributeInfo) +
                                    + } +
                                    + } +
                                    + } +} diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/_ProductAddAttributes.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/_ProductAddAttributes.cshtml index 1fc150e959..77602f54cb 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/_ProductAddAttributes.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/_ProductAddAttributes.cshtml @@ -4,97 +4,73 @@ @if (Model.ProductVariantAttributes.Count > 0) { -
                                    - @foreach (var attribute in Model.ProductVariantAttributes) +
                                    + @foreach (var attribute in Model.ProductVariantAttributes) { var controlId = attribute.GetControlId(Model.ProductId, 0); -
                                    - @if (attribute.IsRequired) - { - * - } - @if (!string.IsNullOrEmpty(attribute.TextPrompt)) - { - @attribute.TextPrompt - } - else - { - @attribute.Name - } - @if (attribute.AttributeControlType == AttributeControlType.TextBox || - attribute.AttributeControlType == AttributeControlType.FileUpload) - { -     - } - else - { -
                                    - } - @switch (attribute.AttributeControlType) - { - case AttributeControlType.DropdownList: - { - - } - break; - case AttributeControlType.RadioList: - case AttributeControlType.Boxes: - { - foreach (var pvaValue in attribute.Values) - { -
                                    - -
                                    +
                                    +
                                    +
                                    + +
                                    +
                                    +
                                    + @switch (attribute.AttributeControlType) + { + case AttributeControlType.DropdownList: + { + + } + break; + case AttributeControlType.RadioList: + case AttributeControlType.Boxes: + case AttributeControlType.Checkboxes: + { + foreach (var pvaValue in attribute.Values) + { +
                                    + + +
                                    + } + } + break; + case AttributeControlType.TextBox: + { + + } + break; + case AttributeControlType.MultilineTextbox: + { + + } + break; + case AttributeControlType.Datepicker: + { + @Html.DatePickerDropDowns(controlId + "-day", controlId + "-month", controlId + "-year", DateTime.Now.Year, DateTime.Now.Year + 1) } - } - break; - case AttributeControlType.Checkboxes: - { - foreach (var pvaValue in attribute.Values) + break; + case AttributeControlType.FileUpload: { -
                                    - -
                                    + } - } - break; - case AttributeControlType.TextBox: - { - - } - break; - case AttributeControlType.MultilineTextbox: - { - - } - break; - case AttributeControlType.Datepicker: - { - @Html.DatePickerDropDowns(controlId + "-day", controlId + "-month", controlId + "-year", DateTime.Now.Year, DateTime.Now.Year + 1) - } - break; - case AttributeControlType.FileUpload: - { - - } - break; - } -
                                    - } -
                                    + break; + } +
                                    +
                                    + } +
                                    } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Order/_ProductAddGiftCardInfo.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Order/_ProductAddGiftCardInfo.cshtml index 0a5d1e6d40..a4efcd840f 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Order/_ProductAddGiftCardInfo.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Order/_ProductAddGiftCardInfo.cshtml @@ -1,79 +1,63 @@ -@model OrderModel.AddOrderProductModel.GiftCardModel +@using SmartStore.Core.Domain.Catalog; +@model OrderModel.AddOrderProductModel.GiftCardModel + @if (Model.IsGiftCard) { - //Actually just a copy of \Presentation\SmartStore.Web\Views\Catalog\_GiftCardInfo.cshtml -

                                     

                                    -
                                    +
                                    +
                                    +
                                    + @Html.SmartLabelFor(model => model.RecipientName) +
                                    +
                                    + @Html.EditorFor(model => model.RecipientName) + @Html.ValidationMessageFor(model => model.RecipientName) +
                                    +
                                    + + @if (Model.GiftCardType == GiftCardType.Virtual) + { +
                                    +
                                    + @Html.SmartLabelFor(model => model.RecipientEmail) +
                                    +
                                    + @Html.EditorFor(model => model.RecipientEmail) + @Html.ValidationMessageFor(model => model.RecipientEmail) +
                                    +
                                    + } + +
                                    +
                                    + @Html.SmartLabelFor(model => model.SenderName) +
                                    +
                                    + @Html.EditorFor(model => model.SenderName) + @Html.ValidationMessageFor(model => model.SenderName) +
                                    +
                                    + + @if (Model.GiftCardType == GiftCardType.Virtual) + { +
                                    +
                                    + @Html.SmartLabelFor(model => model.SenderEmail) +
                                    +
                                    + @Html.EditorFor(model => model.SenderEmail) + @Html.ValidationMessageFor(model => model.SenderEmail) +
                                    +
                                    + } -
                                    - @Html.LabelFor(model => model.RecipientName, new { @class = "control-label required", @for = "RecipientName" }) -
                                    - @Html.EditorFor(model => model.RecipientName, "BlockInput") -
                                    -
                                    - - @if (Model.GiftCardType == SmartStore.Core.Domain.Catalog.GiftCardType.Virtual) - { -
                                    - @Html.LabelFor(model => model.RecipientEmail, new { @class = "control-label required", @for = "RecipientEmail" }) -
                                    - @Html.EditorFor(model => model.RecipientEmail, "BlockInput") -
                                    -
                                    - } - -
                                    - @Html.LabelFor(model => model.SenderName, new { @class = "control-label required", @for = "SenderName" }) -
                                    - @Html.EditorFor(model => model.SenderName, "BlockInput") -
                                    -
                                    - - @if (Model.GiftCardType == SmartStore.Core.Domain.Catalog.GiftCardType.Virtual) - { -
                                    - @Html.LabelFor(model => model.SenderEmail, new { @class = "control-label required", @for = "SenderEmail" }) -
                                    - @Html.EditorFor(model => model.SenderEmail, "BlockInput") -
                                    -
                                    - } - -
                                    - @Html.LabelFor(model => model.Message, new { @class = "control-label", @for = "Message" }) -
                                    - @Html.TextAreaFor(model => model.Message, new { @class = "message input-block-level" }) -
                                    -
                                    - - @*
                                    -
                                    @Html.LabelFor(model => model.RecipientName)
                                    -
                                    - @Html.TextBoxFor(model => model.RecipientName) -
                                    - @if (Model.GiftCardType == SmartStore.Core.Domain.Catalog.GiftCardType.Virtual) - { -
                                    @Html.LabelFor(model => model.RecipientEmail)
                                    -
                                    - @Html.TextBoxFor(model => model.RecipientEmail) -
                                    - } -
                                    - @Html.LabelFor(model => model.SenderName)
                                    -
                                    - @Html.TextBoxFor(model => model.SenderName) -
                                    - @if (Model.GiftCardType == SmartStore.Core.Domain.Catalog.GiftCardType.Virtual) - { -
                                    @Html.LabelFor(model => model.SenderEmail)
                                    -
                                    - @Html.TextBoxFor(model => model.SenderEmail)
                                    - } -
                                    - @Html.LabelFor(model => model.Message)
                                    -
                                    - @Html.TextAreaFor(model => model.Message, new { style = "Width: 300px; Height: 100px;" }) -
                                    -
                                    *@ -
                                    -} \ No newline at end of file +
                                    +
                                    + @Html.SmartLabelFor(model => model.Message) +
                                    +
                                    + @Html.TextAreaFor(model => model.Message) + @Html.ValidationMessageFor(model => model.Message) +
                                    +
                                    +
                                    +} diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Packaging/UploadPackage.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Packaging/UploadPackage.cshtml index ecb6937ec0..1f0888df7d 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Packaging/UploadPackage.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Packaging/UploadPackage.cshtml @@ -1,34 +1,42 @@ - +@model dynamic + @{ string dialogTitle = (string)Model.Title; string dialogInfo = (string)Model.Info; } @{Html.SmartStore().Window() - .Name("uploadpackage-window") - .Title(dialogTitle) - .Content(@ - @using (Html.BeginForm("UploadPackage", "Packaging", FormMethod.Post, new { enctype = "multipart/form-data", @class = "form-horizontal" })) - { + .Name("uploadpackage-window") + .Title(dialogTitle) + .Size(WindowSize.Large) + .Content(@ +
                                    @dialogInfo
                                    -
                                    - -
                                    - +
                                    +
                                    +
                                    +
                                    + +
                                    +
                                    +
                                    + +
                                    - } - - ) - .FooterContent(@ - - @T("Common.Cancel") - ) - .Modal(true) - .Visible(false) - .Render(); + + ) + .FooterContent(@ + + @T("Common.Cancel") + + + ) + .Render(); } - - - - } - else - { - @T("Admin.ContentManagement.Polls.Answers.SaveBeforeEdit") - } + +
                                    + } + else + { + @T("Admin.ContentManagement.Polls.Answers.SaveBeforeEdit") + } } @helper TabStores() { - - - - - - - - - - - -
                                    - @Html.SmartLabelFor(model => model.LimitedToStores) - - @Html.EditorFor(model => model.LimitedToStores) - @Html.ValidationMessageFor(model => model.LimitedToStores) -
                                    - @Html.SmartLabelFor(model => model.AvailableStores) - - @if (Model.AvailableStores != null && Model.AvailableStores.Count > 0) - { - foreach (var store in Model.AvailableStores) - { - - } - } - else - { -
                                    @T("Admin.Configuration.Stores.NoStoresDefined")
                                    - }
                                    + @Html.Partial("StoreSelector", Model) } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/AttributeCombinationEditPopup.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/AttributeCombinationEditPopup.cshtml index 8d41fc2a81..3e76fe00d6 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/AttributeCombinationEditPopup.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/AttributeCombinationEditPopup.cshtml @@ -18,7 +18,7 @@ @T("Admin.Catalog.Products.ProductVariantAttributes.AttributeCombinations.EditTitle")
                                    - diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/BulkEdit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/BulkEdit.cshtml index eede0613d2..7e1ebc3ba5 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/BulkEdit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/BulkEdit.cshtml @@ -5,6 +5,7 @@ ViewBag.Title = T("Admin.Catalog.BulkEdit").Text; } + @using (Html.BeginForm()) {
                                    @@ -20,193 +21,160 @@ @T("Admin.Catalog.BulkEdit.Info")
                                    - - - - - - - - - +
                                    +
                                    + @Html.SmartLabelFor(model => model.SearchProductName) + @Html.TextBoxFor(model => Model.SearchProductName, new { @class = "form-control" }) +
                                    +
                                    + @Html.SmartLabelFor(model => model.SearchProductTypeId) + @Html.DropDownList("SearchProductTypeId", Model.AvailableProductTypes, allString) +
                                    @if (Model.AvailableStores.Count > 1) { -
                                    - - - +
                                    + @Html.SmartLabelFor(model => model.SearchStoreId) + @Html.DropDownListFor(model => model.SearchStoreId, Model.AvailableStores, T("Admin.Common.All"), new { @class = "form-control" }) +
                                    } - - - - - - - - - - - - -
                                    - @Html.SmartLabelFor(model => model.SearchProductName) - - @Html.EditorFor(model => Model.SearchProductName) -
                                    - @Html.SmartLabelFor(model => model.SearchProductTypeId) - - @Html.DropDownList("SearchProductTypeId", Model.AvailableProductTypes, allString) -
                                    - @Html.SmartLabelFor(model => model.SearchStoreId) - - @Html.DropDownList("SearchStoreId", Model.AvailableStores, allString) -
                                    - @Html.SmartLabelFor(model => model.SearchCategoryId) - - @Html.DropDownList("SearchCategoryId", Model.AvailableCategories, allString) -
                                    - @Html.SmartLabelFor(model => model.SearchManufacturerId) - - @Html.DropDownList("SearchManufacturerId", Model.AvailableManufacturers, allString) -
                                    -   - - -
                                    - -

                                    - - - - - -
                                    - @(Html.Telerik().Grid() - .Name("products-grid") - .DataKeys(keys => - { - keys.Add(p => p.Id); - }) - .ToolBar(commands => - { - commands.SubmitChanges(); - }) - .DataBinding(dataBinding => - dataBinding.Ajax() - .Select("BulkEditSelect", "Product") - .Update("BulkEditSave", "Product") - ) - .Columns(columns => - { - columns.Bound(p => p.Name) - .ReadOnly() - .Template(x => @Html.LabeledProductName(x.Id, x.Name, x.ProductTypeName, x.ProductTypeLabelHint)) - .ClientTemplate(@Html.LabeledProductName("Id", "Name")); - columns.Bound(p => p.Sku) - .Width(150); - columns.Bound(p => p.Price) - .Format("{0:0.00}") - .RightAlign() - .Width(120); - columns.Bound(p => p.OldPrice) - .Format("{0:0.00}") - .RightAlign() - .Width(120); - columns.Bound(p => p.ManageInventoryMethod) - .ReadOnly(); - columns.Bound(p => p.StockQuantity) - .Centered() - .Width(80); - columns.Bound(p => p.Published) - .Width(80) - .Template(item => @Html.SymbolForBool(item.Published)) - .ClientTemplate(@Html.SymbolForBool("Published")) - .Centered(); - columns.Bound(x => x.Id) - .ReadOnly() - .Sortable(false) - .Width(90) - .Centered() - .Template(x => Html.ActionLink(T("Admin.Common.Edit").Text, "Edit", new { id = x.Id })) - .ClientTemplate("\">" + T("Admin.Common.Edit").Text + "") - .Title(T("Admin.Common.Edit").Text); - columns.Command( - commands => commands.Delete().Localize(T) - ).Width(90); - }) - .Sortable(x => - { - x.AllowUnsort(true); - x.SortMode(GridSortMode.SingleColumn); - }) - .ClientEvents(events => events.OnDataBinding("Grid_onDataBinding").OnError("Grid_onError").OnSubmitChanges("Grid_onSubmitChanges")) - .Editable(editing => editing.Mode(GridEditMode.InCell)) - .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) - ) +
                                    + @Html.SmartLabelFor(model => model.SearchCategoryId) + @Html.DropDownList("SearchCategoryId", Model.AvailableCategories, allString) +
                                    +
                                    + @Html.SmartLabelFor(model => model.SearchManufacturerId) + @Html.DropDownList("SearchManufacturerId", Model.AvailableManufacturers, allString) +
                                    +
                                    + + +
                                    + - - function Grid_onSubmitChanges(e) { - //Pass search parameters - e.values["SearchProductName"] = $('#@Html.FieldIdFor(model => model.SearchProductName)').val(); - e.values["SearchStoreId"] = $('#SearchStoreId').val(); - e.values["SearchCategoryId"] = $('#SearchCategoryId').val(); - e.values["SearchManufacturerId"] = $('#SearchManufacturerId').val(); - e.values["SearchProductTypeId"] = $('#SearchProductTypeId').val(); - } - -
                                    } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/BundleItemEditPopup.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/BundleItemEditPopup.cshtml index 575ed984ad..617313103c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/BundleItemEditPopup.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/BundleItemEditPopup.cshtml @@ -16,11 +16,12 @@
                                    @ViewBag.Title
                                    - -
                                    @@ -34,16 +35,10 @@ $('#attribute_@attribute.Id').selectWrapper(); } - - $('#@Html.FieldIdFor(model => model.FilterAttributes)').change(function () { - $('#AttributeFilters').toggle( - $('#@Html.FieldIdFor(model => model.FilterAttributes)').is(':checked') - ); - }).trigger('change'); }); - @Html.SmartStore().TabStrip().Name("bundleitem-edit").Items(x => + @Html.SmartStore().TabStrip().Name("bundleitem-edit").Style(TabsStyle.Material).Items(x => { x.Add().Text(T("Common.General").Text).Content(TabInfo()).Selected(true); x.Add().Text(T("Admin.Catalog.Products.BundleItems.Attributes").Text).Content(TabAttributeFilters()); @@ -61,6 +56,9 @@ @Html.SmartLabelFor(model => model.Locales[item].Name) + @*IMPORTANT: Do not delete, this hidden element contains the id to assign localized values to the corresponding language *@ + @Html.HiddenFor(model => model.Locales[item].LanguageId) + @Html.EditorFor(model => Model.Locales[item].Name) @Html.ValidationMessageFor(model => model.Locales[item].Name) @@ -74,11 +72,6 @@ @Html.ValidationMessageFor(model => model.Locales[item].ShortDescription) - - - @Html.HiddenFor(model => model.Locales[item].LanguageId) - - , @ @@ -176,7 +169,9 @@ @Html.SmartLabelFor(model => model.CreatedOn) @@ -184,7 +179,9 @@ @Html.SmartLabelFor(model => model.UpdatedOn)
                                    - @Html.DisplayFor(model => model.CreatedOn) +
                                    + @Model.CreatedOn +
                                    - @Html.DisplayFor(model => model.UpdatedOn) +
                                    + @Model.UpdatedOn +
                                    @@ -192,64 +189,47 @@ @helper TabAttributeFilters() { -
                                    - - - - - -
                                    - @Html.SmartLabelFor(model => model.FilterAttributes) - - @Html.EditorFor(model => model.FilterAttributes) - @Html.ValidationMessageFor(model => model.FilterAttributes) -
                                    -
                                    - -
                                    - + if (Model.Attributes.Count == 0) + { +
                                    + @T("Admin.Catalog.Products.BundleItems.Fields.FilterAttributes.NoneNote") +
                                    + return; + } +
                                    + + + + + @foreach (var attribute in Model.Attributes) { - if(Model.Attributes.IndexOf(attribute) != 0) - { - - - - } if (attribute.PreSelect.Count > 0) { } } -
                                    + @Html.SmartLabelFor(model => model.FilterAttributes) + + @Html.CheckBoxFor(model => model.FilterAttributes, new { data_toggler_for = "#AttributeFilters" }) + @Html.ValidationMessageFor(model => model.FilterAttributes) +
                                    -
                                    -
                                    -
                                    - -
                                    + @Html.SmartLabel(attribute.AttributeControlId, attribute.Name)
                                    - @Html.DropDownList(attribute.AttributeControlId, attribute.Values, null, new { multiple = "multiple" }) + @Html.DropDownList(attribute.AttributeControlId, attribute.Values, null, new { multiple = "multiple", @class = "form-control" })
                                    -
                                    - - @Html.Hint(T("Admin.Catalog.Products.BundleItems.Fields.FilterPreSelect.Hint")) -
                                    + @Html.SmartLabel(attribute.PreSelectControlId, T("Admin.Catalog.Products.BundleItems.Fields.FilterPreSelect"), T("Admin.Catalog.Products.BundleItems.Fields.FilterPreSelect.Hint"))
                                    - @Html.DropDownList(attribute.PreSelectControlId, attribute.PreSelect) + @Html.DropDownList(attribute.PreSelectControlId, attribute.PreSelect, new { @class = "form-control" })
                                    - @if (Model.Attributes.Count == 0) - { -
                                    - @T("Admin.Catalog.Products.BundleItems.Fields.FilterAttributes.NoneNote") -
                                    - } -
                                    + + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/Create.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/Create.cshtml index 56625618c2..fae1eea678 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/Create.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/Create.cshtml @@ -1,6 +1,5 @@ @model ProductModel @{ - //page title ViewBag.Title = T("Admin.Catalog.Products.AddNew").Text; } @using (Html.BeginForm(null, null, FormMethod.Post, new { id = "product-form" })) @@ -13,10 +12,10 @@
                                    -
                                    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/Edit.cshtml index e15fc2c17a..acdf321841 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/Edit.cshtml @@ -16,23 +16,24 @@
                                    @{ Html.RenderWidget("admin_button_toolbar_before"); } - - - - -  @T("Admin.Catalog.Products.Copy") + + + @T("Admin.Catalog.Products.Copy") @{ Html.RenderWidget("admin_button_toolbar_after"); } @@ -43,56 +44,58 @@ } @Html.DeleteConfirmation("product-delete") -@*copy product form*@ @{Html.SmartStore().Window() .Name("copyproduct-window") - .Title(T("Admin.Catalog.Products.Copy").Text) + .Title(T("Admin.Catalog.Products.Copy")) + .Size(WindowSize.Large) .Content(@ - @using (Html.BeginForm("CopyProduct", "Product", FormMethod.Post, new { style = "margin:0;" })) { - @Html.HiddenFor(model => Model.CopyProductModel.Id) - -
                                    - - - - - - - - - - - - - -
                                    - @Html.SmartLabelFor(model => model.CopyProductModel.Name) - - @Html.EditorFor(model => Model.CopyProductModel.Name) -
                                    - @Html.SmartLabelFor(model => model.CopyProductModel.Published) - - @Html.EditorFor(model => Model.CopyProductModel.Published) -
                                    - @Html.SmartLabelFor(model => model.CopyProductModel.CopyImages) - - @Html.EditorFor(model => Model.CopyProductModel.CopyImages) -
                                    -
                                    - } -
                                    ) - .FooterContent(@ - - ) - .Width(500) - .Height(500) - .Modal(true) - .Visible(false) - .Render(); +
                                    + @Html.HiddenFor(model => Model.CopyProductModel.Id) + + + + + + + + + + + + + + + + + +
                                    + @Html.SmartLabelFor(model => model.CopyProductModel.NumberOfCopies) + + @Html.EditorFor(model => Model.CopyProductModel.NumberOfCopies) +
                                    + @Html.SmartLabelFor(model => model.CopyProductModel.Name) + + @Html.EditorFor(model => Model.CopyProductModel.Name) +
                                    + @Html.SmartLabelFor(model => model.CopyProductModel.Published) + + @Html.EditorFor(model => Model.CopyProductModel.Published) +
                                    + @Html.SmartLabelFor(model => model.CopyProductModel.CopyImages) + + @Html.EditorFor(model => Model.CopyProductModel.CopyImages) +
                                    +
                                    + ) + .FooterContent(@ + + ) + .Render(); } + -@* Copy attribute options dialog. Must not be placed inside another form. *@ @if (Model.Id != 0 && Model.NumberOfAvailableProductAttributes > 0) { - diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/EditorTemplates/AttributeControlType.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/EditorTemplates/AttributeControlType.cshtml index a2494c300f..4149706492 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/EditorTemplates/AttributeControlType.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/EditorTemplates/AttributeControlType.cshtml @@ -3,7 +3,7 @@ var controlTypeList = AttributeControlType.DropdownList.ToSelectList(); } - @foreach (var item in controlTypeList) { diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/EditorTemplates/ProductAttribute.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/EditorTemplates/ProductAttribute.cshtml index 4e3d7d0def..5f43c5e07d 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/EditorTemplates/ProductAttribute.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/EditorTemplates/ProductAttribute.cshtml @@ -1,8 +1,7 @@ - - diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/EditorTemplates/ProductCategory.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/EditorTemplates/ProductCategory.cshtml index 6c61bf7d44..887c399a23 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/EditorTemplates/ProductCategory.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/EditorTemplates/ProductCategory.cshtml @@ -1,5 +1,8 @@ - - + - - - -} \ No newline at end of file +
                                    + @(Html.Telerik().Grid() + .Name("product-tags-grid") + .DataKeys(x => { x.Add(y => y.Id).RouteKey("Id"); }) + .Columns(columns => + { + columns.Bound(x => x.Name); + columns.Bound(x => x.ProductCount) + .Centered() + .Width(150); + columns.Command(commands => + { + commands.Custom("edit-product-tag").Text(T("Common.Edit")); + commands.Delete().Localize(T); + }) + .Width(90) + .HtmlAttributes(new { align = "right" }); + }) + .DataBinding(dataBinding => + { + dataBinding.Ajax() + .Select("ProductTags", "Product") + .Delete("ProductTagDelete", "Product"); + }) + .Filterable() + .Pageable(settings => settings.PageSize(gridPageSize).Position(GridPagerPosition.Both)) + .PreserveGridState() + .EnableCustomBinding(true)) + + +
                                    +} + + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Acl.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Acl.cshtml index 873a7becf4..8c8c81a5ae 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Acl.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Acl.cshtml @@ -1,50 +1,3 @@ @model ProductModel - - - - - - - - - - -
                                    - @Html.SmartLabelFor(model => model.SubjectToAcl) - - @Html.EditorFor(model => model.SubjectToAcl) - @Html.ValidationMessageFor(model => model.SubjectToAcl) -
                                    - @Html.SmartLabelFor(model => model.AvailableCustomerRoles) - - @if (Model.AvailableCustomerRoles != null && Model.AvailableCustomerRoles.Count > 0) - { - foreach (var customerRole in Model.AvailableCustomerRoles) - { -
                                    - -
                                    - } - } - else - { -
                                    No customer roles defined
                                    - } -
                                    +@Html.Partial("AclSelector", Model) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.AssociatedProducts.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.AssociatedProducts.cshtml index 16c4417235..7db058511c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.AssociatedProducts.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.AssociatedProducts.cshtml @@ -5,95 +5,84 @@ @if (Model.Id > 0) {
                                    -
                                    - @T("Admin.Catalog.Products.AssociatedProducts.Note1") - @T("Admin.Catalog.Products.AssociatedProducts.Note2") -
                                    +
                                      +
                                    • @T("Admin.Catalog.Products.AssociatedProducts.Note1")
                                    • +
                                    • @T("Admin.Catalog.Products.AssociatedProducts.Note2")
                                    • +
                                    -

                                    +
                                    + @(Html.Telerik().Grid() + .Name("associatedproducts-grid") + .DataKeys(keys => + { + keys.Add(x => x.Id); + }) + .DataBinding(dataBinding => + { + dataBinding.Ajax() + .Select("AssociatedProductList", "Product", new { productId = Model.Id }) + .Update("AssociatedProductUpdate", "Product") + .Delete("AssociatedProductDelete", "Product"); + }) + .Columns(columns => + { + columns.Bound(x => x.ProductName) + .ReadOnly() + .Width("40%") + .ClientTemplate(@Html.LabeledProductName("Id", "ProductName")); + columns.Bound(x => x.Sku) + .Width("20%") + .ReadOnly(); + columns.Bound(x => x.Published) + .ReadOnly() + .Width("10%") + .ClientTemplate(@Html.SymbolForBool("Published")) + .Centered(); + columns.Bound(x => x.DisplayOrder) + .Width("10%") + .Centered(); + columns.Command(commands => + { + commands.Edit().Localize(T); + commands.Delete().Localize(T); + }) + .HtmlAttributes(new { align = "right" }) + .Width("20%"); + }) + .ToolBar(commands => commands.Template(GridCommands)) + .EnableCustomBinding(true)) +
                                    - - - - - - - -
                                    - @(Html.Telerik().Grid() - .Name("associatedproducts-grid") - .DataKeys(keys => - { - keys.Add(x => x.Id); - }) - .DataBinding(dataBinding => - { - dataBinding.Ajax() - .Select("AssociatedProductList", "Product", new { productId = Model.Id }) - .Update("AssociatedProductUpdate", "Product") - .Delete("AssociatedProductDelete", "Product"); - }) - .Columns(columns => - { - columns.Bound(x => x.ProductName) - .ReadOnly() - .Width(520) - .ClientTemplate(@Html.LabeledProductName("Id", "ProductName")); - columns.Bound(x => x.Sku) - .ReadOnly(); - columns.Bound(x => x.Published) - .ReadOnly() - .ClientTemplate(@Html.SymbolForBool("Published")) - .Centered(); - columns.Bound(x => x.DisplayOrder) - .Centered(); - columns.Command(commands => - { - commands.Edit().Localize(T); - commands.Delete().Localize(T); - }).Width(220); - }) - .EnableCustomBinding(true)) -
                                    - - - -
                                    } else { - @T("Admin.Catalog.Products.AssociatedProducts.SaveBeforeEdit") -} \ No newline at end of file +

                                    + @T("Admin.Catalog.Products.AssociatedProducts.SaveBeforeEdit") +

                                    +} + +@helper GridCommands(Grid grid) +{ + @(Html.SmartStore().EntityPicker() + .IconCssClass("fa fa-plus") + .DisableGroupedProducts(true) + .HtmlAttribute("class", "t-button t-button-primary") + .DialogTitle(T("Admin.Catalog.Products.AssociatedProducts.AddNew").Text.EncodeJsString('\'', false)) + .Caption(T("Admin.Catalog.Products.AssociatedProducts.AddNew").Text.EncodeJsString('\'', false)) + .OnSelectionCompleted("AssociatedProducts_Selected")) +} + + \ No newline at end of file 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 dbac115756..e3e2ff4eb5 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Attributes.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Attributes.cshtml @@ -3,18 +3,22 @@ @using Telerik.Web.Mvc.UI;
                                    -

                                    @T("Admin.Catalog.Products.ProductVariantAttributes.Attributes")

                                    +

                                    @T("Admin.Catalog.Products.ProductVariantAttributes.Attributes")

                                    @if (Model.Id > 0) { if (Model.NumberOfAvailableProductAttributes > 0) { - @@ -205,7 +190,7 @@ .Centered(); columns.Bound(x => x.AllowOutOfStockOrders) .Width(60) - .HeaderTemplate("") + .HeaderTemplate("") .Template(item => @Html.SymbolForBool(item.AllowOutOfStockOrders)) .ClientTemplate(@Html.SymbolForBool("AllowOutOfStockOrders")) .Centered(); @@ -218,6 +203,7 @@ { commands.Delete().Localize(T); }) + .HtmlAttributes(new { align = "right" }) .Width(90); }) .Pageable(settings => settings.PageSize(100).Position(GridPagerPosition.Both)) @@ -228,15 +214,9 @@ .Delete("ProductVariantAttributeCombinationDelete", "Product"); }) .ClientEvents(events => events.OnRowDataBound("onRowDataBound_Combinations")) + .ToolBar(commands => commands.Template(AttributeCombinationsGridCommands)) .EnableCustomBinding(true))
                                    - -
                                    - -
                                    } else { @@ -252,3 +232,22 @@
                                    } + +@helper AttributeCombinationsGridCommands(Grid grid) +{ + + + + + +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.BundleItems.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.BundleItems.cshtml index dbc420e577..7b742117d3 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.BundleItems.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.BundleItems.cshtml @@ -4,19 +4,10 @@ @if (Model.Id > 0) { - - -
                                    - +
                                    @T("Admin.Catalog.Products.BundleItems.NotesOnProductBundles"): - @T("Common.Show") + @T("Common.Show")
                                    @Html.Raw(T("Admin.Catalog.Products.BundleItems.AdminNoteGeneral")) @@ -28,6 +19,7 @@ @Html.Raw(T("Admin.Catalog.Products.BundleItems.AdminNotePerItemShipping")) }
                                    +
                                    @@ -38,6 +30,9 @@ @Html.SmartLabelFor(model => model.Locales[item].BundleTitleText) + @*IMPORTANT: Do not delete, this hidden element contains the id to assign localized values to the corresponding language *@ + @Html.HiddenFor(model => model.Locales[item].LanguageId) + @Html.EditorFor(model => model.Locales[item].BundleTitleText) @Html.ValidationMessageFor(model => model.Locales[item].BundleTitleText) @@ -88,112 +83,111 @@
                                    -

                                    - - - - - - - - -
                                    - @(Html.Telerik().Grid() - .Name("bundleitems-grid") - .DataKeys(keys => - { - keys.Add(x => x.Id); - }) - .DataBinding(dataBinding => - { - dataBinding.Ajax() - .Select("BundleItemList", "Product", new { productId = Model.Id }) - .Delete("BundleItemDelete", "Product"); - }) - .Columns(columns => - { - columns.Bound(x => x.ProductName) - .ReadOnly() - .Width(520) - .ClientTemplate(@Html.LabeledProductName("ProductId", "ProductName")); - columns.Bound(x => x.Sku) - .ReadOnly(); - columns.Bound(x => x.Quantity) - .Centered(); - columns.Bound(x => x.Discount) - .Centered(); - columns.Bound(x => x.Visible) - .ClientTemplate(@Html.SymbolForBool("Visible")) - .Centered(); - columns.Bound(x => x.Published) - .ClientTemplate(@Html.SymbolForBool("Published")) - .Centered(); - columns.Bound(x => x.DisplayOrder) - .Centered(); - columns.Bound(x => x.Id) - .Centered() - .ClientTemplate("") - .Title(""); - columns.Command(commands => - { - commands.Delete().Localize(T); - }); - }) - .ClientEvents(events => events.OnRowDataBound("onRowDataBound_bundleItems")) - .EnableCustomBinding(true)) -
                                    - - - -
                                    - - } else { - @T("Admin.Catalog.Products.BundleItems.SaveBeforeEdit") +

                                    + @T("Admin.Catalog.Products.BundleItems.SaveBeforeEdit") +

                                    +} + +@helper GridCommands(Grid grid) +{ + @(Html.SmartStore().EntityPicker() + .HtmlAttribute("class", "t-button t-button-primary") + .DialogTitle(T("Admin.Catalog.Products.BundleItems.AddNew").Text.EncodeJsString('\'', false)) + .Caption(T("Common.AddNew").Text.EncodeJsString('\'', false)) + .DisableBundleProducts(true) + .DisableGroupedProducts(true) + .IconCssClass("fa fa-plus") + .OnSelectionCompleted("EntPicker_Completed")) + + } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Categories.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Categories.cshtml index 5e758d0e85..a579c658ad 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Categories.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Categories.cshtml @@ -8,7 +8,11 @@ { @(Html.Telerik().Grid() - .Name("productcategories-grid") - .DataKeys(keys => - { - keys.Add(x => x.Id); - }) - .DataBinding(dataBinding => - { - dataBinding.Ajax() - .Select("ProductCategoryList", "Product", new { productId = Model.Id }) - .Insert("ProductCategoryInsert", "Product", new { productId = Model.Id }) - .Update("ProductCategoryUpdate", "Product") - .Delete("ProductCategoryDelete", "Product"); - }) - .Columns(columns => - { - columns.Bound(x => x.Category) + .Name("productcategories-grid") + .DataKeys(keys => + { + keys.Add(x => x.Id); + }) + .DataBinding(dataBinding => + { + dataBinding.Ajax() + .Select("ProductCategoryList", "Product", new { productId = Model.Id }) + .Insert("ProductCategoryInsert", "Product", new { productId = Model.Id }) + .Update("ProductCategoryUpdate", "Product") + .Delete("ProductCategoryDelete", "Product"); + }) + .Columns(columns => + { + columns.Bound(x => x.Category) + .Width("60%") .Template(x => Html.ActionLink(x.Category, "Edit", "Category", new { id = x.CategoryId })) .ClientTemplate("\"><#= Category #>"); - columns.Bound(x => x.IsFeaturedProduct) - .Width(100) - .Template(item => @Html.SymbolForBool(item.IsFeaturedProduct)) - .ClientTemplate(@Html.SymbolForBool("IsFeaturedProduct")) - .Centered(); + columns.Bound(x => x.IsFeaturedProduct) + .Width(100) + .Template(item => @Html.SymbolForBool(item.IsFeaturedProduct)) + .ClientTemplate(@Html.SymbolForBool("IsFeaturedProduct")) + .Centered(); columns.Bound(x => x.DisplayOrder) .Centered() - .Width(100); - columns.Command(commands => - { - commands.Edit().Localize(T); - commands.Delete().Localize(T); - }) - .Width(180); - }) - .ToolBar(commands => commands.Insert()) - .ClientEvents(events => { + .Width(100); + columns.Command(commands => + { + commands.Edit().Localize(T); + commands.Delete().Localize(T); + }) + .HtmlAttributes(new { align = "right" }); + }) + .ToolBar(commands => commands.Insert()) + .ClientEvents(events => { events.OnEdit("onProductCategoryEdit"); events.OnDataBound("onProductCategoryDataBound"); }) - .EnableCustomBinding(true)) + .EnableCustomBinding(true)) } else { diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Discounts.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Discounts.cshtml index 1e9e472856..4ca1174d22 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Discounts.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Discounts.cshtml @@ -2,17 +2,20 @@ @if (Model.AvailableDiscounts != null && Model.AvailableDiscounts.Count > 0) { - foreach (var discount in Model.AvailableDiscounts) - { - - } + foreach (var discount in Model.AvailableDiscounts) + { +
                                    + + +
                                    + } } else { -
                                    +
                                    @T("Admin.Promotions.Discounts.NoDiscountsAvailable")
                                    } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Info.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Info.cshtml index bddc71b5ad..12b9b8d6e5 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Info.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Info.cshtml @@ -2,58 +2,11 @@ @using SmartStore.Core.Domain.Catalog; -@{ - var availableTags = Model.AvailableProductTags.Select(x => x.Value).ToArray(); - for (int i = 0; i < availableTags.Length; i++) - { - availableTags[i] = availableTags[i].EncodeJsString(); - } - ViewBag.SerializedTags = String.Join(",", availableTags); -} - - -
                                    +
                                    @if (Model.Id != 0) { @@ -201,7 +45,7 @@ @Html.SmartLabelFor(model => model.Id) @@ -210,7 +54,9 @@ @Html.SmartLabelFor(model => model.ProductUrl) } @@ -239,7 +85,9 @@ @Html.SmartLabelFor(model => model.AssociatedToProductId) } @@ -263,7 +111,10 @@ @Html.SmartLabelFor(model => model.Locales[item].Name) @@ -272,7 +123,7 @@ @Html.SmartLabelFor(model => model.Locales[item].ShortDescription) @@ -280,16 +131,11 @@ - - - -
                                    - @Html.DisplayFor(model => model.Id) + @Html.TextBoxFor(model => model.Id, new { @readonly = "readonly", @class = "form-control-plaintext" }) @Html.ValidationMessageFor(model => model.Id)
                                    - @Model.ProductUrl +
                                    - @Html.ActionLink(Model.AssociatedToProductName, "Edit", "Product", new { id = Model.AssociatedToProductId }, new { }) +
                                    + @Html.ActionLink(Model.AssociatedToProductName, "Edit", "Product", new { id = Model.AssociatedToProductId }, new { }) +
                                    - @Html.TextBoxFor(model => Model.Locales[item].Name, new { @class = "input-large" }) + @*IMPORTANT: Do not delete, this hidden element contains the id to assign localized values to the corresponding language *@ + @Html.HiddenFor(model => model.Locales[item].LanguageId) + + @Html.TextBoxFor(model => Model.Locales[item].Name) @Html.ValidationMessageFor(model => model.Locales[item].Name)
                                    - @Html.TextAreaFor(model => model.Locales[item].ShortDescription, new { @class = "input-large" }) + @Html.TextAreaFor(model => model.Locales[item].ShortDescription) @Html.ValidationMessageFor(model => model.Locales[item].ShortDescription)
                                    @Html.SmartLabelFor(model => model.Locales[item].FullDescription) - @Html.EditorFor(model => model.Locales[item].FullDescription, Html.RichEditorFlavor()) + + @Html.EditorFor(model => model.Locales[item].FullDescription, "Html") @Html.ValidationMessageFor(model => model.Locales[item].FullDescription)
                                    - @Html.HiddenFor(model => model.Locales[item].LanguageId) -
                                    , @ @@ -298,7 +144,7 @@ @Html.SmartLabelFor(model => model.Name) @@ -307,7 +153,7 @@ @Html.SmartLabelFor(model => model.ShortDescription) @@ -315,13 +161,13 @@ -
                                    - @Html.TextBoxFor(model => model.Name, new { @class = "input-large" }) + @Html.TextBoxFor(model => model.Name) @Html.ValidationMessageFor(model => model.Name)
                                    - @Html.TextAreaFor(x => x.ShortDescription, new { @class = "input-large" }) + @Html.TextAreaFor(x => x.ShortDescription) @Html.ValidationMessageFor(model => model.ShortDescription)
                                    @Html.SmartLabelFor(model => model.FullDescription) - @Html.EditorFor(x => x.FullDescription, Html.RichEditorFlavor()) + + @Html.EditorFor(x => x.FullDescription, "Html") @Html.ValidationMessageFor(model => model.FullDescription)
                                    - )) + )) @@ -383,7 +229,7 @@ @Html.SmartLabelFor(model => model.ProductTags) @@ -401,7 +247,7 @@ @Html.SmartLabelFor(model => model.AvailableEndDateTimeUtc) @@ -419,7 +265,7 @@ @Html.SmartLabelFor(model => model.ShowOnHomePage) @@ -448,7 +294,7 @@ @Html.SmartLabelFor(model => model.CreatedOn) } @@ -459,7 +305,7 @@ @Html.SmartLabelFor(model => model.UpdatedOn) } @@ -476,33 +322,38 @@ @Html.SmartLabelFor(model => model.RequireOtherProducts) - - - - - - - - + + + + + + + + + +
                                    - @Html.HiddenFor(model => model.ProductTags, new { @class = "tag-chooser" }) + @Html.ListBoxFor(model => model.ProductTags, Model.AvailableProductTags, new { multiple = "multiple", data_tags = "true" }) @Html.ValidationMessageFor(model => model.ProductTags)
                                    - @Html.EditorFor(model => model.AvailableEndDateTimeUtc) + @Html.EditorFor(model => model.AvailableEndDateTimeUtc, new { pickTime = true }) @Html.ValidationMessageFor(model => model.AvailableEndDateTimeUtc)
                                    - @Html.EditorFor(model => model.ShowOnHomePage) + @Html.CheckBoxFor(model => model.ShowOnHomePage, new { data_toggler_for = "#pnlHomePageDisplayOrder" }) @Html.ValidationMessageFor(model => model.ShowOnHomePage)
                                    - @Html.DisplayFor(model => model.CreatedOn) + @Html.TextBoxFor(model => model.CreatedOn, new { @readonly = "readonly", @class = "form-control-plaintext" })
                                    - @Html.DisplayFor(model => model.UpdatedOn) + @Html.TextBoxFor(model => model.UpdatedOn, new { @readonly = "readonly", @class = "form-control-plaintext" })
                                    - @Html.EditorFor(model => model.RequireOtherProducts) + @Html.CheckBoxFor(model => model.RequireOtherProducts, new { data_toggler_for = "#pnlRequireOtherProducts" }) @Html.ValidationMessageFor(model => model.RequireOtherProducts)
                                    - @Html.SmartLabelFor(model => model.RequiredProductIds) - - @Html.EditorFor(model => model.RequiredProductIds) - - - - @Html.ValidationMessageFor(model => model.RequiredProductIds) -
                                    - @Html.SmartLabelFor(model => model.AutomaticallyAddRequiredProducts) - - @Html.EditorFor(model => model.AutomaticallyAddRequiredProducts) - @Html.ValidationMessageFor(model => model.AutomaticallyAddRequiredProducts) -
                                    + @Html.SmartLabelFor(model => model.RequiredProductIds) + +
                                    + @Html.EditorFor(model => model.RequiredProductIds) + + @(Html.SmartStore().EntityPicker() + .For(x => x.RequiredProductIds) + .DialogTitle(T("Admin.Catalog.Products.Fields.RequireOtherProducts").Text.EncodeJsString('\'', false)) + .Caption(T("Admin.Common.Search").Text.EncodeJsString('\'', false))) + +
                                    + @Html.ValidationMessageFor(model => model.RequiredProductIds) +
                                    + @Html.SmartLabelFor(model => model.AutomaticallyAddRequiredProducts) + + @Html.EditorFor(model => model.AutomaticallyAddRequiredProducts) + @Html.ValidationMessageFor(model => model.AutomaticallyAddRequiredProducts) +
                                    @@ -516,7 +367,7 @@ @Html.SmartLabelFor(model => model.IsGiftCard) @@ -542,91 +393,93 @@ @Html.SmartLabelFor(model => model.IsDownload) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                                    - @Html.EditorFor(model => model.IsGiftCard) + @Html.CheckBoxFor(model => model.IsGiftCard, new { data_toggler_for = "#pnlGiftCardType" }) @Html.ValidationMessageFor(model => model.IsGiftCard)
                                    - @Html.EditorFor(model => model.IsDownload) + @Html.CheckBoxFor(model => model.IsDownload, new { data_toggler_for = "#pnlIsDownload" }) @Html.ValidationMessageFor(model => model.IsDownload)
                                    - @Html.SmartLabelFor(model => model.DownloadId) - - @Html.EditorFor(model => model.DownloadId) - @Html.ValidationMessageFor(model => model.DownloadId) -
                                    - @Html.SmartLabelFor(model => model.UnlimitedDownloads) - - @Html.EditorFor(model => model.UnlimitedDownloads) - @Html.ValidationMessageFor(model => model.UnlimitedDownloads) -
                                    - @Html.SmartLabelFor(model => model.MaxNumberOfDownloads) - - @Html.EditorFor(model => model.MaxNumberOfDownloads) - @Html.ValidationMessageFor(model => model.MaxNumberOfDownloads) -
                                    - @Html.SmartLabelFor(model => model.DownloadExpirationDays) - - @Html.TextBoxFor(model => model.DownloadExpirationDays) - @Html.ValidationMessageFor(model => model.DownloadExpirationDays) -
                                    - @Html.SmartLabelFor(model => model.DownloadActivationTypeId) - - @Html.DropDownListFor(model => model.DownloadActivationTypeId, ((DownloadActivationType)Model.DownloadActivationTypeId).ToSelectList()) - @Html.ValidationMessageFor(model => model.DownloadActivationTypeId) -
                                    - @Html.SmartLabelFor(model => model.HasUserAgreement) - - @Html.EditorFor(model => model.HasUserAgreement) - @Html.ValidationMessageFor(model => model.HasUserAgreement) -
                                    - @Html.SmartLabelFor(model => model.UserAgreementText) - - @Html.EditorFor(model => model.UserAgreementText, Html.RichEditorFlavor()) - @Html.ValidationMessageFor(model => model.UserAgreementText) -
                                    - @Html.SmartLabelFor(model => model.HasSampleDownload) - - @Html.EditorFor(model => model.HasSampleDownload) - @Html.ValidationMessageFor(model => model.HasSampleDownload) -
                                    - @Html.SmartLabelFor(model => model.SampleDownloadId) - - @Html.EditorFor(model => model.SampleDownloadId) - @Html.ValidationMessageFor(model => model.SampleDownloadId) -
                                    + @Html.SmartLabelFor(model => model.DownloadId) + + @Html.EditorFor(model => model.DownloadId) + @Html.ValidationMessageFor(model => model.DownloadId) +
                                    + @Html.SmartLabelFor(model => model.UnlimitedDownloads) + + @Html.CheckBoxFor(model => model.UnlimitedDownloads, new { data_toggler_for = "#pnlMaxNumberOfDownloads", data_toggler_reverse = true }) + @Html.ValidationMessageFor(model => model.UnlimitedDownloads) +
                                    + @Html.SmartLabelFor(model => model.MaxNumberOfDownloads) + + @Html.EditorFor(model => model.MaxNumberOfDownloads) + @Html.ValidationMessageFor(model => model.MaxNumberOfDownloads) +
                                    + @Html.SmartLabelFor(model => model.DownloadExpirationDays) + + @Html.EditorFor(model => model.DownloadExpirationDays) + @Html.ValidationMessageFor(model => model.DownloadExpirationDays) +
                                    + @Html.SmartLabelFor(model => model.DownloadActivationTypeId) + + @Html.DropDownListFor(model => model.DownloadActivationTypeId, ((DownloadActivationType)Model.DownloadActivationTypeId).ToSelectList()) + @Html.ValidationMessageFor(model => model.DownloadActivationTypeId) +
                                    + @Html.SmartLabelFor(model => model.HasUserAgreement) + + @Html.CheckBoxFor(model => model.HasUserAgreement, new { data_toggler_for = "#pnlUserAgreementText" }) + @Html.ValidationMessageFor(model => model.HasUserAgreement) +
                                    + @Html.SmartLabelFor(model => model.UserAgreementText) + + @Html.EditorFor(model => model.UserAgreementText, "Html") + @Html.ValidationMessageFor(model => model.UserAgreementText) +
                                    + @Html.SmartLabelFor(model => model.HasSampleDownload) + + @Html.CheckBoxFor(model => model.HasSampleDownload, new { data_toggler_for = "#pnlSampleDownloadFile" }) + @Html.ValidationMessageFor(model => model.HasSampleDownload) +
                                    + @Html.SmartLabelFor(model => model.SampleDownloadId) + + @Html.EditorFor(model => model.SampleDownloadId) + @Html.ValidationMessageFor(model => model.SampleDownloadId) +
                                    @@ -640,37 +493,39 @@ @Html.SmartLabelFor(model => model.IsRecurring) - - - - - - - - - - - - + + + + + + + + + + + + + +
                                    - @Html.EditorFor(model => model.IsRecurring) + @Html.CheckBoxFor(model => model.IsRecurring, new { data_toggler_for = "#pnlIsRecurring" }) @Html.ValidationMessageFor(model => model.IsRecurring)
                                    - @Html.SmartLabelFor(model => model.RecurringCycleLength) - - @Html.EditorFor(model => model.RecurringCycleLength) - @Html.ValidationMessageFor(model => model.RecurringCycleLength) -
                                    - @Html.SmartLabelFor(model => model.RecurringCyclePeriodId) - - @Html.DropDownListFor(model => model.RecurringCyclePeriodId, ((RecurringProductCyclePeriod)Model.RecurringCyclePeriodId).ToSelectList()) - @Html.ValidationMessageFor(model => model.RecurringCyclePeriodId) -
                                    - @Html.SmartLabelFor(model => model.RecurringTotalCycles) - - @Html.EditorFor(model => model.RecurringTotalCycles) - @Html.ValidationMessageFor(model => model.RecurringTotalCycles) -
                                    + @Html.SmartLabelFor(model => model.RecurringCycleLength) + + @Html.EditorFor(model => model.RecurringCycleLength) + @Html.ValidationMessageFor(model => model.RecurringCycleLength) +
                                    + @Html.SmartLabelFor(model => model.RecurringCyclePeriodId) + + @Html.DropDownListFor(model => model.RecurringCyclePeriodId, ((RecurringProductCyclePeriod)Model.RecurringCyclePeriodId).ToSelectList()) + @Html.ValidationMessageFor(model => model.RecurringCyclePeriodId) +
                                    + @Html.SmartLabelFor(model => model.RecurringTotalCycles) + + @Html.EditorFor(model => model.RecurringTotalCycles) + @Html.ValidationMessageFor(model => model.RecurringTotalCycles) +
                                    @@ -684,82 +539,84 @@ @Html.SmartLabelFor(model => model.IsShipEnabled) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                                    - @Html.EditorFor(model => model.IsShipEnabled) + @Html.CheckBoxFor(model => model.IsShipEnabled, new { data_toggler_for = "#pnlIsShipEnabled" }) @Html.ValidationMessageFor(model => model.IsShipEnabled)
                                    - @Html.SmartLabelFor(model => model.DeliveryTimeId) - - @Html.DropDownListFor(model => model.DeliveryTimeId, Model.AvailableDeliveryTimes, T("Common.Unspecified")) - @Html.ValidationMessageFor(model => model.DeliveryTimeId) -
                                    - @Html.SmartLabelFor(model => model.IsFreeShipping) - - @Html.EditorFor(model => model.IsFreeShipping) - @Html.ValidationMessageFor(model => model.IsFreeShipping) -
                                    - @Html.SmartLabelFor(model => model.AdditionalShippingCharge) - - @Html.EditorFor(model => model.AdditionalShippingCharge) @Model.PrimaryStoreCurrencyCode - @Html.ValidationMessageFor(model => model.AdditionalShippingCharge) -
                                    - @Html.SmartLabelFor(model => model.QuantityUnitId) - - @Html.DropDownListFor(model => model.QuantityUnitId, Model.AvailableQuantityUnits, T("Common.Unspecified")) - @Html.ValidationMessageFor(model => model.QuantityUnitId) -
                                    - @Html.SmartLabelFor(model => model.Weight) - - @Html.EditorFor(model => model.Weight) @Model.BaseWeightIn - @Html.ValidationMessageFor(model => model.Weight) -
                                    - @Html.SmartLabelFor(model => model.Length) - - @Html.EditorFor(model => model.Length) @Model.BaseDimensionIn - @Html.ValidationMessageFor(model => model.Length) -
                                    - @Html.SmartLabelFor(model => model.Width) - - @Html.EditorFor(model => model.Width) @Model.BaseDimensionIn - @Html.ValidationMessageFor(model => model.Width) -
                                    - @Html.SmartLabelFor(model => model.Height) - - @Html.EditorFor(model => model.Height) @Model.BaseDimensionIn - @Html.ValidationMessageFor(model => model.Height) -
                                    + @Html.SmartLabelFor(model => model.DeliveryTimeId) + + @Html.DropDownListFor(model => model.DeliveryTimeId, Model.AvailableDeliveryTimes, T("Common.Unspecified")) + @Html.ValidationMessageFor(model => model.DeliveryTimeId) +
                                    + @Html.SmartLabelFor(model => model.IsFreeShipping) + + @Html.EditorFor(model => model.IsFreeShipping) + @Html.ValidationMessageFor(model => model.IsFreeShipping) +
                                    + @Html.SmartLabelFor(model => model.AdditionalShippingCharge) + + @Html.EditorFor(model => model.AdditionalShippingCharge, new { postfix = Model.PrimaryStoreCurrencyCode }) + @Html.ValidationMessageFor(model => model.AdditionalShippingCharge) +
                                    + @Html.SmartLabelFor(model => model.QuantityUnitId) + + @Html.DropDownListFor(model => model.QuantityUnitId, Model.AvailableQuantityUnits, T("Common.Unspecified")) + @Html.ValidationMessageFor(model => model.QuantityUnitId) +
                                    + @Html.SmartLabelFor(model => model.Weight) + + @Html.EditorFor(model => model.Weight, new { postfix = Model.BaseWeightIn }) + @Html.ValidationMessageFor(model => model.Weight) +
                                    + @Html.SmartLabelFor(model => model.Length) + + @Html.EditorFor(model => model.Length, new { postfix = Model.BaseDimensionIn }) + @Html.ValidationMessageFor(model => model.Length) +
                                    + @Html.SmartLabelFor(model => model.Width) + + @Html.EditorFor(model => model.Width, new { postfix = Model.BaseDimensionIn }) + @Html.ValidationMessageFor(model => model.Width) +
                                    + @Html.SmartLabelFor(model => model.Height) + + @Html.EditorFor(model => model.Height, new { postfix = Model.BaseDimensionIn }) + @Html.ValidationMessageFor(model => model.Height) +
                                    @@ -773,34 +630,29 @@ @Html.SmartLabelFor(model => model.IsTaxExempt) - - - - - - - - + + + + + + + + + +
                                    - @Html.EditorFor(model => model.IsTaxExempt) + @Html.CheckBoxFor(model => model.IsTaxExempt, new { data_toggler_for = "#pnlIsTaxExempt" }) @Html.ValidationMessageFor(model => model.IsTaxExempt)
                                    - @Html.SmartLabelFor(model => model.TaxCategoryId) - - @Html.DropDownListFor(model => model.TaxCategoryId, Model.AvailableTaxCategories, new { placeholder = T("Common.PleaseSelect").Text }) - @Html.ValidationMessageFor(model => model.TaxCategoryId) -
                                    - @Html.SmartLabelFor(model => model.IsEsd) - - @Html.EditorFor(model => model.IsEsd) - @Html.ValidationMessageFor(model => model.IsEsd) -
                                    + @Html.SmartLabelFor(model => model.TaxCategoryId) + + @Html.DropDownListFor(model => model.TaxCategoryId, Model.AvailableTaxCategories, new { placeholder = T("Common.PleaseSelect").Text }) + @Html.ValidationMessageFor(model => model.TaxCategoryId) +
                                    + @Html.SmartLabelFor(model => model.IsEsd) + + @Html.EditorFor(model => model.IsEsd) + @Html.ValidationMessageFor(model => model.IsEsd) +
                                    - -
                                    \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Inventory.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Inventory.cshtml index bbed4351fd..f564238252 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Inventory.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Inventory.cshtml @@ -7,7 +7,6 @@ $("#@Html.FieldIdFor(model => model.ManageInventoryMethodId)").change(toggleManageStock); $("#@Html.FieldIdFor(model => model.BackorderModeId)").change(toggleManageStock); - $("#@Html.FieldIdFor(model => model.DisplayStockAvailability)").click(toggleManageStock); toggleManageStock(); @@ -103,7 +102,7 @@ @Html.SmartLabelFor(model => model.DisplayStockAvailability) - @Html.EditorFor(model => model.DisplayStockAvailability) + @Html.CheckBoxFor(model => model.DisplayStockAvailability, new { data_toggler_for = "#pnlDisplayStockQuantity" }) @Html.ValidationMessageFor(model => model.DisplayStockAvailability) @@ -199,7 +198,7 @@ - @* TODO: comment in when dropdown was implemented *@ + @* TODO: display when dropdown was implemented *@ @* Can be hidden if HideQuantityControl == true *@ diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Manufacturers.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Manufacturers.cshtml index e06230c752..f50ed231a9 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Manufacturers.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Manufacturers.cshtml @@ -8,7 +8,11 @@ { @(Html.Telerik().Grid() - .Name("productmanufacturers-grid") - .DataKeys(keys => - { - keys.Add(x => x.Id); - }) - .DataBinding(dataBinding => - { - dataBinding.Ajax() - .Select("ProductManufacturerList", "Product", new { productId = Model.Id }) - .Insert("ProductManufacturerInsert", "Product", new { productId = Model.Id }) - .Update("ProductManufacturerUpdate", "Product") - .Delete("ProductManufacturerDelete", "Product"); - }) - .Columns(columns => - { - columns.Bound(x => x.Manufacturer) + .Name("productmanufacturers-grid") + .DataKeys(keys => { keys.Add(x => x.Id); }) + .DataBinding(dataBinding => + { + dataBinding.Ajax() + .Select("ProductManufacturerList", "Product", new { productId = Model.Id }) + .Insert("ProductManufacturerInsert", "Product", new { productId = Model.Id }) + .Update("ProductManufacturerUpdate", "Product") + .Delete("ProductManufacturerDelete", "Product"); + }) + .Columns(columns => + { + columns.Bound(x => x.Manufacturer) + .Width("60%") .Template(x => Html.ActionLink(x.Manufacturer, "Edit", "Manufacturer", new { id = x.ManufacturerId })) .ClientTemplate("\"><#= Manufacturer #>"); - columns.Bound(x => x.IsFeaturedProduct) - .Width(100) - .Template(item => Html.SymbolForBool(item.IsFeaturedProduct)) - .ClientTemplate(Html.SymbolForBool("IsFeaturedProduct")) - .Centered(); - columns.Bound(x => x.DisplayOrder) + columns.Bound(x => x.IsFeaturedProduct) + .Width(100) + .Template(item => Html.SymbolForBool(item.IsFeaturedProduct)) + .ClientTemplate(Html.SymbolForBool("IsFeaturedProduct")) + .Centered(); + columns.Bound(x => x.DisplayOrder) .Centered() - .Width(100); - columns.Command(commands => - { - commands.Edit().Localize(T); - commands.Delete().Localize(T); - }) - .Width(180); - }) - .ToolBar(commands => commands.Insert()) - .ClientEvents(events => + .Width(100); + columns.Command(commands => + { + commands.Edit().Localize(T); + commands.Delete().Localize(T); + }) + .HtmlAttributes(new { align = "right" }); + }) + .ToolBar(commands => commands.Insert()) + .ClientEvents(events => { events.OnEdit("onProductManufacturerEdit"); events.OnDataBound("onProductManufacturerDataBound"); }) - .EnableCustomBinding(true)) + .EnableCustomBinding(true)) } else { diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Pictures.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Pictures.cshtml index a8e66f1e74..54c8701440 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Pictures.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Pictures.cshtml @@ -1,57 +1,19 @@ @model ProductModel - @using Telerik.Web.Mvc.UI; @if (Model.Id > 0) { - - - + - +
                                    - - - - - - - - - + + + + + + + + + - -
                                    -
                                    @T("Admin.Catalog.Products.Pictures.AddNew")
                                    +
                                    @T("Admin.Catalog.Products.Pictures.AddNew")
                                    - @Html.SmartLabelFor(model => model.AddPictureModel.PictureId) - - @Html.EditorFor(model => model.AddPictureModel.PictureId, new { transientUpload = true }) - @Html.ValidationMessageFor(model => model.AddPictureModel.PictureId) -
                                    - @Html.SmartLabelFor(model => model.AddPictureModel.DisplayOrder) - - @Html.EditorFor(model => model.AddPictureModel.DisplayOrder) - @Html.ValidationMessageFor(model => model.AddPictureModel.DisplayOrder) -
                                    + @Html.SmartLabelFor(model => model.AddPictureModel.PictureId) + + @Html.EditorFor(model => model.AddPictureModel.PictureId, new { transientUpload = true }) + @Html.ValidationMessageFor(model => model.AddPictureModel.PictureId) +
                                    + @Html.SmartLabelFor(model => model.AddPictureModel.DisplayOrder) + + @Html.EditorFor(model => model.AddPictureModel.DisplayOrder) + @Html.ValidationMessageFor(model => model.AddPictureModel.DisplayOrder) +
                                      -
                                    + + + +
                                    + @(Html.Telerik().Grid() + .Name("productpictures-grid") + .DataKeys(x => + { + x.Add(y => y.Id).RouteKey("Id"); + }) + .Columns(columns => + { + columns.Bound(x => x.PictureUrl) + .Width(400) + .Centered() + .ClientTemplate("<#= PictureId #>") + .ReadOnly(); + columns.Bound(x => x.DisplayOrder).Width(100).Centered(); + columns.Command(commands => + { + commands.Edit().Localize(T); + commands.Delete().Localize(T); + }).Width(180).HtmlAttributes(new { align = "right" }); + + }) + .Editable(x => + { + x.Mode(GridEditMode.InLine); + }) + .DataBinding(dataBinding => + { + dataBinding.Ajax().Select("ProductPictureList", "Product", new { productId = Model.Id }) + .Update("ProductPictureUpdate", "Product", new { productId = Model.Id }) + .Delete("ProductPictureDelete", "Product", new { productId = Model.Id }); + }) + .EnableCustomBinding(true)) +
                                    } else { diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Price.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Price.cshtml index 4a3c54f963..8d2099c24b 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Price.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Price.cshtml @@ -8,9 +8,6 @@ var fieldBasePriceBaseAmount; $(function () { - $("#@Html.FieldIdFor(model => model.CustomerEntersPrice)").click(toggleCustomerEntersPrice); - $("#@Html.FieldIdFor(model => model.BasePriceEnabled)").click(toggleBasePrice); - fieldBasePriceMeasureUnit = $("#@Html.FieldIdFor(model => model.BasePriceMeasureUnit)"); fieldBasePriceAmount = $("#@Html.FieldIdFor(model => model.BasePriceAmount)"); fieldBasePriceBaseAmount = $("#@Html.FieldIdFor(model => model.BasePriceBaseAmount)"); @@ -19,13 +16,11 @@ fieldBasePriceAmount.on("blur", getCurrentBasePrice); fieldBasePriceBaseAmount.on("blur", getCurrentBasePrice); - toggleCustomerEntersPrice(); getCurrentBasePrice(); - toggleBasePrice(); function getCurrentBasePrice() { - var basePriceAmount = Globalize.parseFloat(fieldBasePriceAmount.val()); - var basePriceBaseAmount = Globalize.parseInt(fieldBasePriceBaseAmount.val()); + var basePriceAmount = SmartStore.globalization.parseFloat(fieldBasePriceAmount.val()); + var basePriceBaseAmount = SmartStore.globalization.parseInt(fieldBasePriceBaseAmount.val()); if (basePriceAmount > 0 && basePriceBaseAmount > 0) { $.ajax({ cache:false, @@ -47,209 +42,195 @@ } return false; } - - function toggleBasePrice() { - if ($('#@Html.FieldIdFor(model => model.BasePriceEnabled)').is(':checked')) { - $('#pnlBasePriceBaseAmount').show(); - $('#pnlBasePriceAmount').show(); - $('#pnlBasePriceAmountHint').show(); - } - else { - $('#pnlBasePriceBaseAmount').hide(); - $('#pnlBasePriceAmount').hide(); - $('#pnlBasePriceAmountHint').hide(); - } - } - - function toggleCustomerEntersPrice() { - var src = $('#@Html.FieldIdFor(model => model.CustomerEntersPrice)'); - if (src.is(':checked')) { - $('#pnlMinimumCustomerEnteredPrice').show(); - $('#pnlMaximumCustomerEnteredPrice').show(); - } - else { - $('#pnlMinimumCustomerEnteredPrice').hide(); - $('#pnlMaximumCustomerEnteredPrice').hide(); - } - } })
                                    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +
                                    +
                                    +
                                    + @Html.SmartLabelFor(model => model.MinimumCustomerEnteredPrice) +
                                    +
                                    + @Html.EditorFor(model => model.MinimumCustomerEnteredPrice, new { postfix = Model.PrimaryStoreCurrencyCode }) + @Html.ValidationMessageFor(model => model.MinimumCustomerEnteredPrice) +
                                    +
                                    +
                                    +
                                    + @Html.SmartLabelFor(model => model.MaximumCustomerEnteredPrice) +
                                    +
                                    + @Html.EditorFor(model => model.MaximumCustomerEnteredPrice, new { postfix = Model.PrimaryStoreCurrencyCode }) + @Html.ValidationMessageFor(model => model.MaximumCustomerEnteredPrice) +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    - - - - - - - - - - - - - - - - - -
                                    +
                                    +
                                    +
                                    @Html.SmartLabelFor(model => model.Price) -
                                    - @Html.EditorFor(model => model.Price) @Model.PrimaryStoreCurrencyCode + +
                                    + @Html.EditorFor(model => model.Price, new { postfix = Model.PrimaryStoreCurrencyCode }) @Html.ValidationMessageFor(model => model.Price) -
                                    + + +
                                    +
                                    @Html.SmartLabelFor(model => model.OldPrice) -
                                    - @Html.EditorFor(model => model.OldPrice) @Model.PrimaryStoreCurrencyCode + +
                                    + @Html.EditorFor(model => model.OldPrice, new { postfix = Model.PrimaryStoreCurrencyCode }) @Html.ValidationMessageFor(model => model.OldPrice) -
                                    + + +
                                    +
                                    @Html.SmartLabelFor(model => model.ProductCost) -
                                    - @Html.EditorFor(model => model.ProductCost) @Model.PrimaryStoreCurrencyCode + +
                                    + @Html.EditorFor(model => model.ProductCost, new { postfix = Model.PrimaryStoreCurrencyCode }) @Html.ValidationMessageFor(model => model.ProductCost) -
                                    + + +
                                    +
                                    @Html.SmartLabelFor(model => model.SpecialPrice) -
                                    - @Html.EditorFor(model => model.SpecialPrice) @Model.PrimaryStoreCurrencyCode + +
                                    + @Html.EditorFor(model => model.SpecialPrice, new { postfix = Model.PrimaryStoreCurrencyCode }) @Html.ValidationMessageFor(model => model.SpecialPrice) -
                                    + + +
                                    +
                                    @Html.SmartLabelFor(model => model.SpecialPriceStartDateTimeUtc) -
                                    + +
                                    @Html.EditorFor(model => model.SpecialPriceStartDateTimeUtc, new { pickTime = true }) @Html.ValidationMessageFor(model => model.SpecialPriceStartDateTimeUtc) -
                                    + + +
                                    +
                                    @Html.SmartLabelFor(model => model.SpecialPriceEndDateTimeUtc) -
                                    + +
                                    @Html.EditorFor(model => model.SpecialPriceEndDateTimeUtc, new { pickTime = true }) @Html.ValidationMessageFor(model => model.SpecialPriceEndDateTimeUtc) -
                                    + + +
                                    +
                                    @Html.SmartLabelFor(model => model.DisableBuyButton) -
                                    + +
                                    @Html.EditorFor(model => model.DisableBuyButton) @Html.ValidationMessageFor(model => model.DisableBuyButton) -
                                    + + +
                                    +
                                    @Html.SmartLabelFor(model => model.DisableWishlistButton) -
                                    + +
                                    @Html.EditorFor(model => model.DisableWishlistButton) @Html.ValidationMessageFor(model => model.DisableWishlistButton) -
                                    + + +
                                    +
                                    @Html.SmartLabelFor(model => model.AvailableForPreOrder) -
                                    + +
                                    @Html.EditorFor(model => model.AvailableForPreOrder) @Html.ValidationMessageFor(model => model.AvailableForPreOrder) -
                                    + + +
                                    +
                                    @Html.SmartLabelFor(model => model.CallForPrice) -
                                    + +
                                    @Html.EditorFor(model => model.CallForPrice) @Html.ValidationMessageFor(model => model.CallForPrice) -
                                    + + +
                                    +
                                    @Html.SmartLabelFor(model => model.CustomerEntersPrice) -
                                    - @Html.EditorFor(model => model.CustomerEntersPrice) + +
                                    + @Html.CheckBoxFor(model => model.CustomerEntersPrice, new { data_toggler_for = "#pnlCustomerEntersPrice" }) @Html.ValidationMessageFor(model => model.CustomerEntersPrice) -
                                    - @Html.SmartLabelFor(model => model.MinimumCustomerEnteredPrice) - - @Html.EditorFor(model => model.MinimumCustomerEnteredPrice) @Model.PrimaryStoreCurrencyCode - @Html.ValidationMessageFor(model => model.MinimumCustomerEnteredPrice) -
                                    - @Html.SmartLabelFor(model => model.MaximumCustomerEnteredPrice) - - @Html.EditorFor(model => model.MaximumCustomerEnteredPrice) @Model.PrimaryStoreCurrencyCode - @Html.ValidationMessageFor(model => model.MaximumCustomerEnteredPrice) -
                                    -
                                    -
                                    +
                                    +
                                    @Html.SmartLabelFor(model => model.BasePriceEnabled) -
                                    - @Html.EditorFor(model => model.BasePriceEnabled) + +
                                    + @Html.CheckBoxFor(model => model.BasePriceEnabled, new { data_toggler_for = "#pnlBasePriceEnabled" }) @Html.ValidationMessageFor(model => model.BasePriceEnabled) -
                                    - @Html.SmartLabelFor(model => model.BasePriceBaseAmount) - - @Html.EditorFor(model => model.BasePriceBaseAmount, new { small = true }) - @Html.DropDownListFor(model => model.BasePriceMeasureUnit, Model.AvailableMeasureUnits, new { @class = "autowidth", style = "width:100px" }) -
                                    - @Html.SmartLabelFor(model => model.BasePriceAmount) - - @Html.EditorFor(model => model.BasePriceAmount, new { small = true }) - @*@T("Admin.Catalog.Products.Fields.BasePriceInfo")*@ -
                                    -   - -
                                    - @T("Admin.Products.BasePrice.Hint") -
                                    -
                                    +
                                    +
                                    +
                                    +
                                    +
                                    + @Html.SmartLabelFor(model => model.BasePriceBaseAmount) +
                                    +
                                    +
                                    +
                                    + @Html.EditorFor(model => model.BasePriceBaseAmount) +
                                    +
                                    + @Html.DropDownListFor(model => model.BasePriceMeasureUnit, Model.AvailableMeasureUnits) +
                                    +
                                    +
                                    +
                                    +
                                    +
                                    + @Html.SmartLabelFor(model => model.BasePriceAmount) +
                                    +
                                    + @Html.EditorFor(model => model.BasePriceAmount) + @*@T("Admin.Catalog.Products.Fields.BasePriceInfo")*@ +
                                    +
                                    +
                                    +
                                    +   +
                                    +
                                    +
                                    + @T("Admin.Products.BasePrice.Hint") +
                                    +
                                    +
                                    +
                                    +
                                    -
                                    -

                                    @T("Admin.Catalog.Products.TierPrices")

                                    +
                                    +

                                    @T("Admin.Catalog.Products.TierPrices")

                                    @if (Model.Id > 0) { @@ -271,20 +252,21 @@ { columns.Bound(x => x.Quantity) .Centered() - .Width(120); + .Width(80); columns.Bound(x => x.Price1) .Format("{0:0.00}") .Width(120) .RightAlign(); - columns.Bound(x => x.CalculationMethod); - columns.Bound(x => x.Store); - columns.Bound(x => x.CustomerRole); + columns.Bound(x => x.CalculationMethod).Width("20%"); + columns.Bound(x => x.Store).Width("25%"); + columns.Bound(x => x.CustomerRole).Width("20%"); columns.Command(commands => { commands.Edit().Localize(T); commands.Delete().Localize(T); }) - .Width(180); + //.Width(180) + .HtmlAttributes(new { align = "right" }); }) .ToolBar(commands => commands.Insert()) .ClientEvents(events => events.OnEdit("onTierPriceEdit")) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Promotion.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Promotion.cshtml index cdeda617b9..99771f4d29 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Promotion.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Promotion.cshtml @@ -2,65 +2,50 @@ @using Telerik.Web.Mvc.UI;
                                    -

                                    @T("Admin.Catalog.Products.RelatedProducts")

                                    +

                                    @T("Admin.Catalog.Products.RelatedProducts")

                                    @if (Model.Id > 0) { - - - - - - - -
                                    - @(Html.Telerik().Grid() - .Name("relatedproducts-grid") - .DataKeys(keys => - { - keys.Add(x => x.Id); - }) - .DataBinding(dataBinding => - { - dataBinding.Ajax() - .Select("RelatedProductList", "Product", new { productId = Model.Id }) - .Update("RelatedProductUpdate", "Product") - .Delete("RelatedProductDelete", "Product"); - }) - .Columns(columns => - { - columns.Bound(x => x.Product2Name) - .ReadOnly() - .Width(520) - .ClientTemplate(@Html.LabeledProductName("ProductId2", "Product2Name")); - columns.Bound(x => x.Product2Sku) - .ReadOnly(); - columns.Bound(x => x.Product2Published) - .ReadOnly() - .ClientTemplate(@Html.SymbolForBool("Product2Published")) - .Centered(); - columns.Bound(x => x.DisplayOrder) - .Centered(); - columns.Command(commands => - { - commands.Edit().Localize(T); - commands.Delete().Localize(T); - }).Width(220); - }) - .EnableCustomBinding(true)) - -
                                    - - - -
                                    +
                                    + @(Html.Telerik().Grid() + .Name("relatedproducts-grid") + .DataKeys(keys => + { + keys.Add(x => x.Id); + }) + .DataBinding(dataBinding => + { + dataBinding.Ajax() + .Select("RelatedProductList", "Product", new { productId = Model.Id }) + .Update("RelatedProductUpdate", "Product") + .Delete("RelatedProductDelete", "Product"); + }) + .Columns(columns => + { + columns.Bound(x => x.Product2Name) + .ReadOnly() + .Width("60%") + .ClientTemplate(@Html.LabeledProductName("ProductId2", "Product2Name")); + columns.Bound(x => x.Product2Sku) + .Width(150) + .ReadOnly(); + columns.Bound(x => x.Product2Published) + .ReadOnly() + .Width(100) + .ClientTemplate(@Html.SymbolForBool("Product2Published")) + .Centered(); + columns.Bound(x => x.DisplayOrder) + .Width(100) + .Centered(); + columns.Command(commands => + { + commands.Edit().Localize(T); + commands.Delete().Localize(T); + }).HtmlAttributes(new { align = "right" }); + }) + .ToolBar(commands => commands.Template(CrossSellingGridCommands)) + .EnableCustomBinding(true)) +
                                    } else { @@ -70,61 +55,42 @@ }
                                    -

                                     

                                    - -
                                    -

                                    @T("Admin.Catalog.Products.CrossSells")

                                    +
                                    +

                                    @T("Admin.Catalog.Products.CrossSells")

                                    @if (Model.Id > 0) { - - - - - - - -
                                    - @(Html.Telerik().Grid() - .Name("crosssellproducts-grid") - .DataKeys(keys => - { - keys.Add(x => x.Id); - }) - .DataBinding(dataBinding => - { - dataBinding.Ajax() - .Select("CrossSellProductList", "Product", new { productId = Model.Id }) - .Delete("CrossSellProductDelete", "Product"); - }) - .Columns(columns => - { - columns.Bound(x => x.Product2Name) - .ReadOnly() - .Width(520) - .ClientTemplate(@Html.LabeledProductName("ProductId2", "Product2Name")); - columns.Bound(x => x.Product2Sku); - columns.Bound(x => x.Product2Published) - .ClientTemplate(@Html.SymbolForBool("Product2Published")) - .Centered(); - columns.Command(commands => - { - commands.Delete().Localize(T); - }).Width(220); - }) - .EnableCustomBinding(true)) -
                                    - - - -
                                    +
                                    + @(Html.Telerik().Grid() + .Name("crosssellproducts-grid") + .DataKeys(keys => + { + keys.Add(x => x.Id); + }) + .DataBinding(dataBinding => + { + dataBinding.Ajax() + .Select("CrossSellProductList", "Product", new { productId = Model.Id }) + .Delete("CrossSellProductDelete", "Product"); + }) + .Columns(columns => + { + columns.Bound(x => x.Product2Name) + .ReadOnly() + .Width("60%") + .ClientTemplate(@Html.LabeledProductName("ProductId2", "Product2Name")); + columns.Bound(x => x.Product2Sku).Width(150); + columns.Bound(x => x.Product2Published).Width(100) + .ClientTemplate(@Html.SymbolForBool("Product2Published")) + .Centered(); + columns.Command(commands => + { + commands.Delete().Localize(T); + }).HtmlAttributes(new { align = "right" }); + }) + .ToolBar(commands => commands.Template(CheckoutSellingGridCommands)) + .EnableCustomBinding(true)) +
                                    } else { @@ -134,8 +100,63 @@ }
                                    - \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.SEO.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.SEO.cshtml index ce90cf7e9b..39222065bd 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.SEO.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.SEO.cshtml @@ -7,7 +7,10 @@ @Html.SmartLabelFor(model => model.Locales[item].MetaKeywords) - @Html.TextBoxFor(model => model.Locales[item].MetaKeywords, new { @class = "input-large" }) + @*IMPORTANT: Do not delete, this hidden element contains the id to assign localized values to the corresponding language *@ + @Html.HiddenFor(model => model.Locales[item].LanguageId) + + @Html.TextBoxFor(model => model.Locales[item].MetaKeywords) @Html.ValidationMessageFor(model => model.Locales[item].MetaKeywords) @@ -16,7 +19,7 @@ @Html.SmartLabelFor(model => model.Locales[item].MetaDescription) - @Html.TextAreaFor(model => model.Locales[item].MetaDescription, new { @class = "input-large" }) + @Html.TextAreaFor(model => model.Locales[item].MetaDescription) @Html.ValidationMessageFor(model => model.Locales[item].MetaDescription) @@ -25,7 +28,7 @@ @Html.SmartLabelFor(model => model.Locales[item].MetaTitle) - @Html.TextAreaFor(model => model.Locales[item].MetaTitle, new { @class = "input-large" }) + @Html.TextAreaFor(model => model.Locales[item].MetaTitle) @Html.ValidationMessageFor(model => model.Locales[item].MetaTitle) @@ -34,15 +37,10 @@ @Html.SmartLabelFor(model => model.Locales[item].SeName) - @Html.TextAreaFor(model => model.Locales[item].SeName, new { @class = "input-large" }) + @Html.TextAreaFor(model => model.Locales[item].SeName) @Html.ValidationMessageFor(model => model.Locales[item].SeName) - - - @Html.HiddenFor(model => model.Locales[item].LanguageId) - - , @ @@ -51,7 +49,7 @@ @Html.SmartLabelFor(model => model.MetaKeywords) @@ -60,7 +58,7 @@ @Html.SmartLabelFor(model => model.MetaDescription) @@ -69,7 +67,7 @@ @Html.SmartLabelFor(model => model.MetaTitle) @@ -78,7 +76,7 @@ @Html.SmartLabelFor(model => model.SeName) @@ -140,7 +136,7 @@ @Html.SmartLabelFor(model => model.Length) @@ -149,7 +145,7 @@ @Html.SmartLabelFor(model => model.Width) @@ -158,7 +154,7 @@ @Html.SmartLabelFor(model => model.Height) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateProductAttributeValue.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateProductAttributeValue.cshtml index 883e527527..6f222381a4 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateProductAttributeValue.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateProductAttributeValue.cshtml @@ -7,8 +7,7 @@ @Html.HiddenFor(model => model.ProductVariantAttributeId) -
                                    - @Html.TextBoxFor(x => x.MetaKeywords, new { @class = "input-large" }) + @Html.TextBoxFor(x => x.MetaKeywords) @Html.ValidationMessageFor(model => model.MetaKeywords)
                                    - @Html.TextAreaFor(x => x.MetaDescription, new { @class = "input-large" }) + @Html.TextAreaFor(x => x.MetaDescription) @Html.ValidationMessageFor(model => model.MetaDescription)
                                    - @Html.TextAreaFor(x => x.MetaTitle, new { @class = "input-large" }) + @Html.TextAreaFor(x => x.MetaTitle) @Html.ValidationMessageFor(model => model.MetaTitle)
                                    - @Html.TextAreaFor(x => x.SeName, new { @class = "input-large" }) + @Html.TextAreaFor(x => x.SeName) @if (Model.Id != 0) { @Html.Action("NamesPerEntity", "UrlRecord", new { entityName = "Product", entityId = @Model.Id }) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.SpecificationAttributes.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.SpecificationAttributes.cshtml index e91ae2b52d..717f738cf4 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.SpecificationAttributes.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.SpecificationAttributes.cshtml @@ -11,7 +11,7 @@ { - +
                                    @@ -124,22 +124,16 @@  
                                    -
                                    - -

                                     

                                    - +
                                    @(Html.Telerik().Grid() @@ -213,7 +218,7 @@ .Width(200) .ReadOnly(); columns.Bound(x => x.SpecificationAttributeOptionName) - .ClientTemplate("<#= SpecificationAttributeOptionName #>") + .ClientTemplate("<#= SpecificationAttributeOptionName #>") .ReadOnly() .Width(300); columns.Bound(x => x.AllowFiltering) @@ -231,7 +236,7 @@ { commands.Edit().Localize(T); commands.Delete().Localize(T); - }).Width(180); + }).Width(180).HtmlAttributes(new { align = "right" }); }) .ClientEvents(e => { diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Stores.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Stores.cshtml index 25682b32f8..470cfa438d 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Stores.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.Stores.cshtml @@ -1,50 +1,3 @@ @model ProductModel - - - - - - - - - - -
                                    - @Html.SmartLabelFor(model => model.LimitedToStores) - - @Html.EditorFor(model => model.LimitedToStores) - @Html.ValidationMessageFor(model => model.LimitedToStores) -
                                    - @Html.SmartLabelFor(model => model.AvailableStores) - - @if (Model.AvailableStores != null && Model.AvailableStores.Count > 0) - { - foreach (var store in Model.AvailableStores) - { - - } - } - else - { -
                                    @T("Admin.Configuration.Stores.NoStoresDefined")
                                    - } -
                                    +@Html.Partial("StoreSelector", Model) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.cshtml index 4f5763b2db..a2d29c74f0 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdate.cshtml @@ -1,12 +1,9 @@ -@model ProductModel - -@using SmartStore.Core.Domain.Catalog; -@using Telerik.Web.Mvc.UI; +@using SmartStore.Core.Domain.Catalog; @using SmartStore.Web.Framework.UI; - +@model ProductModel @{ - Html.AddCssFileParts(true, "~/Content/x-editable/bootstrap-editable.css"); - Html.AppendScriptParts(true, "~/Content/x-editable/bootstrap-editable.js"); + Html.AddCssFileParts(true, "~/Content/vendors/x-editable/bootstrap-editable.css"); + Html.AppendScriptParts(true, "~/Content/vendors/x-editable/bootstrap-editable.js"); } @Html.ValidationSummary(false) @@ -18,9 +15,9 @@ } -@Html.SmartStore().TabStrip().Name("product-edit").OnAjaxSuccess("productEditTab_onAjaxSuccess").HtmlAttributes(new { data_product_id = Model.Id }).Style(TabsStyle.Tabs).Position(TabsPosition.Left).Items(x => +@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").Text) + x.Add().Text(T("Admin.Catalog.Products.Info")) .Icon("fa fa-pencil fa-lg fa-fw") .LinkHtmlAttributes(new { data_tab_name = "Info" }) .Content(Html.Partial("_CreateOrUpdate.Info", Model).ToHtmlString()) @@ -66,7 +63,7 @@ x.Add().Text(T("Admin.Catalog.Products.Discounts").Text) .Name("tab-discounts") .Visible(!(Model.ProductTypeId == (int)ProductType.BundledProduct && Model.BundlePerItemPricing)) - .Icon("fa fa-signal fa-lg fa-flip-horizontal fa-fw") + .Icon("fa fa-percent fa-lg fa-fw") .LinkHtmlAttributes(new { data_tab_name = "Discounts" }) .Action("LoadEditTab", "Product", new { id = Model.Id, tabName = "Discounts" }) .Ajax(); diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateAttributeCombinationPopup.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateAttributeCombinationPopup.cshtml index 3360739ed2..0834aab50d 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateAttributeCombinationPopup.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateAttributeCombinationPopup.cshtml @@ -3,12 +3,10 @@ @Html.ValidationSummary(false) @Html.HiddenFor(model => model.Id)@*otherwise we get a model state error*@ -
                                    +
                                    @if (ViewBag.IsEdit) { -
                                    - @Html.Raw(Html.TableFormattedVariantAttributes(Model.AttributesXml)) -
                                    + @Html.Raw(Html.TableFormattedVariantAttributes(Model.AttributesXml)) } else { @@ -23,7 +21,7 @@
                                    @if (Model.Warnings.Count > 0) { -
                                    +
                                    @foreach (var warning in Model.Warnings) {

                                     @warning

                                    @@ -52,16 +50,14 @@ @Html.SmartLabelFor(model => model.AssignedPictureIds)
                                    -
                                      +
                                        @foreach (var x in Model.AssignablePictures) {
                                      • @@ -101,7 +97,7 @@ @Html.SmartLabelFor(model => model.Price)
                                    - @Html.EditorFor(model => model.Price) + @Html.EditorFor(model => model.Price, new { postfix = Model.PrimaryStoreCurrencyCode }) @Html.ValidationMessageFor(model => model.Price)
                                    - @Html.EditorFor(model => model.Length) + @Html.EditorFor(model => model.Length, new { postfix = Model.BaseDimensionIn }) @Html.ValidationMessageFor(model => model.Length)
                                    - @Html.EditorFor(model => model.Width) + @Html.EditorFor(model => model.Width, new { postfix = Model.BaseDimensionIn }) @Html.ValidationMessageFor(model => model.Width)
                                    - @Html.EditorFor(model => model.Height) + @Html.EditorFor(model => model.Height, new { postfix = Model.BaseDimensionIn }) @Html.ValidationMessageFor(model => model.Height)
                                    +
                                    @@ -109,8 +98,6 @@
                                    @Html.SmartLabelFor(model => model.ValueTypeId) @@ -84,16 +62,27 @@ @Html.HiddenFor(model => model.LinkedProductId) -
                                    @Model.LinkedProductName
                                    - - - +
                                    + +
                                    + + @(Html.SmartStore().EntityPicker() + .For(x => x.LinkedProductId) + .IconCssClass("fa fa-link") + .HtmlAttribute("class", "btn btn-outline-secondary") + .HtmlAttribute("id", "AddProductLinkageButton") + .MaxItems(1) + .AppendMode(false) + .DisabledEntityIds(Model.ProductId) + .DialogTitle(T("Admin.Catalog.Products.ProductVariantAttributes.Attributes.Values.Fields.LinkedProduct.AddNew").Text.EncodeJsString('\'', false)) + .Caption(T("Admin.Catalog.Products.ProductVariantAttributes.Attributes.Values.LinkProduct").Text.EncodeJsString('\'', false)) + .OnSelectionCompleted("PickLinkedProduct_Completed")) +
                                    +
                                    @Html.ValidationMessageFor(model => model.LinkedProductId)
                                    -

                                    - @(Html.LocalizedEditor("productattributevalue-localized", @ @@ -118,6 +105,9 @@ @Html.SmartLabelFor(model => model.Locales[item].Name) @@ -131,11 +121,6 @@ @Html.ValidationMessageFor(model => model.Locales[item].Alias) - - -
                                    + @*IMPORTANT: Do not delete, this hidden element contains the id to assign localized values to the corresponding language *@ + @Html.HiddenFor(model => model.Locales[item].LanguageId) + @Html.EditorFor(model => Model.Locales[item].Name) @Html.ValidationMessageFor(model => model.Locales[item].Name)
                                    - @Html.HiddenFor(model => model.Locales[item].LanguageId) -
                                    , @ diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateProductTag.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateProductTag.cshtml index 327eb63338..116e5601d5 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateProductTag.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_CreateOrUpdateProductTag.cshtml @@ -10,15 +10,13 @@ @Html.SmartLabelFor(model => model.Locales[item].Name) - - -
                                    + @*IMPORTANT: Do not delete, this hidden element contains the id to assign localized values to the corresponding language *@ + @Html.HiddenFor(model => model.Locales[item].LanguageId) + @Html.EditorFor(model => Model.Locales[item].Name) @Html.ValidationMessageFor(model => model.Locales[item].Name)
                                    - @Html.HiddenFor(model => model.Locales[item].LanguageId) -
                                    , @ diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Product/_ProductAttributes.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Product/_ProductAttributes.cshtml index 706e6f50c2..4361fb4c5b 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Product/_ProductAttributes.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Product/_ProductAttributes.cshtml @@ -1,44 +1,39 @@ @using SmartStore.Web.Framework; @using SmartStore.Core.Domain.Catalog; + @model ProductVariantAttributeCombinationModel @if (Model.ProductVariantAttributes.Count > 0) { -
                                    - @foreach (var attribute in Model.ProductVariantAttributes) + foreach (var attribute in Model.ProductVariantAttributes) { var controlId = attribute.GetControlId(Model.ProductId, 0); -
                                    - -
                                    - @if (attribute.AttributeControlType == AttributeControlType.Checkboxes || attribute.AttributeControlType == AttributeControlType.RadioList || attribute.AttributeControlType == AttributeControlType.Boxes) - { - @ControlsCheckboxesAndRadios(attribute, controlId, attribute.AttributeControlType == AttributeControlType.Checkboxes ? "checkbox" : "radio") - } - else - { - @ControlsOthers(attribute, controlId) - } +
                                    + +
                                    + @if (attribute.AttributeControlType == AttributeControlType.Checkboxes || attribute.AttributeControlType == AttributeControlType.RadioList || attribute.AttributeControlType == AttributeControlType.Boxes) + { + @ControlsCheckboxesAndRadios(attribute, controlId, attribute.AttributeControlType == AttributeControlType.Checkboxes ? "checkbox" : "radio") + } + else + { + @ControlsOthers(attribute, controlId) + } +
                                    -
                                    } -
                                    } @helper ControlsCheckboxesAndRadios(ProductVariantAttributeCombinationModel.ProductVariantAttributeModel attribute, string controlId, string type) { foreach (var pvaValue in attribute.Values) { - +
                                    + + +
                                    } } @@ -46,24 +41,24 @@ { if(attribute.AttributeControlType == AttributeControlType.DropdownList) { - @if (!attribute.IsRequired) { - + } @foreach (var pvaValue in attribute.Values) { - + } } else if(attribute.AttributeControlType == AttributeControlType.TextBox) { - + } else if (attribute.AttributeControlType == AttributeControlType.MultilineTextbox) { - + } else if (attribute.AttributeControlType == AttributeControlType.Datepicker) { diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/Create.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/Create.cshtml index 75daf0103e..33e0e1d91c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/Create.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/Create.cshtml @@ -1,6 +1,5 @@ @model ProductAttributeModel @{ - //page title ViewBag.Title = T("Admin.Catalog.Attributes.ProductAttributes.AddNew").Text; } @using (Html.BeginForm()) @@ -10,8 +9,13 @@ @T("Admin.Catalog.Attributes.ProductAttributes.AddNew") @Html.ActionLink("(" + T("Admin.Catalog.Attributes.ProductAttributes.BackToList") + ")", "List")
                                    - - + +
                                    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/Edit.cshtml index bcb2910e8e..c0e28de45d 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/Edit.cshtml @@ -1,6 +1,5 @@ @model ProductAttributeModel @{ - //page title ViewBag.Title = T("Admin.Catalog.Attributes.ProductAttributes.EditAttributeDetails").Text; } @using (Html.BeginForm()) @@ -10,9 +9,17 @@ @T("Admin.Catalog.Attributes.ProductAttributes.EditAttributeDetails") - @Model.Name @Html.ActionLink("(" + T("Admin.Catalog.Attributes.ProductAttributes.BackToList") + ")", "List")
                                    - - - + + +
                                    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/List.cshtml index 8914a471d4..971c1e8bbb 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/List.cshtml @@ -7,34 +7,31 @@ @T("Admin.Catalog.Attributes.ProductAttributes") -
                                    - - - -
                                    - @(Html.Telerik().Grid() - .Name("productattributes-grid") - .Columns(columns => - { - columns.Bound(x => x.Name) - .ClientTemplate("<#= Name #>"); - columns.Bound(x => x.Alias); - columns.Bound(x => x.AllowFiltering) - .Width(200) - .ClientTemplate(@Html.SymbolForBool("AllowFiltering")) - .Centered(); - columns.Bound(x => x.OptionCount) - .Width(200) - .Centered(); - columns.Bound(x => x.DisplayOrder) - .Width(200) - .Centered(); - }) - .Pageable(settings => settings.PageSize((int)ViewData["GridPageSize"]).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("List", "ProductAttribute")) - .PreserveGridState() - .EnableCustomBinding(true)) -
                                    \ No newline at end of file + +
                                    + @(Html.Telerik().Grid() + .Name("productattributes-grid") + .Columns(columns => + { + columns.Bound(x => x.Name) + .ClientTemplate("<#= Name #>"); + columns.Bound(x => x.Alias); + columns.Bound(x => x.AllowFiltering) + .Width(200) + .ClientTemplate(@Html.SymbolForBool("AllowFiltering")) + .Centered(); + columns.Bound(x => x.DisplayOrder) + .Width(200) + .Centered(); + }) + .Pageable(settings => settings.PageSize((int)ViewData["GridPageSize"]).Position(GridPagerPosition.Both)) + .DataBinding(dataBinding => dataBinding.Ajax().Select("List", "ProductAttribute")) + .PreserveGridState() + .EnableCustomBinding(true)) +
                                    \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/OptionCreatePopup.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/OptionCreatePopup.cshtml index 30977045b3..97f2c8e5db 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/OptionCreatePopup.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/OptionCreatePopup.cshtml @@ -10,7 +10,10 @@ @T("Common.Options.Add")
                                    - +
                                    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/OptionEditPopup.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/OptionEditPopup.cshtml index 815dba7dbe..2dcf025f65 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/OptionEditPopup.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/OptionEditPopup.cshtml @@ -10,7 +10,10 @@ @T("Common.Options.Edit")
                                    - +
                                    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/_CreateOrUpdate.cshtml index 6f7a9fa587..03ba9ce78a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/_CreateOrUpdate.cshtml @@ -4,7 +4,7 @@ @Html.ValidationSummary(false) @Html.HiddenFor(model => model.Id) -@Html.SmartStore().TabStrip().Name("productattribute-edit").Items(x => +@Html.SmartStore().TabStrip().Name("productattribute-edit").Style(TabsStyle.Material).Items(x => { x.Add().Text(T("Common.General").Text).Content(TabInfo()).Selected(true); x.Add().Text(T("Admin.Catalog.Attributes.OptionsSets").Text).Content(TabOptionsSets()); @@ -22,13 +22,11 @@ @Html.SmartLabelFor(model => model.Id) - @Html.DisplayFor(model => model.Id) + @Html.TextBoxFor(model => model.Id, new { @readonly = "readonly", @class = "form-control-plaintext" }) @Html.ValidationMessageFor(model => model.Id) - -

                                    } @(Html.LocalizedEditor("productattribute-localized", @@ -38,6 +36,9 @@ @Html.SmartLabelFor(model => model.Locales[item].Name) + @*IMPORTANT: Do not delete, this hidden element contains the id to assign localized values to the corresponding language *@ + @Html.HiddenFor(model => model.Locales[item].LanguageId) + @Html.EditorFor(model => Model.Locales[item].Name) @Html.ValidationMessageFor(model => model.Locales[item].Name) @@ -55,18 +56,13 @@ @Html.SmartLabelFor(model => model.Locales[item].Description) - - @Html.EditorFor(model => model.Locales[item].Description, Html.RichEditorFlavor()) + + @Html.EditorFor(model => model.Locales[item].Description, "Html") @Html.ValidationMessageFor(model => model.Locales[item].Description) - - - @Html.HiddenFor(model => model.Locales[item].LanguageId) - - - , + , @ - @@ -113,11 +109,11 @@ @Html.SmartLabelFor(model => model.AllowFiltering) - + @@ -127,118 +123,129 @@ @Html.ValidationMessageFor(model => model.FacetTemplateHint) + + + + + + + + + + + +
                                    @@ -90,8 +86,8 @@ @Html.SmartLabelFor(model => model.Description) - @Html.EditorFor(x => x.Description, Html.RichEditorFlavor()) + + @Html.EditorFor(x => x.Description, "Html") @Html.ValidationMessageFor(model => model.Description)
                                    - @Html.EditorFor(model => model.AllowFiltering) + @Html.CheckBoxFor(model => model.AllowFiltering, new { data_toggler_for = "#pnlAllowFiltering" }) @Html.ValidationMessageFor(model => model.AllowFiltering)
                                    @Html.SmartLabelFor(model => model.FacetTemplateHint)
                                    + @Html.SmartLabelFor(model => model.IndexOptionNames) + + @Html.EditorFor(model => model.IndexOptionNames) + @Html.ValidationMessageFor(model => model.IndexOptionNames) +
                                    + @Html.SmartLabelFor(model => model.ExportMappings) + + @Html.TextAreaFor(model => model.ExportMappings, new { style = "min-height: 120px;" }) + @Html.ValidationMessageFor(model => model.ExportMappings) +
                                    +
                                    + @Html.Raw(T("Admin.Catalog.Attributes.ProductAttributes.Fields.ExportMappings.Note")) +
                                    +
                                    - - } @helper TabOptionsSets() { if (Model.Id > 0) { - - - - -
                                    - @(Html.Telerik().Grid() - .Name("productattributeoptionsset-grid") +
                                    + @(Html.Telerik().Grid() + .Name("productattributeoptionsset-grid") + .DataKeys(keys => + { + keys.Add(x => x.Id); + keys.Add(x => x.ProductAttributeId).RouteKey("productAttributeId"); + }) + .ToolBar(commands => commands.Insert()) + .Columns(columns => + { + columns.Bound(x => x.Name); + columns.Command(commands => + { + commands.Edit().Localize(T); + commands.Delete().Localize(T); + }) + .HtmlAttributes(new { align = "right" }) + .Width(120); + }) + .DataBinding(dataBinding => dataBinding.Ajax() + .Select("OptionsSetList", "ProductAttribute", new { productAttributeId = Model.Id }) + .Insert("OptionsSetInsert", "ProductAttribute", new { productAttributeId = Model.Id }) + .Update("OptionsSetUpdate", "ProductAttribute") + .Delete("OptionsSetDelete", "ProductAttribute") + ) + .EnableCustomBinding(true) + .DetailView(details => details.ClientTemplate( + Html.Telerik().Grid() + .Name("productattributeoptions-grid<#= Id #>") .DataKeys(keys => { keys.Add(x => x.Id); - keys.Add(x => x.ProductAttributeId).RouteKey("productAttributeId"); + keys.Add(x => x.ProductAttributeOptionsSetId).RouteKey("optionsSetId"); }) - .ToolBar(commands => commands.Insert()) + .ToolBar(commands => commands.Template(OptionGridCommands)) .Columns(columns => { - columns.Bound(x => x.Name); - columns.Command(commands => - { - commands.Edit().Localize(T); - commands.Delete().Localize(T); - }) - .Width(120); + columns.Bound(x => x.Name) + .ClientTemplate(@Html.VariantAttributeValueName()); + columns.Bound(x => x.Alias); + columns.Bound(x => x.LinkedProductName) + .ClientTemplate(@Html.LabeledProductName("LinkedProductId", "LinkedProductName", "LinkedProductTypeName", "LinkedProductTypeLabelHint")); + columns.Bound(x => x.PriceAdjustmentString) + .Centered(); + columns.Bound(x => x.WeightAdjustmentString) + .Centered(); + columns.Bound(x => x.IsPreSelected) + .ClientTemplate(@Html.SymbolForBool("IsPreSelected")) + .Centered(); + columns.Bound(x => x.DisplayOrder) + .Centered(); + columns.Bound(x => x.Id) + .Width(220) + .Centered() + .Title("") + .ClientTemplate( + "" + @T("Admin.Common.Edit") + "" + + "" + @T("Admin.Common.Delete") + "") + .HtmlAttributes(new { align = "right" }); }) .DataBinding(dataBinding => dataBinding.Ajax() - .Select("OptionsSetList", "ProductAttribute", new { productAttributeId = Model.Id }) - .Insert("OptionsSetInsert", "ProductAttribute", new { productAttributeId = Model.Id }) - .Update("OptionsSetUpdate", "ProductAttribute") - .Delete("OptionsSetDelete", "ProductAttribute") + .Select("OptionsSetListDetails", "ProductAttribute", new { id = "<#= Id #>" }) ) - .EnableCustomBinding(true) - .DetailView(details => details.ClientTemplate( - Html.Telerik().Grid() - .Name("productattributeoptions-grid<#= Id #>") - .DataKeys(keys => - { - keys.Add(x => x.Id); - keys.Add(x => x.ProductAttributeOptionsSetId).RouteKey("optionsSetId"); - }) - .ToolBar(commands => commands.Custom() - .Name("insert-option") - .HtmlAttributes(new { href = "#", @class = "btn btn-link insert-option" }) - .Text(" " + @T("Common.Options.Add")) - ) - .Columns(columns => - { - columns.Bound(x => x.Name) - .ClientTemplate(@Html.VariantAttributeValueName()); - columns.Bound(x => x.Alias); - columns.Bound(x => x.LinkedProductName) - .ClientTemplate(@Html.LabeledProductName("LinkedProductId", "LinkedProductName", "LinkedProductTypeName", "LinkedProductTypeLabelHint")); - columns.Bound(x => x.PriceAdjustmentString) - .Centered(); - columns.Bound(x => x.WeightAdjustmentString) - .Centered(); - columns.Bound(x => x.IsPreSelected) - .ClientTemplate(@Html.SymbolForBool("IsPreSelected")) - .Centered(); - columns.Bound(x => x.DisplayOrder) - .Centered(); - columns.Bound(x => x.Id) - .Width(220) - .Centered() - .Title("") - .ClientTemplate( - "" + @T("Admin.Common.Edit") + "" + - "" + @T("Admin.Common.Delete") + ""); - }) - .DataBinding(dataBinding => dataBinding.Ajax() - .Select("OptionsSetListDetails", "ProductAttribute", new { id = "<#= Id #>" }) - ) - .ClientEvents(events => events.OnDataBound("onDataBoundOptionsGrid")) - .ToHtmlString() - ))) -
                                    + .ToHtmlString() + ))) + - - + + } else @@ -282,3 +285,11 @@ } } + +@helper OptionGridCommands(Grid grid) +{ + +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/_CreateOrUpdateOption.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/_CreateOrUpdateOption.cshtml index 9a036f086a..0f98d75ee6 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/_CreateOrUpdateOption.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ProductAttribute/_CreateOrUpdateOption.cshtml @@ -24,16 +24,26 @@ @Html.HiddenFor(model => model.LinkedProductId) -
                                    @Model.LinkedProductName
                                    - - - - +
                                    + +
                                    + + @(Html.SmartStore().EntityPicker() + .For(x => x.LinkedProductId) + .IconCssClass("fa fa-link") + .HtmlAttribute("class", "btn btn-secondary") + .HtmlAttribute("id", "AddProductLinkageButton") + .MaxItems(1) + .AppendMode(false) + .DisabledEntityIds(Model.ProductId) + .DialogTitle(T("Admin.Catalog.Products.ProductVariantAttributes.Attributes.Values.Fields.LinkedProduct.AddNew").Text.EncodeJsString('\'', false)) + .Caption(T("Admin.Catalog.Products.ProductVariantAttributes.Attributes.Values.LinkProduct").Text.EncodeJsString('\'', false)) + .OnSelectionCompleted("PickLinkedProduct_Completed")) +
                                    +
                                    @Html.ValidationMessageFor(model => model.LinkedProductId) @@ -58,6 +68,9 @@ @Html.SmartLabelFor(model => model.Locales[item].Name) + @*IMPORTANT: Do not delete, this hidden element contains the id to assign localized values to the corresponding language *@ + @Html.HiddenFor(model => model.Locales[item].LanguageId) + @Html.EditorFor(model => Model.Locales[item].Name) @Html.ValidationMessageFor(model => model.Locales[item].Name) @@ -71,11 +84,6 @@ @Html.ValidationMessageFor(model => model.Locales[item].Alias) - - - @Html.HiddenFor(model => model.Locales[item].LanguageId) - - , @ @@ -159,9 +167,8 @@
                                    - + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ProductReview/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ProductReview/_CreateOrUpdate.cshtml index e3cb60068a..5b013c3acd 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ProductReview/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ProductReview/_CreateOrUpdate.cshtml @@ -8,7 +8,9 @@ @Html.SmartLabelFor(model => model.ProductName) - @Html.ActionLink(Model.ProductName, "Edit", "Product", new { id = Model.ProductId }, new { }) +
                                    + @Html.ActionLink(Model.ProductName, "Edit", "Product", new { id = Model.ProductId }, new { }) +
                                    @@ -16,15 +18,17 @@ @Html.SmartLabelFor(model => model.CustomerId) - @Html.ActionLink(Model.CustomerName, "Edit", "Customer", new { id = Model.CustomerId }, new { }) - +
                                    + @Html.ActionLink(Model.CustomerName, "Edit", "Customer", new { id = Model.CustomerId }, new { }) +
                                    + @Html.SmartLabelFor(model => model.IpAddress) - @Html.DisplayFor(model => model.IpAddress) + @Html.TextBoxFor(model => model.IpAddress, new { @readonly = "readonly", @class = "form-control-plaintext" }) @@ -41,7 +45,7 @@ @Html.SmartLabelFor(model => model.ReviewText) - @Html.TextAreaFor(model => model.ReviewText, new { style = "Width: 500px; Height: 150px;" }) + @Html.TextAreaFor(model => model.ReviewText, new { style = "height:150px" }) @Html.ValidationMessageFor(model => model.ReviewText) @@ -50,7 +54,7 @@ @Html.SmartLabelFor(model => model.Rating) - @Html.DisplayFor(model => model.Rating) + @Html.TextBoxFor(model => model.Rating, new { @readonly = "readonly", @class = "form-control-plaintext" }) @@ -67,7 +71,7 @@ @Html.SmartLabelFor(model => model.CreatedOn) - @Html.DisplayFor(model => model.CreatedOn) + @Html.TextBoxFor(model => model.CreatedOn, new { @readonly = "readonly", @class = "form-control-plaintext" }) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/QuantityUnit/Create.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/QuantityUnit/Create.cshtml index a838ae4a60..311c5de9f5 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/QuantityUnit/Create.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/QuantityUnit/Create.cshtml @@ -9,8 +9,13 @@ @T("Admin.Configuration.QuantityUnit.AddNew") @Html.ActionLink("(" + T("Admin.Common.BackToList") + ")", "List")
                                    - - + +
                                    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/QuantityUnit/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/QuantityUnit/Edit.cshtml index 13a858b56a..41d7320538 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/QuantityUnit/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/QuantityUnit/Edit.cshtml @@ -1,6 +1,5 @@ @model QuantityUnitModel @{ - //page title ViewBag.Title = T("Admin.Configuration.QuantityUnit.EditQuantityUnitDetails").Text; } @using (Html.BeginForm()) @@ -10,9 +9,17 @@ @T("Admin.Configuration.QuantityUnit.EditQuantityUnitDetails") - @Model.Name @Html.ActionLink("(" + @T("Admin.Common.BackToList") + ")", "List")
                                    - - - + + +
                                    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/QuantityUnit/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/QuantityUnit/List.cshtml index a97430855e..536eb76f19 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/QuantityUnit/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/QuantityUnit/List.cshtml @@ -12,34 +12,30 @@ - - - - -
                                    - @(Html.Telerik().Grid() - .Name("measure-unit-grid") - .BindTo(Model.Data) - .Columns(columns => - { - columns.Bound(x => x.Name) - .Template(x => Html.ActionLink(x.Name, "Edit", new { id = x.Id })) - .ClientTemplate("<#= Name #>"); - columns.Bound(x => x.Description) - .Centered(); - columns.Bound(x => x.DisplayOrder) - .Width(100) - .Centered(); - columns.Bound(x => x.IsDefault) - .Template(item => @Html.SymbolForBool(item.IsDefault)) - .ClientTemplate(@Html.SymbolForBool("IsDefault")) - .Width(100) - .Centered(); - }) - ) -
                                    +
                                    + @(Html.Telerik().Grid() + .Name("measure-unit-grid") + .BindTo(Model.Data) + .Columns(columns => + { + columns.Bound(x => x.Name) + .Template(x => Html.ActionLink(x.Name, "Edit", new { id = x.Id })) + .ClientTemplate("<#= Name #>"); + columns.Bound(x => x.Description) + .Centered(); + columns.Bound(x => x.DisplayOrder) + .Width(100) + .Centered(); + columns.Bound(x => x.IsDefault) + .Template(item => @Html.SymbolForBool(item.IsDefault)) + .ClientTemplate(@Html.SymbolForBool("IsDefault")) + .Width(100) + .Centered(); + }) + ) +
                                    \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/QuantityUnit/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/QuantityUnit/_CreateOrUpdate.cshtml index c1022d62af..ef74e483f3 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/QuantityUnit/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/QuantityUnit/_CreateOrUpdate.cshtml @@ -12,6 +12,9 @@ @Html.SmartLabelFor(model => model.Locales[item].Name) + @*IMPORTANT: Do not delete, this hidden element contains the id to assign localized values to the corresponding language *@ + @Html.HiddenFor(model => model.Locales[item].LanguageId) + @Html.EditorFor(model => Model.Locales[item].Name) @Html.ValidationMessageFor(model => model.Locales[item].Name) @@ -25,11 +28,6 @@ @Html.ValidationMessageFor(model => Model.Locales[item].Description) - - - @Html.HiddenFor(model => model.Locales[item].LanguageId) - - , @ diff --git a/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/Edit.cshtml index 79d97ef5bf..2e6588dedd 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/Edit.cshtml @@ -9,16 +9,28 @@ @T("Admin.System.QueuedEmails.EditQueuedEmailDetails") @Html.ActionLink("(" + T("Admin.System.QueuedEmails.BackToList") + ")", "List")
                                    - - - + + + @if (Model.SendManually) { - } - +
                                    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/List.cshtml index 44c2fe7f1e..b31449248f 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/List.cshtml @@ -12,139 +12,121 @@ @T("Admin.System.QueuedEmails")
                                    - + +
                                    + + + - + - -
                                    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                                    - @Html.SmartLabelFor(model => model.SearchStartDate) - - @Html.EditorFor(model => model.SearchStartDate) -
                                    - @Html.SmartLabelFor(model => model.SearchEndDate) - - @Html.EditorFor(model => Model.SearchEndDate) -
                                    - @Html.SmartLabelFor(model => model.SearchFromEmail) - - @Html.EditorFor(model => Model.SearchFromEmail) -
                                    - @Html.SmartLabelFor(model => model.SearchToEmail) - - @Html.EditorFor(model => Model.SearchToEmail) -
                                    - @Html.SmartLabelFor(model => model.SearchLoadNotSent) - - @Html.EditorFor(model => Model.SearchLoadNotSent) -
                                    - @Html.SmartLabelFor(model => model.SearchSendManually) - - @Html.EditorFor(model => Model.SearchSendManually) -
                                    - @Html.SmartLabelFor(model => model.SearchMaxSentTries) - - @Html.EditorFor(model => Model.SearchMaxSentTries) -
                                    - @Html.SmartLabelFor(model => model.GoDirectlyToNumber) - - @Html.EditorFor(model => Model.GoDirectlyToNumber) - -
                                    -   - - -
                                    - -

                                    - - - - - -
                                    - @(Html.Telerik().Grid() - .Name("queuedEmails-grid") - .Columns(columns => - { - columns.Bound(x => x.Id) - .ClientTemplate("") - .Title("") - .HtmlAttributes(new { style = "text-align:center" }) - .HeaderHtmlAttributes(new { style = "text-align:center" }); - columns.Bound(x => x.Id) - .ClientTemplate("\"><#= Id #>"); - columns.Bound(x => x.AttachmentsCount) - .HeaderTemplate(@) - .Width(20); - columns.Bound(x => x.Subject); - columns.Bound(x => x.Priority) - .Centered(); - columns.Bound(x => x.From) - .ClientTemplate("<#= FromName #> (<#= From #>)"); - columns.Bound(x => x.SendManually) - .ClientTemplate(@Html.SymbolForBool("SendManually")) - .Centered(); - columns.Bound(x => x.SentTries) - .Centered(); - columns.Bound(x => x.SentOn); - columns.Bound(x => x.CreatedOn); - }) - .Pageable(settings => settings.PageSize(gridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("QueuedEmailList", "QueuedEmail")) - .ClientEvents(events => events.OnDataBinding("onDataBinding").OnDataBound("onDataBound").OnRowDataBound("onRowDataBound")) - .PreserveGridState() - .EnableCustomBinding(true)) -
                                    - + + + +
                                    + @(Html.Telerik().Grid() + .Name("queuedEmails-grid") + .Columns(columns => + { + columns.Bound(x => x.Id) + .ClientTemplate("") + .Title("") + .HtmlAttributes(new { style = "text-align:center" }) + .HeaderHtmlAttributes(new { style = "text-align:center" }); + columns.Bound(x => x.Id) + .ClientTemplate("\"><#= Id #>"); + columns.Bound(x => x.AttachmentsCount) + .HeaderTemplate(@) + .Width(20); + columns.Bound(x => x.Subject); + columns.Bound(x => x.Priority) + .Centered(); + columns.Bound(x => x.From) + .ClientTemplate("<#= From #>"); + columns.Bound(x => x.SendManually) + .ClientTemplate(@Html.SymbolForBool("SendManually")) + .Centered(); + columns.Bound(x => x.SentTries) + .Centered(); + columns.Bound(x => x.SentOn); + columns.Bound(x => x.CreatedOn); + }) + .Pageable(settings => settings.PageSize(gridPageSize).Position(GridPagerPosition.Both)) + .DataBinding(dataBinding => dataBinding.Ajax().Select("QueuedEmailList", "QueuedEmail")) + .ClientEvents(events => events.OnDataBinding("onDataBinding").OnDataBound("onDataBound").OnRowDataBound("onRowDataBound")) + .PreserveGridState() + .EnableCustomBinding(true)) +
                                    + } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/_CreateOrUpdate.cshtml index b1720f9b11..c8b415c3f6 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/QueuedEmail/_CreateOrUpdate.cshtml @@ -1,7 +1,4 @@ @model QueuedEmailModel - - -@using Telerik.Web.Mvc.UI; @Html.ValidationSummary(true) @Html.HiddenFor(model => model.Id) @@ -25,15 +22,6 @@ @Html.ValidationMessageFor(model => model.From) - - - @Html.SmartLabelFor(model => model.FromName) - - - @Html.EditorFor(model => model.FromName) - @Html.ValidationMessageFor(model => model.FromName) - - @Html.SmartLabelFor(model => model.To) @@ -43,15 +31,6 @@ @Html.ValidationMessageFor(model => model.To) - - - @Html.SmartLabelFor(model => model.ToName) - - - @Html.EditorFor(model => model.ToName) - @Html.ValidationMessageFor(model => model.ToName) - - @Html.SmartLabelFor(model => model.CC) @@ -75,7 +54,7 @@ @Html.SmartLabelFor(model => model.Subject) - @Html.TextBoxFor(model => model.Subject, new { @class = "input-large" }) + @Html.TextBoxFor(model => model.Subject) @Html.ValidationMessageFor(model => model.Subject) @@ -88,7 +67,7 @@ @foreach (var attach in Model.Attachments) { - + @attach.Name @@ -100,8 +79,8 @@ @Html.SmartLabelFor(model => model.Body) - - @Html.EditorFor(model => model.Body, Html.RichEditorFlavor()) + + @Html.EditorFor(model => model.Body, "Html") @Html.ValidationMessageFor(model => model.Body) @@ -130,11 +109,11 @@ @if (Model.SentOn.HasValue) { - @Html.DisplayFor(model => model.SentOn) - } - else - { - @("".NaIfEmpty()) + @Html.TextBoxFor(model => model.SentOn, new { @readonly = "readonly", @class = "form-control-plaintext" }) + } + else + { +
                                    @("".NaIfEmpty())
                                    } @@ -143,7 +122,7 @@ @Html.SmartLabelFor(model => model.EmailAccountName) - @Html.DisplayFor(model => model.EmailAccountName) + @Html.TextBoxFor(model => model.EmailAccountName, new { @readonly = "readonly", @class = "form-control-plaintext" }) @@ -151,7 +130,7 @@ @Html.SmartLabelFor(model => model.CreatedOn) - @Html.DisplayFor(model => model.CreatedOn) + @Html.TextBoxFor(model => model.CreatedOn, new { @readonly = "readonly", @class = "form-control-plaintext" }) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/RecurringPayment/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/RecurringPayment/Edit.cshtml index 89e766857e..ed04ef37b0 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/RecurringPayment/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/RecurringPayment/Edit.cshtml @@ -1,6 +1,5 @@ @model RecurringPaymentModel @{ - //page title ViewBag.Title = T("Admin.RecurringPayments.EditPaymentDetails").Text; } @using (Html.BeginForm()) @@ -10,9 +9,17 @@ @T("Admin.RecurringPayments.EditPaymentDetails") @Html.ActionLink("(" + T("Admin.RecurringPayments.BackToList") + ")", "List")
                                    - - - + + +
                                    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/RecurringPayment/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/RecurringPayment/List.cshtml index 7c20b8063e..3aaf4761bc 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/RecurringPayment/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/RecurringPayment/List.cshtml @@ -4,7 +4,6 @@ @{ var gridPageSize = EngineContext.Current.Resolve().GridPageSize; - //page title ViewBag.Title = T("Admin.RecurringPayments").Text; }
                                    @@ -16,47 +15,43 @@
                                    - - - - -
                                    - @(Html.Telerik().Grid(Model.Data) - .Name("recurringpayments-grid") - .Columns(columns => - { - columns.Bound(x => x.CustomerId) - .Template(x => Html.ActionLink(x.CustomerEmail, "Edit", "Customer", new { id = x.CustomerId }, new { })) - .ClientTemplate("\"><#= CustomerEmail #>") - .Width(100); - columns.Bound(x => x.CycleLength) - .Width(50); - columns.Bound(x => x.CyclePeriodStr) - .Width(100) - .Centered(); - columns.Bound(x => x.IsActive) - .Width(100) - .Template(item => @Html.SymbolForBool(item.IsActive)) - .ClientTemplate(@Html.SymbolForBool("IsActive")) - .Centered(); - columns.Bound(x => x.StartDate) - .Width(100) - .Centered(); - columns.Bound(x => x.NextPaymentDate) - .Width(100); - columns.Bound(x => x.TotalCycles) - .Width(50); - columns.Bound(x => x.CyclesRemaining) - .Width(50); - columns.Bound(x => x.Id) - .Width(50) - .Centered() - .Template(x => Html.ActionLink(T("Admin.Common.Edit").Text, "Edit", new { id = x.Id })) - .ClientTemplate("\">" + T("Admin.Common.Edit").Text + "") - .Title(T("Admin.Common.Edit").Text); - }) - .Pageable(settings => settings.Total(Model.Total).PageSize(gridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("List", "RecurringPayment")) - .PreserveGridState() - .EnableCustomBinding(true)) -
                                    \ No newline at end of file + +
                                    + @(Html.Telerik().Grid(Model.Data) + .Name("recurringpayments-grid") + .Columns(columns => + { + columns.Bound(x => x.CustomerId) + .Template(x => Html.ActionLink(x.CustomerEmail, "Edit", "Customer", new { id = x.CustomerId }, new { })) + .ClientTemplate("\"><#= CustomerEmail #>") + .Width(100); + columns.Bound(x => x.CycleLength) + .Width(50); + columns.Bound(x => x.CyclePeriodStr) + .Width(100) + .Centered(); + columns.Bound(x => x.IsActive) + .Width(100) + .Template(item => @Html.SymbolForBool(item.IsActive)) + .ClientTemplate(@Html.SymbolForBool("IsActive")) + .Centered(); + columns.Bound(x => x.StartDate) + .Width(100); + columns.Bound(x => x.NextPaymentDate) + .Width(100); + columns.Bound(x => x.TotalCycles) + .Width(50); + columns.Bound(x => x.CyclesRemaining) + .Width(50); + columns.Bound(x => x.Id) + .Width(100) + .Template(x => Html.ActionLink(T("Admin.Common.Edit").Text, "Edit", new { id = x.Id }, new { @class = "t-button" })) + .ClientTemplate("\" class=\"t-button\">" + T("Admin.Common.Edit").Text + "") + .Title(String.Empty) + .HtmlAttributes(new { align = "right" }); + }) + .Pageable(settings => settings.Total(Model.Total).PageSize(gridPageSize).Position(GridPagerPosition.Both)) + .DataBinding(dataBinding => dataBinding.Ajax().Select("List", "RecurringPayment")) + .PreserveGridState() + .EnableCustomBinding(true)) +
                                    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/RecurringPayment/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/RecurringPayment/_CreateOrUpdate.cshtml index 9091971da4..f2a130c7fa 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/RecurringPayment/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/RecurringPayment/_CreateOrUpdate.cshtml @@ -5,24 +5,27 @@ @Html.ValidationSummary(false) @Html.HiddenFor(model => model.Id) -@Html.SmartStore().TabStrip().Name("recurringpayment-edit").Items(x => + +@Html.SmartStore().TabStrip().Name("recurringpayment-edit").Style(TabsStyle.Material).Items(x => { - var tabInfo = x.Add().Text(T("Admin.RecurringPayments.Info").Text).Content(TabInfo()).Selected(true); - var tabHistory = x.Add().Text(T("Admin.RecurringPayments.History").Text).Content(TabHistory()); + x.Add().Text(T("Admin.RecurringPayments.Info").Text).Content(TabInfo()).Selected(true); + x.Add().Text(T("Admin.RecurringPayments.History").Text).Content(TabHistory()); //generate an event EngineContext.Current.Resolve().Publish(new TabStripCreated(x, "recurringpayment-edit", this.Html, this.Model)); }) + @helper TabInfo() - { - +{ @@ -30,15 +33,17 @@ @Html.SmartLabelFor(model => model.CustomerEmail) +
                                    + @Html.ActionLink(Model.CustomerEmail, "Edit", "Customer", new { id = Model.CustomerId }, new { }) +
                                    + @@ -56,7 +61,7 @@ @Html.SmartLabelFor(model => model.TotalCycles) @@ -65,15 +70,19 @@ @Html.SmartLabelFor(model => model.CyclesRemaining) +
                                    + @Model.CyclesRemaining +
                                    + @@ -81,7 +90,9 @@ @Html.SmartLabelFor(model => model.StartDate) @@ -95,28 +106,35 @@
                                    @Html.SmartLabelFor(model => model.InitialOrderId) - @Html.ActionLink(T("Admin.Common.View").Text, "Edit", "Order", new { id = Model.InitialOrderId }, new { }) +
                                    + @Html.ActionLink(T("Admin.Common.View").Text, "Edit", "Order", new { id = Model.InitialOrderId }, new { }) +
                                    - @Html.ActionLink(Model.CustomerEmail, "Edit", "Customer", new { id = Model.CustomerId }, new { }) -
                                    @Html.SmartLabelFor(model => model.CycleLength) - @Html.Telerik().IntegerTextBoxFor(model => model.CycleLength).MinValue(1) + @Html.EditorFor(model => model.CycleLength) @Html.ValidationMessageFor(model => model.CycleLength)
                                    - @Html.Telerik().IntegerTextBoxFor(model => model.TotalCycles).MinValue(1) + @Html.EditorFor(model => model.TotalCycles) @Html.ValidationMessageFor(model => model.TotalCycles)
                                    - @Model.CyclesRemaining -
                                    @Html.SmartLabelFor(model => model.PaymentType) - @Model.PaymentType +
                                    + @Model.PaymentType +
                                    - @Model.StartDate +
                                    + @Model.StartDate +
                                    } + @helper TabHistory() - { - +{ + - diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ReturnRequest/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ReturnRequest/Edit.cshtml index 7fcbc10e62..6dd81bd552 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ReturnRequest/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ReturnRequest/Edit.cshtml @@ -11,24 +11,37 @@
                                    @if (Model.CanAccept) { - } @if (Model.CanSendEmailToCustomer) { - + } - - - + + +
                                    Html.RenderPartial("_CreateOrUpdate", Model); } + @Html.DeleteConfirmation("returnrequest-delete") @{ Html.RenderPartial("~/Administration/Views/Order/_AutoUpdateOrderItem.cshtml", Model.AutoUpdateOrderItem); } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ReturnRequest/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ReturnRequest/List.cshtml index c9a62c9b5c..84593db1a6 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ReturnRequest/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ReturnRequest/List.cshtml @@ -13,91 +13,71 @@ -
                                    - @if (!String.IsNullOrEmpty(Model.NextPaymentDate)) - { - @T("Admin.RecurringPayments.History.NextPaymentDate") - - : @Model.NextPaymentDate - - - } - @if (Model.CanCancelRecurringPayment) - { - - } + +
                                    + @if (!String.IsNullOrEmpty(Model.NextPaymentDate)) + { +
                                    + @T("Admin.RecurringPayments.History.NextPaymentDate"): @Model.NextPaymentDate +
                                    + +
                                    + +
                                    + } + @if (Model.CanCancelRecurringPayment) + { +
                                    + +
                                    + } +
                                    - - - - +
                                    +
                                    + @Html.SmartLabelFor(model => model.SearchId) + @Html.TextBoxFor(model => Model.SearchId, new { @class = "form-control" }) +
                                    @if (Model.AvailableStores.Count > 1) { -
                                    - - - +
                                    + @Html.SmartLabelFor(model => model.SearchStoreId) + @Html.DropDownListFor(model => model.SearchStoreId, Model.AvailableStores, T("Admin.Common.All"), new { @class = "form-control" }) +
                                    } - - - - - - - - -
                                    - @Html.SmartLabelFor(model => model.SearchId) - - @Html.EditorFor(model => Model.SearchId) -
                                    - @Html.SmartLabelFor(model => model.SearchStoreId) - - @Html.DropDownListFor(model => model.SearchStoreId, Model.AvailableStores, T("Admin.Common.All")) -
                                    - @Html.SmartLabelFor(model => model.SearchReturnRequestStatusId) - - @Html.DropDownListFor(model => model.SearchReturnRequestStatusId, Model.AvailableReturnRequestStatus, T("Admin.Common.All")) -
                                    -   - - -
                                    - -

                                    +
                                    + @Html.SmartLabelFor(model => model.SearchReturnRequestStatusId) + @Html.DropDownListFor(model => model.SearchReturnRequestStatusId, Model.AvailableReturnRequestStatus, T("Admin.Common.All")) +
                                    +
                                    + + +
                                    + - - - - -
                                    - @(Html.Telerik().Grid() - .Name("returnrequests-grid") - .Columns(columns => - { - columns.Bound(x => x.Id) - .Centered() - .Width(80); - columns.Bound(x => x.ProductName) - .Template(x => @Html.LabeledProductName(x.ProductId, x.ProductName, x.ProductTypeName, x.ProductTypeLabelHint)) - .ClientTemplate(@Html.LabeledProductName("ProductId", "ProductName")); - columns.Bound(x => x.Quantity) - .Width(80) - .Centered(); - columns.Bound(x => x.CustomerId) - .Template(x => Html.ActionLink(T("Admin.Common.View").Text, "Edit", "Customer", new { id = x.CustomerId }, new { })) - .ClientTemplate("\"><#= CustomerFullName #>"); - columns.Bound(x => x.OrderId) - .Width(80) - .Centered() - .Template(x => Html.ActionLink(T("Admin.Common.View").Text, "Edit", "Order", new { id = x.OrderId }, new { })) - .ClientTemplate("\"><#= OrderId #>"); - columns.Bound(x => x.ReturnRequestStatusStr) - .Width(180); - columns.Bound(x => x.CreatedOn) - .Width(180); - columns.Bound(x => x.StoreName) - .Hidden(Model.AvailableStores.Count <= 1); - columns.Bound(x => x.Id) - .Width(100) - .Centered() - .Template(x => Html.ActionLink(T("Admin.Common.Edit").Text, "Edit", new { id = x.Id })) - .ClientTemplate("\">" + T("Admin.Common.Edit").Text + "") - .Title(T("Admin.Common.Edit").Text); - }) - .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("List", "ReturnRequest")) - .ClientEvents(events => events.OnDataBinding("returnRequestGrid_onDataBinding")) - .PreserveGridState() - .EnableCustomBinding(true)) -
                                    +
                                    + @(Html.Telerik().Grid() + .Name("returnrequests-grid") + .Columns(columns => + { + columns.Bound(x => x.Id) + .Centered() + .Width(80); + columns.Bound(x => x.ProductName) + .Template(x => @Html.LabeledProductName(x.ProductId, x.ProductName, x.ProductTypeName, x.ProductTypeLabelHint)) + .ClientTemplate(@Html.LabeledProductName("ProductId", "ProductName")); + columns.Bound(x => x.Quantity) + .Width(80) + .Centered(); + columns.Bound(x => x.CustomerId) + .Template(x => Html.ActionLink(T("Admin.Common.View").Text, "Edit", "Customer", new { id = x.CustomerId }, new { })) + .ClientTemplate("\"><#= CustomerFullName #>"); + columns.Bound(x => x.OrderId) + .Width(80) + .Centered() + .Template(x => Html.ActionLink(T("Admin.Common.View").Text, "Edit", "Order", new { id = x.OrderId }, new { })) + .ClientTemplate("\"><#= OrderId #>"); + columns.Bound(x => x.ReturnRequestStatusStr) + .Width(180); + columns.Bound(x => x.CreatedOn) + .Width(180); + columns.Bound(x => x.StoreName) + .Hidden(Model.AvailableStores.Count <= 1); + columns.Bound(x => x.Id) + .Width(100) + .Centered() + .Template(x => Html.ActionLink(T("Admin.Common.Edit").Text, "Edit", new { id = x.Id })) + .ClientTemplate("\">" + T("Admin.Common.Edit").Text + "") + .Title(T("Admin.Common.Edit").Text); + }) + .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) + .DataBinding(dataBinding => dataBinding.Ajax().Select("List", "ReturnRequest")) + .ClientEvents(events => events.OnDataBinding("returnRequestGrid_onDataBinding")) + .PreserveGridState() + .EnableCustomBinding(true)) +
                                    + + @Html.SmartCssFiles(this.Url, ResourceLocation.Head) + @Html.SmartScripts(this.Url, ResourceLocation.Head) + + + + + + + + + + + +
                                    +
                                    + + + + +
                                    +
                                    +
                                    Loading directories...
                                    + +
                                    +
                                    +
                                      +
                                      +
                                      + + +
                                      +
                                      + +
                                      +
                                      + + + + +
                                      +
                                      + + +
                                      +
                                      + + + +
                                      +
                                      + +
                                      +
                                      +
                                      +
                                      +
                                      +
                                      Loading files...
                                      +
                                      +
                                      +
                                      +
                                      +
                                      + This folder is empty +
                                      +
                                      + No files found +
                                      +
                                        +
                                        +
                                        +
                                        +    © 2013 - RoxyFileman + +
                                        Status bar
                                        +
                                        + + +
                                        +
                                        + +
                                        +
                                        + +
                                        +
                                        +
                                        +
                                        +
                                        +
                                        +
                                        + + +
                                        +
                                        + +
                                        +
                                        +
                                        + +
                                        + + @Html.SmartScripts(this.Url, ResourceLocation.Foot) + @Scripts.Render("~/bundles/roxyfm") + @Html.LocalizationScript(WorkContext.WorkingLanguage.UniqueSeoCode, "~/Administration/Content/filemanager/lang/", "*.js", null) + + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/Edit.cshtml index a1018ad9ea..4fa1223183 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/Edit.cshtml @@ -20,11 +20,19 @@ (@T("Common.Back"))
                                        - - + + @if (!Model.IsRunning) { -  @T("Admin.System.ScheduleTasks.RunNow") + + + @T("Admin.System.ScheduleTasks.RunNow") + }
                                        @@ -64,8 +72,9 @@ @Html.SmartLabelFor(model => model.LastStart) - - @(Model.LastStart.HasValue ? Model.LastStart.Value.ToString("g") : T("Common.Never").Text) +
                                        + @(Model.LastStart.HasValue ? Model.LastStart.Value.ToString("g") : T("Common.Never").Text) +
                                        @if (Model.Duration.HasValue()) @@ -75,7 +84,9 @@ @Html.SmartLabelFor(model => model.Duration) - @Html.DisplayFor(model => model.Duration) +
                                        + @Html.DisplayFor(model => model.Duration) +
                                        } @@ -86,7 +97,9 @@ @Html.SmartLabelFor(model => model.LastError) - @Html.DisplayFor(model => model.LastError) +
                                        + @Html.DisplayFor(model => model.LastError) +
                                        if (Model.LastSuccess.HasValue && Model.LastSuccess != Model.LastEnd) @@ -96,7 +109,9 @@ @Html.SmartLabelFor(model => model.LastSuccess) - @Model.LastSuccess.Value.ToString("g") +
                                        + @Model.LastSuccess.Value.ToString("g") +
                                        } @@ -108,7 +123,9 @@ @Html.SmartLabelFor(model => model.NextRun) - @Model.NextRun.Value.ToString("g") +
                                        + @Model.NextRun.Value.ToString("g") +
                                        } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/List.cshtml index 92f7eef418..46c1ae1718 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/List.cshtml @@ -12,73 +12,78 @@ - - - - - - - - - - - - - @foreach (var task in Model) - { - - - - - - - - - - +
                                        +
                                        @T("Admin.System.ScheduleTasks.Name")@T("Admin.System.ScheduleTasks.Enabled")@T("Admin.System.ScheduleTasks.CronExpression")@T("Admin.System.ScheduleTasks.LastStart")@T("Admin.System.ScheduleTasks.NextRun")@T("Admin.Common.Actions")
                                        - @task.Name - @Html.SymbolForBool(task.Enabled) -
                                        @task.CronExpression
                                        - @if (task.CronDescription.HasValue()) - { -
                                        @task.CronDescription
                                        - } -
                                        -
                                        - @{ Html.RenderPartial("_LastRun", task); } -
                                        -
                                        -
                                        - @{ Html.RenderPartial("_NextRun", task); } -
                                        -
                                        -
                                        - @T("Common.Edit") - - @T("Admin.System.ScheduleTasks.RunNow") - - - @T("Common.Cancel") - -
                                        + + + + + + + + - } - -
                                        @T("Admin.System.ScheduleTasks.Name")@T("Admin.System.ScheduleTasks.Enabled")@T("Admin.System.ScheduleTasks.CronExpression")@T("Admin.System.ScheduleTasks.LastStart")@T("Admin.System.ScheduleTasks.NextRun") 
                                        + + + @foreach (var task in Model) + { + + + @task.Name + + @Html.SymbolForBool(task.Enabled) + +
                                        @task.CronExpression
                                        + @if (task.CronDescription.HasValue()) + { +
                                        @task.CronDescription
                                        + } + + + +
                                        + @{ Html.RenderPartial("_LastRun", task); } +
                                        + + + +
                                        + @{ Html.RenderPartial("_NextRun", task); } +
                                        +
                                        + + + + @T("Common.Edit") + + @T("Admin.System.ScheduleTasks.RunNow") + + + @T("Common.Cancel") + + + + } + + + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/AllSettings.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/AllSettings.cshtml index 2ad6b7fb2e..eeb3e7d7aa 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/AllSettings.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/AllSettings.cshtml @@ -1,7 +1,5 @@ @{ var gridPageSize = EngineContext.Current.Resolve().GridPageSize; - - //page title ViewBag.Title = T("Admin.Configuration.Settings.AllSettings").Text; } @using Telerik.Web.Mvc.UI; @@ -17,55 +15,56 @@ @T("Admin.Configuration.Settings.AllSettings.Description") - - - - -
                                        - @(Html.Telerik().Grid() - .Name("settings-grid") - .DataKeys(x => - { - x.Add(y => y.Id).RouteKey("Id"); - }) - .Columns(columns => - { - columns.Bound(x => x.Name).Width("25%"); - columns.Bound(x => x.Value).Width("55%").EditorTemplateName("MultilineText"); - columns.Bound(x => x.Store).EditorTemplateName("Store").Width("15%"); - columns.Command(commands => - { - commands.Edit().Localize(T); - commands.Delete().Localize(T); - }); +
                                        + @(Html.Telerik().Grid() + .Name("settings-grid") + .DataKeys(x => + { + x.Add(y => y.Id).RouteKey("Id"); + }) + .Columns(columns => + { + columns.Bound(x => x.Name).Width("25%"); + columns.Bound(x => x.Value).Width("55%").EditorTemplateName("MultilineText"); + columns.Bound(x => x.Store).EditorTemplateName("Store").Width("15%"); + columns.Command(commands => + { + commands.Edit().Localize(T); + commands.Delete().Localize(T); + }) + .HtmlAttributes(new { align = "right" }); + }) + .ToolBar(x => x.Insert()) + .Editable(x => + { + x.Mode(GridEditMode.InLine); + }) + .Filterable() + .Pageable(settings => settings.PageSize(gridPageSize).Position(GridPagerPosition.Both)) + .DataBinding(dataBinding => + { + dataBinding.Ajax().Select("AllSettings", "Setting") + .Update("SettingUpdate", "Setting") + .Delete("SettingDelete", "Setting") + .Insert("SettingAdd", "Setting"); + }) + .ClientEvents(x => x.OnError("grid_onError").OnEdit("grid_onStoreEdit")) + .PreserveGridState() + .EnableCustomBinding(true)) +
                                        - }) - .ToolBar(x => x.Insert()) - .Editable(x => - { - x.Mode(GridEditMode.InLine); - }) - .Filterable() - .Pageable(settings => settings.PageSize(gridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => - { - dataBinding.Ajax().Select("AllSettings", "Setting") - .Update("SettingUpdate", "Setting") - .Delete("SettingDelete", "Setting") - .Insert("SettingAdd", "Setting"); - }) - .ClientEvents(x => x.OnError("grid_onError").OnEdit("grid_onStoreEdit")) - .PreserveGridState() - .EnableCustomBinding(true)) - -
                                        + function grid_onStoreEdit(e) { + if (e.mode == "edit") { + _.delay(function () { + $('#Store').val(e.dataItem['StoreId']).trigger('change'); + }, 0); + } + } + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Blog.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Blog.cshtml index 17e11145c5..727b184698 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Blog.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Blog.cshtml @@ -1,7 +1,6 @@ @model BlogSettingsModel @using Telerik.Web.Mvc.UI; @{ - //page title ViewBag.Title = T("Admin.Configuration.Settings.Blog").Text; } @using (Html.BeginForm()) @@ -12,7 +11,10 @@ @T("Admin.Configuration.Settings.Blog")
                                        - +
                                        diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml index 1889ad8011..95e2e7cc4a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml @@ -1,5 +1,4 @@ @model CatalogSettingsModel -@using Telerik.Web.Mvc.UI; @{ ViewBag.Title = T("Admin.Configuration.Settings.Catalog").Text; } @@ -11,164 +10,73 @@ @T("Admin.Configuration.Settings.Catalog")
                                        - +
                                        - Html.RenderAction("StoreScopeConfiguration", "Setting"); @Html.ValidationSummary(false) - @(Html.SmartStore().TabStrip().Name("catalogsettings-edit").Items(x => + @(Html.SmartStore().TabStrip().Name("catalogsettings-edit").Style(TabsStyle.Material).Items(x => { x.Add().Text(T("Admin.Configuration.Settings.Catalog.MiscSettings").Text).Content(@TabMiscSettings()).Selected(true); x.Add().Text(T("Admin.Configuration.Settings.Catalog.ProductListSettings").Text).Content(@TabProductListSettings()); x.Add().Text(T("Admin.Configuration.Settings.Catalog.ProductDetailSettings").Text).Content(@TabProductDetailSettings()); x.Add().Text(T("Admin.Configuration.Settings.Catalog.UserSettings").Text).Content(@TabUserSettings()); + x.Add().Text(T("Admin.Catalog.Manufacturers").Text).Content(@TabBrandsSettings()); })) @helper TabMiscSettings() { + + + + + + + + + + + + + + + +
                                        +
                                        +
                                        @T("Admin.CatalogSettings.Homepage")
                                        +
                                        +
                                        + @Html.SmartLabelFor(model => model.ShowBestsellersOnHomepage) + + @Html.SettingEditorFor(model => model.ShowBestsellersOnHomepage, Html.CheckBoxFor(model => model.ShowBestsellersOnHomepage, new { data_toggler_for = "#pnlNumberOfBestsellersOnHomepage" })) + @Html.ValidationMessageFor(model => model.ShowBestsellersOnHomepage) +
                                        + @Html.SmartLabelFor(model => model.NumberOfBestsellersOnHomepage) + + @Html.SettingEditorFor(model => model.NumberOfBestsellersOnHomepage) + @Html.ValidationMessageFor(model => model.NumberOfBestsellersOnHomepage) +
                                        + @Html.SmartLabelFor(model => model.ShowPopularProductTagsOnHomepage) + + @Html.SettingEditorFor(model => model.ShowPopularProductTagsOnHomepage) + @Html.ValidationMessageFor(model => model.ShowPopularProductTagsOnHomepage) +
                                        + + + + + + + + + - - -
                                        +
                                        +
                                        @T("Admin.CatalogSettings.ProductDisplay")
                                        +
                                        +
                                        @Html.SmartLabelFor(model => model.ShowProductSku) @@ -226,101 +134,123 @@
                                        - @Html.SmartLabelFor(model => model.IgnoreDiscounts) + @Html.SmartLabelFor(model => model.ShowDefaultQuantityUnit) - @Html.SettingEditorFor(model => model.IgnoreDiscounts) - @Html.ValidationMessageFor(model => model.IgnoreDiscounts) + @Html.SettingEditorFor(model => model.ShowDefaultQuantityUnit) + @Html.ValidationMessageFor(model => model.ShowDefaultQuantityUnit)
                                        - @Html.SmartLabelFor(model => model.ApplyTierPricePercentageToAttributePriceAdjustments) + @Html.SmartLabelFor(model => model.ShowDefaultDeliveryTime) - @Html.SettingEditorFor(model => model.ApplyTierPricePercentageToAttributePriceAdjustments) - @Html.ValidationMessageFor(model => model.ApplyTierPricePercentageToAttributePriceAdjustments) + @Html.SettingEditorFor(model => model.ShowDefaultDeliveryTime) + @Html.ValidationMessageFor(model => model.ShowDefaultDeliveryTime)
                                        + @Html.SmartLabelFor(model => model.DeliveryTimeIdForEmptyStock) + + @Html.SettingEditorFor(model => model.DeliveryTimeIdForEmptyStock, @Html.DropDownListFor(model => model.DeliveryTimeIdForEmptyStock, Model.AvailableDeliveryTimes, T("Common.Unspecified"))) + @Html.ValidationMessageFor(model => model.DeliveryTimeIdForEmptyStock) +
                                        - @Html.SmartLabelFor(model => model.IgnoreFeaturedProducts) + @Html.SmartLabelFor(model => model.HideProductDefaultPictures) - @Html.SettingEditorFor(model => model.IgnoreFeaturedProducts) - @Html.ValidationMessageFor(model => model.IgnoreFeaturedProducts) + @Html.SettingEditorFor(model => model.HideProductDefaultPictures) + @Html.ValidationMessageFor(model => model.HideProductDefaultPictures)
                                        - @Html.SmartLabelFor(model => model.CompareProductsEnabled) - - @Html.SettingEditorFor(model => model.CompareProductsEnabled) - @Html.ValidationMessageFor(model => model.CompareProductsEnabled) +
                                        + + + + - + - + +
                                        +
                                        +
                                        @T("Admin.CatalogSettings.Prices")
                                        +
                                        - @Html.SmartLabelFor(model => model.IncludeShortDescriptionInCompareProducts) + @Html.SmartLabelFor(model => model.IgnoreDiscounts) - @Html.SettingEditorFor(model => model.IncludeShortDescriptionInCompareProducts) - @Html.ValidationMessageFor(model => model.IncludeShortDescriptionInCompareProducts) + @Html.SettingEditorFor(model => model.IgnoreDiscounts) + @Html.ValidationMessageFor(model => model.IgnoreDiscounts)
                                        - @Html.SmartLabelFor(model => model.IncludeFullDescriptionInCompareProducts) + @Html.SmartLabelFor(model => model.ApplyTierPricePercentageToAttributePriceAdjustments) - @Html.SettingEditorFor(model => model.IncludeFullDescriptionInCompareProducts) - @Html.ValidationMessageFor(model => model.IncludeFullDescriptionInCompareProducts) + @Html.SettingEditorFor(model => model.ApplyTierPricePercentageToAttributePriceAdjustments) + @Html.ValidationMessageFor(model => model.ApplyTierPricePercentageToAttributePriceAdjustments)
                                        + + - - - + + + + + + + + + + + + +
                                        - @Html.SmartLabelFor(model => model.ShowBestsellersOnHomepage) - - @Html.SettingEditorFor(model => model.ShowBestsellersOnHomepage) - @Html.ValidationMessageFor(model => model.ShowBestsellersOnHomepage) + +
                                        +
                                        @T("Admin.CatalogSettings.CompareProducts")
                                        +
                                        - @Html.SmartLabelFor(model => model.NumberOfBestsellersOnHomepage) + @Html.SmartLabelFor(model => model.CompareProductsEnabled) - @Html.SettingEditorFor(model => model.NumberOfBestsellersOnHomepage) - @Html.ValidationMessageFor(model => model.NumberOfBestsellersOnHomepage) + @Html.SettingEditorFor(model => model.CompareProductsEnabled, Html.CheckBoxFor(model => model.CompareProductsEnabled, new { data_toggler_for = "#pnlCompareProducts" })) + @Html.ValidationMessageFor(model => model.CompareProductsEnabled) +
                                        + @Html.SmartLabelFor(model => model.IncludeShortDescriptionInCompareProducts) + + @Html.SettingEditorFor(model => model.IncludeShortDescriptionInCompareProducts) + @Html.ValidationMessageFor(model => model.IncludeShortDescriptionInCompareProducts) +
                                        + @Html.SmartLabelFor(model => model.IncludeFullDescriptionInCompareProducts) + + @Html.SettingEditorFor(model => model.IncludeFullDescriptionInCompareProducts) + @Html.ValidationMessageFor(model => model.IncludeFullDescriptionInCompareProducts) +
                                        + + + + - - - - - - - - - @@ -329,379 +259,297 @@ @Html.SmartLabelFor(model => model.HtmlTextCollapsedHeight) - - - -
                                        +
                                        +
                                        @T("Common.Misc")
                                        +
                                        - @Html.SmartLabelFor(model => model.HideCategoryDefaultPictures) - - @Html.SettingEditorFor(model => model.HideCategoryDefaultPictures) - @Html.ValidationMessageFor(model => model.HideCategoryDefaultPictures) -
                                        - @Html.SmartLabelFor(model => model.HideProductDefaultPictures) - - @Html.SettingEditorFor(model => model.HideProductDefaultPictures) - @Html.ValidationMessageFor(model => model.HideProductDefaultPictures) -
                                        @Html.SmartLabelFor(model => model.EnableHtmlTextCollapser) - @Html.SettingEditorFor(model => model.EnableHtmlTextCollapser) + @Html.SettingEditorFor(model => model.EnableHtmlTextCollapser, Html.CheckBoxFor(model => model.EnableHtmlTextCollapser, new { data_toggler_for = "#pnlHtmlTextCollapsedHeight" })) @Html.ValidationMessageFor(model => model.EnableHtmlTextCollapser)
                                        - @Html.SettingEditorFor(model => model.HtmlTextCollapsedHeight) + @Html.SettingEditorFor(model => model.HtmlTextCollapsedHeight, null, new { postfix = T("Common.Pixel").Text }) @Html.ValidationMessageFor(model => model.HtmlTextCollapsedHeight)
                                        - @Html.SmartLabelFor(model => model.ShowDefaultQuantityUnit) + @Html.SmartLabelFor(model => model.MaxItemsToDisplayInCatalogMenu) - @Html.SettingEditorFor(model => model.ShowDefaultQuantityUnit) - @Html.ValidationMessageFor(model => model.ShowDefaultQuantityUnit) + @Html.SettingEditorFor(model => model.MaxItemsToDisplayInCatalogMenu) + @Html.ValidationMessageFor(model => model.MaxItemsToDisplayInCatalogMenu)
                                        - @Html.SmartLabelFor(model => model.ShowDefaultDeliveryTime) - - @Html.SettingEditorFor(model => model.ShowDefaultDeliveryTime) - @Html.ValidationMessageFor(model => model.ShowDefaultDeliveryTime) -
                                        - @Html.SmartLabelFor(model => model.ShowPopularProductTagsOnHomepage) + @Html.SmartLabelFor(model => model.IgnoreFeaturedProducts) - @Html.SettingEditorFor(model => model.ShowPopularProductTagsOnHomepage) - @Html.ValidationMessageFor(model => model.ShowPopularProductTagsOnHomepage) + @Html.SettingEditorFor(model => model.IgnoreFeaturedProducts) + @Html.ValidationMessageFor(model => model.IgnoreFeaturedProducts)
                                        +} +@helper TabProductListSettings() +{ - - - - - - - - - - - - - - - - + - - + - - + - - -
                                        -
                                        -
                                        @T("Admin.Catalog.Manufacturers")
                                        -
                                        -
                                        - @Html.SmartLabelFor(model => model.ShowManufacturersOnHomepage) - - @Html.SettingEditorFor(model => model.ShowManufacturersOnHomepage) - @Html.ValidationMessageFor(model => model.ShowManufacturersOnHomepage) -
                                        - @Html.SmartLabelFor(model => model.ManufacturerItemsToDisplayOnHomepage) - - @Html.SettingEditorFor(model => model.ManufacturerItemsToDisplayOnHomepage) - @Html.ValidationMessageFor(model => model.ManufacturerItemsToDisplayOnHomepage) -
                                        - @Html.SmartLabelFor(model => model.ShowManufacturersInOffCanvas) - - @Html.SettingEditorFor(model => model.ShowManufacturersInOffCanvas) - @Html.ValidationMessageFor(model => model.ShowManufacturersInOffCanvas) +
                                        +
                                        +
                                        @T("Common.Navigation")
                                        +
                                        - @Html.SmartLabelFor(model => model.ManufacturerItemsToDisplayInOffCanvasMenu) + @Html.SmartLabelFor(model => model.ShowProductsFromSubcategories) - @Html.SettingEditorFor(model => model.ManufacturerItemsToDisplayInOffCanvasMenu) - @Html.ValidationMessageFor(model => model.ManufacturerItemsToDisplayInOffCanvasMenu) + @Html.SettingEditorFor(model => model.ShowProductsFromSubcategories) + @Html.ValidationMessageFor(model => model.ShowProductsFromSubcategories)
                                        - @Html.SmartLabelFor(model => model.ShowManufacturerPictures) + @Html.SmartLabelFor(model => model.IncludeFeaturedProductsInNormalLists) - @Html.SettingEditorFor(model => model.ShowManufacturerPictures) - @Html.ValidationMessageFor(model => model.ShowManufacturerPictures) + @Html.SettingEditorFor(model => model.IncludeFeaturedProductsInNormalLists) + @Html.ValidationMessageFor(model => model.IncludeFeaturedProductsInNormalLists)
                                        - @Html.SmartLabelFor(model => model.HideManufacturerDefaultPictures) + @Html.SmartLabelFor(model => model.HideCategoryDefaultPictures) - @Html.SettingEditorFor(model => model.HideManufacturerDefaultPictures) - @Html.ValidationMessageFor(model => model.HideManufacturerDefaultPictures) + @Html.SettingEditorFor(model => model.HideCategoryDefaultPictures) + @Html.ValidationMessageFor(model => model.HideCategoryDefaultPictures)
                                        - @Html.SmartLabelFor(model => model.SortManufacturersAlphabetically) + @Html.SmartLabelFor(model => model.ShowCategoryProductNumber) - @Html.SettingEditorFor(model => model.SortManufacturersAlphabetically) - @Html.ValidationMessageFor(model => model.SortManufacturersAlphabetically) -
                                        -} - -@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.ShowCategoryProductNumber) - - @Html.SettingEditorFor(model => model.ShowCategoryProductNumber) - @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.SettingOverrideCheckbox(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.SettingOverrideCheckbox(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.SettingOverrideCheckbox(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.SettingOverrideCheckbox(model => Model.PriceDisplayType) - @Html.DropDownListFor(model => model.PriceDisplayType, Model.AvailablePriceDisplayTypes) - @Html.ValidationMessageFor(model => model.PriceDisplayType) -
                                        - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                                        -
                                        -
                                        @T("Admin.Catalog.Products")
                                        -
                                        -
                                        - @Html.SmartLabelFor(model => model.ShowShortDescriptionInGridStyleLists) - - @Html.SettingEditorFor(model => model.ShowShortDescriptionInGridStyleLists) - @Html.ValidationMessageFor(model => model.ShowShortDescriptionInGridStyleLists) -
                                        - @Html.SmartLabelFor(model => model.ShowDeliveryTimesInProductLists) - - @Html.SettingEditorFor(model => model.ShowDeliveryTimesInProductLists) - @Html.ValidationMessageFor(model => model.ShowDeliveryTimesInProductLists) -
                                        - @Html.SmartLabelFor(model => model.ShowManufacturerInGridStyleLists) - - @Html.SettingEditorFor(model => model.ShowManufacturerInGridStyleLists) - @Html.ValidationMessageFor(model => model.ShowManufacturerInGridStyleLists) -
                                        - @Html.SmartLabelFor(model => model.ShowManufacturerLogoInLists) - - @Html.SettingEditorFor(model => model.ShowManufacturerLogoInLists) - @Html.ValidationMessageFor(model => model.ShowManufacturerLogoInLists) -
                                        - @Html.SmartLabelFor(model => model.ShowBasePriceInProductLists) - - @Html.SettingEditorFor(model => model.ShowBasePriceInProductLists) - @Html.ValidationMessageFor(model => model.ShowBasePriceInProductLists) -
                                        - @Html.SmartLabelFor(model => model.ShowProductOptionsInLists) - - @Html.SettingEditorFor(model => model.ShowProductOptionsInLists) - @Html.ValidationMessageFor(model => model.ShowProductOptionsInLists) -
                                        - @Html.SmartLabelFor(model => model.ShowColorSquaresInLists) - - @Html.SettingEditorFor(model => model.ShowColorSquaresInLists) - @Html.ValidationMessageFor(model => model.ShowColorSquaresInLists) -
                                        - @Html.SmartLabelFor(model => model.HideBuyButtonInLists) - - @Html.SettingEditorFor(model => model.HideBuyButtonInLists) - @Html.ValidationMessageFor(model => model.HideBuyButtonInLists) -
                                        - @Html.SmartLabelFor(model => model.LabelAsNewForMaxDays) - - @Html.SettingEditorFor(model => model.LabelAsNewForMaxDays) - @Html.ValidationMessageFor(model => model.LabelAsNewForMaxDays) -
                                        - - - - - - - - - -
                                        -
                                        -
                                        @T("Admin.Catalog.ProductTags")
                                        -
                                        -
                                        - @Html.SmartLabelFor(model => model.NumberOfProductTags) - - @Html.SettingEditorFor(model => model.NumberOfProductTags) - @Html.ValidationMessageFor(model => model.NumberOfProductTags) -
                                        + @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.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.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.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.SettingEditorFor(model => model.PriceDisplayType, @Html.DropDownListFor(model => model.PriceDisplayType, Model.AvailablePriceDisplayTypes)) + @Html.ValidationMessageFor(model => model.PriceDisplayType) +
                                        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                                        +
                                        +
                                        @T("Admin.Catalog.Products")
                                        +
                                        +
                                        + @Html.SmartLabelFor(model => model.ShowShortDescriptionInGridStyleLists) + + @Html.SettingEditorFor(model => model.ShowShortDescriptionInGridStyleLists) + @Html.ValidationMessageFor(model => model.ShowShortDescriptionInGridStyleLists) +
                                        + @Html.SmartLabelFor(model => model.ShowDeliveryTimesInProductLists) + + @Html.SettingEditorFor(model => model.ShowDeliveryTimesInProductLists) + @Html.ValidationMessageFor(model => model.ShowDeliveryTimesInProductLists) +
                                        + @Html.SmartLabelFor(model => model.ShowManufacturerInGridStyleLists) + + @Html.SettingEditorFor(model => model.ShowManufacturerInGridStyleLists) + @Html.ValidationMessageFor(model => model.ShowManufacturerInGridStyleLists) +
                                        + @Html.SmartLabelFor(model => model.ShowManufacturerLogoInLists) + + @Html.SettingEditorFor(model => model.ShowManufacturerLogoInLists) + @Html.ValidationMessageFor(model => model.ShowManufacturerLogoInLists) +
                                        + @Html.SmartLabelFor(model => model.ShowBasePriceInProductLists) + + @Html.SettingEditorFor(model => model.ShowBasePriceInProductLists) + @Html.ValidationMessageFor(model => model.ShowBasePriceInProductLists) +
                                        + @Html.SmartLabelFor(model => model.ShowProductOptionsInLists) + + @Html.SettingEditorFor(model => model.ShowProductOptionsInLists) + @Html.ValidationMessageFor(model => model.ShowProductOptionsInLists) +
                                        + @Html.SmartLabelFor(model => model.ShowColorSquaresInLists) + + @Html.SettingEditorFor(model => model.ShowColorSquaresInLists) + @Html.ValidationMessageFor(model => model.ShowColorSquaresInLists) +
                                        + @Html.SmartLabelFor(model => model.HideBuyButtonInLists) + + @Html.SettingEditorFor(model => model.HideBuyButtonInLists) + @Html.ValidationMessageFor(model => model.HideBuyButtonInLists) +
                                        + @Html.SmartLabelFor(model => model.LabelAsNewForMaxDays) + + @Html.SettingEditorFor(model => model.LabelAsNewForMaxDays, null, new { postfix = T("Time.Days").Text }) + @Html.ValidationMessageFor(model => model.LabelAsNewForMaxDays) +
                                        + + + + + + + + + +
                                        +
                                        +
                                        @T("Admin.Catalog.ProductTags")
                                        +
                                        +
                                        + @Html.SmartLabelFor(model => model.NumberOfProductTags) + + @Html.SettingEditorFor(model => model.NumberOfProductTags) + @Html.ValidationMessageFor(model => model.NumberOfProductTags) +
                                        } @helper TabUserSettings() @@ -766,31 +614,33 @@ @Html.SmartLabelFor(model => model.EmailAFriendEnabled) - @Html.SettingEditorFor(model => model.EmailAFriendEnabled) + @Html.SettingEditorFor(model => model.EmailAFriendEnabled, Html.CheckBoxFor(model => model.EmailAFriendEnabled, new { data_toggler_for = "#pnlAllowAnonymousUsersToEmailAFriend" })) @Html.ValidationMessageFor(model => model.EmailAFriendEnabled) - - - @Html.SmartLabelFor(model => model.AllowAnonymousUsersToEmailAFriend) - - - @Html.SettingEditorFor(model => model.AllowAnonymousUsersToEmailAFriend) - @Html.ValidationMessageFor(model => model.AllowAnonymousUsersToEmailAFriend) - - - - - @Html.SmartLabelFor(model => model.AllowDifferingEmailAddressForEmailAFriend) - - - @Html.SettingEditorFor(model => model.AllowDifferingEmailAddressForEmailAFriend) - @Html.ValidationMessageFor(model => model.AllowDifferingEmailAddressForEmailAFriend) - - + + + + @Html.SmartLabelFor(model => model.AllowAnonymousUsersToEmailAFriend) + + + @Html.SettingEditorFor(model => model.AllowAnonymousUsersToEmailAFriend) + @Html.ValidationMessageFor(model => model.AllowAnonymousUsersToEmailAFriend) + + + + + @Html.SmartLabelFor(model => model.AllowDifferingEmailAddressForEmailAFriend) + + + @Html.SettingEditorFor(model => model.AllowDifferingEmailAddressForEmailAFriend) + @Html.ValidationMessageFor(model => model.AllowDifferingEmailAddressForEmailAFriend) + + + } - + @helper TabProductDetailSettings() { @@ -799,7 +649,7 @@ @Html.SmartLabelFor(model => model.RecentlyViewedProductsEnabled) @@ -817,7 +667,7 @@ @Html.SmartLabelFor(model => model.RecentlyAddedProductsEnabled) @@ -835,7 +685,7 @@ @Html.SmartLabelFor(model => model.ProductsAlsoPurchasedEnabled) @@ -862,19 +712,19 @@ @Html.SmartLabelFor(model => model.ShowManufacturerInProductDetail) - - - - + + + + - - - - - - - - + + + + - +
                                        - @Html.SettingEditorFor(model => model.RecentlyViewedProductsEnabled) + @Html.SettingEditorFor(model => model.RecentlyViewedProductsEnabled, Html.CheckBoxFor(model => model.RecentlyViewedProductsEnabled, new { data_toggler_for = "#pnlRecentlyViewedProductsNumber" })) @Html.ValidationMessageFor(model => model.RecentlyViewedProductsEnabled)
                                        - @Html.SettingEditorFor(model => model.RecentlyAddedProductsEnabled) + @Html.SettingEditorFor(model => model.RecentlyAddedProductsEnabled, Html.CheckBoxFor(model => model.RecentlyAddedProductsEnabled, new { data_toggler_for = "#pnlRecentlyAddedProductsNumber" })) @Html.ValidationMessageFor(model => model.RecentlyAddedProductsEnabled)
                                        - @Html.SettingEditorFor(model => model.ProductsAlsoPurchasedEnabled) + @Html.SettingEditorFor(model => model.ProductsAlsoPurchasedEnabled, Html.CheckBoxFor(model => model.ProductsAlsoPurchasedEnabled, new { data_toggler_for = "#pnlProductsAlsoPurchasedNumber" })) @Html.ValidationMessageFor(model => model.ProductsAlsoPurchasedEnabled)
                                        - @Html.SettingEditorFor(model => model.ShowManufacturerInProductDetail) + @Html.SettingEditorFor(model => model.ShowManufacturerInProductDetail, Html.CheckBoxFor(model => model.ShowManufacturerInProductDetail, new { data_toggler_for = "#pnlManufacturerPictures" })) @Html.ValidationMessageFor(model => model.ShowManufacturerInProductDetail)
                                        - @Html.SmartLabelFor(model => model.ShowManufacturerPicturesInProductDetail) - - @Html.SettingEditorFor(model => model.ShowManufacturerPicturesInProductDetail) - @Html.ValidationMessageFor(model => model.ShowManufacturerPicturesInProductDetail) -
                                        + @Html.SmartLabelFor(model => model.ShowManufacturerPicturesInProductDetail) + + @Html.SettingEditorFor(model => model.ShowManufacturerPicturesInProductDetail) + @Html.ValidationMessageFor(model => model.ShowManufacturerPicturesInProductDetail) +
                                        @Html.SmartLabelFor(model => model.ShowDeliveryTimesInProductDetail) @@ -884,16 +734,6 @@ @Html.ValidationMessageFor(model => model.ShowDeliveryTimesInProductDetail)
                                        - @Html.SmartLabelFor(model => model.DeliveryTimeIdForEmptyStock) - - @Html.SettingOverrideCheckbox(model => model.DeliveryTimeIdForEmptyStock) - @Html.DropDownListFor(model => model.DeliveryTimeIdForEmptyStock, Model.AvailableDeliveryTimes, T("Common.Unspecified")) - @Html.ValidationMessageFor(model => model.DeliveryTimeIdForEmptyStock) -
                                        @Html.SmartLabelFor(model => model.EnableDynamicPriceUpdate) @@ -903,15 +743,15 @@ @Html.ValidationMessageFor(model => model.EnableDynamicPriceUpdate)
                                        - @Html.SmartLabelFor(model => model.BundleItemShowBasePrice) - - @Html.SettingEditorFor(model => model.BundleItemShowBasePrice) - @Html.ValidationMessageFor(model => model.BundleItemShowBasePrice) -
                                        + @Html.SmartLabelFor(model => model.BundleItemShowBasePrice) + + @Html.SettingEditorFor(model => model.BundleItemShowBasePrice) + @Html.ValidationMessageFor(model => model.BundleItemShowBasePrice) +
                                        @Html.SmartLabelFor(model => model.ShowVariantCombinationPriceAdjustment) @@ -944,19 +784,88 @@ @Html.SmartLabelFor(model => model.ShowShareButton) - @Html.SettingEditorFor(model => model.ShowShareButton) + @Html.SettingEditorFor(model => model.ShowShareButton, Html.CheckBoxFor(model => model.ShowShareButton, new { data_toggler_for = "#pnlPageShareCode" })) @Html.ValidationMessageFor(model => model.ShowShareButton)
                                        @Html.SmartLabelFor(model => model.PageShareCode) - @Html.TextAreaFor(model => model.PageShareCode, new { @class = "input-large", style = "height:150px" }) + @Html.TextAreaFor(model => model.PageShareCode, new { style = "height:150px" }) @Html.ValidationMessageFor(model => model.PageShareCode)
                                        -} -} \ No newline at end of file +} + +@helper TabBrandsSettings() +{ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                                        + @Html.SmartLabelFor(model => model.ShowManufacturersOnHomepage) + + @Html.SettingEditorFor(model => model.ShowManufacturersOnHomepage, Html.CheckBoxFor(model => model.ShowManufacturersOnHomepage, new { data_toggler_for = "#pnlManufacturerItemsToDisplayOnHomepage" })) + @Html.ValidationMessageFor(model => model.ShowManufacturersOnHomepage) +
                                        + @Html.SmartLabelFor(model => model.ManufacturerItemsToDisplayOnHomepage) + + @Html.SettingEditorFor(model => model.ManufacturerItemsToDisplayOnHomepage) + @Html.ValidationMessageFor(model => model.ManufacturerItemsToDisplayOnHomepage) +
                                        + @Html.SmartLabelFor(model => model.ShowManufacturersInOffCanvas) + + @Html.SettingEditorFor(model => model.ShowManufacturersInOffCanvas, Html.CheckBoxFor(model => model.ShowManufacturersInOffCanvas, new { data_toggler_for = "#pnlManufacturerItemsToDisplayInOffCanvas" })) + @Html.ValidationMessageFor(model => model.ShowManufacturersInOffCanvas) +
                                        + @Html.SmartLabelFor(model => model.ManufacturerItemsToDisplayInOffCanvasMenu) + + @Html.SettingEditorFor(model => model.ManufacturerItemsToDisplayInOffCanvasMenu) + @Html.ValidationMessageFor(model => model.ManufacturerItemsToDisplayInOffCanvasMenu) +
                                        + @Html.SmartLabelFor(model => model.ShowManufacturerPictures) + + @Html.SettingEditorFor(model => model.ShowManufacturerPictures) + @Html.ValidationMessageFor(model => model.ShowManufacturerPictures) +
                                        + @Html.SmartLabelFor(model => model.HideManufacturerDefaultPictures) + + @Html.SettingEditorFor(model => model.HideManufacturerDefaultPictures) + @Html.ValidationMessageFor(model => model.HideManufacturerDefaultPictures) +
                                        + @Html.SmartLabelFor(model => model.SortManufacturersAlphabetically) + + @Html.SettingEditorFor(model => model.SortManufacturersAlphabetically) + @Html.ValidationMessageFor(model => model.SortManufacturersAlphabetically) +
                                        +} +} diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/CustomerUser.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/CustomerUser.cshtml index f7e06e7f8b..1d7ac7c4b6 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/CustomerUser.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/CustomerUser.cshtml @@ -1,7 +1,5 @@ @model CustomerUserSettingsModel -@using Telerik.Web.Mvc.UI; @using SmartStore.Core.Domain.Customers; -@using SmartStore.Core.Domain.Security; @{ ViewBag.Title = T("Admin.Configuration.Settings.CustomerUser").Text; } @@ -13,25 +11,26 @@ @T("Admin.Configuration.Settings.CustomerUser")
                                        - +
                                        Html.RenderAction("StoreScopeConfiguration", "Setting"); @Html.ValidationSummary(false) - - - @Html.SmartStore().TabStrip().Name("customersettings-edit").Items(x => + + @Html.SmartStore().TabStrip().Name("customersettings-edit").Style(TabsStyle.Material).Position(TabsPosition.Top).Items(x => { - x.Add().Text(T("Admin.Configuration.Settings.CustomerUser.CustomerSettings").Text).Content(TabCustomerSettings()).Selected(true); - x.Add().Text(T("Admin.Configuration.Settings.CustomerUser.CustomerFormFields").Text).Content(TabCustomerFormFields().ToHtmlString()); - x.Add().Text(T("Admin.Configuration.Settings.CustomerUser.AddressFormFields").Text).Content(TabAddressFormFields().ToHtmlString()); - x.Add().Text(T("Admin.Configuration.Settings.CustomerUser.DateTimeSettings").Text).Content(TabDateTimeSettings()); - x.Add().Text(T("Admin.Configuration.Settings.CustomerUser.ExternalAuthenticationSettings").Text).Content(TabExternalAuthenticationSettings()); + x.Add().Text(T("Admin.Configuration.Settings.CustomerUser.CustomerSettings").Text).Content(TabCustomerSettings()).Selected(true); + x.Add().Text(T("Admin.Configuration.Settings.CustomerUser.CustomerFormFields").Text).Content(TabCustomerFormFields().ToHtmlString()); + x.Add().Text(T("Admin.Configuration.Settings.CustomerUser.AddressFormFields").Text).Content(TabAddressFormFields().ToHtmlString()); + x.Add().Text(T("Admin.Configuration.Settings.CustomerUser.DateTimeSettings").Text).Content(TabDateTimeSettings()); + x.Add().Text(T("Admin.Configuration.Settings.CustomerUser.ExternalAuthenticationSettings").Text).Content(TabExternalAuthenticationSettings()); - //generate an event - EngineContext.Current.Resolve().Publish(new TabStripCreated(x, "customersettings-edit", this.Html, this.Model)); + EngineContext.Current.Resolve().Publish(new TabStripCreated(x, "customersettings-edit", this.Html, this.Model)); }) } @@ -39,38 +38,10 @@ { @@ -78,35 +49,36 @@ @Html.SmartLabelFor(model => model.CustomerSettings.UsernamesEnabled) - - - - - - - - + + + + + + + + + + @@ -125,8 +97,7 @@ @Html.SmartLabelFor(model => model.CustomerSettings.CustomerNumberMethod) @@ -135,8 +106,7 @@ @Html.SmartLabelFor(model => model.CustomerSettings.CustomerNumberVisibility) @@ -146,8 +116,7 @@ @Html.SmartLabelFor(model => model.CustomerSettings.UserRegistrationType) @@ -165,8 +134,8 @@ @Html.SmartLabelFor(model => model.CustomerSettings.RegisterCustomerRoleId) @@ -176,7 +145,8 @@ @Html.SmartLabelFor(model => model.CustomerSettings.AllowCustomersToUploadAvatars) @@ -261,6 +231,15 @@ @Html.ValidationMessageFor(model => model.CustomerSettings.StoreLastVisitedPage) + + + +
                                        - @Html.SettingEditorFor(model => model.CustomerSettings.UsernamesEnabled) + @Html.SettingEditorFor(model => model.CustomerSettings.UsernamesEnabled, Html.CheckBoxFor(model => model.CustomerSettings.UsernamesEnabled, new { data_toggler_for = "#pnlUsernamesEnabled" })) @Html.ValidationMessageFor(model => model.CustomerSettings.UsernamesEnabled)
                                        - @Html.SmartLabelFor(model => model.CustomerSettings.AllowUsersToChangeUsernames) - - @Html.SettingEditorFor(model => model.CustomerSettings.AllowUsersToChangeUsernames) - @Html.ValidationMessageFor(model => model.CustomerSettings.AllowUsersToChangeUsernames) -
                                        - @Html.SmartLabelFor(model => model.CustomerSettings.CheckUsernameAvailabilityEnabled) - - @Html.SettingEditorFor(model => model.CustomerSettings.CheckUsernameAvailabilityEnabled) - @Html.ValidationMessageFor(model => model.CustomerSettings.CheckUsernameAvailabilityEnabled) -
                                        + @Html.SmartLabelFor(model => model.CustomerSettings.AllowUsersToChangeUsernames) + + @Html.SettingEditorFor(model => model.CustomerSettings.AllowUsersToChangeUsernames) + @Html.ValidationMessageFor(model => model.CustomerSettings.AllowUsersToChangeUsernames) +
                                        + @Html.SmartLabelFor(model => model.CustomerSettings.CheckUsernameAvailabilityEnabled) + + @Html.SettingEditorFor(model => model.CustomerSettings.CheckUsernameAvailabilityEnabled) + @Html.ValidationMessageFor(model => model.CustomerSettings.CheckUsernameAvailabilityEnabled) +
                                        @Html.SmartLabelFor(model => model.CustomerSettings.CustomerNameFormat) - @Html.SettingOverrideCheckbox(model => Model.CustomerSettings.UserRegistrationType) - @Html.DropDownListFor(model => model.CustomerSettings.CustomerNameFormat, ((CustomerNameFormat)Model.CustomerSettings.CustomerNameFormat).ToSelectList()) + @Html.EnumSettingEditorFor(model => model.CustomerSettings.CustomerNameFormat) @Html.ValidationMessageFor(model => model.CustomerSettings.CustomerNameFormat)
                                        - @Html.SettingOverrideCheckbox(model => Model.CustomerSettings.CustomerNumberMethod) - @Html.DropDownListFor(model => model.CustomerSettings.CustomerNumberMethod, Model.CustomerSettings.AvailableCustomerNumberMethods) + @Html.EnumSettingEditorFor(model => model.CustomerSettings.CustomerNumberMethod) @Html.ValidationMessageFor(model => model.CustomerSettings.CustomerNumberMethod)
                                        - @Html.SettingOverrideCheckbox(model => Model.CustomerSettings.CustomerNumberVisibility) - @Html.DropDownListFor(model => model.CustomerSettings.CustomerNumberVisibility, Model.CustomerSettings.AvailableCustomerNumberVisibilities) + @Html.EnumSettingEditorFor(model => model.CustomerSettings.CustomerNumberVisibility) @Html.ValidationMessageFor(model => model.CustomerSettings.CustomerNumberVisibility)
                                        - @Html.SettingOverrideCheckbox(model => Model.CustomerSettings.UserRegistrationType) - @Html.DropDownListFor(model => model.CustomerSettings.UserRegistrationType, ((UserRegistrationType)Model.CustomerSettings.UserRegistrationType).ToSelectList()) + @Html.EnumSettingEditorFor(model => model.CustomerSettings.UserRegistrationType) @Html.ValidationMessageFor(model => model.CustomerSettings.UserRegistrationType)
                                        - @Html.SettingOverrideCheckbox(model => Model.CustomerSettings.RegisterCustomerRoleId) - @Html.DropDownListFor(model => model.CustomerSettings.RegisterCustomerRoleId, Model.CustomerSettings.AvailableRegisterCustomerRoles, T("Common.Unspecified")) + @Html.SettingEditorFor(model => model.CustomerSettings.RegisterCustomerRoleId, + Html.DropDownListFor(model => model.CustomerSettings.RegisterCustomerRoleId, Model.CustomerSettings.AvailableRegisterCustomerRoles, T("Common.Unspecified"))) @Html.ValidationMessageFor(model => model.CustomerSettings.RegisterCustomerRoleId)
                                        - @Html.SettingEditorFor(model => model.CustomerSettings.AllowCustomersToUploadAvatars) + @Html.SettingEditorFor(model => model.CustomerSettings.AllowCustomersToUploadAvatars, + Html.CheckBoxFor(model => model.CustomerSettings.AllowCustomersToUploadAvatars, new { data_toggler_for = "#pnlDefaultAvatarEnabled" })) @Html.ValidationMessageFor(model => model.CustomerSettings.AllowCustomersToUploadAvatars)
                                        + @Html.SmartLabelFor(model => model.CustomerSettings.StoreLastIpAddress) + + @Html.SettingEditorFor(model => model.CustomerSettings.StoreLastIpAddress) + @Html.ValidationMessageFor(model => model.CustomerSettings.StoreLastIpAddress) +
                                        @Html.SmartLabelFor(model => model.CustomerSettings.DisplayPrivacyAgreementOnContactUs) @@ -272,90 +251,12 @@
                                        } + @helper TabCustomerFormFields() { - - -
                                        - +
                                        @T("Admin.Configuration.Settings.CustomerUser.CustomerFormFields.Description") +
                                        @@ -391,7 +292,8 @@ @Html.SmartLabelFor(model => model.CustomerSettings.CompanyEnabled) @@ -409,7 +311,8 @@ @Html.SmartLabelFor(model => model.CustomerSettings.StreetAddressEnabled) @@ -427,7 +330,8 @@ @Html.SmartLabelFor(model => model.CustomerSettings.StreetAddress2Enabled) @@ -445,7 +349,8 @@ @Html.SmartLabelFor(model => model.CustomerSettings.ZipPostalCodeEnabled) @@ -463,7 +368,8 @@ @Html.SmartLabelFor(model => model.CustomerSettings.CityEnabled) @@ -481,7 +387,8 @@ @Html.SmartLabelFor(model => model.CustomerSettings.CountryEnabled) @@ -499,7 +406,8 @@ @Html.SmartLabelFor(model => model.CustomerSettings.PhoneEnabled) @@ -517,7 +425,8 @@ @Html.SmartLabelFor(model => model.CustomerSettings.FaxEnabled) @@ -541,98 +450,12 @@
                                        - @Html.SettingEditorFor(model => model.CustomerSettings.CompanyEnabled) + @Html.SettingEditorFor(model => model.CustomerSettings.CompanyEnabled, + Html.CheckBoxFor(model => model.CustomerSettings.CompanyEnabled, new { data_toggler_for = "#pnlCompanyRequired" })) @Html.ValidationMessageFor(model => model.CustomerSettings.CompanyEnabled)
                                        - @Html.SettingEditorFor(model => model.CustomerSettings.StreetAddressEnabled) + @Html.SettingEditorFor(model => model.CustomerSettings.StreetAddressEnabled, + Html.CheckBoxFor(model => model.CustomerSettings.StreetAddressEnabled, new { data_toggler_for = "#pnlStreetAddressRequired" })) @Html.ValidationMessageFor(model => model.CustomerSettings.StreetAddressEnabled)
                                        - @Html.SettingEditorFor(model => model.CustomerSettings.StreetAddress2Enabled) + @Html.SettingEditorFor(model => model.CustomerSettings.StreetAddress2Enabled, + Html.CheckBoxFor(model => model.CustomerSettings.StreetAddress2Enabled, new { data_toggler_for = "#pnlStreetAddress2Required" })) @Html.ValidationMessageFor(model => model.CustomerSettings.StreetAddress2Enabled)
                                        - @Html.SettingEditorFor(model => model.CustomerSettings.ZipPostalCodeEnabled) + @Html.SettingEditorFor(model => model.CustomerSettings.ZipPostalCodeEnabled, + Html.CheckBoxFor(model => model.CustomerSettings.ZipPostalCodeEnabled, new { data_toggler_for = "#pnlZipPostalCodeRequired" })) @Html.ValidationMessageFor(model => model.CustomerSettings.ZipPostalCodeEnabled)
                                        - @Html.SettingEditorFor(model => model.CustomerSettings.CityEnabled) + @Html.SettingEditorFor(model => model.CustomerSettings.CityEnabled, + Html.CheckBoxFor(model => model.CustomerSettings.CityEnabled, new { data_toggler_for = "#pnlCityRequired" })) @Html.ValidationMessageFor(model => model.CustomerSettings.CityEnabled)
                                        - @Html.SettingEditorFor(model => model.CustomerSettings.CountryEnabled) + @Html.SettingEditorFor(model => model.CustomerSettings.CountryEnabled, + Html.CheckBoxFor(model => model.CustomerSettings.CountryEnabled, new { data_toggler_for = "#pnlStateProvincEnabled" })) @Html.ValidationMessageFor(model => model.CustomerSettings.CountryEnabled)
                                        - @Html.SettingEditorFor(model => model.CustomerSettings.PhoneEnabled) + @Html.SettingEditorFor(model => model.CustomerSettings.PhoneEnabled, + Html.CheckBoxFor(model => model.CustomerSettings.PhoneEnabled, new { data_toggler_for = "#pnlPhoneRequired" })) @Html.ValidationMessageFor(model => model.CustomerSettings.PhoneEnabled)
                                        - @Html.SettingEditorFor(model => model.CustomerSettings.FaxEnabled) + @Html.SettingEditorFor(model => model.CustomerSettings.FaxEnabled, + Html.CheckBoxFor(model => model.CustomerSettings.FaxEnabled, new { data_toggler_for = "#pnlFaxRequired" })) @Html.ValidationMessageFor(model => model.CustomerSettings.FaxEnabled)
                                        } + @helper TabAddressFormFields() { - - -
                                        - +
                                        @T("Admin.Configuration.Settings.CustomerUser.AddressFormFields.Description") +
                                        @@ -650,42 +473,43 @@ @Html.SmartLabelFor(model => model.AddressSettings.SalutationEnabled)
                                        - @Html.SettingEditorFor(model => model.AddressSettings.SalutationEnabled) + @Html.SettingEditorFor(model => model.AddressSettings.SalutationEnabled, + Html.CheckBoxFor(model => model.AddressSettings.SalutationEnabled, new { data_toggler_for = "#pnlAddressSettingsSalutations" })) @Html.ValidationMessageFor(model => model.AddressSettings.SalutationEnabled)
                                        - - @(Html.LocalizedEditor("setting-customer-localized", - @ - - - - - - - -
                                        - @Html.SmartLabelFor(model => model.Locales[item].Salutations) - - @Html.TextBoxFor(model => Model.Locales[item].Salutations, new { @class = "input-xlarge" }) - @Html.ValidationMessageFor(model => model.Locales[item].Salutations) -
                                        - @Html.HiddenFor(model => model.Locales[item].LanguageId) -
                                        - , - @ - - - - -
                                        - @Html.SmartLabelFor(model => model.AddressSettings.Salutations) - - @Html.TextBoxFor(model => model.AddressSettings.Salutations, new { @class = "input-xlarge" }) - @Html.ValidationMessageFor(model => model.AddressSettings.Salutations) -
                                        - )) + +
                                        + @(Html.LocalizedEditor("setting-customer-localized", + @ + + + + +
                                        + @Html.SmartLabelFor(model => model.Locales[item].Salutations) + + @*IMPORTANT: Do not delete, this hidden element contains the id to assign localized values to the corresponding language *@ + @Html.HiddenFor(model => model.Locales[item].LanguageId) + + @Html.TextBoxFor(model => Model.Locales[item].Salutations) + @Html.ValidationMessageFor(model => model.Locales[item].Salutations) +
                                        + , + @ + + + + +
                                        + @Html.SmartLabelFor(model => model.AddressSettings.Salutations) + + @Html.TextBoxFor(model => model.AddressSettings.Salutations) + @Html.ValidationMessageFor(model => model.AddressSettings.Salutations) +
                                        + )) +
                                        @@ -702,7 +526,8 @@ @Html.SmartLabelFor(model => model.AddressSettings.CompanyEnabled) @@ -720,7 +545,8 @@ @Html.SmartLabelFor(model => model.AddressSettings.StreetAddressEnabled) @@ -738,7 +564,8 @@ @Html.SmartLabelFor(model => model.AddressSettings.StreetAddress2Enabled) @@ -756,7 +583,8 @@ @Html.SmartLabelFor(model => model.AddressSettings.ZipPostalCodeEnabled) @@ -774,7 +602,8 @@ @Html.SmartLabelFor(model => model.AddressSettings.CityEnabled) @@ -792,25 +621,47 @@ @Html.SmartLabelFor(model => model.AddressSettings.CountryEnabled) - - - - + + + + + + + + + + + + + + @@ -828,7 +679,8 @@ @Html.SmartLabelFor(model => model.AddressSettings.FaxEnabled) @@ -843,6 +695,7 @@
                                        - @Html.SettingEditorFor(model => model.AddressSettings.CompanyEnabled) + @Html.SettingEditorFor(model => model.AddressSettings.CompanyEnabled, + Html.CheckBoxFor(model => model.AddressSettings.CompanyEnabled, new { data_toggler_for = "#pnlAddressCompanyRequired" })) @Html.ValidationMessageFor(model => model.AddressSettings.CompanyEnabled)
                                        - @Html.SettingEditorFor(model => model.AddressSettings.StreetAddressEnabled) + @Html.SettingEditorFor(model => model.AddressSettings.StreetAddressEnabled, + Html.CheckBoxFor(model => model.AddressSettings.StreetAddressEnabled, new { data_toggler_for = "#pnlAddressStreetAddressRequired" })) @Html.ValidationMessageFor(model => model.AddressSettings.StreetAddressEnabled)
                                        - @Html.SettingEditorFor(model => model.AddressSettings.StreetAddress2Enabled) + @Html.SettingEditorFor(model => model.AddressSettings.StreetAddress2Enabled, + Html.CheckBoxFor(model => model.AddressSettings.StreetAddress2Enabled, new { data_toggler_for = "#pnlAddressStreetAddress2Required" })) @Html.ValidationMessageFor(model => model.AddressSettings.StreetAddress2Enabled)
                                        - @Html.SettingEditorFor(model => model.AddressSettings.ZipPostalCodeEnabled) + @Html.SettingEditorFor(model => model.AddressSettings.ZipPostalCodeEnabled, + Html.CheckBoxFor(model => model.AddressSettings.ZipPostalCodeEnabled, new { data_toggler_for = "#pnlAddressZipPostalCodeRequired" })) @Html.ValidationMessageFor(model => model.AddressSettings.ZipPostalCodeEnabled)
                                        - @Html.SettingEditorFor(model => model.AddressSettings.CityEnabled) + @Html.SettingEditorFor(model => model.AddressSettings.CityEnabled, + Html.CheckBoxFor(model => model.AddressSettings.CityEnabled, new { data_toggler_for = "#pnlAddressCityRequired" })) @Html.ValidationMessageFor(model => model.AddressSettings.CityEnabled)
                                        - @Html.SettingEditorFor(model => model.AddressSettings.CountryEnabled) + @Html.SettingEditorFor(model => model.AddressSettings.CountryEnabled, + Html.CheckBoxFor(model => model.AddressSettings.CountryEnabled, new { data_toggler_for = "#pnlAddressStateProvinceEnabled" })) @Html.ValidationMessageFor(model => model.AddressSettings.CountryEnabled)
                                        - @Html.SmartLabelFor(model => model.AddressSettings.StateProvinceEnabled) - - @Html.SettingEditorFor(model => model.AddressSettings.StateProvinceEnabled) - @Html.ValidationMessageFor(model => model.AddressSettings.StateProvinceEnabled) -
                                        + @Html.SmartLabelFor(model => model.AddressSettings.CountryRequired) + + @Html.SettingEditorFor(model => model.AddressSettings.CountryRequired) + @Html.ValidationMessageFor(model => model.AddressSettings.CountryRequired) +
                                        + @Html.SmartLabelFor(model => model.AddressSettings.StateProvinceEnabled) + + @Html.SettingEditorFor(model => model.AddressSettings.StateProvinceEnabled) + @Html.ValidationMessageFor(model => model.AddressSettings.StateProvinceEnabled) +
                                        + @Html.SmartLabelFor(model => model.AddressSettings.StateProvinceRequired) + + @Html.SettingEditorFor(model => model.AddressSettings.StateProvinceRequired) + @Html.ValidationMessageFor(model => model.AddressSettings.StateProvinceRequired) +
                                        @Html.SmartLabelFor(model => model.AddressSettings.PhoneEnabled) - @Html.SettingEditorFor(model => model.AddressSettings.PhoneEnabled) + @Html.SettingEditorFor(model => model.AddressSettings.PhoneEnabled, + Html.CheckBoxFor(model => model.AddressSettings.PhoneEnabled, new { data_toggler_for = "#pnlAddressPhoneRequired" })) @Html.ValidationMessageFor(model => model.AddressSettings.PhoneEnabled)
                                        - @Html.SettingEditorFor(model => model.AddressSettings.FaxEnabled) + @Html.SettingEditorFor(model => model.AddressSettings.FaxEnabled, + Html.CheckBoxFor(model => model.AddressSettings.FaxEnabled, new { data_toggler_for = "#pnlAddressFaxRequired" })) @Html.ValidationMessageFor(model => model.AddressSettings.FaxEnabled)
                                        } + @helper TabDateTimeSettings() { @@ -860,13 +713,14 @@ @Html.SmartLabelFor(model => model.DateTimeSettings.DefaultStoreTimeZoneId)
                                        - @Html.SettingOverrideCheckbox(model => model.DateTimeSettings.DefaultStoreTimeZoneId) - @Html.DropDownListFor(model => model.DateTimeSettings.DefaultStoreTimeZoneId, Model.DateTimeSettings.AvailableTimeZones) + @Html.SettingEditorFor(model => model.DateTimeSettings.DefaultStoreTimeZoneId, + Html.DropDownListFor(model => model.DateTimeSettings.DefaultStoreTimeZoneId, Model.DateTimeSettings.AvailableTimeZones)) @Html.ValidationMessageFor(model => model.DateTimeSettings.DefaultStoreTimeZoneId)
                                        } + @helper TabExternalAuthenticationSettings() { diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/DataExchange.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/DataExchange.cshtml index 3527f04ac0..b1194aa108 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/DataExchange.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/DataExchange.cshtml @@ -10,7 +10,10 @@ @T("Admin.Common.DataExchange")
                                        - +
                                        @@ -49,7 +52,7 @@ @Html.SmartLabelFor(model => model.ImageDownloadTimeout) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Forum.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Forum.cshtml index dddbdae530..c2b7ded458 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Forum.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Forum.cshtml @@ -1,5 +1,4 @@ @model ForumSettingsModel -@using Telerik.Web.Mvc.UI; @{ ViewBag.Title = T("Admin.Configuration.Settings.Forums").Text; } @@ -11,7 +10,10 @@ @T("Admin.Configuration.Settings.Forums")
                                        - +
                                        @@ -19,16 +21,19 @@ @Html.ValidationSummary(false) -
                                        - @Html.SettingEditorFor(model => model.ImageDownloadTimeout) + @Html.SettingEditorFor(model => model.ImageDownloadTimeout, null, new { postfix = T("Time.Minutes").Text }) @Html.ValidationMessageFor(model => model.ImageDownloadTimeout)
                                        - - - - +
                                        +
                                        +
                                        + @Html.SmartLabelFor(model => model.ForumsEnabled) +
                                        +
                                        + @Html.SettingEditorFor(model => model.ForumsEnabled, Html.CheckBoxFor(model => model.ForumsEnabled, new { data_toggler_for = "#pnlForumsEnabled" })) + @Html.ValidationMessageFor(model => model.ForumsEnabled) +
                                        +
                                        +
                                        + +
                                        - @Html.SmartLabelFor(model => model.ForumsEnabled) - - @Html.SettingEditorFor(model => model.ForumsEnabled) - @Html.ValidationMessageFor(model => model.ForumsEnabled) -
                                        @@ -148,29 +152,30 @@ @Html.SmartLabelFor(model => model.AllowPrivateMessages) - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + +
                                        @Html.SmartLabelFor(model => model.RelativeDateTimeFormattingEnabled) @@ -124,8 +129,7 @@ @Html.SmartLabelFor(model => model.ForumEditor) - @Html.SettingOverrideCheckbox(model => Model.ForumEditor) - @Html.DropDownListForEnum(model => model.ForumEditor) + @Html.EnumSettingEditorFor(model => model.ForumEditor) @Html.ValidationMessageFor(model => model.ForumEditor)
                                        - @Html.SettingOverrideCheckbox(model => Model.AllowPrivateMessages) - @Html.EditorFor(model => model.AllowPrivateMessages) + @Html.SettingEditorFor(model => model.AllowPrivateMessages, Html.CheckBoxFor(model => model.AllowPrivateMessages, new { data_toggler_for = "#pnlPrivateMessages" })) @Html.ValidationMessageFor(model => model.AllowPrivateMessages)
                                        - @Html.SmartLabelFor(model => model.ShowAlertForPM) - - @Html.SettingEditorFor(model => model.ShowAlertForPM) - @Html.ValidationMessageFor(model => model.ShowAlertForPM) -
                                        - @Html.SmartLabelFor(model => model.NotifyAboutPrivateMessages) - - @Html.SettingEditorFor(model => model.NotifyAboutPrivateMessages) - @Html.ValidationMessageFor(model => model.NotifyAboutPrivateMessages) -
                                        + @Html.SmartLabelFor(model => model.ShowAlertForPM) + + @Html.SettingEditorFor(model => model.ShowAlertForPM) + @Html.ValidationMessageFor(model => model.ShowAlertForPM) +
                                        + @Html.SmartLabelFor(model => model.NotifyAboutPrivateMessages) + + @Html.SettingEditorFor(model => model.NotifyAboutPrivateMessages) + @Html.ValidationMessageFor(model => model.NotifyAboutPrivateMessages) +

                                        @@ -181,36 +186,38 @@ @Html.SmartLabelFor(model => model.ForumFeedsEnabled)
                                        - @Html.SettingEditorFor(model => model.ForumFeedsEnabled) + @Html.SettingEditorFor(model => model.ForumFeedsEnabled, Html.CheckBoxFor(model => model.ForumFeedsEnabled, new { data_toggler_for = "#pnlForumFeeds" })) @Html.ValidationMessageFor(model => model.ForumFeedsEnabled)
                                        - @Html.SmartLabelFor(model => model.ForumFeedCount) - - @Html.SettingEditorFor(model => model.ForumFeedCount) - @Html.ValidationMessageFor(model => model.ForumFeedCount) -
                                        - @Html.SmartLabelFor(model => model.ActiveDiscussionsFeedEnabled) - - @Html.SettingEditorFor(model => model.ActiveDiscussionsFeedEnabled) - @Html.ValidationMessageFor(model => model.ActiveDiscussionsFeedEnabled) -
                                        - @Html.SmartLabelFor(model => model.ActiveDiscussionsFeedCount) - - @Html.SettingEditorFor(model => model.ActiveDiscussionsFeedCount) - @Html.ValidationMessageFor(model => model.ActiveDiscussionsFeedCount) -
                                        + @Html.SmartLabelFor(model => model.ForumFeedCount) + + @Html.SettingEditorFor(model => model.ForumFeedCount) + @Html.ValidationMessageFor(model => model.ForumFeedCount) +
                                        + @Html.SmartLabelFor(model => model.ActiveDiscussionsFeedEnabled) + + @Html.SettingEditorFor(model => model.ActiveDiscussionsFeedEnabled) + @Html.ValidationMessageFor(model => model.ActiveDiscussionsFeedEnabled) +
                                        + @Html.SmartLabelFor(model => model.ActiveDiscussionsFeedCount) + + @Html.SettingEditorFor(model => model.ActiveDiscussionsFeedCount) + @Html.ValidationMessageFor(model => model.ActiveDiscussionsFeedCount) +
                                        } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/GeneralCommon.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/GeneralCommon.cshtml index ae766be3b4..51af5d105a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/GeneralCommon.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/GeneralCommon.cshtml @@ -1,10 +1,5 @@ -@model GeneralCommonSettingsModel - -@using Telerik.Web.Mvc.UI; -@using SmartStore.Core.Domain.Customers; -@using SmartStore.Core.Domain.Security; -@using SmartStore.Web.Framework.UI; - +@using SmartStore.Web.Framework.UI; +@model GeneralCommonSettingsModel @{ ViewBag.Title = T("Admin.Configuration.Settings.GeneralCommon").Text; } @@ -16,65 +11,36 @@ @T("Admin.Configuration.Settings.GeneralCommon")
                                        - +
                                        Html.RenderAction("StoreScopeConfiguration", "Setting"); @Html.ValidationSummary(false) - @(Html.SmartStore().TabStrip().Name("generalsettings-edit").Items(x => + @(Html.SmartStore().TabStrip().Name("generalsettings-edit").Style(TabsStyle.Material).Items(x => { x.Add().Text(T("Common.General").Text).Content(@TabStoreInformationSettings()).Selected(true); x.Add().Text(T("Admin.Configuration.Settings.GeneralCommon.SEOSettings").Text).Content(@TabSEOSettings()); @@ -86,15 +52,17 @@ x.Add().Text(T("Admin.Configuration.Settings.GeneralCommon.SocialSettings").Text).Content(@TabSocialSettings()); })) } + @helper TabStoreInformationSettings() - { +{ @@ -109,8 +77,9 @@
                                        @Html.SmartLabelFor(model => model.StoreInformationSettings.StoreClosed) - @Html.SettingEditorFor(model => model.StoreInformationSettings.StoreClosed) + @Html.SettingEditorFor(model => model.StoreInformationSettings.StoreClosed, + Html.CheckBoxFor(model => model.StoreInformationSettings.StoreClosed, new { data_toggler_for = "#pnlStoreClosedAllowForAdmins" })) @Html.ValidationMessageFor(model => model.StoreInformationSettings.StoreClosed)
                                        } + @helper TabSEOSettings() - { +{ @@ -163,16 +131,17 @@ @Html.SmartLabelFor(model => model.SeoSettings.MetaRobotsContent) @@ -190,8 +159,7 @@ @Html.SmartLabelFor(model => model.SeoSettings.CanonicalHostNameRule) @@ -200,7 +168,7 @@ @Html.SmartLabelFor(model => model.SeoSettings.ExtraRobotsDisallows) @@ -234,7 +202,7 @@ @Html.SmartLabelFor(model => model.SeoSettings.SeoNameCharConversion) @@ -243,11 +211,13 @@ @Html.SmartLabelFor(model => model.SeoSettings.TestSeoNameCreation) @@ -255,70 +225,78 @@  
                                        @@ -126,8 +95,7 @@ @Html.SmartLabelFor(model => model.SeoSettings.PageTitleSeoAdjustment) - @Html.SettingOverrideCheckbox(model => model.SeoSettings.PageTitleSeoAdjustment) - @Html.DropDownListForEnum(model => model.SeoSettings.PageTitleSeoAdjustment ) + @Html.EnumSettingEditorFor(model => model.SeoSettings.PageTitleSeoAdjustment) @Html.ValidationMessageFor(model => model.SeoSettings.PageTitleSeoAdjustment)
                                        - @Html.SettingOverrideCheckbox(model => model.SeoSettings.MetaRobotsContent) - @Html.DropDownListFor(model => model.SeoSettings.MetaRobotsContent, new List - { - new SelectListItem { Text = "index", Value = "index" }, - new SelectListItem { Text = "noindex", Value = "noindex" }, - new SelectListItem { Text = "index, follow", Value = "index, follow" }, - new SelectListItem { Text = "index, nofollow", Value = "index, nofollow" }, - new SelectListItem { Text = "noindex, follow", Value = "noindex, follow" }, - new SelectListItem { Text = "noindex, nofollow", Value = "noindex, nofollow" } - }, T("Common.Unspecified")) + @Html.SettingEditorFor(model => model.SeoSettings.MetaRobotsContent, Html.DropDownListFor(model => model.SeoSettings.MetaRobotsContent, + new List + { + new SelectListItem { Text = "index", Value = "index" }, + new SelectListItem { Text = "noindex", Value = "noindex" }, + new SelectListItem { Text = "index, follow", Value = "index, follow" }, + new SelectListItem { Text = "index, nofollow", Value = "index, nofollow" }, + new SelectListItem { Text = "noindex, follow", Value = "noindex, follow" }, + new SelectListItem { Text = "noindex, nofollow", Value = "noindex, nofollow" } + }, + T("Common.Unspecified"))) @Html.ValidationMessageFor(model => model.SeoSettings.MetaRobotsContent)
                                        - @Html.SettingOverrideCheckbox(model => model.SeoSettings.CanonicalHostNameRule) - @Html.DropDownListForEnum(model => model.SeoSettings.CanonicalHostNameRule) + @Html.EnumSettingEditorFor(model => model.SeoSettings.CanonicalHostNameRule) @Html.ValidationMessageFor(model => model.SeoSettings.CanonicalHostNameRule)
                                        - @Html.TextAreaFor(model => model.SeoSettings.ExtraRobotsDisallows, new { @class = "input-large", style = "height:250px" }) + @Html.TextAreaFor(model => model.SeoSettings.ExtraRobotsDisallows, new { style = "height:250px" }) @Html.ValidationMessageFor(model => model.SeoSettings.ExtraRobotsDisallows)
                                        - @Html.TextAreaFor(model => model.SeoSettings.SeoNameCharConversion, new { @class = "input-large", style = "height:250px" }) + @Html.TextAreaFor(model => model.SeoSettings.SeoNameCharConversion, new { style = "height:250px" }) @Html.ValidationMessageFor(model => model.SeoSettings.SeoNameCharConversion)
                                        - @Html.EditorFor(model => model.SeoSettings.TestSeoNameCreation) - - +
                                        + @Html.EditorFor(model => model.SeoSettings.TestSeoNameCreation, new { @class = "form-control" }) + +
                                        +
                                        } + @helper TabSecuritySettings() - { - - - - - - - - - - - - - - - - - - - - - - - +{ +
                                        - @Html.SmartLabelFor(model => model.SecuritySettings.EncryptionKey) - - @Html.EditorFor(model => model.SecuritySettings.EncryptionKey) - @Html.ValidationMessageFor(model => model.SecuritySettings.EncryptionKey) - -
                                        - @Html.SmartLabelFor(model => model.SecuritySettings.AdminAreaAllowedIpAddresses) - - @Html.EditorFor(model => model.SecuritySettings.AdminAreaAllowedIpAddresses) - @Html.ValidationMessageFor(model => model.SecuritySettings.AdminAreaAllowedIpAddresses) -
                                        - @Html.SmartLabelFor(model => model.SecuritySettings.HideAdminMenuItemsBasedOnPermissions) - - @Html.EditorFor(model => model.SecuritySettings.HideAdminMenuItemsBasedOnPermissions) - @Html.ValidationMessageFor(model => model.SecuritySettings.HideAdminMenuItemsBasedOnPermissions) -
                                        - @Html.SmartLabelFor(model => model.SecuritySettings.ForceSslForAllPages) - - @Html.EditorFor(model => model.SecuritySettings.ForceSslForAllPages) - @Html.ValidationMessageFor(model => model.SecuritySettings.ForceSslForAllPages) -
                                         
                                        -
                                        - @T("Admin.Configuration.Settings.General.Common.Captcha.Hint") -
                                        -
                                        + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - @@ -327,7 +305,7 @@ @Html.ValidationMessageFor(model => model.CaptchaSettings.ShowOnLoginPage) - + @@ -336,7 +314,7 @@ @Html.ValidationMessageFor(model => model.CaptchaSettings.ShowOnRegistrationPage) - + @@ -345,7 +323,7 @@ @Html.ValidationMessageFor(model => model.CaptchaSettings.ShowOnContactUsPage) - + @@ -354,7 +332,7 @@ @Html.ValidationMessageFor(model => model.CaptchaSettings.ShowOnEmailWishlistToFriendPage) - + @@ -363,7 +341,7 @@ @Html.ValidationMessageFor(model => model.CaptchaSettings.ShowOnEmailProductToFriendPage) - + @@ -372,7 +350,7 @@ @Html.ValidationMessageFor(model => model.CaptchaSettings.ShowOnAskQuestionPage) - + @@ -381,7 +359,7 @@ @Html.ValidationMessageFor(model => model.CaptchaSettings.ShowOnBlogCommentPage) - + @@ -390,7 +368,7 @@ @Html.ValidationMessageFor(model => model.CaptchaSettings.ShowOnNewsCommentPage) - + @@ -399,7 +377,7 @@ @Html.ValidationMessageFor(model => model.CaptchaSettings.ShowOnProductReviewPage) - + @@ -408,7 +386,7 @@ @Html.ValidationMessageFor(model => model.CaptchaSettings.ReCaptchaPublicKey) - + @@ -417,10 +395,12 @@ @Html.ValidationMessageFor(model => model.CaptchaSettings.ReCaptchaPrivateKey) -
                                        + @Html.SmartLabelFor(model => model.SecuritySettings.EncryptionKey) + +
                                        + @Html.EditorFor(model => model.SecuritySettings.EncryptionKey, new { @class = "form-control" }) + +
                                        + @Html.ValidationMessageFor(model => model.SecuritySettings.EncryptionKey) +
                                        + @Html.SmartLabelFor(model => model.SecuritySettings.AdminAreaAllowedIpAddresses) + + @Html.EditorFor(model => model.SecuritySettings.AdminAreaAllowedIpAddresses) + @Html.ValidationMessageFor(model => model.SecuritySettings.AdminAreaAllowedIpAddresses) +
                                        + @Html.SmartLabelFor(model => model.SecuritySettings.HideAdminMenuItemsBasedOnPermissions) + + @Html.EditorFor(model => model.SecuritySettings.HideAdminMenuItemsBasedOnPermissions) + @Html.ValidationMessageFor(model => model.SecuritySettings.HideAdminMenuItemsBasedOnPermissions) +
                                        + @Html.SmartLabelFor(model => model.SecuritySettings.ForceSslForAllPages) + + @Html.EditorFor(model => model.SecuritySettings.ForceSslForAllPages) + @Html.ValidationMessageFor(model => model.SecuritySettings.ForceSslForAllPages) +
                                         
                                        +
                                        + @T("Admin.Configuration.Settings.General.Common.Captcha.Hint") +
                                        +
                                        + @Html.SmartLabelFor(model => model.CaptchaSettings.Enabled) + + @Html.SettingEditorFor(model => model.CaptchaSettings.Enabled, + Html.CheckBoxFor(model => model.CaptchaSettings.Enabled, new { data_toggler_for = "#pnlCaptchaSettings" })) + @Html.ValidationMessageFor(model => model.CaptchaSettings.Enabled) +
                                        - @Html.SmartLabelFor(model => model.CaptchaSettings.Enabled) - - @Html.SettingEditorFor(model => model.CaptchaSettings.Enabled) - @Html.ValidationMessageFor(model => model.CaptchaSettings.Enabled) -
                                        @Html.SmartLabelFor(model => model.CaptchaSettings.ShowOnLoginPage)
                                        @Html.SmartLabelFor(model => model.CaptchaSettings.ShowOnRegistrationPage)
                                        @Html.SmartLabelFor(model => model.CaptchaSettings.ShowOnContactUsPage)
                                        @Html.SmartLabelFor(model => model.CaptchaSettings.ShowOnEmailWishlistToFriendPage)
                                        @Html.SmartLabelFor(model => model.CaptchaSettings.ShowOnEmailProductToFriendPage)
                                        @Html.SmartLabelFor(model => model.CaptchaSettings.ShowOnAskQuestionPage)
                                        @Html.SmartLabelFor(model => model.CaptchaSettings.ShowOnBlogCommentPage)
                                        @Html.SmartLabelFor(model => model.CaptchaSettings.ShowOnNewsCommentPage)
                                        @Html.SmartLabelFor(model => model.CaptchaSettings.ShowOnProductReviewPage)
                                        @Html.SmartLabelFor(model => model.CaptchaSettings.ReCaptchaPublicKey)
                                        @Html.SmartLabelFor(model => model.CaptchaSettings.ReCaptchaPrivateKey)
                                        + + } + @helper TabPdfSettings() - { +{
                                        @@ -474,6 +454,7 @@
                                        } + @helper TabLocalizationSettings() { @@ -482,28 +463,33 @@ @Html.SmartLabelFor(model => model.LocalizationSettings.SeoFriendlyUrlsForLanguagesEnabled) - - - - - - - - + + + + + + + + + + @@ -583,8 +569,8 @@ @Html.SmartLabelFor(model => model.CompanyInformationSettings.CompanyManagementDescription) @@ -640,8 +626,8 @@ @Html.SmartLabelFor(model => model.CompanyInformationSettings.CountryId) @@ -840,54 +826,66 @@ @Html.SmartLabelFor(model => model.SocialSettings.ShowSocialLinksInFooter) - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + +
                                        - @Html.EditorFor(model => model.LocalizationSettings.SeoFriendlyUrlsForLanguagesEnabled) + @Html.SettingEditorFor(model => model.LocalizationSettings.SeoFriendlyUrlsForLanguagesEnabled, + Html.CheckBoxFor(model => model.LocalizationSettings.SeoFriendlyUrlsForLanguagesEnabled, new { data_toggler_for = "#pnlSeoFriendlyUrlsForLanguages" })) @Html.ValidationMessageFor(model => model.LocalizationSettings.SeoFriendlyUrlsForLanguagesEnabled)
                                        - @Html.SmartLabelFor(model => model.LocalizationSettings.DefaultLanguageRedirectBehaviour) - - @Html.DropDownListForEnum(model => model.LocalizationSettings.DefaultLanguageRedirectBehaviour) - @Html.ValidationMessageFor(model => model.LocalizationSettings.DefaultLanguageRedirectBehaviour) -
                                        - @Html.SmartLabelFor(model => model.LocalizationSettings.InvalidLanguageRedirectBehaviour) - - @Html.DropDownListForEnum(model => model.LocalizationSettings.InvalidLanguageRedirectBehaviour) - @Html.ValidationMessageFor(model => model.LocalizationSettings.InvalidLanguageRedirectBehaviour) -
                                        + @Html.SmartLabelFor(model => model.LocalizationSettings.DefaultLanguageRedirectBehaviour) + + @Html.SettingEditorFor(model => model.LocalizationSettings.DefaultLanguageRedirectBehaviour, + Html.DropDownListForEnum(model => model.LocalizationSettings.DefaultLanguageRedirectBehaviour)) + @Html.ValidationMessageFor(model => model.LocalizationSettings.DefaultLanguageRedirectBehaviour) +
                                        + @Html.SmartLabelFor(model => model.LocalizationSettings.InvalidLanguageRedirectBehaviour) + + @Html.SettingEditorFor(model => model.LocalizationSettings.InvalidLanguageRedirectBehaviour, + Html.DropDownListForEnum(model => model.LocalizationSettings.InvalidLanguageRedirectBehaviour)) + @Html.ValidationMessageFor(model => model.LocalizationSettings.InvalidLanguageRedirectBehaviour) +
                                        @Html.SmartLabelFor(model => model.LocalizationSettings.UseImagesForLanguageSelection) @@ -546,8 +532,8 @@ @Html.SmartLabelFor(model => model.CompanyInformationSettings.Salutation) - @Html.SettingOverrideCheckbox(model => model.CompanyInformationSettings.Salutation) - @Html.DropDownListFor(model => model.CompanyInformationSettings.Salutation, Model.CompanyInformationSettings.Salutations) + @Html.SettingEditorFor(model => model.CompanyInformationSettings.Salutation, + Html.DropDownListFor(model => model.CompanyInformationSettings.Salutation, Model.CompanyInformationSettings.Salutations)) @Html.ValidationMessageFor(model => model.CompanyInformationSettings.Salutation)
                                        - @Html.SettingOverrideCheckbox(model => model.CompanyInformationSettings.CompanyManagementDescription) - @Html.DropDownListFor(model => model.CompanyInformationSettings.CompanyManagementDescription, Model.CompanyInformationSettings.ManagementDescriptions) + @Html.SettingEditorFor(model => model.CompanyInformationSettings.CompanyManagementDescription, + Html.DropDownListFor(model => model.CompanyInformationSettings.CompanyManagementDescription, Model.CompanyInformationSettings.ManagementDescriptions)) @Html.ValidationMessageFor(model => model.CompanyInformationSettings.CompanyManagementDescription)
                                        - @Html.SettingOverrideCheckbox(model => model.CompanyInformationSettings.CountryId) - @Html.DropDownListFor(model => model.CompanyInformationSettings.CountryId, Model.CompanyInformationSettings.AvailableCountries, new { style = "min-width:289px" } ) + @Html.SettingEditorFor(model => model.CompanyInformationSettings.CountryId, + Html.DropDownListFor(model => model.CompanyInformationSettings.CountryId, Model.CompanyInformationSettings.AvailableCountries)) @Html.ValidationMessageFor(model => model.CompanyInformationSettings.CountryId)
                                        - @Html.SettingEditorFor(model => model.SocialSettings.ShowSocialLinksInFooter) + @Html.SettingEditorFor(model => model.SocialSettings.ShowSocialLinksInFooter, + Html.CheckBoxFor(model => model.SocialSettings.ShowSocialLinksInFooter, new { data_toggler_for = "#pnlSocialLinks" })) @Html.ValidationMessageFor(model => model.SocialSettings.ShowSocialLinksInFooter)
                                        + @Html.SmartLabelFor(model => model.SocialSettings.FacebookLink) + + @Html.SettingEditorFor(model => model.SocialSettings.FacebookLink) + @Html.ValidationMessageFor(model => model.SocialSettings.FacebookLink) +
                                        + @Html.SmartLabelFor(model => model.SocialSettings.GooglePlusLink) + + @Html.SettingEditorFor(model => model.SocialSettings.GooglePlusLink) + @Html.ValidationMessageFor(model => model.SocialSettings.GooglePlusLink) +
                                        + @Html.SmartLabelFor(model => model.SocialSettings.TwitterLink) + + @Html.SettingEditorFor(model => model.SocialSettings.TwitterLink) + @Html.ValidationMessageFor(model => model.SocialSettings.TwitterLink) +
                                        + @Html.SmartLabelFor(model => model.SocialSettings.PinterestLink) + + @Html.SettingEditorFor(model => model.SocialSettings.PinterestLink) + @Html.ValidationMessageFor(model => model.SocialSettings.PinterestLink) +
                                        + @Html.SmartLabelFor(model => model.SocialSettings.YoutubeLink) + + @Html.SettingEditorFor(model => model.SocialSettings.YoutubeLink) + @Html.ValidationMessageFor(model => model.SocialSettings.YoutubeLink) +
                                        + @Html.SmartLabelFor(model => model.SocialSettings.InstagramLink) + + @Html.SettingEditorFor(model => model.SocialSettings.InstagramLink) + @Html.ValidationMessageFor(model => model.SocialSettings.InstagramLink) +
                                        } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Media.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Media.cshtml index c819d0e1cf..cd2ee3bb54 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Media.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Media.cshtml @@ -1,5 +1,4 @@ @model MediaSettingsModel -@using Telerik.Web.Mvc.UI; @{ ViewBag.Title = T("Admin.Configuration.Settings.Media").Text; } @@ -11,7 +10,10 @@ @T("Admin.Configuration.Settings.Media")
                                        - +
                                        @@ -20,12 +22,21 @@ @Html.ValidationSummary(false) - + + + + + @@ -34,7 +45,7 @@ @Html.SmartLabelFor(model => model.ProductThumbPictureSize) @@ -43,7 +54,7 @@ @Html.SmartLabelFor(model => model.ProductThumbPictureSizeOnProductDetailsPage) @@ -52,7 +63,7 @@ @Html.SmartLabelFor(model => model.MessageProductThumbPictureSize) @@ -61,7 +72,7 @@ @Html.SmartLabelFor(model => model.ProductDetailsPictureSize) @@ -79,7 +90,7 @@ @Html.SmartLabelFor(model => model.AssociatedProductPictureSize) @@ -88,7 +99,7 @@ @Html.SmartLabelFor(model => model.BundledProductPictureSize) @@ -97,7 +108,7 @@ @Html.SmartLabelFor(model => model.CategoryThumbPictureSize) @@ -106,7 +117,7 @@ @Html.SmartLabelFor(model => model.ManufacturerThumbPictureSize) @@ -115,7 +126,7 @@ @Html.SmartLabelFor(model => model.CartThumbPictureSize) @@ -124,7 +135,7 @@ @Html.SmartLabelFor(model => model.CartThumbBundleItemPictureSize) @@ -133,7 +144,7 @@ @Html.SmartLabelFor(model => model.MiniCartThumbPictureSize) @@ -142,7 +153,7 @@ @Html.SmartLabelFor(model => model.MaximumImageSize) @@ -154,10 +165,13 @@
                                        - @T("Admin.Configuration.Settings.Media.MoveMediaNote") -

                                        - @(T("Admin.Configuration.Settings.Media.CurrentStorageLocation")): - @Model.StorageProvider +

                                        + @T("Admin.Configuration.Settings.Media.MoveMediaNote") +

                                        +

                                        + @(T("Admin.Configuration.Settings.Media.CurrentStorageLocation")): + @Model.StorageProvider +

                                        @@ -168,9 +182,9 @@ - - - - - - - - + + + + + + + + + + - - + + + + + - - -
                                        + @Html.SmartLabelFor(model => model.AutoGenerateAbsoluteUrls) + + @Html.SettingEditorFor(model => model.AutoGenerateAbsoluteUrls) + @Html.ValidationMessageFor(model => model.AutoGenerateAbsoluteUrls) +
                                        @Html.SmartLabelFor(model => model.AvatarPictureSize) - @Html.SettingEditorFor(model => model.AvatarPictureSize) + @Html.SettingEditorFor(model => model.AvatarPictureSize, null, new { postfix = T("Common.Pixel").Text }) @Html.ValidationMessageFor(model => model.AvatarPictureSize)
                                        - @Html.SettingEditorFor(model => model.ProductThumbPictureSize) + @Html.SettingEditorFor(model => model.ProductThumbPictureSize, null, new { postfix = T("Common.Pixel").Text }) @Html.ValidationMessageFor(model => model.ProductThumbPictureSize)
                                        - @Html.SettingEditorFor(model => model.ProductThumbPictureSizeOnProductDetailsPage) + @Html.SettingEditorFor(model => model.ProductThumbPictureSizeOnProductDetailsPage, null, new { postfix = T("Common.Pixel").Text }) @Html.ValidationMessageFor(model => model.ProductThumbPictureSizeOnProductDetailsPage)
                                        - @Html.SettingEditorFor(model => model.MessageProductThumbPictureSize) + @Html.SettingEditorFor(model => model.MessageProductThumbPictureSize, null, new { postfix = T("Common.Pixel").Text }) @Html.ValidationMessageFor(model => model.MessageProductThumbPictureSize)
                                        - @Html.SettingEditorFor(model => model.ProductDetailsPictureSize) + @Html.SettingEditorFor(model => model.ProductDetailsPictureSize, null, new { postfix = T("Common.Pixel").Text }) @Html.ValidationMessageFor(model => model.ProductDetailsPictureSize)
                                        - @Html.SettingEditorFor(model => model.AssociatedProductPictureSize) + @Html.SettingEditorFor(model => model.AssociatedProductPictureSize, null, new { postfix = T("Common.Pixel").Text }) @Html.ValidationMessageFor(model => model.AssociatedProductPictureSize)
                                        - @Html.SettingEditorFor(model => model.BundledProductPictureSize) + @Html.SettingEditorFor(model => model.BundledProductPictureSize, null, new { postfix = T("Common.Pixel").Text }) @Html.ValidationMessageFor(model => model.BundledProductPictureSize)
                                        - @Html.SettingEditorFor(model => model.CategoryThumbPictureSize) + @Html.SettingEditorFor(model => model.CategoryThumbPictureSize, null, new { postfix = T("Common.Pixel").Text }) @Html.ValidationMessageFor(model => model.CategoryThumbPictureSize)
                                        - @Html.SettingEditorFor(model => model.ManufacturerThumbPictureSize) + @Html.SettingEditorFor(model => model.ManufacturerThumbPictureSize, null, new { postfix = T("Common.Pixel").Text }) @Html.ValidationMessageFor(model => model.ManufacturerThumbPictureSize)
                                        - @Html.SettingEditorFor(model => model.CartThumbPictureSize) + @Html.SettingEditorFor(model => model.CartThumbPictureSize, null, new { postfix = T("Common.Pixel").Text }) @Html.ValidationMessageFor(model => model.CartThumbPictureSize)
                                        - @Html.SettingEditorFor(model => model.CartThumbBundleItemPictureSize) + @Html.SettingEditorFor(model => model.CartThumbBundleItemPictureSize, null, new { postfix = T("Common.Pixel").Text }) @Html.ValidationMessageFor(model => model.CartThumbBundleItemPictureSize)
                                        - @Html.SettingEditorFor(model => model.MiniCartThumbPictureSize) + @Html.SettingEditorFor(model => model.MiniCartThumbPictureSize, null, new { postfix = T("Common.Pixel").Text }) @Html.ValidationMessageFor(model => model.MiniCartThumbPictureSize)
                                        - @Html.SettingEditorFor(model => model.MaximumImageSize) + @Html.SettingEditorFor(model => model.MaximumImageSize, null, new { postfix = T("Common.Pixel").Text }) @Html.ValidationMessageFor(model => model.MaximumImageSize)
                                        @Html.DropDownListFor(model => model.StorageProvider, Model.AvailableStorageProvider) - @Html.ValidationMessageFor(model => model.StorageProvider) @@ -182,19 +196,18 @@ \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Shipping.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Shipping.cshtml index de94788f79..1cfc0d969d 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Shipping.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Shipping.cshtml @@ -3,33 +3,17 @@ ViewBag.Title = T("Admin.Configuration.Settings.Shipping").Text; } @using (Html.BeginForm()) -{ - - +{
                                        @T("Admin.Configuration.Settings.Shipping")
                                        - +
                                        @@ -43,28 +27,30 @@ @Html.SmartLabelFor(model => model.FreeShippingOverXEnabled)
                                        - @Html.SettingEditorFor(model => model.FreeShippingOverXEnabled) + @Html.SettingEditorFor(model => model.FreeShippingOverXEnabled, Html.CheckBoxFor(model => model.FreeShippingOverXEnabled, new { data_toggler_for = "#pnlFreeShipping" })) @Html.ValidationMessageFor(model => model.FreeShippingOverXEnabled)
                                        - @Html.SmartLabelFor(model => model.FreeShippingOverXValue) - - @Html.SettingEditorFor(model => model.FreeShippingOverXValue) - @Html.ValidationMessageFor(model => model.FreeShippingOverXValue) -
                                        - @Html.SmartLabelFor(model => model.FreeShippingOverXIncludingTax) - - @Html.SettingEditorFor(model => model.FreeShippingOverXIncludingTax) - @Html.ValidationMessageFor(model => model.FreeShippingOverXIncludingTax) -
                                        + @Html.SmartLabelFor(model => model.FreeShippingOverXValue) + + @Html.SettingEditorFor(model => model.FreeShippingOverXValue, null, new { postfix = Model.PrimaryStoreCurrencyCode }) + @Html.ValidationMessageFor(model => model.FreeShippingOverXValue) +
                                        + @Html.SmartLabelFor(model => model.FreeShippingOverXIncludingTax) + + @Html.SettingEditorFor(model => model.FreeShippingOverXIncludingTax) + @Html.ValidationMessageFor(model => model.FreeShippingOverXIncludingTax) +
                                        @Html.SmartLabelFor(model => model.EstimateShippingEnabled) @@ -92,19 +78,24 @@ @Html.ValidationMessageFor(model => model.SkipShippingIfSingleOption)
                                        + @Html.SmartLabelFor(model => model.ChargeOnlyHighestProductShippingSurcharge) + + @Html.SettingEditorFor(model => model.ChargeOnlyHighestProductShippingSurcharge) + @Html.ValidationMessageFor(model => model.ChargeOnlyHighestProductShippingSurcharge) +
                                        @Html.SmartLabelFor(model => model.ShippingOriginAddress) - @Html.SettingOverrideCheckbox(model => Model.ShippingOriginAddress, "#pnlShippingOriginAddress") + @Html.SettingEditorFor(model => Model.ShippingOriginAddress, @
                                        + @Html.EditorFor(model => model.ShippingOriginAddress, "Address") +
                                        )
                                        - @Html.EditorFor(model => model.ShippingOriginAddress, "Address") -
                                        } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/ShoppingCart.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/ShoppingCart.cshtml index a02e0115f8..8eb7e14064 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/ShoppingCart.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/ShoppingCart.cshtml @@ -10,52 +10,27 @@ @T("Admin.Configuration.Settings.ShoppingCart")
                                        - +
                                        + Html.RenderAction("StoreScopeConfiguration", "Setting"); @Html.ValidationSummary(false) - @(Html.SmartStore().TabStrip().Name("catalogsettings-edit").Items(x => + @(Html.SmartStore().TabStrip().Name("catalogsettings-edit").Style(TabsStyle.Material).Items(x => { x.Add().Text(T("Admin.Configuration.Settings.ShoppingCart.CartSettings").Text).Content(@TabCartSettings()).Selected(true); x.Add().Text(T("Admin.Configuration.Settings.ShoppingCart.Checkout").Text).Content(@TabCheckoutSettings()); @@ -159,29 +134,20 @@ @Html.SmartLabelFor(model => model.MiniShoppingCartEnabled) - @Html.SettingEditorFor(model => model.MiniShoppingCartEnabled) + @Html.SettingEditorFor(model => model.MiniShoppingCartEnabled, Html.CheckBoxFor(model => model.MiniShoppingCartEnabled, new { data_toggler_for = "#pnlShowProductImagesInMiniShoppingCart" })) @Html.ValidationMessageFor(model => model.MiniShoppingCartEnabled) - - - @Html.SmartLabelFor(model => model.MiniShoppingCartProductNumber) - - - @Html.SettingEditorFor(model => model.MiniShoppingCartProductNumber) - @Html.ValidationMessageFor(model => model.MiniShoppingCartProductNumber) - - - - - @Html.SmartLabelFor(model => model.ShowProductImagesInMiniShoppingCart) - - - @Html.SettingEditorFor(model => model.ShowProductImagesInMiniShoppingCart) - @Html.ValidationMessageFor(model => model.ShowProductImagesInMiniShoppingCart) - - - + + + @Html.SmartLabelFor(model => model.ShowProductImagesInMiniShoppingCart) + + + @Html.SettingEditorFor(model => model.ShowProductImagesInMiniShoppingCart) + @Html.ValidationMessageFor(model => model.ShowProductImagesInMiniShoppingCart) + + +
                                        @@ -215,15 +181,6 @@ @Html.ValidationMessageFor(model => model.CrossSellsNumber) - - - @Html.SmartLabelFor(model => model.RoundPricesDuringCalculation) - - - @Html.SettingEditorFor(model => model.RoundPricesDuringCalculation) - @Html.ValidationMessageFor(model => model.RoundPricesDuringCalculation) - - } @@ -280,7 +237,7 @@ @Html.SmartLabelFor(model => model.EmailWishlistEnabled) - @Html.SettingEditorFor(model => model.EmailWishlistEnabled) + @Html.SettingEditorFor(model => model.EmailWishlistEnabled, Html.CheckBoxFor(model => model.EmailWishlistEnabled, new { data_toggler_for = "#pnlAllowAnonymousUsersToEmailWishlist" })) @Html.ValidationMessageFor(model => model.EmailWishlistEnabled) @@ -298,7 +255,7 @@ @helper TabCheckoutSettings() { - +
                                        @@ -348,16 +304,13 @@ @Html.SmartLabelFor(model => model.ThirdPartyEmailHandOver)
                                        @@ -320,8 +277,7 @@ @Html.SmartLabelFor(model => model.NewsLetterSubscription)
                                        - @Html.SettingOverrideCheckbox(model => Model.NewsLetterSubscription) - @Html.DropDownListFor(model => model.NewsLetterSubscription, Model.AvailableNewsLetterSubscriptions) + @Html.SettingEditorFor(model => model.NewsLetterSubscription, Html.DropDownListFor(model => model.NewsLetterSubscription, Model.AvailableNewsLetterSubscriptions)) @Html.ValidationMessageFor(model => model.NewsLetterSubscription)
                                        - @Html.SettingOverrideCheckbox(model => Model.ThirdPartyEmailHandOver) - @Html.DropDownListFor(model => model.ThirdPartyEmailHandOver, Model.AvailableThirdPartyEmailHandOver) + @Html.SettingEditorFor(model => model.ThirdPartyEmailHandOver, Html.DropDownListFor(model => model.ThirdPartyEmailHandOver, Model.AvailableThirdPartyEmailHandOver)) @Html.ValidationMessageFor(model => model.ThirdPartyEmailHandOver)
                                        -

                                        - @(Html.LocalizedEditor("setting-shopping-cart-localized", @ @@ -365,13 +318,11 @@ @Html.SmartLabelFor(model => model.Locales[item].ThirdPartyEmailHandOverLabel) - - -
                                        - @Html.TextAreaFor(model => Model.Locales[item].ThirdPartyEmailHandOverLabel, new { @class = "input-large" }) - @Html.ValidationMessageFor(model => model.Locales[item].ThirdPartyEmailHandOverLabel) -
                                        + @*IMPORTANT: Do not delete, this hidden element contains the id to assign localized values to the corresponding language *@ @Html.HiddenFor(model => model.Locales[item].LanguageId) + + @Html.TextAreaFor(model => Model.Locales[item].ThirdPartyEmailHandOverLabel) + @Html.ValidationMessageFor(model => model.Locales[item].ThirdPartyEmailHandOverLabel)
                                        @@ -382,7 +333,7 @@ @Html.SmartLabelFor(model => model.ThirdPartyEmailHandOverLabel) - @Html.TextAreaFor(model => model.ThirdPartyEmailHandOverLabel, new { @class = "input-large" }) + @Html.TextAreaFor(model => model.ThirdPartyEmailHandOverLabel) @Html.ValidationMessageFor(model => model.ThirdPartyEmailHandOverLabel) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/StoreScopeConfiguration.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/StoreScopeConfiguration.cshtml index 10acb90540..bc2df5e4b9 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/StoreScopeConfiguration.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/StoreScopeConfiguration.cshtml @@ -1,13 +1,11 @@ @model StoreScopeConfigurationModel @using SmartStore.Core -
                                        - +
                                        +
                                        - - + } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Tax.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Tax.cshtml index 7638da9f81..da8451c205 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Tax.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Tax.cshtml @@ -1,6 +1,4 @@ -@using Telerik.Web.Mvc.UI -@using SmartStore.Core.Domain.Tax - +@using SmartStore.Core.Domain.Tax @model TaxSettingsModel @{ ViewBag.Title = T("Admin.Configuration.Settings.Tax").Text; @@ -9,19 +7,8 @@ {
                                        @@ -79,7 +28,10 @@ @T("Admin.Configuration.Settings.Tax")
                                        - +
                                        @@ -99,25 +51,24 @@ @*not relevant for european market but we have to deal with this option later*@ - @* - - - *@ + @* + + + *@ - + - + - + - - - @@ -230,62 +176,61 @@ @Html.SmartLabelFor(model => model.ShippingIsTaxable) - - - - - - - - - - - + + + + + + + + + + - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + +
                                        -
                                        - @T("Admin.Configuration.Settings.StoreScope") -
                                        + @Html.SmartLabel("store-scope-configuration", T("Admin.Configuration.Settings.StoreScope"))
                                        @Html.DropDownList("store-scope-configuration", Model.AllStores, new { onchange = "setLocation(this.value);" }) @@ -16,13 +14,12 @@ @if (Model.StoreId > 0) {
                                          - +   +
                                        + + +
                                        - @Html.SmartLabelFor(model => model.AllowCustomersToSelectTaxDisplayType) - - @Html.EditorFor(model => model.AllowCustomersToSelectTaxDisplayType) - @Html.ValidationMessageFor(model => model.AllowCustomersToSelectTaxDisplayType) -
                                        + @Html.SmartLabelFor(model => model.AllowCustomersToSelectTaxDisplayType) + + @Html.CheckBoxFor(model => model.AllowCustomersToSelectTaxDisplayType, new { data_toggler_for = "#pnlTaxDisplayType" }) + @Html.ValidationMessageFor(model => model.AllowCustomersToSelectTaxDisplayType) +
                                        @Html.SmartLabelFor(model => model.TaxDisplayType) - @Html.SettingOverrideCheckbox(model => Model.TaxDisplayType) - @Html.DropDownListForEnum(model => model.TaxDisplayType) - @Html.ValidationMessageFor(model => model.TaxDisplayType) - + @Html.EnumSettingEditorFor(model => model.TaxDisplayType) + @Html.ValidationMessageFor(model => model.TaxDisplayType) +
                                        @@ -191,25 +142,21 @@ @Html.SmartLabelFor(model => model.TaxBasedOn) - @Html.SettingOverrideCheckbox(model => Model.TaxBasedOn) - @Html.DropDownListForEnum(model => model.TaxBasedOn) - @Html.ValidationMessageFor(model => model.TaxBasedOn) - + @Html.EnumSettingEditorFor(model => model.TaxBasedOn) + @Html.ValidationMessageFor(model => model.TaxBasedOn) +
                                        @Html.SmartLabelFor(model => model.DefaultTaxAddress) - @Html.SettingOverrideCheckbox(model => Model.DefaultTaxAddress, "#pnlDefaultTaxAddress") + @Html.SettingEditorFor(model => Model.DefaultTaxAddress, @
                                        + @Html.EditorFor(x => x.DefaultTaxAddress, "Address") +
                                        )
                                        - @Html.EditorFor(x => x.DefaultTaxAddress, "Address") -

                                        @@ -220,8 +167,7 @@ @Html.SmartLabelFor(model => model.AuxiliaryServicesTaxingType)
                                        - @Html.SettingOverrideCheckbox(model => Model.AuxiliaryServicesTaxingType) - @Html.DropDownListFor(model => model.AuxiliaryServicesTaxingType, Model.AvailableAuxiliaryServicesTaxTypes) + @Html.EnumSettingEditorFor(model => model.AuxiliaryServicesTaxingType) @Html.ValidationMessageFor(model => model.AuxiliaryServicesTaxingType)
                                        - @Html.SettingEditorFor(model => model.ShippingIsTaxable) + @Html.SettingEditorFor(model => model.ShippingIsTaxable, Html.CheckBoxFor(model => model.ShippingIsTaxable, new { data_toggler_for = "#pnlShippingIsTaxable" })) @Html.ValidationMessageFor(model => model.ShippingIsTaxable)
                                        - @Html.SmartLabelFor(model => model.ShippingPriceIncludesTax) - - @Html.SettingEditorFor(model => model.ShippingPriceIncludesTax) - @Html.ValidationMessageFor(model => model.ShippingPriceIncludesTax) -
                                        - @Html.SmartLabelFor(model => model.ShippingTaxClassId) - - @Html.SettingOverrideCheckbox(model => Model.ShippingTaxClassId) - @Html.DropDownListFor(model => model.ShippingTaxClassId, Model.ShippingTaxCategories, T("Common.PleaseSelect").Text) - @Html.ValidationMessageFor(model => model.ShippingTaxClassId) -
                                        -   -
                                        + @Html.SmartLabelFor(model => model.ShippingPriceIncludesTax) + + @Html.SettingEditorFor(model => model.ShippingPriceIncludesTax) + @Html.ValidationMessageFor(model => model.ShippingPriceIncludesTax) +
                                        + @Html.SmartLabelFor(model => model.ShippingTaxClassId) + + @Html.SettingEditorFor(model => model.ShippingTaxClassId, Html.DropDownListFor(model => model.ShippingTaxClassId, Model.ShippingTaxCategories, T("Common.PleaseSelect"))) + @Html.ValidationMessageFor(model => model.ShippingTaxClassId) +
                                        @Html.SmartLabelFor(model => model.PaymentMethodAdditionalFeeIsTaxable) - @Html.SettingEditorFor(model => model.PaymentMethodAdditionalFeeIsTaxable) + @Html.SettingEditorFor(model => model.PaymentMethodAdditionalFeeIsTaxable, + Html.CheckBoxFor(model => model.PaymentMethodAdditionalFeeIsTaxable, new { data_toggler_for = "#pnlPaymentMethodAdditionalFeeIsTaxable" })) @Html.ValidationMessageFor(model => model.PaymentMethodAdditionalFeeIsTaxable)
                                        - @Html.SmartLabelFor(model => model.PaymentMethodAdditionalFeeIncludesTax) - - @Html.SettingEditorFor(model => model.PaymentMethodAdditionalFeeIncludesTax) - @Html.ValidationMessageFor(model => model.PaymentMethodAdditionalFeeIncludesTax) -
                                        - @Html.SmartLabelFor(model => model.PaymentMethodAdditionalFeeTaxClassId) - - @Html.SettingOverrideCheckbox(model => Model.PaymentMethodAdditionalFeeTaxClassId) - @Html.DropDownListFor(model => model.PaymentMethodAdditionalFeeTaxClassId, Model.PaymentMethodAdditionalFeeTaxCategories, T("Common.PleaseSelect").Text) - @Html.ValidationMessageFor(model => model.PaymentMethodAdditionalFeeTaxClassId) -
                                        + @Html.SmartLabelFor(model => model.PaymentMethodAdditionalFeeIncludesTax) + + @Html.SettingEditorFor(model => model.PaymentMethodAdditionalFeeIncludesTax) + @Html.ValidationMessageFor(model => model.PaymentMethodAdditionalFeeIncludesTax) +
                                        + @Html.SmartLabelFor(model => model.PaymentMethodAdditionalFeeTaxClassId) + + @Html.SettingEditorFor(model => model.PaymentMethodAdditionalFeeTaxClassId, + Html.DropDownListFor(model => model.PaymentMethodAdditionalFeeTaxClassId, Model.PaymentMethodAdditionalFeeTaxCategories, T("Common.PleaseSelect"))) + @Html.ValidationMessageFor(model => model.PaymentMethodAdditionalFeeTaxClassId) +

                                        @@ -296,55 +241,56 @@ @Html.SmartLabelFor(model => model.EuVatEnabled)
                                        - @Html.SettingEditorFor(model => model.EuVatEnabled) + @Html.SettingEditorFor(model => model.EuVatEnabled, Html.CheckBoxFor(model => model.EuVatEnabled, new { data_toggler_for = "#pnlEuVat" })) @Html.ValidationMessageFor(model => model.EuVatEnabled)
                                        - @Html.SmartLabelFor(model => model.EuVatShopCountryId) - - @Html.SettingOverrideCheckbox(model => Model.EuVatShopCountryId) - @Html.DropDownListFor(model => model.EuVatShopCountryId, Model.EuVatShopCountries, T("Admin.Address.SelectCountry").Text) - @Html.ValidationMessageFor(model => model.EuVatShopCountryId) -
                                        - @Html.SmartLabelFor(model => model.EuVatAllowVatExemption) - - @Html.SettingEditorFor(model => model.EuVatAllowVatExemption) - @Html.ValidationMessageFor(model => model.EuVatAllowVatExemption) -
                                        - @Html.SmartLabelFor(model => model.EuVatUseWebService) - - @Html.SettingEditorFor(model => model.EuVatUseWebService) - @Html.ValidationMessageFor(model => model.EuVatUseWebService) -
                                        - @Html.SmartLabelFor(model => model.EuVatEmailAdminWhenNewVatSubmitted) - - @Html.SettingEditorFor(model => model.EuVatEmailAdminWhenNewVatSubmitted) - @Html.ValidationMessageFor(model => model.EuVatEmailAdminWhenNewVatSubmitted) -
                                        - @Html.SmartLabelFor(model => model.VatRequired) - - @Html.SettingEditorFor(model => model.VatRequired) - @Html.ValidationMessageFor(model => model.VatRequired) -
                                        + @Html.SmartLabelFor(model => model.EuVatShopCountryId) + + @Html.SettingEditorFor(model => model.EuVatShopCountryId, Html.DropDownListFor(model => model.EuVatShopCountryId, Model.EuVatShopCountries, T("Admin.Address.SelectCountry"))) + @Html.ValidationMessageFor(model => model.EuVatShopCountryId) +
                                        + @Html.SmartLabelFor(model => model.EuVatAllowVatExemption) + + @Html.SettingEditorFor(model => model.EuVatAllowVatExemption) + @Html.ValidationMessageFor(model => model.EuVatAllowVatExemption) +
                                        + @Html.SmartLabelFor(model => model.EuVatUseWebService) + + @Html.SettingEditorFor(model => model.EuVatUseWebService) + @Html.ValidationMessageFor(model => model.EuVatUseWebService) +
                                        + @Html.SmartLabelFor(model => model.EuVatEmailAdminWhenNewVatSubmitted) + + @Html.SettingEditorFor(model => model.EuVatEmailAdminWhenNewVatSubmitted) + @Html.ValidationMessageFor(model => model.EuVatEmailAdminWhenNewVatSubmitted) +
                                        + @Html.SmartLabelFor(model => model.VatRequired) + + @Html.SettingEditorFor(model => model.VatRequired) + @Html.ValidationMessageFor(model => model.VatRequired) +
                                        } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Address.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Address.cshtml index 3cd761d0c3..999d1b198a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Address.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Address.cshtml @@ -1,32 +1,5 @@ @model AddressModel -@if (Model.CountryEnabled && Model.StateProvinceEnabled) -{ - -} @Html.HiddenFor(model => model.Id) @Html.HiddenFor(model => model.FirstNameEnabled) @Html.HiddenFor(model => model.FirstNameRequired) @@ -51,7 +24,8 @@ @Html.HiddenFor(model => model.PhoneRequired) @Html.HiddenFor(model => model.FaxEnabled) @Html.HiddenFor(model => model.FaxRequired) - + +
                                        @if (Model.FirstNameEnabled) { @@ -119,8 +93,21 @@ @Html.SmartLabelFor(model => model.CountryId) } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Download.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Download.cshtml index 04de1aee37..ce6b893eef 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Download.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Download.cshtml @@ -1,4 +1,5 @@ @model int? + @using SmartStore.Core; @using SmartStore.Web.Framework.UI; @using SmartStore.Utilities; @@ -33,8 +34,9 @@ } @{ - var clientId = "download-editor-" + CommonHelper.GenerateRandomInteger(); - var downloadService = EngineContext.Current.Resolve(); + var random = CommonHelper.GenerateRandomInteger(); + var clientId = "download-editor-" + random; + var downloadService = EngineContext.Current.Resolve(); var download = downloadService.GetDownloadById(Model.GetValueOrDefault()); var initiallyShowUrlPanel = false; var hasFile = false; @@ -44,13 +46,11 @@ initiallyShowUrlPanel = !MinimalMode && download.UseDownloadUrl; hasFile = !download.UseDownloadUrl; } - - Html.AddScriptParts("~/bundles/fileupload"); - Html.AddCssFileParts("~/css/fileupload"); + Html.AddScriptParts(true, "~/Administration/Scripts/smartstore.download.js"); } -
                                        - - @if (!MinimalMode) - { -
                                        - - - + - } - else if (hasFile) - { - - - - } + } +
                                        -
                                        - +
                                        -
                                        - +
                                        @if (hasFile) { - - @download.Filename@download.Extension - - } - - - @if (hasFile) - { - - } - else - { - @T("Common.Fileuploader.Upload") - } - - - - - - -
                                        - -
                                        -
                                        + - -
                                         
                                        + } +
                                        + @(Html.SmartStore().FileUploader() + .UploadUrl(Url.Action("AsyncUpload", "Download", new { minimalMode = MinimalMode, fieldName = FieldName })) + .AcceptedFileTypes("") + .IconCssClass(MinimalMode ? "fa fa-upload" : "") + .ButtonOutlineStyle(hasFile) + .UploadText(hasFile ? T("Common.Replace").Text : T("Common.Fileuploader.Upload").Text) + .ShowRemoveButton(false) + .ShowRemoveButtonAfterUpload(false) + .OnUploadingHandlerName("onUploading_" + random) + .OnCompletedHandlerName("onCompleted_" + random) + .OnUploadCompletedHandlerName("onUploadCompleted_" + random) + )
                                        @@ -129,21 +122,31 @@ @if (!MinimalMode) {
                                        -
                                        +
                                        @{ var value = download != null ? download.DownloadUrl : ""; } - - - - + +
                                        } -
                                        \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Html.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Html.cshtml new file mode 100644 index 0000000000..933f097eb4 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Html.cshtml @@ -0,0 +1,73 @@ +@model String +@using SmartStore.Web.Framework.UI; +@{ + Html.AddScriptParts("~/bundles/codemirror"); + Html.AddCssFileParts("~/css/codemirror"); + + Html.AddScriptParts("~/bundles/summernote"); + Html.AppendCssFileParts(true, "~/Content/editors/summernote/summernote-bs4.css"); + + var lazy = ViewData["lazy"].Convert() ?? true; + var id = ViewData.TemplateInfo.GetFullHtmlFieldId(string.Empty); + var html = ((string)ViewData.TemplateInfo.FormattedModelValue); + var htmlIsEmpty = !html.HasValue() || html == "

                                        " || html == "

                                         

                                        " || html == "


                                        "; + + // Find summernote localization file + string culture = null; + var locFile = base.ResolveLocalizationFile(WorkContext.WorkingLanguage.LanguageCulture, "~/Content/editors/summernote/lang", "summernote-*.js", null); + if (locFile != null) + { + culture = locFile.Culture; + Html.AddScriptParts(true, locFile.VirtualPath); + } + + // Find summernote Link plugin localization file + locFile = base.ResolveLocalizationFile(WorkContext.WorkingLanguage.LanguageCulture, "~/Content/editors/summernote/plugin/link/lang", "*.js", null); + if (locFile != null) + { + Html.AddScriptParts(true, locFile.VirtualPath); + } + + // Find summernote ImageAttributes plugin localization file + locFile = base.ResolveLocalizationFile(WorkContext.WorkingLanguage.LanguageCulture, "~/Content/editors/summernote/plugin/image/lang", "*.js", null); + if (locFile != null) + { + Html.AddScriptParts(true, locFile.VirtualPath); + } +} + +@if (lazy) +{ +
                                        + @if (htmlIsEmpty) + { +
                                        + + @T("HtmlEditor.ClickToEdit") +
                                        + } + else + { + @Html.Raw(html) + } +
                                        +} + + +@Html.TextArea(string.Empty, /* Name suffix */ + html, /* Initial value */ + new { @class = "summernote-editor" + (lazy ? " d-none" : ""), data_file_browser_url = Url.Action("Index", "RoxyFileManager", new { area = "admin" }) } +) + + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Liquid.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Liquid.cshtml new file mode 100644 index 0000000000..79b651db02 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Liquid.cshtml @@ -0,0 +1,49 @@ + +@{ + string templateName = ViewData["TemplateName"]?.Convert(); + + Html.AddScriptParts("~/bundles/codemirror"); + Html.AddCssFileParts("~/css/codemirror"); +} + + + +@Html.TextArea(string.Empty, /* Name suffix */ + (string)ViewData.TemplateInfo.FormattedModelValue, /* Initial value */ + new { @class = "form-control", style = "max-width: initial;", rows = 20 } +) + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/ModelTree.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/ModelTree.cshtml new file mode 100644 index 0000000000..0563ad0b16 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/ModelTree.cshtml @@ -0,0 +1,141 @@ +@model TreeNode + +@using Telerik.Web.Mvc.UI; +@using Telerik.Web.Mvc.UI.Fluent; +@using SmartStore.Collections; +@using SmartStore.Services.Messages; + +@helper TokenSelector() +{ + + + @T("Admin.Common.ChooseToken") + +} + +@{Html.SmartStore().Window() + .Name("addtoken-window") + .Size(WindowSize.FlexSmall) + .Title(T("Admin.Common.ChooseToken")) + .Content( + @ + @(Html.Telerik().TreeView() + .Name("token-treeview") + .Items(tv => + { + AddItemsToModelTree(tv, Model); + }) + .ShowLines(true) + .ClientEvents(events => events.OnSelect("onNodeSelect")) + .DragAndDrop(false)) + ) + .FooterContent(@ + + + ) + .Render(); +} + +@functions { + public static void AddItemsToModelTree(TreeViewItemFactory tv, TreeNode root) + { + if (root != null) + { + foreach (var node in root.Children) + { + + List tokens = new List(); + var curNode = node; + while (!curNode.IsRoot) + { + tokens.Insert(0, curNode.Value.Name); + curNode = curNode.Parent; + } + + tv.Add() + .Text(node.Value.Name) + .Value(String.Join(".", tokens)) + //.HtmlAttributes(new { _data-leaf = node.IsLeaf.ToString() }) + .HtmlAttributes(new { isleaf = node.IsLeaf.ToString().ToLower() }) + .Items(tvc => + { + + if (!node.IsLeaf) + { + AddItemsToModelTree(tvc, node); + } + }); + } + } + } +} + +@if (Model == null || !Model.HasChildren) +{ +
                                        + @T("Admin.ContentManagement.MessageTemplates.NoModelTree") +
                                        + +} +else +{ + @TokenSelector() + + +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Picture.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Picture.cshtml index 4d4fb1b0f2..5bd494ff9a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Picture.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Picture.cshtml @@ -1,127 +1,67 @@ @model int? -@using SmartStore.Core; + @using SmartStore.Web.Framework.UI; @using SmartStore.Utilities; @functions { - private bool TransientUpload - { - get - { - if (ViewData.ContainsKey("transientUpload")) - { - var x = ViewData["transientUpload"].Convert(); - return x; - } - return false; - } - } - - private bool ValidatePicture - { - get - { - if (ViewData.ContainsKey("validate")) - { - var x = ViewData["validate"].Convert(); - return x; - } - return true; - } - } + private bool TransientUpload + { + get + { + if (ViewData.ContainsKey("transientUpload")) + return ViewData["transientUpload"].Convert(); + return false; + } + } + + private bool ValidatePicture + { + get + { + if (ViewData.ContainsKey("validate")) + return ViewData["validate"].Convert(); + return true; + } + } } @{ - var random = CommonHelper.GenerateRandomInteger(); - var clientId = "picture" + random; - var pictureService = EngineContext.Current.Resolve(); - int pictureId = Model.HasValue ? Model.Value : 0; + var random = CommonHelper.GenerateRandomInteger(); + var pictureService = this.CommonServices.PictureService; + var mediaSettings = this.CommonServices.Resolve(); + int pictureId = Model.HasValue ? Model.Value : 0; var picture = pictureService.GetPictureById(pictureId); - - Html.AddScriptParts("~/bundles/fileupload"); - Html.AddCssFileParts("~/css/fileupload"); } -
                                        - - @Html.HiddenFor(x => x, new { @class = "hidden"} ) - - - - - - - - @T("Common.Fileuploader.Upload") - - - - - - -
                                        - -
                                        -
                                        -
                                        - -
                                         
                                        -
                                        - +
                                        +
                                        + @Html.HiddenFor(x => x, new { @class = "hidden" }) + +
                                        + +
                                        + @(Html.SmartStore().FileUploader() + .UploadUrl(Url.Action("AsyncUpload", "Picture", new { isTransient = TransientUpload, validate = ValidatePicture, area = "Admin" })) + .HtmlAttribute("data-fallback-url", pictureService.GetFallbackUrl(mediaSettings.ProductThumbPictureSize)) + .AcceptedFileTypes("gif|jpe?g|png") + .ShowRemoveButton(picture != null) + .ShowRemoveButtonAfterUpload(true) + .OnUploadCompletedHandlerName("onUploadCompleted_" + random) + ) +
                                        - + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/RichEditor.SummerNote.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/RichEditor.SummerNote.cshtml deleted file mode 100644 index dce7c51a8a..0000000000 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/RichEditor.SummerNote.cshtml +++ /dev/null @@ -1,46 +0,0 @@ -@model String -@using SmartStore.Web.Framework.UI; -@{ - var availableLangs = new string[] { "de-DE", "en-US" }; - var lang = WorkContext.WorkingLanguage.LanguageCulture; - if (!availableLangs.Contains(lang)) - { - lang = "en-US"; - } - - Html.AddScriptParts(true, - "//cdnjs.cloudflare.com/ajax/libs/codemirror/3.20.0/codemirror.min.js", - "//cdnjs.cloudflare.com/ajax/libs/codemirror/3.20.0/mode/xml/xml.min.js", - "//cdnjs.cloudflare.com/ajax/libs/codemirror/2.36.0/formatting.min.js", - "~/Content/editors/summernote/summernote.js", - "~/Content/editors/summernote/globalinit.js"); - - if (lang != "en-US") - { - Html.AddScriptParts(true, "~/Content/editors/summernote/langs/summernote-{0}.js".FormatInvariant(lang)); - } - - Html.AppendCssFileParts(true, - "//cdnjs.cloudflare.com/ajax/libs/codemirror/3.20.0/codemirror.min.css", - "~/Content/editors/summernote/summernote.css"); -} - - - -@Html.TextArea(string.Empty, /* Name suffix */ - (string)ViewData.TemplateInfo.FormattedModelValue, /* Initial value */ - new { @class = "summernote-editor", data_upload_url = Url.Action("UploadImageAjax", "Media") } -) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/RichEditor.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/RichEditor.cshtml deleted file mode 100644 index 8dd130fe83..0000000000 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/RichEditor.cshtml +++ /dev/null @@ -1,65 +0,0 @@ -@model String -@using SmartStore.Web.Framework.UI; -@using SmartStore.Web.Framework.Theming; - -@functions { - private bool FullPage - { - get - { - if (ViewData.ContainsKey("fullPage")) - { - var x = ViewData["fullPage"].Convert(); - return x; - } - return false; - } - } -} - -@{ - var availableLangs = new string[] { "de", "en" }; - var lang = WorkContext.WorkingLanguage.UniqueSeoCode.EmptyNull().ToLower(); - if (!availableLangs.Contains(lang)) - { - lang = "en"; - } - - Html.AddScriptParts(true, "~/Content/editors/ckeditor/ckeditor.js"); - - var themeCssPath = Url.ThemeAwareContent("Content/theme.scss"); -} - - - -@Html.TextArea( - string.Empty, /* Name suffix */ - (string)ViewData.TemplateInfo.FormattedModelValue, /* Initial value */ - new { @class = "", style = "", data_fullpage = FullPage.ToString().ToLower(), data_upload_url = Url.Action("UploadImageAjax", "Media") } -) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/TokenSelector.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/TokenSelector.cshtml deleted file mode 100644 index 45af33e7e6..0000000000 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/TokenSelector.cshtml +++ /dev/null @@ -1,72 +0,0 @@ -@model TreeNode -@using SmartStore.Collections; - -@helper TokenSelector(TreeNode root) -{ - if (root.HasChildren) - { - - } -} - -@helper TokenList(TreeNode node) -{ - -} - -@TokenSelector(Model) - - \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/WidgetZone.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/WidgetZone.cshtml index 38e4a48472..c145e382ad 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/WidgetZone.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/WidgetZone.cshtml @@ -11,7 +11,8 @@ } } - + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/Layouts/_AdminBareLayout.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/Layouts/_AdminBareLayout.cshtml new file mode 100644 index 0000000000..13bee4e6d1 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/Layouts/_AdminBareLayout.cshtml @@ -0,0 +1,8 @@ +@{ + Layout = "_AdminLayout"; + Html.AddBodyCssClass("popup bare"); +} +@section navbar +{ +} +@RenderBody() diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/Layouts/_AdminLayout.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/Layouts/_AdminLayout.cshtml index 1f2923cfbc..06c60c05b1 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/Layouts/_AdminLayout.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/Layouts/_AdminLayout.cshtml @@ -27,8 +27,8 @@
                                        @if (ViewData["warning.panel.message"] != null) { -
                                        - +
                                        + @Html.Raw(ViewData["warning.panel.message"])
                                        } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/Layouts/_AdminRoot.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/Layouts/_AdminRoot.cshtml index 1c4c11580f..338a9b47f1 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/Layouts/_AdminRoot.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/Layouts/_AdminRoot.cshtml @@ -1,10 +1,9 @@ @using SmartStore; @using SmartStore.Core; @using SmartStore.Web.Framework.UI; +@using System.Web.Hosting; @{ - var currentUICulture = System.Threading.Thread.CurrentThread.CurrentUICulture; - // page title string adminPageTitle = ""; if (!string.IsNullOrWhiteSpace(ViewBag.Title)) @@ -13,101 +12,79 @@ } adminPageTitle += T("Admin.PageTitle").Text; + var adminJsRoot = "~/Administration/Scripts/"; + var vendorsRoot = "~/Content/vendors/"; + var contentRoot = "~/Content/"; + var jsRoot = "~/Scripts/"; + // add css assets - var telerikCssRootPath = "~/Content/2012.2.607/"; + var telerikCssRoot = "~/Administration/Content/telerik/css/2012.2.607/"; Html.AppendCssFileParts( - "~/Content/font-awesome.css", - telerikCssRootPath + "telerik.common.css", - "~/Content/jquery.pnotify.default.css", - "~/Content/jquery.pnotify.default.icons.css", - "~/Content/smartstore.entitypicker.css", - "~/Administration/Content/theme.less"); + contentRoot + "fontastic/fontastic.css", + vendorsRoot + "font-awesome/font-awesome.css", + telerikCssRoot + "telerik.common.css", + vendorsRoot + "pnotify/css/pnotify.css", + vendorsRoot + "pnotify/css/pnotify.mobile.css", + vendorsRoot + "pnotify/css/pnotify.buttons.css", + "~/Administration/Content/theme.scss"); // add js assets (Head) Html.AppendScriptParts(ResourceLocation.Head, - "~/Scripts/modernizr.js", - // Telerik doesn't like jQuery 1.9+, a shame! - "~/Administration/Scripts/jquery-1.8.3.js", - //"~/Scripts/jquery-2.1.1.js", - //"~/Scripts/jquery-migrate-1.2.1.js", - "~/Scripts/globalize/globalize.js", - "~/Scripts/smartstore.globalize.adapter.js"); - - Html.AddScriptParts(ResourceLocation.Head, true, - "~/Scripts/globalize/cultures/globalize.culture.{0}.js".FormatInvariant(currentUICulture.ToString())); + vendorsRoot + "modernizr/modernizr.js", + vendorsRoot + "jquery/jquery-3.2.1.js", + //vendorsRoot + "jquery/jquery-migrate-3.0.0.js", + adminJsRoot + "jquery-shims.js"); - // add js assets (Foot) - var bootstrapJsRoot = "~/Content/bootstrap/js/"; Html.AppendScriptParts(ResourceLocation.Foot, - // jQuery UI Core - "~/Scripts/jquery-ui/effect.js", - "~/Scripts/jquery-ui/effect-transfer.js", - "~/Scripts/jquery-ui/effect-shake.js", - "~/Scripts/jquery-ui/position.js", - // jQuery Validation - "~/Scripts/jquery.unobtrusive-ajax.js", - "~/Scripts/jquery.validate.js", - "~/Scripts/jquery.validate.unobtrusive.js", - // SmartStore system - "~/Scripts/smartstore.system.js", - "~/Scripts/underscore.js", - "~/Scripts/underscore.string.js", - "~/Scripts/underscore.mixins.js", - "~/Scripts/smartstore.jquery.utils.js", - "~/Scripts/jquery.ba-outside-events.js", - "~/Scripts/jquery.preload.js", - "~/Scripts/jquery.menu-aim.js", - "~/Scripts/smartstore.doAjax.js", - "~/Scripts/smartstore.entityPicker.js", - "~/Scripts/jquery.addeasing.js", - "~/Scripts/smartstore.eventbroker.js", - "~/Scripts/smartstore.hacks.js", - "~/Scripts/smartstore.common.js", - // Bootstrap - bootstrapJsRoot + "bootstrap-transition.js", - bootstrapJsRoot + "bootstrap-alert.js", - bootstrapJsRoot + "bootstrap-button.js", - bootstrapJsRoot + "bootstrap-collapse.js", - bootstrapJsRoot + "bootstrap-dropdown.js", - bootstrapJsRoot + "bootstrap-modal.js", - bootstrapJsRoot + "bootstrap-tooltip.js", - bootstrapJsRoot + "bootstrap-popover.js", - bootstrapJsRoot + "bootstrap-tab.js", - bootstrapJsRoot + "bootstrap-typeahead.js", - // Bootstrap custom - bootstrapJsRoot + "custom/bootstrap-datetimepicker.js", - // Shared UI - "~/Scripts/smartstore.placeholder.js", - "~/Scripts/select2.js", - "~/Scripts/smartstore.selectwrapper.js", - "~/Scripts/smartstore.throbber.js", - "~/Scripts/smartstore.navbar.js", - "~/Scripts/smartstore.thumbzoomer.js", - "~/Scripts/smartstore.column-equalizer.js", - "~/Scripts/smartstore.shrinkmenu.js", - "~/Scripts/smartstore.scrollbutton.js", - "~/Scripts/smartstore.tabselector.js", - "~/Scripts/jquery.pnotify.js", - "~/Scripts/jquery.scrollTo.js", - "~/Scripts/jquery.sortable.js", + // Vendors + vendorsRoot + "underscore/underscore.js", + vendorsRoot + "underscore/underscore.string.js", + vendorsRoot + "jquery/jquery.addeasing.js", + vendorsRoot + "jquery-ui/effect.js", + vendorsRoot + "jquery-ui/effect-transfer.js", + vendorsRoot + "jquery-ui/position.js", + vendorsRoot + "jquery/jquery.unobtrusive-ajax.js", + vendorsRoot + "jquery/jquery.validate.js", + vendorsRoot + "jquery/jquery.validate.unobtrusive.js", + vendorsRoot + "jquery/jquery.scrollTo.js", + vendorsRoot + "jquery/jquery.sortable.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", + 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.entitypicker.js", // Admin - "~/Administration/Scripts/admin.common.js", - "~/Administration/Scripts/admin.globalinit.js"); + adminJsRoot + "admin.common.js", + adminJsRoot + "admin.globalinit.js"); } - + @adminPageTitle - + + - - + @{ Html.RenderPartial("_ClientRes"); } @{ Html.RenderPartial("ConditionalComments"); } @Html.MetaAcceptLanguage() @@ -116,12 +93,22 @@ + @Html.CustomHead() + + + + @{ Html.RenderZone("head"); } + @{ Html.RenderZone("start"); } @RenderBody() + @{ Html.RenderZone("aftercontent"); } + @RenderSection("foot", required: false) @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.Telerik().ScriptRegistrar() @@ -129,6 +116,7 @@ .jQueryValidation(false) .Globalization(true) ) + @{ Html.RenderZone("end"); } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/Partials/AclSelector.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/Partials/AclSelector.cshtml new file mode 100644 index 0000000000..9abc2c7a00 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/Partials/AclSelector.cshtml @@ -0,0 +1,37 @@ +@model IAclSelector + +
                                        +
                                        +
                                        + @Html.SmartLabelFor(model => model.SubjectToAcl) +
                                        +
                                        + @Html.CheckBoxFor(model => model.SubjectToAcl, new { data_toggler_for = "#pnl-acl" }) + @Html.ValidationMessageFor(model => model.SubjectToAcl) +
                                        +
                                        +
                                        +
                                        + @Html.SmartLabelFor(model => model.AvailableCustomerRoles) +
                                        +
                                        + @if (Model.AvailableCustomerRoles != null && Model.AvailableCustomerRoles.Any()) + { + foreach (var role in Model.AvailableCustomerRoles) + { +
                                        + + +
                                        + } + } + else + { +
                                        + @T("Admin.Configuration.Acl.NoRolesDefined") +
                                        + } +
                                        +
                                        +
                                        \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/Partials/CsvConfiguration.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/Partials/CsvConfiguration.cshtml index 833d27649a..b616b32317 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/Partials/CsvConfiguration.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/Partials/CsvConfiguration.cshtml @@ -44,7 +44,7 @@ @Html.SmartLabelFor(x => x.Delimiter)
                                        @@ -53,7 +53,7 @@ @Html.SmartLabelFor(x => x.Quote) @@ -62,7 +62,7 @@ @Html.SmartLabelFor(x => x.Escape) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/Partials/Delete.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/Partials/Delete.cshtml index 67d399c6e1..bc276716bc 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/Partials/Delete.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/Partials/Delete.cshtml @@ -1,17 +1,15 @@ @model SmartStore.Web.Framework.Modelling.DeleteConfirmationModel @using (Html.BeginForm(Model.ActionName, Model.ControllerName, new { id = Model.Id }, FormMethod.Post, new { style = "margin:0;" })) { -
                                        -

                                        - @T("Admin.Common.DeleteConfirmation") -

                                        -
                                        - - -
                                        +
                                        + @T("Admin.Common.DeleteConfirmation") +
                                        +
                                        + +
                                        } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/Partials/Menu.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/Partials/Menu.cshtml index 2d9240b554..2a427bb74f 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/Partials/Menu.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/Partials/Menu.cshtml @@ -33,7 +33,7 @@ } } -
                                        + } - + + - - + - - -
                                        - @Html.DropDownListFor(model => model.CountryId, Model.AvailableCountries, T("Admin.Address.SelectCountry").Text) + @Html.DropDownList("CountryId", Model.AvailableCountries, + new + { + @class = "form-control country-input country-selector", + data_region_control_selector = "#" + Html.FieldIdFor(m => m.StateProvinceId), + data_states_ajax_url = Url.Action("GetStatesByCountryId", "Country"), + data_addEmptyStateIfRequired = "true" + }) @Html.ValidationMessageFor(model => model.CountryId) + +
                                        - @Html.TextBoxFor(x => x.Delimiter, new { style = "width: 30px;", maxlength = "2" }) + @Html.TextBoxFor(x => x.Delimiter, new { maxlength = "2" }) @Html.ValidationMessageFor(x => x.Delimiter)
                                        - @Html.TextBoxFor(x => x.Quote, new { style = "width: 30px;", maxlength = "2" }) + @Html.TextBoxFor(x => x.Quote, new { maxlength = "2" }) @Html.ValidationMessageFor(x => x.Quote)
                                        - @Html.TextBoxFor(x => x.Escape, new { style = "width: 30px;", maxlength = "2" }) + @Html.TextBoxFor(x => x.Escape, new { maxlength = "2" }) @Html.ValidationMessageFor(x => x.Escape)
                                        - } - else - { +{ + if (Model.Id > 0) + { +
                                        + @(Html.Telerik().Grid() + .Name("specificationattributeoptions-grid") + .DataKeys(x => + { + x.Add(y => y.Id).RouteKey("optionId"); + x.Add(y => y.SpecificationAttributeId).RouteKey("specificationAttributeId"); + }) + .Columns(columns => + { + columns.Bound(x => x.Name) + .Width("50%"); + //TODO display localized values here + columns.Bound(x => x.Alias); + columns.Bound(x => x.NumberValue) + .Width(180) + .Centered(); + columns.Bound(x => x.DisplayOrder) + .Width(180) + .Centered(); + columns.Command(commands => + { + commands.Custom("edit-spec-attr").Text(T("Common.Edit")); + commands.Delete().Localize(T); + }) + .HtmlAttributes(new { align = "right" }); + }) + .ToolBar(commands => commands.Template(SpecificationAttributeOptionGridCommands)) + .DataBinding(dataBinding => + dataBinding.Ajax().Select("OptionList", "SpecificationAttribute", new { specificationAttributeId = Model.Id }) + .Delete("OptionDelete", "SpecificationAttribute")) + .EnableCustomBinding(true)) +
                                        + } + else + { @T("Admin.Catalog.Attributes.SpecificationAttributes.Options.SaveBeforeEdit") } -} \ No newline at end of file +} + +@helper SpecificationAttributeOptionGridCommands(Grid grid) +{ + + +} + + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/SpecificationAttribute/_CreateOrUpdateOption.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/SpecificationAttribute/_CreateOrUpdateOption.cshtml index 8386f678cb..961f495190 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/SpecificationAttribute/_CreateOrUpdateOption.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/SpecificationAttribute/_CreateOrUpdateOption.cshtml @@ -12,6 +12,9 @@ @Html.SmartLabelFor(model => model.Locales[item].Name) + @*IMPORTANT: Do not delete, this hidden element contains the id to assign localized values to the corresponding language *@ + @Html.HiddenFor(model => model.Locales[item].LanguageId) + @Html.EditorFor(model => Model.Locales[item].Name) @Html.ValidationMessageFor(model => model.Locales[item].Name) @@ -25,11 +28,6 @@ @Html.ValidationMessageFor(model => model.Locales[item].Alias) - - - @Html.HiddenFor(model => model.Locales[item].LanguageId) - - , @ diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Store/Create.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Store/Create.cshtml index 4779db253b..8cc60acc6a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Store/Create.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Store/Create.cshtml @@ -1,6 +1,5 @@ @model StoreModel @{ - //page title ViewBag.Title = T("Admin.Configuration.Stores.AddNew").Text; } @using (Html.BeginForm()) @@ -10,8 +9,13 @@ @T("Admin.Configuration.Stores.AddNew") @Html.ActionLink("(" + T("Admin.Configuration.Stores.BackToList") + ")", "List")
                                        - - + +
                                        diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Store/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Store/Edit.cshtml index 1ce7392c64..b160c23138 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Store/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Store/Edit.cshtml @@ -1,6 +1,5 @@ @model StoreModel @{ - //page title ViewBag.Title = T("Admin.Configuration.Stores.EditStoreDetails").Text; } @@ -11,9 +10,17 @@ @T("Admin.Configuration.Stores.EditStoreDetails") - @Model.Name @Html.ActionLink("(" + T("Admin.Configuration.Stores.BackToList") + ")", "List")
                                        - - - + + +
                                        diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Store/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Store/List.cshtml index b3832779c4..1c320f37b8 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Store/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Store/List.cshtml @@ -8,37 +8,38 @@ @T("Admin.Common.Stores") -
                                        - - - -
                                        - @(Html.Telerik().Grid() - .Name("stores-grid") - .Columns(columns => - { - columns.Bound(x => x.Name) - .Template(x => Html.ActionLink(x.Name, "Edit", new { id = x.Id })) - .ClientTemplate("\"><#= Name #>"); - columns.Bound(x => x.Url) - .ClientTemplate("\" target=\"_blank\"><#= Url #>"); - columns.Bound(x => x.Hosts) - .Encoded(false); - columns.Bound(x => x.PrimaryStoreCurrencyName); - columns.Bound(x => x.PrimaryExchangeRateCurrencyName); - columns.Bound(x => x.ContentDeliveryNetwork) - .ClientTemplate("\" target=\"_blank\"><#= ContentDeliveryNetwork #>"); - columns.Bound(x => x.SslEnabled) - .Template(item => @Html.SymbolForBool(item.SslEnabled)) - .ClientTemplate(@Html.SymbolForBool("SslEnabled")) - .Centered() - .Width(80); - columns.Bound(x => x.DisplayOrder) - .Centered(); - }) - .DataBinding(dataBinding => dataBinding.Ajax().Select("List", "Store")) - .EnableCustomBinding(true)) -
                                        + +
                                        + @(Html.Telerik().Grid() + .Name("stores-grid") + .Columns(columns => + { + columns.Bound(x => x.Name) + .Template(x => Html.ActionLink(x.Name, "Edit", new { id = x.Id })) + .ClientTemplate("\"><#= Name #>"); + columns.Bound(x => x.Url) + .ClientTemplate("\" target=\"_blank\"><#= Url #>"); + columns.Bound(x => x.Hosts) + .Encoded(false); + columns.Bound(x => x.PrimaryStoreCurrencyName); + columns.Bound(x => x.PrimaryExchangeRateCurrencyName); + columns.Bound(x => x.ContentDeliveryNetwork) + .ClientTemplate("\" target=\"_blank\"><#= ContentDeliveryNetwork #>"); + columns.Bound(x => x.SslEnabled) + .Template(item => @Html.SymbolForBool(item.SslEnabled)) + .ClientTemplate(@Html.SymbolForBool("SslEnabled")) + .Centered() + .Width(80); + columns.Bound(x => x.DisplayOrder) + .Centered(); + }) + .DataBinding(dataBinding => dataBinding.Ajax().Select("List", "Store")) + .EnableCustomBinding(true)) +
                                        + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Store/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Store/_CreateOrUpdate.cshtml index 4b3855f9ab..545c816618 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Store/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Store/_CreateOrUpdate.cshtml @@ -3,24 +3,6 @@ @Html.ValidationSummary(true) @Html.HiddenFor(model => model.Id) - - @@ -90,7 +72,7 @@ @Html.SmartLabelFor(model => model.SslEnabled) @@ -128,7 +110,7 @@ @Html.SmartLabelFor(model => model.Id) } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Tax/Categories.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Tax/Categories.cshtml index 49c9571567..8fdd5069ad 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Tax/Categories.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Tax/Categories.cshtml @@ -2,7 +2,6 @@ @using Telerik.Web.Mvc.UI; @using System.Linq; @{ - //page title ViewBag.Title = T("Admin.Configuration.Tax.Categories").Text; }
                                        @@ -10,48 +9,44 @@ @T("Admin.Configuration.Tax.Categories")
                                        -
                                        @@ -54,7 +36,7 @@ @Html.SmartLabelFor(model => model.Hosts) - @Html.TextBoxFor(model => model.Hosts, new { @class = "input-large" }) + @Html.TextBoxFor(model => model.Hosts) @Html.ValidationMessageFor(model => model.Hosts)
                                        - @Html.EditorFor(model => model.SslEnabled) + @Html.CheckBoxFor(model => model.SslEnabled, new { data_toggler_for = "#pnlSecureUrl" }) @Html.ValidationMessageFor(model => model.SslEnabled)
                                        - @Model.Id + @Html.TextBoxFor(model => model.Id, new { @readonly = "readonly", @class = "form-control-plaintext" })
                                        - - - -
                                        - @(Html.Telerik().Grid(Model.Data) - .Name("taxcategory-grid") - .DataKeys(x => - { - x.Add(y => y.Id).RouteKey("Id"); - }) - .Columns(columns => - { - columns.Bound(x => x.Name); - columns.Bound(x => x.DisplayOrder) - .Centered() - .Width(120); - columns.Command(commands => - { - commands.Edit().Localize(T); - commands.Delete().Localize(T); - }).Width(180); +
                                        + @(Html.Telerik().Grid(Model.Data) + .Name("taxcategory-grid") + .DataKeys(x => + { + x.Add(y => y.Id).RouteKey("Id"); + }) + .Columns(columns => + { + columns.Bound(x => x.Name) + .Width("70%"); + columns.Bound(x => x.DisplayOrder) + .Centered() + .Width("10%"); + columns.Command(commands => + { + commands.Edit().Localize(T); + commands.Delete().Localize(T); + }) + .HtmlAttributes(new { align = "right", @class= "omega" }) + .Width("20%"); + }) + .DataBinding(dataBinding => + { + dataBinding.Ajax().Select("Categories", "Tax") + .Update("CategoryUpdate", "Tax") + .Delete("CategoryDelete", "Tax") + .Insert("CategoryAdd", "Tax"); + }) + .ToolBar(x => x.Insert()) + .Editable(x => { x.Mode(GridEditMode.InLine); }) + .ClientEvents(x => x.OnError("grid_onError")) + .EnableCustomBinding(true)) +
                                        - }) - .ToolBar(x => x.Insert()) - .Editable(x => - { - x.Mode(GridEditMode.InLine); - }) - .DataBinding(dataBinding => - { - dataBinding.Ajax().Select("Categories", "Tax") - .Update("CategoryUpdate", "Tax") - .Delete("CategoryDelete", "Tax") - .Insert("CategoryAdd", "Tax"); - }) - .ClientEvents(x => x.OnError("grid_onError")) - .EnableCustomBinding(true)) - -
                                        + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Tax/Providers.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Tax/Providers.cshtml index f531a9112d..a4384f4ddb 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Tax/Providers.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Tax/Providers.cshtml @@ -1,6 +1,5 @@ @model IEnumerable @{ - //page title ViewBag.Title = T("Admin.Configuration.Tax.Providers").Text; }
                                        @@ -8,21 +7,19 @@ @T("Admin.Configuration.Tax.Providers")
                                        - - - - -
                                        - @Html.ProviderList(Model, - @ - @if (item.IsPrimaryTaxProvider) - { - - @T("Admin.Configuration.Tax.Providers.Fields.IsPrimaryTaxProvider") - } - else - { - @T("Admin.Configuration.Tax.Providers.Fields.MarkAsPrimaryProvider") - } - ) -
                                        + + + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Theme/Configure.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Theme/Configure.cshtml index 24a52db8c4..58dc521a0b 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Theme/Configure.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Theme/Configure.cshtml @@ -8,8 +8,8 @@ @using (Html.BeginForm()) { -
                                        - +
                                        +
                                        @@ -209,8 +194,8 @@ diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Topic/Create.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Topic/Create.cshtml index d9c69ff536..44b81e80b7 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Topic/Create.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Topic/Create.cshtml @@ -1,6 +1,5 @@ @model TopicModel @{ - //page title ViewBag.Title = T("Admin.ContentManagement.Topics.AddNew").Text; } @using (Html.BeginForm()) @@ -10,8 +9,13 @@ @T("Admin.ContentManagement.Topics.AddNew") @Html.ActionLink("(" + T("Admin.ContentManagement.Topics.BackToList") + ")", "List")
                                        - - + +
                                        diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Topic/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Topic/Edit.cshtml index f27ddef63d..3522053ec2 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Topic/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Topic/Edit.cshtml @@ -1,6 +1,5 @@ @model TopicModel @{ - //page title ViewBag.Title = T("Admin.ContentManagement.Topics.EditTopicDetails").Text; } @using (Html.BeginForm()) @@ -10,9 +9,17 @@ @T("Admin.ContentManagement.Topics.EditTopicDetails") - @Model.SystemName @Html.ActionLink("(" + T("Admin.ContentManagement.Topics.BackToList") + ")", "List")
                                        - - - + + +
                                        diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Topic/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Topic/List.cshtml index 18fda90102..55c28da1db 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Topic/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Topic/List.cshtml @@ -11,76 +11,63 @@ @if (Model.AvailableStores.Count > 1) { -
                                        @Html.SmartLabelFor(model => model.StoreId) @@ -29,24 +29,24 @@
                                        @{ Html.RenderWidget("admin_button_toolbar_before"); } - - - + - @T("Common.Import") + @T("Common.Import") - + - @T("Common.Export") + @T("Common.Export") - @T("Admin.Common.Reset") + @T("Admin.Common.Reset") @{ Html.RenderWidget("admin_button_toolbar_after"); } @@ -55,8 +55,8 @@ if (parsingError.HasValue()) { -
                                        -

                                        @T("Admin.Configuration.Themes.Validation.ErrorReportTitle")

                                        +
                                        +

                                        @T("Admin.Configuration.Themes.Validation.ErrorReportTitle")

                                        @parsingError
                                        @T("Admin.Configuration.Themes.Validation.RestorePrevValues") @@ -75,23 +75,24 @@ .Title(T("Admin.Configuration.Themes.ImportVars")) .Content( @ - @using (Html.BeginForm("ImportVariables", "Theme", new { theme = Model.ThemeName, storeId = Model.StoreId }, FormMethod.Post, new { enctype = "multipart/form-data" })) - { -
                                        +
                                        +

                                        @T("Admin.Configuration.Themes.ImportVars.Note") -

                                        +

                                        @T("Admin.Configuration.Themes.ImportVars.XmlFile"):
                                        - } +
                                        ) .FooterContent( @ - - + + ) - .Modal(true) - .Visible(false) .Render(); } @@ -101,23 +102,25 @@ .Title(T("Admin.Configuration.Themes.ExportVars")) .Content( @ - @using (Html.BeginForm("ExportVariables", "Theme", new { theme = Model.ThemeName, storeId = Model.StoreId }, FormMethod.Post)) - { -
                                        -
                                        - +
                                        +
                                        @Html.SmartLabelFor(model => model.StoreId) @@ -48,11 +48,13 @@
                                        - @Html.SmartStore().TabStrip().Name("themes-tab").Items(x => -{ - x.Add().Text(T("Admin.Configuration.Themes")).Content(ThemesTab()).Selected(true); - x.Add().Text(T("Admin.Common.Settings")).Content(ThemeSettingsTab()); -}) + @Html.SmartStore().TabStrip().Name("themes-tab").Style(TabsStyle.Material).Items(x => + { + x.Add().Text(T("Admin.Configuration.Themes")).Content(ThemesTab()).Selected(true); + x.Add().Text(T("Admin.Common.Settings")).Content(ThemeSettingsTab()); + + EngineContext.Current.Resolve().Publish(new TabStripCreated(x, "themes-config", this.Html, this.Model)); + }) } @helper ThemesTab() @@ -64,9 +66,6 @@ - @Html.HiddenFor(x => x.DefaultTheme) @Html.ValidationSummary(false) @@ -189,7 +174,7 @@ @Html.SmartLabelFor(model => model.AllowCustomerToSelectTheme)
                                        - @Html.EditorFor(model => model.AllowCustomerToSelectTheme) + @Html.CheckBoxFor(model => model.AllowCustomerToSelectTheme, new { data_toggler_for = "#pnlSaveThemeChoiceInCookie" }) @Html.ValidationMessageFor(model => model.AllowCustomerToSelectTheme)
                                        -
                                        - +
                                        +

                                        @T("Admin.Configuration.Themes.Options.Title")

                                        @T("Admin.Configuration.Themes.Options.Info")

                                        @@ -240,7 +225,7 @@
                                        - @T("Admin.Configuration.Themes.ClearAssetCache") + @T("Admin.Configuration.Themes.ClearAssetCache")
                                        - - - - - - - - -
                                        - @Html.SmartLabelFor(model => model.SearchStoreId) - - @Html.DropDownList("SearchStoreId", Model.AvailableStores, T("Admin.Common.All")) -
                                        -   - - -
                                        -} +
                                        +
                                        + @Html.SmartLabelFor(model => model.SearchStoreId) + @Html.DropDownList("SearchStoreId", Model.AvailableStores, T("Admin.Common.All")) +
                                        -

                                        +
                                        + + +
                                        +
                                        +} - - - - -
                                        - @(Html.Telerik().Grid() - .Name("topics-grid") - .Columns(columns => - { - columns.Bound(x => x.SystemName) - .Width(280) - .Template(x => Html.ActionLink(x.SystemName, "Edit", new { id = x.Id })) - .ClientTemplate("<#= SystemName #>"); - columns.Bound(x => x.Title); - columns.Bound(x => x.IsPasswordProtected) - .Template(item => @Html.SymbolForBool(item.IsPasswordProtected)) - .ClientTemplate(@Html.SymbolForBool("IsPasswordProtected")) - .Centered(); - columns.Bound(x => x.IncludeInSitemap) - .Template(item => @Html.SymbolForBool(item.IncludeInSitemap)) - .ClientTemplate(@Html.SymbolForBool("IncludeInSitemap")) - .Centered(); - columns.Bound(x => x.RenderAsWidget) - .Template(item => @Html.SymbolForBool(item.RenderAsWidget)) - .ClientTemplate(@Html.SymbolForBool("RenderAsWidget")) - .Centered(); - columns.Bound(x => x.WidgetZone); - columns.Bound(x => x.LimitedToStores) - .Template(item => @Html.SymbolForBool(item.LimitedToStores)) - .ClientTemplate(@Html.SymbolForBool("LimitedToStores")) - .Hidden(Model.AvailableStores.Count <= 1) - .Centered(); - columns.Bound(x => x.Priority) - .Centered(); - }) - .DataBinding(dataBinding => dataBinding.Ajax().Select("List", "Topic")) - .ClientEvents(events => events.OnDataBinding("onDataBinding")) - .EnableCustomBinding(true)) -
                                        +
                                        + @(Html.Telerik().Grid() + .Name("topics-grid") + .Columns(columns => + { + columns.Bound(x => x.SystemName) + .Width(280) + .Template(x => Html.ActionLink(x.SystemName, "Edit", new { id = x.Id })) + .ClientTemplate("<#= SystemName #>"); + columns.Bound(x => x.Title); + columns.Bound(x => x.IsPasswordProtected) + .Template(item => @Html.SymbolForBool(item.IsPasswordProtected)) + .ClientTemplate(@Html.SymbolForBool("IsPasswordProtected")) + .Centered(); + columns.Bound(x => x.IncludeInSitemap) + .Template(item => @Html.SymbolForBool(item.IncludeInSitemap)) + .ClientTemplate(@Html.SymbolForBool("IncludeInSitemap")) + .Centered(); + columns.Bound(x => x.RenderAsWidget) + .Template(item => @Html.SymbolForBool(item.RenderAsWidget)) + .ClientTemplate(@Html.SymbolForBool("RenderAsWidget")) + .Centered(); + columns.Bound(x => x.WidgetZone); + columns.Bound(x => x.LimitedToStores) + .Template(item => @Html.SymbolForBool(item.LimitedToStores)) + .ClientTemplate(@Html.SymbolForBool("LimitedToStores")) + .Hidden(Model.AvailableStores.Count <= 1) + .Centered(); + columns.Bound(x => x.Priority) + .Centered(); + }) + .DataBinding(dataBinding => dataBinding.Ajax().Select("List", "Topic")) + .ClientEvents(events => events.OnDataBinding("onDataBinding")) + .EnableCustomBinding(true)) +
                                        @Html.HiddenFor(model => model.Id) -@Html.SmartStore().TabStrip().Name("topic-edit").Items(x => +@Html.SmartStore().TabStrip().Name("topic-edit").Style(TabsStyle.Material).Items(x => { x.Add().Text(T("Admin.ContentManagement.Topics.Info").Text).Content(TabInfo()).Selected(true); x.Add().Text(T("Admin.Common.SEO").Text).Content(TabSeo()); @@ -72,15 +12,16 @@ //generate an event EngineContext.Current.Resolve().Publish(new TabStripCreated(x, "topic-edit", this.Html, this.Model)); }) + @helper TabInfo() - { +{ @@ -89,7 +30,7 @@ @Html.SmartLabelFor(model => model.IsPasswordProtected) @@ -118,7 +59,9 @@ @Html.SmartLabelFor(model => model.Url) } @@ -132,235 +75,187 @@ @Html.SmartLabelFor(model => model.RenderAsWidget) - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                                        @Html.SmartLabelFor(model => model.SystemName) - @Html.TextBoxFor(model => model.SystemName, Model.IsSystemTopic ? new { disabled = "disabled" } : null ) + @Html.TextBoxFor(model => model.SystemName, Model.IsSystemTopic ? new { @readonly = "readonly" } : null ) @Html.ValidationMessageFor(model => model.SystemName)
                                        - @Html.EditorFor(model => model.IsPasswordProtected) + @Html.CheckBoxFor(model => model.IsPasswordProtected, new { data_toggler_for = "#pnlPasswordEnabled" }) @Html.ValidationMessageFor(model => model.IsPasswordProtected)
                                        - @Model.Url +
                                        - @Html.EditorFor(x => x.RenderAsWidget) + @Html.CheckBoxFor(model => model.RenderAsWidget, new { data_toggler_for = "#pnlRenderAsWidget" }) @Html.ValidationMessageFor(model => model.RenderAsWidget)
                                        - @Html.SmartLabelFor(model => model.WidgetZone) - - @Html.EditorFor(model => model.WidgetZone, "WidgetZone") - @Html.ValidationMessageFor(model => model.WidgetZone) -
                                        - @Html.SmartLabelFor(model => model.Priority) - - @Html.EditorFor(x => x.Priority) - @Html.ValidationMessageFor(model => model.Priority) -
                                        - @Html.SmartLabelFor(model => model.WidgetWrapContent) - - @Html.EditorFor(x => x.WidgetWrapContent) - @Html.ValidationMessageFor(model => model.WidgetWrapContent) -
                                        - @Html.SmartLabelFor(model => model.WidgetShowTitle) - - @Html.EditorFor(x => x.WidgetShowTitle) - @Html.ValidationMessageFor(model => model.WidgetShowTitle) -
                                        - @Html.SmartLabelFor(model => model.TitleTag) - - @Html.DropDownListFor(model => model.TitleTag, Model.AvailableTitleTags, new { @class = "autowidth", data_select_min_results_for_search = 100 }) - @Html.ValidationMessageFor(model => model.TitleTag) -
                                        - @Html.SmartLabelFor(model => model.WidgetBordered) - - @Html.EditorFor(x => x.WidgetBordered) - @Html.ValidationMessageFor(model => model.WidgetBordered) -
                                        + @Html.SmartLabelFor(model => model.WidgetZone) + + @Html.EditorFor(model => model.WidgetZone, "WidgetZone") + @Html.ValidationMessageFor(model => model.WidgetZone) +
                                        + @Html.SmartLabelFor(model => model.Priority) + + @Html.EditorFor(x => x.Priority) + @Html.ValidationMessageFor(model => model.Priority) +
                                        + @Html.SmartLabelFor(model => model.WidgetWrapContent) + + @Html.CheckBoxFor(model => model.WidgetWrapContent, new { data_toggler_for = "#pnlWidgetWrapContent" }) + @Html.ValidationMessageFor(model => model.WidgetWrapContent) +
                                        + @Html.SmartLabelFor(model => model.WidgetShowTitle) + + @Html.CheckBoxFor(model => model.WidgetShowTitle, new { data_toggler_for = "#pnlTitleTag" }) + @Html.ValidationMessageFor(model => model.WidgetShowTitle) +
                                        + @Html.SmartLabelFor(model => model.TitleTag) + + @Html.DropDownListFor(model => model.TitleTag, Model.AvailableTitleTags, new { @class = "autowidth", data_select_min_results_for_search = 100 }) + @Html.ValidationMessageFor(model => model.TitleTag) +
                                        + @Html.SmartLabelFor(model => model.WidgetBordered) + + @Html.EditorFor(x => x.WidgetBordered) + @Html.ValidationMessageFor(model => model.WidgetBordered) +
                                        -
                                        - @(Html.LocalizedEditor("topic-info-localized", - @ - - - - - - - - - - - -
                                        - @Html.SmartLabelFor(model => model.Locales[item].Title) - - @Html.EditorFor(model => Model.Locales[item].Title) - @Html.ValidationMessageFor(model => model.Locales[item].Title) -
                                        - @Html.SmartLabelFor(model => model.Locales[item].Body) - - @Html.EditorFor(model => model.Locales[item].Body, Html.RichEditorFlavor(), new { ForceRootBlock = false }) - @Html.ValidationMessageFor(model => model.Locales[item].Body) -
                                        - @Html.HiddenFor(model => model.Locales[item].LanguageId) -
                                        - , - @ - - - - - - - - -
                                        - @Html.SmartLabelFor(model => model.Title) - - @Html.EditorFor(model => model.Title) - @Html.ValidationMessageFor(model => model.Title) -
                                        - @Html.SmartLabelFor(model => model.Body) - - @Html.EditorFor(x => x.Body, Html.RichEditorFlavor(), new { ForceRootBlock = false }) - @Html.ValidationMessageFor(model => model.Body) -
                                        - )) + @ + + + + + + + + +
                                        + @Html.SmartLabelFor(model => model.Locales[item].Title) + + @*IMPORTANT: Do not delete, this hidden element contains the id to assign localized values to the corresponding language *@ + @Html.HiddenFor(model => model.Locales[item].LanguageId, new { id = Html.FieldIdFor(model => model.Locales[item].LanguageId) + "_1" }) + + @Html.EditorFor(model => Model.Locales[item].Title) + @Html.ValidationMessageFor(model => model.Locales[item].Title) +
                                        + @Html.SmartLabelFor(model => model.Locales[item].Body) + + @Html.EditorFor(model => model.Locales[item].Body, "Html", new { ForceRootBlock = false }) + @Html.ValidationMessageFor(model => model.Locales[item].Body) +
                                        + , + @ + + + + + + + + +
                                        + @Html.SmartLabelFor(model => model.Title) + + @Html.EditorFor(model => model.Title) + @Html.ValidationMessageFor(model => model.Title) +
                                        + @Html.SmartLabelFor(model => model.Body) + + @Html.EditorFor(x => x.Body, "Html", new { ForceRootBlock = false }) + @Html.ValidationMessageFor(model => model.Body) +
                                        + )) } + @helper TabSeo() - { +{ @(Html.LocalizedEditor("topic-seo-localized", - @ - - - - - - - - - - - - - - - -
                                        - @Html.SmartLabelFor(model => model.Locales[item].MetaKeywords) - - @Html.EditorFor(model => model.Locales[item].MetaKeywords) - @Html.ValidationMessageFor(model => model.Locales[item].MetaKeywords) -
                                        - @Html.SmartLabelFor(model => model.Locales[item].MetaDescription) - - @Html.TextAreaFor(model => model.Locales[item].MetaDescription) - @Html.ValidationMessageFor(model => model.Locales[item].MetaDescription) -
                                        - @Html.SmartLabelFor(model => model.Locales[item].MetaTitle) - - @Html.TextAreaFor(model => model.Locales[item].MetaTitle) - @Html.ValidationMessageFor(model => model.Locales[item].MetaTitle) -
                                        - @Html.HiddenFor(model => model.Locales[item].LanguageId) -
                                        - , - @ - - - - - - - - - - - - -
                                        - @Html.SmartLabelFor(model => model.MetaKeywords) - - @Html.EditorFor(x => x.MetaKeywords) - @Html.ValidationMessageFor(model => model.MetaKeywords) -
                                        - @Html.SmartLabelFor(model => model.MetaDescription) - - @Html.TextAreaFor(x => x.MetaDescription) - @Html.ValidationMessageFor(model => model.MetaDescription) -
                                        - @Html.SmartLabelFor(model => model.MetaTitle) - - @Html.TextAreaFor(x => x.MetaTitle) - @Html.ValidationMessageFor(model => model.MetaTitle) -
                                        )) + @ + + + + + + + + + + + + +
                                        + @Html.SmartLabelFor(model => model.Locales[item].MetaKeywords) + + @*IMPORTANT: Do not delete, this hidden element contains the id to assign localized values to the corresponding language *@ + @Html.HiddenFor(model => model.Locales[item].LanguageId, new { id = Html.FieldIdFor(model => model.Locales[item].LanguageId) + "_2" }) + + @Html.EditorFor(model => model.Locales[item].MetaKeywords) + @Html.ValidationMessageFor(model => model.Locales[item].MetaKeywords) +
                                        + @Html.SmartLabelFor(model => model.Locales[item].MetaDescription) + + @Html.TextAreaFor(model => model.Locales[item].MetaDescription) + @Html.ValidationMessageFor(model => model.Locales[item].MetaDescription) +
                                        + @Html.SmartLabelFor(model => model.Locales[item].MetaTitle) + + @Html.TextAreaFor(model => model.Locales[item].MetaTitle) + @Html.ValidationMessageFor(model => model.Locales[item].MetaTitle) +
                                        + , + @ + + + + + + + + + + + + +
                                        + @Html.SmartLabelFor(model => model.MetaKeywords) + + @Html.EditorFor(x => x.MetaKeywords) + @Html.ValidationMessageFor(model => model.MetaKeywords) +
                                        + @Html.SmartLabelFor(model => model.MetaDescription) + + @Html.TextAreaFor(x => x.MetaDescription) + @Html.ValidationMessageFor(model => model.MetaDescription) +
                                        + @Html.SmartLabelFor(model => model.MetaTitle) + + @Html.TextAreaFor(x => x.MetaTitle) + @Html.ValidationMessageFor(model => model.MetaTitle) +
                                        )) } + @helper TabStores() { - - - - - - - - - - -
                                        - @Html.SmartLabelFor(model => model.LimitedToStores) - - @Html.EditorFor(model => model.LimitedToStores) - @Html.ValidationMessageFor(model => model.LimitedToStores) -
                                        - @Html.SmartLabelFor(model => model.AvailableStores) - - @if (Model.AvailableStores != null && Model.AvailableStores.Count > 0) - { - foreach (var store in Model.AvailableStores) - { - - } - } - else - { -
                                        @T("Admin.Configuration.Stores.NoStoresDefined")
                                        - }
                                        + @Html.Partial("StoreSelector", Model) } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/Edit.cshtml index 21b18e2ac1..f707cd4689 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/Edit.cshtml @@ -11,12 +11,20 @@ @title - @Model.Slug @Html.ActionLink("(" + T("Admin.Common.BackToList") + ")", "List")
                                        - +  @T("Admin.Common.Entity") - - - + + +
                                        diff --git a/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/List.cshtml index fa2a560a7d..871078e88e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/List.cshtml @@ -14,109 +14,81 @@
                                        - - - - - - - - - - - - - - - - - - - - - - - - - -
                                        - @Html.SmartLabelFor(model => model.SeName) - - @Html.EditorFor(model => Model.SeName) -
                                        - @Html.SmartLabelFor(model => model.EntityName) - - @Html.EditorFor(model => Model.EntityName) -
                                        - @Html.SmartLabelFor(model => model.EntityId) - - @Html.EditorFor(model => Model.EntityId) -
                                        - @Html.SmartLabelFor(model => model.IsActive) - - @Html.EditorFor(model => Model.IsActive) -
                                        - @Html.SmartLabelFor(x => x.LanguageId) - - @Html.DropDownListFor(x => x.LanguageId, Model.AvailableLanguages, T("Common.Unspecified")) -
                                        -   - - -
                                        - -

                                        - - - - - -
                                        - @(Html.Telerik().Grid() - .Name("urlrecord-grid") - .Columns(columns => - { - columns.Bound(x => x.Id) - .ClientTemplate("") - .Title("") - .Width(50) - .HtmlAttributes(new { style = "text-align:center" }) - .HeaderHtmlAttributes(new { style = "text-align:center" }); - columns.Bound(x => x.Id) - .Width(100) - .Centered(); - columns.Bound(x => x.Slug) - .ClientTemplate("<#= Slug #>"); - columns.Bound(x => x.SlugsPerEntity) - .Centered() - .Width(180) - .ClientTemplate("<#= SlugsPerEntity #>"); - columns.Bound(x => x.EntityName) - .Width(200); - columns.Bound(x => x.EntityId) - .Width(140) - .Centered() - .ClientTemplate("<#= EntityId #>"); - columns.Bound(x => x.IsActive) - .Template(item => @Html.SymbolForBool(item.IsActive)) - .ClientTemplate(@Html.SymbolForBool("IsActive")) - .Width(100) - .Centered(); - columns.Bound(x => x.Language) - .Width(200); - }) - .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) - .DataBinding(dataBinding => dataBinding.Ajax().Select("List", "UrlRecord")) - .ClientEvents(events => events.OnDataBinding("onDataBinding").OnDataBound("onDataBound").OnRowDataBound("onRowDataBound")) - .EnableCustomBinding(true)) -
                                        +
                                        +
                                        + @Html.SmartLabelFor(model => model.SeName) + @Html.TextBoxFor(model => Model.SeName, new { @class = "form-control" }) +
                                        +
                                        + @Html.SmartLabelFor(model => model.EntityName) + @Html.TextBoxFor(model => model.EntityName, new { @class = "form-control" }) +
                                        +
                                        + @Html.SmartLabelFor(model => model.EntityId) + @Html.TextBoxFor(model => model.EntityId, new { @class = "form-control" }) +
                                        +
                                        + @Html.SmartLabelFor(model => model.IsActive) + @Html.TextBoxFor(model => model.IsActive, new { @class = "form-control" }) +
                                        +
                                        + @Html.SmartLabelFor(model => model.LanguageId) + @Html.DropDownListFor(x => x.LanguageId, Model.AvailableLanguages, T("Common.Unspecified")) +
                                        + +
                                        + + +
                                        +
                                        + +
                                        + @(Html.Telerik().Grid() + .Name("urlrecord-grid") + .Columns(columns => + { + columns.Bound(x => x.Id) + .ClientTemplate("") + .Title("") + .Width(50) + .HtmlAttributes(new { style = "text-align:center" }) + .HeaderHtmlAttributes(new { style = "text-align:center" }); + columns.Bound(x => x.Id) + .Width(100) + .Centered(); + columns.Bound(x => x.Slug) + .ClientTemplate("<#= Slug #>"); + columns.Bound(x => x.SlugsPerEntity) + .Centered() + .Width(180) + .ClientTemplate("<#= SlugsPerEntity #>"); + columns.Bound(x => x.EntityName) + .Width(200); + columns.Bound(x => x.EntityId) + .Width(140) + .Centered() + .ClientTemplate("<#= EntityId #>"); + columns.Bound(x => x.IsActive) + .Template(item => @Html.SymbolForBool(item.IsActive)) + .ClientTemplate(@Html.SymbolForBool("IsActive")) + .Width(100) + .Centered(); + columns.Bound(x => x.Language) + .Width(200); + }) + .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) + .DataBinding(dataBinding => dataBinding.Ajax().Select("List", "UrlRecord")) + .ClientEvents(events => events.OnDataBinding("onDataBinding").OnDataBound("onDataBound").OnRowDataBound("onRowDataBound")) + .EnableCustomBinding(true)) +
                                        } - -

                                        - diff --git a/src/Presentation/SmartStore.Web/Content/editors/ckeditor/plugins/wsc/dialogs/tmpFrameset.html b/src/Presentation/SmartStore.Web/Content/editors/ckeditor/plugins/wsc/dialogs/tmpFrameset.html deleted file mode 100644 index c2d82aa402..0000000000 --- a/src/Presentation/SmartStore.Web/Content/editors/ckeditor/plugins/wsc/dialogs/tmpFrameset.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/src/Presentation/SmartStore.Web/Content/editors/ckeditor/plugins/wsc/dialogs/wsc.css b/src/Presentation/SmartStore.Web/Content/editors/ckeditor/plugins/wsc/dialogs/wsc.css deleted file mode 100644 index 496d731250..0000000000 --- a/src/Presentation/SmartStore.Web/Content/editors/ckeditor/plugins/wsc/dialogs/wsc.css +++ /dev/null @@ -1,82 +0,0 @@ -/* -Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved. -For licensing, see LICENSE.html or http://ckeditor.com/license -*/ - -html, body -{ - background-color: transparent; - margin: 0px; - padding: 0px; -} - -body -{ - padding: 10px; -} - -body, td, input, select, textarea -{ - font-size: 11px; - font-family: 'Microsoft Sans Serif' , Arial, Helvetica, Verdana; -} - -.midtext -{ - padding:0px; - margin:10px; -} - -.midtext p -{ - padding:0px; - margin:10px; -} - -.Button -{ - border: #737357 1px solid; - color: #3b3b1f; - background-color: #c7c78f; -} - -.PopupTabArea -{ - color: #737357; - background-color: #e3e3c7; -} - -.PopupTitleBorder -{ - border-bottom: #d5d59d 1px solid; -} -.PopupTabEmptyArea -{ - padding-left: 10px; - border-bottom: #d5d59d 1px solid; -} - -.PopupTab, .PopupTabSelected -{ - border-right: #d5d59d 1px solid; - border-top: #d5d59d 1px solid; - border-left: #d5d59d 1px solid; - padding: 3px 5px 3px 5px; - color: #737357; -} - -.PopupTab -{ - margin-top: 1px; - border-bottom: #d5d59d 1px solid; - cursor: pointer; -} - -.PopupTabSelected -{ - font-weight: bold; - cursor: default; - padding-top: 4px; - border-bottom: #f1f1e3 1px solid; - background-color: #f1f1e3; -} diff --git a/src/Presentation/SmartStore.Web/Content/editors/ckeditor/plugins/wsc/dialogs/wsc.js b/src/Presentation/SmartStore.Web/Content/editors/ckeditor/plugins/wsc/dialogs/wsc.js deleted file mode 100644 index b53a48cf47..0000000000 --- a/src/Presentation/SmartStore.Web/Content/editors/ckeditor/plugins/wsc/dialogs/wsc.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved. - For licensing, see LICENSE.html or http://ckeditor.com/license -*/ -(function(){function q(a){return a&&a.domId&&a.getInputElement().$?a.getInputElement():a&&a.$?a:!1}function z(a){if(!a)throw"Languages-by-groups list are required for construct selectbox";var c=[],d="",f;for(f in a)for(var g in a[f]){var h=a[f][g];"en_US"==h?d=h:c.push(h)}c.sort();d&&c.unshift(d);return{getCurrentLangGroup:function(c){a:{for(var d in a)for(var f in a[d])if(f.toUpperCase()===c.toUpperCase()){c=d;break a}c=""}return c},setLangList:function(){var c={},d;for(d in a)for(var f in a[d])c[a[d][f]]= -f;return c}()}}var e=function(){var a=function(a,b,f){var f=f||{},g=f.expires;if("number"==typeof g&&g){var h=new Date;h.setTime(h.getTime()+1E3*g);g=f.expires=h}g&&g.toUTCString&&(f.expires=g.toUTCString());var b=encodeURIComponent(b),a=a+"="+b,e;for(e in f)b=f[e],a+="; "+e,!0!==b&&(a+="="+b);document.cookie=a};return{postMessage:{init:function(a){window.addEventListener?window.addEventListener("message",a,!1):window.attachEvent("onmessage",a)},send:function(a){var b=Object.prototype.toString,f= -a.fn||null,g=a.id||"",e=a.target||window,i=a.message||{id:g};a.message&&"[object Object]"==b.call(a.message)&&(a.message.id||(a.message.id=g),i=a.message);a=window.JSON.stringify(i,f);e.postMessage(a,"*")},unbindHandler:function(a){window.removeEventListener?window.removeEventListener("message",a,!1):window.detachEvent("onmessage",a)}},hash:{create:function(){},parse:function(){}},cookie:{set:a,get:function(a){return(a=document.cookie.match(RegExp("(?:^|; )"+a.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, -"\\$1")+"=([^;]*)")))?decodeURIComponent(a[1]):void 0},remove:function(c){a(c,"",{expires:-1})}},misc:{findFocusable:function(a){var b=null;a&&(b=a.find("a[href], area[href], input, select, textarea, button, *[tabindex], *[contenteditable]"));return b},isVisible:function(a){return!(0===a.offsetWidth||0==a.offsetHeight||"none"===(document.defaultView&&document.defaultView.getComputedStyle?document.defaultView.getComputedStyle(a,null).display:a.currentStyle?a.currentStyle.display:a.style.display))}, -hasClass:function(a,b){return!(!a.className||!a.className.match(RegExp("(\\s|^)"+b+"(\\s|$)")))}}}}(),a=a||{};a.TextAreaNumber=null;a.load=!0;a.cmd={SpellTab:"spell",Thesaurus:"thes",GrammTab:"grammar"};a.dialog=null;a.optionNode=null;a.selectNode=null;a.grammerSuggest=null;a.textNode={};a.iframeMain=null;a.dataTemp="";a.div_overlay=null;a.textNodeInfo={};a.selectNode={};a.selectNodeResponce={};a.langList=null;a.langSelectbox=null;a.banner="";a.show_grammar=null;a.div_overlay_no_check=null;a.targetFromFrame= -{};a.onLoadOverlay=null;a.LocalizationComing={};a.OverlayPlace=null;a.LocalizationButton={ChangeTo:{instance:null,text:"Change to"},ChangeAll:{instance:null,text:"Change All"},IgnoreWord:{instance:null,text:"Ignore word"},IgnoreAllWords:{instance:null,text:"Ignore all words"},Options:{instance:null,text:"Options",optionsDialog:{instance:null}},AddWord:{instance:null,text:"Add word"},FinishChecking:{instance:null,text:"Finish Checking"}};a.LocalizationLabel={ChangeTo:{instance:null,text:"Change to"}, -Suggestions:{instance:null,text:"Suggestions"}};var A=function(b){var c,d;for(d in b)c=b[d].instance.getElement().getFirst()||b[d].instance.getElement(),c.setText(a.LocalizationComing[d])},B=function(b){for(var c in b){if(!b[c].instance.setLabel)break;b[c].instance.setLabel(a.LocalizationComing[c])}},j,r;a.framesetHtml=function(b){return"'}; -a.setIframe=function(b,c){var d;d=a.framesetHtml(c);var f=a.iframeNumber+"_"+c;b.getElement().setHtml(d);d=document.getElementById(f);d=d.contentWindow?d.contentWindow:d.contentDocument.document?d.contentDocument.document:d.contentDocument;d.document.open();d.document.write('iframe
                                        + + @property value + @type mixed + @default element's text + **/ + value: null, + /** + Callback to perform custom displaying of value in element's text. + If `null`, default input's display used. + If `false`, no displaying methods will be called, element's text will never change. + Runs under element's scope. + _**Parameters:**_ + + * `value` current value to be displayed + * `response` server response (if display called after ajax submit), since 1.4.0 + + For _inputs with source_ (select, checklist) parameters are different: + + * `value` current value to be displayed + * `sourceData` array of items for current input (e.g. dropdown items) + * `response` server response (if display called after ajax submit), since 1.4.0 + + To get currently selected items use `$.fn.editableutils.itemsByValue(value, sourceData)`. + + @property display + @type function|boolean + @default null + @since 1.2.0 + @example + display: function(value, sourceData) { + //display checklist as comma-separated values + var html = [], + checked = $.fn.editableutils.itemsByValue(value, sourceData); + + if(checked.length) { + $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); }); + $(this).html(html.join(', ')); + } else { + $(this).empty(); + } + } + **/ + display: null, + /** + Css class applied when editable text is empty. + + @property emptyclass + @type string + @since 1.4.1 + @default editable-empty + **/ + emptyclass: 'editable-empty', + /** + Css class applied when value was stored but not sent to server (`pk` is empty or `send = 'never'`). + You may set it to `null` if you work with editables locally and submit them together. + + @property unsavedclass + @type string + @since 1.4.1 + @default editable-unsaved + **/ + unsavedclass: 'editable-unsaved', + /** + If selector is provided, editable will be delegated to the specified targets. + Usefull for dynamically generated DOM elements. + **Please note**, that delegated targets can't be initialized with `emptytext` and `autotext` options, + as they actually become editable only after first click. + You should manually set class `editable-click` to these elements. + Also, if element originally empty you should add class `editable-empty`, set `data-value=""` and write emptytext into element: + + @property selector + @type string + @since 1.4.1 + @default null + @example +
                                        + + Empty + + Operator +
                                        + + + **/ + selector: null, + /** + Color used to highlight element after update. Implemented via CSS3 transition, works in modern browsers. + + @property highlight + @type string|boolean + @since 1.4.5 + @default #FFFF80 + **/ + highlight: '#FFFF80' + }; + +}(window.jQuery)); + +/** +AbstractInput - base class for all editable inputs. +It defines interface to be implemented by any input type. +To create your own input you can inherit from this class. + +@class abstractinput +**/ +(function ($) { + "use strict"; + + //types + $.fn.editabletypes = {}; + + var AbstractInput = function () { }; + + AbstractInput.prototype = { + /** + Initializes input + + @method init() + **/ + init: function (type, options, defaults) { + this.type = type; + this.options = $.extend({}, defaults, options); + }, + + /* + this method called before render to init $tpl that is inserted in DOM + */ + prerender: function () { + this.$tpl = $(this.options.tpl); //whole tpl as jquery object + this.$input = this.$tpl; //control itself, can be changed in render method + this.$clear = null; //clear button + this.error = null; //error message, if input cannot be rendered + }, + + /** + Renders input from tpl. Can return jQuery deferred object. + Can be overwritten in child objects + + @method render() + **/ + render: function () { + + }, + + /** + Sets element's html by value. + + @method value2html(value, element) + @param {mixed} value + @param {DOMElement} element + **/ + value2html: function (value, element) { + $(element)[this.options.escape ? 'text' : 'html']($.trim(value)); + }, + + /** + Converts element's html to value + + @method html2value(html) + @param {string} html + @returns {mixed} + **/ + html2value: function (html) { + return $('
                                        ').html(html).text(); + }, + + /** + Converts value to string (for internal compare). For submitting to server used value2submit(). + + @method value2str(value) + @param {mixed} value + @returns {string} + **/ + value2str: function (value) { + return String(value); + }, + + /** + Converts string received from server into value. Usually from `data-value` attribute. + + @method str2value(str) + @param {string} str + @returns {mixed} + **/ + str2value: function (str) { + return str; + }, + + /** + Converts value for submitting to server. Result can be string or object. + + @method value2submit(value) + @param {mixed} value + @returns {mixed} + **/ + value2submit: function (value) { + return value; + }, + + /** + Sets value of input. + + @method value2input(value) + @param {mixed} value + **/ + value2input: function (value) { + this.$input.val(value); + }, + + /** + Returns value of input. Value can be object (e.g. datepicker) + + @method input2value() + **/ + input2value: function () { + return this.$input.val(); + }, + + /** + Activates input. For text it sets focus. + + @method activate() + **/ + activate: function () { + if (this.$input.is(':visible')) { + this.$input.focus(); + } + }, + + /** + Creates input. + + @method clear() + **/ + clear: function () { + this.$input.val(null); + }, + + /** + method to escape html. + **/ + escape: function (str) { + return $('
                                        ').text(str).html(); + }, + + /** + attach handler to automatically submit form when value changed (useful when buttons not shown) + **/ + autosubmit: function () { + + }, + + /** + Additional actions when destroying element + **/ + destroy: function () { + }, + + // -------- helper functions -------- + setClass: function () { + if (this.options.inputclass) { + this.$input.addClass(this.options.inputclass); + } + }, + + setAttr: function (attr) { + if (this.options[attr] !== undefined && this.options[attr] !== null) { + this.$input.attr(attr, this.options[attr]); + } + }, + + option: function (key, value) { + this.options[key] = value; + } + + }; + + AbstractInput.defaults = { + /** + HTML template of input. Normally you should not change it. + + @property tpl + @type string + @default '' + **/ + tpl: '', + /** + CSS class automatically applied to input + + @property inputclass + @type string + @default null + **/ + inputclass: null, + + /** + If `true` - html will be escaped in content of element via $.text() method. + If `false` - html will not be escaped, $.html() used. + When you use own `display` function, this option obviosly has no effect. + + @property escape + @type boolean + @since 1.5.0 + @default true + **/ + escape: true, + + //scope for external methods (e.g. source defined as function) + //for internal use only + scope: null, + + //need to re-declare showbuttons here to get it's value from common config (passed only options existing in defaults) + showbuttons: true + }; + + $.extend($.fn.editabletypes, { abstractinput: AbstractInput }); + +}(window.jQuery)); + +/** +List - abstract class for inputs that have source option loaded from js array or via ajax + +@class list +@extends abstractinput +**/ +(function ($) { + "use strict"; + + var List = function (options) { + + }; + + $.fn.editableutils.inherit(List, $.fn.editabletypes.abstractinput); + + $.extend(List.prototype, { + render: function () { + var deferred = $.Deferred(); + + this.error = null; + this.onSourceReady(function () { + this.renderList(); + deferred.resolve(); + }, function () { + this.error = this.options.sourceError; + deferred.resolve(); + }); + + return deferred.promise(); + }, + + html2value: function (html) { + return null; //can't set value by text + }, + + value2html: function (value, element, display, response) { + var deferred = $.Deferred(), + success = function () { + if (typeof display === 'function') { + //custom display method + display.call(element, value, this.sourceData, response); + } else { + this.value2htmlFinal(value, element); + } + deferred.resolve(); + }; + + //for null value just call success without loading source + if (value === null) { + success.call(this); + } else { + this.onSourceReady(success, function () { deferred.resolve(); }); + } + + return deferred.promise(); + }, + + // ------------- additional functions ------------ + + onSourceReady: function (success, error) { + //run source if it function + var source; + if ($.isFunction(this.options.source)) { + source = this.options.source.call(this.options.scope); + this.sourceData = null; + //note: if function returns the same source as URL - sourceData will be taken from cahce and no extra request performed + } else { + source = this.options.source; + } + + //if allready loaded just call success + if (this.options.sourceCache && $.isArray(this.sourceData)) { + success.call(this); + return; + } + + //try parse json in single quotes (for double quotes jquery does automatically) + try { + source = $.fn.editableutils.tryParseJson(source, false); + } catch (e) { + error.call(this); + return; + } + + //loading from url + if (typeof source === 'string') { + //try to get sourceData from cache + if (this.options.sourceCache) { + var cacheID = source, + cache; + + if (!$(document).data(cacheID)) { + $(document).data(cacheID, {}); + } + cache = $(document).data(cacheID); + + //check for cached data + if (cache.loading === false && cache.sourceData) { //take source from cache + this.sourceData = cache.sourceData; + this.doPrepend(); + success.call(this); + return; + } else if (cache.loading === true) { //cache is loading, put callback in stack to be called later + cache.callbacks.push($.proxy(function () { + this.sourceData = cache.sourceData; + this.doPrepend(); + success.call(this); + }, this)); + + //also collecting error callbacks + cache.err_callbacks.push($.proxy(error, this)); + return; + } else { //no cache yet, activate it + cache.loading = true; + cache.callbacks = []; + cache.err_callbacks = []; + } + } + + //ajaxOptions for source. Can be overwritten bt options.sourceOptions + var ajaxOptions = $.extend({ + url: source, + type: 'get', + cache: false, + dataType: 'json', + success: $.proxy(function (data) { + if (cache) { + cache.loading = false; + } + this.sourceData = this.makeArray(data); + if ($.isArray(this.sourceData)) { + if (cache) { + //store result in cache + cache.sourceData = this.sourceData; + //run success callbacks for other fields waiting for this source + $.each(cache.callbacks, function () { this.call(); }); + } + this.doPrepend(); + success.call(this); + } else { + error.call(this); + if (cache) { + //run error callbacks for other fields waiting for this source + $.each(cache.err_callbacks, function () { this.call(); }); + } + } + }, this), + error: $.proxy(function () { + error.call(this); + if (cache) { + cache.loading = false; + //run error callbacks for other fields + $.each(cache.err_callbacks, function () { this.call(); }); + } + }, this) + }, this.options.sourceOptions); + + //loading sourceData from server + $.ajax(ajaxOptions); + + } else { //options as json/array + this.sourceData = this.makeArray(source); + + if ($.isArray(this.sourceData)) { + this.doPrepend(); + success.call(this); + } else { + error.call(this); + } + } + }, + + doPrepend: function () { + if (this.options.prepend === null || this.options.prepend === undefined) { + return; + } + + if (!$.isArray(this.prependData)) { + //run prepend if it is function (once) + if ($.isFunction(this.options.prepend)) { + this.options.prepend = this.options.prepend.call(this.options.scope); + } + + //try parse json in single quotes + this.options.prepend = $.fn.editableutils.tryParseJson(this.options.prepend, true); + + //convert prepend from string to object + if (typeof this.options.prepend === 'string') { + this.options.prepend = { '': this.options.prepend }; + } + + this.prependData = this.makeArray(this.options.prepend); + } + + if ($.isArray(this.prependData) && $.isArray(this.sourceData)) { + this.sourceData = this.prependData.concat(this.sourceData); + } + }, + + /* + renders input list + */ + renderList: function () { + // this method should be overwritten in child class + }, + + /* + set element's html by value + */ + value2htmlFinal: function (value, element) { + // this method should be overwritten in child class + }, + + /** + * convert data to array suitable for sourceData, e.g. [{value: 1, text: 'abc'}, {...}] + */ + makeArray: function (data) { + var count, obj, result = [], item, iterateItem; + if (!data || typeof data === 'string') { + return null; + } + + if ($.isArray(data)) { //array + /* + function to iterate inside item of array if item is object. + Caclulates count of keys in item and store in obj. + */ + iterateItem = function (k, v) { + obj = { value: k, text: v }; + if (count++ >= 2) { + return false;// exit from `each` if item has more than one key. + } + }; + + for (var i = 0; i < data.length; i++) { + item = data[i]; + if (typeof item === 'object') { + count = 0; //count of keys inside item + $.each(item, iterateItem); + //case: [{val1: 'text1'}, {val2: 'text2} ...] + if (count === 1) { + result.push(obj); + //case: [{value: 1, text: 'text1'}, {value: 2, text: 'text2'}, ...] + } else if (count > 1) { + //removed check of existance: item.hasOwnProperty('value') && item.hasOwnProperty('text') + if (item.children) { + item.children = this.makeArray(item.children); + } + result.push(item); + } + } else { + //case: ['text1', 'text2' ...] + result.push({ value: item, text: item }); + } + } + } else { //case: {val1: 'text1', val2: 'text2, ...} + $.each(data, function (k, v) { + result.push({ value: k, text: v }); + }); + } + return result; + }, + + option: function (key, value) { + this.options[key] = value; + if (key === 'source') { + this.sourceData = null; + } + if (key === 'prepend') { + this.prependData = null; + } + } + + }); + + List.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { + /** + Source data for list. + If **array** - it should be in format: `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]` + For compability, object format is also supported: `{"1": "text1", "2": "text2" ...}` but it does not guarantee elements order. + + If **string** - considered ajax url to load items. In that case results will be cached for fields with the same source and name. See also `sourceCache` option. + + If **function**, it should return data in format above (since 1.4.0). + + Since 1.4.1 key `children` supported to render OPTGROUP (for **select** input only): + @example + [ + {text: "group1", children: [ + {value: 1, text: "text1"}, + {value: 2, text: "text2"} + ]}, + ... + ] + + + @property source + @type string | array | object | function + @default null + **/ + source: null, + /** + Data automatically prepended to the beginning of dropdown list. + + @property prepend + @type string | array | object | function + @default false + **/ + prepend: false, + /** + Error message when list cannot be loaded (e.g. ajax error) + + @property sourceError + @type string + @default Error when loading list + **/ + sourceError: 'Error when loading list', + /** + if `true` and source is **string url** - results will be cached for fields with the same source. + Usefull for editable column in grid to prevent extra requests. + + @property sourceCache + @type boolean + @default true + @since 1.2.0 + **/ + sourceCache: true, + /** + Additional ajax options to be used in $.ajax() when loading list from server. + Useful to send extra parameters or change request method. + + @property sourceOptions + @type object|function + @default null + @since 1.5.0 + @example + sourceOptions: { + data: {param: 123}, + type: 'post' + } + + **/ + sourceOptions: null + }); + + $.fn.editabletypes.list = List; + +}(window.jQuery)); + +/** +Text input + +@class text +@extends abstractinput +@final +@example +awesome + +**/ +(function ($) { + "use strict"; + + var Text = function (options) { + this.init('text', options, Text.defaults); + }; + + $.fn.editableutils.inherit(Text, $.fn.editabletypes.abstractinput); + + $.extend(Text.prototype, { + render: function () { + this.renderClear(); + this.setClass(); + this.setAttr('placeholder'); + }, + + activate: function () { + if (this.$input.is(':visible')) { + this.$input.focus(); + // if (this.$input.is('input,textarea') && !this.$input.is('[type="checkbox"],[type="range"],[type="number"],[type="email"]')) { + if (this.$input.is('input,textarea') && !this.$input.is('[type="checkbox"],[type="range"]')) { + $.fn.editableutils.setCursorPosition(this.$input.get(0), this.$input.val().length); + } + + if (this.toggleClear) { + this.toggleClear(); + } + } + }, + + //render clear button + renderClear: function () { + if (this.options.clear) { + this.$clear = $(''); + this.$input.after(this.$clear) + .css('padding-right', 24) + .keyup($.proxy(function (e) { + //arrows, enter, tab, etc + if (~$.inArray(e.keyCode, [40, 38, 9, 13, 27])) { + return; + } + + clearTimeout(this.t); + var that = this; + this.t = setTimeout(function () { + that.toggleClear(e); + }, 100); + + }, this)) + .parent().css('position', 'relative'); + + this.$clear.click($.proxy(this.clear, this)); + } + }, + + postrender: function () { + /* + //now `clear` is positioned via css + if(this.$clear) { + //can position clear button only here, when form is shown and height can be calculated +// var h = this.$input.outerHeight(true) || 20, + var h = this.$clear.parent().height(), + delta = (h - this.$clear.height()) / 2; + + //this.$clear.css({bottom: delta, right: delta}); + } + */ + }, + + //show / hide clear button + toggleClear: function (e) { + if (!this.$clear) { + return; + } + + var len = this.$input.val().length, + visible = this.$clear.is(':visible'); + + if (len && !visible) { + this.$clear.show(); + } + + if (!len && visible) { + this.$clear.hide(); + } + }, + + clear: function () { + this.$clear.hide(); + this.$input.val('').focus(); + } + }); + + Text.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { + /** + @property tpl + @default + **/ + tpl: '', + /** + Placeholder attribute of input. Shown when input is empty. + + @property placeholder + @type string + @default null + **/ + placeholder: null, + + /** + Whether to show `clear` button + + @property clear + @type boolean + @default true + **/ + clear: true + }); + + $.fn.editabletypes.text = Text; + +}(window.jQuery)); + +/** +Textarea input + +@class textarea +@extends abstractinput +@final +@example +awesome comment! + +**/ +(function ($) { + "use strict"; + + var Textarea = function (options) { + this.init('textarea', options, Textarea.defaults); + }; + + $.fn.editableutils.inherit(Textarea, $.fn.editabletypes.abstractinput); + + $.extend(Textarea.prototype, { + render: function () { + this.setClass(); + this.setAttr('placeholder'); + this.setAttr('rows'); + + //ctrl + enter + this.$input.keydown(function (e) { + if (e.ctrlKey && e.which === 13) { + $(this).closest('form').submit(); + } + }); + }, + + //using `white-space: pre-wrap` solves \n <--> BR conversion very elegant! + /* + value2html: function(value, element) { + var html = '', lines; + if(value) { + lines = value.split("\n"); + for (var i = 0; i < lines.length; i++) { + lines[i] = $('
                                        ').text(lines[i]).html(); + } + html = lines.join('
                                        '); + } + $(element).html(html); + }, + + html2value: function(html) { + if(!html) { + return ''; + } + + var regex = new RegExp(String.fromCharCode(10), 'g'); + var lines = html.split(//i); + for (var i = 0; i < lines.length; i++) { + var text = $('
                                        ').html(lines[i]).text(); + + // Remove newline characters (\n) to avoid them being converted by value2html() method + // thus adding extra
                                        tags + text = text.replace(regex, ''); + + lines[i] = text; + } + return lines.join("\n"); + }, + */ + activate: function () { + $.fn.editabletypes.text.prototype.activate.call(this); + } + }); + + Textarea.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { + /** + @property tpl + @default + **/ + tpl: '', + /** + @property inputclass + @default input-large + **/ + inputclass: 'input-large', + /** + Placeholder attribute of input. Shown when input is empty. + + @property placeholder + @type string + @default null + **/ + placeholder: null, + /** + Number of rows in textarea + + @property rows + @type integer + @default 7 + **/ + rows: 7 + }); + + $.fn.editabletypes.textarea = Textarea; + +}(window.jQuery)); + +/** +Select (dropdown) + +@class select +@extends list +@final +@example + + +**/ +(function ($) { + "use strict"; + + var Select = function (options) { + this.init('select', options, Select.defaults); + }; + + $.fn.editableutils.inherit(Select, $.fn.editabletypes.list); + + $.extend(Select.prototype, { + renderList: function () { + this.$input.empty(); + var escape = this.options.escape; + + var fillItems = function ($el, data) { + var attr; + if ($.isArray(data)) { + for (var i = 0; i < data.length; i++) { + attr = {}; + if (data[i].children) { + attr.label = data[i].text; + $el.append(fillItems($('', attr), data[i].children)); + } else { + attr.value = data[i].value; + if (data[i].disabled) { + attr.disabled = true; + } + var $option = $('