diff --git a/README.md b/README.md index 33df0f7bb9..f8e84aa9bb 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,60 @@ -## Overview +

+ + Smartstore.NET + + + +
+
+ +

+ Smartstore.NET +

+

Ready. Sell. Grow.

+

- SmartStore.NET Demoshop + GitHub release + GitHub All Releases + GitHub repo size + GitHub closed issues + GitHub commit activity + GitHub

-SmartStore.NET is a free, open source, full-featured e-commerce solution for companies of any size. It is web standards compliant and incorporates the newest Microsoft web technology stack. +

+ Try Online ∙ + Docs ∙ + Forum ∙ + Marketplace ∙ + Translations +

+
+ +Smartstore.NET is a free, open source, full-featured e-commerce solution for companies of any size. It is web standards compliant and incorporates the newest Microsoft web technology stack. -**SmartStore.NET includes all essential features to create multilingual and multi-currency stores** targeting desktop or mobile devices and enabling SEO optimized rich product catalogs with support for an unlimited number of products and categories, variants, bundles, datasheets, ESD, discounts, coupons and many more. +**Smartstore.NET includes all essential features to create multilingual and multi-currency stores** targeting desktop or mobile devices and enabling SEO optimized rich product catalogs with support for an unlimited number of products and categories, variants, bundles, datasheets, ESD, discounts, coupons and many more. -A comprehensive set of tools for CRM & CMS, sales, marketing, payment & shipping handling, etc. makes SmartStore.NET a powerful all-in-one solution fulfilling all your needs. +A comprehensive set of tools for CRM & CMS, sales, marketing, payment & shipping handling, etc. makes Smartstore.NET a powerful all-in-one solution fulfilling all your needs. + +
+

+ Smartstore.NET Demoshop +

+
-**SmartStore.NET delivers a beautiful and configurable shop front-end out-of-the-box**, built with a design approach on the highest level, including components like `Bootstrap 4`, `Sass` and others. The supplied theme _Flex_ is modern, clean and fully responsive, giving buyers the best possible shopping experience on any device. +**Smartstore.NET delivers a beautiful and configurable shop front-end out-of-the-box**, built with a design approach on the highest level, including components like `Bootstrap 4`, `Sass` and others. The supplied theme _Flex_ is modern, clean and fully responsive, giving buyers the best possible shopping experience on any device. -The state-of-the-art architecture of SmartStore.NET - with `ASP.NET 4.5` + `MVC 5`, `Entity Framework 6` and Domain Driven Design approach - makes it easy to extend, extremely flexible and essentially fun to work with ;-) +The state-of-the-art architecture of Smartstore.NET - with `ASP.NET 4.5` + `MVC 5`, `Entity Framework 6` and Domain Driven Design approach - makes it easy to extend, extremely flexible and essentially fun to work with ;-) * **Website:** [http://www.smartstore.com/en/net](http://www.smartstore.com/en/net) * **Forum:** [http://community.smartstore.com](http://community.smartstore.com) * **Marketplace:** [http://community.smartstore.com/marketplace](http://community.smartstore.com/marketplace) * **Translations:** [http://translate.smartstore.com/](http://translate.smartstore.com/) -* **Documentation:** [SmartStore.NET Documentation in English](http://docs.smartstore.com/display/SMNET/SmartStore.NET+Documentation+Home) +* **Documentation:** [Smartstore.NET Documentation in English](http://docs.smartstore.com/display/SMNET/SmartStore.NET+Documentation+Home) + +

 

+ ## Highlights @@ -57,8 +93,11 @@ The state-of-the-art architecture of SmartStore.NET - with `ASP.NET 4.5` + `MVC * CMS (Blog, Forum, custom pages & HTML content etc.) * and many more... + + ## Project Status -SmartStore.NET V3.2.0 has been released on 10 May, 2019. The highlights are: + +Smartstore.NET V3.2.0 has been released on 10 May, 2019. The highlights are: * (NEW) **Page Builder**: Create fascinating content that will boost your sales. No coding is required thanks to a powerful WYSIWYG editor which utilizes the revolutionary *CSS Grid* system (commercial plugin) * (NEW) **Menu Builder**: Visual manager for all sorts of menus. Change existing menus or create your own and place them anywhere you want. @@ -69,11 +108,13 @@ SmartStore.NET V3.2.0 has been released on 10 May, 2019. The highlights are: ## Try it online -We have set up a live online demo for you so you are able to test SmartStore.NET without local installation. Get a first impression and test all available features in the front- and in the backend. Please keep in mind that the backend demo is shared and other testers can modify data at the same time. +We have set up a live online demo for you so you are able to test Smartstore.NET without local installation. Get a first impression and test all available features in the front- and in the backend. Please keep in mind that the backend demo is shared and other testers can modify data at the same time. * [**Frontend**](http://frontend.smartstore.net/en) (User: demo, PWD: 1234) * [**Backend**](http://backend.smartstore.net/en/login) (User: demo, PWD: 1234) + + ## How to install * Download the latest stable release from the download tab and unzip it to your web folder @@ -81,7 +122,7 @@ We have set up a live online demo for you so you are able to test SmartStore.NET * Fire up your browser and follow the installation instructions * Enjoy ;-) -NOTE: SmartStore.NET 3 requires [Visual C++ Redistributable für Visual Studio 2015](https://www.microsoft.com/en-US/download/details.aspx?id=52685) which is already pre-installed on most systems. If, nevertheless, it is missing on your web server, just download and execute the installer or ask your hosting provider to do that for you. +NOTE: Smartstore.NET 3 requires [Visual C++ Redistributable für Visual Studio 2015](https://www.microsoft.com/en-US/download/details.aspx?id=52685) which is already pre-installed on most systems. If, nevertheless, it is missing on your web server, just download and execute the installer or ask your hosting provider to do that for you. ### System requirements @@ -92,6 +133,7 @@ NOTE: SmartStore.NET 3 requires [Visual C++ Redistributable für Visual Studio 2 * Full Trust + ## License -SmartStore.NET Community Edition is released under the [GPLv3 license](http://www.gnu.org/licenses/gpl-3.0.txt). +Smartstore.NET Community Edition is released under the [GPLv3 license](http://www.gnu.org/licenses/gpl-3.0.txt). diff --git a/SmartStoreNET.Tasks.Targets b/SmartStoreNET.Tasks.Targets index 2f84ccec5c..080c78d497 100644 --- a/SmartStoreNET.Tasks.Targets +++ b/SmartStoreNET.Tasks.Targets @@ -39,7 +39,7 @@ x86 $(BUILD_NUMBER) - 3.2.0 + 3.2.2 $(StageFolder) .$(Version) diff --git a/assets/SmartStoreNET.png b/assets/SmartStoreNET.png deleted file mode 100644 index 1fc5ca5409..0000000000 Binary files a/assets/SmartStoreNET.png and /dev/null differ diff --git a/assets/smartstore-icon.png b/assets/smartstore-icon.png new file mode 100644 index 0000000000..e6414db195 Binary files /dev/null and b/assets/smartstore-icon.png differ diff --git a/assets/smartstore-text.png b/assets/smartstore-text.png new file mode 100644 index 0000000000..25bf9558fb Binary files /dev/null and b/assets/smartstore-text.png differ diff --git a/build.bat b/build.bat index ef0004b549..94a6c72519 100644 --- a/build.bat +++ b/build.bat @@ -1,37 +1,45 @@ +SETLOCAL ENABLEDELAYEDEXPANSION + for /f "usebackq tokens=1* delims=: " %%i in (`lib\vswhere\vswhere -latest -requires Microsoft.Component.MSBuild`) do ( - if /i "%%i"=="installationPath" set InstallDir=%%j + if /i "%%i"=="installationPath" ( + set InstallDir=%%j + echo !InstallDir! + if exist "!InstallDir!\MSBuild\15.0\Bin\MSBuild.exe" ( + echo "Using MSBuild from Visual Studio 2017" + set msbuild="!InstallDir!\MSBuild\15.0\Bin\MSBuild.exe" + goto build + ) + if exist "!InstallDir!\MSBuild\Current\Bin\MSBuild.exe" ( + echo "Using MSBuild from Visual Studio 2019" + set msbuild="!InstallDir!\MSBuild\Current\Bin\MSBuild.exe" + goto build + ) + ) ) FOR %%b in ( - "%InstallDir%\Common7\Tools\VsMSBuildCmd.bat" - "%VS140COMNTOOLS%\Common7\Tools\vsvars32.bat" + "%VS150COMNTOOLS%\vsvars32.bat" + "%VS140COMNTOOLS%\vsvars32.bat" + "%VS120COMNTOOLS%\vsvars32.bat" + "%VS110COMNTOOLS%\vsvars32.bat" ) do ( if exist %%b ( - call %%b - goto findmsbuild + echo "Using MSBuild from %%b" + call %%b + set msbuild="msbuild" + goto build ) ) echo "Unable to detect suitable environment. Build may not succeed." -:findmsbuild - -SETLOCAL ENABLEDELAYEDEXPANSION - -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 (15.0). Build may not succeed." - :build + cd /d %~dp0 echo "Restoring NuGet packages" lib\nuget\nuget.exe restore "src\SmartStoreNET.sln" -call "!MsBuildPath!" SmartStoreNET.proj /p:SlnName=SmartStoreNET /m /p:DebugSymbols=false /p:DebugType=None /maxcpucount %* +%msbuild% SmartStoreNET.proj /p:SlnName=SmartStoreNET /m /p:DebugSymbols=false /p:DebugType=None /maxcpucount %* + +:end diff --git a/changelog.md b/changelog.md index 9166840c1d..81357cca18 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,78 @@ # Release Notes +## SmartStore.NET 3.2.2 + +### New Features +* External category links: alternative links for categories in the main menu and in category listings. +* Page Builder: multistore support for stories. +* #1639 Shipping.ByTotal: Add option to calculate the shipping rate based on the net total instead of the gross total. +* #1537 Implement "Ratenzahlung powered by PayPal" provider. +* New Heidelpay payment plugin. +* Shop-Connector: mapping of product SKUs that differ between client shops. +* MegaMenu supports configuration of menu items + + +### Improvements +* Amazon Pay: PSD2. Strong customer authentication (SCA) upgrade. +* Updated *MaxMind.GeoIP* library and country database +* Removed HTML encoding for meta tags and document title. +* #1662 Quantity Units: added field for name pluralization. +* (SEO) combined SSL and canonical host 301 redirections to prevent successive redirects. +* (SEO) Activated GZIP compression for all dynamic and static responses. +* Set max. file upload size to 2 GB (globally) +* (Dev) Added a sample block for the *Page Builder* in DevTools plugin. +* Various other minor improvements. +* PayPal PLUS: paying by instalments can be selected in payment wall. +* Payone: send more information to API (e.g. shipping address) to stay as frictionless as possible for PSD2. +* PageBuilder: Added keyboard shortcuts +* #1582 Added structured data to reviews +* BMEcat: added support for import of specification attributes + +### Bugfixes +* Fixed wrong XML sitemap index numbering. +* #1646 Setting ManufacturerItemsToDisplayOnHomepage to 1 will not show any manufacturer. +* #1647 All manufacturers page: link of last character missing in alphabetical letter list. +* #1664 RTL: CMS menu issues in backend. +* #1677 Page Builder: moving and resizing blocks via drag & drop didn't work correctly in RTL. +* Do not adjust inventory twice when an order is cancelled and deleted. +* #1500 Fallback to fixed VAT rate instead of rate by region for countries that are not subject to vat. +* Fixed "The entity wrapper stored in the proxy does not reference the same proxy" when copying an export profile. +* Various other minor fixes. +* BMEcat: set TaxCategoryId & MainPictureId for newly imported products + + + +## SmartStore.NET 3.2.1 + +### Improvements +* Implemented file range support for CachedFileResult +* Sitemap should only include products visible for current customer, which is the search engine system customer +* Fixed uniform spelling for query string parameter "returnUrl" +* Fixed search engine issue (itemtype, "offers" required) on product detail page +* Summernote: solved html formatting issues thanks to "Beautifier" library +* Media middleware: simplified ETag generation & handling + +### Bugfixes + +* Mega Menu: Product rotator wasn't working +* Tiny Image: Fixed periodic IOException +* URL Rewriter: performance optimization +* Fixed more menu for main catalog navigation +* Fixed DateTime nor expressed as UTC in AzureFile & LocalFile +* Content Slider: Fixed slide display bug on surface and other touch devices +* Content Slider: Randomize slides > Value was stored in database but toggle button has always shown "off" +* Page Builder: Fixed slider bug for list blocks (brand, category, products) in Firefox +* Page Builder: fixes blurry background images in IOS +* Output Cache: action widgets didn't render when they were part of a widgetzone within a donut child action +* Multishop resolution failed under certain circumstances +* Fixed menu issue "collection was modified, enumeration operation may not execute" +* Fixed "The parameters dictionary contains a null entry for parameter 'catId' of non-nullable type 'System.Int32'" +* Fixed discount coupon sometimes wasn't applied +* Fixed "Value cannot be null. Parameter name: key" in order list +* Fixed topic link resolving by system name in multistore environment +* Async event consumers were not guaranteed to be awaited + + ## SmartStore.NET 3.2 ### Highlights @@ -401,7 +474,7 @@ * Added config setting *sm:PdfEngineBaseUrl*. There are cases where the PDF converter exits with a network error, when it is unable to load automatically resolved URLs. * (Dev) Added *Retry* utility class * #1176 Admin > Product Search: It ain't possible to search for parts of a product name - + ### Bugfixes * #1145: Fixed HTTP 404 after switching language * Fixed null reference exception in product lists if sorting is not allowed @@ -1310,7 +1383,7 @@ * [SMNET-202] - SmartTabSelection mit verschachtelten Tabs fehlerhaft nach Reload einer Seite ### Improvement - + * [SMNET-13] - Attributwerte: der Text "Aufpreis" muss um "Minderpreis" erweitert werden. * [SMNET-15] - Umgestaltung der Darstellung der Staffelpreise (Popover ab dem fünften Element) * [SMNET-30] - MessageTemplates teilweise auf Englisch diff --git a/src/AssemblyVersionInfo.cs b/src/AssemblyVersionInfo.cs index be8f7210d9..719cd1d6ff 100644 --- a/src/AssemblyVersionInfo.cs +++ b/src/AssemblyVersionInfo.cs @@ -9,7 +9,7 @@ // // You can specify all the values or you can default the Revision and Build Numbers // by using the '*' as shown below: -[assembly: AssemblyVersion("3.2.0.0")] +[assembly: AssemblyVersion("3.2.2.0")] -[assembly: AssemblyFileVersion("3.2.0.0")] -[assembly: AssemblyInformationalVersion("3.2.0.0")] +[assembly: AssemblyFileVersion("3.2.2.0")] +[assembly: AssemblyInformationalVersion("3.2.2.0")] diff --git a/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs b/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs index 4403a010fb..2751259eef 100644 --- a/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs +++ b/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -8,85 +9,161 @@ namespace SmartStore.Core.Async { - public static class AsyncRunner + using EventQueue = ConcurrentQueue>; + + public class AsyncRunner : IDisposable { - private static readonly BackgroundWorkHost _host = new BackgroundWorkHost(); + #region Instance members - /// - /// Gets the global cancellation token which signals the application shutdown - /// - public static CancellationToken AppShutdownCancellationToken + private ExclusiveSynchronizationContext CurrentContext; + private SynchronizationContext OldContext; + private int TaskCount; + + private AsyncRunner() { - get { return _host.ShutdownCancellationTokenSource.Token; } + OldContext = SynchronizationContext.Current; + CurrentContext = new ExclusiveSynchronizationContext(OldContext); + SynchronizationContext.SetSynchronizationContext(CurrentContext); + } + + public void Dispose() + { + try + { + CurrentContext.BeginMessageLoop(); + } + catch (Exception e) + { + throw e; + } + finally + { + SynchronizationContext.SetSynchronizationContext(OldContext); + } + } + + private void Increment() + { + Interlocked.Increment(ref TaskCount); + } + + private void Decrement() + { + Interlocked.Decrement(ref TaskCount); + if (TaskCount == 0) + { + CurrentContext.EndMessageLoop(); + } } /// /// Executes an async Task method which has a void return value synchronously /// - /// Task method to execute - public static void RunSync(Func func) + /// Task execute + public void Run(Task task, Action continuation = null) { - var oldContext = SynchronizationContext.Current; - var synch = new ExclusiveSynchronizationContext(); - SynchronizationContext.SetSynchronizationContext(synch); - synch.Post(async _ => + CurrentContext.Post(async _ => { try { - await func(); + Increment(); + await task; + + continuation?.Invoke(task); } catch (Exception e) { - synch.InnerException = e; - throw; + CurrentContext.InnerException = e; } finally { - synch.EndMessageLoop(); + Decrement(); } }, null); - synch.BeginMessageLoop(); - - SynchronizationContext.SetSynchronizationContext(oldContext); } /// /// Executes an async Task method which has a TResult return type synchronously /// /// Return Type - /// Task method to execute - /// - public static TResult RunSync(Func> func) + /// Task to execute + public TResult Run(Task task, Action> continuation = null) { - var oldContext = SynchronizationContext.Current; - var synch = new ExclusiveSynchronizationContext(); - SynchronizationContext.SetSynchronizationContext(synch); - TResult ret = default(TResult); - synch.Post(async _ => + var result = default(TResult); + + CurrentContext.Post(async _ => { try { - ret = await func(); + Increment(); + result = await task; + + continuation?.Invoke(task); } catch (Exception e) { - synch.InnerException = e; - throw; + CurrentContext.InnerException = e; } finally { - synch.EndMessageLoop(); + Decrement(); } }, null); - synch.BeginMessageLoop(); - SynchronizationContext.SetSynchronizationContext(oldContext); - return ret; + + return result; + } + + #endregion + + private static readonly BackgroundWorkHost _host = new BackgroundWorkHost(); + + /// + /// Gets the global cancellation token which signals the application shutdown + /// + public static CancellationToken AppShutdownCancellationToken + { + get { return _host.ShutdownCancellationTokenSource.Token; } + } + + public static AsyncRunner Create() + { + return new AsyncRunner(); + } + + /// + /// Executes an async Task method which has a void return value synchronously + /// + /// Task method to execute + public static void RunSync(Func func, Action continuation = null) + { + using (var runner = new AsyncRunner()) + { + runner.Run(func(), continuation); + } + } + + /// + /// Executes an async Task method which has a TResult return type synchronously + /// + /// Return Type + /// Task method to execute + /// + public static TResult RunSync(Func> func, Action> continuation = null) + { + var result = default(TResult); + + using (var runner = new AsyncRunner()) + { + result = runner.Run(func(), continuation); + } + + return result; } public static Task Run( - Action action, - CancellationToken cancellationToken = default(CancellationToken), - TaskCreationOptions options = TaskCreationOptions.LongRunning, + Action action, + CancellationToken cancellationToken = default(CancellationToken), + TaskCreationOptions options = TaskCreationOptions.LongRunning, TaskScheduler scheduler = null) { Guard.NotNull(action, nameof(action)); @@ -107,10 +184,10 @@ public static Task Run( } public static Task Run( - Action action, - object state, - CancellationToken cancellationToken = default(CancellationToken), - TaskCreationOptions options = TaskCreationOptions.LongRunning, + Action action, + object state, + CancellationToken cancellationToken = default(CancellationToken), + TaskCreationOptions options = TaskCreationOptions.LongRunning, TaskScheduler scheduler = null) { Guard.NotNull(state, nameof(state)); @@ -133,9 +210,9 @@ public static Task Run( } public static Task Run( - Func function, - CancellationToken cancellationToken = default(CancellationToken), - TaskCreationOptions options = TaskCreationOptions.LongRunning, + Func function, + CancellationToken cancellationToken = default(CancellationToken), + TaskCreationOptions options = TaskCreationOptions.LongRunning, TaskScheduler scheduler = null) { Guard.NotNull(function, nameof(function)); @@ -157,7 +234,7 @@ public static Task Run( } public static Task Run( - Func function, + Func function, object state, CancellationToken cancellationToken = default(CancellationToken), TaskCreationOptions options = TaskCreationOptions.LongRunning, @@ -199,7 +276,7 @@ public static Task Run( try { - task = function(scope, ct).ContinueWith(x => + task = function(scope, ct).ContinueWith(x => { scope.Dispose(); }); @@ -213,12 +290,23 @@ public static Task Run( return task; } - private class ExclusiveSynchronizationContext : SynchronizationContext { private bool _done; - readonly AutoResetEvent _workItemsWaiting = new AutoResetEvent(false); - readonly Queue> _items = new Queue>(); + private readonly AutoResetEvent _workItemsWaiting = new AutoResetEvent(false); + private readonly EventQueue _items; + + public ExclusiveSynchronizationContext(SynchronizationContext old) + { + if (old is ExclusiveSynchronizationContext oldEx) + { + this._items = oldEx._items; + } + else + { + this._items = new EventQueue(); + } + } public Exception InnerException { get; set; } @@ -229,10 +317,7 @@ public override void Send(SendOrPostCallback d, object state) public override void Post(SendOrPostCallback d, object state) { - lock (_items) - { - _items.Enqueue(Tuple.Create(d, state)); - } + _items.Enqueue(Tuple.Create(d, state)); _workItemsWaiting.Set(); } @@ -245,26 +330,19 @@ public void BeginMessageLoop() { while (!_done) { - Tuple task = null; - lock (_items) + if (!_items.TryDequeue(out var task)) { - if (_items.Count > 0) - { - task = _items.Dequeue(); - } + _workItemsWaiting.WaitOne(); } - if (task != null) + else { task.Item1(task.Item2); - if (InnerException != null) // the method threw an exeption + + if (InnerException != null) // method threw an exeption { throw new AggregateException("AsyncHelpers.Run method threw an exception.", InnerException); } } - else - { - _workItemsWaiting.WaitOne(); - } } } @@ -280,7 +358,7 @@ internal class BackgroundWorkHost : IRegisteredObject { private readonly CancellationTokenSource _shutdownCancellationTokenSource = new CancellationTokenSource(); private int _numRunningWorkItems; - + public BackgroundWorkHost() { HostingEnvironment.RegisterObject(this); @@ -326,7 +404,7 @@ public void Register(Task work, CancellationToken cancellationToken) } _numRunningWorkItems++; } - + work.ContinueWith( WorkItemComplete, CancellationToken.None, @@ -342,7 +420,7 @@ private void WorkItemComplete(Task work) lock (this) { num = --_numRunningWorkItems; - isCancellationRequested = _shutdownCancellationTokenSource.IsCancellationRequested; + isCancellationRequested = _shutdownCancellationTokenSource.IsCancellationRequested; } if (num == 0 && isCancellationRequested) { diff --git a/src/Libraries/SmartStore.Core/Collections/SyncedCollection.cs b/src/Libraries/SmartStore.Core/Collections/SyncedCollection.cs index 089d9090e4..d8100effd8 100644 --- a/src/Libraries/SmartStore.Core/Collections/SyncedCollection.cs +++ b/src/Libraries/SmartStore.Core/Collections/SyncedCollection.cs @@ -26,6 +26,8 @@ public SyncedCollection(ICollection wrappedCollection, object syncRoot) public object SyncRoot { get; } + public bool ReadLockFree { get; set; } + public void AddRange(IEnumerable collection) { lock (SyncRoot) @@ -112,10 +114,17 @@ public T this[int index] { get { - lock (SyncRoot) + if (ReadLockFree) { return _col.ElementAt(index); } + else + { + lock (SyncRoot) + { + return _col.ElementAt(index); + } + } } } @@ -125,10 +134,17 @@ public int Count { get { - lock (SyncRoot) + if (ReadLockFree) { return _col.Count(); } + else + { + lock (SyncRoot) + { + return _col.Count(); + } + } } } @@ -152,10 +168,17 @@ public void Clear() public bool Contains(T item) { - lock (SyncRoot) + if (ReadLockFree) { return _col.Contains(item); } + else + { + lock (SyncRoot) + { + return _col.Contains(item); + } + } } public void CopyTo(T[] array, int arrayIndex) @@ -174,17 +197,24 @@ public bool Remove(T item) } } + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + public IEnumerator GetEnumerator() { - lock (SyncRoot) + if (ReadLockFree) { return _col.GetEnumerator(); } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return _col.GetEnumerator(); + else + { + lock (SyncRoot) + { + return _col.GetEnumerator(); + } + } } #endregion diff --git a/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs b/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs index 06449caef6..db54bf8936 100644 --- a/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs +++ b/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs @@ -31,7 +31,7 @@ public TrimmedBuffer(string collection, T newItem, int maxSize) public TrimmedBuffer(IEnumerable collection, T newItem, int maxSize) { - Guard.IsPositive(maxSize, nameof(maxSize)); + Guard.IsTrue(maxSize >= 0, nameof(maxSize)); _maxSize = maxSize; _list = new List(collection ?? Enumerable.Empty()); diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs index 5687317362..ae60b58488 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs @@ -125,7 +125,7 @@ public override object ConvertTo(CultureInfo culture, string format, object valu if (value is IEnumerable enumerable) { - // we don't use string.Join() because it doesn't support invariant culture + // We don't use string.Join() because it doesn't support invariant culture foreach (var token in enumerable) { var str = _elementTypeConverter.ConvertTo(culture, format, token, typeof(string)); diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs b/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs index 0700430685..39126f48f8 100644 --- a/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs +++ b/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs @@ -8,7 +8,6 @@ namespace SmartStore.Core.Data.Hooks { public class DefaultDbHookHandler : IDbHookHandler { - private readonly IEnumerable> _hooks; private readonly IList> _saveHooks; private readonly Multimap _hooksRequestCache = new Multimap(); @@ -27,7 +26,6 @@ public class DefaultDbHookHandler : IDbHookHandler public DefaultDbHookHandler(IEnumerable> hooks) { - _hooks = hooks; _saveHooks = hooks.Where(x => x.Metadata.IsLoadHook == false).ToList(); } @@ -35,7 +33,7 @@ public ILogger Logger { get; set; - } + } = NullLogger.Instance; public bool HasImportantSaveHooks() { diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/Category.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/Category.cs index 5a895b03a8..d74e7c0c73 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/Category.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/Category.cs @@ -42,6 +42,12 @@ public partial class Category : BaseEntity, ICategoryNode, IAuditable, ISoftDele [DataMember] public string BottomDescription { get; set; } + /// + /// Gets or sets the external link expression. If set, any category menu item will navigate to the specified link. + /// + [DataMember] + public string ExternalLink { get; set; } + /// /// Gets or sets a text displayed in a badge next to the category within menus /// diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/CategoryNode.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/CategoryNode.cs index 14ba3736a4..c07c9fe69e 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/CategoryNode.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/CategoryNode.cs @@ -10,7 +10,8 @@ public interface ICategoryNode : ILocalizedEntity, ISlugSupported, IAclSupported { int ParentCategoryId { get; } string Name { get; } - string Alias { get; } + string ExternalLink { get; } + string Alias { get; } int? PictureId { get; } bool Published { get; } int DisplayOrder { get; } @@ -25,7 +26,8 @@ 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 string ExternalLink { get; set; } + public string Alias { get; set; } public int? PictureId { get; set; } public bool Published { get; set; } public int DisplayOrder { get; set; } diff --git a/src/Libraries/SmartStore.Core/Domain/Directory/QuantityUnit.cs b/src/Libraries/SmartStore.Core/Domain/Directory/QuantityUnit.cs index f10f0543c3..4e671a5384 100644 --- a/src/Libraries/SmartStore.Core/Domain/Directory/QuantityUnit.cs +++ b/src/Libraries/SmartStore.Core/Domain/Directory/QuantityUnit.cs @@ -1,6 +1,5 @@ -using System; -using SmartStore.Core.Domain.Localization; using System.Runtime.Serialization; +using SmartStore.Core.Domain.Localization; namespace SmartStore.Core.Domain.Directory { @@ -16,6 +15,12 @@ public partial class QuantityUnit : BaseEntity, ILocalizedEntity [DataMember] public string Name { get; set; } + /// + /// Gets or sets the name plural. + /// + [DataMember] + public string NamePlural { get; set; } + /// /// Gets or sets the description /// @@ -38,7 +43,6 @@ public partial class QuantityUnit : BaseEntity, ILocalizedEntity /// Gets or sets the default quantity unit /// [DataMember] - public bool IsDefault { get; set; } - + public bool IsDefault { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs b/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs index 7160bcf30c..9d1d5f5bce 100644 --- a/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs @@ -81,7 +81,7 @@ public SeoSettings() public string SeoNameCharConversion { get; set; } public bool CanonicalUrlsEnabled { get; set; } - public CanonicalHostNameRule CanonicalHostNameRule { get; set; } + public CanonicalHostNameRule CanonicalHostNameRule { get; set; } /// /// Slugs (sename) reserved for some other needs diff --git a/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs b/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs index 0fb734efbd..d9325aa60a 100644 --- a/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs +++ b/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs @@ -97,7 +97,7 @@ public partial class Store : BaseEntity /// /// Gets the security mode for the store /// - public HttpSecurityMode GetSecurityMode(bool? useSsl = null) + public virtual HttpSecurityMode GetSecurityMode(bool? useSsl = null) { if (useSsl ?? SslEnabled) { diff --git a/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs b/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs index c6d22dd2e0..f516fc53da 100644 --- a/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs +++ b/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs @@ -88,12 +88,25 @@ protected set public ScheduleTask Clone() { - return (ScheduleTask)this.MemberwiseClone(); + var task = new ScheduleTask + { + Name = Name, + Alias = Alias, + CronExpression = CronExpression, + Type = Type, + Enabled = Enabled, + StopOnError = StopOnError, + NextRunUtc = NextRunUtc, + IsHidden = IsHidden, + RunPerMachine = RunPerMachine + }; + + return task; } object ICloneable.Clone() { - return this.MemberwiseClone(); + return this.Clone(); } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs b/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs index 1d79ba018b..28fd36b269 100644 --- a/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs @@ -12,7 +12,8 @@ public TaxSettings() DisplayTaxSuffix = true; PricesIncludeTax = true; EuVatAllowVatExemption = true; - ShowLegalHintsInFooter = true; + ShippingPriceIncludesTax = true; + ShowLegalHintsInFooter = true; ShowLegalHintsInProductDetails = true; ShowLegalHintsInProductList = true; AuxiliaryServicesTaxingType = AuxiliaryServicesTaxType.SpecifiedTaxCategory; diff --git a/src/Libraries/SmartStore.Core/Events/ConsumerDescriptor.cs b/src/Libraries/SmartStore.Core/Events/ConsumerDescriptor.cs index f510573a8e..f990c1e61c 100644 --- a/src/Libraries/SmartStore.Core/Events/ConsumerDescriptor.cs +++ b/src/Libraries/SmartStore.Core/Events/ConsumerDescriptor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; @@ -40,5 +41,10 @@ public ConsumerDescriptor(EventConsumerMetadata metadata) /// or empty array if is the only parameter. /// public ParameterInfo[] Parameters { get; set; } + + public override string ToString() + { + return "MessageType: {0} - Consumer: {1}.{2}".FormatCurrent(MessageType.Name, ContainerType.FullName, Method.Name); + } } } diff --git a/src/Libraries/SmartStore.Core/Events/ConsumerInvoker.cs b/src/Libraries/SmartStore.Core/Events/ConsumerInvoker.cs index 52d959e8da..9eb50ecb00 100644 --- a/src/Libraries/SmartStore.Core/Events/ConsumerInvoker.cs +++ b/src/Libraries/SmartStore.Core/Events/ConsumerInvoker.cs @@ -30,9 +30,6 @@ public void Invoke(ConsumerDescriptor descriptor, IConsumer consumer, var p = descriptor.WithEnvelope ? (object)envelope : envelope.Message; var fastInvoker = FastInvoker.GetInvoker(d.Method); - var items = HttpContext.Current.GetItem("ConsumerInvoker", () => new List()); - items.Add(d); - if (!d.FireForget && !d.IsAsync) { // The all synch case @@ -47,8 +44,22 @@ public void Invoke(ConsumerDescriptor descriptor, IConsumer consumer, } else if (!d.FireForget && d.IsAsync) { - // The awaitable Task case - BeginInvoke((Task)InvokeCore(cancelToken: AsyncRunner.AppShutdownCancellationToken), EndInvoke, d); + //// The awaitable Task case + //BeginInvoke((Task)InvokeCore(cancelToken: AsyncRunner.AppShutdownCancellationToken), EndInvoke, d); + + // For now we must go with the AsyncRunner, the above call to BeginInvoke (APM > TPL) does not always + // guarantee that the task is awaited and throws exceptions especially when EF is involved. + using (var runner = AsyncRunner.Create()) + { + try + { + runner.Run((Task)InvokeCore(cancelToken: AsyncRunner.AppShutdownCancellationToken)); + } + catch (Exception ex) + { + HandleException(ex, d); + } + } } else if (d.FireForget && !d.IsAsync) { @@ -87,6 +98,43 @@ public void Invoke(ConsumerDescriptor descriptor, IConsumer consumer, } } + private void HandleException(Exception ex, ConsumerDescriptor descriptor) + { + if (ex is AggregateException ae) + { + ae.InnerExceptions.Each(x => Logger.Error(x)); + } + else + { + Logger.Error(ex); + } + + if (!descriptor.FireForget) + { + throw ex; + } + } + + protected internal virtual IEnumerable ResolveParameters( + IComponentContext container, + ConsumerDescriptor descriptor, + CancellationToken cancelToken) + { + foreach (var p in descriptor.Parameters) + { + if (p.ParameterType == typeof(CancellationToken)) + { + yield return cancelToken; + } + else + { + yield return _resolver.ResolveParameter(p, container); + } + } + } + + #region APM > TPL pattern (does not always work stable) + /// /// Wraps a into the Begin method of an APM pattern. /// @@ -96,7 +144,8 @@ public void Invoke(ConsumerDescriptor descriptor, IConsumer consumer, /// The asynchronous operation, to be returned by the Begin method of the APM pattern. protected IAsyncResult BeginInvoke(Task task, AsyncCallback callback, ConsumerDescriptor descriptor) { - var tcs = new TaskCompletionSource(descriptor, TaskCreationOptions.RunContinuationsAsynchronously); + var options = TaskCreationOptions.RunContinuationsAsynchronously; + var tcs = new TaskCompletionSource(descriptor, options); // "_ =" to discard 'async/await' compiler warning _ = AwaitCompletionAsync(task, callback, tcs, descriptor); @@ -105,9 +154,9 @@ protected IAsyncResult BeginInvoke(Task task, AsyncCallback callback, ConsumerDe } private async Task AwaitCompletionAsync( - Task task, - AsyncCallback callback, - TaskCompletionSource tcs, + Task task, + AsyncCallback callback, + TaskCompletionSource tcs, ConsumerDescriptor descriptor) { try @@ -137,60 +186,8 @@ protected virtual void EndInvoke(IAsyncResult asyncResult) { HandleException(task.Exception, (ConsumerDescriptor)asyncResult.AsyncState); } - if (HttpContext.Current != null) - { - var d = HttpContext.Current.Items["ConsumerInvoker"]; - } - } - - private void HandleException(Exception ex, ConsumerDescriptor descriptor) - { - if (ex is AggregateException ae) - { - ae.InnerExceptions.Each(x => Logger.Error(x)); - } - else - { - Logger.Error(ex); - } - - if (!descriptor.FireForget) - { - throw ex; - } - } - - protected internal virtual IEnumerable ResolveParameters( - IComponentContext container, - ConsumerDescriptor descriptor, - CancellationToken cancelToken) - { - foreach (var p in descriptor.Parameters) - { - if (p.ParameterType == typeof(CancellationToken)) - { - yield return cancelToken; - } - else - { - yield return _resolver.ResolveParameter(p, container); - } - } } - //private void NoContext(Action action) - //{ - // var synchContext = SynchronizationContext.Current; - // SynchronizationContext.SetSynchronizationContext(null); - - // try - // { - // action(); - // } - // finally - // { - // SynchronizationContext.SetSynchronizationContext(synchContext); - // } - //} + #endregion } } diff --git a/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs index 35fa7b73e2..58992963be 100644 --- a/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs @@ -213,16 +213,21 @@ public static byte[] ToByteArray(this Stream stream) if (stream == null) throw new ArgumentNullException(nameof(stream)); - if (stream is MemoryStream) + if (stream is MemoryStream mem) { - return ((MemoryStream)stream).ToArray(); + if (mem.TryGetBuffer(out var buffer)) + { + return buffer.Array; + } + + return mem.ToArray(); } else { using (var streamReader = new MemoryStream()) { stream.CopyTo(streamReader); - return streamReader.ToArray(); + return streamReader.GetBuffer(); } } } @@ -232,9 +237,14 @@ public static async Task ToByteArrayAsync(this Stream stream) if (stream == null) throw new ArgumentNullException(nameof(stream)); - if (stream is MemoryStream) + if (stream is MemoryStream mem) { - return ((MemoryStream)stream).ToArray(); + if (mem.TryGetBuffer(out var buffer)) + { + return buffer.Array; + } + + return mem.ToArray(); } else { diff --git a/src/Libraries/SmartStore.Core/Extensions/HttpExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/HttpExtensions.cs index 0b4f39ebd5..59851619db 100644 --- a/src/Libraries/SmartStore.Core/Extensions/HttpExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/HttpExtensions.cs @@ -11,6 +11,7 @@ using SmartStore.Core.Infrastructure; using SmartStore.Core; using System.Web.Mvc; +using SmartStore.Core.Fakes; namespace SmartStore { @@ -28,6 +29,47 @@ public static class HttpExtensions new Tuple("X-Url-Scheme", "https") }; + /// + /// Tries to get the instance without throwing exceptions + /// + /// The instance or null. + public static HttpRequestBase SafeGetHttpRequest(this HttpContext httpContext) + { + if (httpContext == null) + { + return null; + } + + return SafeGetHttpRequest(new HttpContextWrapper(httpContext)); + } + + /// + /// Tries to get the instance without throwing exceptions + /// + /// The instance or null. + public static HttpRequestBase SafeGetHttpRequest(this HttpContextBase httpContext) + { + if (httpContext == null) + { + return null; + } + + if (httpContext.Handler != null || httpContext is FakeHttpContext) + { + // Having a handler means we're most likely in the MVC routing pipeline. + return httpContext.Request; + } + + try + { + return httpContext.Request; + } + catch + { + return null; + } + } + /// /// Returns wether the specified url is local to the host or not /// diff --git a/src/Libraries/SmartStore.Core/Extensions/RouteExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/RouteExtensions.cs index e57d2ca275..845d7a9a79 100644 --- a/src/Libraries/SmartStore.Core/Extensions/RouteExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/RouteExtensions.cs @@ -34,10 +34,10 @@ public static string GetAreaName(this RouteBase route) return null; } - /// - /// Generates an identifier for the given route in the form "[{area}.]{controller}.{action}" - /// - public static string GenerateRouteIdentifier(this RouteData routeData) + /// + /// Generates an identifier for the given route in the form "[{area}.]{controller}.{action}" + /// + public static string GenerateRouteIdentifier(this RouteData routeData) { string area = routeData.GetAreaName(); string controller = routeData.GetRequiredString("controller"); diff --git a/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs index 9258d0b49b..211fb926af 100644 --- a/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs @@ -611,30 +611,29 @@ public static string[] SplitSafe(this string value, string separator) if (string.IsNullOrEmpty(value)) return new string[0]; - // do not use separator.IsEmpty() here because whitespace like " " is a valid separator. + // Do not use separator.IsEmpty() here because whitespace like " " is a valid separator. // an empty separator "" returns array with value. if (separator == null) { - separator = "|"; - - if (value.IndexOf(separator) < 0) + for (var i = 0; i < value.Length; i++) { - if (value.IndexOf(';') > -1) - { - separator = ";"; - } - else if (value.IndexOf(',') > -1) + var c = value[i]; + if (c == ';' || c == ',' || c == '|') { - separator = ","; + return value.Split(new char[] { c }, StringSplitOptions.RemoveEmptyEntries); } - else if (value.IndexOf(Environment.NewLine) > -1) + if (c == '\r' && (i + 1) < value.Length & value[i + 1] == '\n') { - separator = Environment.NewLine; + return value.GetLines(false, true).ToArray(); } } - } - return value.Split(new string[] { separator }, StringSplitOptions.RemoveEmptyEntries); + return new string[] { value }; + } + else + { + return value.Split(new string[] { separator }, StringSplitOptions.RemoveEmptyEntries); + } } /// Splits a string into two strings diff --git a/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs b/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs index 5238183265..ee5ecc7928 100644 --- a/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs +++ b/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs @@ -115,8 +115,8 @@ public override string ApplicationPath { get { - //we know that relative paths always start with ~/ - //ApplicationPath should start with / + // We know that relative paths always start with ~/ + // ApplicationPath should start with / if (_relativeUrl != null && _relativeUrl.StartsWith("~/")) return _relativeUrl.Remove(0, 1); return null; @@ -137,10 +137,11 @@ public override string UserHostAddress } public override string RawUrl => this.ApplicationPath; - public override bool IsSecureConnection => false; + public override bool IsSecureConnection => _url?.Scheme?.EmptyNull().StartsWith("https", StringComparison.OrdinalIgnoreCase) == true; public override bool IsAuthenticated => false; public override string[] UserLanguages => new string[] { }; public override string UserAgent => "SmartStore.NET"; + public override bool IsLocal => false; public override RequestContext RequestContext { diff --git a/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs b/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs index 617aa3f5c3..9591f17d81 100644 --- a/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs +++ b/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs @@ -517,7 +517,7 @@ public long Size public DateTime LastUpdated { - get { return _fileInfo.LastWriteTime; } + get { return _fileInfo.LastWriteTimeUtc; } } public string Extension @@ -595,7 +595,7 @@ public string Name public DateTime LastUpdated { - get { return _directoryInfo.LastWriteTime; } + get { return _directoryInfo.LastWriteTimeUtc; } } public long Size diff --git a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs index d3615b73e7..8a934935e0 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using Autofac; @@ -26,7 +27,8 @@ public IContainer Container get { return _container; } } - public T Resolve(object key = null, ILifetimeScope scope = null) where T : class + [DebuggerStepThrough] + public T Resolve(object key = null, ILifetimeScope scope = null) where T : class { if (key == null) { @@ -36,22 +38,26 @@ public T Resolve(object key = null, ILifetimeScope scope = null) where T : cl return (scope ?? Scope()).ResolveKeyed(key); } - public T ResolveNamed(string name, ILifetimeScope scope = null) where T : class + [DebuggerStepThrough] + public T ResolveNamed(string name, ILifetimeScope scope = null) where T : class { return (scope ?? Scope()).ResolveNamed(name); } - public object Resolve(Type type, ILifetimeScope scope = null) + [DebuggerStepThrough] + public object Resolve(Type type, ILifetimeScope scope = null) { return (scope ?? Scope()).Resolve(type); } - public object ResolveNamed(string name, Type type, ILifetimeScope scope = null) + [DebuggerStepThrough] + public object ResolveNamed(string name, Type type, ILifetimeScope scope = null) { return (scope ?? Scope()).ResolveNamed(name, type); } - public T[] ResolveAll(object key = null, ILifetimeScope scope = null) + [DebuggerStepThrough] + public T[] ResolveAll(object key = null, ILifetimeScope scope = null) { if (key == null) { @@ -130,7 +136,8 @@ private bool TryResolveAll(Type[] types, out object[] instances, ILifetimeScope } } - public bool TryResolve(Type serviceType, ILifetimeScope scope, out object instance) + [DebuggerStepThrough] + public bool TryResolve(Type serviceType, ILifetimeScope scope, out object instance) { instance = null; @@ -144,7 +151,8 @@ public bool TryResolve(Type serviceType, ILifetimeScope scope, out object instan } } - public bool TryResolve(ILifetimeScope scope, out T instance) + [DebuggerStepThrough] + public bool TryResolve(ILifetimeScope scope, out T instance) { instance = default(T); @@ -178,6 +186,7 @@ public T InjectUnsetProperties(T instance, ILifetimeScope scope = null) return (scope ?? Scope()).InjectUnsetProperties(instance); } + [DebuggerStepThrough] public ILifetimeScope Scope() { var scope = _container.Resolve().GetLifetimeScope(null); diff --git a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeAccessor.cs b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeAccessor.cs index 703a26fd2a..1c7fed864c 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeAccessor.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeAccessor.cs @@ -61,6 +61,7 @@ public ILifetimeScope GetLifetimeScope(Action configurationAct _state.SetState((scope = BeginLifetimeScope(configurationAction))); //scope.CurrentScopeEnding += OnScopeEnding; } + return scope; } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/SmartStoreEngine.cs b/src/Libraries/SmartStore.Core/Infrastructure/SmartStoreEngine.cs index 18dcfc4222..0029454b28 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/SmartStoreEngine.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/SmartStoreEngine.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using System.Web.Mvc; @@ -148,6 +149,7 @@ public bool IsFullyInitialized set; } + [DebuggerStepThrough] public T Resolve(string name = null) where T : class { if (name.HasValue()) @@ -157,6 +159,7 @@ public T Resolve(string name = null) where T : class return ContainerManager.Resolve(); } + [DebuggerStepThrough] public object Resolve(Type type, string name = null) { if (name.HasValue()) @@ -166,7 +169,7 @@ public object Resolve(Type type, string name = null) return ContainerManager.Resolve(type); } - + [DebuggerStepThrough] public T[] ResolveAll() { return ContainerManager.ResolveAll(); diff --git a/src/Libraries/SmartStore.Core/Logging/log4net/Log4netLogger.cs b/src/Libraries/SmartStore.Core/Logging/log4net/Log4netLogger.cs index 3a251a58d3..6bd1f49e02 100644 --- a/src/Libraries/SmartStore.Core/Logging/log4net/Log4netLogger.cs +++ b/src/Libraries/SmartStore.Core/Logging/log4net/Log4netLogger.cs @@ -74,17 +74,12 @@ public void Log(LogLevel level, Exception exception, string message, object[] ar protected internal void TryAddExtendedThreadInfo(LoggingEvent loggingEvent) { - HttpRequest httpRequest = null; - bool httpRequestReady = HttpContext.Current?.Handler != null; + var isAppInitialized = EngineContext.Current.IsFullyInitialized; - try - { - if (httpRequestReady) - { - httpRequest = HttpContext.Current.Request; - } - } - catch + // Don't knowingly run into exception + var httpRequest = isAppInitialized ? HttpContext.Current.SafeGetHttpRequest() : null; + + if (httpRequest == null) { loggingEvent.Properties["CustomerId"] = DBNull.Value; loggingEvent.Properties["Url"] = DBNull.Value; @@ -104,7 +99,7 @@ protected internal void TryAddExtendedThreadInfo(LoggingEvent loggingEvent) { using (new ActionDisposable(() => props["sm:ThreadInfoAdded"] = true)) { - if (DataSettings.DatabaseIsInstalled() && EngineContext.Current.IsFullyInitialized) + if (DataSettings.DatabaseIsInstalled() && isAppInitialized) { var container = EngineContext.Current.ContainerManager; diff --git a/src/Libraries/SmartStore.Core/Money.cs b/src/Libraries/SmartStore.Core/Money.cs index e4bdf4a24f..33244b92ef 100644 --- a/src/Libraries/SmartStore.Core/Money.cs +++ b/src/Libraries/SmartStore.Core/Money.cs @@ -135,8 +135,14 @@ int IComparable.CompareTo(object obj) public override bool Equals(object obj) { - return Equals(obj as Money); - } + // Prevent stack overflow. + if (obj != null) + { + return Equals(obj as Money); + } + + return false; + } bool IEquatable.Equals(Money other) { diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginFinder.cs b/src/Libraries/SmartStore.Core/Plugins/PluginFinder.cs index f45a4a012c..4eddd033dd 100644 --- a/src/Libraries/SmartStore.Core/Plugins/PluginFinder.cs +++ b/src/Libraries/SmartStore.Core/Plugins/PluginFinder.cs @@ -35,7 +35,10 @@ private void LoadPlugins() foreach (var plugin in plugins) { _nameMap[plugin.SystemName] = plugin; - _assemblyMap[plugin.Assembly.Assembly] = plugin; + if (plugin.Assembly.Assembly != null) + { + _assemblyMap[plugin.Assembly.Assembly] = plugin; + } } } diff --git a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj index 60553396a0..e89e76c15d 100644 --- a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj +++ b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj @@ -1,5 +1,6 @@  - + + Debug AnyCPU diff --git a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs index 7573391f3d..e5bc5e5e48 100644 --- a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs +++ b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs @@ -44,7 +44,9 @@ public static class SmartStoreVersion new Version("3.0"), new Version("3.1"), new Version("3.1.5"), - new Version("3.2") + new Version("3.2"), + new Version("3.2.1"), + new Version("3.2.2") }; private const string HELP_BASEURL = "https://docs.smartstore.com/display/"; diff --git a/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs b/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs index 5a4a2253b2..0c90853524 100644 --- a/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs +++ b/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs @@ -8,6 +8,7 @@ using System.Threading; using SmartStore.Collections; using SmartStore.Core.Events; +using SmartStore.Core.Infrastructure.DependencyManagement; using SmartStore.Core.Logging; using SmartStore.Utilities; @@ -29,10 +30,10 @@ public partial class DefaultThemeRegistry : DisposableObject, IThemeRegistry public DefaultThemeRegistry(IEventPublisher eventPublisher, IApplicationEnvironment env, bool? enableMonitoring, string themesBasePath, bool autoLoadThemes) { - this._enableMonitoring = enableMonitoring ?? CommonHelper.GetAppSetting("sm:MonitorThemesFolder", true); - this._eventPublisher = eventPublisher; - this._env = env; - this._themesBasePath = themesBasePath.NullEmpty() ?? _env.ThemesFolder.RootPath; + _enableMonitoring = enableMonitoring ?? CommonHelper.GetAppSetting("sm:MonitorThemesFolder", true); + _eventPublisher = eventPublisher; + _env = env; + _themesBasePath = themesBasePath.NullEmpty() ?? _env.ThemesFolder.RootPath; Logger = NullLogger.Instance; diff --git a/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs b/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs index 8c7f9e1a70..1fb1f2240d 100644 --- a/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs @@ -19,8 +19,20 @@ namespace SmartStore.Utilities public static partial class CommonHelper { private static bool? _isDevEnvironment; - private readonly static Random _random = new Random(); + + [ThreadStatic] + private static Random _random; + private static Random GetRandomizer() + { + if (_random == null) + { + _random = new Random(); + } + + return _random; + } + /// /// Generate random digit code /// @@ -31,7 +43,7 @@ public static string GenerateRandomDigitCode(int length) var buffer = new int[length]; for (int i = 0; i < length; ++i) { - buffer[i] = _random.Next(10); + buffer[i] = GetRandomizer().Next(10); } return string.Join("", buffer); diff --git a/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs b/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs index 4c2b752d0b..721b1a38b8 100644 --- a/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs @@ -22,88 +22,111 @@ public static class SeoHelper [SuppressMessage("ReSharper", "PossibleNullReferenceException")] public static string GetSeName(string name, bool convertNonWesternChars, bool allowUnicodeChars, string charConversions = null) { - // Return empty value if text is null - if (name == null) return ""; - - const int maxlen = 400; - - if (charConversions != null && _userSeoCharacterTable == null) - { - InitializeUserSeoCharacterTable(charConversions); - } - - // Normalize - name = name.ToLowerInvariant(); - - var len = name.Length; - var sb = new StringBuilder(len); - var prevdash = false; - - char c; - - for (int i = 0; i < len; i++) - { - c = name[i]; - - if (charConversions != null && _userSeoCharacterTable != null && _userSeoCharacterTable.TryGetValue(c, out string userChar)) - { - sb.Append(userChar); - prevdash = false; - } - else if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '/') - { - sb.Append(c); - prevdash = (c == '_'); - } - else if (c == ' ' || c == ',' || c == '.' || c == '\\' || c == '-' || c == '_' || c == '=') - { - if (!prevdash && sb.Length > 0) - { - sb.Append('-'); - prevdash = true; - } - } - else - { - var category = CharUnicodeInfo.GetUnicodeCategory(c); - - if (category >= UnicodeCategory.ConnectorPunctuation && category <= UnicodeCategory.MathSymbol) - { - if (!prevdash && sb.Length > 0) - { - sb.Append('-'); - prevdash = true; - } - } - else if ((int)c >= 128) - { - var prevlen = sb.Length; - var c2 = c; - - if (convertNonWesternChars) - { - c2 = c.TryRemoveDiacritic(); - } - - if ((allowUnicodeChars && Char.IsLetterOrDigit(c2)) || (c2 >= 'a' && c2 <= 'z')) - { - sb.Append(c2); - } - - if (prevlen != sb.Length) prevdash = false; - } - } - - if (i >= maxlen) break; - } - - if (prevdash) - return sb.ToString().Substring(0, sb.Length - 1).Trim('/'); - else - return sb.ToString().Trim('/'); + return GetSeName(name, convertNonWesternChars, allowUnicodeChars, true, charConversions); } - public static void ResetUserSeoCharacterTable() + /// + /// Get SEO friendly string + /// + /// String to be converted + /// A value indicating whether non western chars should be converted + /// A value indicating whether Unicode chars are allowed + /// A value indicating whether the forward slash (/) is allowed. Should be false for physical file names. + /// Raw data of semicolon separated char conversions + /// SEO friendly string + [SuppressMessage("ReSharper", "PossibleNullReferenceException")] + public static string GetSeName( + string name, + bool convertNonWesternChars, + bool allowUnicodeChars, + bool allowForwardSlash, + string charConversions = null) + { + // Return empty value if text is null + if (name == null) return ""; + + const int maxlen = 400; + + if (charConversions != null && _userSeoCharacterTable == null) + { + InitializeUserSeoCharacterTable(charConversions); + } + + // Normalize + name = name.ToLowerInvariant(); + + var len = name.Length; + var sb = new StringBuilder(len); + var prevdash = false; + + char c; + + for (int i = 0; i < len; i++) + { + c = name[i]; + + if (charConversions != null && _userSeoCharacterTable != null && _userSeoCharacterTable.TryGetValue(c, out string userChar)) + { + sb.Append(userChar); + prevdash = false; + } + else if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '/') + { + if (c != '/' || allowForwardSlash) + { + sb.Append(c); + prevdash = (c == '_'); + } + } + else if (c == ' ' || c == ',' || c == '.' || c == '\\' || c == '-' || c == '_' || c == '=') + { + if (!prevdash && sb.Length > 0) + { + sb.Append('-'); + prevdash = true; + } + } + else + { + var category = CharUnicodeInfo.GetUnicodeCategory(c); + + if (category >= UnicodeCategory.ConnectorPunctuation && category <= UnicodeCategory.MathSymbol) + { + if (!prevdash && sb.Length > 0) + { + sb.Append('-'); + prevdash = true; + } + } + else if ((int)c >= 128) + { + var prevlen = sb.Length; + var c2 = c; + + if (convertNonWesternChars) + { + c2 = c.TryRemoveDiacritic(); + } + + if ((allowUnicodeChars && Char.IsLetterOrDigit(c2)) || (c2 >= 'a' && c2 <= 'z')) + { + sb.Append(c2); + } + + if (prevlen != sb.Length) prevdash = false; + } + } + + if (i >= maxlen) break; + } + + if (prevdash) + return sb.ToString().Substring(0, sb.Length - 1).Trim('/'); + else + return sb.ToString().Trim('/'); + } + + public static void ResetUserSeoCharacterTable() { if (_userSeoCharacterTable != null) { diff --git a/src/Libraries/SmartStore.Core/WebHelper.cs b/src/Libraries/SmartStore.Core/WebHelper.cs index 5d501165ca..3a94e1a417 100644 --- a/src/Libraries/SmartStore.Core/WebHelper.cs +++ b/src/Libraries/SmartStore.Core/WebHelper.cs @@ -45,27 +45,15 @@ public WebHelper(HttpContextBase httpContext) _httpContext = httpContext; } - private HttpRequestBase GetHttpRequest() - { - try - { - return _httpContext.Request; - } - catch - { - return null; - } - } - public virtual string GetUrlReferrer() { - return GetHttpRequest()?.UrlReferrer?.ToString() ?? string.Empty; + return _httpContext.SafeGetHttpRequest()?.UrlReferrer?.ToString() ?? string.Empty; } public virtual string GetClientIdent() { var ipAddress = this.GetCurrentIpAddress(); - var userAgent = GetHttpRequest()?.UserAgent.EmptyNull(); + var userAgent = _httpContext.SafeGetHttpRequest()?.UserAgent.EmptyNull(); if (ipAddress.HasValue() && userAgent.HasValue()) { @@ -82,7 +70,7 @@ public virtual string GetCurrentIpAddress() return _ipAddress; } - var httpRequest = GetHttpRequest(); + var httpRequest = _httpContext.SafeGetHttpRequest(); if (httpRequest == null) { return string.Empty; @@ -103,29 +91,43 @@ public virtual string GetCurrentIpAddress() }; string result = null; + IPAddress ipv6 = null; foreach (var key in keysToCheck) { var ipString = vars[key]; - if (ipString.HasValue()) + + if (!string.IsNullOrEmpty(ipString)) { var arrStrings = ipString.Split(','); - // Take the last entry - ipString = arrStrings[arrStrings.Length - 1].Trim(); - IPAddress address; - if (IPAddress.TryParse(ipString, out address)) - { - result = ipString; - break; - } + // Iterate list from end to start (IPv6 addresses usually have precedence) + for (int i = arrStrings.Length - 1; i >= 0; i--) + { + ipString = arrStrings[i].Trim(); + + if (IPAddress.TryParse(ipString, out var address)) + { + if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + ipv6 = address; + } + else + { + result = ipString; + break; + } + } + } } } - if (result == "::1") - { - result = "127.0.0.1"; - } + if (string.IsNullOrEmpty(result) && ipv6 != null) + { + result = ipv6.ToString() == "::1" + ? "127.0.0.1" + : ipv6.MapToIPv4().ToString(); + } return (_ipAddress = result.EmptyNull()); } @@ -139,7 +141,7 @@ public virtual string GetThisPageUrl(bool includeQueryString) public virtual string GetThisPageUrl(bool includeQueryString, bool useSsl) { string url = string.Empty; - var httpRequest = GetHttpRequest(); + var httpRequest = _httpContext.SafeGetHttpRequest(); if (httpRequest == null) return url; @@ -181,7 +183,7 @@ public virtual bool IsCurrentConnectionSecured() if (!_isCurrentConnectionSecured.HasValue) { _isCurrentConnectionSecured = false; - var httpRequest = GetHttpRequest(); + var httpRequest = _httpContext.SafeGetHttpRequest(); if (httpRequest != null) { _isCurrentConnectionSecured = httpRequest.IsSecureConnection(); @@ -193,7 +195,7 @@ public virtual bool IsCurrentConnectionSecured() public virtual string ServerVariables(string name) { - return GetHttpRequest()?.ServerVariables[name].EmptyNull(); + return _httpContext.SafeGetHttpRequest()?.ServerVariables[name].EmptyNull(); } [SuppressMessage("ReSharper", "UnusedMember.Local")] @@ -328,7 +330,7 @@ public virtual string GetStoreLocation(bool useSsl) result = result.Substring(0, result.Length - 1); } - var httpRequest = GetHttpRequest(); + var httpRequest = _httpContext.SafeGetHttpRequest(); if (httpRequest != null) { @@ -421,7 +423,7 @@ public virtual string RemoveQueryString(string url, string queryString) public virtual T QueryString(string name) { - var queryParam = GetHttpRequest()?.QueryString[name]; + var queryParam = _httpContext.SafeGetHttpRequest()?.QueryString[name]; if (!string.IsNullOrEmpty(queryParam)) { diff --git a/src/Libraries/SmartStore.Data/Caching/DbCacheExtensions.cs b/src/Libraries/SmartStore.Data/Caching/DbCacheExtensions.cs index cdb68ddb14..e055db6a8c 100644 --- a/src/Libraries/SmartStore.Data/Caching/DbCacheExtensions.cs +++ b/src/Libraries/SmartStore.Data/Caching/DbCacheExtensions.cs @@ -115,7 +115,7 @@ internal static DbCacheEntry GetCacheEntry(this IQueryable source, string { return null; } - + if (!cache.RequestTryGet(cacheKey.Key, out var entry)) { entry = cache.RequestPut(cacheKey.Key, valueFactory(), cacheKey.AffectedEntitySets); diff --git a/src/Libraries/SmartStore.Data/Mapping/Catalog/CategoryMap.cs b/src/Libraries/SmartStore.Data/Mapping/Catalog/CategoryMap.cs index 4920d45081..c69a6f88d4 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Catalog/CategoryMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Catalog/CategoryMap.cs @@ -12,7 +12,8 @@ public CategoryMap() this.Property(c => c.Name).IsRequired().HasMaxLength(400); this.Property(c => c.FullName).HasMaxLength(400); this.Property(c => c.BottomDescription).IsMaxLength(); - this.Property(c => c.Description).IsMaxLength(); + this.Property(c => c.ExternalLink).HasMaxLength(255).IsOptional(); + this.Property(c => c.Description).IsMaxLength(); this.Property(c => c.MetaKeywords).HasMaxLength(400); this.Property(c => c.MetaTitle).HasMaxLength(400); this.Property(c => c.PageSizeOptions).HasMaxLength(200).IsOptional(); diff --git a/src/Libraries/SmartStore.Data/Mapping/Directory/QuantityUnitMap.cs b/src/Libraries/SmartStore.Data/Mapping/Directory/QuantityUnitMap.cs index 7bd783bcad..a840571e67 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Directory/QuantityUnitMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Directory/QuantityUnitMap.cs @@ -7,12 +7,13 @@ public partial class QuantityUnitMap : EntityTypeConfiguration { public QuantityUnitMap() { - this.ToTable("QuantityUnit"); - this.HasKey(c => c.Id); - this.Property(c => c.Name).IsRequired().HasMaxLength(50); - this.Property(c => c.Description).HasMaxLength(50); - this.Property(c => c.DisplayLocale).HasMaxLength(50); - this.Property(c => c.DisplayOrder); + ToTable("QuantityUnit"); + HasKey(c => c.Id); + Property(c => c.Name).IsRequired().HasMaxLength(50); + Property(c => c.NamePlural).IsRequired().HasMaxLength(50); + Property(c => c.Description).HasMaxLength(50); + Property(c => c.DisplayLocale).HasMaxLength(50); + Property(c => c.DisplayOrder); } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/201905271110370_V321Resources.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201905271110370_V321Resources.Designer.cs new file mode 100644 index 0000000000..a194593e7c --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201905271110370_V321Resources.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 V321Resources : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(V321Resources)); + + string IMigrationMetadata.Id + { + get { return "201905271110370_V321Resources"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201905271110370_V321Resources.cs b/src/Libraries/SmartStore.Data/Migrations/201905271110370_V321Resources.cs new file mode 100644 index 0000000000..89ade79f68 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201905271110370_V321Resources.cs @@ -0,0 +1,35 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + using SmartStore.Data.Setup; + + public partial class V321Resources : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + } + + public override void Down() + { + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + context.SaveChanges(); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + 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 verfgbaren Sprachen fr Version {0} gefunden. Auf translate.smartstore.com finden Sie weitere Details zu verfgbaren Ressourcen."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201905271110370_V321Resources.resx b/src/Libraries/SmartStore.Data/Migrations/201905271110370_V321Resources.resx new file mode 100644 index 0000000000..eea8366a2c --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201905271110370_V321Resources.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/201906252008551_QuantityUnitNamePlural.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201906252008551_QuantityUnitNamePlural.Designer.cs new file mode 100644 index 0000000000..186f72e27d --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201906252008551_QuantityUnitNamePlural.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 QuantityUnitNamePlural : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(QuantityUnitNamePlural)); + + string IMigrationMetadata.Id + { + get { return "201906252008551_QuantityUnitNamePlural"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201906252008551_QuantityUnitNamePlural.cs b/src/Libraries/SmartStore.Data/Migrations/201906252008551_QuantityUnitNamePlural.cs new file mode 100644 index 0000000000..313530a024 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201906252008551_QuantityUnitNamePlural.cs @@ -0,0 +1,170 @@ +namespace SmartStore.Data.Migrations +{ + using System.Collections.Generic; + using System.Data.Entity.Migrations; + using System.Linq; + using SmartStore.Core.Data; + using SmartStore.Core.Domain.Directory; + using SmartStore.Core.Domain.Localization; + using SmartStore.Data.Setup; + + public partial class QuantityUnitNamePlural : DbMigration, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.QuantityUnit", "NamePlural", c => c.String(nullable: false, maxLength: 50, defaultValue: "" )); + } + + public override void Down() + { + DropColumn("dbo.QuantityUnit", "NamePlural"); + } + + public bool RollbackOnFailure => true; + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + context.SaveChanges(); + + if (!DataSettings.DatabaseIsInstalled()) + { + return; + } + + var quPluralDe = new Dictionary + { + { "Stck", "Stck" }, + { "Schachtel", "Schachteln" }, + { "Paket", "Pakete" }, + { "Palette", "Paletten" }, + { "Kiste", "Kisten" }, + { "Einheit", "Einheiten" }, + { "Sack", "Scke" }, + { "Tte", "Tten" }, + { "Dose", "Dosen" }, + { "Stange", "Stangen" }, + { "Flasche", "Flaschen" }, + { "Glas", "Glser" }, + { "Bund", "Bnde" }, + { "Rolle", "Rollen" }, + { "Fass", "Fsser" }, + { "Set", "Sets" } + }; + + var quPluralEn = new Dictionary + { + { "Piece", "Pieces" }, + { "Box", "Boxes" }, + { "Parcel", "Parcels" }, + { "Palette", "Pallets" }, + { "Unit", "Units" }, + { "Sack", "Sacks" }, + { "Bag", "Bags" }, + { "Can", "Cans" }, + { "Tin", "Tins" }, + { "Packet", "Packets" }, + { "Package", "Packages" }, + { "Bar", "Bars" }, + { "Bottle", "Bottles" }, + { "Glass", "Glasses" }, + { "Bunch", "Bunches" }, + { "Roll", "Rolls" }, + { "Cup", "Cups" }, + { "Bundle", "Bundles" }, + { "Barrel", "Barrels" }, + { "Set", "Sets" }, + { "Bucket", "Buckets" } + }; + + using (var scope = new DbContextScope(ctx: context, validateOnSave: false, hooksEnabled: false, autoCommit: false)) + { + var languages = context.Set().ToDictionary(x => x.Id, x => x); + var defaultLang = languages.Values.OrderBy(x => x.DisplayOrder).First(); + + // Quantity units. + var quantityUnits = context.Set().ToList(); + if (quantityUnits.Any()) + { + foreach (var quantityUnit in quantityUnits) + { + var name = quantityUnit.Name.EmptyNull(); + string namePlural = null; + + if (defaultLang.UniqueSeoCode.IsCaseInsensitiveEqual("de")) + { + quPluralDe.TryGetValue(name, out namePlural); + } + else if (defaultLang.UniqueSeoCode.IsCaseInsensitiveEqual("en")) + { + quPluralEn.TryGetValue(name, out namePlural); + } + + quantityUnit.NamePlural = namePlural.NullEmpty() ?? name; + } + + scope.Commit(); + } + + // Localized properties. + var lpSet = context.Set(); + var pluralCount = lpSet + .Where(x => x.LocaleKeyGroup == "QuantityUnit" && x.LocaleKey == "NamePlural") + .Count(); + + if (pluralCount == 0) + { + var localizedProperties = lpSet + .Where(x => x.LocaleKeyGroup == "QuantityUnit" && x.LocaleKey == "Name") + .ToList(); + + if (localizedProperties.Any()) + { + foreach (var lp in localizedProperties) + { + if (languages.TryGetValue(lp.LanguageId, out var language)) + { + var name = lp.LocaleValue.EmptyNull(); + string namePlural = null; + + if (language.UniqueSeoCode.IsCaseInsensitiveEqual("de")) + { + quPluralDe.TryGetValue(name, out namePlural); + } + else if (language.UniqueSeoCode.IsCaseInsensitiveEqual("en")) + { + quPluralEn.TryGetValue(name, out namePlural); + } + + if (namePlural.HasValue()) + { + lpSet.Add(new LocalizedProperty + { + EntityId = lp.EntityId, + LanguageId = lp.LanguageId, + LocaleKeyGroup = lp.LocaleKeyGroup, + LocaleKey = "NamePlural", + LocaleValue = namePlural + }); + } + } + } + + scope.Commit(); + } + } + } + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Common.Plural", "Plural", "Mehrzahl"); + + builder.AddOrUpdate("Admin.Configuration.QuantityUnit.Fields.NamePlural", + "Name plural", + "Name Mehrzahl", + "Sets the name in plural. Example: \"Barrels\" for the unit \"Barrel\".", + "Legt den Namen in Mehrzahl fest. Beispiel: \"Fsser\" fr die Verpackungseinheit \"Fass\"."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201906252008551_QuantityUnitNamePlural.resx b/src/Libraries/SmartStore.Data/Migrations/201906252008551_QuantityUnitNamePlural.resx new file mode 100644 index 0000000000..887e09fc9d --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201906252008551_QuantityUnitNamePlural.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/201907032251575_CategoryExternalLink.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201907032251575_CategoryExternalLink.Designer.cs new file mode 100644 index 0000000000..784b2cb7ce --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201907032251575_CategoryExternalLink.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 CategoryExternalLink : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(CategoryExternalLink)); + + string IMigrationMetadata.Id + { + get { return "201907032251575_CategoryExternalLink"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201907032251575_CategoryExternalLink.cs b/src/Libraries/SmartStore.Data/Migrations/201907032251575_CategoryExternalLink.cs new file mode 100644 index 0000000000..708ca38b83 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201907032251575_CategoryExternalLink.cs @@ -0,0 +1,18 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class CategoryExternalLink : DbMigration + { + public override void Up() + { + AddColumn("dbo.Category", "ExternalLink", c => c.String(maxLength: 255)); + } + + public override void Down() + { + DropColumn("dbo.Category", "ExternalLink"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201907032251575_CategoryExternalLink.resx b/src/Libraries/SmartStore.Data/Migrations/201907032251575_CategoryExternalLink.resx new file mode 100644 index 0000000000..3a1de6db32 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201907032251575_CategoryExternalLink.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/201908150749388_V322Resources.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.Designer.cs new file mode 100644 index 0000000000..be8c91d1d2 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.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 V322Resources : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(V322Resources)); + + string IMigrationMetadata.Id + { + get { return "201908150749388_V322Resources"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.cs b/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.cs new file mode 100644 index 0000000000..12a38b031e --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.cs @@ -0,0 +1,69 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + using SmartStore.Data.Setup; + + public partial class V322Resources : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + } + + public override void Down() + { + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + context.SaveChanges(); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + 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 verfgbaren Sprachen fr Version {0} gefunden. Auf translate.smartstore.com finden Sie weitere Details zu verfgbaren Ressourcen."); + + builder.AddOrUpdate("Checkout.OrderCompletes", + "Your order will be completed.", + "Ihre Bestellung wird abgeschlossen."); + + builder.AddOrUpdate("Admin.Catalog.Attributes.CheckoutAttributes.Fields.TextPrompt", + "Text prompt", + "Text Eingabeaufforderung", + "Specifies the prompt text.", + "Legt den Text zur Eingabeaufforderung fest."); + + builder.AddOrUpdate("Admin.Catalog.Products.ProductVariantAttributes.Attributes.Fields.TextPrompt", + "Text prompt", + "Text Eingabeaufforderung"); + + builder.AddOrUpdate("Admin.Catalog.Categories.Fields.ExternalLink", + "External link", + "Externer Link", + "Alternative external link for this category in the main menu and in category listings. For example, to a landing page that contains a back link to the category.", + "Abweichender, externer Verweis fr diese Warengruppe im Hauptmen und in Warengruppen-Listings. Z.B. auf eine Landingpage, die einen Rckverweis auf die Warengruppe enthlt."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Title", + "Title", + "Titel", + "Specifies the title. Please keep in mind that this title is not displayed in all design templates.", + "Legt den Titel fest. Bitte beachten Sie, dass dieser Titel nicht bei allen Design-Vorlagen ausgegeben wird."); + + builder.AddOrUpdate("Admin.ContentManagement.Menus.Item.InvalidRouteValues", + "Please check the link data. No link can be created on the basis of your input.", + "Bitte berprfen Sie die Link-Daten. Auf Basis Ihrer Eingabe kann kein Link erzeugt werden."); + + builder.AddOrUpdate("Admin.Packaging.IsIncompatible", + "The package is not compatible the current app version {0}. Please update Smartstore.NET or install another version of this package.", + "Das Paket ist nicht kompatibel mit der aktuellen Programmversion {0}. Bitte aktualisieren Sie Smartstore.NET oder nutzen Sie eine andere, kompatible Paket-Version."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.resx b/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.resx new file mode 100644 index 0000000000..a3562fc0f2 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201908150749388_V322Resources.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 4b88dbe4c6..18a610b862 100644 --- a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs +++ b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs @@ -30,15 +30,17 @@ public MigrationsConfiguration() public void SeedDatabase(SmartObjectContext context) { - Seed(context); + using (var scope = new DbContextScope(context, hooksEnabled: false)) + { + Seed(context); + scope.Commit(); + } } protected override void Seed(SmartObjectContext context) { context.MigrateLocaleResources(MigrateLocaleResources); MigrateSettings(context); - - context.SaveChanges(); } public void MigrateSettings(SmartObjectContext context) @@ -48,7 +50,7 @@ public void MigrateSettings(SmartObjectContext context) public void MigrateLocaleResources(LocaleResourcesBuilder builder) { - - } + + } } } diff --git a/src/Libraries/SmartStore.Data/ObjectContextBase.SaveChanges.cs b/src/Libraries/SmartStore.Data/ObjectContextBase.SaveChanges.cs index d3658af812..3faed3ad16 100644 --- a/src/Libraries/SmartStore.Data/ObjectContextBase.SaveChanges.cs +++ b/src/Libraries/SmartStore.Data/ObjectContextBase.SaveChanges.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Data.Entity; using System.Data.Entity.Infrastructure; using System.Data.Entity.Validation; using System.Linq; @@ -9,9 +8,7 @@ using System.Threading; using System.Threading.Tasks; 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; diff --git a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs index 00adad3721..25ec8fc33c 100644 --- a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs +++ b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs @@ -202,42 +202,42 @@ public IList MeasureWeights() protected virtual string TaxNameBooks { - get { return "Books"; } + get => "Books"; } protected virtual string TaxNameDigitalGoods { - get { return "Downloadable Products"; } + get => "Downloadable Products"; } protected virtual string TaxNameJewelry { - get { return "Jewelry"; } + get => "Jewelry"; } protected virtual string TaxNameApparel { - get { return "Apparel & Shoes"; } + get => "Apparel & Shoes"; } protected virtual string TaxNameFood { - get { return "Food"; } + get => "Food"; } protected virtual string TaxNameElectronics { - get { return "Electronics & Software"; } + get => "Electronics & Software"; } protected virtual string TaxNameTaxFree { - get { return "Tax free"; } + get => "Tax free"; } public virtual decimal[] FixedTaxRates { - get { return new decimal[] { 0, 0, 0, 0, 0 }; } + get => new decimal[] { 0, 0, 0, 0, 0 }; } public IList TaxCategories() @@ -3975,6 +3975,11 @@ public IList Topics() public IList Settings() { + var defaultDimensionId = _ctx.Set().FirstOrDefault(x => x.SystemKeyword == "inch")?.Id ?? 0; + var defaultWeightId = _ctx.Set().FirstOrDefault(x => x.SystemKeyword == "lb")?.Id ?? 0; + var defaultLanguageId = _ctx.Set().FirstOrDefault()?.Id ?? 0; + var defaultEmailAccountId = _ctx.Set().FirstOrDefault()?.Id ?? 0; + var entities = new List { new PdfSettings @@ -3983,60 +3988,60 @@ public IList Settings() new CommonSettings { }, - new SeoSettings() + new SeoSettings { }, - new SocialSettings() + new SocialSettings { }, - new AdminAreaSettings() + new AdminAreaSettings { }, - new CatalogSettings() + new CatalogSettings { }, - new LocalizationSettings() + new LocalizationSettings { - DefaultAdminLanguageId = _ctx.Set().First().Id + DefaultAdminLanguageId = defaultLanguageId }, - new CustomerSettings() + new CustomerSettings { }, - new AddressSettings() + new AddressSettings { }, - new MediaSettings() + new MediaSettings { }, - new StoreInformationSettings() + new StoreInformationSettings { }, - new RewardPointsSettings() + new RewardPointsSettings { }, - new CurrencySettings() + new CurrencySettings { }, - new MeasureSettings() + new MeasureSettings { - BaseDimensionId = _ctx.Set().Where(m => m.SystemKeyword == "inch").Single().Id, - BaseWeightId = _ctx.Set().Where(m => m.SystemKeyword == "lb").Single().Id, + BaseDimensionId = defaultDimensionId, + BaseWeightId = defaultWeightId, }, - new ShoppingCartSettings() + new ShoppingCartSettings { }, - new OrderSettings() + new OrderSettings { }, - new SecuritySettings() + new SecuritySettings { }, - new ShippingSettings() + new ShippingSettings { }, - new PaymentSettings() + new PaymentSettings { - ActivePaymentMethodSystemNames = new List() + ActivePaymentMethodSystemNames = new List { "Payments.CashOnDelivery", "Payments.Manual", @@ -4044,23 +4049,23 @@ public IList Settings() "Payments.Prepayment" } }, - new TaxSettings() + new TaxSettings { }, - new BlogSettings() + new BlogSettings { }, - new NewsSettings() + new NewsSettings { }, - new ForumSettings() + new ForumSettings { }, - new EmailAccountSettings() + new EmailAccountSettings { - DefaultEmailAccountId = _ctx.Set().First().Id + DefaultEmailAccountId = defaultEmailAccountId }, - new ThemeSettings() + new ThemeSettings { } }; @@ -9045,6 +9050,77 @@ public IList CategoriesSecondLevel() #region category definitions + #region new + + var categoryFashionJackets = new Category + { + Name = "Jackets", + Alias = "Jackets", + CategoryTemplateId = categoryTemplateInGridAndLines.Id, + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "category_jackets.jpg"), "image/png", GetSeName("Jackets")), + Published = true, + ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, + DisplayOrder = 1, + MetaTitle = "Jackets", + ShowOnHomePage = true + }; + + var categoryFashionLeatherJackets = new Category + { + Name = "Leather jackets", + Alias = "Leather jackets", + CategoryTemplateId = categoryTemplateInGridAndLines.Id, + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "category_leather_jackets.jpg"), "image/png", GetSeName("Leather jackets")), + Published = true, + ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, + DisplayOrder = 1, + MetaTitle = "Leather jackets", + ShowOnHomePage = true + }; + + var categoryFashionShoes = new Category + { + Name = "Shoes", + Alias = "Shoes", + CategoryTemplateId = categoryTemplateInGridAndLines.Id, + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "category_shoes.png"), "image/png", GetSeName("Shoes")), + Published = true, + ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, + DisplayOrder = 1, + MetaTitle = "Shoes", + ShowOnHomePage = true + }; + + var categoryFashionTrousers = new Category + { + Name = "Trousers", + Alias = "Pants", + CategoryTemplateId = categoryTemplateInGridAndLines.Id, + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "category_trousers.png"), "image/png", GetSeName("Trousers")), + Published = true, + ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, + DisplayOrder = 1, + MetaTitle = "Trousers", + ShowOnHomePage = true + }; + + var categoryFashionTracksuits = new Category + { + Name = "Tracksuits", + Alias = "Tracksuits", + CategoryTemplateId = categoryTemplateInGridAndLines.Id, + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "category_tracksuit.png"), "image/png", GetSeName("Tracksuits")), + Published = true, + ParentCategoryId = _ctx.Set().Where(x => x.MetaTitle == "Fashion").First().Id, + DisplayOrder = 1, + MetaTitle = "Tracksuits", + ShowOnHomePage = true + }; + + #endregion + + + var categorySportsGolf = new Category { Name = "Golf", @@ -9174,7 +9250,8 @@ public IList CategoriesSecondLevel() var entities = new List { categorySportsSunglasses,categorySportsSoccer, categorySportsBasketball,categorySportsGolf, categoryBooksSpiegel, categoryBooksCookAndEnjoy, - categoryGamingAccessories, categoryGamingGames + categoryGamingAccessories, categoryGamingGames, categoryFashionJackets, categoryFashionLeatherJackets, categoryFashionShoes, categoryFashionTrousers, + categoryFashionTracksuits }; this.Alter(entities); @@ -9192,11 +9269,37 @@ public IList Manufacturers() //var categoryTemplateInGridAndLines = // this.CategoryTemplates().Where(pt => pt.Name == "Products in Grid or Lines").FirstOrDefault(); - //categories + // + + #region Jack Wolfskin + + var manufacturerJackWolfskin = new Manufacturer + { + Name = "Jack-Wolfskin", + ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "manufacturer_Jack_Wolfskin.png"), "image/png", GetSeName("Jack Wolfskin")), + Published = true, + DisplayOrder = 1 + }; + + #endregion + + #region Mey & Edlich + var manufacturerMeyAndEdlich = new Manufacturer + { + Name = "Mey-And-Edlich", + ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, + Picture = CreatePicture(File.ReadAllBytes(sampleImagesPath + "manufacturer_MeyAndEdlich.jpg"), "image/png", GetSeName("Mey Edlich")), + Published = true, + DisplayOrder = 1 + }; + + #endregion + #region EA Sports - var manufacturerWarnerHome = new Manufacturer + var manufacturerEASports = new Manufacturer { Name = "EA Sports", ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, @@ -9209,7 +9312,7 @@ public IList Manufacturers() #region Warner Home Video Games - var manufacturerEASports = new Manufacturer + var manufacturerWarnerHome = new Manufacturer { Name = "Warner Home Video Games", ManufacturerTemplateId = manufacturerTemplateInGridAndLines.Id, @@ -9611,17 +9714,19 @@ public IList Manufacturers() #endregion Ubisoft var entities = new List - { - manufacturerEASports,manufacturerWarnerHome,manufacturerBreitling,manufacturerTissot,manufacturerSeiko, manufacturerTitleist,manufacturerApple,manufacturerSamsung,manufacturerLG,manufacturerTrekStor, manufacturerWesternDigital,manufacturerDell, manufacturerMSI, + { + manufacturerEASports,manufacturerWarnerHome,manufacturerBreitling,manufacturerTissot,manufacturerSeiko, manufacturerTitleist, manufacturerApple, + manufacturerSamsung,manufacturerLG,manufacturerTrekStor, manufacturerWesternDigital,manufacturerDell, manufacturerMSI, manufacturerCanon, manufacturerCasio, manufacturerPanasonic, manufacturerBlackBerry, manufacturerHTC, manufacturerFestina, manufacturerCertina, - manufacturerHP, manufacturerAcer, manufacturerSony, manufacturerUbisoft,manufacturerOakley,manufacturerRayban,manufacturerAdidas, manufacturerWilson,manufacturerPuma,manufacturerNike + manufacturerHP, manufacturerAcer, manufacturerSony, manufacturerUbisoft, manufacturerOakley, manufacturerRayban, manufacturerAdidas, manufacturerWilson, + manufacturerPuma,manufacturerNike, manufacturerMeyAndEdlich, manufacturerJackWolfskin, manufacturerMicrosoft }; this.Alter(entities); return entities; } - private List GetFashionProducts() + private List GetFashionProducts(Dictionary specAttributes) { var result = new List(); var productTemplateSimple = _ctx.Set().First(x => x.ViewPath == "Product"); @@ -9678,7 +9783,7 @@ private List GetFashionProducts() AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, - SpecificationAttributeOption = specOptionCotton + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) }); result.Add(converseAllStar); @@ -9726,8 +9831,8 @@ private List GetFashionProducts() Price = 29.00M }); - var shirtMeccanicaImages = new string[] { "product_shirt_meccanica_red_1.jpg", "product_shirt_meccanica_red_2.jpg", "product_shirt_meccanica_red_3.jpg", - "product_shirt_meccanica_red_4.jpg", "product_shirt_meccanica_black_1.jpg", "product_shirt_meccanica_black_2.jpg", "product_shirt_meccanica_black_3.jpg" }; + var shirtMeccanicaImages = new string[] { "product_shirt_meccanica_black_1.jpg", "product_shirt_meccanica_black_2.jpg", "product_shirt_meccanica_black_3.jpg", + "product_shirt_meccanica_red_1.jpg", "product_shirt_meccanica_red_2.jpg", "product_shirt_meccanica_red_3.jpg", "product_shirt_meccanica_red_4.jpg" }; for (var i = 0; i < shirtMeccanicaImages.Length; ++i) { @@ -9743,8 +9848,8 @@ private List GetFashionProducts() AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, - SpecificationAttributeOption = specOptionCotton - }); + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) + }); result.Add(shirtMeccanica); @@ -9783,8 +9888,8 @@ private List GetFashionProducts() DisplayOrder = 1 }); - var ladiesJacketImages = new string[] { "product_ladies_jacket_red.jpg", "product_ladies_jacket_orange.jpg", "product_ladies_jacket_green.jpg", - "product_ladies_jacket_blue.jpg", "product_ladies_jacket_navy.jpg", "product_ladies_jacket_silver.jpg", "product_ladies_jacket_black.jpg" }; + var ladiesJacketImages = new string[] { "product_ladies_jacket_silver.jpg", "product_ladies_jacket_black.jpg", "product_ladies_jacket_red.jpg", + "product_ladies_jacket_orange.jpg", "product_ladies_jacket_green.jpg", "product_ladies_jacket_blue.jpg", "product_ladies_jacket_navy.jpg", }; for (var i = 0; i < ladiesJacketImages.Length; ++i) { @@ -9800,7 +9905,7 @@ private List GetFashionProducts() AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, - SpecificationAttributeOption = _ctx.Set().First(x => x.DisplayOrder == 8).SpecificationAttributeOptions.First(x => x.DisplayOrder == 11) + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 11) }); result.Add(ladiesJacket); @@ -9846,30 +9951,23 @@ private List GetFashionProducts() AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, - SpecificationAttributeOption = specOptionCotton - }); + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) + }); result.Add(clarkJeans); - - return result; + return result; } - private List GetFurnitureProducts() + private List GetFurnitureProducts(Dictionary specAttributes) { var result = new List(); var productTemplateSimple = _ctx.Set().First(x => x.ViewPath == "Product"); var thirdDeliveryTime = _ctx.Set().First(x => x.DisplayOrder == 2); var furnitureCategory = _ctx.Set().First(x => x.MetaTitle == "Furniture"); - var specOptionLeather = _ctx.Set().First(x => x.DisplayOrder == 8).SpecificationAttributeOptions.First(x => x.DisplayOrder == 5); - var specOptionWood = _ctx.Set().First(x => x.DisplayOrder == 8).SpecificationAttributeOptions.First(x => x.DisplayOrder == 13); - var specOptionPlastic = _ctx.Set().First(x => x.DisplayOrder == 8).SpecificationAttributeOptions.First(x => x.DisplayOrder == 3); - var specOptionGlass = _ctx.Set().First(x => x.DisplayOrder == 8).SpecificationAttributeOptions.First(x => x.DisplayOrder == 14); - var specOptionSteel = _ctx.Set().First(x => x.DisplayOrder == 8).SpecificationAttributeOptions.First(x => x.DisplayOrder == 1); - var specOptionAluminium = _ctx.Set().First(x => x.DisplayOrder == 8).SpecificationAttributeOptions.First(x => x.DisplayOrder == 4); - // Le Corbusier LC 6 table - var corbusierTable = new Product + // Le Corbusier LC 6 table + var corbusierTable = new Product { ProductType = ProductType.SimpleProduct, VisibleIndividually = true, @@ -9926,15 +10024,15 @@ private List GetFurnitureProducts() AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, - SpecificationAttributeOption = specOptionSteel - }); + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); corbusierTable.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 2, - SpecificationAttributeOption = specOptionGlass - }); + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 14) + }); result.Add(corbusierTable); @@ -9994,15 +10092,15 @@ private List GetFurnitureProducts() AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, - SpecificationAttributeOption = specOptionPlastic - }); + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) + }); ballChair.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 2, - SpecificationAttributeOption = specOptionLeather - }); + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 5) + }); result.Add(ballChair); @@ -10070,21 +10168,21 @@ private List GetFurnitureProducts() AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, - SpecificationAttributeOption = specOptionWood - }); + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 13) + }); loungeChair.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 2, - SpecificationAttributeOption = specOptionLeather - }); + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 5) + }); loungeChair.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 3, - SpecificationAttributeOption = specOptionAluminium + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 4) }); result.Add(loungeChair); @@ -10142,29 +10240,154 @@ private List GetFurnitureProducts() AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, - SpecificationAttributeOption = specOptionLeather - }); + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 5) + }); result.Add(cubeChair); return result; } - public IList Products() - { - #region definitions - - var productTemplate = _ctx.Set().First(x => x.ViewPath == "Product"); - var firstDeliveryTime = _ctx.Set().First(sa => sa.DisplayOrder == 0); - var secondDeliveryTime = _ctx.Set().First(sa => sa.DisplayOrder == 1); - var thirdDeliveryTime = _ctx.Set().First(sa => sa.DisplayOrder == 2); + public IList Products() + { var specialPriceEndDate = DateTime.UtcNow.AddMonths(1); - #endregion definitions + var productTemplate = _ctx.Set().First(x => x.ViewPath == "Product"); + var firstDeliveryTime = _ctx.Set().First(x => x.DisplayOrder == 0); + var secondDeliveryTime = _ctx.Set().First(x => x.DisplayOrder == 1); + var thirdDeliveryTime = _ctx.Set().First(x => x.DisplayOrder == 2); + + var manufacturers = _ctx.Set().ToList().ToDictionarySafe(x => x.Name, x => x); + var categories = _ctx.Set().ToList().ToDictionarySafe(x => x.Alias, x => x); + var specAttributes = _ctx.Set().ToList().ToDictionarySafe(x => x.DisplayOrder, x => x); + + + //#region category shoes + + //var categoryShoes = _ctx.Set().First(x => x.Alias == "Shoes"); + + //#region product Jack Wolfskin COOGEE LOW M + + //var productJackWolfskinCooGeeLowM = new Product + //{ + // ProductType = ProductType.SimpleProduct, + // VisibleIndividually = true, + // Name = "COOGEE LOW M", + // MetaTitle = "COOGEE LOW M", + // ShortDescription = "MÄNNER FREIZEITSCHUHE", + // FullDescription = "

Du bist immer auf dem Sprung: zum Kino, zur neueröffneten Bar oder zum nächsten Stadtfest. Der stylische COOGEE LOW ist DER Schuh für dein Leben in der Stadt. Denn er verbindet Funktion mit Style. Die sportliche EVA - Zwischensohle ist nicht nur leicht und flexibel, sondern dämpft deine Ferse mit ihrer Erhöhung im hinteren Bereich auch besonders gut und ist sehr komfortabel.So sind deine Füße auch nach vielen Stunden noch fit – du wirst es merken, wenn nach einem langen Tag z.B.noch eine spontane Party ansteht. Die Passform haben wir nach dem Vorbild einer Socke gestaltet, sie ist locker und komfortabel.Dennoch bleibt der Schuh in seiner schmalen Form. Dank des atmungsaktiven Obermaterials bleibt das Fußklima angenehm, egal, wie warm es draussen ist.Gleichzeitig ist der Schuh sehr pflegeleicht.Worauf wartest du also noch ? Ab in die City!

", + // Sku = "Wolfskin-4032541", + // ProductTemplateId = productTemplate.Id, + // AllowCustomerReviews = true, + // Published = true, + // Price = 69.90M, + // OldPrice = 99.95M, + // ManageInventoryMethod = ManageInventoryMethod.ManageStock, + // OrderMinimumQuantity = 1, + // OrderMaximumQuantity = 10000, + // StockQuantity = 10000, + // NotifyAdminForQuantityBelow = 1, + // IsShipEnabled = true, + // DeliveryTime = firstDeliveryTime, + // DisplayOrder = 5 + //}; + + //AddProductPicture(productJackWolfskinCooGeeLowM, "product_wolfskin_shoes_coogee_1.jpg", "jack-wolfskin-shoes-coogee-low-m-1"); + + //productJackWolfskinCooGeeLowM.ProductCategories.Add(new ProductCategory { Category = categoryShoes, DisplayOrder = 1 }); + + //productJackWolfskinCooGeeLowM.ProductManufacturers.Add(new ProductManufacturer + //{ + // Manufacturer = _ctx.Set().Where(c => c.Name == "Jack-Wolfskin").Single(), + // DisplayOrder = 1, + //}); + + //#endregion product Jack Wolfskin COOGEE LOW M + + //#region product Adidas SUPERSTAR SCHUH + + //var productAdidasSuperstarSchuh = new Product + //{ + // ProductType = ProductType.SimpleProduct, + // VisibleIndividually = true, + // Name = "SUPERSTAR SCHUH", + // MetaTitle = "SUPERSTAR SCHUH", + // ShortDescription = "DER STREETWEAR-KLASSIKER MIT DER SHELL TOE.", + // FullDescription = "

Der adidas Superstar wurde erstmals 1969 herausgebracht und machte seinem Namen schon bald alle Ehre. Heute gilt er als Streetstyle-Legende. In dieser Version kommt der Schuh mit einem bequemen Obermaterial aus Full-Grain-Leder. Perfekt wird der Look durch die klassische Shell Toe aus Gummi für mehr Strapazierfähigkeit.

", + // Sku = "Adidas-C77124", + // ProductTemplateId = productTemplate.Id, + // AllowCustomerReviews = true, + // Published = true, + // Price = 99.95M, + // ManageInventoryMethod = ManageInventoryMethod.ManageStock, + // OrderMinimumQuantity = 1, + // OrderMaximumQuantity = 10000, + // StockQuantity = 10000, + // NotifyAdminForQuantityBelow = 1, + // IsShipEnabled = true, + // DeliveryTime = firstDeliveryTime, + // DisplayOrder = 5 + //}; + + //AddProductPicture(productAdidasSuperstarSchuh, "product_adidas_superstar_schuh_1.jpg", "adidas-superstar-schuh-1"); + + //productJackWolfskinCooGeeLowM.ProductCategories.Add(new ProductCategory { Category = categoryShoes, DisplayOrder = 1 }); + + //productJackWolfskinCooGeeLowM.ProductManufacturers.Add(new ProductManufacturer + //{ + // Manufacturer = _ctx.Set().Where(c => c.Name == "Adidas").Single(), + // DisplayOrder = 1, + //}); + + + //#endregion product Adidas SUPERSTAR SCHUH + //#endregion category shoes + + #region category jackets + + var categoryJackets = _ctx.Set().First(x => x.Alias == "Jackets"); + + #region product Jack Wolfskin KANUKA POINT + + var productJackWolfsKinKanukaPoint = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "KANUKA POINT JACKET M", + MetaTitle = "KANUKA POINT JACKET M", + ShortDescription = "SOFTSHELLJACKE MÄNNER", + FullDescription = "

Sportliches Design für sportliche Touren: Die KANUKA POINT ist so gern in Bewegung wie du. Die Softshelljacke besteht aus superelastischem und sehr atmungsaktivem Material, das sich unterwegs jeder deiner Bewegungen anpasst. Deswegen nimmst du mit der KANUKA POINT jeden Pass mit Leichtigkeit.Und musst dir auch bei Kraxeleien zum Gipfel keine Gedanken um deine Jacke machen, denn ihr Material hält einiges aus.Auch bei Wind und leichten Schauern bleibst du gelassen.

", + Sku = "jack-1305851", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + Price = 83.90M, + OldPrice = 119.95M, + ManageInventoryMethod = ManageInventoryMethod.ManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + IsShipEnabled = true, + DeliveryTime = firstDeliveryTime, + DisplayOrder = 5 + }; + + AddProductPicture(productJackWolfsKinKanukaPoint, "product_jack_wolfskin_kanuka_point_1.png", "jack-wolfskin-kanuka-point-1"); - #region category golf + productJackWolfsKinKanukaPoint.ProductCategories.Add(new ProductCategory { Category = categoryJackets, DisplayOrder = 1 }); + + productJackWolfsKinKanukaPoint.ProductManufacturers.Add(new ProductManufacturer + { + Manufacturer = _ctx.Set().Where(c => c.Name == "Jack-Wolfskin").Single(), + DisplayOrder = 1, + }); + + #endregion product Jack Wolfskin KANUKA POINT + #endregion category jackets - var categoryGolf = _ctx.Set().First(c => c.Alias == "Golf"); + + #region category golf #region product Titleist SM6 Tour Chrome @@ -10182,7 +10405,7 @@ public IList Products() Published = true, MetaTitle = "Titleist SM6 Tour Chrome", Price = 164.95M, - OldPrice= 199.95M, + OldPrice = 199.95M, IsGiftCard = false, ManageInventoryMethod = ManageInventoryMethod.ManageStock, OrderMinimumQuantity = 1, @@ -10196,13 +10419,9 @@ public IList Products() AddProductPicture(productTitleistSM6TourChrome, "product_titleist_sm6_tour_chrome.jpg"); - productTitleistSM6TourChrome.ProductCategories.Add(new ProductCategory { Category = categoryGolf, DisplayOrder = 1 }); + productTitleistSM6TourChrome.ProductCategories.Add(new ProductCategory { Category = categories["Golf"], DisplayOrder = 1 }); - productTitleistSM6TourChrome.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Titleist").Single(), - DisplayOrder = 1, - }); + productTitleistSM6TourChrome.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Titleist"], DisplayOrder = 1 }); #endregion product Titleist SM6 Tour Chrome @@ -10235,13 +10454,9 @@ public IList Products() AddProductPicture(productTitleistProV1x, "product_titleist-pro-v1x.jpg"); - productTitleistProV1x.ProductCategories.Add(new ProductCategory { Category = categoryGolf, DisplayOrder = 1 }); + productTitleistProV1x.ProductCategories.Add(new ProductCategory { Category = categories["Golf"], DisplayOrder = 1 }); - productTitleistProV1x.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Titleist").Single(), - DisplayOrder = 1, - }); + productTitleistProV1x.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Titleist"], DisplayOrder = 1 }); #endregion product Titleist Pro V1x @@ -10275,13 +10490,9 @@ public IList Products() AddProductPicture(productSupremeGolfball, "product_supremeGolfball_1.jpg", "golfball-1"); AddProductPicture(productSupremeGolfball, "product_supremeGolfball_2.jpg", "golfball-2"); - productSupremeGolfball.ProductCategories.Add(new ProductCategory { Category = categoryGolf, DisplayOrder = 1 }); + productSupremeGolfball.ProductCategories.Add(new ProductCategory { Category = categories["Golf"], DisplayOrder = 1 }); - productSupremeGolfball.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Titleist").Single(), - DisplayOrder = 1, - }); + productSupremeGolfball.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Titleist"], DisplayOrder = 1 }); #endregion product Supreme Golfball @@ -10314,13 +10525,9 @@ public IList Products() AddProductPicture(productGBBEpicSubZeroDriver, "product_gbb-epic-sub-zero-driver.jpg"); - productGBBEpicSubZeroDriver.ProductCategories.Add(new ProductCategory { Category = categoryGolf, DisplayOrder = 1 }); + productGBBEpicSubZeroDriver.ProductCategories.Add(new ProductCategory { Category = categories["Golf"], DisplayOrder = 1 }); - productGBBEpicSubZeroDriver.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Titleist").Single(), - DisplayOrder = 1, - }); + productGBBEpicSubZeroDriver.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Titleist"], DisplayOrder = 1 }); #endregion product GBB Epic Sub Zero Driver @@ -10328,8 +10535,6 @@ public IList Products() #region category Soccer - var categorySoccer = _ctx.Set().First(c => c.Alias == "Soccer"); - #region product Nike Strike Football var productNikeStrikeFootball = new Product @@ -10339,7 +10544,7 @@ public IList Products() Name = "Nike Strike Football", IsEsd = false, ShortDescription = "GREAT TOUCH. HIGH VISIBILITY.", - FullDescription = "

Enhance play everyday, with the Nike Strike Football.

Reinforced rubber retains its shape for confident and consistent control. A stand out Visual Power graphic in black, green and orange is best for ball tracking, despite dark or inclement conditions.

  • Visual Power graphic helps give a true read on flight trajectory.
  • Textured casing offers superior touch.
  • Reinforced rubber bladder supports air and shape retention.
  • 66% rubber/ 15% polyurethane/ 13% polyester/ 7% EVA.
", + FullDescription = "

Enhance play everyday, with the Nike Strike Football.

Reinforced rubber retains its shape for confident and consistent control. A stand out Visual Power graphic in black, green and orange is best for ball tracking, despite dark or inclement conditions.

  • Visual Power graphic helps give a true read on flight trajectory.
  • Textured casing offers superior touch.
  • Reinforced rubber bladder supports air and shape retention.
  • 66% rubber/ 15% polyurethane/ 13% polyester/ 7% EVA.
", Sku = "P-5004", ProductTemplateId = productTemplate.Id, AllowCustomerReviews = true, @@ -10361,13 +10566,9 @@ public IList Products() AddProductPicture(productNikeStrikeFootball, "products_nike-strike-football.jpg"); - productNikeStrikeFootball.ProductCategories.Add(new ProductCategory { Category = categorySoccer, DisplayOrder = 1 }); + productNikeStrikeFootball.ProductCategories.Add(new ProductCategory { Category = categories["Soccer"], DisplayOrder = 1 }); - productNikeStrikeFootball.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Nike").Single(), - DisplayOrder = 1, - }); + productNikeStrikeFootball.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Nike"], DisplayOrder = 1 }); productNikeStrikeFootball.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10375,7 +10576,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Manufacturer -> Nike - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder ==20).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 20).Single() + SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 20) }); productNikeStrikeFootball.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10383,7 +10584,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Material -> rubber - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 12).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 12) }); productNikeStrikeFootball.TierPrices.Add(new TierPrice { Quantity = 6, Price = 26.90M }); @@ -10421,13 +10622,9 @@ public IList Products() AddProductPicture(productNikeEvoPowerBall, "product_nike-vopower-53-trainer-hs-ball.jpg"); - productNikeEvoPowerBall.ProductCategories.Add(new ProductCategory { Category = categorySoccer, DisplayOrder = 1 }); + productNikeEvoPowerBall.ProductCategories.Add(new ProductCategory { Category = categories["Soccer"], DisplayOrder = 1 }); - productNikeEvoPowerBall.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Nike").Single(), - DisplayOrder = 1, - }); + productNikeEvoPowerBall.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Nike"], DisplayOrder = 1 }); productNikeEvoPowerBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10435,7 +10632,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Manufacturer -> Nike - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 20).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 20).Single() + SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 20) }); productNikeEvoPowerBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10443,7 +10640,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Material -> leather - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 5).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 5) }); #endregion Evopower 5.3 Trainer HS Ball @@ -10481,13 +10678,9 @@ public IList Products() AddProductPicture(productTorfabrikOfficialGameBall, "product_torfabrik-offizieller-spielball_blue.png", "official-game-ball-blue"); AddProductPicture(productTorfabrikOfficialGameBall, "product_torfabrik-offizieller-spielball_green.png", "official-game-ball-green"); - productTorfabrikOfficialGameBall.ProductCategories.Add(new ProductCategory { Category = categorySoccer, DisplayOrder = 1 }); + productTorfabrikOfficialGameBall.ProductCategories.Add(new ProductCategory { Category = categories["Soccer"], DisplayOrder = 1 }); - productTorfabrikOfficialGameBall.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Adidas").Single(), - DisplayOrder = 1, - }); + productTorfabrikOfficialGameBall.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Adidas"], DisplayOrder = 1 }); productTorfabrikOfficialGameBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10495,7 +10688,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Manufacturer -> Adidas - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 20).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 19).Single() + SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 19) }); productTorfabrikOfficialGameBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10503,7 +10696,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Material -> leather - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 5).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 5) }); #endregion Torfabrik official game ball @@ -10543,13 +10736,9 @@ public IList Products() AddProductPicture(productAdidasTangoSalaBall, "product_adidas-tango-pasadena-ball-brown.jpg", "adidas-tango-pasadena-ball-brown"); AddProductPicture(productAdidasTangoSalaBall, "product_adidas-tango-pasadena-ball-blue.jpg", "adidas-tango-pasadena-ball-blue"); - productAdidasTangoSalaBall.ProductCategories.Add(new ProductCategory { Category = categorySoccer, DisplayOrder = 1 }); + productAdidasTangoSalaBall.ProductCategories.Add(new ProductCategory { Category = categories["Soccer"], DisplayOrder = 1 }); - productAdidasTangoSalaBall.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Adidas").Single(), - DisplayOrder = 1, - }); + productAdidasTangoSalaBall.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Adidas"], DisplayOrder = 1 }); productAdidasTangoSalaBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10557,7 +10746,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Manufacturer -> Adidas - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 20).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 19).Single() + SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 19) }); productAdidasTangoSalaBall.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10565,7 +10754,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Material -> leather - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 5).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 5) }); #endregion Adidas TANGO SALA BALL @@ -10574,8 +10763,6 @@ public IList Products() #region category Basketball - var categoryBasketball = _ctx.Set().First(c => c.Alias == "Basketball"); - #region Wilson Evolution High School Game Basketball var productEvolutionHighSchoolGameBasketball = new Product @@ -10607,13 +10794,9 @@ public IList Products() AddProductPicture(productEvolutionHighSchoolGameBasketball, "product_evolution-high-school-game-basketball.jpg"); - productEvolutionHighSchoolGameBasketball.ProductCategories.Add(new ProductCategory { Category = categoryBasketball, DisplayOrder = 1 }); + productEvolutionHighSchoolGameBasketball.ProductCategories.Add(new ProductCategory { Category = categories["Basketball"], DisplayOrder = 1 }); - productEvolutionHighSchoolGameBasketball.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Adidas").Single(), - DisplayOrder = 1, - }); + productEvolutionHighSchoolGameBasketball.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Adidas"], DisplayOrder = 1 }); productEvolutionHighSchoolGameBasketball.TierPrices.Add(new TierPrice { Quantity = 6, Price = 24.90M }); productEvolutionHighSchoolGameBasketball.TierPrices.Add(new TierPrice { Quantity = 12, Price = 22.90M }); @@ -10650,13 +10833,9 @@ public IList Products() AddProductPicture(productAllCourtBasketball, "product_all-court-basketball.png"); - productAllCourtBasketball.ProductCategories.Add(new ProductCategory { Category = categoryBasketball, DisplayOrder = 1 }); + productAllCourtBasketball.ProductCategories.Add(new ProductCategory { Category = categories["Basketball"], DisplayOrder = 1 }); - productAllCourtBasketball.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Adidas").Single(), - DisplayOrder = 1, - }); + productAllCourtBasketball.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Adidas"], DisplayOrder = 1 }); #endregion All Court Basketball @@ -10664,8 +10843,6 @@ public IList Products() #region category sunglasses - var categorySunglasses = _ctx.Set().First(c => c.Alias == "Sunglasses"); - #region product Top bar var productRayBanTopBar = new Product @@ -10697,13 +10874,9 @@ public IList Products() AddProductPicture(productRayBanTopBar, "product_RayBanTopBar_2.jpg", "rayban-top-bar-2"); AddProductPicture(productRayBanTopBar, "product_RayBanTopBar_3.jpg", "rayban-top-bar-3"); - productRayBanTopBar.ProductCategories.Add(new ProductCategory { Category = categorySunglasses, DisplayOrder = 1 }); + productRayBanTopBar.ProductCategories.Add(new ProductCategory { Category = categories["Sunglasses"], DisplayOrder = 1 }); - productRayBanTopBar.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Ray-Ban").Single(), - DisplayOrder = 1, - }); + productRayBanTopBar.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Ray-Ban"], DisplayOrder = 1 }); #endregion product Top bar @@ -10741,13 +10914,9 @@ public IList Products() AddProductPicture(productOriginalWayfarer, "product_productOriginalWayfarer_5.jpg", "wayfarer-green-classic-havana-black"); AddProductPicture(productOriginalWayfarer, "product_productOriginalWayfarer_6.jpg", "wayfarer-blue-gray-classic-black-3"); - productOriginalWayfarer.ProductCategories.Add(new ProductCategory { Category = categorySunglasses, DisplayOrder = 1 }); + productOriginalWayfarer.ProductCategories.Add(new ProductCategory { Category = categories["Sunglasses"], DisplayOrder = 1 }); - productOriginalWayfarer.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Ray-Ban").Single(), - DisplayOrder = 1, - }); + productOriginalWayfarer.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Ray-Ban"], DisplayOrder = 1 }); #endregion product ORIGINAL WAYFARER AT COLLECTION @@ -10780,13 +10949,9 @@ public IList Products() AddProductPicture(productRadarEVPrizmSportsSunglasses, "product_radar_ev_prizm.jpg"); - productRadarEVPrizmSportsSunglasses.ProductCategories.Add(new ProductCategory { Category = categorySunglasses, DisplayOrder = 1 }); + productRadarEVPrizmSportsSunglasses.ProductCategories.Add(new ProductCategory { Category = categories["Sunglasses"], DisplayOrder = 1 }); - productRadarEVPrizmSportsSunglasses.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Oakley").Single(), - DisplayOrder = 1, - }); + productRadarEVPrizmSportsSunglasses.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Oakley"], DisplayOrder = 1 }); #endregion product Radar EV Prizm Sports Sunglasses @@ -10865,13 +11030,9 @@ public IList Products() AddProductPicture(productCustomFlakSunglasses, "product_CustomFlak_skyblue_sapphireiridium.jpg", "custom_flak_skyblue_sapphireiridium"); AddProductPicture(productCustomFlakSunglasses, "product_CustomFlak_skyblue_violetiridium.jpg", "custom_flak_skyblue_violetiridium"); - productCustomFlakSunglasses.ProductCategories.Add(new ProductCategory { Category = categorySunglasses, DisplayOrder = 1 }); + productCustomFlakSunglasses.ProductCategories.Add(new ProductCategory { Category = categories["Sunglasses"], DisplayOrder = 1 }); - productCustomFlakSunglasses.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Oakley").Single(), - DisplayOrder = 1, - }); + productCustomFlakSunglasses.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Oakley"], DisplayOrder = 1 }); #endregion product Custom Flak Sunglasses @@ -10880,8 +11041,6 @@ public IList Products() #region category apple - var categoryApple = _ctx.Set().First(c => c.Alias == "Apple"); - #region product iphone plus var productIphoneplus = new Product @@ -10919,7 +11078,7 @@ public IList Products() AddProductPicture(productIphoneplus, "product_iphone-plus_rose.jpg", "iphone-plus-rose"); AddProductPicture(productIphoneplus, "product_iphone-plus_gold.jpg", "iphone-plus-gold"); - productIphoneplus.ProductCategories.Add(new ProductCategory { Category = categoryApple, DisplayOrder = 1 }); + productIphoneplus.ProductCategories.Add(new ProductCategory { Category = categories["Apple"], DisplayOrder = 1 }); productIphoneplus.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10927,7 +11086,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // offer type -> Permanent low price - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 22).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 2).Single() + SpecificationAttributeOption = specAttributes[22].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); productIphoneplus.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute @@ -10936,7 +11095,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // storage capacity -> 64gb - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 27).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 2).Single() + SpecificationAttributeOption = specAttributes[27].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); productIphoneplus.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10944,7 +11103,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // storage capacity -> 128gb - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 27).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[27].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); productIphoneplus.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -10952,7 +11111,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // operating system -> ios - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 5).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 9).Single() + SpecificationAttributeOption = specAttributes[5].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) }); #endregion product iphone plus @@ -10988,22 +11147,17 @@ public IList Products() AddProductPicture(productWatchSeries2, "product_watchseries2_1.jpg", "watchseries-1"); AddProductPicture(productWatchSeries2, "product_watchseries2_2.jpg", "watchseries-2"); - productWatchSeries2.ProductCategories.Add(new ProductCategory { Category = categoryApple, DisplayOrder = 1 }); + productWatchSeries2.ProductCategories.Add(new ProductCategory { Category = categories["Apple"], DisplayOrder = 1 }); - productWatchSeries2.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Apple").Single(), - DisplayOrder = 1, - }); + productWatchSeries2.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Apple"], DisplayOrder = 1 }); - //attributes productWatchSeries2.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, // offer type -> offer of the day - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 22).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 6).Single() + SpecificationAttributeOption = specAttributes[22].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 6) }); productWatchSeries2.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute @@ -11012,7 +11166,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // storage capacity -> 32gb - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 27).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[27].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productWatchSeries2.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute @@ -11021,7 +11175,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // operating system -> ios - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 5).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 9).Single() + SpecificationAttributeOption = specAttributes[5].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) }); #endregion product Watch Series 2 @@ -11060,13 +11214,9 @@ public IList Products() AddProductPicture(productAirpods, "products_airpods_gold.jpg", "airpods-gold"); AddProductPicture(productAirpods, "products_airpods_mint.jpg", "airpods-mint"); - productAirpods.ProductCategories.Add(new ProductCategory { Category = categoryApple, DisplayOrder = 1 }); + productAirpods.ProductCategories.Add(new ProductCategory { Category = categories["Apple"], DisplayOrder = 1 }); - productAirpods.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Apple").Single(), - DisplayOrder = 7, - }); + productAirpods.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Apple"], DisplayOrder = 7 }); #endregion product Airpods @@ -11107,16 +11257,12 @@ public IList Products() AddProductPicture(productAppleProHipsterBundle, "product_iphoneplus_2.jpg", "bundle-iphoneplus"); AddProductPicture(productAppleProHipsterBundle, "category_apple.png", "bundle-apple"); - productAppleProHipsterBundle.ProductCategories.Add(new ProductCategory { Category = categoryApple, DisplayOrder = 1 }); + productAppleProHipsterBundle.ProductCategories.Add(new ProductCategory { Category = categories["Apple"], DisplayOrder = 1 }); - productAppleProHipsterBundle.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Apple").Single(), - DisplayOrder = 1, - }); + productAppleProHipsterBundle.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Apple"], DisplayOrder = 1 }); #endregion product Ultimate Apple Pro Hipster Bundle - + #region product 9,7 iPad var product97ipad = new Product @@ -11161,14 +11307,9 @@ public IList Products() AddProductPicture(product97ipad, "product_97-ipad-gold.jpg", "ipad-gold"); AddProductPicture(product97ipad, "product_97-ipad-silver.jpg", "ipad-silver"); - product97ipad.ProductCategories.Add(new ProductCategory { Category = categoryApple, DisplayOrder = 1 }); - - product97ipad.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Apple").Single(), - DisplayOrder = 1, - }); + product97ipad.ProductCategories.Add(new ProductCategory { Category = categories["Apple"], DisplayOrder = 1 }); + product97ipad.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Apple"], DisplayOrder = 1 }); product97ipad.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -11176,16 +11317,16 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // offer type -> promotion - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 22).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[22].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); - + product97ipad.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 1, // storage capacity -> 64gb - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 27).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 2).Single() + SpecificationAttributeOption = specAttributes[27].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); product97ipad.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -11193,7 +11334,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // storage capacity -> 128gb - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 27).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[27].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); product97ipad.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -11201,7 +11342,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // operating system -> ios - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 5).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 9).Single() + SpecificationAttributeOption = specAttributes[5].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) }); #endregion product 9,7 iPad @@ -11210,641 +11351,633 @@ public IList Products() #region category Gift Cards - var categoryGiftCards = _ctx.Set().First(c => c.Alias == "Gift Cards"); - - #region product10GiftCard - - var product10GiftCard = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "$10 Virtual Gift Card", - IsEsd = true, - ShortDescription = "$10 Gift Card. Gift Cards must be redeemed through our site Web site toward the purchase of eligible products.", - FullDescription = "

Gift Cards must be redeemed through our site Web site toward the purchase of eligible products. Purchases are deducted from the GiftCard balance. Any unused balance will be placed in the recipient's GiftCard account when redeemed. If an order exceeds the amount of the GiftCard, the balance must be paid with a credit card or other available payment method.

", - Sku = "P-1000", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "$10 Virtual Gift Card", - Price = 10M, - IsGiftCard = true, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - DisplayOrder = 1 - }; - - AddProductPicture(product10GiftCard, "product_gift_card_10.png"); - product10GiftCard.ProductCategories.Add(new ProductCategory { Category = categoryGiftCards, DisplayOrder = 1 }); - - #endregion product10GiftCard - - #region product25GiftCard - - var product25GiftCard = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "$25 Virtual Gift Card", - IsEsd = true, - ShortDescription = "$25 Gift Card. Gift Cards must be redeemed through our site Web site toward the purchase of eligible products.", - FullDescription = "

Gift Cards must be redeemed through our site Web site toward the purchase of eligible products. Purchases are deducted from the GiftCard balance. Any unused balance will be placed in the recipient's GiftCard account when redeemed. If an order exceeds the amount of the GiftCard, the balance must be paid with a credit card or other available payment method.

", - Sku = "P-1001", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "$25 Virtual Gift Card", - Price = 25M, - IsGiftCard = true, - GiftCardType = GiftCardType.Virtual, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - DisplayOrder = 2 - }; - - AddProductPicture(product25GiftCard, "product_gift_card_25.png"); - product25GiftCard.ProductCategories.Add(new ProductCategory { Category = categoryGiftCards, DisplayOrder = 1 }); - - #endregion product25GiftCard - - #region product50GiftCard - - var product50GiftCard = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "$50 Virtual Gift Card", - IsEsd = true, - ShortDescription = "$50 Gift Card. Gift Cards must be redeemed through our site Web site toward the purchase of eligible products.", - FullDescription = "

Gift Cards must be redeemed through our site Web site toward the purchase of eligible products. Purchases are deducted from the GiftCard balance. Any unused balance will be placed in the recipient's GiftCard account when redeemed. If an order exceeds the amount of the GiftCard, the balance must be paid with a credit card or other available payment method.

", - Sku = "P-1002", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "$50 Virtual Gift Card", - Price = 50M, - IsGiftCard = true, - GiftCardType = GiftCardType.Virtual, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - DisplayOrder = 3 - }; - - AddProductPicture(product50GiftCard, "product_gift_card_50.png"); - product50GiftCard.ProductCategories.Add(new ProductCategory { Category = categoryGiftCards, DisplayOrder = 1 }); - - #endregion product50GiftCard - - #region product100GiftCard + #region product10GiftCard - var product100GiftCard = new Product + var product10GiftCard = new Product { ProductType = ProductType.SimpleProduct, VisibleIndividually = true, - Name = "$100 Virtual Gift Card", + Name = "$10 Virtual Gift Card", IsEsd = true, - ShortDescription = "$100 Gift Card. Gift Cards must be redeemed through our site Web site toward the purchase of eligible products.", + ShortDescription = "$10 Gift Card. Gift Cards must be redeemed through our site Web site toward the purchase of eligible products.", FullDescription = "

Gift Cards must be redeemed through our site Web site toward the purchase of eligible products. Purchases are deducted from the GiftCard balance. Any unused balance will be placed in the recipient's GiftCard account when redeemed. If an order exceeds the amount of the GiftCard, the balance must be paid with a credit card or other available payment method.

", - Sku = "P-10033", + Sku = "P-1000", ProductTemplateId = productTemplate.Id, AllowCustomerReviews = true, Published = true, - MetaTitle = "$100 Virtual Gift Card", - Price = 100M, + MetaTitle = "$10 Virtual Gift Card", + Price = 10M, IsGiftCard = true, - GiftCardType = GiftCardType.Virtual, ManageInventoryMethod = ManageInventoryMethod.DontManageStock, OrderMinimumQuantity = 1, OrderMaximumQuantity = 10000, StockQuantity = 10000, NotifyAdminForQuantityBelow = 1, AllowBackInStockSubscriptions = false, - DisplayOrder = 4, + DisplayOrder = 1 }; - AddProductPicture(product100GiftCard, "product_gift_card_100.png"); - product100GiftCard.ProductCategories.Add(new ProductCategory { Category = categoryGiftCards, DisplayOrder = 1 }); + AddProductPicture(product10GiftCard, "product_gift_card_10.png"); + product10GiftCard.ProductCategories.Add(new ProductCategory { Category = categories["Gift Cards"], DisplayOrder = 1 }); - #endregion product100GiftCard + #endregion product10GiftCard + + #region product25GiftCard + + var product25GiftCard = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "$25 Virtual Gift Card", + IsEsd = true, + ShortDescription = "$25 Gift Card. Gift Cards must be redeemed through our site Web site toward the purchase of eligible products.", + FullDescription = "

Gift Cards must be redeemed through our site Web site toward the purchase of eligible products. Purchases are deducted from the GiftCard balance. Any unused balance will be placed in the recipient's GiftCard account when redeemed. If an order exceeds the amount of the GiftCard, the balance must be paid with a credit card or other available payment method.

", + Sku = "P-1001", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "$25 Virtual Gift Card", + Price = 25M, + IsGiftCard = true, + GiftCardType = GiftCardType.Virtual, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + DisplayOrder = 2 + }; + + AddProductPicture(product25GiftCard, "product_gift_card_25.png"); + product25GiftCard.ProductCategories.Add(new ProductCategory { Category = categories["Gift Cards"], DisplayOrder = 1 }); + + #endregion product25GiftCard + + #region product50GiftCard + + var product50GiftCard = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "$50 Virtual Gift Card", + IsEsd = true, + ShortDescription = "$50 Gift Card. Gift Cards must be redeemed through our site Web site toward the purchase of eligible products.", + FullDescription = "

Gift Cards must be redeemed through our site Web site toward the purchase of eligible products. Purchases are deducted from the GiftCard balance. Any unused balance will be placed in the recipient's GiftCard account when redeemed. If an order exceeds the amount of the GiftCard, the balance must be paid with a credit card or other available payment method.

", + Sku = "P-1002", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "$50 Virtual Gift Card", + Price = 50M, + IsGiftCard = true, + GiftCardType = GiftCardType.Virtual, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + DisplayOrder = 3 + }; + + AddProductPicture(product50GiftCard, "product_gift_card_50.png"); + product50GiftCard.ProductCategories.Add(new ProductCategory { Category = categories["Gift Cards"], DisplayOrder = 1 }); + + #endregion product50GiftCard + + #region product100GiftCard + + var product100GiftCard = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "$100 Virtual Gift Card", + IsEsd = true, + ShortDescription = "$100 Gift Card. Gift Cards must be redeemed through our site Web site toward the purchase of eligible products.", + FullDescription = "

Gift Cards must be redeemed through our site Web site toward the purchase of eligible products. Purchases are deducted from the GiftCard balance. Any unused balance will be placed in the recipient's GiftCard account when redeemed. If an order exceeds the amount of the GiftCard, the balance must be paid with a credit card or other available payment method.

", + Sku = "P-10033", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "$100 Virtual Gift Card", + Price = 100M, + IsGiftCard = true, + GiftCardType = GiftCardType.Virtual, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + DisplayOrder = 4, + }; + + AddProductPicture(product100GiftCard, "product_gift_card_100.png"); + product100GiftCard.ProductCategories.Add(new ProductCategory { Category = categories["Gift Cards"], DisplayOrder = 1 }); + + #endregion product100GiftCard #endregion category Gift Cards #region category books - var categorySpiegelBestseller = _ctx.Set().First(c => c.Alias == "SPIEGEL-Bestseller"); - var categoryCookAndEnjoy = _ctx.Set().First(c => c.Alias == "Cook and enjoy"); - var categoryBooks = _ctx.Set().First(c => c.Alias == "Books"); - - #region productBooksUberMan + #region productBooksUberMan - var productBooksUberMan = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "Überman: The novel", - ShortDescription = "(Hardcover)", - FullDescription = "

From idiots to riches - and back ... Ever since it with my Greek financial advisors were no more delicious cookies to meetings, I should have known something. Was the last cookie it when I bought a Romanian forest funds and leveraged discount certificates on lean hogs - which is sort of a more stringent bet that the price of lean hogs will remain stable, and that's nothing special because it is also available for cattle and cotton and fat pig. Again and again and I joked Kosmas Nikiforos Sarantakos. About all the part-time seer who tremblingly put for fear the euro crisis gold coins under the salami slices of their frozen pizzas And then came the day that revealed to me in almost Sarantakos fraudulent casualness that my plan had not worked out really. 'Why all of a sudden> my plan', 'I heard myself asking yet, but it was in the garage I realized what that really meant minus 211.2 percent in my portfolio report: personal bankruptcy, gutter and Drug Addiction with subsequent loss of the incisors . Not even the study of my friend, I would still be able to finance. The only way out was to me as quickly as secretly again to draw from this unspeakable Greek shit - I had to be Überman! By far the bekloppteste story about 'idiot' Simon Peter! »Tommy Jaud – Deutschlands witzigste Seite.« Alex Dengler, Bild am Sonntag

", + var productBooksUberMan = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "Überman: The novel", + ShortDescription = "(Hardcover)", + FullDescription = "

From idiots to riches - and back ... Ever since it with my Greek financial advisors were no more delicious cookies to meetings, I should have known something. Was the last cookie it when I bought a Romanian forest funds and leveraged discount certificates on lean hogs - which is sort of a more stringent bet that the price of lean hogs will remain stable, and that's nothing special because it is also available for cattle and cotton and fat pig. Again and again and I joked Kosmas Nikiforos Sarantakos. About all the part-time seer who tremblingly put for fear the euro crisis gold coins under the salami slices of their frozen pizzas And then came the day that revealed to me in almost Sarantakos fraudulent casualness that my plan had not worked out really. 'Why all of a sudden> my plan', 'I heard myself asking yet, but it was in the garage I realized what that really meant minus 211.2 percent in my portfolio report: personal bankruptcy, gutter and Drug Addiction with subsequent loss of the incisors . Not even the study of my friend, I would still be able to finance. The only way out was to me as quickly as secretly again to draw from this unspeakable Greek shit - I had to be Überman! By far the bekloppteste story about 'idiot' Simon Peter! »Tommy Jaud – Deutschlands witzigste Seite.« Alex Dengler, Bild am Sonntag

", Sku = "P-1003", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "Überman: The novel", - Price = 16.99M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true - }; + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Überman: The novel", + Price = 16.99M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true + }; AddProductPicture(productBooksUberMan, "0000932_uberman-der-roman.jpeg"); - productBooksUberMan.ProductCategories.Add(new ProductCategory { Category = categorySpiegelBestseller, DisplayOrder = 1 }); + productBooksUberMan.ProductCategories.Add(new ProductCategory { Category = categories["SPIEGEL-Bestseller"], DisplayOrder = 1 }); - productBooksUberMan.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 13).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); - productBooksUberMan.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 14).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 7).Single() - }); - productBooksUberMan.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 12).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); + productBooksUberMan.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + productBooksUberMan.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 7) + }); + productBooksUberMan.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); - #endregion productBooksUberMan + #endregion productBooksUberMan - #region productBooksGefangeneDesHimmels + #region productBooksGefangeneDesHimmels - var productBooksGefangeneDesHimmels = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "The Prisoner of Heaven: A Novel", - ShortDescription = "(Hardcover)", - FullDescription = "

By Shadow of the Wind and The Angel's Game, the new large-Barcelona novel by Carlos Ruiz Zafón. - Barcelona, Christmas 1957th The bookseller Daniel Sempere and his friend Fermín be drawn again into a great adventure. In the continuation of his international success with Carlos Ruiz Zafón takes the reader on a fascinating journey into his Barcelona. Creepy and fascinating, with incredible suction power and humor, the novel, the story of Fermin, who 'rose from the dead, and the key to the future is.' Fermin's life story linking the threads of The Shadow of the Wind with those from The Angel's Game. A masterful puzzle that keeps the reader around the world in thrall.

Product Hardcover: 416 pages Publisher: S. Fischer Verlag; 1 edition (October 25, 2012) Language: German ISBN-10: 3,100,954,025 ISBN-13: 978-3100954022 Original title: El prisionero del cielo Size and / or weight: 21.4 x 13.6 cm x 4.4

", - ProductTemplateId = productTemplate.Id, + var productBooksGefangeneDesHimmels = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "The Prisoner of Heaven: A Novel", + ShortDescription = "(Hardcover)", + FullDescription = "

By Shadow of the Wind and The Angel's Game, the new large-Barcelona novel by Carlos Ruiz Zafón. - Barcelona, Christmas 1957th The bookseller Daniel Sempere and his friend Fermín be drawn again into a great adventure. In the continuation of his international success with Carlos Ruiz Zafón takes the reader on a fascinating journey into his Barcelona. Creepy and fascinating, with incredible suction power and humor, the novel, the story of Fermin, who 'rose from the dead, and the key to the future is.' Fermin's life story linking the threads of The Shadow of the Wind with those from The Angel's Game. A masterful puzzle that keeps the reader around the world in thrall.

Product Hardcover: 416 pages Publisher: S. Fischer Verlag; 1 edition (October 25, 2012) Language: German ISBN-10: 3,100,954,025 ISBN-13: 978-3100954022 Original title: El prisionero del cielo Size and / or weight: 21.4 x 13.6 cm x 4.4

", + ProductTemplateId = productTemplate.Id, Sku = "P-1004", - AllowCustomerReviews = true, - Published = true, - MetaTitle = "The Prisoner of Heaven: A Novel", - Price = 22.99M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "The Prisoner of Heaven: A Novel", + Price = 22.99M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id }; AddProductPicture(productBooksGefangeneDesHimmels, "0000935_der-gefangene-des-himmels-roman_300.jpeg"); - productBooksGefangeneDesHimmels.ProductCategories.Add(new ProductCategory { Category = categorySpiegelBestseller, DisplayOrder = 1 }); + productBooksGefangeneDesHimmels.ProductCategories.Add(new ProductCategory { Category = categories["SPIEGEL-Bestseller"], DisplayOrder = 1 }); - productBooksGefangeneDesHimmels.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Edition -> bound - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 13).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); + productBooksGefangeneDesHimmels.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Edition -> bound + SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); - productBooksGefangeneDesHimmels.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Category -> bound - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 14).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 7).Single() - }); - productBooksGefangeneDesHimmels.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Language -> German - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 12).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); + productBooksGefangeneDesHimmels.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Category -> bound + SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 7) + }); + productBooksGefangeneDesHimmels.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Language -> German + SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); - #endregion productBooksGefangeneDesHimmels + #endregion productBooksGefangeneDesHimmels - #region productBooksBestGrillingRecipes + #region productBooksBestGrillingRecipes - var productBooksBestGrillingRecipes = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "Best Grilling Recipes", - ShortDescription = "More Than 100 Regional Favorites Tested and Perfected for the Outdoor Cook (Hardcover)", - FullDescription = "

Take a winding cross-country trip and you'll discover barbecue shacks with offerings like tender-smoky Baltimore pit beef and saucy St. Louis pork steaks. To bring you the best of these hidden gems, along with all the classics, the editors of Cook's Country magazine scoured the country, then tested and perfected their favorites. HEre traditions large and small are brought into the backyard, from Hawaii's rotisserie favorite, the golden-hued Huli Huli Chicken, to fall-off-the-bone Chicago Barbecued Ribs. In Kansas City, they're all about the sauce, and for our saucy Kansas City Sticky Ribs, we found a surprise ingredient-root beer. We also tackle all the best sides.

Not sure where or how to start? This cookbook kicks off with an easy-to-follow primer that will get newcomers all fired up. Whether you want to entertain a crowd or just want to learn to make perfect burgers, Best Grilling Recipes shows you the way.

", - ProductTemplateId = productTemplate.Id, + var productBooksBestGrillingRecipes = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "Best Grilling Recipes", + ShortDescription = "More Than 100 Regional Favorites Tested and Perfected for the Outdoor Cook (Hardcover)", + FullDescription = "

Take a winding cross-country trip and you'll discover barbecue shacks with offerings like tender-smoky Baltimore pit beef and saucy St. Louis pork steaks. To bring you the best of these hidden gems, along with all the classics, the editors of Cook's Country magazine scoured the country, then tested and perfected their favorites. HEre traditions large and small are brought into the backyard, from Hawaii's rotisserie favorite, the golden-hued Huli Huli Chicken, to fall-off-the-bone Chicago Barbecued Ribs. In Kansas City, they're all about the sauce, and for our saucy Kansas City Sticky Ribs, we found a surprise ingredient-root beer. We also tackle all the best sides.

Not sure where or how to start? This cookbook kicks off with an easy-to-follow primer that will get newcomers all fired up. Whether you want to entertain a crowd or just want to learn to make perfect burgers, Best Grilling Recipes shows you the way.

", + ProductTemplateId = productTemplate.Id, Sku = "P-1005", - AllowCustomerReviews = true, - Published = true, - MetaTitle = "Best Grilling Recipes", - Price = 27.00M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Best Grilling Recipes", + Price = 27.00M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id }; AddProductPicture(productBooksBestGrillingRecipes, "product_bestgrillingrecipes.jpg"); - productBooksBestGrillingRecipes.ProductCategories.Add(new ProductCategory { Category = categoryCookAndEnjoy, DisplayOrder = 1 }); - - productBooksBestGrillingRecipes.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Edition -> bound - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 13).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); - productBooksBestGrillingRecipes.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Category -> cook & bake - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 14).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 8).Single() - }); - productBooksBestGrillingRecipes.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Language -> German - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 12).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 2).Single() - }); + productBooksBestGrillingRecipes.ProductCategories.Add(new ProductCategory { Category = categories["Cook and enjoy"], DisplayOrder = 1 }); - #endregion productBooksBestGrillingRecipes + productBooksBestGrillingRecipes.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Edition -> bound + SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + productBooksBestGrillingRecipes.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Category -> cook & bake + SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 8) + }); + productBooksBestGrillingRecipes.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Language -> German + SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) + }); - #region productBooksCookingForTwo + #endregion productBooksBestGrillingRecipes - var productBooksCookingForTwo = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "Cooking for Two", - ShortDescription = "More Than 200 Foolproof Recipes for Weeknights and Special Occasions (Hardcover)", - FullDescription = "

In Cooking for Two, the test kitchen's goal was to take traditional recipes and cut them down to size to serve just twowith tailored cooking techniques and smart shopping tips that will cut down on wasted food and wasted money. Great lasagna starts to lose its luster when you're eating the leftovers for the fourth day in a row. While it may seem obvious that a recipe for four can simply be halved to work, our testing has proved that this is not always the case; cooking with smaller amounts of ingredients often requires different preparation techniques, cooking time, temperature, and the proportion of ingredients. This was especially true as we worked on scaled-down desserts; baking is an unforgiving science in which any changes in recipe amounts often called for changes in baking times and temperatures.

Hardcover: 352 pages
Publisher: America's Test Kitchen (May 2009)
Language: English
ISBN-10: 1933615435
ISBN-13: 978-1933615431

", - ProductTemplateId = productTemplate.Id, + #region productBooksCookingForTwo + + var productBooksCookingForTwo = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "Cooking for Two", + ShortDescription = "More Than 200 Foolproof Recipes for Weeknights and Special Occasions (Hardcover)", + FullDescription = "

In Cooking for Two, the test kitchen's goal was to take traditional recipes and cut them down to size to serve just twowith tailored cooking techniques and smart shopping tips that will cut down on wasted food and wasted money. Great lasagna starts to lose its luster when you're eating the leftovers for the fourth day in a row. While it may seem obvious that a recipe for four can simply be halved to work, our testing has proved that this is not always the case; cooking with smaller amounts of ingredients often requires different preparation techniques, cooking time, temperature, and the proportion of ingredients. This was especially true as we worked on scaled-down desserts; baking is an unforgiving science in which any changes in recipe amounts often called for changes in baking times and temperatures.

Hardcover: 352 pages
Publisher: America's Test Kitchen (May 2009)
Language: English
ISBN-10: 1933615435
ISBN-13: 978-1933615431

", + ProductTemplateId = productTemplate.Id, Sku = "P-1006", - AllowCustomerReviews = true, - Published = true, - MetaTitle = "Cooking for Two", - Price = 27.00M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Cooking for Two", + Price = 27.00M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = secondDeliveryTime.Id }; AddProductPicture(productBooksCookingForTwo, "product_cookingfortwo.jpg"); - productBooksCookingForTwo.ProductCategories.Add(new ProductCategory { Category = categoryCookAndEnjoy, DisplayOrder = 1 }); + productBooksCookingForTwo.ProductCategories.Add(new ProductCategory { Category = categories["Cook and enjoy"], DisplayOrder = 1 }); - productBooksCookingForTwo.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Edition -> bound - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 13).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); - productBooksCookingForTwo.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Category -> cook & bake - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 14).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 8).Single() - }); - productBooksCookingForTwo.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Language -> German - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 12).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 2).Single() - }); + productBooksCookingForTwo.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Edition -> bound + SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + productBooksCookingForTwo.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Category -> cook & bake + SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 8) + }); + productBooksCookingForTwo.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Language -> German + SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) + }); - #endregion productBooksCookingForTwo + #endregion productBooksCookingForTwo - #region productBooksAutosDerSuperlative + #region productBooksAutosDerSuperlative - var productBooksAutosDerSuperlative = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "Car of superlatives: the strongest, the first, the most beautiful, the fastest", - ShortDescription = "Hardcover", - FullDescription = "

For some, the car is only a useful means of transportation. For everyone else, there are 'cars - The Ultimate Guide' of art-connoisseur Michael Doerflinger. With authentic images, all important data and a lot of information can be presented to the fastest, most innovative, the strongest, the most unusual and the most successful examples of automotive history. A comprehensive manual for the specific reference and extensive browsing.

", + var productBooksAutosDerSuperlative = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "Car of superlatives: the strongest, the first, the most beautiful, the fastest", + ShortDescription = "Hardcover", + FullDescription = "

For some, the car is only a useful means of transportation. For everyone else, there are 'cars - The Ultimate Guide' of art-connoisseur Michael Doerflinger. With authentic images, all important data and a lot of information can be presented to the fastest, most innovative, the strongest, the most unusual and the most successful examples of automotive history. A comprehensive manual for the specific reference and extensive browsing.

", Sku = "P-1007", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "Car of superlatives", - Price = 14.95M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Car of superlatives", + Price = 14.95M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = thirdDeliveryTime.Id }; AddProductPicture(productBooksAutosDerSuperlative, "0000944_autos-der-superlative-die-starksten-die-ersten-die-schonsten-die-schnellsten.jpeg"); - productBooksAutosDerSuperlative.ProductCategories.Add(new ProductCategory { Category = categoryBooks, DisplayOrder = 1 }); - - productBooksAutosDerSuperlative.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Edition -> bound - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 13).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); - productBooksAutosDerSuperlative.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Category -> cars - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 14).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 6).Single() - }); - productBooksAutosDerSuperlative.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Language -> German - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 12).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); + productBooksAutosDerSuperlative.ProductCategories.Add(new ProductCategory { Category = categories["Books"], DisplayOrder = 1 }); - #endregion productBooksAutosDerSuperlative + productBooksAutosDerSuperlative.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Edition -> bound + SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + productBooksAutosDerSuperlative.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Category -> cars + SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 6) + }); + productBooksAutosDerSuperlative.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Language -> German + SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); - #region productBooksBildatlasMotorraeder + #endregion productBooksAutosDerSuperlative - var productBooksBildatlasMotorraeder = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "Picture Atlas Motorcycles: With more than 350 brilliant images", - ShortDescription = "Hardcover", - FullDescription = "

Motorcycles are like no other means of transportation for the great dream of freedom and adventure. This richly illustrated atlas image portrayed in brilliant color photographs and informative text, the most famous bikes of the world's motorcycle history. From the primitive steam engine under the saddle of the late 19th Century up to the hugely powerful, equipped with the latest electronics and computer technology superbikes of today he is an impressive picture of the development and fabrication of noble and fast-paced motorcycles. The myth of the motorcycle is just as much investigated as a motorcycle as a modern lifestyle product of our time. Country-specific, company-historical background information and interesting stories and History about the people who preceded drove one of the seminal inventions of recent centuries and evolved, make this comprehensive illustrated book an incomparable reference for any motorcycle enthusiast and technology enthusiasts.

• Extensive history of the legendary models of all major motorcycle manufacturers worldwide
• With more than 350 brilliant color photographs and fascinating background information relating
• With informative drawings, stunning detail shots and explanatory info-boxes

content • 1817 1913: The beginning of a success story
• 1914 1945: mass mobility
• 1946 1990: Battle for the World Market
• In 1991: The modern motorcycle
• motorcycle cult object: From Transportation to Lifestyle

", + #region productBooksBildatlasMotorraeder + + var productBooksBildatlasMotorraeder = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "Picture Atlas Motorcycles: With more than 350 brilliant images", + ShortDescription = "Hardcover", + FullDescription = "

Motorcycles are like no other means of transportation for the great dream of freedom and adventure. This richly illustrated atlas image portrayed in brilliant color photographs and informative text, the most famous bikes of the world's motorcycle history. From the primitive steam engine under the saddle of the late 19th Century up to the hugely powerful, equipped with the latest electronics and computer technology superbikes of today he is an impressive picture of the development and fabrication of noble and fast-paced motorcycles. The myth of the motorcycle is just as much investigated as a motorcycle as a modern lifestyle product of our time. Country-specific, company-historical background information and interesting stories and History about the people who preceded drove one of the seminal inventions of recent centuries and evolved, make this comprehensive illustrated book an incomparable reference for any motorcycle enthusiast and technology enthusiasts.

• Extensive history of the legendary models of all major motorcycle manufacturers worldwide
• With more than 350 brilliant color photographs and fascinating background information relating
• With informative drawings, stunning detail shots and explanatory info-boxes

content • 1817 1913: The beginning of a success story
• 1914 1945: mass mobility
• 1946 1990: Battle for the World Market
• In 1991: The modern motorcycle
• motorcycle cult object: From Transportation to Lifestyle

", Sku = "P-1008", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "Picture Atlas Motorcycles", - Price = 14.99M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Picture Atlas Motorcycles", + Price = 14.99M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id }; AddProductPicture(productBooksBildatlasMotorraeder, "0000942_bildatlas-motorrader-mit-mehr-als-350-brillanten-abbildungen.jpeg"); - productBooksBildatlasMotorraeder.ProductCategories.Add(new ProductCategory { Category = categoryBooks, DisplayOrder = 1 }); + productBooksBildatlasMotorraeder.ProductCategories.Add(new ProductCategory { Category = categories["Books"], DisplayOrder = 1 }); - productBooksBildatlasMotorraeder.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Edition -> bound - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 13).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); - productBooksBildatlasMotorraeder.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Category -> non-fiction - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 14).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 9).Single() - }); - productBooksBildatlasMotorraeder.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Language -> German - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 12).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); + productBooksBildatlasMotorraeder.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Edition -> bound + SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + productBooksBildatlasMotorraeder.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Category -> non-fiction + SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) + }); + productBooksBildatlasMotorraeder.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Language -> German + SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); - #endregion productBooksBildatlasMotorraeder + #endregion productBooksBildatlasMotorraeder - #region productBooksAutoBuch + #region productBooksAutoBuch - var productBooksAutoBuch = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "The Car Book. The great history with over 1200 models", - ShortDescription = "Hardcover", - FullDescription = "

Makes, models, milestones
The car - for some, a utensil for other expression of lifestyle, cult object and passion. Few inventions have changed their lives as well as the good of the automobile 125 years ago - one more reason for this extensive chronicle. The car-book brings the history of the automobile to life. It presents more than 1200 important models - Karl Benz 'Motorwagen about legendary cult car to advanced hybrid vehicles. It explains the milestones in engine technology and portrays the big brands and their designers. Characteristics from small cars to limousines and send racing each era invite you to browse and discover. The most comprehensive and bestbebildert illustrated book on the market - it would be any car lover!

Hardcover: 360 pages
Publisher: Dorling Kindersley Publishing (September 27, 2012)
Language: German
ISBN-10: 3,831,022,062
ISBN-13: 978-3831022069
Size and / or weight: 30.6 x 25.8 x 2.8 cm

", + var productBooksAutoBuch = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "The Car Book. The great history with over 1200 models", + ShortDescription = "Hardcover", + FullDescription = "

Makes, models, milestones
The car - for some, a utensil for other expression of lifestyle, cult object and passion. Few inventions have changed their lives as well as the good of the automobile 125 years ago - one more reason for this extensive chronicle. The car-book brings the history of the automobile to life. It presents more than 1200 important models - Karl Benz 'Motorwagen about legendary cult car to advanced hybrid vehicles. It explains the milestones in engine technology and portrays the big brands and their designers. Characteristics from small cars to limousines and send racing each era invite you to browse and discover. The most comprehensive and bestbebildert illustrated book on the market - it would be any car lover!

Hardcover: 360 pages
Publisher: Dorling Kindersley Publishing (September 27, 2012)
Language: German
ISBN-10: 3,831,022,062
ISBN-13: 978-3831022069
Size and / or weight: 30.6 x 25.8 x 2.8 cm

", Sku = "P-1009", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "The Car Book", - Price = 29.95M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "The Car Book", + Price = 29.95M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id }; AddProductPicture(productBooksAutoBuch, "0000947_das-auto-buch-die-grose-chronik-mit-uber-1200-modellen_300.jpeg"); - productBooksAutoBuch.ProductCategories.Add(new ProductCategory { Category = categoryBooks, DisplayOrder = 1 }); + productBooksAutoBuch.ProductCategories.Add(new ProductCategory { Category = categories["Books"], DisplayOrder = 1 }); - productBooksAutoBuch.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Edition -> bound - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 13).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); - productBooksAutoBuch.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Category -> non-fiction - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 14).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 9).Single() - }); - productBooksAutoBuch.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Language -> German - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 12).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); + productBooksAutoBuch.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Edition -> bound + SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + productBooksAutoBuch.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Category -> non-fiction + SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) + }); + productBooksAutoBuch.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Language -> German + SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); - #endregion productBooksAutoBuch + #endregion productBooksAutoBuch - #region productBooksFastCars + #region productBooksFastCars - var productBooksFastCars = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "Fast Cars, Image Calendar 2013", - ShortDescription = "spiral bound", - FullDescription = "

Large Size: 48.5 x 34 cm.
This impressive picture calendar with silver ring binding thrilled with impressive photographs of exclusive sports cars. Who understands cars not only as a pure commercial vehicles, will find the most sought-after status symbols at all: fast cars are effectively set to the razor sharp and vivid photos in scene and convey freedom, speed, strength and the highest technical perfection. Starting with the 450-horsepower Maserati GranTurismo MC Stradale on the stylish, luxurious Aston Martin Virage Volante accompany up to the produced only in small numbers Mosler Photon MT900S the fast racer with style and elegance through the months.

Besides the calendar draws another picture to look at interesting details. There are the essential information on any sports car in the English language. After this year, the high-quality photos are framed an eye-catcher on the wall of every lover of fast cars. Even as a gift this beautiful years companion is wonderfully suited. 12 calendar pages, neutral and discreet held calendar. Printed on paper from sustainable forests. For lovers of luxury vintage cars also available in ALPHA EDITION: the large format image Classic Cars Calendar 2013: ISBN 9,783,840,733,376th

Spiral-bound: 14 pages
Publisher: Alpha Edition (June 1, 2012)
Language: German
ISBN-10: 3,840,733,383
ISBN-13: 978-3840733383
Size and / or weight: 48.8 x 34.2 x 0.6 cm

", + var productBooksFastCars = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "Fast Cars, Image Calendar 2013", + ShortDescription = "spiral bound", + FullDescription = "

Large Size: 48.5 x 34 cm.
This impressive picture calendar with silver ring binding thrilled with impressive photographs of exclusive sports cars. Who understands cars not only as a pure commercial vehicles, will find the most sought-after status symbols at all: fast cars are effectively set to the razor sharp and vivid photos in scene and convey freedom, speed, strength and the highest technical perfection. Starting with the 450-horsepower Maserati GranTurismo MC Stradale on the stylish, luxurious Aston Martin Virage Volante accompany up to the produced only in small numbers Mosler Photon MT900S the fast racer with style and elegance through the months.

Besides the calendar draws another picture to look at interesting details. There are the essential information on any sports car in the English language. After this year, the high-quality photos are framed an eye-catcher on the wall of every lover of fast cars. Even as a gift this beautiful years companion is wonderfully suited. 12 calendar pages, neutral and discreet held calendar. Printed on paper from sustainable forests. For lovers of luxury vintage cars also available in ALPHA EDITION: the large format image Classic Cars Calendar 2013: ISBN 9,783,840,733,376th

Spiral-bound: 14 pages
Publisher: Alpha Edition (June 1, 2012)
Language: German
ISBN-10: 3,840,733,383
ISBN-13: 978-3840733383
Size and / or weight: 48.8 x 34.2 x 0.6 cm

", Sku = "P-1010", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "Fast Cars", - Price = 16.95M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Fast Cars", + Price = 16.95M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id }; AddProductPicture(productBooksFastCars, "0000946_fast-cars-bildkalender-2013_300.jpeg"); - productBooksFastCars.ProductCategories.Add(new ProductCategory { Category = categoryBooks, DisplayOrder = 1 }); + productBooksFastCars.ProductCategories.Add(new ProductCategory { Category = categories["Books"], DisplayOrder = 1 }); - productBooksFastCars.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Edition -> bound - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 13).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); - productBooksFastCars.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Category -> cars - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 14).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 6).Single() - }); - productBooksFastCars.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Language -> German - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 12).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); + productBooksFastCars.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Edition -> bound + SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + productBooksFastCars.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Category -> cars + SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 6) + }); + productBooksFastCars.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Language -> German + SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); - #endregion productBooksFastCars + #endregion productBooksFastCars - #region productBooksMotorradAbenteuer + #region productBooksMotorradAbenteuer - var productBooksMotorradAbenteuer = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "Motorcycle Adventures: Riding for travel enduros", - ShortDescription = "Hardcover", - FullDescription = "

Modern travel enduro bikes are ideal for adventure travel. Their technique is complex, their weight considerably. The driving behavior changes depending on the load and distance.

Before the tour starts, you should definitely attend a training course. This superbly illustrated book presents practical means of many informative series photos the right off-road driving in mud and sand, gravel and rock with and without luggage. In addition to the driving course full of information and tips on choosing the right motorcycle for travel planning and practical issues may be on the way.

", + var productBooksMotorradAbenteuer = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "Motorcycle Adventures: Riding for travel enduros", + ShortDescription = "Hardcover", + FullDescription = "

Modern travel enduro bikes are ideal for adventure travel. Their technique is complex, their weight considerably. The driving behavior changes depending on the load and distance.

Before the tour starts, you should definitely attend a training course. This superbly illustrated book presents practical means of many informative series photos the right off-road driving in mud and sand, gravel and rock with and without luggage. In addition to the driving course full of information and tips on choosing the right motorcycle for travel planning and practical issues may be on the way.

", Sku = "P-1011", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "Motorcycle Adventures", - Price = 24.90M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Motorcycle Adventures", + Price = 24.90M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = secondDeliveryTime.Id }; AddProductPicture(productBooksMotorradAbenteuer, "0000943_motorrad-abenteuer-fahrtechnik-fur-reise-enduros.jpeg"); - productBooksMotorradAbenteuer.ProductCategories.Add(new ProductCategory { Category = categoryBooks, DisplayOrder = 1 }); + productBooksMotorradAbenteuer.ProductCategories.Add(new ProductCategory { Category = categories["Books"], DisplayOrder = 1 }); - productBooksMotorradAbenteuer.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Edition -> bound - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 13).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); - productBooksMotorradAbenteuer.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Category -> cars - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 14).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 9).Single() - }); - productBooksMotorradAbenteuer.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 3, - // Language -> German - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 12).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); + productBooksMotorradAbenteuer.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Edition -> bound + SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); + productBooksMotorradAbenteuer.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Category -> cars + SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 9) + }); + productBooksMotorradAbenteuer.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 3, + // Language -> German + SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); - #endregion productBooksMotorradAbenteuer + #endregion productBooksMotorradAbenteuer - #endregion category books + #endregion category books #region Instant Download Music / Digital Products - var categoryDigitalProducts = _ctx.Set().First(c => c.Alias == "Digital Products"); - #region product Books Stone of the Wise var productBooksStoneOfTheWise = new Product @@ -11872,7 +12005,7 @@ public IList Products() }; AddProductPicture(productBooksStoneOfTheWise, "stone_of_wisdom.jpg"); - productBooksStoneOfTheWise.ProductCategories.Add(new ProductCategory { Category = categoryDigitalProducts, DisplayOrder = 1 }); + productBooksStoneOfTheWise.ProductCategories.Add(new ProductCategory { Category = categories["Digital Products"], DisplayOrder = 1 }); productBooksStoneOfTheWise.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -11880,7 +12013,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Edition -> bound - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 13).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[13].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productBooksStoneOfTheWise.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -11888,7 +12021,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Category -> cars - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 14).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 6).Single() + SpecificationAttributeOption = specAttributes[14].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 6) }); productBooksStoneOfTheWise.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -11896,7 +12029,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 3, // Language -> German - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 12).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[12].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); #endregion product Books Stone of the Wise @@ -11929,7 +12062,7 @@ public IList Products() }; AddProductPicture(productInstantDownloadVivaldi, "vivaldi.jpg"); - productInstantDownloadVivaldi.ProductCategories.Add(new ProductCategory { Category = categoryDigitalProducts, DisplayOrder = 1 }); + productInstantDownloadVivaldi.ProductCategories.Add(new ProductCategory { Category = categories["Digital Products"], DisplayOrder = 1 }); productInstantDownloadVivaldi.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -11937,7 +12070,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // mp3 quality > 320 kbit/S - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 18).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[18].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); productInstantDownloadVivaldi.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -11945,7 +12078,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 2, // genre > classic - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 19).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 6).Single() + SpecificationAttributeOption = specAttributes[19].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 6) }); #endregion Antonio Vivildi: then spring @@ -11978,7 +12111,7 @@ public IList Products() }; AddProductPicture(productInstantDownloadBeethoven, "Beethoven.jpg"); - productInstantDownloadBeethoven.ProductCategories.Add(new ProductCategory { Category = categoryDigitalProducts, DisplayOrder = 1 }); + productInstantDownloadBeethoven.ProductCategories.Add(new ProductCategory { Category = categories["Digital Products"], DisplayOrder = 1 }); productInstantDownloadBeethoven.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -11986,7 +12119,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // mp3 quality > 320 kbit/S - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 18).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[18].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); productInstantDownloadBeethoven.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -11994,7 +12127,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 2, // genre > classic - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 19).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 6).Single() + SpecificationAttributeOption = specAttributes[19].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 6) }); #endregion Beethoven für Elise @@ -12003,8 +12136,6 @@ public IList Products() #region watches - var categoryWatches = _ctx.Set().First(c => c.Alias == "Watches"); - #region productTRANSOCEANCHRONOGRAPH var productTRANSOCEANCHRONOGRAPH = new Product @@ -12033,13 +12164,9 @@ public IList Products() }; AddProductPicture(productTRANSOCEANCHRONOGRAPH, "product_transocean-chronograph.jpg"); - productTRANSOCEANCHRONOGRAPH.ProductCategories.Add(new ProductCategory { Category = categoryWatches, DisplayOrder = 1 }); + productTRANSOCEANCHRONOGRAPH.ProductCategories.Add(new ProductCategory { Category = categories["Watches"], DisplayOrder = 1 }); - productTRANSOCEANCHRONOGRAPH.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Breitling").Single(), - DisplayOrder = 1, - }); + productTRANSOCEANCHRONOGRAPH.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Breitling"], DisplayOrder = 1 }); productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12047,7 +12174,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // offer > promotion - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 22).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[22].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12055,7 +12182,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 2, // manufacturer > Breitling - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 20).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 18).Single() + SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 18) }); productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12063,7 +12190,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // housing > steel - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12071,7 +12198,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // material -> leather - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 5).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 5) }); productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12079,7 +12206,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // Gender -> gentlemen - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 7).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[7].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12087,7 +12214,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // movement -> mechanical, self winding - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 9).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[9].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12095,7 +12222,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // diameter -> 44mm - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 24).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[24].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); productTRANSOCEANCHRONOGRAPH.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12103,7 +12230,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // closure -> folding clasp - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 25).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 2).Single() + SpecificationAttributeOption = specAttributes[25].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); #endregion productTRANSOCEANCHRONOGRAPH @@ -12138,13 +12265,9 @@ public IList Products() AddProductPicture(productTissotTTouchExpertSolar, "product_tissot-t-touch-expert-solar.jpg", "tissot-1"); AddProductPicture(productTissotTTouchExpertSolar, "product_tissot-t-touch-expert-solar-t091_2.jpg", "tissot-2"); - productTissotTTouchExpertSolar.ProductCategories.Add(new ProductCategory { Category = categoryWatches, DisplayOrder = 1 }); + productTissotTTouchExpertSolar.ProductCategories.Add(new ProductCategory { Category = categories["Watches"], DisplayOrder = 1 }); - productTissotTTouchExpertSolar.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Tissot").Single(), - DisplayOrder = 1, - }); + productTissotTTouchExpertSolar.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Tissot"], DisplayOrder = 1 }); productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12152,7 +12275,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // offer > best price - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 22).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 8).Single() + SpecificationAttributeOption = specAttributes[22].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 8) }); productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12160,7 +12283,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 2, // manufacturer > Tissot - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 20).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 17).Single() + SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 17) }); productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12168,7 +12291,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // housing > steel - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12176,7 +12299,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // material -> silicone - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 7).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 7) }); productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12184,7 +12307,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // Gender -> gentlemen - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 7).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[7].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12192,7 +12315,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // movement -> Automatic, self-winding - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 9).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[9].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12200,7 +12323,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // diameter -> 44mm - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 24).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[24].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); productTissotTTouchExpertSolar.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12208,7 +12331,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // closure -> thorn close - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 25).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[25].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); #endregion productTissotT-TouchExpertSolar @@ -12241,13 +12364,9 @@ public IList Products() }; AddProductPicture(productSeikoSRPA49K1, "product_SeikoSRPA49K1.jpg"); - productSeikoSRPA49K1.ProductCategories.Add(new ProductCategory { Category = categoryWatches, DisplayOrder = 1 }); + productSeikoSRPA49K1.ProductCategories.Add(new ProductCategory { Category = categories["Watches"], DisplayOrder = 1 }); - productSeikoSRPA49K1.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Seiko").Single(), - DisplayOrder = 1, - }); + productSeikoSRPA49K1.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Seiko"], DisplayOrder = 1 }); productSeikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12255,7 +12374,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 1, // housing > steel - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productSeikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12263,7 +12382,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // material -> stainless steel - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productSeikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12271,7 +12390,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 2, // manufacturer > Seiko - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 20).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 16).Single() + SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 16) }); productSeikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12279,7 +12398,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // Gender -> gentlemen - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 7).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[7].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productSeikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12287,7 +12406,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // movement -> quarz - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 9).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() + SpecificationAttributeOption = specAttributes[9].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) }); productSeikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12295,7 +12414,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // closure -> folding clasp - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 25).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 2).Single() + SpecificationAttributeOption = specAttributes[25].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); productSeikoSRPA49K1.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12303,7 +12422,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // diameter -> 44mm - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 24).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[24].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); #endregion productSeikoSRPA49K1 @@ -12312,77 +12431,73 @@ public IList Products() #region productWatchesCertinaDSPodiumBigSize var productWatchesCertinaDSPodiumBigSize = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Name = "Certina DS Podium Big Size", - ShortDescription = "C001.617.26.037.00", - FullDescription = "

Since 1888, Certina has maintained an enviable reputation for its excellent watches and reliable movements. From the time of its integration into the SMH (today's Swatch Group) in the early 1980s, every Certina has been equipped with a high-quality ETA movement.

In a quartz watch movement, high-frequency oscillations are generated in a tiny synthetic crystal, then divided down electronically to provide the extreme accuracy of the Certina internal clock. A battery supplies the necessary energy.

The quartz movement is sometimes equipped with an end-of-life (EOL) indicator. When the seconds hand begins moving in four-second increments, the battery should be replaced within two weeks.

An automatic watch movement is driven by a rotor. Arm and wrist movements spin the rotor, which in turn winds the main spring. Energy is continuously produced, eliminating the need for a battery. The rate precision therefore depends on a rigorous manufacturing process and the original calibration, as well as the lifestyle of the user.

Most automatic movements are driven by an offset rotor. To earn the title of chronometer, a watch must be equipped with a movement that has obtained an official rate certificate from the COSC (Contrôle Officiel Suisse des Chronomètres). To obtain this, precision tests in different positions and at different temperatures must be carried out. These tests take place over a 15-day period. Thermocompensated means that the effective temperature inside the watch is measured and taken into account when improving precision. This allows fluctuations in the rate precision of a normal quartz watch due to temperature variations to be reduced by several seconds a week. The precision is 20 times more accurate than on a normal quartz watch, i.e. +/- 10 seconds per year (0.07 seconds/day).

", + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Name = "Certina DS Podium Big Size", + ShortDescription = "C001.617.26.037.00", + FullDescription = "

Since 1888, Certina has maintained an enviable reputation for its excellent watches and reliable movements. From the time of its integration into the SMH (today's Swatch Group) in the early 1980s, every Certina has been equipped with a high-quality ETA movement.

In a quartz watch movement, high-frequency oscillations are generated in a tiny synthetic crystal, then divided down electronically to provide the extreme accuracy of the Certina internal clock. A battery supplies the necessary energy.

The quartz movement is sometimes equipped with an end-of-life (EOL) indicator. When the seconds hand begins moving in four-second increments, the battery should be replaced within two weeks.

An automatic watch movement is driven by a rotor. Arm and wrist movements spin the rotor, which in turn winds the main spring. Energy is continuously produced, eliminating the need for a battery. The rate precision therefore depends on a rigorous manufacturing process and the original calibration, as well as the lifestyle of the user.

Most automatic movements are driven by an offset rotor. To earn the title of chronometer, a watch must be equipped with a movement that has obtained an official rate certificate from the COSC (Contrôle Officiel Suisse des Chronomètres). To obtain this, precision tests in different positions and at different temperatures must be carried out. These tests take place over a 15-day period. Thermocompensated means that the effective temperature inside the watch is measured and taken into account when improving precision. This allows fluctuations in the rate precision of a normal quartz watch due to temperature variations to be reduced by several seconds a week. The precision is 20 times more accurate than on a normal quartz watch, i.e. +/- 10 seconds per year (0.07 seconds/day).

", Sku = "P-9004", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "Certina DS Podium Big Size", - ShowOnHomePage = true, - Price = 479.00M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Certina DS Podium Big Size", + ShowOnHomePage = true, + Price = 479.00M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = thirdDeliveryTime.Id }; AddProductPicture(productWatchesCertinaDSPodiumBigSize, "product_certina_ds_podium_big.png"); - productWatchesCertinaDSPodiumBigSize.ProductCategories.Add(new ProductCategory { Category = categoryWatches, DisplayOrder = 1 }); + productWatchesCertinaDSPodiumBigSize.ProductCategories.Add(new ProductCategory { Category = categories["Watches"], DisplayOrder = 1 }); - productWatchesCertinaDSPodiumBigSize.ProductManufacturers.Add(new ProductManufacturer - { - Manufacturer = _ctx.Set().Where(c => c.Name == "Certina").Single(), - DisplayOrder = 1, - }); + productWatchesCertinaDSPodiumBigSize.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Certina"], DisplayOrder = 1 }); - productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 1, - // housing > steel - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); + productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 1, + // housing > steel + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 5, // material -> leather - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 8).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 5).Single() + SpecificationAttributeOption = specAttributes[8].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 5) }); productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 2, - // manufacturer > Certina - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 20).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 14).Single() - }); - productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute - { - AllowFiltering = true, - ShowOnProductPage = true, - DisplayOrder = 5, - // Gender -> gentlemen - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 7).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 1).Single() - }); + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 2, + // manufacturer > Certina + SpecificationAttributeOption = specAttributes[20].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 14) + }); + productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute + { + AllowFiltering = true, + ShowOnProductPage = true, + DisplayOrder = 5, + // Gender -> gentlemen + SpecificationAttributeOption = specAttributes[7].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 1) + }); productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { AllowFiltering = true, ShowOnProductPage = true, DisplayOrder = 5, // movement -> quarz - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 9).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 3).Single() + SpecificationAttributeOption = specAttributes[9].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 3) }); productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12390,7 +12505,7 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // closure -> folding clasp - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 25).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 2).Single() + SpecificationAttributeOption = specAttributes[25].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); productWatchesCertinaDSPodiumBigSize.ProductSpecificationAttributes.Add(new ProductSpecificationAttribute { @@ -12398,16 +12513,17 @@ public IList Products() ShowOnProductPage = true, DisplayOrder = 5, // diameter -> 40mm - SpecificationAttributeOption = _ctx.Set().Where(sa => sa.DisplayOrder == 24).Single().SpecificationAttributeOptions.Where(sao => sao.DisplayOrder == 2).Single() + SpecificationAttributeOption = specAttributes[24].SpecificationAttributeOptions.FirstOrDefault(x => x.DisplayOrder == 2) }); #endregion productWatchesCertinaDSPodiumBigSize - #endregion watches + #endregion watches #region gaming var manuSony = _ctx.Set().First(c => c.Name == "Sony"); + var manuMicrosoft = _ctx.Set().First(c => c.Name == "Microsoft"); var manuEASports = _ctx.Set().First(c => c.Name == "EA Sports"); var manuUbisoft = _ctx.Set().First(c => c.Name == "Ubisoft"); var categoryGaming = _ctx.Set().First(c => c.Alias == "Gaming"); @@ -12415,286 +12531,286 @@ public IList Products() var categoryGamingGames = _ctx.Set().First(c => c.Alias == "Games"); var manuWarnerHomme = _ctx.Set().First(c => c.Name == "Warner Home Video Games"); - #region bundlePs3AssassinCreed - - var productPs3 = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Sku = "Sony-PS399000", - Name = "Playstation 4 Pro", - ShortDescription = "The Sony PlayStation 4 Pro is the multi media console for next-generation digital home entertainment. It offers the Blu-ray technology, which enables you to enjoy movies in high definition.", - FullDescription = "
  • PowerPC-base Core @5.2GHz
  • 1 VMX vector unit per core
  • 512KB L2 cache
  • 7 x SPE @5.2GHz
  • 7 x 128b 128 SIMD GPRs
  • 7 x 256MB SRAM for SPE
  • * 1 of 8 SPEs reserved for redundancy total floating point performance: 218 GFLOPS
  • 1.8 TFLOPS floating point performance
  • Full HD (up to 1080p) x 2 channels
  • Multi-way programmable parallel floating point shader pipelines
  • GPU: RSX @550MHz
  • 256MB XDR Main RAM @3.2GHz
  • 256MB GDDR3 VRAM @700MHz
  • Sound: Dolby 5.1ch, DTS, LPCM, etc. (Cell-base processing)
  • Wi-Fi: IEEE 802.11 b/g
  • USB: Front x 4, Rear x 2 (USB2.0)
  • Memory Stick: standard/Duo, PRO x 1
", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, + #region bundlePs3AssassinCreed + + var productPs3 = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Sku = "Sony-PS399000", + Name = "Playstation 4 Pro", + ShortDescription = "The Sony PlayStation 4 Pro is the multi media console for next-generation digital home entertainment. It offers the Blu-ray technology, which enables you to enjoy movies in high definition.", + FullDescription = "
  • PowerPC-base Core @5.2GHz
  • 1 VMX vector unit per core
  • 512KB L2 cache
  • 7 x SPE @5.2GHz
  • 7 x 128b 128 SIMD GPRs
  • 7 x 256MB SRAM for SPE
  • * 1 of 8 SPEs reserved for redundancy total floating point performance: 218 GFLOPS
  • 1.8 TFLOPS floating point performance
  • Full HD (up to 1080p) x 2 channels
  • Multi-way programmable parallel floating point shader pipelines
  • GPU: RSX @550MHz
  • 256MB XDR Main RAM @3.2GHz
  • 256MB GDDR3 VRAM @700MHz
  • Sound: Dolby 5.1ch, DTS, LPCM, etc. (Cell-base processing)
  • Wi-Fi: IEEE 802.11 b/g
  • USB: Front x 4, Rear x 2 (USB2.0)
  • Memory Stick: standard/Duo, PRO x 1
", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, //MetaTitle = "Playstation 4 Super Slim", MetaTitle = "Playstation 4 Pro", Price = 189.00M, - OldPrice = 199.99M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, - DeliveryTimeId = firstDeliveryTime.Id - }; + OldPrice = 199.99M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, + DeliveryTimeId = firstDeliveryTime.Id + }; AddProductPicture(productPs3, "product_ps4_w_controller.jpg", "ps4-w-controller"); AddProductPicture(productPs3, "product_ps4_wo_controller.jpg", "ps4-wo-single"); - productPs3.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuSony, DisplayOrder = 1 }); - productPs3.ProductCategories.Add(new ProductCategory { Category = categoryGaming, DisplayOrder = 4 }); + productPs3.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + productPs3.ProductCategories.Add(new ProductCategory { Category = categories["Gaming"], DisplayOrder = 4 }); var productDualshock4Controller = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Sku = "Sony-PS399004", - Name = "DUALSHOCK 4 Wireless Controller", - ShortDescription = "Revolutionary. Intuitive. Precise. A revolutionary controller for a new era of gaming, the DualShock 4 Wireless Controller features familiar PlayStation controls and innovative new additions, such as a touch pad, light bar, and more.", - FullDescription = "
  • Precision Controller for PlayStation 4: The feel, shape, and sensitivity of the DualShock 4’s analog sticks and trigger buttons have been enhanced to offer players absolute control for all games
  • Sharing at your Fingertips: The addition of the Share button makes sharing your greatest gaming moments as easy as a push of a button. Upload gameplay videos and screenshots directly from your system or live-stream your gameplay, all without disturbing the game in progress.
  • New ways to Play: Revolutionary features like the touch pad, integrated light bar, and built-in speaker offer exciting new ways to experience and interact with your games and its 3.5mm audio jack offers a practical personal audio solution for gamers who want to listen to their games in private.
  • Charge Efficiently: The DualShock 4 Wireless Controller can easily be recharged by plugging it into your PlayStation 4 system, even when on standby, or with any standard charger with a micro-USB port.
", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "DUALSHOCK 4 Wireless Controller", - Price = 54.90M, + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Sku = "Sony-PS399004", + Name = "DUALSHOCK 4 Wireless Controller", + ShortDescription = "Revolutionary. Intuitive. Precise. A revolutionary controller for a new era of gaming, the DualShock 4 Wireless Controller features familiar PlayStation controls and innovative new additions, such as a touch pad, light bar, and more.", + FullDescription = "
  • Precision Controller for PlayStation 4: The feel, shape, and sensitivity of the DualShock 4’s analog sticks and trigger buttons have been enhanced to offer players absolute control for all games
  • Sharing at your Fingertips: The addition of the Share button makes sharing your greatest gaming moments as easy as a push of a button. Upload gameplay videos and screenshots directly from your system or live-stream your gameplay, all without disturbing the game in progress.
  • New ways to Play: Revolutionary features like the touch pad, integrated light bar, and built-in speaker offer exciting new ways to experience and interact with your games and its 3.5mm audio jack offers a practical personal audio solution for gamers who want to listen to their games in private.
  • Charge Efficiently: The DualShock 4 Wireless Controller can easily be recharged by plugging it into your PlayStation 4 system, even when on standby, or with any standard charger with a micro-USB port.
", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "DUALSHOCK 4 Wireless Controller", + Price = 54.90M, OldPrice = 59.90M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id }; AddProductPicture(productDualshock4Controller, "product_dualshock4.jpg"); - productDualshock4Controller.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuSony, DisplayOrder = 1 }); - productDualshock4Controller.ProductCategories.Add(new ProductCategory { Category = categoryGamingAccessories, DisplayOrder = 1 }); + productDualshock4Controller.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + productDualshock4Controller.ProductCategories.Add(new ProductCategory { Category = categories["Gaming Accessories"], DisplayOrder = 1 }); - var productMinecraft = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, + var productMinecraft = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, //Sku = "Ubi-acreed3", Sku = "PD-Minecraft4ps4", Name = "Minecraft - Playstation 4 Edition", - ShortDescription = "Third-person action-adventure title set.", - FullDescription = "

Build! Craft! Explore!

The critically acclaimed Minecraft comes to PlayStation 4, offering bigger worlds and greater draw distance than the PS3 and PS Vita editions.

Create your own world, then, build, explore and conquer. When night falls the monsters appear, so be sure to build a shelter before they arrive.

The world is only limited by your imagination! Bigger worlds and greater draw distance than PS3 and PS Vita Editions Includes all features from the PS3 version Import your PS3 and PS Vita worlds to the PS4 Editition.

", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - //MetaTitle = "Assassin's Creed III", + ShortDescription = "Third-person action-adventure title set.", + FullDescription = "

Build! Craft! Explore!

The critically acclaimed Minecraft comes to PlayStation 4, offering bigger worlds and greater draw distance than the PS3 and PS Vita editions.

Create your own world, then, build, explore and conquer. When night falls the monsters appear, so be sure to build a shelter before they arrive.

The world is only limited by your imagination! Bigger worlds and greater draw distance than PS3 and PS Vita Editions Includes all features from the PS3 version Import your PS3 and PS Vita worlds to the PS4 Editition.

", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + //MetaTitle = "Assassin's Creed III", MetaTitle = "Minecraft - Playstation 4 Edition", Price = 49.90M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id }; AddProductPicture(productMinecraft, "product_minecraft.jpg"); - productMinecraft.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuSony, DisplayOrder = 1 }); - productMinecraft.ProductCategories.Add(new ProductCategory { Category = categoryGamingGames, DisplayOrder = 4 }); + productMinecraft.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + productMinecraft.ProductCategories.Add(new ProductCategory { Category = categories["Games"], DisplayOrder = 4 }); - var productBundlePs3AssassinCreed = new Product - { - ProductType = ProductType.BundledProduct, - VisibleIndividually = true, - Sku = "Sony-PS399105", - Name = "PlayStation 4 Minecraft Bundle", - ShortDescription = "100GB PlayStation®4 system, 2 × DUALSHOCK®4 wireless controller and Minecraft for PS4 Edition.", - FullDescription = - "
  • Processor

    • Processor Technology : Cell Broadband Engine™
  • General

    • Communication : Ethernet (10BASE-T, 100BASE-TX, 1000BASE-T IEEE 802.11 b/g Wi-Fi
      Bluetooth 2.0 (EDR)
    • Inputs and Outputs : USB 2.0 X 2
  • Graphics

    • Graphics Processor : RSX
  • Memory

    • Internal Memory : 256MB XDR Main RAM
      256MB GDDR3 VRAM
  • Power

    • Power Consumption (in Operation) : Approximately 250 watts
  • Storage

    • Storage Capacity : 2.5' Serial ATA (500GB)
  • Video

    • Resolution : 480i, 480p, 720p, 1080i, 1080p (24p/60p)
  • Weights and Measurements

    • Dimensions (Approx.) : Approximately 11.42\" (W) x 2.56\" (H) x 11.42\" (D) (290mm x 65mm x 290mm)
    • Weight (Approx.) : Approximately 7.055 lbs (3.2 kg)
", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, + var productBundlePs3AssassinCreed = new Product + { + ProductType = ProductType.BundledProduct, + VisibleIndividually = true, + Sku = "Sony-PS399105", + Name = "PlayStation 4 Minecraft Bundle", + ShortDescription = "100GB PlayStation®4 system, 2 × DUALSHOCK®4 wireless controller and Minecraft for PS4 Edition.", + FullDescription = + "
  • Processor

    • Processor Technology : Cell Broadband Engine™
  • General

    • Communication : Ethernet (10BASE-T, 100BASE-TX, 1000BASE-T IEEE 802.11 b/g Wi-Fi
      Bluetooth 2.0 (EDR)
    • Inputs and Outputs : USB 2.0 X 2
  • Graphics

    • Graphics Processor : RSX
  • Memory

    • Internal Memory : 256MB XDR Main RAM
      256MB GDDR3 VRAM
  • Power

    • Power Consumption (in Operation) : Approximately 250 watts
  • Storage

    • Storage Capacity : 2.5' Serial ATA (500GB)
  • Video

    • Resolution : 480i, 480p, 720p, 1080i, 1080p (24p/60p)
  • Weights and Measurements

    • Dimensions (Approx.) : Approximately 11.42\" (W) x 2.56\" (H) x 11.42\" (D) (290mm x 65mm x 290mm)
    • Weight (Approx.) : Approximately 7.055 lbs (3.2 kg)
", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, MetaTitle = "PlayStation 4 Minecraft Bundle", Price = 269.97M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id, ShowOnHomePage = true, - BundleTitleText = "Bundle includes", - BundlePerItemPricing = true, - BundlePerItemShoppingCart = true - }; + BundleTitleText = "Bundle includes", + BundlePerItemPricing = true, + BundlePerItemShoppingCart = true + }; AddProductPicture(productBundlePs3AssassinCreed, "ps4_bundle_minecraft.jpg"); - productBundlePs3AssassinCreed.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuSony, DisplayOrder = 1 }); - productBundlePs3AssassinCreed.ProductCategories.Add(new ProductCategory { Category = categoryGaming, DisplayOrder = 1 }); + productBundlePs3AssassinCreed.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + productBundlePs3AssassinCreed.ProductCategories.Add(new ProductCategory { Category = categories["Gaming"], DisplayOrder = 1 }); - #endregion bundlePs3AssassinCreed + #endregion bundlePs3AssassinCreed - #region bundlePs4 + #region bundlePs4 - var productPs4 = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Sku = "Sony-PS410034", + var productPs4 = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Sku = "Sony-PS410034", //Sku = "PS410037", Name = "PlayStation 4", - ShortDescription = "The best place to play. Working with some of the most creative minds in the industry, PlayStation®4 delivers breathtaking and unique gaming experiences.", - FullDescription = "

The power to perform.

PlayStation®4 was designed from the ground up to ensure that game creators can unleash their imaginations to develop the very best games and deliver new play experiences never before possible. With ultra-fast customized processors and 8GB of high-performance unified system memory, PS4™ is the home to games with rich, high-fidelity graphics and deeply immersive experiences that shatter expectations.

  • Processor

    • Processor Technology : Low power x86-64 AMD 'Jaguar', 8 cores
  • Software

    • Processor : Single-chip custom processor
  • Display

    • Display Technology : HDMI
      Digital Output (optical)
  • General

    • Ethernet ports x speed : Ethernet (10BASE-T, 100BASE-TX, 1000BASE-T); IEEE 802.11 b/g/n; Bluetooth® 2.1 (EDR)
    • Hard disk : Built-in
  • General Specifications

    • Video : BD 6xCAV
      DVD 8xCAV
  • Graphics

    • Graphics Processor : 1.84 TFLOPS, AMD Radeon™ Graphics Core Next engine
  • Interface

    • I/O Port : Super-Speed USB (USB 3.0), AUX
  • Memory

    • Internal Memory : GDDR5 8GB

", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "PlayStation 4", - Price = 399.99M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 3, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + ShortDescription = "The best place to play. Working with some of the most creative minds in the industry, PlayStation®4 delivers breathtaking and unique gaming experiences.", + FullDescription = "

The power to perform.

PlayStation®4 was designed from the ground up to ensure that game creators can unleash their imaginations to develop the very best games and deliver new play experiences never before possible. With ultra-fast customized processors and 8GB of high-performance unified system memory, PS4™ is the home to games with rich, high-fidelity graphics and deeply immersive experiences that shatter expectations.

  • Processor

    • Processor Technology : Low power x86-64 AMD 'Jaguar', 8 cores
  • Software

    • Processor : Single-chip custom processor
  • Display

    • Display Technology : HDMI
      Digital Output (optical)
  • General

    • Ethernet ports x speed : Ethernet (10BASE-T, 100BASE-TX, 1000BASE-T); IEEE 802.11 b/g/n; Bluetooth® 2.1 (EDR)
    • Hard disk : Built-in
  • General Specifications

    • Video : BD 6xCAV
      DVD 8xCAV
  • Graphics

    • Graphics Processor : 1.84 TFLOPS, AMD Radeon™ Graphics Core Next engine
  • Interface

    • I/O Port : Super-Speed USB (USB 3.0), AUX
  • Memory

    • Internal Memory : GDDR5 8GB

", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "PlayStation 4", + Price = 399.99M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 3, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id }; AddProductPicture(productPs4, "product_sony_ps4.png", "sony-ps4"); AddProductPicture(productPs4, "product_sony_dualshock4_wirelesscontroller.png", "sony-ps4-dualshock"); - productPs4.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuSony, DisplayOrder = 1 }); - productPs4.ProductCategories.Add(new ProductCategory { Category = categoryGaming, DisplayOrder = 5 }); + productPs4.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + productPs4.ProductCategories.Add(new ProductCategory { Category = categories["Gaming"], DisplayOrder = 5 }); var productPs4Camera = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Sku = "Sony-PS410040", - Name = "PlayStation 4 Camera", - ShortDescription = "Play, challenge and share your epic gaming moments with PlayStation®Camera and your PS4™. Multiplayer is enhanced through immediate, crystal clear audio and picture-in-picture video sharing.", - FullDescription = "
  • When combined with the DualShock 4 Wireless Controller's light bar, the evolutionary 3D depth-sensing technology in the PlayStation Camera allows it to precisely track a player's position in the room.
  • From navigational voice commands to facial recognition, the PlayStation Camera adds incredible innovation to your gaming.
  • Automatically integrate a picture-in-picture video of yourself during gameplay broadcasts, and challenge your friends during play.
  • Never leave a friend hanging or miss a chance to taunt your opponents with voice chat that keeps the conversation going, whether it's between rounds, between games, or just while kicking back.
", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "PlayStation 4 Camera", - Price = 59.99M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Sku = "Sony-PS410040", + Name = "PlayStation 4 Camera", + ShortDescription = "Play, challenge and share your epic gaming moments with PlayStation®Camera and your PS4™. Multiplayer is enhanced through immediate, crystal clear audio and picture-in-picture video sharing.", + FullDescription = "
  • When combined with the DualShock 4 Wireless Controller's light bar, the evolutionary 3D depth-sensing technology in the PlayStation Camera allows it to precisely track a player's position in the room.
  • From navigational voice commands to facial recognition, the PlayStation Camera adds incredible innovation to your gaming.
  • Automatically integrate a picture-in-picture video of yourself during gameplay broadcasts, and challenge your friends during play.
  • Never leave a friend hanging or miss a chance to taunt your opponents with voice chat that keeps the conversation going, whether it's between rounds, between games, or just while kicking back.
", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "PlayStation 4 Camera", + Price = 59.99M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id }; AddProductPicture(productPs4Camera, "product_sony_ps4_camera.png"); - productPs4Camera.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuSony, DisplayOrder = 1 }); - productPs4Camera.ProductCategories.Add(new ProductCategory { Category = categoryGamingAccessories, DisplayOrder = 3 }); + productPs4Camera.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + productPs4Camera.ProductCategories.Add(new ProductCategory { Category = categories["Gaming Accessories"], DisplayOrder = 3 }); - var productBundlePs4 = new Product - { - ProductType = ProductType.BundledProduct, - VisibleIndividually = true, - Sku = "Sony-PS410099", - Name = "PlayStation 4 Bundle", - ShortDescription = "PlayStation®4 system, DUALSHOCK®4 wireless controller and PS4 camera.", - FullDescription = - "

The best place to play

PlayStation 4 is the best place to play with dynamic, connected gaming, powerful graphics and speed, intelligent personalization, deeply integrated social capabilities, and innovative second-screen features. Combining unparalleled content, immersive gaming experiences, all of your favorite digital entertainment apps, and PlayStation exclusives, PS4 centers on gamers, enabling them to play when, where and how they want. PS4 enables the greatest game developers in the world to unlock their creativity and push the boundaries of play through a system that is tuned specifically to their needs.

Gamer focused, developer inspired

The PS4 system focuses on the gamer, ensuring that the very best games and the most immersive experiences are possible on the platform. The PS4 system enables the greatest game developers in the world to unlock their creativity and push the boundaries of play through a system that is tuned specifically to their needs. The PS4 system is centered around a powerful custom chip that contains eight x86-64 cores and a state of the art 1.84 TFLOPS graphics processor with 8 GB of ultra-fast GDDR5 unified system memory, easing game creation and increasing the richness of content achievable on the platform. The end result is new games with rich, high-fidelity graphics and deeply immersive experiences.

Personalized, curated content

The PS4 system has the ability to learn about your preferences. It will learn your likes and dislikes, allowing you to discover content pre-loaded and ready to go on your console in your favorite game genres or by your favorite creators. Players also can look over game-related information shared by friends, view friends’ gameplay with ease, or obtain information about recommended content, including games, TV shows and movies.

New DUALSHOCK controller

DUALSHOCK 4 features new innovations to deliver more immersive gaming experiences, including a highly sensitive six-axis sensor as well as a touch pad located on the top of the controller, which offers gamers completely new ways to play and interact with games.

Shared game experiences

Engage in endless personal challenges with your community and share your epic triumphs with the press of a button. Simply hit the SHARE button on the controller, scan through the last few minutes of gameplay, tag it and return to the game—the video uploads as you play. The PS4 system also enhances social spectating by enabling you to broadcast your gameplay in real-time.

Remote play

Remote Play on the PS4 system fully unlocks the PlayStation Vita system’s potential, making it the ultimate companion device. With the PS Vita system, gamers will be able to seamlessly play a range of PS4 titles on the beautiful 5-inch display over Wi-Fi access points in a local area network.

PlayStation app

The PlayStation App will enable iPhone, iPad, and Android-based smartphones and tablets to become second screens for the PS4 system. Once installed on these devices, players can view in-game items, purchase PS4 games and download them directly to the console at home, or remotely watch the gameplay of other gamers playing on their devices.

PlayStation Plus

Built to bring games and gamers together and fuel the next generation of gaming, PlayStation Plus helps you discover a world of exceptional gaming experiences. PlayStation Plus is a membership service that takes your gaming experience to the next level. Each month members receive an Instant Game Collection of top rated blockbuster and innovative Indie games, which they can download direct to their console.

", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "PlayStation 4 Bundle", - Price = 429.99M, - OldPrice = 449.99M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + var productBundlePs4 = new Product + { + ProductType = ProductType.BundledProduct, + VisibleIndividually = true, + Sku = "Sony-PS410099", + Name = "PlayStation 4 Bundle", + ShortDescription = "PlayStation®4 system, DUALSHOCK®4 wireless controller and PS4 camera.", + FullDescription = + "

The best place to play

PlayStation 4 is the best place to play with dynamic, connected gaming, powerful graphics and speed, intelligent personalization, deeply integrated social capabilities, and innovative second-screen features. Combining unparalleled content, immersive gaming experiences, all of your favorite digital entertainment apps, and PlayStation exclusives, PS4 centers on gamers, enabling them to play when, where and how they want. PS4 enables the greatest game developers in the world to unlock their creativity and push the boundaries of play through a system that is tuned specifically to their needs.

Gamer focused, developer inspired

The PS4 system focuses on the gamer, ensuring that the very best games and the most immersive experiences are possible on the platform. The PS4 system enables the greatest game developers in the world to unlock their creativity and push the boundaries of play through a system that is tuned specifically to their needs. The PS4 system is centered around a powerful custom chip that contains eight x86-64 cores and a state of the art 1.84 TFLOPS graphics processor with 8 GB of ultra-fast GDDR5 unified system memory, easing game creation and increasing the richness of content achievable on the platform. The end result is new games with rich, high-fidelity graphics and deeply immersive experiences.

Personalized, curated content

The PS4 system has the ability to learn about your preferences. It will learn your likes and dislikes, allowing you to discover content pre-loaded and ready to go on your console in your favorite game genres or by your favorite creators. Players also can look over game-related information shared by friends, view friends’ gameplay with ease, or obtain information about recommended content, including games, TV shows and movies.

New DUALSHOCK controller

DUALSHOCK 4 features new innovations to deliver more immersive gaming experiences, including a highly sensitive six-axis sensor as well as a touch pad located on the top of the controller, which offers gamers completely new ways to play and interact with games.

Shared game experiences

Engage in endless personal challenges with your community and share your epic triumphs with the press of a button. Simply hit the SHARE button on the controller, scan through the last few minutes of gameplay, tag it and return to the game—the video uploads as you play. The PS4 system also enhances social spectating by enabling you to broadcast your gameplay in real-time.

Remote play

Remote Play on the PS4 system fully unlocks the PlayStation Vita system’s potential, making it the ultimate companion device. With the PS Vita system, gamers will be able to seamlessly play a range of PS4 titles on the beautiful 5-inch display over Wi-Fi access points in a local area network.

PlayStation app

The PlayStation App will enable iPhone, iPad, and Android-based smartphones and tablets to become second screens for the PS4 system. Once installed on these devices, players can view in-game items, purchase PS4 games and download them directly to the console at home, or remotely watch the gameplay of other gamers playing on their devices.

PlayStation Plus

Built to bring games and gamers together and fuel the next generation of gaming, PlayStation Plus helps you discover a world of exceptional gaming experiences. PlayStation Plus is a membership service that takes your gaming experience to the next level. Each month members receive an Instant Game Collection of top rated blockbuster and innovative Indie games, which they can download direct to their console.

", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "PlayStation 4 Bundle", + Price = 429.99M, + OldPrice = 449.99M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id, BundleTitleText = "Bundle includes" - }; + }; AddProductPicture(productBundlePs4, "product_sony_ps4_bundle.png"); - productBundlePs4.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuSony, DisplayOrder = 1 }); - productBundlePs4.ProductCategories.Add(new ProductCategory { Category = categoryGaming, DisplayOrder = 2 }); + productBundlePs4.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + productBundlePs4.ProductCategories.Add(new ProductCategory { Category = categories["Gaming"], DisplayOrder = 2 }); - #endregion bundlePs4 + #endregion bundlePs4 - #region groupAccessories + #region groupAccessories - var productGroupAccessories = new Product - { - ProductType = ProductType.GroupedProduct, - VisibleIndividually = true, - Sku = "Sony-GroupAccessories", - Name = "Accessories for unlimited gaming experience", - ShortDescription = "The future of gaming is now with dynamic, connected gaming, powerful graphics and speed, intelligent personalization, deeply integrated social capabilities, and innovative second-screen features. The brilliant culmination of the most creative minds in the industry, PlayStation®4 delivers a unique gaming environment that will take your breath away.", - FullDescription = "
  • Immerse yourself in a new world of gameplay with powerful graphics and speed.
  • Eliminate lengthy load times of saved games with Suspend mode.
  • Immediately play digital titles without waiting for them to finish downloading thanks to background downloading and updating capability.
  • Instantly share images and videos of your favorite gameplay moments on Facebook with the SHARE button on the DUALSHOCK®4 controller.
  • Broadcast while you play in real-time through Ustream.
", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "Accessories for unlimited gaming experience", - Price = 0.0M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 3, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, - ShowOnHomePage = true - }; + var productGroupAccessories = new Product + { + ProductType = ProductType.GroupedProduct, + VisibleIndividually = true, + Sku = "Sony-GroupAccessories", + Name = "Accessories for unlimited gaming experience", + ShortDescription = "The future of gaming is now with dynamic, connected gaming, powerful graphics and speed, intelligent personalization, deeply integrated social capabilities, and innovative second-screen features. The brilliant culmination of the most creative minds in the industry, PlayStation®4 delivers a unique gaming environment that will take your breath away.", + FullDescription = "
  • Immerse yourself in a new world of gameplay with powerful graphics and speed.
  • Eliminate lengthy load times of saved games with Suspend mode.
  • Immediately play digital titles without waiting for them to finish downloading thanks to background downloading and updating capability.
  • Instantly share images and videos of your favorite gameplay moments on Facebook with the SHARE button on the DUALSHOCK®4 controller.
  • Broadcast while you play in real-time through Ustream.
", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Accessories for unlimited gaming experience", + Price = 0.0M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 3, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, + ShowOnHomePage = true + }; AddProductPicture(productGroupAccessories, "category_gaming_accessories.png"); - productGroupAccessories.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuSony, DisplayOrder = 1 }); - productGroupAccessories.ProductCategories.Add(new ProductCategory { Category = categoryGaming, DisplayOrder = 3 }); + productGroupAccessories.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + productGroupAccessories.ProductCategories.Add(new ProductCategory { Category = categories["Gaming"], DisplayOrder = 3 }); - #endregion groupAccessories + #endregion groupAccessories - #region Ps3PlusOneGame + #region Ps3PlusOneGame - var productPrinceOfPersia = new Product - { - ProductType = ProductType.SimpleProduct, - VisibleIndividually = true, - Sku = "Ubi-princepersia", - Name = "Prince of Persia \"The Forgotten Sands\"", - ShortDescription = "Play the epic story of the heroic Prince as he fights and outwits his enemies in order to save his kingdom.", - FullDescription = "

This game marks the return to the Prince of Persia® Sands of Time storyline. Prince of Persia: The Forgotten Sands™ will feature many of the fan-favorite elements from the original series as well as new gameplay innovations that gamers have come to expect from Prince of Persia.

Experience the story, setting, and gameplay in this return to the Sands of Time universe as we follow the original Prince of Persia through a new untold chapter.

Created by Ubisoft Montreal, the team that brought you various Prince of Persia® and Assassin’s Creed® games, Prince of Persia The Forgotten Sands™ has been over 2 years in the making.

", - ProductTemplateId = productTemplate.Id, - AllowCustomerReviews = true, - Published = true, - MetaTitle = "Prince of Persia", - Price = 39.90M, - ManageInventoryMethod = ManageInventoryMethod.DontManageStock, - OrderMinimumQuantity = 1, - OrderMaximumQuantity = 10000, - StockQuantity = 10000, - NotifyAdminForQuantityBelow = 1, - AllowBackInStockSubscriptions = false, - IsShipEnabled = true, + var productPrinceOfPersia = new Product + { + ProductType = ProductType.SimpleProduct, + VisibleIndividually = true, + Sku = "Ubi-princepersia", + Name = "Prince of Persia \"The Forgotten Sands\"", + ShortDescription = "Play the epic story of the heroic Prince as he fights and outwits his enemies in order to save his kingdom.", + FullDescription = "

This game marks the return to the Prince of Persia® Sands of Time storyline. Prince of Persia: The Forgotten Sands™ will feature many of the fan-favorite elements from the original series as well as new gameplay innovations that gamers have come to expect from Prince of Persia.

Experience the story, setting, and gameplay in this return to the Sands of Time universe as we follow the original Prince of Persia through a new untold chapter.

Created by Ubisoft Montreal, the team that brought you various Prince of Persia® and Assassin’s Creed® games, Prince of Persia The Forgotten Sands™ has been over 2 years in the making.

", + ProductTemplateId = productTemplate.Id, + AllowCustomerReviews = true, + Published = true, + MetaTitle = "Prince of Persia", + Price = 39.90M, + ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + OrderMinimumQuantity = 1, + OrderMaximumQuantity = 10000, + StockQuantity = 10000, + NotifyAdminForQuantityBelow = 1, + AllowBackInStockSubscriptions = false, + IsShipEnabled = true, DeliveryTimeId = firstDeliveryTime.Id }; AddProductPicture(productPrinceOfPersia, "products_princeofpersia.jpg"); - productPrinceOfPersia.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuUbisoft, DisplayOrder = 1 }); - productPrinceOfPersia.ProductCategories.Add(new ProductCategory { Category = categoryGamingGames, DisplayOrder = 2 }); + productPrinceOfPersia.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Ubisoft"], DisplayOrder = 1 }); + productPrinceOfPersia.ProductCategories.Add(new ProductCategory { Category = categories["Games"], DisplayOrder = 2 }); #endregion Ps3PlusOneGame @@ -12726,8 +12842,8 @@ public IList Products() }; AddProductPicture(productHorizonZeroDown, "product_horizon.jpg"); - productHorizonZeroDown.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuSony, DisplayOrder = 1 }); - productHorizonZeroDown.ProductCategories.Add(new ProductCategory { Category = categoryGamingGames, DisplayOrder = 2 }); + productHorizonZeroDown.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Sony"], DisplayOrder = 1 }); + productHorizonZeroDown.ProductCategories.Add(new ProductCategory { Category = categories["Games"], DisplayOrder = 2 }); #endregion Horizon Zero Down @@ -12759,8 +12875,8 @@ public IList Products() }; AddProductPicture(productFifa17, "product_fifa17.jpg"); - productFifa17.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuEASports, DisplayOrder = 1 }); - productFifa17.ProductCategories.Add(new ProductCategory { Category = categoryGamingGames, DisplayOrder = 2 }); + productFifa17.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["EA Sports"], DisplayOrder = 1 }); + productFifa17.ProductCategories.Add(new ProductCategory { Category = categories["Games"], DisplayOrder = 2 }); #endregion Fifa 17 @@ -12791,11 +12907,43 @@ public IList Products() }; AddProductPicture(productLegoWorlds, "product_legoworlds.jpg"); - productLegoWorlds.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuWarnerHomme, DisplayOrder = 1 }); - productLegoWorlds.ProductCategories.Add(new ProductCategory { Category = categoryGamingGames, DisplayOrder = 3 }); + productLegoWorlds.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manufacturers["Warner Home Video Games"], DisplayOrder = 1 }); + productLegoWorlds.ProductCategories.Add(new ProductCategory { Category = categories["Games"], DisplayOrder = 3 }); #endregion Lego Worlds + //#region XBox One S + + //var productXBoxOneS = new Product + //{ + // ProductType = ProductType.SimpleProduct, + // VisibleIndividually = true, + // Sku = "Microsoft-xbox1s", + // Name = "Xbox One S 500 GB Konsole", + // ShortDescription = "Genieße über 100 Spiele, die es nur für die Konsole gibt, sowie eine ständig größer werdende Bibliothek an Xbox 360-Spielen auf der Xbox One S im neuen Design – der einzigen Konsole mit 4K Ultra HD Blu-ray, 4K-Videostreaming und HDR. Streame deine Lieblingsfilme und -sendungen in atemberaubendem 4K Ultra HD. Spiele Blockbuster wie Gears of War 4 und Battlefield 1 mit Freunden auf Xbox Live, dem schnellsten und zuverlässigsten Gaming-Netzwerk.", + // FullDescription = "
  • Die ultimativen Spiele und 4K-Entertainment-System.
  • 40 % kompaktere Konsole
    Lasse dich nicht von der Größe täuschen. Mit integriertem Netzteil und bis zu 2 TB Speicherplatz ist die Xbox One S die fortschrittlichste Xbox überhaupt.
  • Der beste Controller - jetzt noch besser
    Der neue Xbox Wireless Controller bietet ein schlankes, optimiertes Design, texturierte Grip - Fläche und Bluetooth zum Spielen auf Windows 10 Geräten. Genieße individuelle Tastenbelegung und verbesserte drahtlose Reichweite und stecke jeden kompatiblen Kopfhörer mit der 3, 5 mm Stereo - Headset - Buchse ein.
", + // ProductTemplateId = productTemplate.Id, + // AllowCustomerReviews = true, + // Published = true, + // MetaTitle = "Xbox One S", + // Price = 279.99M, + // OldPrice = 279.99M, + // ManageInventoryMethod = ManageInventoryMethod.DontManageStock, + // OrderMinimumQuantity = 1, + // OrderMaximumQuantity = 10000, + // StockQuantity = 10000, + // NotifyAdminForQuantityBelow = 1, + // AllowBackInStockSubscriptions = false, + // IsShipEnabled = true, + // DeliveryTimeId = firstDeliveryTime.Id + //}; + + //AddProductPicture(productXBoxOneS, "product_xbox_one_s_1.png", "microsoft-xbox-one-s-1"); + //productXBoxOneS.ProductManufacturers.Add(new ProductManufacturer { Manufacturer = manuMicrosoft, DisplayOrder = 1 }); + //productXBoxOneS.ProductCategories.Add(new ProductCategory { Category = categoryGaming, DisplayOrder = 1 }); + + //#endregion + #endregion gaming var entities = new List @@ -12811,12 +12959,11 @@ public IList Products() productInstantDownloadVivaldi, productInstantDownloadBeethoven, productWatchesCertinaDSPodiumBigSize, productPs3, productMinecraft, productBundlePs3AssassinCreed, productPs4, productDualshock4Controller, productPs4Camera, productBundlePs4, - productGroupAccessories, - productPrinceOfPersia, productLegoWorlds, productHorizonZeroDown, productFifa17 + productGroupAccessories, productPrinceOfPersia, productLegoWorlds, productHorizonZeroDown, productFifa17, productJackWolfsKinKanukaPoint }; - entities.AddRange(GetFashionProducts()); - entities.AddRange(GetFurnitureProducts()); + entities.AddRange(GetFashionProducts(specAttributes)); + entities.AddRange(GetFurnitureProducts(specAttributes)); this.Alter(entities); return entities; @@ -13067,9 +13214,11 @@ public IList ForumGroups() public IList Forums() { - var newProductsForum = new Forum + var group = _ctx.Set().FirstOrDefault(c => c.DisplayOrder == 1); + + var newProductsForum = new Forum { - ForumGroup = _ctx.Set().Where(c => c.DisplayOrder == 1).Single(), + ForumGroup = group, Name = "New Products", Description = "Discuss new products and industry trends", NumTopics = 0, @@ -13081,7 +13230,7 @@ public IList Forums() var packagingShippingForum = new Forum { - ForumGroup = _ctx.Set().Where(c => c.DisplayOrder == 1).Single(), + ForumGroup = group, Name = "Packaging & Shipping", Description = "Discuss packaging & shipping", NumTopics = 0, @@ -13163,34 +13312,44 @@ public IList DeliveryTimes() public IList QuantityUnits() { - var entities = new List() - { - new QuantityUnit - { - Name = "Piece", - Description = "Piece", - IsDefault = true, - DisplayOrder = 0, - }, - new QuantityUnit - { - Name = "Box", - Description = "Box", - DisplayOrder = 1, - }, - new QuantityUnit - { - Name = "Parcel", - Description = "Parcel", - DisplayOrder = 2, - }, - new QuantityUnit - { - Name = "Palette", - Description = "Palette", - DisplayOrder = 3, - }, - }; + var count = 0; + var entities = new List(); + + var quPluralEn = new Dictionary + { + { "Piece", "Pieces" }, + { "Box", "Boxes" }, + { "Parcel", "Parcels" }, + { "Palette", "Pallets" }, + { "Unit", "Units" }, + { "Sack", "Sacks" }, + { "Bag", "Bags" }, + { "Can", "Cans" }, + { "Packet", "Packets" }, + { "Bar", "Bars" }, + { "Bottle", "Bottles" }, + { "Glass", "Glasses" }, + { "Bunch", "Bunches" }, + { "Roll", "Rolls" }, + { "Cup", "Cups" }, + { "Bundle", "Bundles" }, + { "Barrel", "Barrels" }, + { "Set", "Sets" }, + { "Bucket", "Buckets" } + }; + + foreach (var qu in quPluralEn) + { + entities.Add(new QuantityUnit + { + Name = qu.Key, + NamePlural = qu.Value, + Description = qu.Key, + IsDefault = qu.Key == "Piece", + DisplayOrder = count++ + }); + } + this.Alter(entities); return entities; } @@ -13638,7 +13797,7 @@ protected string SampleDownloadsPath languageId = x.LanguageId; break; case Topic x: - name = SeoHelper.GetSeName(x.SystemName, true, false).Truncate(400); + name = SeoHelper.GetSeName(x.SystemName, true, false, true).Truncate(400); break; } diff --git a/src/Libraries/SmartStore.Data/SmartDbConfiguration.cs b/src/Libraries/SmartStore.Data/SmartDbConfiguration.cs index 3f6c70b9ba..d18414d6e9 100644 --- a/src/Libraries/SmartStore.Data/SmartDbConfiguration.cs +++ b/src/Libraries/SmartStore.Data/SmartDbConfiguration.cs @@ -17,10 +17,13 @@ public class SmartDbConfiguration : DbConfiguration public SmartDbConfiguration() { // DB model caching - var cacheLocation = CommonHelper.MapPath("~/App_Data/EfCache"); - System.IO.Directory.CreateDirectory(cacheLocation); - var modelStore = new EfDbModelStore(cacheLocation); - AddDependencyResolver(new SingletonDependencyResolver(modelStore)); + if (HostingEnvironment.IsHosted) + { + var cacheLocation = CommonHelper.MapPath("~/App_Data/EfCache"); + System.IO.Directory.CreateDirectory(cacheLocation); + var modelStore = new EfDbModelStore(cacheLocation); + AddDependencyResolver(new SingletonDependencyResolver(modelStore)); + } IEfDataProvider provider = null; try diff --git a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj index b2c08c0fc4..9f7c8ce217 100644 --- a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj +++ b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj @@ -1,5 +1,6 @@  - + + Debug AnyCPU @@ -669,6 +670,22 @@ 201905101159134_V320Resources.cs + + + 201905271110370_V321Resources.cs + + + + 201906252008551_QuantityUnitNamePlural.cs + + + + 201907032251575_CategoryExternalLink.cs + + + + 201908150749388_V322Resources.cs + @@ -1199,6 +1216,18 @@ 201905101159134_V320Resources.cs + + 201905271110370_V321Resources.cs + + + 201906252008551_QuantityUnitNamePlural.cs + + + 201907032251575_CategoryExternalLink.cs + + + 201908150749388_V322Resources.cs + diff --git a/src/Libraries/SmartStore.Data/Utilities/DataMigrator.cs b/src/Libraries/SmartStore.Data/Utilities/DataMigrator.cs index b93c7ca64b..49c93d1b22 100644 --- a/src/Libraries/SmartStore.Data/Utilities/DataMigrator.cs +++ b/src/Libraries/SmartStore.Data/Utilities/DataMigrator.cs @@ -506,13 +506,13 @@ public static void CreateSystemMenus(IDbContext context) .Where(x => x.LanguageId == defaultLang.Id && resourceNames.Contains(x.ResourceName)) .Select(x => new { x.ResourceName, x.ResourceValue }) .ToList() - .ToDictionarySafe(x => x.ResourceName, x => x.ResourceValue); + .ToDictionarySafe(x => x.ResourceName, x => x.ResourceValue, StringComparer.OrdinalIgnoreCase); settings = context.Set().AsNoTracking() .Where(x => x.StoreId == 0 && settingNames.Contains(x.Name)) .Select(x => new { x.Name, x.Value }) .ToList() - .ToDictionarySafe(x => x.Name, x => x.Value); + .ToDictionarySafe(x => x.Name, x => x.Value, StringComparer.OrdinalIgnoreCase); #region System menus diff --git a/src/Libraries/SmartStore.Services/Blogs/BlogService.cs b/src/Libraries/SmartStore.Services/Blogs/BlogService.cs index ea9c74c547..7cdfafa8b7 100644 --- a/src/Libraries/SmartStore.Services/Blogs/BlogService.cs +++ b/src/Libraries/SmartStore.Services/Blogs/BlogService.cs @@ -163,10 +163,10 @@ public virtual IPagedList GetAllBlogPostsByTag( foreach (var blogPost in blogPostsAll) { - var tags = blogPost.ParseTags().Select(x => SeoHelper.GetSeName(x, _seoSettings.ConvertNonWesternChars, _seoSettings.AllowUnicodeCharsInUrls, + true, _seoSettings.SeoNameCharConversion)); if (!String.IsNullOrEmpty(tags.FirstOrDefault(t => t.Equals(tag, StringComparison.InvariantCultureIgnoreCase)))) diff --git a/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs b/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs index bd1dd2f669..f0c104405b 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs @@ -20,7 +20,7 @@ namespace SmartStore.Services.Catalog { - public partial class CategoryService : ICategoryService, IConsumer + public partial class CategoryService : ICategoryService { internal static TimeSpan CategoryTreeCacheDuration = TimeSpan.FromHours(6); @@ -85,11 +85,6 @@ public CategoryService(IRequestCache requestCache, QuerySettings = DbQuerySettings.Default; } - public void HandleEvent(CategoryTreeChangedEvent e) - { - - } - public DbQuerySettings QuerySettings { get; set; } private void DeleteAllCategories(IList categories, bool delete) @@ -779,6 +774,7 @@ public TreeNode GetCategoryTree(int rootCategoryId = 0, bool incl x.Id, x.ParentCategoryId, x.Name, + x.ExternalLink, x.Alias, x.PictureId, x.Published, @@ -795,6 +791,7 @@ public TreeNode GetCategoryTree(int rootCategoryId = 0, bool incl Id = x.Id, ParentCategoryId = x.ParentCategoryId, Name = x.Name, + ExternalLink = x.ExternalLink, Alias = x.Alias, PictureId = x.PictureId, Published = x.Published, diff --git a/src/Libraries/SmartStore.Services/Catalog/CategoryTreeChangeHandler.cs b/src/Libraries/SmartStore.Services/Catalog/CategoryTreeChangeHandler.cs index ef476d9658..804584a665 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CategoryTreeChangeHandler.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CategoryTreeChangeHandler.cs @@ -48,7 +48,7 @@ public class CategoryTreeChangeHook : IDbSaveHook, IConsumer // 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 string[] _d = new string[] { "Name", "Alias", "ExternalLink", "PictureId", "BadgeText", "BadgeStyle" }; private static readonly HashSet _candidateTypes = new HashSet { @@ -145,6 +145,7 @@ public void OnBeforeSave(IHookedEntity entry) else { value.Name = category.Name; + value.ExternalLink = category.ExternalLink; value.Alias = category.Alias; value.PictureId = category.PictureId; value.BadgeText = category.BadgeText; diff --git a/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs b/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs index 997f6372bc..14daae3178 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Drawing; using System.IO; using System.Linq; using System.Linq.Expressions; @@ -9,7 +8,6 @@ using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Domain.Media; -using SmartStore.Core.Events; using SmartStore.Services.DataExchange.Import; using SmartStore.Services.DataExchange.Import.Events; using SmartStore.Services.Localization; @@ -18,11 +16,10 @@ namespace SmartStore.Services.Catalog.Importer { - public class CategoryImporter : EntityImporterBase + public class CategoryImporter : EntityImporterBase { private readonly IRepository _categoryRepository; private readonly IRepository _pictureRepository; - private readonly ICommonServices _services; private readonly ICategoryTemplateService _categoryTemplateService; private readonly IPictureService _pictureService; private readonly ILocalizedEntityService _localizedEntityService; @@ -42,7 +39,6 @@ public class CategoryImporter : EntityImporterBase public CategoryImporter( IRepository categoryRepository, IRepository pictureRepository, - ICommonServices services, ICategoryTemplateService categoryTemplateService, IPictureService pictureService, ILocalizedEntityService localizedEntityService, @@ -50,7 +46,6 @@ public CategoryImporter( { _categoryRepository = categoryRepository; _pictureRepository = pictureRepository; - _services = services; _categoryTemplateService = categoryTemplateService; _pictureService = pictureService; _localizedEntityService = localizedEntityService; @@ -82,20 +77,20 @@ protected override void Import(ImportExecuteContext context) { ProcessCategories(context, batch, templateViewPaths, srcToDestId); } - catch (Exception exception) + catch (Exception ex) { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessCategories"); + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessCategories"); } - // reduce batch to saved (valid) products. + // Reduce batch to saved (valid) products. // No need to perform import operations on errored products. batch = batch.Where(x => x.Entity != null && !x.IsTransient).ToArray(); - // update result object + // Update result object. context.Result.NewRecords += batch.Count(x => x.IsNew && !x.IsTransient); context.Result.ModifiedRecords += batch.Count(x => !x.IsNew && !x.IsTransient); - // process slugs + // Process slugs. if (segmenter.HasColumn("SeName", true) || batch.Any(x => x.IsNew || x.NameChanged)) { try @@ -103,9 +98,9 @@ protected override void Import(ImportExecuteContext context) _categoryRepository.Context.AutoDetectChangesEnabled = true; ProcessSlugs(context, batch, typeof(Category).Name); } - catch (Exception exception) + catch (Exception ex) { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessSlugs"); + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessSlugs"); } finally { @@ -113,40 +108,40 @@ protected override void Import(ImportExecuteContext context) } } - // process store mappings + // Process store mappings. if (segmenter.HasColumn("StoreIds")) { try { ProcessStoreMappings(context, batch); } - catch (Exception exception) + catch (Exception ex) { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessStoreMappings"); + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessStoreMappings"); } } - // localizations + // Localizations. try { ProcessLocalizations(context, batch, _localizableProperties); } - catch (Exception exception) + catch (Exception ex) { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessLocalizedProperties"); + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessLocalizedProperties"); } - // process pictures - if (srcToDestId.Any() && segmenter.HasColumn("ImageUrl") && !segmenter.IsIgnored("PictureId")) + // Process pictures. + if (segmenter.HasColumn("ImageUrl") && !segmenter.IsIgnored("PictureId")) { try { _categoryRepository.Context.AutoDetectChangesEnabled = true; - ProcessPictures(context, batch, srcToDestId); + ProcessPictures(context, batch); } - catch (Exception exception) + catch (Exception ex) { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessPictures"); + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessPictures"); } finally { @@ -157,7 +152,7 @@ protected override void Import(ImportExecuteContext context) context.Services.EventPublisher.Publish(new ImportBatchExecutedEvent(context, batch)); } - // map parent id of inserted categories + // Map parent id of inserted categories. if (srcToDestId.Any() && segmenter.HasColumn("Id") && segmenter.HasColumn("ParentCategoryId") && !segmenter.IsIgnored("ParentCategoryId")) { segmenter.Reset(); @@ -171,9 +166,9 @@ protected override void Import(ImportExecuteContext context) { ProcessParentMappings(context, batch, srcToDestId); } - catch (Exception exception) + catch (Exception ex) { - context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessParentMappings"); + context.Result.AddError(ex, segmenter.CurrentSegment, "ProcessParentMappings"); } } } @@ -182,82 +177,74 @@ protected override void Import(ImportExecuteContext context) protected virtual int ProcessPictures( ImportExecuteContext context, - IEnumerable> batch, - Dictionary srcToDestId) + IEnumerable> batch) { - var equalPictureId = 0; - foreach (var row in batch) { try { var srcId = row.GetDataValue("Id"); - var urlOrPath = row.GetDataValue("ImageUrl"); - - if (srcId != 0 && srcToDestId.ContainsKey(srcId) && urlOrPath.HasValue()) - { - var currentPictures = new List(); - var category = _categoryRepository.GetById(srcToDestId[srcId].DestinationId); - var seoName = _pictureService.GetPictureSeName(row.EntityDisplayName); - var image = CreateDownloadImage(urlOrPath, seoName, 1); - - if (category != null && image != null) - { - if (image.Url.HasValue() && !image.Success.HasValue) - { - AsyncRunner.RunSync(() => _fileDownloadManager.DownloadAsync(DownloaderContext, new FileDownloadManagerItem[] { image })); - } - - if ((image.Success ?? false) && File.Exists(image.Path)) - { - Succeeded(image); - var pictureBinary = File.ReadAllBytes(image.Path); - - if (pictureBinary != null && pictureBinary.Length > 0) - { - var pictureId = category.PictureId ?? 0; - if (pictureId != 0) - { - var picture = _pictureRepository.TableUntracked.Expand(x => x.MediaStorage).FirstOrDefault(x => x.Id == pictureId); - if (picture != null) - { - currentPictures.Add(picture); - } - } - - pictureBinary = _pictureService.FindEqualPicture(pictureBinary, currentPictures, out equalPictureId); - - if (pictureBinary != null && pictureBinary.Length > 0) - { - //var picture = _pictureService.InsertPicture(pictureBinary, image.MimeType, seoName, true, size.Width, size.Height, false); - var picture = _pictureService.InsertPicture(pictureBinary, image.MimeType, seoName, true, false, false); ; - if (picture != null) - { - category.PictureId = picture.Id; - _categoryRepository.Update(category); - } - } - else - { - context.Result.AddInfo("Found equal picture in data store. Skipping field.", row.GetRowInfo(), "ImageUrls"); - } - } - } - else if (image.Url.HasValue()) - { - context.Result.AddInfo("Download of an image failed.", row.GetRowInfo(), "ImageUrls"); - } - } - } + var imageUrl = row.GetDataValue("ImageUrl"); + if (imageUrl.IsEmpty()) + { + continue; + } + + var seoName = _pictureService.GetPictureSeName(row.EntityDisplayName); + var image = CreateDownloadImage(context, imageUrl, seoName, 1); + + if (image.Url.HasValue() && !image.Success.HasValue) + { + AsyncRunner.RunSync(() => _fileDownloadManager.DownloadAsync(DownloaderContext, new FileDownloadManagerItem[] { image })); + } + + if ((image.Success ?? false) && File.Exists(image.Path)) + { + Succeeded(image); + var pictureBinary = File.ReadAllBytes(image.Path); + + if (pictureBinary != null && pictureBinary.Length > 0) + { + var currentPictures = new List(); + var pictureId = row.Entity.PictureId ?? 0; + if (pictureId != 0) + { + var picture = _pictureRepository.TableUntracked.Expand(x => x.MediaStorage).FirstOrDefault(x => x.Id == pictureId); + if (picture != null) + { + currentPictures.Add(picture); + } + } + + pictureBinary = _pictureService.FindEqualPicture(pictureBinary, currentPictures, out var equalPictureId); + + if (pictureBinary != null && pictureBinary.Length > 0) + { + var picture = _pictureService.InsertPicture(pictureBinary, image.MimeType, seoName, true, false, false); + if (picture != null) + { + row.Entity.PictureId = picture.Id; + _categoryRepository.Update(row.Entity); + } + } + else + { + context.Result.AddInfo("Found equal picture in data store. Skipping field.", row.GetRowInfo(), "ImageUrls"); + } + } + } + else if (image.Url.HasValue()) + { + context.Result.AddInfo("Download of an image failed.", row.GetRowInfo(), "ImageUrls"); + } } - catch (Exception exception) + catch (Exception ex) { - context.Result.AddWarning(exception.ToAllMessages(), row.GetRowInfo(), "ImageUrls"); + context.Result.AddWarning(ex.ToAllMessages(), row.GetRowInfo(), "ImageUrls"); } } var num = _categoryRepository.Context.SaveChanges(); - return num; } @@ -342,8 +329,9 @@ protected virtual int ProcessCategories( _categoryRepository.AutoCommitEnabled = true; var defaultTemplateId = templateViewPaths["CategoryTemplate.ProductsInGridOrLines"]; + var hasNameColumn = context.DataSegmenter.HasColumn("Name"); - foreach (var row in batch) + foreach (var row in batch) { Category category = null; var id = row.GetDataValue("Id"); @@ -375,9 +363,9 @@ protected virtual int ProcessCategories( continue; } - // a Name is required with new categories - if (!row.Segmenter.HasColumn("Name")) - { + // A name is required for new categories. + if (!row.HasDataValue("Name")) + { ++context.Result.SkippedRecords; context.Result.AddError("The 'Name' field is required for new categories. Skipping row.", row.GetRowInfo(), "Name"); continue; @@ -388,7 +376,7 @@ protected virtual int ProcessCategories( row.Initialize(category, name ?? category.Name); - if (!row.IsNew && !category.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) + if (!row.IsNew && hasNameColumn && !category.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) { // Perf: use this later for SeName updates. row.NameChanged = true; @@ -435,16 +423,17 @@ protected virtual int ProcessCategories( } } - // commit whole batch at once + // Commit whole batch at once. var num = _categoryRepository.Context.SaveChanges(); - // get new category ids + // Get new category ids. foreach (var row in batch) { var id = row.GetDataValue("Id"); - - if (id != 0 && srcToDestId.ContainsKey(id)) - srcToDestId[id].DestinationId = row.Entity.Id; + if (id != 0 && srcToDestId.ContainsKey(id)) + { + srcToDestId[id].DestinationId = row.Entity.Id; + } } return num; diff --git a/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs b/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs index 139e0c836a..ed77e3abc3 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs @@ -613,6 +613,7 @@ protected virtual int ProcessProducts( _productRepository.AutoCommitEnabled = false; var defaultTemplateId = templateViewPaths["Product"]; + var hasNameColumn = context.DataSegmenter.HasColumn("Name"); foreach (var row in batch) { @@ -672,7 +673,7 @@ protected virtual int ProcessProducts( row.Initialize(product, name ?? product.Name); - if (!row.IsNew) + if (!row.IsNew && hasNameColumn) { if (!product.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) { @@ -872,7 +873,7 @@ protected virtual void ProcessProductPictures(ImportExecuteContext context, IEnu // Collect required image file infos. foreach (var urlOrPath in imageUrls) { - var image = CreateDownloadImage(urlOrPath, seoName, ++imageNumber); + var image = CreateDownloadImage(context, urlOrPath, seoName, ++imageNumber); if (image != null) imageFiles.Add(image); diff --git a/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryFactory.cs b/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryFactory.cs index f115f77341..954eebf7d5 100644 --- a/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryFactory.cs +++ b/src/Libraries/SmartStore.Services/Catalog/Modelling/ProductVariantQueryFactory.cs @@ -4,6 +4,7 @@ using System.Text.RegularExpressions; using System.Web; using SmartStore.Collections; +using SmartStore.Core.Html; using SmartStore.Services.Search.Modelling; namespace SmartStore.Services.Catalog.Modelling @@ -38,11 +39,10 @@ protected Multimap QueryItems { _queryItems = new Multimap(); - if (_httpContext.Request != null) + if (_httpContext.Request?.Unvalidated != null) { - var form = _httpContext.Request.Form; - var query = _httpContext.Request.QueryString; - + var form = _httpContext.Request.Unvalidated.Form; + if (form != null) { foreach (var key in form.AllKeys) @@ -54,7 +54,9 @@ protected Multimap QueryItems } } - if (query != null) + var query = _httpContext.Request.Unvalidated.QueryString; + + if (query != null) { foreach (var key in query.AllKeys) { @@ -71,7 +73,51 @@ protected Multimap QueryItems } } - private DateTime? ConvertToDate(string key, string value) + public ProductVariantQuery Current { get; private set; } + + public ProductVariantQuery CreateFromQuery() + { + var query = new ProductVariantQuery(); + Current = query; + + if (_httpContext.Request == null) + return query; + + var languageId = _services.WorkContext.WorkingLanguage.Id; + + foreach (var item in QueryItems) + { + if (!item.Value.Any() || item.Key.EndsWith("-day") || item.Key.EndsWith("-month")) + { + continue; + } + + if (IsVariantKey.IsMatch(item.Key)) + { + ConvertVariant(query, item.Key, item.Value); + } + else if (IsGiftCardKey.IsMatch(item.Key)) + { + item.Value.Each(value => ConvertGiftCard(query, item.Key, value)); + } + else if (IsCheckoutAttributeKey.IsMatch(item.Key)) + { + ConvertCheckoutAttribute(query, item.Key, item.Value); + } + else if (IsVariantAliasKey.IsMatch(item.Key)) + { + ConvertVariantAlias(query, item.Key, item.Value, languageId); + } + else + { + ConvertItems(_httpContext.Request, query, item.Key, item.Value); + } + } + + return query; + } + + private DateTime? ConvertToDate(string key, string value) { var year = 0; var month = 0; @@ -119,15 +165,17 @@ protected virtual void ConvertVariant(ProductVariantQuery query, string key, ICo if (isDate || isFile || isText) { var value = isText ? string.Join(",", values) : values.First(); - var variant = new ProductVariantQueryItem(value); - variant.ProductId = ids[0].ToInt(); - variant.BundleItemId = ids[1].ToInt(); - variant.AttributeId = ids[2].ToInt(); - variant.VariantAttributeId = ids[3].ToInt(); - variant.IsFile = isFile; - variant.IsText = isText; - - if (isDate) + var variant = new ProductVariantQueryItem(value) + { + ProductId = ids[0].ToInt(), + BundleItemId = ids[1].ToInt(), + AttributeId = ids[2].ToInt(), + VariantAttributeId = ids[3].ToInt(), + IsFile = isFile, + IsText = isText + }; + + if (isDate) { variant.Date = ConvertToDate(key, value); } @@ -138,13 +186,15 @@ protected virtual void ConvertVariant(ProductVariantQuery query, string key, ICo { foreach (var value in values) { - var variant = new ProductVariantQueryItem(value); - variant.ProductId = ids[0].ToInt(); - variant.BundleItemId = ids[1].ToInt(); - variant.AttributeId = ids[2].ToInt(); - variant.VariantAttributeId = ids[3].ToInt(); - - query.AddVariant(variant); + var variant = new ProductVariantQueryItem(value) + { + ProductId = ids[0].ToInt(), + BundleItemId = ids[1].ToInt(), + AttributeId = ids[2].ToInt(), + VariantAttributeId = ids[3].ToInt() + }; + + query.AddVariant(variant); } } } @@ -181,16 +231,18 @@ protected virtual void ConvertVariantAlias(ProductVariantQuery query, string key if (isDate || isFile || isText) { var value = isText ? string.Join(",", values) : values.First(); - var variant = new ProductVariantQueryItem(value); - variant.ProductId = productId; - variant.BundleItemId = bundleItemId; - variant.AttributeId = attributeId; - variant.VariantAttributeId = variantAttributeId; - variant.Alias = alias; - variant.IsFile = isFile; - variant.IsText = isText; - - if (isDate) + var variant = new ProductVariantQueryItem(value) + { + ProductId = productId, + BundleItemId = bundleItemId, + AttributeId = attributeId, + VariantAttributeId = variantAttributeId, + Alias = alias, + IsFile = isFile, + IsText = isText + }; + + if (isDate) { variant.Date = ConvertToDate(key, value); } @@ -213,14 +265,16 @@ protected virtual void ConvertVariantAlias(ProductVariantQuery query, string key valueAlias = string.Join("-", valueIds.Take(valueIds.Length - 1)); } - var variant = new ProductVariantQueryItem(optionId == 0 ? value : optionId.ToString()); - variant.ProductId = productId; - variant.BundleItemId = bundleItemId; - variant.AttributeId = attributeId; - variant.VariantAttributeId = variantAttributeId; - variant.Alias = alias; + var variant = new ProductVariantQueryItem(optionId == 0 ? value : optionId.ToString()) + { + ProductId = productId, + BundleItemId = bundleItemId, + AttributeId = attributeId, + VariantAttributeId = variantAttributeId, + Alias = alias + }; - if (optionId != 0) + if (optionId != 0) { variant.ValueAlias = valueAlias; } @@ -257,11 +311,13 @@ protected virtual void ConvertCheckoutAttribute(ProductVariantQuery query, strin if (isDate || isFile || isText) { var value = isText ? string.Join(",", values) : values.First(); - var attribute = new CheckoutAttributeQueryItem(attributeId, value); - attribute.IsFile = isFile; - attribute.IsText = isText; + var attribute = new CheckoutAttributeQueryItem(attributeId, value) + { + IsFile = isFile, + IsText = isText + }; - if (isDate) + if (isDate) { attribute.Date = ConvertToDate(key, value); } @@ -280,49 +336,5 @@ protected virtual void ConvertCheckoutAttribute(ProductVariantQuery query, strin protected virtual void ConvertItems(HttpRequestBase request, ProductVariantQuery query, string key, ICollection values) { } - - public ProductVariantQuery Current { get; private set; } - - public ProductVariantQuery CreateFromQuery() - { - var query = new ProductVariantQuery(); - Current = query; - - if (_httpContext.Request == null) - return query; - - var languageId = _services.WorkContext.WorkingLanguage.Id; - - foreach (var item in QueryItems) - { - if (!item.Value.Any() || item.Key.EndsWith("-day") || item.Key.EndsWith("-month")) - { - continue; - } - - if (IsVariantKey.IsMatch(item.Key)) - { - ConvertVariant(query, item.Key, item.Value); - } - else if (IsGiftCardKey.IsMatch(item.Key)) - { - item.Value.Each(value => ConvertGiftCard(query, item.Key, value)); - } - else if (IsCheckoutAttributeKey.IsMatch(item.Key)) - { - ConvertCheckoutAttribute(query, item.Key, item.Value); - } - else if (IsVariantAliasKey.IsMatch(item.Key)) - { - ConvertVariantAlias(query, item.Key, item.Value, languageId); - } - else - { - ConvertItems(_httpContext.Request, query, item.Key, item.Value); - } - } - - return query; - } } } diff --git a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs index 0ff2e926fd..5fc429449c 100644 --- a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs @@ -598,7 +598,7 @@ public virtual decimal GetFinalPrice( { var itemPrice = GetFinalPrice(itemData.Item.Product, customer, itemData.AdditionalCharge, includeDiscounts, 1, itemData, context); - result = result + decimal.Multiply(itemPrice, itemData.Item.Quantity); + result += decimal.Multiply(itemPrice, itemData.Item.Quantity); } return result < decimal.Zero ? decimal.Zero : result; diff --git a/src/Libraries/SmartStore.Services/Catalog/SpecificationAttributeService.cs b/src/Libraries/SmartStore.Services/Catalog/SpecificationAttributeService.cs index 1602eeb0b1..ac5bda82b6 100644 --- a/src/Libraries/SmartStore.Services/Catalog/SpecificationAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/SpecificationAttributeService.cs @@ -105,10 +105,16 @@ public virtual SpecificationAttributeOption GetSpecificationAttributeOptionById( public virtual IList GetSpecificationAttributeOptionsBySpecificationAttribute(int specificationAttributeId) { + if (specificationAttributeId == 0) + { + return new List(); + } + var query = from sao in _specificationAttributeOptionRepository.Table orderby sao.DisplayOrder where sao.SpecificationAttributeId == specificationAttributeId select sao; + var specificationAttributeOptions = query.ToList(); return specificationAttributeOptions; } diff --git a/src/Libraries/SmartStore.Services/Cms/Blocks/BlockHandlerBase.cs b/src/Libraries/SmartStore.Services/Cms/Blocks/BlockHandlerBase.cs index 5296dc15ac..03d182c4a3 100644 --- a/src/Libraries/SmartStore.Services/Cms/Blocks/BlockHandlerBase.cs +++ b/src/Libraries/SmartStore.Services/Cms/Blocks/BlockHandlerBase.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Web; using System.Web.Mvc; +using System.Web.Mvc.Async; using System.Web.Mvc.Html; using System.Web.Routing; using Newtonsoft.Json; @@ -170,7 +171,7 @@ private ViewEngineResult FindFirstView(IBlockMetadata blockMetadata, IEnumerable return null; } - protected void RenderByChildAction(IBlockContainer element, IEnumerable templates, HtmlHelper htmlHelper, TextWriter textWriter) + protected void RenderByChildAction(IBlockContainer element, IEnumerable templates, HtmlHelper htmlHelper, TextWriter textWriter) { Guard.NotNull(element, nameof(element)); Guard.NotNull(templates, nameof(templates)); @@ -183,18 +184,220 @@ protected void RenderByChildAction(IBlockContainer element, IEnumerable throw new InvalidOperationException("The return value of the 'GetRoute()' method cannot be NULL."); } - //routeInfo.RouteValues["model"] = element.Block; + var routeValues = routeInfo.RouteValues; + + routeValues["action"] = routeInfo.Action; + routeValues["controller"] = routeInfo.Controller; + + VirtualPathData vpd = GetVirtualPathForArea(htmlHelper.RouteCollection, htmlHelper.ViewContext.RequestContext, null /* name */, routeValues, out var usingAreas); + if (vpd == null) + { + throw new InvalidOperationException("Could not find any matching route."); + } + + if (usingAreas) + { + routeValues.Remove("area"); + } + + var routeData = CreateRouteData(vpd.Route, routeValues, vpd.DataTokens, htmlHelper.ViewContext); + var httpContext = htmlHelper.ViewContext.HttpContext; + var requestContext = new RequestContext(httpContext, routeData); + + // Create the controller instance + var controller = ControllerBuilder.Current.GetControllerFactory().CreateController(requestContext, routeInfo.Controller) as Controller; + if (controller == null) + { + throw new InvalidOperationException($"Could not activate controller '{routeInfo.Controller}'. Please ensure that the controller class exists and inherits from '{typeof(Controller).FullName}'."); + } var originalWriter = htmlHelper.ViewContext.Writer; htmlHelper.ViewContext.Writer = textWriter; - using (new ActionDisposable(() => htmlHelper.ViewContext.Writer = originalWriter)) + var originalOutput = httpContext.Response.Output; + httpContext.Response.Output = textWriter; + + var originalActionInvoker = controller.ActionInvoker; + controller.ActionInvoker = new ActionInvokerWithResultValidator(); + + void endRender() + { + htmlHelper.ViewContext.Writer = originalWriter; + httpContext.Response.Output = originalOutput; + controller.ActionInvoker = originalActionInvoker; + } + + using (new ActionDisposable((Action)endRender)) { - htmlHelper.RenderAction(routeInfo.Action, routeInfo.Controller, routeInfo.RouteValues); + ((IController)controller).Execute(requestContext); } } + #region Legacy 'RenderByChildAction' + + //protected void RenderByChildAction(IBlockContainer element, IEnumerable templates, HtmlHelper htmlHelper, TextWriter textWriter) + // { + // Guard.NotNull(element, nameof(element)); + // Guard.NotNull(templates, nameof(templates)); + // Guard.NotNull(htmlHelper, nameof(htmlHelper)); + // Guard.NotNull(textWriter, nameof(textWriter)); + + // var routeInfo = templates.Select(x => GetRoute(element, x)).FirstOrDefault(); + // if (routeInfo == null) + // { + // throw new InvalidOperationException("The return value of the 'GetRoute()' method cannot be NULL."); + // } + + // //routeInfo.RouteValues["model"] = element.Block; + + // var originalWriter = htmlHelper.ViewContext.Writer; + // htmlHelper.ViewContext.Writer = textWriter; + + // using (new ActionDisposable(() => htmlHelper.ViewContext.Writer = originalWriter)) + // { + // htmlHelper.RenderAction(routeInfo.Action, routeInfo.Controller, routeInfo.RouteValues); + // } + // } + + #endregion + protected virtual RouteInfo GetRoute(IBlockContainer element, string template) => throw new NotImplementedException(); - } + + + class ActionInvokerWithResultValidator : AsyncControllerActionInvoker + { + protected override void InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult) + { + switch (actionResult) + { + case PartialViewResult _: + case ContentResult _: + base.InvokeActionResult(controllerContext, actionResult); + break; + default: + throw new InvalidOperationException($"The action result type of an MVC route block must either be '{nameof(PartialViewResult)}' or '{nameof(ContentResult)}'"); + } + } + } + + #region Copied from ASP.NET MVC + + private VirtualPathData GetVirtualPathForArea(RouteCollection routes, RequestContext requestContext, string name, RouteValueDictionary values, out bool usingAreas) + { + // Copied over from https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Web.Mvc/RouteCollectionExtensions.cs#L53 + + Guard.NotNull(routes, nameof(routes)); + + if (!String.IsNullOrEmpty(name)) + { + // the route name is a stronger qualifier than the area name, so just pipe it through + usingAreas = false; + return routes.GetVirtualPath(requestContext, name, values); + } + + string targetArea = null; + if (values != null) + { + if (values.TryGetValue("area", out var targetAreaRawValue)) + { + targetArea = targetAreaRawValue as string; + } + else + { + // set target area to current area + if (requestContext != null) + { + targetArea = requestContext.RouteData.GetAreaName(); + } + } + } + + // need to apply a correction to the RVD if areas are in use + RouteValueDictionary correctedValues = values; + RouteCollection filteredRoutes = FilterRouteCollectionByArea(routes, targetArea, out usingAreas); + if (usingAreas) + { + correctedValues = new RouteValueDictionary(values); + correctedValues.Remove("area"); + } + + VirtualPathData vpd = filteredRoutes.GetVirtualPath(requestContext, correctedValues); + return vpd; + } + + // This method returns a new RouteCollection containing only routes that matched a particular area. + // The Boolean out parameter is just a flag specifying whether any registered routes were area-aware. + private RouteCollection FilterRouteCollectionByArea(RouteCollection routes, string areaName, out bool usingAreas) + { + // Copied over from https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Web.Mvc/RouteCollectionExtensions.cs#L18 + + if (areaName == null) + { + areaName = String.Empty; + } + + usingAreas = false; + + // Ensure that we continue using the same settings as the previous route collection + // if we are using areas and the route collection is exchanged + RouteCollection filteredRoutes = new RouteCollection + { + AppendTrailingSlash = routes.AppendTrailingSlash, + LowercaseUrls = routes.LowercaseUrls, + RouteExistingFiles = routes.RouteExistingFiles + }; + + using (routes.GetReadLock()) + { + foreach (RouteBase route in routes) + { + string thisAreaName = route.GetAreaName() ?? String.Empty; + usingAreas |= (thisAreaName.Length > 0); + if (String.Equals(thisAreaName, areaName, StringComparison.OrdinalIgnoreCase)) + { + filteredRoutes.Add(route); + } + } + } + + // if areas are not in use, the filtered route collection might be incorrect + return (usingAreas) ? filteredRoutes : routes; + } + + private RouteData CreateRouteData(RouteBase route, RouteValueDictionary routeValues, RouteValueDictionary dataTokens, ViewContext parentViewContext) + { + var routeData = new RouteData(); + + foreach (var kvp in routeValues) + { + routeData.Values.Add(kvp.Key, kvp.Value); + } + + foreach (var kvp in dataTokens) + { + routeData.DataTokens.Add(kvp.Key, kvp.Value); + } + + routeData.Route = route; + routeData.DataTokens["ParentActionViewContext"] = parentViewContext; + + return routeData; + } + + class ChildActionMvcHandler : MvcHandler + { + public ChildActionMvcHandler(RequestContext context) + : base(context) + { + } + + protected override void AddVersionHeader(HttpContextBase httpContext) + { + // No version header for child actions + } + } + + #endregion + } } diff --git a/src/Libraries/SmartStore.Services/Cms/LinkResolver.cs b/src/Libraries/SmartStore.Services/Cms/LinkResolver.cs index b541e73ed4..de254b8fd4 100644 --- a/src/Libraries/SmartStore.Services/Cms/LinkResolver.cs +++ b/src/Libraries/SmartStore.Services/Cms/LinkResolver.cs @@ -13,6 +13,7 @@ using SmartStore.Services.Security; using SmartStore.Services.Seo; using SmartStore.Services.Stores; +using SmartStore.Services.Topics; namespace SmartStore.Services.Cms { @@ -34,6 +35,7 @@ public partial class LinkResolver : ILinkResolver protected readonly ILocalizedEntityService _localizedEntityService; protected readonly IAclService _aclService; protected readonly IStoreMappingService _storeMappingService; + protected readonly ITopicService _topicService; protected readonly UrlHelper _urlHelper; public LinkResolver( @@ -42,6 +44,7 @@ public LinkResolver( ILocalizedEntityService localizedEntityService, IAclService aclService, IStoreMappingService storeMappingService, + ITopicService topicService, UrlHelper urlHelper) { _services = services; @@ -49,6 +52,7 @@ public LinkResolver( _localizedEntityService = localizedEntityService; _aclService = aclService; _storeMappingService = storeMappingService; + _topicService = topicService; _urlHelper = urlHelper; QuerySettings = DbQuerySettings.Default; @@ -109,7 +113,7 @@ public virtual LinkResolverResult Resolve(string linkExpression, IEnumerable(d2, languageId, x => new ResolverEntitySummary + GetEntityData(d2, storeId, languageId, x => new ResolverEntitySummary { Name = x.Name, Published = x.Published, @@ -120,7 +124,7 @@ public virtual LinkResolverResult Resolve(string linkExpression, IEnumerable(d2, languageId, x => new ResolverEntitySummary + GetEntityData(d2, storeId, languageId, x => new ResolverEntitySummary { Name = x.Name, Published = x.Published, @@ -131,7 +135,7 @@ public virtual LinkResolverResult Resolve(string linkExpression, IEnumerable(d2, languageId, x => new ResolverEntitySummary + GetEntityData(d2, storeId, languageId, x => new ResolverEntitySummary { Name = x.Name, Published = x.Published, @@ -141,7 +145,7 @@ public virtual LinkResolverResult Resolve(string linkExpression, IEnumerable(d2, languageId, x => null); + GetEntityData(d2, storeId, languageId, x => null); break; default: throw new SmartException("Unknown link builder type."); @@ -171,11 +175,19 @@ public virtual LinkResolverResult Resolve(string linkExpression, IEnumerable(LinkResolverData data, int languageId, Expression> selector) where T : BaseEntity + internal void GetEntityData( + LinkResolverData data, + int storeId, + int languageId, + Expression> selector) where T : BaseEntity { ResolverEntitySummary summary = null; string systemName = null; @@ -285,25 +301,33 @@ internal void GetEntityData(LinkResolverData data, int languageId, Expression if (data.Type == LinkType.Topic) { - var query = _services.DbContext.Set() - .AsNoTracking() - .AsQueryable(); + Topic topic = null; - query = string.IsNullOrEmpty(systemName) - ? query.Where(x => x.Id == data.Id) - : query.Where(x => x.SystemName == systemName); + if (string.IsNullOrEmpty(systemName)) + { + topic = _services.DbContext.Set() + .AsNoTracking() + .FirstOrDefault(x => x.Id == data.Id); + } + else + { + topic = _topicService.GetTopicBySystemName(systemName, storeId, false); + data.CheckLimitedToStores = false; + } - summary = query.Select(x => new ResolverEntitySummary + if (topic != null) { - Id = x.Id, - Name = x.SystemName, - Title = x.Title, - ShortTitle = x.ShortTitle, - Published = x.IsPublished, - SubjectToAcl = x.SubjectToAcl, - LimitedToStores = x.LimitedToStores - }) - .FirstOrDefault(); + summary = new ResolverEntitySummary + { + Id = topic.Id, + Name = topic.SystemName, + Title = topic.Title, + ShortTitle = topic.ShortTitle, + Published = topic.IsPublished, + SubjectToAcl = topic.SubjectToAcl, + LimitedToStores = topic.LimitedToStores + }; + } } else { diff --git a/src/Libraries/SmartStore.Services/Cms/LinkResolverResult.cs b/src/Libraries/SmartStore.Services/Cms/LinkResolverResult.cs index 630050273c..71002103a9 100644 --- a/src/Libraries/SmartStore.Services/Cms/LinkResolverResult.cs +++ b/src/Libraries/SmartStore.Services/Cms/LinkResolverResult.cs @@ -81,8 +81,9 @@ public partial class LinkResolverData : LinkResolverResult, ICloneable(); var seoName = _pictureService.GetPictureSeName(row.EntityDisplayName); - var image = CreateDownloadImage(urlOrPath, seoName, 1); + var image = CreateDownloadImage(context, urlOrPath, seoName, 1); if (image == null) continue; diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs index ac18b7edac..8c89087b72 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs @@ -461,10 +461,12 @@ private dynamic ToDynamic(DataExporterContext ctx, QuantityUnit quantityUnit) var translations = ctx.Translations[nameof(QuantityUnit)]; result.Name = translations.GetValue(ctx.LanguageId, quantityUnit.Id, nameof(quantityUnit.Name)) ?? quantityUnit.Name; - result.Description = translations.GetValue(ctx.LanguageId, quantityUnit.Id, nameof(quantityUnit.Description)) ?? quantityUnit.Description; + result.NamePlural = translations.GetValue(ctx.LanguageId, quantityUnit.Id, nameof(quantityUnit.NamePlural)) ?? quantityUnit.NamePlural; + result.Description = translations.GetValue(ctx.LanguageId, quantityUnit.Id, nameof(quantityUnit.Description)) ?? quantityUnit.Description; result._Localized = GetLocalized(ctx, translations, null, quantityUnit, x => x.Name, + x => x.NamePlural, x => x.Description); return result; diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs index 8c5f870ab2..4c9fd6fdb1 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs @@ -140,11 +140,11 @@ public static string ResolveFileNamePattern(this ExportProfile profile, Store st if (profile.FileNamePattern.Contains("%Profile.SeoName%")) { - sb.Replace("%Profile.SeoName%", SeoHelper.GetSeName(profile.Name, true, false).Replace("/", "").Replace("-", "")); + sb.Replace("%Profile.SeoName%", SeoHelper.GetSeName(profile.Name, true, false, false).Replace("-", "")); } if (profile.FileNamePattern.Contains("%Store.SeoName%")) { - sb.Replace("%Store.SeoName%", profile.PerStore ? SeoHelper.GetSeName(store.Name, true, false) : "allstores"); + sb.Replace("%Store.SeoName%", profile.PerStore ? SeoHelper.GetSeName(store.Name, true, false, true) : "allstores"); } if (profile.FileNamePattern.Contains("%Random.Number%")) { diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs index 0d003e9497..e3a4f541b6 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs @@ -148,7 +148,7 @@ public virtual ExportProfile InsertExportProfile( .Replace("/", "") .Replace("-", ""); - var folderName = SeoHelper.GetSeName(cleanedSystemName, true, false) + var folderName = SeoHelper.GetSeName(cleanedSystemName, true, false, false) .ToValidPath() .Truncate(_dataExchangeSettings.MaxFileNameLength); diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs index df53b4cb3f..4af3ac1836 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs @@ -318,7 +318,8 @@ public void WriteQuantityUnit(dynamic quantityUnit, string node) _writer.Write("Id", entity.Id.ToString()); _writer.Write("Name", (string)quantityUnit.Name); - _writer.Write("Description", (string)quantityUnit.Description); + _writer.Write("NamePlural", (string)quantityUnit.NamePlural); + _writer.Write("Description", (string)quantityUnit.Description); _writer.Write("DisplayLocale", entity.DisplayLocale); _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); _writer.Write("IsDefault", entity.IsDefault.ToString()); diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs index f4c963a045..4452f00382 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs @@ -86,7 +86,7 @@ protected void Initialize(ImportExecuteContext context) context.Result.TotalRecords = context.DataSegmenter.TotalRows; } - public FileDownloadManagerItem CreateDownloadImage(string urlOrPath, string seoName, int displayOrder) + public FileDownloadManagerItem CreateDownloadImage(ImportExecuteContext context, string urlOrPath, string seoName, int displayOrder) { var item = new FileDownloadManagerItem { @@ -102,10 +102,10 @@ public FileDownloadManagerItem CreateDownloadImage(string urlOrPath, string seoN if (urlOrPath.IsWebUrl()) { - item.Url = urlOrPath; - item.FileName = "{0}-{1}".FormatInvariant(seoName, item.Id).ToValidFileName(); + item.Url = context.Services.WebHelper.ModifyQueryString(urlOrPath, "q=100", null); + item.FileName = "{0}-{1}".FormatInvariant(seoName, item.Id).ToValidFileName(); - if (DownloadedItems.ContainsKey(urlOrPath)) + if (DownloadedItems.ContainsKey(urlOrPath)) { item.Path = Path.Combine(ImageDownloadFolder, DownloadedItems[urlOrPath]); item.Success = true; diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs index fbded791c7..da6ad91076 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs @@ -223,7 +223,7 @@ public virtual ImportProfile InsertImportProfile(string fileName, string name, I profile.KeyFieldNames = string.Join(",", keyFieldNames); - profile.FolderName = SeoHelper.GetSeName(name, true, false) + profile.FolderName = SeoHelper.GetSeName(name, true, false, false) .ToValidPath() .Truncate(_dataExchangeSettings.MaxFileNameLength); diff --git a/src/Libraries/SmartStore.Services/Directory/GeoCountryLookup.cs b/src/Libraries/SmartStore.Services/Directory/GeoCountryLookup.cs index 497cd4a8a7..71ec189d47 100644 --- a/src/Libraries/SmartStore.Services/Directory/GeoCountryLookup.cs +++ b/src/Libraries/SmartStore.Services/Directory/GeoCountryLookup.cs @@ -1,123 +1,58 @@ using System; using System.Net; +using MaxMind.GeoIP2; using SmartStore.Core; using SmartStore.Core.Caching; +using SmartStore.Utilities; using SmDir = SmartStore.Core.Domain.Directory; namespace SmartStore.Services.Directory { - public partial class GeoCountryLookup : IGeoCountryLookup + public partial class GeoCountryLookup : DisposableObject, IGeoCountryLookup { - private readonly IWebHelper _webHelper; - private readonly ICountryService _countryService; - private readonly IRequestCache _requestCache; - private readonly ICacheManager _cache; + private readonly DatabaseReader _reader; + private readonly object _lock = new object(); - public GeoCountryLookup(IWebHelper webHelper, IRequestCache requestCache, ICacheManager cache, ICountryService countryService) + public GeoCountryLookup() { - this._webHelper = webHelper; - this._requestCache = requestCache; - this._cache = cache; - this._countryService = countryService; + _reader = new DatabaseReader(CommonHelper.MapPath("~/App_Data/GeoLite2/GeoLite2-Country.mmdb")); } - private MaxMind.GeoIP.LookupService GetLookupService() - { - return _cache.Get("GeoCountryLookup", () => - { - var lookupService = new MaxMind.GeoIP.LookupService(_webHelper.MapPath("~/App_Data/GeoIP.dat")); - return lookupService; - }); - } - - public virtual string LookupCountryCode(string str) + public LookupCountryResponse LookupCountry(string addr) { - if (String.IsNullOrEmpty(str)) - return string.Empty; - - IPAddress addr; - try + if (addr.HasValue() && IPAddress.TryParse(addr, out var ipAddress)) { - addr = IPAddress.Parse(str); + return LookupCountry(ipAddress); } - catch - { - return string.Empty; - } - return LookupCountryCode(addr); - } - public virtual string LookupCountryCode(IPAddress addr) - { - try - { - var lookupService = GetLookupService(); - var country = lookupService.getCountry(addr); - var code = country.getCode(); - if (code == "--") - return string.Empty; - - return code; - } - catch - { - return string.Empty; - } + return null; } - public virtual string LookupCountryName(string str) + public LookupCountryResponse LookupCountry(IPAddress addr) { - if (String.IsNullOrEmpty(str)) - return string.Empty; + Guard.NotNull(addr, nameof(addr)); - IPAddress addr; - try + if (_reader.TryCountry(addr, out var response) && response.Country != null) { - addr = IPAddress.Parse(str); + var country = response.Country; + return new LookupCountryResponse + { + GeoNameId = country.GeoNameId, + IsoCode = country.IsoCode, + Name = country.Name, + IsInEu = country.IsInEuropeanUnion + }; } - catch - { - return string.Empty; - } - return LookupCountryName(addr); + + return null; } - public virtual string LookupCountryName(IPAddress addr) + protected override void OnDispose(bool disposing) { - try - { - var lookupService = GetLookupService(); - var country = lookupService.getCountry(addr); - return country.getName(); - } - catch - { - return string.Empty; - } + if (disposing && _reader != null) + { + _reader.Dispose(); + } } - - public virtual bool IsEuIpAddress(string ipAddress, out SmDir.Country euCountry) - { - euCountry = null; - - if (ipAddress.IsEmpty()) - return false; - - euCountry = _requestCache.Get("GeoCountryLookup.EuCountry.{0}".FormatInvariant(ipAddress), () => - { - var countryCode = LookupCountryCode(ipAddress); - if (countryCode.IsEmpty()) - return (SmDir.Country)null; - - var country = _countryService.GetCountryByTwoLetterIsoCode(countryCode); - return country; - }); - - if (euCountry == null) - return false; - - return euCountry.SubjectToVat; - } - } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Directory/IGeoCountryLookup.cs b/src/Libraries/SmartStore.Services/Directory/IGeoCountryLookup.cs index 815a030b2c..cfe4cae186 100644 --- a/src/Libraries/SmartStore.Services/Directory/IGeoCountryLookup.cs +++ b/src/Libraries/SmartStore.Services/Directory/IGeoCountryLookup.cs @@ -3,27 +3,36 @@ namespace SmartStore.Services.Directory { - /// - /// Country lookup helper - /// - public partial interface IGeoCountryLookup + public sealed class LookupCountryResponse { - string LookupCountryCode(string str); + /// + /// The GeoName ID for the country. + /// + public int? GeoNameId { get; set; } - string LookupCountryCode(IPAddress addr); + /// + /// The english name of the country. + /// + public string Name { get; set; } - string LookupCountryName(string str); + /// + /// The two-letter ISO 3166-1 alpha code for the country + /// + public string IsoCode { get; set; } + + /// + /// This is true if the country is a member state of the European Union. + /// + public bool IsInEu { get; set; } + } - string LookupCountryName(IPAddress addr); + /// + /// Country lookup helper for IPv4/6 addresses + /// + public partial interface IGeoCountryLookup + { + LookupCountryResponse LookupCountry(string addr); - /// - /// Gets a value indicating whether the given IP address originates from an EU country - /// - /// IP address - /// An instance of if the IP originates from a EU country - /// - /// true if the IP address originates from an EU country, false if not - /// - bool IsEuIpAddress(string ipAddress, out Country euCountry); + LookupCountryResponse LookupCountry(IPAddress addr); } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Events/ConsumerResolver.cs b/src/Libraries/SmartStore.Services/Events/ConsumerResolver.cs index 88742e689c..ebab9eff71 100644 --- a/src/Libraries/SmartStore.Services/Events/ConsumerResolver.cs +++ b/src/Libraries/SmartStore.Services/Events/ConsumerResolver.cs @@ -41,12 +41,10 @@ public virtual object ResolveParameter(ParameterInfo p, IComponentContext c = nu private bool IsActiveForStore(PluginDescriptor plugin) { - var storeContext = EngineContext.Current.Resolve(); - int storeId = 0; if (EngineContext.Current.IsFullyInitialized) { - storeId = storeContext.CurrentStore.Id; + storeId = _container.Value.Resolve().CurrentStore.Id; } if (storeId == 0) diff --git a/src/Libraries/SmartStore.Services/Media/CachedImageResult.cs b/src/Libraries/SmartStore.Services/Media/CachedImageResult.cs index 613192513a..8761739ab8 100644 --- a/src/Libraries/SmartStore.Services/Media/CachedImageResult.cs +++ b/src/Libraries/SmartStore.Services/Media/CachedImageResult.cs @@ -13,6 +13,7 @@ namespace SmartStore.Services.Media public class CachedImageResult { private bool? _exists; + private string _mimeType; public CachedImageResult(IFile file) { @@ -59,11 +60,19 @@ public long FileSize /// The file extension (without 'dot') /// public string Extension { get; set; } - - /// - /// The path relative to the cache root folder - /// - public string Path { get; set; } + + /// + /// The filemime type + /// + public string MimeType + { + get => _mimeType ?? (_mimeType = MimeTypes.MapNameToMimeType(FileName)); + } + + /// + /// The path relative to the cache root folder + /// + public string Path { get; set; } /// /// The last modified date or null if the file does not exist diff --git a/src/Libraries/SmartStore.Services/Media/DefaultImageProcessor.cs b/src/Libraries/SmartStore.Services/Media/DefaultImageProcessor.cs index e912b6324a..1c5e8dc893 100644 --- a/src/Libraries/SmartStore.Services/Media/DefaultImageProcessor.cs +++ b/src/Libraries/SmartStore.Services/Media/DefaultImageProcessor.cs @@ -9,6 +9,7 @@ using SmartStore.Core.Logging; using ImageProcessor.Configuration; using SmartStore.Core.Events; +using SmartStore.Utilities; namespace SmartStore.Services.Media { @@ -48,6 +49,7 @@ public ProcessImageResult ProcessImage(ProcessImageQuery query) ValidateQuery(query); var watch = new Stopwatch(); + byte[] inBuffer = null; try { @@ -58,28 +60,36 @@ public ProcessImageResult ProcessImage(ProcessImageQuery query) var source = query.Source; // Load source - if (source is byte[]) + if (source is byte[] b) { - processor.Load((byte[])source); + inBuffer = b; } - else if (source is Stream) + else if (source is Stream s) { - processor.Load((Stream)source); + inBuffer = s.ToByteArray(); } - else if (source is Image) + else if (source is Image img) { - processor.Load((Image)source); + processor.Load(img); } - else if (source is string) + else if (source is string str) { - // TODO: (mc) map virtual pathes - processor.Load((string)source); + var path = NormalizePath(str); + using (var fs = File.OpenRead(path)) + { + inBuffer = fs.ToByteArray(); + } } else { throw new ProcessImageException("Invalid source type '{0}' in query.".FormatInvariant(query.Source.GetType().FullName), query); } + if (inBuffer != null) + { + processor.Load(inBuffer); + } + // Pre-process event _eventPublisher.Publish(new ImageProcessingEvent(query, processor)); @@ -87,25 +97,60 @@ public ProcessImageResult ProcessImage(ProcessImageQuery query) { Query = query, SourceWidth = processor.Image.Width, - SourceHeight = processor.Image.Height + SourceHeight = processor.Image.Height, + SourceMimeType = processor.CurrentImageFormat.MimeType }; // Core processing - ProcessImageCore(query, processor); + ProcessImageCore(query, processor, out var fxApplied); // Create & prepare result var outStream = new MemoryStream(); processor.Save(outStream); + var fmt = processor.CurrentImageFormat; + result.FileExtension = fmt.DefaultExtension == "jpeg" ? "jpg" : fmt.DefaultExtension; + result.MimeType = fmt.MimeType; + + result.HasAppliedVisualEffects = fxApplied; result.Width = processor.Image.Width; result.Height = processor.Image.Height; - result.FileExtension = processor.CurrentImageFormat.DefaultExtension; - result.MimeType = processor.CurrentImageFormat.MimeType; - result.OutputStream = outStream; + + if (inBuffer != null) + { + // Check whether it is more beneficial to return the source instead of the result. + // Prefer result only if its size is smaller than the source size. + // Result size may be larger if a high-compressed image has been uploaded. + // During image processing the source compression algorithm gets lost and the image may be larger in size + // after encoding with default encoders. + var compare = + // only when image was not altered visually... + !fxApplied + // ...size has not changed + && result.Width == result.SourceWidth + && result.Height == result.SourceHeight + // ...and format has not changed + && result.MimeType == result.SourceMimeType; + + if (compare && inBuffer.LongLength <= outStream.GetBuffer().LongLength) + { + // Source is smaller. Throw away result and get back to source. + outStream.Dispose(); + result.OutputStream = new MemoryStream(inBuffer, 0, inBuffer.Length, true, true); + } + } + + // Set output stream + if (result.OutputStream == null) + { + result.OutputStream = outStream; + } // Post-process event _eventPublisher.Publish(new ImageProcessedEvent(query, processor, result)); + result.OutputStream.Position = 0; + result.ProcessTimeMs = watch.ElapsedMilliseconds; return result; @@ -119,9 +164,9 @@ public ProcessImageResult ProcessImage(ProcessImageQuery query) } finally { - if (query.DisposeSource && query.Source is IDisposable) + if (query.DisposeSource && query.Source is IDisposable source) { - ((IDisposable)query.Source).Dispose(); + source.Dispose(); } watch.Stop(); @@ -134,8 +179,14 @@ public ProcessImageResult ProcessImage(ProcessImageQuery query) /// /// Query /// Processor instance - protected virtual void ProcessImageCore(ProcessImageQuery query, ImageFactory processor) + /// + /// Should be true if any effect has been applied that potentially changes the image visually (like background color, contrast, sharpness etc.). + /// Resize and compression quality does NOT count as FX. + /// + protected virtual void ProcessImageCore(ProcessImageQuery query, ImageFactory processor, out bool fxApplied) { + fxApplied = false; + // Resize var size = query.MaxWidth != null || query.MaxHeight != null ? new Size(query.MaxWidth ?? 0, query.MaxHeight ?? 0) @@ -153,6 +204,7 @@ protected virtual void ProcessImageCore(ProcessImageQuery query, ImageFactory pr if (query.BackgroundColor.HasValue()) { processor.BackgroundColor(ColorTranslator.FromHtml(query.BackgroundColor)); + fxApplied = true; } // Format @@ -191,6 +243,21 @@ protected virtual void ProcessImageCore(ProcessImageQuery query, ImageFactory pr } } + private string NormalizePath(string path) + { + if (path.IsWebUrl()) + { + throw new NotSupportedException($"Remote images cannot be processed: Path: {path}"); + } + + if (!PathHelper.IsAbsolutePhysicalPath(path)) + { + path = CommonHelper.MapPath(path); + } + + return path; + } + private void ValidateQuery(ProcessImageQuery query) { if (query.Source == null) diff --git a/src/Libraries/SmartStore.Services/Media/PictureService.cs b/src/Libraries/SmartStore.Services/Media/PictureService.cs index adb0bacf0e..9e421212eb 100644 --- a/src/Libraries/SmartStore.Services/Media/PictureService.cs +++ b/src/Libraries/SmartStore.Services/Media/PictureService.cs @@ -154,8 +154,7 @@ protected virtual string GetFallbackImageFileName(FallbackPictureType defaultPic public virtual byte[] ValidatePicture(byte[] pictureBinary, string mimeType) { - var size = Size.Empty; - return ValidatePicture(pictureBinary, mimeType, out size); + return ValidatePicture(pictureBinary, mimeType, out _); } public virtual byte[] ValidatePicture(byte[] pictureBinary, string mimeType, out Size size) @@ -513,7 +512,7 @@ public int ClearUrlCache() public virtual string GetPictureSeName(string name) { - return SeoHelper.GetSeName(name, true, false); + return SeoHelper.GetSeName(name, true, false, false); } public virtual Picture SetSeoFilename(int pictureId, string seoFilename) diff --git a/src/Libraries/SmartStore.Services/Media/ProcessImageException.cs b/src/Libraries/SmartStore.Services/Media/ProcessImageException.cs index 1cb2038497..d16095ff8c 100644 --- a/src/Libraries/SmartStore.Services/Media/ProcessImageException.cs +++ b/src/Libraries/SmartStore.Services/Media/ProcessImageException.cs @@ -21,23 +21,25 @@ public ProcessImageException(string message, ProcessImageQuery query) } public ProcessImageException(ProcessImageQuery query, Exception innerException) - : base(CreateMessage(query), innerException) + : base(CreateMessage(query, innerException), innerException) { Query = query; } - private static string CreateMessage(ProcessImageQuery query) + private static string CreateMessage(ProcessImageQuery query, Exception innerException) { var fileName = query?.FileName; - if (fileName.HasValue()) - { - return "Error while processing image '{0}'.".FormatCurrent(fileName); - } - else + var msg = fileName.HasValue() + ? "Error while processing image '{0}'".FormatCurrent(fileName) + : "Error while processing image"; + + if (innerException != null) { - return "Error while processing image."; + msg += " (" + innerException.Message + ")"; } + + return msg; } public ProcessImageQuery Query { get; private set; } diff --git a/src/Libraries/SmartStore.Services/Media/ProcessImageQuery.cs b/src/Libraries/SmartStore.Services/Media/ProcessImageQuery.cs index 7bc49499a7..9b3c5998e2 100644 --- a/src/Libraries/SmartStore.Services/Media/ProcessImageQuery.cs +++ b/src/Libraries/SmartStore.Services/Media/ProcessImageQuery.cs @@ -170,13 +170,19 @@ 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; } + if (base.Count == 1 && Quality == 100) + { + // If "q" is the only flag and its value is 100, we don't need to process + return false; + } + return true; } diff --git a/src/Libraries/SmartStore.Services/Media/ProcessImageResult.cs b/src/Libraries/SmartStore.Services/Media/ProcessImageResult.cs index e520fcfefd..9ff224d143 100644 --- a/src/Libraries/SmartStore.Services/Media/ProcessImageResult.cs +++ b/src/Libraries/SmartStore.Services/Media/ProcessImageResult.cs @@ -11,12 +11,19 @@ public class ProcessImageResult : DisposableObject public int? SourceWidth { get; set; } public int? SourceHeight { get; set; } + public string SourceMimeType { get; set; } public string FileExtension { get; set; } public string MimeType { get; set; } public int Width { get; set; } public int Height { get; set; } + /// + /// Is true if any effect has been applied that changed the image visually (like background color, contrast, sharpness etc.). + /// Resize and compression quality does NOT count as FX. + /// + public bool HasAppliedVisualEffects { get; set; } + public long ProcessTimeMs { get; set; } protected override void OnDispose(bool disposing) diff --git a/src/Libraries/SmartStore.Services/Orders/IOrderProcessingService.cs b/src/Libraries/SmartStore.Services/Orders/IOrderProcessingService.cs index 9c3f825b03..5ac6d24daf 100644 --- a/src/Libraries/SmartStore.Services/Orders/IOrderProcessingService.cs +++ b/src/Libraries/SmartStore.Services/Orders/IOrderProcessingService.cs @@ -2,6 +2,7 @@ using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Shipping; +using SmartStore.Core.Domain.Stores; using SmartStore.Services.Payments; namespace SmartStore.Services.Orders @@ -16,7 +17,35 @@ public partial interface IOrderProcessingService /// /// Order to be validated void CheckOrderStatus(Order order); - + + /// + /// Checks whether orders are placed at too short intervals. + /// + /// Customer. + /// Store + /// true valid interval, false invalid interval. + bool IsMinimumOrderPlacementIntervalValid(Customer customer, Store store); + + /// + /// Checks whether an order can be placed. + /// + /// Process payment request. + /// List of warning messages. Empty list if an order can be placed. + IList GetOrderPlacementWarnings(ProcessPaymentRequest processPaymentRequest); + + /// + /// Checks whether an order can be placed. + /// + /// Process payment request. + /// Initial order if any (recurring payment). + /// Customer placing the order. + /// List of warning messages. Empty list if an order can be placed. + IList GetOrderPlacementWarnings( + ProcessPaymentRequest processPaymentRequest, + Order initialOrder, + Customer customer, + out IList cart); + /// /// Places an order /// diff --git a/src/Libraries/SmartStore.Services/Orders/IOrderService.cs b/src/Libraries/SmartStore.Services/Orders/IOrderService.cs index 44ce9cc26f..e0d8dddfc4 100644 --- a/src/Libraries/SmartStore.Services/Orders/IOrderService.cs +++ b/src/Libraries/SmartStore.Services/Orders/IOrderService.cs @@ -130,12 +130,6 @@ IPagedList SearchOrders(int storeId, int customerId, DateTime? startTime, /// Orders IPagedList GetAllOrders(int affiliateId, int pageIndex, int pageSize); - /// - /// Load all orders - /// - /// Order collection - IList LoadAllOrders(); - /// /// Gets all orders by affiliate identifier /// diff --git a/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs b/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs index 492618410a..f09a198d95 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs @@ -12,6 +12,7 @@ using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; using SmartStore.Core.Domain.Shipping; +using SmartStore.Core.Domain.Stores; using SmartStore.Core.Domain.Tax; using SmartStore.Core.Events; using SmartStore.Core.Localization; @@ -426,45 +427,321 @@ public void CheckOrderStatus(Order order) #region Methods - /// - /// Places an order - /// - /// Process payment request - /// Place order result + public virtual bool IsMinimumOrderPlacementIntervalValid(Customer customer, Store store) + { + Guard.NotNull(customer, nameof(customer)); + Guard.NotNull(store, nameof(store)); + + // Prevent 2 orders being placed within an X seconds time frame. + if (_orderSettings.MinimumOrderPlacementInterval == 0) + { + return true; + } + + var lastOrder = _orderService.SearchOrders(store.Id, customer.Id, null, null, null, null, null, null, null, null, 0, 1).FirstOrDefault(); + if (lastOrder == null) + { + return true; + } + + var interval = DateTime.UtcNow - lastOrder.CreatedOnUtc; + return interval.TotalSeconds > _orderSettings.MinimumOrderPlacementInterval; + } + + public virtual IList GetOrderPlacementWarnings(ProcessPaymentRequest processPaymentRequest) + { + Guard.NotNull(processPaymentRequest, nameof(processPaymentRequest)); + + var initialOrder = _orderService.GetOrderById(processPaymentRequest.InitialOrderId); + var customer = _customerService.GetCustomerById(processPaymentRequest.CustomerId); + + return GetOrderPlacementWarnings(processPaymentRequest, initialOrder, customer, out var cart); + } + + public virtual IList GetOrderPlacementWarnings( + ProcessPaymentRequest processPaymentRequest, + Order initialOrder, + Customer customer, + out IList cart) + { + Guard.NotNull(processPaymentRequest, nameof(processPaymentRequest)); + + cart = null; + + var warnings = new List(); + var skipPaymentWorkflow = false; + var isRecurringShoppingCart = false; + var paymentMethodSystemName = processPaymentRequest.PaymentMethodSystemName; + + if (customer == null) + { + warnings.Add(T("Customer.DoesNotExist")); + return warnings; + } + + // Check whether guest checkout is allowed. + if (customer.IsGuest() && !_orderSettings.AnonymousCheckoutAllowed) + { + warnings.Add(T("Checkout.AnonymousNotAllowed")); + return warnings; + } + + if (!processPaymentRequest.IsRecurringPayment) + { + if (processPaymentRequest.ShoppingCartItemIds.Any()) + { + cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, processPaymentRequest.StoreId) + .Where(x => processPaymentRequest.ShoppingCartItemIds.Contains(x.Item.Id)) + .ToList(); + } + else + { + cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, processPaymentRequest.StoreId); + } + + if (!cart.Any()) + { + warnings.Add(T("ShoppingCart.CartIsEmpty")); + return warnings; + } + + // Validate the entire shopping cart. + var cartWarnings = _shoppingCartService.GetShoppingCartWarnings(cart, customer.GetAttribute(SystemCustomerAttributeNames.CheckoutAttributes), true); + if (cartWarnings.Any()) + { + warnings.AddRange(cartWarnings); + return warnings; + } + + // Validate individual cart items. + foreach (var sci in cart) + { + var sciWarnings = _shoppingCartService.GetShoppingCartItemWarnings(customer, sci.Item.ShoppingCartType, + sci.Item.Product, processPaymentRequest.StoreId, sci.Item.AttributesXml, + sci.Item.CustomerEnteredPrice, sci.Item.Quantity, false, childItems: sci.ChildItems); + + if (sciWarnings.Any()) + { + warnings.AddRange(sciWarnings); + return warnings; + } + } + + // Min totals validation. + var minOrderSubtotalAmountOk = ValidateMinOrderSubtotalAmount(cart); + if (!minOrderSubtotalAmountOk) + { + var minOrderSubtotalAmount = _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderSubtotalAmount, _workContext.WorkingCurrency); + warnings.Add(T("Checkout.MinOrderSubtotalAmount", _priceFormatter.FormatPrice(minOrderSubtotalAmount, true, false))); + return warnings; + } + + var minOrderTotalAmountOk = ValidateMinOrderTotalAmount(cart); + if (!minOrderTotalAmountOk) + { + var minOrderTotalAmount = _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderTotalAmount, _workContext.WorkingCurrency); + warnings.Add(T("Checkout.MinOrderTotalAmount", _priceFormatter.FormatPrice(minOrderTotalAmount, true, false))); + return warnings; + } + + // Total validations. + var orderShippingTotalInclTax = _orderTotalCalculationService.GetShoppingCartShippingTotal(cart, true, out var orderShippingTaxRate, out var shippingTotalDiscount); + var orderShippingTotalExclTax = _orderTotalCalculationService.GetShoppingCartShippingTotal(cart, false); + + if (!orderShippingTotalInclTax.HasValue || !orderShippingTotalExclTax.HasValue) + { + warnings.Add(T("Order.CannotCalculateShippingTotal")); + return warnings; + } + + var cartTotal = _orderTotalCalculationService.GetShoppingCartTotal(cart); + if (!cartTotal.TotalAmount.HasValue) + { + warnings.Add(T("Order.CannotCalculateOrderTotal")); + return warnings; + } + + skipPaymentWorkflow = cartTotal.TotalAmount.Value == decimal.Zero; + + // Address validations. + if (customer.BillingAddress == null) + { + warnings.Add(T("Order.BillingAddressMissing")); + } + else if (!customer.BillingAddress.Email.IsEmail()) + { + warnings.Add(T("Common.Error.InvalidEmail")); + } + else if (customer.BillingAddress.Country != null && !customer.BillingAddress.Country.AllowsBilling) + { + warnings.Add(T("Order.CountryNotAllowedForBilling", customer.BillingAddress.Country.Name)); + } + + if (cart.RequiresShipping()) + { + if (customer.ShippingAddress == null) + { + warnings.Add(T("Order.ShippingAddressMissing")); + throw new SmartException(); + } + else if (!customer.ShippingAddress.Email.IsEmail()) + { + warnings.Add(T("Common.Error.InvalidEmail")); + } + else if (customer.ShippingAddress.Country != null && !customer.ShippingAddress.Country.AllowsShipping) + { + warnings.Add(T("Order.CountryNotAllowedForShipping", customer.ShippingAddress.Country.Name)); + } + } + } + else + { + // Recurring order. + if (initialOrder == null) + { + warnings.Add(T("Order.InitialOrderDoesNotExistForRecurringPayment")); + return warnings; + } + + var cartTotal = new ShoppingCartTotal(initialOrder.OrderTotal); + skipPaymentWorkflow = cartTotal.TotalAmount.Value == decimal.Zero; + paymentMethodSystemName = initialOrder.PaymentMethodSystemName; + + // Address validations. + if (initialOrder.BillingAddress == null) + { + warnings.Add(T("Order.BillingAddressMissing")); + } + else if (initialOrder.BillingAddress.Country != null && !initialOrder.BillingAddress.Country.AllowsBilling) + { + warnings.Add(T("Order.CountryNotAllowedForBilling", initialOrder.BillingAddress.Country.Name)); + } + + if (initialOrder.ShippingStatus != ShippingStatus.ShippingNotRequired) + { + if (initialOrder.ShippingAddress == null) + { + warnings.Add(T("Order.ShippingAddressMissing")); + } + else if (initialOrder.ShippingAddress.Country != null && !initialOrder.ShippingAddress.Country.AllowsShipping) + { + warnings.Add(T("Order.CountryNotAllowedForShipping", initialOrder.ShippingAddress.Country.Name)); + } + } + } + + // Payment. + if (!warnings.Any() && !skipPaymentWorkflow) + { + var paymentMethod = _paymentService.LoadPaymentMethodBySystemName(paymentMethodSystemName); + if (paymentMethod == null) + { + warnings.Add(T("Payment.CouldNotLoadMethod")); + } + else if (!paymentMethod.IsPaymentMethodActive(_paymentSettings)) + { + warnings.Add(T("Payment.MethodNotAvailable")); + } + } + + // Recurring or standard shopping cart? + if (!warnings.Any() && !processPaymentRequest.IsRecurringPayment) + { + isRecurringShoppingCart = cart.IsRecurring(); + if (isRecurringShoppingCart) + { + var recurringCyclesError = cart.GetRecurringCycleInfo(_localizationService, out var recurringCycleLength, out var recurringCyclePeriod, out var recurringTotalCycles); + if (recurringCyclesError.HasValue()) + { + warnings.Add(recurringCyclesError); + } + } + } + else + { + isRecurringShoppingCart = true; + } + + // Validate recurring payment type. + if (!warnings.Any() && !skipPaymentWorkflow && !processPaymentRequest.IsMultiOrder) + { + RecurringPaymentType? recurringPaymentType = null; + + if (!processPaymentRequest.IsRecurringPayment) + { + if (isRecurringShoppingCart) + { + recurringPaymentType = _paymentService.GetRecurringPaymentType(processPaymentRequest.PaymentMethodSystemName); + } + } + else + { + if (isRecurringShoppingCart) + { + recurringPaymentType = _paymentService.GetRecurringPaymentType(processPaymentRequest.PaymentMethodSystemName); + } + else + { + warnings.Add(T("Order.NoRecurringProducts")); + } + } + + if (recurringPaymentType.HasValue) + { + switch (recurringPaymentType.Value) + { + case RecurringPaymentType.NotSupported: + warnings.Add(T("Payment.RecurringPaymentNotSupported")); + break; + case RecurringPaymentType.Manual: + case RecurringPaymentType.Automatic: + break; + default: + warnings.Add(T("Payment.RecurringPaymentTypeUnknown")); + break; + } + } + } + + return warnings; + } + public virtual PlaceOrderResult PlaceOrder( ProcessPaymentRequest processPaymentRequest, Dictionary extraData) { - // think about moving functionality of processing recurring orders (after the initial order was placed) to ProcessNextRecurringPayment() method - if (processPaymentRequest == null) - throw new ArgumentNullException("processPaymentRequest"); + // Think about moving functionality of processing recurring orders (after the initial order was placed) to ProcessNextRecurringPayment() method. + Guard.NotNull(processPaymentRequest, nameof(processPaymentRequest)); if (processPaymentRequest.OrderGuid == Guid.Empty) + { processPaymentRequest.OrderGuid = Guid.NewGuid(); + } var result = new PlaceOrderResult(); var utcNow = DateTime.UtcNow; try { - #region Order details (customer, totals) - - // Recurring orders. Load initial order var initialOrder = _orderService.GetOrderById(processPaymentRequest.InitialOrderId); - if (processPaymentRequest.IsRecurringPayment) + var customer = _customerService.GetCustomerById(processPaymentRequest.CustomerId); + + var warnings = GetOrderPlacementWarnings(processPaymentRequest, initialOrder, customer, out var cart); + if (warnings.Any()) { - if (initialOrder == null) - throw new ArgumentException(T("Order.InitialOrderDoesNotExistForRecurringPayment")); + result.Errors.AddRange(warnings); + Logger.Warn(string.Join(" ", result.Errors)); + return result; + } + + #region Order details + if (processPaymentRequest.IsRecurringPayment) + { processPaymentRequest.PaymentMethodSystemName = initialOrder.PaymentMethodSystemName; } - // customer - var customer = _customerService.GetCustomerById(processPaymentRequest.CustomerId); - if (customer == null) - throw new ArgumentException(T("Customer.DoesNotExist")); - - // affilites + // Affilites. var affiliateId = 0; var affiliate = _affiliateService.GetAffiliateById(customer.AffiliateId); if (affiliate != null && affiliate.Active && !affiliate.Deleted) @@ -472,9 +749,9 @@ public virtual PlaceOrderResult PlaceOrder( affiliateId = affiliate.Id; } - // customer currency - string customerCurrencyCode = ""; - decimal customerCurrencyRate = decimal.Zero; + // Customer currency. + var customerCurrencyCode = string.Empty; + var customerCurrencyRate = decimal.Zero; if (!processPaymentRequest.IsRecurringPayment) { var currencyTmp = _currencyService.GetCurrencyById(customer.GetAttribute(SystemCustomerAttributeNames.CurrencyId, processPaymentRequest.StoreId)); @@ -482,7 +759,6 @@ public virtual PlaceOrderResult PlaceOrder( customerCurrencyCode = customerCurrency.CurrencyCode; var primaryStoreCurrency = _storeContext.CurrentStore.PrimaryStoreCurrency; - customerCurrencyRate = customerCurrency.Rate / primaryStoreCurrency.Rate; } else @@ -491,7 +767,7 @@ public virtual PlaceOrderResult PlaceOrder( customerCurrencyRate = initialOrder.CurrencyRate; } - // customer language + // Customer language. Language customerLanguage = null; if (!processPaymentRequest.IsRecurringPayment) { @@ -506,66 +782,8 @@ public virtual PlaceOrderResult PlaceOrder( { customerLanguage = _workContext.WorkingLanguage; } - - // check whether customer is guest - if (customer.IsGuest() && !_orderSettings.AnonymousCheckoutAllowed) - throw new SmartException(T("Checkout.AnonymousNotAllowed")); - - var storeId = _storeContext.CurrentStore.Id; - - // load and validate customer shopping cart - IList cart = null; - if (!processPaymentRequest.IsRecurringPayment) - { - //load shopping cart - if (processPaymentRequest.ShoppingCartItemIds.Count > 0) - { - cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, processPaymentRequest.StoreId) - .Where(x => processPaymentRequest.ShoppingCartItemIds.Contains(x.Item.Id)) - .ToList(); - } - else - cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, processPaymentRequest.StoreId); - - if (cart.Count == 0) - throw new SmartException(T("ShoppingCart.CartIsEmpty")); - - // validate the entire shopping cart - var warnings = _shoppingCartService.GetShoppingCartWarnings(cart, customer.GetAttribute(SystemCustomerAttributeNames.CheckoutAttributes), true); - if (warnings.Count > 0) - throw new SmartException(string.Join(" ", warnings)); - - // validate individual cart items - foreach (var sci in cart) - { - var sciWarnings = _shoppingCartService.GetShoppingCartItemWarnings(customer, sci.Item.ShoppingCartType, - sci.Item.Product, processPaymentRequest.StoreId, sci.Item.AttributesXml, - sci.Item.CustomerEnteredPrice, sci.Item.Quantity, false, childItems: sci.ChildItems); - - if (sciWarnings.Count > 0) - throw new SmartException(string.Join(" ", sciWarnings)); - } - } - - // min totals validation - if (!processPaymentRequest.IsRecurringPayment) - { - var minOrderSubtotalAmountOk = ValidateMinOrderSubtotalAmount(cart); - if (!minOrderSubtotalAmountOk) - { - var minOrderSubtotalAmount = _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderSubtotalAmount, _workContext.WorkingCurrency); - throw new SmartException(T("Checkout.MinOrderSubtotalAmount", _priceFormatter.FormatPrice(minOrderSubtotalAmount, true, false))); - } - - var minOrderTotalAmountOk = ValidateMinOrderTotalAmount(cart); - if (!minOrderTotalAmountOk) - { - var minOrderTotalAmount = _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderTotalAmount, _workContext.WorkingCurrency); - throw new SmartException(T("Checkout.MinOrderTotalAmount", _priceFormatter.FormatPrice(minOrderTotalAmount, true, false))); - } - } - - // tax display type + + // Tax display type. var customerTaxDisplayType = TaxDisplayType.IncludingTax; if (!processPaymentRequest.IsRecurringPayment) { @@ -576,7 +794,7 @@ public virtual PlaceOrderResult PlaceOrder( customerTaxDisplayType = initialOrder.CustomerTaxDisplayType; } - // checkout attributes + // Checkout attributes. string checkoutAttributeDescription, checkoutAttributesXml; if (!processPaymentRequest.IsRecurringPayment) { @@ -589,38 +807,28 @@ public virtual PlaceOrderResult PlaceOrder( checkoutAttributeDescription = initialOrder.CheckoutAttributeDescription; } - // applied discount (used to store discount usage history) + // Applied discount (used to store discount usage history). var appliedDiscounts = new List(); - - // sub total decimal orderSubTotalInclTax, orderSubTotalExclTax; decimal orderSubTotalDiscountInclTax = 0, orderSubTotalDiscountExclTax = 0; if (!processPaymentRequest.IsRecurringPayment) { - // sub total (incl tax) - decimal orderSubTotalDiscountAmount1 = decimal.Zero; - Discount orderSubTotalAppliedDiscount1 = null; - decimal subTotalWithoutDiscountBase1 = decimal.Zero; - decimal subTotalWithDiscountBase1 = decimal.Zero; - - _orderTotalCalculationService.GetShoppingCartSubTotal(cart, - true, out orderSubTotalDiscountAmount1, out orderSubTotalAppliedDiscount1, out subTotalWithoutDiscountBase1, out subTotalWithDiscountBase1); + // Sub total (incl tax). + _orderTotalCalculationService.GetShoppingCartSubTotal(cart, true, + out var orderSubTotalDiscountAmount1, out var orderSubTotalAppliedDiscount1, out var subTotalWithoutDiscountBase1, out var subTotalWithDiscountBase1); orderSubTotalInclTax = subTotalWithoutDiscountBase1; orderSubTotalDiscountInclTax = orderSubTotalDiscountAmount1; - // discount history + // Discount history. if (orderSubTotalAppliedDiscount1 != null && !appliedDiscounts.Any(x => x.Id == orderSubTotalAppliedDiscount1.Id)) + { appliedDiscounts.Add(orderSubTotalAppliedDiscount1); + } - // sub total (excl tax) - decimal orderSubTotalDiscountAmount2 = decimal.Zero; - Discount orderSubTotalAppliedDiscount2 = null; - decimal subTotalWithoutDiscountBase2 = decimal.Zero; - decimal subTotalWithDiscountBase2 = decimal.Zero; - - _orderTotalCalculationService.GetShoppingCartSubTotal(cart, - false, out orderSubTotalDiscountAmount2, out orderSubTotalAppliedDiscount2, out subTotalWithoutDiscountBase2, out subTotalWithDiscountBase2); + // Sub total (excl tax). + _orderTotalCalculationService.GetShoppingCartSubTotal(cart, false, + out var orderSubTotalDiscountAmount2, out var orderSubTotalAppliedDiscount2, out var subTotalWithoutDiscountBase2, out var subTotalWithDiscountBase2); orderSubTotalExclTax = subTotalWithoutDiscountBase2; orderSubTotalDiscountExclTax = orderSubTotalDiscountAmount2; @@ -632,8 +840,8 @@ public virtual PlaceOrderResult PlaceOrder( } - // shipping info - bool shoppingCartRequiresShipping = false; + // Shipping info. + var shoppingCartRequiresShipping = false; if (!processPaymentRequest.IsRecurringPayment) { shoppingCartRequiresShipping = cart.RequiresShipping(); @@ -662,19 +870,18 @@ public virtual PlaceOrderResult PlaceOrder( } } - // shipping total + // Shipping total. decimal? orderShippingTotalInclTax, orderShippingTotalExclTax = null; decimal orderShippingTaxRate = decimal.Zero; if (!processPaymentRequest.IsRecurringPayment) { - Discount shippingTotalDiscount = null; - orderShippingTotalInclTax = _orderTotalCalculationService.GetShoppingCartShippingTotal(cart, true, out orderShippingTaxRate, out shippingTotalDiscount); + orderShippingTotalInclTax = _orderTotalCalculationService.GetShoppingCartShippingTotal(cart, true, out orderShippingTaxRate, out var shippingTotalDiscount); orderShippingTotalExclTax = _orderTotalCalculationService.GetShoppingCartShippingTotal(cart, false); - if (!orderShippingTotalInclTax.HasValue || !orderShippingTotalExclTax.HasValue) - throw new SmartException(T("Order.CannotCalculateShippingTotal")); if (shippingTotalDiscount != null && !appliedDiscounts.Any(x => x.Id == shippingTotalDiscount.Id)) + { appliedDiscounts.Add(shippingTotalDiscount); + } } else { @@ -683,11 +890,11 @@ public virtual PlaceOrderResult PlaceOrder( orderShippingTaxRate = initialOrder.OrderShippingTaxRate; } - //payment total + // Payment total. decimal paymentAdditionalFeeInclTax, paymentAdditionalFeeExclTax, paymentAdditionalFeeTaxRate; if (!processPaymentRequest.IsRecurringPayment) { - decimal paymentAdditionalFee = _paymentService.GetAdditionalHandlingFee(cart, processPaymentRequest.PaymentMethodSystemName); + var paymentAdditionalFee = _paymentService.GetAdditionalHandlingFee(cart, processPaymentRequest.PaymentMethodSystemName); paymentAdditionalFeeInclTax = _taxService.GetPaymentMethodAdditionalFee(paymentAdditionalFee, true, customer, out paymentAdditionalFeeTaxRate); paymentAdditionalFeeExclTax = _taxService.GetPaymentMethodAdditionalFee(paymentAdditionalFee, false, customer); } @@ -698,16 +905,15 @@ public virtual PlaceOrderResult PlaceOrder( paymentAdditionalFeeTaxRate = initialOrder.PaymentMethodAdditionalFeeTaxRate; } - //tax total - decimal orderTaxTotal = decimal.Zero; + // Tax total. + var orderTaxTotal = decimal.Zero; string vatNumber = "", taxRates = ""; if (!processPaymentRequest.IsRecurringPayment) { - //tax amount - SortedDictionary taxRatesDictionary = null; - orderTaxTotal = _orderTotalCalculationService.GetTaxTotal(cart, out taxRatesDictionary); + // Tax amount. + orderTaxTotal = _orderTotalCalculationService.GetTaxTotal(cart, out var taxRatesDictionary); - //VAT number + // VAT number. var customerVatStatus = (VatNumberStatus)customer.GetAttribute(SystemCustomerAttributeNames.VatNumberStatusId); if (_taxSettings.EuVatEnabled && customerVatStatus == VatNumberStatus.Valid) { @@ -719,22 +925,19 @@ public virtual PlaceOrderResult PlaceOrder( else { orderTaxTotal = initialOrder.OrderTax; - // VAT number vatNumber = initialOrder.VatNumber; } + processPaymentRequest.OrderTax = orderTaxTotal; - // Order total (and applied discounts, gift cards, reward points) + // Order total (and applied discounts, gift cards, reward points). ShoppingCartTotal cartTotal = null; if (!processPaymentRequest.IsRecurringPayment) { cartTotal = _orderTotalCalculationService.GetShoppingCartTotal(cart); - if (!cartTotal.TotalAmount.HasValue) - throw new SmartException(T("Order.CannotCalculateOrderTotal")); - - // Discount history + // Discount history. if (cartTotal.AppliedDiscount != null && !appliedDiscounts.Any(x => x.Id == cartTotal.AppliedDiscount.Id)) { appliedDiscounts.Add(cartTotal.AppliedDiscount); @@ -750,9 +953,9 @@ public virtual PlaceOrderResult PlaceOrder( #endregion - #region Addresses & pre payment workflow + #region Addresses & pre-payment workflow - // give payment processor the opportunity to fullfill billing address + // Give payment processor the opportunity to fullfill billing address. var preProcessPaymentResult = _paymentService.PreProcessPayment(processPaymentRequest); if (!preProcessPaymentResult.Success) @@ -762,95 +965,44 @@ public virtual PlaceOrderResult PlaceOrder( return result; } - Address billingAddress = null; - if (!processPaymentRequest.IsRecurringPayment) - { - if (customer.BillingAddress == null) - throw new SmartException(T("Order.BillingAddressMissing")); - - if (!customer.BillingAddress.Email.IsEmail()) - throw new SmartException(T("Common.Error.InvalidEmail")); - - billingAddress = (Address)customer.BillingAddress.Clone(); - } - else - { - if (initialOrder.BillingAddress == null) - throw new SmartException(T("Order.BillingAddressMissing")); - - billingAddress = (Address)initialOrder.BillingAddress.Clone(); - } - - if (billingAddress.Country != null && !billingAddress.Country.AllowsBilling) - throw new SmartException(T("Order.CountryNotAllowedForBilling", billingAddress.Country.Name)); + var billingAddress = !processPaymentRequest.IsRecurringPayment + ? (Address)customer.BillingAddress.Clone() + : (Address)initialOrder.BillingAddress.Clone(); Address shippingAddress = null; if (shoppingCartRequiresShipping) { - if (!processPaymentRequest.IsRecurringPayment) - { - if (customer.ShippingAddress == null) - throw new SmartException(T("Order.ShippingAddressMissing")); - - if (!customer.ShippingAddress.Email.IsEmail()) - throw new SmartException(T("Common.Error.InvalidEmail")); - - shippingAddress = (Address)customer.ShippingAddress.Clone(); - } - else - { - if (initialOrder.ShippingAddress == null) - throw new SmartException(T("Order.ShippingAddressMissing")); - - shippingAddress = (Address)initialOrder.ShippingAddress.Clone(); - } - - if (shippingAddress.Country != null && !shippingAddress.Country.AllowsShipping) - throw new SmartException(T("Order.CountryNotAllowedForShipping", shippingAddress.Country.Name)); + shippingAddress = !processPaymentRequest.IsRecurringPayment + ? (Address)customer.ShippingAddress.Clone() + : (Address)initialOrder.ShippingAddress.Clone(); } #endregion #region Payment workflow - // skip payment workflow if order total equals zero - var skipPaymentWorkflow = false; - if (cartTotal.TotalAmount.Value == decimal.Zero) - { - skipPaymentWorkflow = true; - } + // Skip payment workflow if order total equals zero. + var skipPaymentWorkflow = cartTotal.TotalAmount.Value == decimal.Zero; - // payment workflow + // Payment workflow. Provider paymentMethod = null; if (!skipPaymentWorkflow) { paymentMethod = _paymentService.LoadPaymentMethodBySystemName(processPaymentRequest.PaymentMethodSystemName); - if (paymentMethod == null) - throw new SmartException(T("Payment.CouldNotLoadMethod")); - - // ensure that payment method is active - if (!paymentMethod.IsPaymentMethodActive(_paymentSettings)) - throw new SmartException(T("Payment.MethodNotAvailable")); } else { processPaymentRequest.PaymentMethodSystemName = ""; } - // recurring or standard shopping cart? + // Recurring or standard shopping cart? var isRecurringShoppingCart = false; if (!processPaymentRequest.IsRecurringPayment) { isRecurringShoppingCart = cart.IsRecurring(); if (isRecurringShoppingCart) { - var recurringCycleLength = 0; - var recurringTotalCycles = 0; - RecurringProductCyclePeriod recurringCyclePeriod; - string recurringCyclesError = cart.GetRecurringCycleInfo(_localizationService, out recurringCycleLength, out recurringCyclePeriod, out recurringTotalCycles); - - if (!string.IsNullOrEmpty(recurringCyclesError)) - throw new SmartException(recurringCyclesError); + var unused = cart.GetRecurringCycleInfo(_localizationService, out var recurringCycleLength, out var recurringCyclePeriod, out var recurringTotalCycles); processPaymentRequest.RecurringCycleLength = recurringCycleLength; processPaymentRequest.RecurringCyclePeriod = recurringCyclePeriod; @@ -862,7 +1014,7 @@ public virtual PlaceOrderResult PlaceOrder( isRecurringShoppingCart = true; } - // process payment + // Process payment. ProcessPaymentResult processPaymentResult = null; if (!skipPaymentWorkflow && !processPaymentRequest.IsMultiOrder) { @@ -870,7 +1022,7 @@ public virtual PlaceOrderResult PlaceOrder( { if (isRecurringShoppingCart) { - // recurring cart + // Recurring cart. var recurringPaymentType = _paymentService.GetRecurringPaymentType(processPaymentRequest.PaymentMethodSystemName); switch (recurringPaymentType) { @@ -886,7 +1038,7 @@ public virtual PlaceOrderResult PlaceOrder( } else { - // standard cart + // Standard cart. processPaymentResult = _paymentService.ProcessPayment(processPaymentRequest); } } @@ -894,11 +1046,11 @@ public virtual PlaceOrderResult PlaceOrder( { if (isRecurringShoppingCart) { - // Old credit card info + // Old credit card info. processPaymentRequest.CreditCardType = initialOrder.AllowStoringCreditCardNumber ? _encryptionService.DecryptText(initialOrder.CardType) : ""; processPaymentRequest.CreditCardName = initialOrder.AllowStoringCreditCardNumber ? _encryptionService.DecryptText(initialOrder.CardName) : ""; processPaymentRequest.CreditCardNumber = initialOrder.AllowStoringCreditCardNumber ? _encryptionService.DecryptText(initialOrder.CardNumber) : ""; - // MaskedCreditCardNumber + // MaskedCreditCardNumber. processPaymentRequest.CreditCardCvv2 = initialOrder.AllowStoringCreditCardNumber ? _encryptionService.DecryptText(initialOrder.CardCvv2) : ""; try @@ -917,7 +1069,7 @@ public virtual PlaceOrderResult PlaceOrder( processPaymentResult = _paymentService.ProcessRecurringPayment(processPaymentRequest); break; case RecurringPaymentType.Automatic: - //payment is processed on payment gateway site + // Payment is processed on payment gateway site. processPaymentResult = new ProcessPaymentResult(); break; default: @@ -932,20 +1084,19 @@ public virtual PlaceOrderResult PlaceOrder( } else { - // payment is not required - if (processPaymentResult == null) - { - processPaymentResult = new ProcessPaymentResult(); - } - processPaymentResult.NewPaymentStatus = PaymentStatus.Paid; + // Payment is not required. + processPaymentResult = new ProcessPaymentResult + { + NewPaymentStatus = PaymentStatus.Paid + }; } #endregion if (processPaymentResult.Success) { - //save order in data storage - //uncomment this line to support transactions + // Save order. + // Uncomment this line to support transactions. //using (var scope = new System.Transactions.TransactionScope()) { #region Save order details @@ -1112,7 +1263,7 @@ public virtual PlaceOrderResult PlaceOrder( order.OrderItems.Add(orderItem); _orderService.UpdateOrder(order); - // Gift cards + // Gift cards. if (sc.Item.Product.IsGiftCard) { _productAttributeParser.GetGiftCardAttribute( @@ -1147,7 +1298,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)); @@ -1155,11 +1306,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(), @@ -1187,7 +1338,7 @@ public virtual PlaceOrderResult PlaceOrder( order.OrderItems.Add(newOrderItem); _orderService.UpdateOrder(order); - // Gift cards + // Gift cards. if (orderItem.Product.IsGiftCard) { _productAttributeParser.GetGiftCardAttribute( @@ -1223,7 +1374,7 @@ public virtual PlaceOrderResult PlaceOrder( } } - // Discount usage history + // Discount usage history. if (!processPaymentRequest.IsRecurringPayment) { foreach (var discount in appliedDiscounts) @@ -1238,7 +1389,7 @@ public virtual PlaceOrderResult PlaceOrder( } } - // Gift card usage history + // Gift card usage history. if (!processPaymentRequest.IsRecurringPayment && cartTotal.AppliedGiftCards != null) { foreach (var agc in cartTotal.AppliedGiftCards) @@ -1256,7 +1407,7 @@ public virtual PlaceOrderResult PlaceOrder( } } - // Reward points history + // Reward points history. if (cartTotal.RedeemedRewardPointsAmount > decimal.Zero) { customer.AddRewardPointsHistoryEntry(-cartTotal.RedeemedRewardPoints, @@ -1267,10 +1418,10 @@ public virtual PlaceOrderResult PlaceOrder( _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, @@ -1287,26 +1438,22 @@ public virtual PlaceOrderResult PlaceOrder( switch (recurringPaymentType) { case RecurringPaymentType.NotSupported: - { - //not supported - } break; case RecurringPaymentType.Manual: { - //first payment - var rph = new RecurringPaymentHistory() + // First payment. + rp.RecurringPaymentHistory.Add(new RecurringPaymentHistory { RecurringPayment = rp, - CreatedOnUtc = utcNow, - OrderId = order.Id, - }; - rp.RecurringPaymentHistory.Add(rph); + CreatedOnUtc = utcNow, + OrderId = order.Id + }); _orderService.UpdateRecurringPayment(rp); } break; case RecurringPaymentType.Automatic: { - //will be created later (process is automated) + // Will be created later (process is automated). } break; default: @@ -1318,10 +1465,10 @@ public virtual PlaceOrderResult PlaceOrder( #region Notifications, notes and attributes - // notes, messages + // Notes, messages. _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderPlaced")); - // send email notifications + // Send email notifications. var msg = _messageFactory.SendOrderPlacedStoreOwnerNotification(order, _localizationSettings.DefaultAdminLanguageId); if (msg?.Email?.Id != null) { @@ -1334,25 +1481,25 @@ public virtual PlaceOrderResult PlaceOrder( _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerEmailQueued", msg.Email.Id)); } - // check order status + // Check order status. CheckOrderStatus(order); - //reset checkout data + // Reset checkout data. if (!processPaymentRequest.IsRecurringPayment && !processPaymentRequest.IsMultiOrder) { _customerService.ResetCheckoutData(customer, processPaymentRequest.StoreId, true, true, true, clearCreditBalance: true); } - // check for generic attributes to be inserted automatically + // Check for generic attributes to be inserted automatically. foreach (var customProperty in processPaymentRequest.CustomProperties.Where(x => x.Key.HasValue() && x.Value.AutoCreateGenericAttribute)) { _genericAttributeService.SaveAttribute(order, customProperty.Key, customProperty.Value.Value, order.StoreId); } - //uncomment this line to support transactions + // Uncomment this line to support transactions. //scope.Complete(); - // raise event + // Publish events. _eventPublisher.PublishOrderPlaced(order); if (!processPaymentRequest.IsRecurringPayment) @@ -1360,7 +1507,6 @@ public virtual PlaceOrderResult PlaceOrder( _customerActivityService.InsertActivity("PublicStore.PlaceOrder", T("ActivityLog.PublicStore.PlaceOrder", order.GetOrderNumber())); } - //raise event if (order.PaymentStatus == PaymentStatus.Paid) { _eventPublisher.PublishOrderPaid(order); @@ -1378,10 +1524,7 @@ public virtual PlaceOrderResult PlaceOrder( if (nsResult.HasValue) { - if (nsResult.Value) - _orderService.AddOrderNote(order, T("Admin.OrderNotice.NewsLetterSubscriptionAdded")); - else - _orderService.AddOrderNote(order, T("Admin.OrderNotice.NewsLetterSubscriptionRemoved")); + _orderService.AddOrderNote(order, T(nsResult.Value ? "Admin.OrderNotice.NewsLetterSubscriptionAdded" : "Admin.OrderNotice.NewsLetterSubscriptionRemoved")); } } @@ -1404,7 +1547,7 @@ public virtual PlaceOrderResult PlaceOrder( result.AddError(ex.Message); } - if (result.Errors.Count > 0) + if (result.Errors.Any()) { Logger.Error(string.Join(" ", result.Errors)); } @@ -1418,30 +1561,28 @@ public virtual PlaceOrderResult PlaceOrder( /// The order public virtual void DeleteOrder(Order order) { - if (order == null) - throw new ArgumentNullException("order"); - - //reward points - ReduceRewardPoints(order); + Guard.NotNull(order, nameof(order)); - //cancel recurring payments - var recurringPayments = _orderService.SearchRecurringPayments(0, 0, order.Id, null); - foreach (var rp in recurringPayments) + if (order.OrderStatus != OrderStatus.Cancelled) { - //use errors? - var errors = CancelRecurringPayment(rp); + ReduceRewardPoints(order); + + // Cancel recurring payments. + var recurringPayments = _orderService.SearchRecurringPayments(0, 0, order.Id, null); + foreach (var rp in recurringPayments) + { + CancelRecurringPayment(rp); + } + + // Adjust inventory. + foreach (var orderItem in order.OrderItems) + { + _productService.AdjustInventory(orderItem, false, orderItem.Quantity); + } } - //Adjust inventory - foreach (var orderItem in order.OrderItems) - { - _productService.AdjustInventory(orderItem, false, orderItem.Quantity); - } + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderDeleted")); - //add a note - _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderDeleted")); - - //now delete an order _orderService.DeleteOrder(order); } @@ -1714,26 +1855,26 @@ public virtual bool CanCancelOrder(Order order) /// True to notify customer public virtual void CancelOrder(Order order, bool notifyCustomer) { - if (order == null) - throw new ArgumentNullException("order"); + Guard.NotNull(order, nameof(order)); if (!CanCancelOrder(order)) + { throw new SmartException(T("Order.CannotCancel")); + } - //Cancel order + // Cancel order. SetOrderStatus(order, OrderStatus.Cancelled, notifyCustomer); _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderCancelled")); - //cancel recurring payments + // Cancel recurring payments. var recurringPayments = _orderService.SearchRecurringPayments(0, 0, order.Id, null); foreach (var rp in recurringPayments) { - //use errors? - var errors = CancelRecurringPayment(rp); + CancelRecurringPayment(rp); } - //Adjust inventory + // Adjust inventory. foreach (var orderItem in order.OrderItems) { _productService.AdjustInventory(orderItem, false, orderItem.Quantity); diff --git a/src/Libraries/SmartStore.Services/Orders/OrderService.cs b/src/Libraries/SmartStore.Services/Orders/OrderService.cs index 85959f1d5b..0d9d8f965d 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderService.cs @@ -10,10 +10,11 @@ using SmartStore.Core.Domain.Payments; using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Events; +using SmartStore.Data.Caching; namespace SmartStore.Services.Orders { - public partial class OrderService : IOrderService + public partial class OrderService : IOrderService { private readonly IRepository _orderRepository; private readonly IRepository _orderItemRepository; @@ -48,7 +49,7 @@ public virtual Order GetOrderById(int orderId) if (orderId == 0) return null; - return _orderRepository.GetById(orderId); + return _orderRepository.GetByIdCached(orderId, "db.order.id-" + orderId); } public virtual IList GetOrdersByIds(int[] orderIds) @@ -219,11 +220,6 @@ public virtual IPagedList GetAllOrders(int affiliateId, int pageIndex, in return orders; } - public virtual IList LoadAllOrders() - { - return SearchOrders(0, 0, null, null, null, null, null, null, null, null, 0, int.MaxValue); - } - public virtual IList GetOrdersByAffiliateId(int affiliateId) { var query = from o in _orderRepository.Table diff --git a/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs b/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs index 47e05d3015..d0880c760d 100644 --- a/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs +++ b/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs @@ -942,10 +942,16 @@ public virtual IList GetShoppingCartWarnings(IList a.TextPrompt))) - warnings.Add(ca2.GetLocalized(a => a.TextPrompt)); + string textPrompt = ca2.GetLocalized(a => a.TextPrompt); + + if (textPrompt.HasValue()) + { + warnings.Add(textPrompt); + } else + { warnings.Add(T("ShoppingCart.SelectAttribute", ca2.GetLocalized(a => a.Name))); + } } } } diff --git a/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryFactory.cs b/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryFactory.cs index 88bb3da613..3c30544a0d 100644 --- a/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryFactory.cs +++ b/src/Libraries/SmartStore.Services/Search/Catalog/Modelling/CatalogSearchQueryFactory.cs @@ -595,7 +595,7 @@ protected virtual void OnConverted(CatalogSearchQuery query, RouteData routeData protected virtual bool GetValueFor(CatalogSearchQuery query, string key, FacetGroupKind kind, out T value) { - return GetValueFor(_catalogSearchQueryAliasMapper.GetCommonFacetAliasByGroupKind(kind, query.LanguageId ?? 0) ?? key, out value); + return TryGetValueFor(_catalogSearchQueryAliasMapper.GetCommonFacetAliasByGroupKind(kind, query.LanguageId ?? 0) ?? key, out value); } } } diff --git a/src/Libraries/SmartStore.Services/Search/Forum/Modelling/ForumSearchQueryFactory.cs b/src/Libraries/SmartStore.Services/Search/Forum/Modelling/ForumSearchQueryFactory.cs index 05f89aa954..d4c9c754d5 100644 --- a/src/Libraries/SmartStore.Services/Search/Forum/Modelling/ForumSearchQueryFactory.cs +++ b/src/Libraries/SmartStore.Services/Search/Forum/Modelling/ForumSearchQueryFactory.cs @@ -252,7 +252,7 @@ protected virtual void OnConverted(ForumSearchQuery query, RouteData routeData, protected virtual bool GetValueFor(ForumSearchQuery query, string key, FacetGroupKind kind, out T value) { - return GetValueFor(_forumSearchQueryAliasMapper.GetCommonFacetAliasByGroupKind(kind, query.LanguageId ?? 0) ?? key, out value); + return TryGetValueFor(_forumSearchQueryAliasMapper.GetCommonFacetAliasByGroupKind(kind, query.LanguageId ?? 0) ?? key, out value); } protected virtual void AddDates(FacetDescriptor descriptor, DateTime? selectedFrom, DateTime? selectedTo) diff --git a/src/Libraries/SmartStore.Services/Search/SearchQueryFactoryBase.cs b/src/Libraries/SmartStore.Services/Search/SearchQueryFactoryBase.cs index 998503180d..0fa710c25d 100644 --- a/src/Libraries/SmartStore.Services/Search/SearchQueryFactoryBase.cs +++ b/src/Libraries/SmartStore.Services/Search/SearchQueryFactoryBase.cs @@ -62,21 +62,19 @@ protected virtual Multimap Aliases protected virtual T GetValueFor(string key) { - T value; - return GetValueFor(key, out value) ? value : default(T); + return TryGetValueFor(key, out T value) ? value : default; } - protected virtual bool GetValueFor(string key, out T value) + protected virtual bool TryGetValueFor(string key, out T value) { var strValue = _httpContext.Request?.Unvalidated.Form?[key] ?? _httpContext.Request?.Unvalidated.QueryString?[key]; if (strValue.HasValue()) { - value = strValue.Convert(); - return true; + return CommonHelper.TryConvert(strValue, out value); } - value = default(T); + value = default; return false; } diff --git a/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs b/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs index d427ecc2d8..3a2c115b85 100644 --- a/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs +++ b/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs @@ -348,6 +348,7 @@ public static string GetSeName(string name, SeoSettings seoSettings) name, seoSettings == null ? false : seoSettings.ConvertNonWesternChars, seoSettings == null ? false : seoSettings.AllowUnicodeCharsInUrls, + true, seoSettings?.SeoNameCharConversion); } diff --git a/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs b/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs index 49a9cde4b2..17a8e5356e 100644 --- a/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs +++ b/src/Libraries/SmartStore.Services/Seo/XmlSitemapGenerator.cs @@ -295,7 +295,7 @@ public virtual async Task RebuildAsync(XmlSitemapBuildContext ctx) using (new DbContextScope(autoDetectChanges: false, forceNoTracking: true, proxyCreation: false, lazyLoading: false)) { - var entities = await EnumerateEntitiesAsync(queries); + var entities = EnumerateEntities(queries); foreach (var batch in entities.Slice(MaximumSiteMapNodeCount)) { @@ -474,24 +474,33 @@ private void SaveFinal(LanguageData data) } } - private async Task> EnumerateEntitiesAsync(QueryHolder queries) + private IEnumerable EnumerateEntities(QueryHolder queries) { if (queries.Categories != null) { - var categories = await queries.Categories.Select(x => new { x.Id, x.UpdatedOnUtc }).ToListAsync(); - return categories.Select(x => new NamedEntity { EntityName = "Category", Id = x.Id, LastMod = x.UpdatedOnUtc }); + var categories = queries.Categories.Select(x => new { x.Id, x.UpdatedOnUtc }).ToList(); + foreach (var x in categories) + { + yield return new NamedEntity { EntityName = "Category", Id = x.Id, LastMod = x.UpdatedOnUtc }; + } } if (queries.Manufacturers != null) { - var manufacturers = await queries.Manufacturers.Select(x => new { x.Id, x.UpdatedOnUtc }).ToListAsync(); - return manufacturers.Select(x => new NamedEntity { EntityName = "Manufacturer", Id = x.Id, LastMod = x.UpdatedOnUtc }); + var manufacturers = queries.Manufacturers.Select(x => new { x.Id, x.UpdatedOnUtc }).ToList(); + foreach (var x in manufacturers) + { + yield return new NamedEntity { EntityName = "Manufacturer", Id = x.Id, LastMod = x.UpdatedOnUtc }; + } } if (queries.Topics != null) { - var topics = await queries.Topics.Select(x => new { x.Id }).ToListAsync(); - return topics.Select(x => new NamedEntity { EntityName = "Topic", Id = x.Id, LastMod = DateTime.UtcNow }); + var topics = queries.Topics.Select(x => new { x.Id }).ToList(); + foreach (var x in topics) + { + yield return new NamedEntity { EntityName = "Topic", Id = x.Id, LastMod = DateTime.UtcNow }; + } } if (queries.Products != null) @@ -502,12 +511,12 @@ private async Task> EnumerateEntitiesAsync(QueryHolder //var limit = 0; while (maxId > 1) { - var products = await query + var products = query .Where(x => x.Id < maxId) .OrderByDescending(x => x.Id) .Take(() => MaximumSiteMapNodeCount) .Select(x => new { x.Id, x.UpdatedOnUtc }) - .ToListAsync(); + .ToList(); //limit++; //if (limit >= 100) @@ -522,11 +531,12 @@ private async Task> EnumerateEntitiesAsync(QueryHolder maxId = products.Last().Id; - return products.Select(x => new NamedEntity { EntityName = "Product", Id = x.Id, LastMod = x.UpdatedOnUtc }); + foreach (var x in products) + { + yield return new NamedEntity { EntityName = "Product", Id = x.Id, LastMod = x.UpdatedOnUtc }; + } } } - - return Enumerable.Empty(); } private IDictionary GetUrlRecordCollectionsForBatch(IEnumerable batch, int[] languageIds) diff --git a/src/Libraries/SmartStore.Services/SmartStore.Services.csproj b/src/Libraries/SmartStore.Services/SmartStore.Services.csproj index 7304770fc0..723ecf1212 100644 --- a/src/Libraries/SmartStore.Services/SmartStore.Services.csproj +++ b/src/Libraries/SmartStore.Services/SmartStore.Services.csproj @@ -1,5 +1,6 @@  - + + Debug AnyCPU @@ -97,8 +98,11 @@ ..\..\packages\LumenWorksCsvReader.4.0.0\lib\net461\LumenWorks.Framework.IO.dll - - ..\..\packages\MaxMind.GeoIP.2.1.17\lib\net35\MaxMind.GeoIP.dll + + ..\..\packages\MaxMind.Db.2.4.0\lib\net45\MaxMind.Db.dll + + + ..\..\packages\MaxMind.GeoIP2.3.0.0\lib\net45\MaxMind.GeoIP2.dll @@ -279,6 +283,7 @@ + @@ -421,7 +426,6 @@ - diff --git a/src/Libraries/SmartStore.Services/Stores/StoreService.cs b/src/Libraries/SmartStore.Services/Stores/StoreService.cs index 68a7280116..f8cde15ab0 100644 --- a/src/Libraries/SmartStore.Services/Stores/StoreService.cs +++ b/src/Libraries/SmartStore.Services/Stores/StoreService.cs @@ -18,17 +18,6 @@ public StoreService(IRepository storeRepository) _storeRepository = storeRepository; } - public virtual void DeleteStore(Store 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); - } - public virtual IList GetAllStores() { var query = _storeRepository.Table @@ -63,6 +52,17 @@ public virtual void UpdateStore(Store store) _storeRepository.Update(store); } + public virtual void DeleteStore(Store 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); + } + public virtual bool IsSingleStoreMode() { if (!_isSingleStoreMode.HasValue) diff --git a/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs b/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs index 8a92d0abe2..665fca2d30 100644 --- a/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs +++ b/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs @@ -32,15 +32,7 @@ public void OnAuthentication(AuthenticationContext filterContext) var eventPublisher = EngineContext.Current.Resolve(); var logger = EngineContext.Current.Resolve().CreateLogger(); - - // The very first request must set app state to 'fully initialized' - if (!EngineContext.Current.IsFullyInitialized) - { - EngineContext.Current.IsFullyInitialized = true; - eventPublisher.Publish(new AppStartedEvent { HttpContext = filterContext.HttpContext }); - } - - ITaskScheduler taskScheduler = EngineContext.Current.Resolve(); + var taskScheduler = EngineContext.Current.Resolve(); try { diff --git a/src/Libraries/SmartStore.Services/Tax/TaxService.cs b/src/Libraries/SmartStore.Services/Tax/TaxService.cs index bcf14821e3..6d5cb78344 100644 --- a/src/Libraries/SmartStore.Services/Tax/TaxService.cs +++ b/src/Libraries/SmartStore.Services/Tax/TaxService.cs @@ -140,15 +140,9 @@ protected virtual bool IsEuConsumer(Customer customer) return true; } - var country = address == null ? null : address.Country; - - if (country == null) - { - // No Country or BillingAddress set: try to resolve country from IP address - _geoCountryLookup.IsEuIpAddress(customer.LastIpAddress, out country); - } - - if (country == null || !country.SubjectToVat) + // No Country or BillingAddress set: try to resolve country from IP address + var subjectToVat = _geoCountryLookup.LookupCountry(customer.LastIpAddress)?.IsInEu == true; + if (!subjectToVat) { return false; } diff --git a/src/Libraries/SmartStore.Services/Topics/TopicService.cs b/src/Libraries/SmartStore.Services/Topics/TopicService.cs index eb8dfee3a0..07152bf79b 100644 --- a/src/Libraries/SmartStore.Services/Topics/TopicService.cs +++ b/src/Libraries/SmartStore.Services/Topics/TopicService.cs @@ -49,7 +49,7 @@ public virtual Topic GetTopicById(int topicId) if (topicId == 0) return null; - return _topicRepository.GetById(topicId); + return _topicRepository.GetByIdCached(topicId, "db.topic.id-" + topicId); } public virtual Topic GetTopicBySystemName(string systemName, int storeId = 0, bool checkPermission = true) @@ -115,7 +115,7 @@ from a in ca.DefaultIfEmpty() if (joinApplied) { - // Only distinct categories (group by ID) + // Only distinct topics (group by ID) query = from t in query group t by t.Id into tGroup orderby tGroup.Key diff --git a/src/Libraries/SmartStore.Services/app.config b/src/Libraries/SmartStore.Services/app.config index 6aa328e21b..08f9834796 100644 --- a/src/Libraries/SmartStore.Services/app.config +++ b/src/Libraries/SmartStore.Services/app.config @@ -39,6 +39,10 @@ + + + + diff --git a/src/Libraries/SmartStore.Services/packages.config b/src/Libraries/SmartStore.Services/packages.config index 497fe401d6..be5db3727d 100644 --- a/src/Libraries/SmartStore.Services/packages.config +++ b/src/Libraries/SmartStore.Services/packages.config @@ -8,10 +8,12 @@ - + + + diff --git a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayCheckoutController.cs b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayCheckoutController.cs index 30fcf453b6..35fbe06a06 100644 --- a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayCheckoutController.cs +++ b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayCheckoutController.cs @@ -1,25 +1,34 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Web; using System.Web.Mvc; using SmartStore.AmazonPay.Services; +using SmartStore.Core.Html; +using SmartStore.Core.Logging; using SmartStore.Services.Common; +using SmartStore.Services.Orders; +using SmartStore.Services.Payments; namespace SmartStore.AmazonPay.Controllers { - public class AmazonPayCheckoutController : AmazonPayControllerBase + public class AmazonPayCheckoutController : AmazonPayControllerBase { private readonly HttpContextBase _httpContext; private readonly IAmazonPayService _apiService; private readonly IGenericAttributeService _genericAttributeService; + private readonly IOrderProcessingService _orderProcessingService; - public AmazonPayCheckoutController( + public AmazonPayCheckoutController( HttpContextBase httpContext, IAmazonPayService apiService, - IGenericAttributeService genericAttributeService) + IGenericAttributeService genericAttributeService, + IOrderProcessingService orderProcessingService) { _httpContext = httpContext; _apiService = apiService; _genericAttributeService = genericAttributeService; + _orderProcessingService = orderProcessingService; } public ActionResult OrderReferenceCreated(string orderReferenceId/*, string accessToken*/) @@ -49,9 +58,9 @@ public ActionResult OrderReferenceCreated(string orderReferenceId/*, string acce error = error.Grow(T("Plugins.Payments.AmazonPay.MissingAddressConsentToken"), " "); } } - catch (Exception exception) + catch (Exception ex) { - error = exception.Message; + error = ex.Message; } return new JsonResult { Data = new { success = success, error = error } }; @@ -107,5 +116,97 @@ public ActionResult CheckoutCompleted() return new EmptyResult(); } - } + + #region Confirmation flow + + // Ajax. + public ActionResult ConfirmOrder(string formData) + { + string redirectUrl = null; + var messages = new List(); + var success = false; + + try + { + var store = Services.StoreContext.CurrentStore; + var customer = Services.WorkContext.CurrentCustomer; + var processPaymentRequest = (_httpContext.Session["OrderPaymentInfo"] as ProcessPaymentRequest) ?? new ProcessPaymentRequest(); + + processPaymentRequest.StoreId = store.Id; + processPaymentRequest.CustomerId = customer.Id; + processPaymentRequest.PaymentMethodSystemName = AmazonPayPlugin.SystemName; + + // We must check here if an order can be placed to avoid creating unauthorized Amazon payment objects. + var warnings = _orderProcessingService.GetOrderPlacementWarnings(processPaymentRequest); + + if (!warnings.Any()) + { + if (_orderProcessingService.IsMinimumOrderPlacementIntervalValid(customer, store)) + { + if (_apiService.ConfirmOrderReference()) + { + success = true; + + var state = _httpContext.GetAmazonPayState(Services.Localization); + state.FormData = formData.EmptyNull(); + } + else + { + _httpContext.Session["AmazonPayFailedPaymentReason"] = "PaymentMethodNotAllowed"; + + redirectUrl = Url.Action("PaymentMethod", "Checkout", new { area = "" }); + } + } + else + { + messages.Add(T("Checkout.MinOrderPlacementInterval")); + } + } + else + { + messages.AddRange(warnings.Select(x => HtmlUtils.ConvertPlainTextToHtml(x))); + } + } + catch (Exception ex) + { + messages.Add(ex.Message); + Logger.Error(ex); + } + + return Json(new { success, redirectUrl, messages }); + } + + public ActionResult ConfirmationResult(string authenticationStatus) + { + var state = _httpContext.GetAmazonPayState(Services.Localization); + state.SubmitForm = false; + + if (authenticationStatus.IsCaseInsensitiveEqual("Success")) + { + state.SubmitForm = true; + return RedirectToAction("Confirm", "Checkout", new { area = "" }); + } + else if (authenticationStatus.IsCaseInsensitiveEqual("Failure")) + { + // The buyer has exhausted their retry attempts and payment instrument selection on the Amazon Pay page. + // Review: redirect back to shopping cart (like AmazonRejected). + _httpContext.Session["AmazonPayFailedPaymentReason"] = "AuthenticationStatusFailure"; + + return RedirectToRoute("ShoppingCart"); + } + else + { + // authenticationStatus == "Abandoned": The buyer took action to close/cancel the MFA challenge. + // Review: redirect to checkout payment page (like InvalidPaymentMethod). + state.IsConfirmed = false; + state.FormData = null; + + _httpContext.Session["AmazonPayFailedPaymentReason"] = "AuthenticationStatusAbandoned"; + + return RedirectToAction("PaymentMethod", "Checkout", new { area = "" }); + } + } + + #endregion + } } \ 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 bce38ebbd0..669cc4c009 100644 --- a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayController.cs +++ b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayController.cs @@ -1,11 +1,15 @@ using System; using System.Collections.Generic; +using System.Net; +using System.Text; +using System.Web; using System.Web.Mvc; using SmartStore.AmazonPay.Models; using SmartStore.AmazonPay.Services; using SmartStore.ComponentModel; using SmartStore.Core.Domain.Customers; using SmartStore.Services.Authentication.External; +using SmartStore.Services.Customers; using SmartStore.Services.Payments; using SmartStore.Services.Tasks; using SmartStore.Web.Framework; @@ -17,17 +21,20 @@ namespace SmartStore.AmazonPay.Controllers { public class AmazonPayController : PaymentControllerBase { - private readonly IAmazonPayService _apiService; + private readonly HttpContextBase _httpContext; + private readonly IAmazonPayService _apiService; private readonly Lazy _scheduleTaskService; private readonly Lazy _openAuthenticationService; private readonly Lazy _externalAuthenticationSettings; public AmazonPayController( - IAmazonPayService apiService, + HttpContextBase httpContext, + IAmazonPayService apiService, Lazy scheduleTaskService, Lazy openAuthenticationService, Lazy externalAuthenticationSettings) { + _httpContext = httpContext; _apiService = apiService; _scheduleTaskService = scheduleTaskService; _openAuthenticationService = openAuthenticationService; @@ -148,9 +155,57 @@ public ActionResult IPNHandler() return Content("OK"); } - // Authentication + // For payment simulation in sandbox mode. Admin and sandbox only. + public ActionResult SetSellerNote(string paymentState) + { + var customer = Services.WorkContext.CurrentCustomer; + if (!customer.IsAdmin()) + { + return HttpNotFound(); + } + + var store = Services.StoreContext.CurrentStore; + var settings = Services.Settings.LoadSetting(store.Id); + + if (!settings.UseSandbox) + { + return new HttpStatusCodeResult(HttpStatusCode.BadRequest, "Endpoint is for testing purposes and only available in sandbox mode."); + } + + var state = _httpContext.GetAmazonPayState(Services.Localization); + var sellerNote = string.Empty; // Reset payment state. + + // See https://pay.amazon.com/de/developer/documentation/lpwa/201956480 + if (paymentState.IsCaseInsensitiveEqual("PaymentMethodNotAllowed")) + { + sellerNote = "{\"SandboxSimulation\":{\"Constraint\":\"PaymentMethodNotAllowed\"}}"; + } + else if (paymentState.IsCaseInsensitiveEqual("Success")) + { + sellerNote = "{\"SandboxSimulation\":{\"PaymentAuthenticationStatus\":{\"State\":\"Success\"}}}"; + } + else if (paymentState.IsCaseInsensitiveEqual("Failure")) + { + sellerNote = "{\"SandboxSimulation\":{\"PaymentAuthenticationStatus\":{\"State\":\"Failure\"}}}"; + } + else if (paymentState.IsCaseInsensitiveEqual("Abandoned")) + { + sellerNote = "{\"SandboxSimulation\":{\"PaymentAuthenticationStatus\":{\"State\":\"Abandoned\"}}}"; + } + + var result = _apiService.SetOrderReferenceDetails(null, settings, state, sellerNote, out var errorMesage); + + var sb = new StringBuilder(); + sb.Append(result ? "ok. " : "failure. "); + sb.AppendLine(sellerNote); + sb.AppendLine(errorMesage.EmptyNull()); + + return Content(sb.ToString()); + } - [ChildActionOnly] + #region Authentication + + [ChildActionOnly] public ActionResult AuthenticationPublicInfo() { var model = _apiService.CreateViewModel(AmazonPayRequestType.AuthenticationPublicInfo, TempData); @@ -197,5 +252,7 @@ public ActionResult AuthenticationButtonHandler() return new RedirectResult(Url.LogOn(returnUrl)); } } - } + + #endregion + } } diff --git a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayShoppingCartController.cs b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayShoppingCartController.cs index e90edbd309..2bebbbaf81 100644 --- a/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayShoppingCartController.cs +++ b/src/Plugins/SmartStore.AmazonPay/Controllers/AmazonPayShoppingCartController.cs @@ -3,9 +3,9 @@ namespace SmartStore.AmazonPay.Controllers { - public class AmazonPayShoppingCartController : AmazonPayControllerBase + public class AmazonPayShoppingCartController : AmazonPayControllerBase { - private readonly IAmazonPayService _apiService; + private readonly IAmazonPayService _apiService; public AmazonPayShoppingCartController(IAmazonPayService apiService) { @@ -27,6 +27,7 @@ public ActionResult ShoppingCart() return GetActionResult(model); } + return new EmptyResult(); } @@ -53,5 +54,5 @@ public ActionResult MiniShoppingCart(bool renderAmazonPayView) } return new EmptyResult(); } - } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Description.txt b/src/Plugins/SmartStore.AmazonPay/Description.txt index 76e97cc475..7449e587bb 100644 --- a/src/Plugins/SmartStore.AmazonPay/Description.txt +++ b/src/Plugins/SmartStore.AmazonPay/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Login and Pay with Amazon SystemName: SmartStore.AmazonPay Group: Payment -Version: 3.2.0 -MinAppVersion: 3.2.0 +Version: 3.2.2 +MinAppVersion: 3.2.2 Author: SmartStore AG DisplayOrder: 1 FileName: SmartStore.AmazonPay.dll diff --git a/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml b/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml index 3ec171cd79..baa01c2299 100644 --- a/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml @@ -45,13 +45,19 @@ Textvorschläge:
    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 Pay zurückgewiesen. Bitte kontaktieren Sie uns.

    ]]>
    - + + Beim Zahlungsvorgang ist ein Problem aufgetreten. Ihre Bestellung wurde nicht aufgegeben und Ihr Konto nicht belastet. + + + Mit dieser Zahlungsart ist ein Problem aufgetreten. Um Ihre Bestellung abzuschließen, wählen Sie bitte eine andere aus. + + Amazon Pay meldet @@ -111,7 +117,7 @@ Textvorschläge:
      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 diff --git a/src/Plugins/SmartStore.AmazonPay/Localization/resources.en-us.xml b/src/Plugins/SmartStore.AmazonPay/Localization/resources.en-us.xml index bc510fac8e..caaa7ebf43 100644 --- a/src/Plugins/SmartStore.AmazonPay/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.AmazonPay/Localization/resources.en-us.xml @@ -51,7 +51,13 @@ Text suggestions:
        Unfortunately Amazon Pay declined the payment for your order in our online shop. Please contact us.

        ]]> - + + There was a problem with your payment. Your order hasn't been placed, and you haven't been charged. + + + Something's wrong with your payment method. To place your order, try another. + + Amazon Pay reports @@ -111,7 +117,7 @@ Text suggestions:
          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 diff --git a/src/Plugins/SmartStore.AmazonPay/Models/AmazonPayViewModel.cs b/src/Plugins/SmartStore.AmazonPay/Models/AmazonPayViewModel.cs index d56a233de9..77c391b448 100644 --- a/src/Plugins/SmartStore.AmazonPay/Models/AmazonPayViewModel.cs +++ b/src/Plugins/SmartStore.AmazonPay/Models/AmazonPayViewModel.cs @@ -15,10 +15,7 @@ public AmazonPayViewModel() BillingAddress = new AddressModel(); } - public string SystemName - { - get { return AmazonPayPlugin.SystemName; } - } + public string SystemName => AmazonPayPlugin.SystemName; public string SellerId { get; set; } public string ClientId { get; set; } @@ -50,5 +47,10 @@ public string SystemName public string ShippingMethod { get; set; } public AddressModel BillingAddress { get; set; } - } + + // Confirmation flow. + public bool IsConfirmed { get; set; } + public string FormData { get; set; } + public bool SubmitForm { get; set; } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Plugin.cs b/src/Plugins/SmartStore.AmazonPay/Plugin.cs index 9006d725cf..9524984a8d 100644 --- a/src/Plugins/SmartStore.AmazonPay/Plugin.cs +++ b/src/Plugins/SmartStore.AmazonPay/Plugin.cs @@ -70,12 +70,6 @@ public override void Uninstall() base.Uninstall(); } - public override PreProcessPaymentResult PreProcessPayment(ProcessPaymentRequest processPaymentRequest) - { - var result = _apiService.PreProcessPayment(processPaymentRequest); - return result; - } - public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest processPaymentRequest) { var result = _apiService.ProcessPayment(processPaymentRequest); diff --git a/src/Plugins/SmartStore.AmazonPay/Scripts/jquery.deserialize.js b/src/Plugins/SmartStore.AmazonPay/Scripts/jquery.deserialize.js new file mode 100644 index 0000000000..13c00e2a0d --- /dev/null +++ b/src/Plugins/SmartStore.AmazonPay/Scripts/jquery.deserialize.js @@ -0,0 +1,276 @@ +/** + * Copyright (C) 2017 Kyle Florence + * + * 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 2 + * of the License, or (at your option) any later version. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * @website https://github.com/kflorence/jquery-deserialize/ + * @version 2.0.0-rc1 + * + * Dual licensed under the MIT and GPLv2 licenses. + */ +(function( factory ) { + if ( typeof module === "object" && module.exports ) { + // Node/CommonJS + module.exports = factory( require( "jquery" ) ); + + } else { + // Browser globals + factory( window.jQuery ); + } +}(function( $ ) { + + /** + * Updates a key/valueArray with the given property and value. Values will always be stored as arrays. + * + * @param prop The property to add the value to. + * @param value The value to add. + * @param obj The object to update. + * @returns {object} Updated object. + */ + function updateKeyValueArray( prop, value, obj ) { + var current = obj[ prop ]; + + if ( current === undefined ) { + obj[ prop ] = [ value ]; + + } else { + current.push( value ); + } + + return obj; + } + + /** + * Get all of the fields contained within the given elements by name. + * + * @param $elements jQuery object of elements. + * @param filter Custom filter to apply to the list of fields. + * @returns {object} All of the fields contained within the given elements, keyed by name. + */ + function getFieldsByName( $elements, filter ) { + var elementsByName = {}; + + // Extract fields from elements + var fields = $elements + .map(function convertFormToElements() { + return this.elements ? $.makeArray( this.elements ) : this; + }) + .filter( filter || ":input:not(:disabled)" ) + .get(); + + $.each( fields, function( index, field ) { + updateKeyValueArray( field.name, field, elementsByName ); + }); + + return elementsByName; + } + + /** + * Figure out the type of an element. Input type will be used first, falling back to nodeName. + * + * @param element DOM element to check type of. + * @returns {string} The element's type. + */ + function getElementType( element ) { + return ( element.type || element.nodeName ).toLowerCase(); + } + + /** + * Normalize the provided data into a key/valueArray store. + * + * @param data The data provided by the user to the plugin. + * @returns {object} The data normalized into a key/valueArray store. + */ + function normalizeData( data ) { + var normalized = {}; + var rPlus = /\+/g; + + // Convert data from .serializeObject() notation + if ( $.isPlainObject( data ) ) { + $.extend( normalized, data ); + + // Convert non-array values into an array + $.each( normalized, function( name, value ) { + if ( !$.isArray( value ) ) { + normalized[ name ] = [ value ]; + } + }); + + // Convert data from .serializeArray() notation + } else if ( $.isArray( data ) ) { + $.each( data, function( index, field ) { + updateKeyValueArray( field.name, field.value, normalized ); + }); + + // Convert data from .serialize() notation + } else if ( typeof data === "string" ) { + $.each( data.split( "&" ), function( index, field ) { + var current = field.split( "=" ); + var name = decodeURIComponent( current[ 0 ].replace( rPlus, "%20" ) ); + var value = decodeURIComponent( current[ 1 ].replace( rPlus, "%20" ) ); + updateKeyValueArray( name, value, normalized ); + }); + } + + return normalized; + } + + /** + * Map of property name -> element types. + * + * @type {object} + */ + var updateTypes = { + checked: [ + "radio", + "checkbox" + ], + selected: [ + "option", + "select-one", + "select-multiple" + ], + value: [ + "button", + "color", + "date", + "datetime", + "datetime-local", + "email", + "hidden", + "month", + "number", + "password", + "range", + "reset", + "search", + "submit", + "tel", + "text", + "textarea", + "time", + "url", + "week" + ] + }; + + /** + * Get the property to update on an element being updated. + * + * @param element The DOM element to get the property for. + * @returns The name of the property to update if element is supported, otherwise `undefined`. + */ + function getPropertyToUpdate( element ) { + var type = getElementType( element ); + var elementProperty = undefined; + + $.each( updateTypes, function( property, types ) { + if ( $.inArray( type, types ) > -1 ) { + elementProperty = property; + return false; + } + }); + + return elementProperty; + } + + /** + * Update the element based on the provided data. + * + * @param element The DOM element to update. + * @param elementIndex The index of this element in the list of elements with the same name. + * @param value The serialized element value. + * @param valueIndex The index of the value in the list of values for elements with the same name. + * @param callback A function to call if the value of an element was updated. + */ + function update( element, elementIndex, value, valueIndex, callback ) { + var property = getPropertyToUpdate( element ); + + // Handle value inputs + // If there are multiple value inputs with the same name, they will be populated by matching indexes. + if ( property == "value" && elementIndex == valueIndex ) { + element.value = value; + callback.call( element, value ); + + // Handle select menus, checkboxes and radio buttons + } else if ( property == "checked" || property == "selected" ) { + var fields = []; + + // Extract option fields from select menus + if ( element.options ) { + $.each( element.options, function( index, option ) { + fields.push( option ); + }); + + } else { + fields.push( element ); + } + + // #37: Remove selection from multiple select menus before deserialization + if ( element.multiple && valueIndex == 0 ) { + element.selectedIndex = -1; + } + + $.each( fields, function( index, field ) { + if ( field.value == value ) { + field[ property ] = true; + callback.call( field, value ); + } + }); + } + } + + /** + * Default plugin options. + * + * @type {object} + */ + var defaultOptions = { + change: $.noop, + complete: $.noop + }; + + /** + * The $.deserialize function. + * + * @param data The data to deserialize. + * @param options Additional options. + * @returns {jQuery} The jQuery object that was provided to the plugin. + */ + $.fn.deserialize = function( data, options ) { + + // Backwards compatible with old arguments: data, callback + if ( $.isFunction( options ) ) { + options = { complete: options }; + } + + options = $.extend( defaultOptions, options || {} ); + data = normalizeData( data ); + + var elementsByName = getFieldsByName( this, options.filter ); + + $.each( data, function( name, values ) { + $.each( elementsByName[ name ], function( elementIndex, element ) { + $.each( values, function( valueIndex, value ) { + update( element, elementIndex, value, valueIndex, options.change ); + }); + }); + }); + + options.complete.call( this ); + + return this; + }; +})); diff --git a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayService.cs b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayService.cs index 09e7e2b9a6..a24c0646f7 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayService.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayService.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Text; using System.Threading; -using System.Threading.Tasks; using System.Web; using System.Web.Mvc; using AmazonPay; @@ -22,18 +21,17 @@ using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; using SmartStore.Core.Domain.Shipping; +using SmartStore.Core.Domain.Stores; 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.Web; @@ -41,30 +39,27 @@ namespace SmartStore.AmazonPay.Services { - public partial class AmazonPayService : IAmazonPayService + public partial class AmazonPayService : IAmazonPayService { 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 ICustomerService _customerService; private readonly ICountryService _countryService; private readonly IStateProvinceService _stateProvinceService; - private readonly IAddressService _addressService; private readonly IOrderService _orderService; private readonly IOrderProcessingService _orderProcessingService; + private readonly IOrderTotalCalculationService _orderTotalCalculationService; - 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( HttpContextBase httpContext, @@ -72,45 +67,39 @@ public AmazonPayService( ICommonServices services, IPaymentService paymentService, IGenericAttributeService genericAttributeService, - IOrderTotalCalculationService orderTotalCalculationService, ICurrencyService currencyService, ICustomerService customerService, ICountryService countryService, IStateProvinceService stateProvinceService, - IAddressService addressService, IOrderService orderService, IOrderProcessingService orderProcessingService, - IPriceFormatter priceFormatter, + IOrderTotalCalculationService orderTotalCalculationService, IDateTimeHelper dateTimeHelper, IPluginFinder pluginFinder, Lazy authorizer, AddressSettings addressSettings, OrderSettings orderSettings, - CompanyInformationSettings companyInformationSettings, - Lazy externalAuthenticationSettings) + CompanyInformationSettings companyInformationSettings) { _httpContext = httpContext; _orderRepository = orderRepository; _services = services; _paymentService = paymentService; _genericAttributeService = genericAttributeService; - _orderTotalCalculationService = orderTotalCalculationService; _currencyService = currencyService; _customerService = customerService; _countryService = countryService; _stateProvinceService = stateProvinceService; - _addressService = addressService; _orderService = orderService; _orderProcessingService = orderProcessingService; + _orderTotalCalculationService = orderTotalCalculationService; - _priceFormatter = priceFormatter; _dateTimeHelper = dateTimeHelper; _pluginFinder = pluginFinder; _authorizer = authorizer; _addressSettings = addressSettings; _orderSettings = orderSettings; _companyInformationSettings = companyInformationSettings; - _externalAuthenticationSettings = externalAuthenticationSettings; T = NullLocalizer.Instance; Logger = NullLogger.Instance; @@ -352,7 +341,11 @@ public AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDic var state = _httpContext.GetAmazonPayState(_services.Localization); model.OrderReferenceId = state.OrderReferenceId; model.AddressConsentToken = state.AccessToken; - //model.IsOrderConfirmed = state.IsOrderConfirmed; + model.IsConfirmed = state.IsConfirmed; + model.FormData = state.FormData; + model.SubmitForm = state.SubmitForm; + + state.SubmitForm = false; if (type == AmazonPayRequestType.ShippingMethod || type == AmazonPayRequestType.PaymentMethod) { @@ -364,8 +357,6 @@ public AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDic } } - var currency = store.PrimaryStoreCurrency; - model.SellerId = settings.SellerId; model.ClientId = settings.ClientId; model.IsShippable = cart.RequiresShipping(); @@ -390,12 +381,18 @@ public AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDic 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"))); - } + var failureReason = _httpContext.Session["AmazonPayFailedPaymentReason"] as string; + if (failureReason.IsCaseInsensitiveEqual("AmazonRejected")) + { + model.Logout = true; + _services.Notifier.Error(new LocalizedString(T("Plugins.Payments.AmazonPay.AuthorizationSoftDeclineMessage"))); + } + else if (failureReason.IsCaseInsensitiveEqual("AuthenticationStatusFailure")) + { + model.Logout = true; + _services.Notifier.Error(new LocalizedString(T("Plugins.Payments.AmazonPay.AuthenticationStatusFailureMessage"))); + } + _httpContext.Session.SafeRemove("AmazonPayFailedPaymentReason"); } else if (type == AmazonPayRequestType.AuthenticationPublicInfo) @@ -488,24 +485,29 @@ public AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDic } else { - LogError(getOrderResponse); + LogError(getOrderRequest, getOrderResponse); } } } else if (type == AmazonPayRequestType.PaymentMethod) { - _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedPaymentMethod, AmazonPayPlugin.SystemName, store.Id); + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedPaymentMethod, AmazonPayPlugin.SystemName, store.Id); + + var paymentFailure = _httpContext.Session["AmazonPayFailedPaymentReason"] as string; - var failedPaymentReason = _httpContext.Session["AmazonPayFailedPaymentReason"] as string; - if (failedPaymentReason.IsCaseInsensitiveEqual("InvalidPaymentMethod")) + if (paymentFailure.IsCaseInsensitiveEqual("InvalidPaymentMethod")) { _services.Notifier.Warning(new LocalizedString(T("Payment.PayingFailed"))); } - else if (failedPaymentReason.IsCaseInsensitiveEqual("PaymentMethodNotAllowed")) + else if (paymentFailure.IsCaseInsensitiveEqual("PaymentMethodNotAllowed")) { _services.Notifier.Warning(new LocalizedString(T("Plugins.Payments.AmazonPay.AuthorizationSoftDeclineMessage"))); } - else + else if (paymentFailure.IsCaseInsensitiveEqual("AuthenticationStatusAbandoned")) + { + _services.Notifier.Warning(new LocalizedString(T("Plugins.Payments.AmazonPay.AuthenticationStatusAbandonedMessage"))); + } + else { decimal? shoppingCartTotalBase = _orderTotalCalculationService.GetShoppingCartTotal(cart); if (shoppingCartTotalBase.HasValue) @@ -522,7 +524,7 @@ public AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDic var setOrderResponse = client.SetOrderReferenceDetails(setOrderRequest); if (!setOrderResponse.GetSuccess()) { - LogError(setOrderResponse); + LogError(setOrderRequest, setOrderResponse); } } } @@ -531,7 +533,7 @@ public AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDic var paymentRequest = _httpContext.Session["OrderPaymentInfo"] as ProcessPaymentRequest; if (paymentRequest == null) { - _httpContext.Session["OrderPaymentInfo"] = new ProcessPaymentRequest(); + _httpContext.Session["OrderPaymentInfo"] = new ProcessPaymentRequest { OrderGuid = Guid.NewGuid() }; } } else if (type == AmazonPayRequestType.OrderReviewData) @@ -539,8 +541,10 @@ public AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDic if (model.IsShippable) { var shippingOption = customer.GetAttribute(SystemCustomerAttributeNames.SelectedShippingOption, store.Id); - if (shippingOption != null) - model.ShippingMethod = shippingOption.Name; + if (shippingOption != null) + { + model.ShippingMethod = shippingOption.Name; + } } if (customer.BillingAddress != null) @@ -549,10 +553,10 @@ public AmazonPayViewModel CreateViewModel(AmazonPayRequestType type, TempDataDic } } } - catch (Exception exception) + catch (Exception ex) { - Logger.Error(exception); - _services.Notifier.Error(new LocalizedString(exception.Message)); + Logger.Error(ex); + _services.Notifier.Error(new LocalizedString(ex.Message)); } return model; @@ -629,7 +633,7 @@ private void ProcessCaptureResult(Client client, AmazonPaySettings settings, Ord } else { - LogError(authDetailsResponse); + LogError(authDetailsRequest, authDetailsResponse); } } @@ -791,7 +795,7 @@ public void CloseOrderReference(AmazonPaySettings settings, Order order) } else { - LogError(closeResponse, true); + LogError(closeRequest, closeResponse, true); } } } @@ -836,9 +840,9 @@ public void AddCustomerOrderNoteLoop(AmazonPayActionState state) Thread.Sleep(sleepMillSec); } } - catch (Exception exception) + catch (Exception ex) { - Logger.Error(exception); + Logger.Error(ex); } } @@ -919,101 +923,124 @@ public void GetBillingAddress() } else { - LogError(getOrderResponse); + LogError(getOrderRequest, getOrderResponse); } } - public PreProcessPaymentResult PreProcessPayment(ProcessPaymentRequest request) - { - // Fulfill the Amazon checkout. - var result = new PreProcessPaymentResult(); - - try - { - var store = _services.StoreService.GetStoreById(request.StoreId); - - if (!IsPaymentMethodActive(store.Id, true)) - { - result.AddError(T("Plugins.Payments.AmazonPay.PaymentMethodNotActive", store.Name)); - return result; - } - - 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 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(); - - foreach (var id in ids) - { - var idx = ids.IndexOf(id); - if (idx < descriptions.Count) - { - result.Errors.Add($"{descriptions[idx]} ({id})"); - } - - if (id.IsCaseInsensitiveEqual("PaymentMethodNotAllowed")) - { - // Must be redirected to checkout payment page. - _httpContext.Session["AmazonPayFailedPaymentReason"] = id; - - var urlHelper = new UrlHelper(_httpContext.Request.RequestContext); - _httpContext.Response.Redirect(urlHelper.Action("PaymentMethod", "Checkout", new { area = "" })); - } - } - } - } - else - { - var message = LogError(setOrderResponse); - result.AddError(message); - } - - if (!result.Success) - { - return result; - } - } - } - catch (Exception exception) - { - Logger.Error(exception); - result.AddError(exception.Message); - } - - return result; - } + public bool SetOrderReferenceDetails( + Client client, + AmazonPaySettings settings, + AmazonPayCheckoutState state, + string sellerNote, + out string errorMessage) + { + errorMessage = null; + + if (client == null) + { + client = CreateClient(settings); + } + + var store = _services.StoreContext.CurrentStore; + var customer = _services.WorkContext.CurrentCustomer; + var cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, store.Id); + decimal? cartTotal = _orderTotalCalculationService.GetShoppingCartTotal(cart); + + var setOrderRequest = new SetOrderReferenceDetailsRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonOrderReferenceId(state.OrderReferenceId) + .WithPlatformId(PlatformId) + .WithAmount(cartTotal) + .WithCurrencyCode(ConvertCurrency(store.PrimaryStoreCurrency.CurrencyCode)) + .WithStoreName(store.Name); + + var paymentRequest = _httpContext.Session["OrderPaymentInfo"] as ProcessPaymentRequest; + if (paymentRequest != null) + { + setOrderRequest = setOrderRequest.WithSellerOrderId(paymentRequest.OrderGuid.ToString()); + } + + if (sellerNote != null) + { + setOrderRequest = setOrderRequest.WithSellerNote(sellerNote); + } + + var setOrderResponse = client.SetOrderReferenceDetails(setOrderRequest); + if (!setOrderResponse.GetSuccess()) + { + errorMessage = LogError(setOrderRequest, setOrderResponse); + return false; + } + + if (setOrderResponse.GetHasConstraint()) + { + var ids = setOrderResponse.GetConstraintIdList(); + var descriptions = setOrderResponse.GetDescriptionList(); + + foreach (var id in ids) + { + var idx = ids.IndexOf(id); + if (idx < descriptions.Count) + { + errorMessage = $"{descriptions[idx]} ({id})"; + Logger.Error(errorMessage); + } + + if (id.IsCaseInsensitiveEqual("PaymentMethodNotAllowed")) + { + // Must be redirected to checkout payment page. + return false; + } + } + } + + return true; + } + + public bool ConfirmOrderReference() + { + try + { + var store = _services.StoreContext.CurrentStore; + var settings = _services.Settings.LoadSetting(store.Id); + var state = _httpContext.GetAmazonPayState(_services.Localization); + var client = CreateClient(settings); + + // Only call SetOrderReferenceDetailsRequest if it's in draft state, otherwise it returns InvalidOrderReferenceStatus error. + if (!state.DetailsSet && !SetOrderReferenceDetails(client, settings, state, null, out var _)) + { + return false; + } + + state.DetailsSet = true; + + var confirmationResultUrl = _services.WebHelper.GetStoreLocation() + "Plugins/SmartStore.AmazonPay/AmazonPayCheckout/ConfirmationResult"; + + // Confirm order. This already generates the payment object at Amazon. + // Can be called multiple times, for example to move the order reference object from suspended to open state. + var confirmRequest = new ConfirmOrderReferenceRequest() + .WithMerchantId(settings.SellerId) + .WithAmazonOrderReferenceId(state.OrderReferenceId) + .WithSuccessUrl(confirmationResultUrl); + + var confirmResponse = client.ConfirmOrderReference(confirmRequest); + if (confirmResponse.GetSuccess()) + { + state.IsConfirmed = true; + return true; + } + else + { + LogError(confirmRequest, confirmResponse); + return false; + } + } + catch (Exception ex) + { + Logger.Error(ex); + return false; + } + } public ProcessPaymentResult ProcessPayment(ProcessPaymentRequest request) { @@ -1036,30 +1063,24 @@ public ProcessPaymentResult ProcessPayment(ProcessPaymentRequest request) var captureNow = settings.TransactionType == AmazonPayTransactionType.AuthorizeAndCapture; var state = _httpContext.GetAmazonPayState(_services.Localization); var client = CreateClient(settings); - AuthorizeResponse authResponse = null; + AuthorizeRequest authRequest = null; + AuthorizeResponse authResponse = null; informCustomerAboutErrors = settings.InformCustomerAboutErrors; informCustomerAddErrors = settings.InformCustomerAddErrors; - // Confirm order. This already generates the payment object at Amazon. - var confirmRequest = new ConfirmOrderReferenceRequest() - .WithMerchantId(settings.SellerId) - .WithAmazonOrderReferenceId(state.OrderReferenceId); - - client.ConfirmOrderReference(confirmRequest); - // Authorize. if (settings.AuthorizeMethod == AmazonPayAuthorizeMethod.Omnichronous) { // First try synchronously. - authResponse = AuthorizePayment(settings, state, store, request, client, true); + authResponse = AuthorizePayment(settings, state, store, request, client, true, out authRequest); 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); + authResponse = AuthorizePayment(settings, state, store, request, client, false, out authRequest); } else { @@ -1069,7 +1090,7 @@ public ProcessPaymentResult ProcessPayment(ProcessPaymentRequest request) else { isSynchronous = settings.AuthorizeMethod == AmazonPayAuthorizeMethod.Synchronous; - authResponse = AuthorizePayment(settings, state, store, request, client, isSynchronous); + authResponse = AuthorizePayment(settings, state, store, request, client, isSynchronous, out authRequest); } // Process authorization response. @@ -1127,6 +1148,10 @@ public ProcessPaymentResult ProcessPayment(ProcessPaymentRequest request) // Must be redirected to checkout payment page. _httpContext.Session["AmazonPayFailedPaymentReason"] = reason; + // Review: confirmation required to get order reference object from suspended to open state again. + state.IsConfirmed = false; + state.FormData = null; + var urlHelper = new UrlHelper(_httpContext.Request.RequestContext); _httpContext.Response.Redirect(urlHelper.Action("PaymentMethod", "Checkout", new { area = "" })); } @@ -1134,13 +1159,13 @@ public ProcessPaymentResult ProcessPayment(ProcessPaymentRequest request) } else { - error = LogError(authResponse); + error = LogError(authRequest, authResponse); } } - catch (Exception exception) + catch (Exception ex) { - Logger.Error(exception); - error = exception.Message; + Logger.Error(ex); + error = ex.Message; } if (error.HasValue()) @@ -1224,7 +1249,7 @@ public void PostProcessPayment(PostProcessPaymentRequest request) } else { - LogError(closeResponse, true); + LogError(closeRequest, closeResponse, true); } } @@ -1271,7 +1296,7 @@ public CapturePaymentResult Capture(CapturePaymentRequest request) } else { - var message = LogError(captureResponse); + var message = LogError(captureRequest, captureResponse); result.AddError(message); } } @@ -1324,7 +1349,7 @@ public RefundPaymentResult Refund(RefundPaymentRequest request) } else { - var message = LogError(refundResponse); + var message = LogError(refundRequest, refundResponse); result.AddError(message); } } @@ -1379,7 +1404,7 @@ public VoidPaymentResult Void(VoidPaymentRequest request) } else { - var message = LogError(cancelResponse); + var message = LogError(cancelRequest, cancelResponse); result.AddError(message); } } @@ -1550,7 +1575,7 @@ orderby x.Id descending } else { - LogError(authDetailsResponse); + LogError(authDetailsRequest, authDetailsResponse); } } @@ -1578,7 +1603,7 @@ orderby x.Id descending } else { - LogError(captureDetailsResponse); + LogError(captureDetailsRequest, captureDetailsResponse); } } } diff --git a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayServiceHelper.cs b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayServiceHelper.cs index 11e22c003b..baf21a850a 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayServiceHelper.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayServiceHelper.cs @@ -441,11 +441,18 @@ private string GetRandomId(string prefix) return str.Truncate(32); } - private string LogError(IResponse response, bool isWarning = false) + private string LogError(DelegateRequest request, IResponse response, bool isWarning = false) { - var message = $"{response.GetErrorMessage().NaIfEmpty()} ({response.GetErrorCode().NaIfEmpty()})"; + var message = string.Empty; - Logger.Log(isWarning ? LogLevel.Warning : LogLevel.Error, new Exception(response.GetJson()), message, null); + if (response != null) + { + var requestMethod = request != null ? request.GetAction() : null; + + message = $"{requestMethod.NaIfEmpty()} --> {response.GetErrorCode().NaIfEmpty()}. {response.GetErrorMessage().NaIfEmpty()}"; + + Logger.Log(isWarning ? LogLevel.Warning : LogLevel.Error, new Exception(response.GetJson()), message, null); + } return message; } @@ -563,9 +570,10 @@ private AuthorizeResponse AuthorizePayment( Store store, ProcessPaymentRequest request, Client client, - bool synchronously) + bool synchronously, + out AuthorizeRequest authRequest) { - var authRequest = new AuthorizeRequest() + authRequest = new AuthorizeRequest() .WithMerchantId(settings.SellerId) .WithAmazonOrderReferenceId(state.OrderReferenceId) .WithAuthorizationReferenceId(GetRandomId("Authorize")) @@ -578,19 +586,16 @@ private AuthorizeResponse AuthorizePayment( 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); + // 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"); + // authRequest = authRequest.WithSellerAuthorizationNote(authNote); + //} + + var authResponse = client.Authorize(authRequest); return authResponse; } } diff --git a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayUtilities.cs b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayUtilities.cs index 7d54a8456b..8836575653 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayUtilities.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayUtilities.cs @@ -10,7 +10,13 @@ public class AmazonPayCheckoutState { public string OrderReferenceId { get; set; } public string AccessToken { get; set; } - } + + // Confirmation flow. + public bool DetailsSet { get; set; } + public bool IsConfirmed { get; set; } + public string FormData { get; set; } + public bool SubmitForm { get; set; } + } public class AmazonPayActionState diff --git a/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayService.cs b/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayService.cs index 6f3f727d48..f4adc780e2 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayService.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayService.cs @@ -1,5 +1,6 @@ using System.Web; using System.Web.Mvc; +using AmazonPay; using SmartStore.AmazonPay.Models; using SmartStore.Core.Domain.Orders; using SmartStore.Services.Authentication.External; @@ -7,7 +8,7 @@ namespace SmartStore.AmazonPay.Services { - public partial interface IAmazonPayService : IExternalProviderAuthorizer + public partial interface IAmazonPayService : IExternalProviderAuthorizer { void SetupConfiguration(ConfigurationModel model); @@ -19,7 +20,14 @@ public partial interface IAmazonPayService : IExternalProviderAuthorizer void GetBillingAddress(); - PreProcessPaymentResult PreProcessPayment(ProcessPaymentRequest request); + bool SetOrderReferenceDetails( + Client client, + AmazonPaySettings settings, + AmazonPayCheckoutState state, + string sellerNote, + out string errorMessage); + + bool ConfirmOrderReference(); ProcessPaymentResult ProcessPayment(ProcessPaymentRequest request); diff --git a/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayExtensions.cs b/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayExtensions.cs index f77931cc01..1eb4378dfa 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayExtensions.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/Internal/AmazonPayExtensions.cs @@ -116,7 +116,7 @@ internal static AmazonPayCheckoutState GetAmazonPayState(this HttpContextBase ht return state; } - internal static Order GetOrderByAmazonId(this IRepository orderRepository, string amazonId) + internal static Order GetOrderByAmazonId(this IRepository orderRepository, string amazonId) { // S02-9777218-8608106 OrderReferenceId // S02-9777218-8608106-A088344 Auth ID diff --git a/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj b/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj index 6105db6395..b2db65457e 100644 --- a/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj +++ b/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj @@ -1,6 +1,6 @@  - - + + @@ -182,6 +182,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/src/Plugins/SmartStore.AmazonPay/Views/AmazonPayShoppingCart/ScriptingReadWidgets.cshtml b/src/Plugins/SmartStore.AmazonPay/Views/AmazonPayShoppingCart/ScriptingReadWidgets.cshtml index 944e80302a..86fe47e92c 100644 --- a/src/Plugins/SmartStore.AmazonPay/Views/AmazonPayShoppingCart/ScriptingReadWidgets.cshtml +++ b/src/Plugins/SmartStore.AmazonPay/Views/AmazonPayShoppingCart/ScriptingReadWidgets.cshtml @@ -1,9 +1,11 @@ -@model SmartStore.AmazonPay.Models.AmazonPayViewModel +@using SmartStore.AmazonPay +@model SmartStore.AmazonPay.Models.AmazonPayViewModel @{ Html.AddCssFileParts(true, Url.Content("~/Plugins/SmartStore.AmazonPay/Content/SmartStore.AmazonPay.css")); + Html.AddScriptParts(true, "~/Plugins/SmartStore.AmazonPay/Scripts/jquery.deserialize.js"); } - diff --git a/src/Plugins/SmartStore.Clickatell/Description.txt b/src/Plugins/SmartStore.Clickatell/Description.txt index ca5fa47c41..003d6a572e 100644 --- a/src/Plugins/SmartStore.Clickatell/Description.txt +++ b/src/Plugins/SmartStore.Clickatell/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Clickatell SMS Provider SystemName: SmartStore.Clickatell Group: Mobile -Version: 3.2.0 -MinAppVersion: 3.2.0 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 1 FileName: SmartStore.Clickatell.dll ResourceRootKey: Plugins.Sms.Clickatell \ No newline at end of file diff --git a/src/Plugins/SmartStore.Clickatell/SmartStore.Clickatell.csproj b/src/Plugins/SmartStore.Clickatell/SmartStore.Clickatell.csproj index abcbcb82e0..decee9a5f7 100644 --- a/src/Plugins/SmartStore.Clickatell/SmartStore.Clickatell.csproj +++ b/src/Plugins/SmartStore.Clickatell/SmartStore.Clickatell.csproj @@ -1,5 +1,6 @@  - + + diff --git a/src/Plugins/SmartStore.DevTools/Blocks/SampleBlock.cs b/src/Plugins/SmartStore.DevTools/Blocks/SampleBlock.cs new file mode 100644 index 0000000000..a089af9c47 --- /dev/null +++ b/src/Plugins/SmartStore.DevTools/Blocks/SampleBlock.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; +using System.Web.Mvc; +using FluentValidation; +using FluentValidation.Attributes; +using Newtonsoft.Json; +using SmartStore.Services.Cms.Blocks; +using SmartStore.Web.Framework; + +namespace SmartStore.DevTools.Blocks +{ + /// + /// The block handler is the controller which is responsible for loading, instantiating, storing and rendering block types. + /// The 'BlockHandlerBase' abstract class already implements all relevant parts. + /// You can, however, overwrite any method to fulfill your custom needs. + /// + [Block("sample", Icon = "far fa-terminal", FriendlyName = "Sample", DisplayOrder = 50, IsInternal = true)] // REMOVE IsInternal = true to display the block in the page builder + public class SampleBlockHandler : BlockHandlerBase + { + public override SampleBlock Load(IBlockEntity entity, StoryViewMode viewMode) + { + var block = base.Load(entity, viewMode); + + // By default 'BlockHandlerBase' stores block instance data as JSON in the 'Model' field of the 'PageStoryBlock' table. + // You can, however, store data anywhere you like and override loading behaviour in this method. + + if (viewMode == StoryViewMode.Edit) + { + // You can prepare your model especially for the edit mode of the block, + // e.g. add some SelectListItems for dropdownmenus which you will only need in the edit mode of the block + block.MyProperties.Add(new SelectListItem { Text = "Item1", Value = "1" }); + } + else if (viewMode == StoryViewMode.GridEdit) + { + // Manipulate properties especially for the grid edit mode e.g. turn of any animation which could distract the user from editing the grid. + //block.Autoplay = false; + } + + return block; + } + + public override void Save(SampleBlock block, IBlockEntity entity) + { + // By default 'BlockHandlerBase' stores block instance data as JSON in the 'Model' field of the 'PageStoryBlock' table. + // You can, however, store data anywhere you like and override persistance behaviour in this method. + + base.Save(block, entity); + } + + /// + /// By default block templates (Edit & Public) will be searched in '{Area}\Views\Story\BlockTemplates\{BlockSystemName}' or '{Area}\Views\Shared\BlockTemplates\{BlockSystemName}', + /// while {Area} represents your plugin folder. + /// The public action can address a deviating route by overwriting RenderCore & GetRoute. + /// You can override this behaviour by e.g. calling a child action in your plugin controller instead of directly rendering a view. + /// For this to take effect you have to override both methods 'RenderCore()' and 'GetRoute()' + /// + //protected override void RenderCore(IBlockContainer element, IEnumerable templates, HtmlHelper htmlHelper, TextWriter textWriter) + //{ + // if (templates.First() == "Edit") + // { + // base.RenderCore(element, templates, htmlHelper, textWriter); + // } + // else + // { + // base.RenderByChildAction(element, templates, htmlHelper, textWriter); + // } + //} + + //protected override RouteInfo GetRoute(IBlockContainer element, string template) + //{ + // var block = (SampleBlock)element.Block; + // return new RouteInfo("SampleBlock", "DevTools", new + // { + // block = element.Block, + // area = "SmartStore.DevTools" + // }); + //} + } + + /// + /// Any type that implements the 'IBlock' interface acts as a model. + /// + [Validator(typeof(SampleBlockValidator))] + public class SampleBlock : IBlock + { + [SmartResourceDisplayName("Plugins.SmartStore.DevTools.Block.Property")] + public string Property { get; set; } + + // By default a block instance will be converted to JSON and stored in the 'Model' field of the 'PageStoryBlock' table. + // If your block type contains some special properties - e.g. volatile data for the edit mode - and you don't want them to be persisted, add the [JsonIgnore] attribute to your property. + [JsonIgnore] + public IList MyProperties { get; set; } + } + + /// + /// This is the validator which is used to validate the user input while editing your block + /// + public partial class SampleBlockValidator : AbstractValidator + { + public SampleBlockValidator() + { + RuleFor(x => x.Property).NotEmpty(); + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs b/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs index 98c3b165e4..28827a7307 100644 --- a/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs +++ b/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs @@ -1,6 +1,8 @@ using System.Web.Mvc; +using SmartStore.DevTools.Blocks; using SmartStore.DevTools.Models; using SmartStore.Services; +using SmartStore.Services.Cms.Blocks; using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.Settings; @@ -56,6 +58,13 @@ public ActionResult WidgetZone(string widgetZone) return new EmptyResult(); } + [ChildActionOnly] + public ActionResult SampleBlock(SampleBlock block) + { + // Do something here with your block instance and return a result that should be rendered by the Page Builder. + return View(block); + } + [AdminAuthorize, AdminThemed] public ActionResult BackendExtension() { diff --git a/src/Plugins/SmartStore.DevTools/Description.txt b/src/Plugins/SmartStore.DevTools/Description.txt index 8d94078075..54399e9fcf 100644 --- a/src/Plugins/SmartStore.DevTools/Description.txt +++ b/src/Plugins/SmartStore.DevTools/Description.txt @@ -1,8 +1,8 @@ FriendlyName: SmartStore.NET Developer Tools (MiniProfiler and other goodies) SystemName: SmartStore.DevTools Group: Developer -Version: 3.2.0 -MinAppVersion: 3.2.0 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 1 FileName: SmartStore.DevTools.dll ResourceRootKey: Plugins.Developer.DevTools \ 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 3f2db6a143..025ef8e5bf 100644 --- a/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj +++ b/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj @@ -1,5 +1,6 @@  - + + @@ -92,6 +93,14 @@ ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll False + + ..\..\packages\FluentValidation.7.4.0\lib\net45\FluentValidation.dll + False + + + ..\..\packages\FluentValidation.Mvc5.7.4.0\lib\net45\FluentValidation.Mvc.dll + False + ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll False @@ -105,6 +114,10 @@ ..\..\packages\MiniProfiler.EF6.3.0.11\lib\net40\MiniProfiler.EntityFramework6.dll True + + ..\..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll + False + @@ -150,6 +163,7 @@ Properties\AssemblyVersionInfo.cs + @@ -238,6 +252,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + diff --git a/src/Plugins/SmartStore.DevTools/Views/Story/BlockTemplates/Sample/Edit.cshtml b/src/Plugins/SmartStore.DevTools/Views/Story/BlockTemplates/Sample/Edit.cshtml new file mode 100644 index 0000000000..14bf178d59 --- /dev/null +++ b/src/Plugins/SmartStore.DevTools/Views/Story/BlockTemplates/Sample/Edit.cshtml @@ -0,0 +1,15 @@ +@model SampleBlock + +@using SmartStore.DevTools.Blocks + +
          +
          +
          + @Html.SmartLabelFor(model => model.Property) +
          +
          + @Html.EditorFor(model => model.Property) + @Html.ValidationMessageFor(model => model.Property) +
          +
          +
          \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/Views/Story/BlockTemplates/Sample/Public.cshtml b/src/Plugins/SmartStore.DevTools/Views/Story/BlockTemplates/Sample/Public.cshtml new file mode 100644 index 0000000000..0ff1a80f66 --- /dev/null +++ b/src/Plugins/SmartStore.DevTools/Views/Story/BlockTemplates/Sample/Public.cshtml @@ -0,0 +1,14 @@ +@model SampleBlock + +@using SmartStore.DevTools.Blocks + +@{ + if (!Model.Property.HasValue()) + { + return; + } +} + +
          + @Model.Property +
          \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/packages.config b/src/Plugins/SmartStore.DevTools/packages.config index 355e10d46a..d040144b1c 100644 --- a/src/Plugins/SmartStore.DevTools/packages.config +++ b/src/Plugins/SmartStore.DevTools/packages.config @@ -3,10 +3,13 @@ + + + \ No newline at end of file diff --git a/src/Plugins/SmartStore.DiscountRules/Description.txt b/src/Plugins/SmartStore.DiscountRules/Description.txt index fb6db2c435..b7b46e9263 100644 --- a/src/Plugins/SmartStore.DiscountRules/Description.txt +++ b/src/Plugins/SmartStore.DiscountRules/Description.txt @@ -2,8 +2,8 @@ Description: Contains common discount requirement rule providers like "Billing country is", "Customer role is", "Had spent amount" etc. Group: Marketing SystemName: SmartStore.DiscountRules -Version: 3.2.0 -MinAppVersion: 3.2.0 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 0 FileName: SmartStore.DiscountRules.dll ResourceRootKey: Plugins.SmartStore.DiscountRules diff --git a/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj b/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj index a91c5b48ac..97356664ed 100644 --- a/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj +++ b/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj @@ -1,6 +1,6 @@  - - + + diff --git a/src/Plugins/SmartStore.FacebookAuth/Description.txt b/src/Plugins/SmartStore.FacebookAuth/Description.txt index 71a6235e56..447d4bf86d 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Description.txt +++ b/src/Plugins/SmartStore.FacebookAuth/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Facebook Login SystemName: SmartStore.FacebookAuth Group: Security -Version: 3.2.0 -MinAppVersion: 3.2.0 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 5 FileName: SmartStore.FacebookAuth.dll ResourceRootKey: Plugins.ExternalAuth.Facebook \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj b/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj index 3c3eac8964..832cc36b51 100644 --- a/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj +++ b/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj @@ -1,5 +1,6 @@  - + + diff --git a/src/Plugins/SmartStore.GoogleAnalytics/Description.txt b/src/Plugins/SmartStore.GoogleAnalytics/Description.txt index 0ba7f629bb..ac4936f475 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/Description.txt +++ b/src/Plugins/SmartStore.GoogleAnalytics/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Google Analytics SystemName: SmartStore.GoogleAnalytics Group: Analytics -Version: 3.2.0 -MinAppVersion: 3.2.0 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 1 FileName: SmartStore.GoogleAnalytics.dll ResourceRootKey: Plugins.Widgets.GoogleAnalytics diff --git a/src/Plugins/SmartStore.GoogleAnalytics/SmartStore.GoogleAnalytics.csproj b/src/Plugins/SmartStore.GoogleAnalytics/SmartStore.GoogleAnalytics.csproj index 1927f9a6bf..b48e5d3f5d 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/SmartStore.GoogleAnalytics.csproj +++ b/src/Plugins/SmartStore.GoogleAnalytics/SmartStore.GoogleAnalytics.csproj @@ -1,5 +1,6 @@  - + + diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt b/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt index f39c7ff9c2..3b87cd8d04 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Google Merchant Center (GMC) feed SystemName: SmartStore.GoogleMerchantCenter Group: Marketing -Version: 3.2.0 -MinAppVersion: 3.2.0 +Version: 3.2.2 +MinAppVersion: 3.2.2 Author: SmartStore AG DisplayOrder: 1 FileName: SmartStore.GoogleMerchantCenter.dll diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs index 1f57fcf317..c2e73c4fc4 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs @@ -480,19 +480,19 @@ protected override void Export(ExportExecuteContext context) } var customLabel0 = GetAttributeValue(attributeValues, "custom_label_0", languageId, gmc?.CustomLabel0, null); - WriteString(writer, "custom_label_0", gmc.CustomLabel0.NullEmpty()); + WriteString(writer, "custom_label_0", gmc?.CustomLabel0.NullEmpty()); var customLabel1 = GetAttributeValue(attributeValues, "custom_label_1", languageId, gmc?.CustomLabel1, null); - WriteString(writer, "custom_label_1", gmc.CustomLabel1.NullEmpty()); + WriteString(writer, "custom_label_1", gmc?.CustomLabel1.NullEmpty()); var customLabel2 = GetAttributeValue(attributeValues, "custom_label_2", languageId, gmc?.CustomLabel2, null); - WriteString(writer, "custom_label_2", gmc.CustomLabel2.NullEmpty()); + WriteString(writer, "custom_label_2", gmc?.CustomLabel2.NullEmpty()); var customLabel3 = GetAttributeValue(attributeValues, "custom_label_3", languageId, gmc?.CustomLabel3, null); - WriteString(writer, "custom_label_3", gmc.CustomLabel3.NullEmpty()); + WriteString(writer, "custom_label_3", gmc?.CustomLabel3.NullEmpty()); var customLabel4 = GetAttributeValue(attributeValues, "custom_label_4", languageId, gmc?.CustomLabel4, null); - WriteString(writer, "custom_label_4", gmc.CustomLabel4.NullEmpty()); + WriteString(writer, "custom_label_4", gmc?.CustomLabel4.NullEmpty()); ++context.RecordsSucceeded; } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj b/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj index cf87aa8f56..0a93c5cd18 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj @@ -1,5 +1,6 @@  - + + diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml index 7f047f12de..d85fa485ea 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml @@ -66,13 +66,13 @@ })
          - + @Html.SmartLabel(string.Empty, string.Empty)
          - + @Html.SmartLabel(string.Empty, string.Empty) + +
          +
          +
          + @Html.Raw(T("Plugins.Payments.PayPalInstalments.AdminInstruction")) + +
          +
          +
          + + PayPal + +
          +
          + +@{ Html.RenderAction("StoreScopeConfiguration", "Setting", new { area = "Admin" }); } + +@Html.ValidationSummary(false) + +@using (Html.BeginForm()) +{ + @Html.HiddenFor(x => x.TransactMode) + + @Html.Partial("ApiConfiguration") + + + + + + + + + + + + + + + + + + + + + + + @* + + + + + + + + + + + + + *@ + + + + + + + + + + + + + + + + + + + + +
          +
          +
          + @T("Plugins.SmartStore.PayPal.Promotion") +
          +
          +
          + @Html.SmartLabelFor(x => x.ProductPagePromotion) + + @Html.SettingEditorFor(x => x.ProductPagePromotion, + @Html.DropDownListFor(x => x.ProductPagePromotion, Model.ProductPagePromotions, T("Common.Unspecified"))) + @Html.ValidationMessageFor(x => x.ProductPagePromotion) +
          + @Html.SmartLabelFor(x => x.CartPagePromotion) + + @Html.SettingEditorFor(x => x.CartPagePromotion, + @Html.DropDownListFor(x => x.CartPagePromotion, Model.CartPagePromotions, T("Common.Unspecified"))) + @Html.ValidationMessageFor(x => x.CartPagePromotion) +
          + @Html.SmartLabelFor(x => x.PaymentListPromotion) + + @Html.SettingEditorFor(x => x.PaymentListPromotion, + @Html.DropDownListFor(x => x.PaymentListPromotion, Model.PaymentListPromotions, T("Common.Unspecified"))) + @Html.ValidationMessageFor(x => x.PaymentListPromotion) +
          + @Html.SmartLabelFor(x => x.Lender) + + @Html.SettingEditorFor(x => x.Lender) + @Html.ValidationMessageFor(x => x.Lender) +
          + @Html.SmartLabelFor(x => x.Promote) + + @Html.SettingEditorFor(x => x.Promote, Html.CheckBoxFor(x => x.Promote, new { data_toggler_for = "#pnlPromote" })) + @Html.ValidationMessageFor(x => x.Promote) +
          + @Html.SmartLabelFor(x => x.PromotionWidgetZones) + + @Html.SettingEditorFor(x => x.PromotionWidgetZones, @Html.EditorFor(x => x.PromotionWidgetZones)) + @Html.ValidationMessageFor(x => x.PromotionWidgetZones) +
          + @Html.SmartLabelFor(x => x.PromotionDisplayOrder) + + @Html.SettingEditorFor(x => x.PromotionDisplayOrder) + @Html.ValidationMessageFor(x => x.PromotionDisplayOrder) +
          +
          +
          + @T("Common.Misc") +
          +
          +
          + @Html.SmartLabelFor(x => x.FinancingMin) + + @Html.SettingEditorFor(x => x.FinancingMin, null, new { postfix = Model.PrimaryStoreCurrencyCode }) + @Html.ValidationMessageFor(x => x.FinancingMin) +
          + @Html.SmartLabelFor(x => x.FinancingMax) + + @Html.SettingEditorFor(x => x.FinancingMax, null, new { postfix = Model.PrimaryStoreCurrencyCode }) + @Html.ValidationMessageFor(x => x.FinancingMax) +
          + @Html.SmartLabelFor(x => x.AdditionalFee) + + @Html.SettingEditorFor(x => x.AdditionalFee, null, new { postfix = Model.PrimaryStoreCurrencyCode }) + @Html.ValidationMessageFor(x => x.AdditionalFee) +
          + @Html.SmartLabelFor(x => x.AdditionalFeePercentage) + + @Html.SettingEditorFor(x => x.AdditionalFeePercentage) + @Html.ValidationMessageFor(x => x.AdditionalFeePercentage) +
          +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderDetails.Print.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderDetails.Print.cshtml new file mode 100644 index 0000000000..1dd5df86f0 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderDetails.Print.cshtml @@ -0,0 +1,15 @@ +@{ + var financingCosts = ViewBag.FinancingCosts as string; + var totalInclFinancingCosts = ViewBag.TotalInclFinancingCosts as string; +} + + + + + + + + + + +
          @T("Plugins.Payments.PayPalInstalments.FinancingCosts"):@financingCosts
          @T("Plugins.Payments.PayPalInstalments.TotalInclFinancingCosts"):@totalInclFinancingCosts
          \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderDetails.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderDetails.cshtml new file mode 100644 index 0000000000..804e7706bf --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderDetails.cshtml @@ -0,0 +1,15 @@ +@{ + var financingCosts = ViewBag.FinancingCosts as string; + var totalInclFinancingCosts = ViewBag.TotalInclFinancingCosts as string; +} + + + + + + + + + + +
          @T("Plugins.Payments.PayPalInstalments.FinancingCosts"):@financingCosts
          @T("Plugins.Payments.PayPalInstalments.TotalInclFinancingCosts"):@totalInclFinancingCosts
          \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderDetails.liquid b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderDetails.liquid new file mode 100644 index 0000000000..894b198598 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderDetails.liquid @@ -0,0 +1,19 @@ + + + + + +
          +   + + + + + + + + + + +
          {{ 'Plugins.Payments.PayPalInstalments.FinancingCosts' | T }}:{{ PayPalInstalments.FormattedFinancingCosts }}
          {{ 'Plugins.Payments.PayPalInstalments.TotalInclFinancingCosts' | T }}:{{ PayPalInstalments.FormattedTotalInclFinancingCosts }}
          +
          \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderSummaryTotals.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderSummaryTotals.cshtml new file mode 100644 index 0000000000..804e7706bf --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/OrderSummaryTotals.cshtml @@ -0,0 +1,15 @@ +@{ + var financingCosts = ViewBag.FinancingCosts as string; + var totalInclFinancingCosts = ViewBag.TotalInclFinancingCosts as string; +} + + + + + + + + + + +
          @T("Plugins.Payments.PayPalInstalments.FinancingCosts"):@financingCosts
          @T("Plugins.Payments.PayPalInstalments.TotalInclFinancingCosts"):@totalInclFinancingCosts
          \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/PaymentInfo.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/PaymentInfo.cshtml new file mode 100644 index 0000000000..26e095da5a --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/PaymentInfo.cshtml @@ -0,0 +1,18 @@ +@using SmartStore.PayPal.Services; +@model FinancingOptions +@if (Model.Promotion.HasValue) +{ +
          + @{ Html.RenderPartial("Promotion.Content"); } +
          +} +else +{ +
          + @T( +
          + @Html.Raw(T("Plugins.Payments.PayPalInstalments.PaymentInfo")) +
          +
          +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/Promotion.Content.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/Promotion.Content.cshtml new file mode 100644 index 0000000000..e350ab0a68 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/Promotion.Content.cshtml @@ -0,0 +1,101 @@ +@using SmartStore.PayPal; +@using SmartStore.PayPal.Services; +@model FinancingOptions +@{ + var defaultOption = Model.GetDefaultOption(); +} +
          +@T( +
          +@if (Model.Promotion == PayPalPromotion.FinancingExample && defaultOption != null && defaultOption.AnnualPercentageRate > decimal.Zero) +{ +
          + @T("Plugins.Payments.PayPalInstalments.FinancingPerXMonth", defaultOption.MonthlyPayment.ToString(), defaultOption.Term) +
          +
          + @T("Plugins.Payments.PayPalInstalments.RepresentativeExample"): +
          +
          + + + @*German: Darlehehnsbetrag.*@ + + + + + @*German: Nominalzins. Is the same as Sollzins.*@ + + + + + @*German: Effektivzins.*@ + + + + + @*MonthlyPayment * Term != TotalCost --> last monthly payment is different\lower.*@ + + + + + + + +
          @T("Plugins.Payments.PayPalInstalments.NetLoanAmount"):@(Model.NetLoanAmount.ToString())
          @T("Plugins.Payments.PayPalInstalments.FixedNominalRate"):@(Math.Round(defaultOption.NominalRate, 2).ToString("0.00")) %
          @T("Plugins.Payments.PayPalInstalments.AnnualPercentageRate"):@(Math.Round(defaultOption.AnnualPercentageRate, 2).ToString("0.00")) %
          @T("Plugins.Payments.PayPalInstalments.TotalPayableAmount"):@(defaultOption.TotalCost.ToString())
          @T("Plugins.Payments.PayPalInstalments.XMonthlyInstalmentsAmount", defaultOption.Term):@(defaultOption.MonthlyPayment.ToString())
          +
          +
          @T("Plugins.Payments.PayPalInstalments.Lender"): @Html.Raw(Model.Lender)
          +} +else +{ +
          + @if (defaultOption != null) + { + @T("Plugins.Payments.PayPalInstalments.FinancingPerMonth", defaultOption.MonthlyPayment.ToString()) + } + else + { + @T("Plugins.Payments.PayPalInstalments.FinancingWanted") + } +
          +} + + +@{ + using (Html.BeginZoneContent("end", key: "pp-instalments-promotion-modal")) + { + Html.SmartStore().Window() + .Name("paypal-promotion-modal") + .Size(WindowSize.FlexSmall) + .RenderAtPageEnd(true) + .FooterContent(@) + .Render(); + } +} + + \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/Promotion.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/Promotion.cshtml new file mode 100644 index 0000000000..ab7d6645c1 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/Promotion.cshtml @@ -0,0 +1,7 @@ +@using SmartStore.PayPal.Services; +@model FinancingOptions +
          +
          + @{ Html.RenderPartial("Promotion.Content"); } +
          +
          \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/PromotionPopup.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/PromotionPopup.cshtml new file mode 100644 index 0000000000..56690c6801 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalInstalments/PromotionPopup.cshtml @@ -0,0 +1,65 @@ +@using SmartStore.PayPal.Services; +@model FinancingOptions +@{ + var defaultOption = Model.GetDefaultOption(false); +} +
          +
          + @T( +
          +
          @T("Plugins.Payments.PayPalInstalments.PromotionDetailsTitle")
          +
          @Html.Raw(T("Plugins.Payments.PayPalInstalments.PromotionDetailsIntro"))
          +
          @T("Plugins.Payments.PayPalInstalments.NetLoanAmount"): @Model.NetLoanAmount.ToString()
          +
          + @foreach (var option in Model.Qualified) + { +
          +
          + @(T("Plugins.Payments.PayPalInstalments.PlanX", option.Index)) @(defaultOption != null && defaultOption.Index == option.Index ? " *" : "") +
          +
          + + + + + + @*PayPal review: do not display last instalment.*@ + @*@if (option.MonthlyPayment.Amount != option.LastInstalment.Amount) + { + + + + + }*@ + + + + + + + + + + + + + + + + +
          @T("Plugins.Payments.PayPalInstalments.XMonthlyInstalmentsAmount", option.Term)@(option.MonthlyPayment.ToString())
          @T("Plugins.Payments.PayPalInstalments.LastInstalment")@(option.LastInstalment.ToString())
          @T("Plugins.Payments.PayPalInstalments.FixedNominalRate"):@(Math.Round(option.NominalRate, 2).ToString("0.00")) %
          @T("Plugins.Payments.PayPalInstalments.AnnualPercentageRate"):@(Math.Round(option.AnnualPercentageRate, 2).ToString("0.00")) %
          @T("Plugins.Payments.PayPalInstalments.InterestAmount"):@(option.TotalInterest.ToString())
          @T("Plugins.Payments.PayPalInstalments.TotalPayableAmount"):@(option.TotalCost.ToString())
          +
          +
          + } +
          +
          + @if (defaultOption != null) + { +
          * @T("Plugins.Payments.PayPalInstalments.RepresentativeExample").
          + } +
          @T("Plugins.Payments.PayPalInstalments.Lender"): @Html.Raw(Model.Lender)
          +
          +
          +
          +
          \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/Configure.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/Configure.cshtml index efce2e5bbc..a4f7f4476c 100644 --- a/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/Configure.cshtml +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/Configure.cshtml @@ -1,13 +1,8 @@ -@using SmartStore.PayPal; -@using SmartStore.PayPal.Models; +@using SmartStore.PayPal.Models; @using SmartStore.Web.Framework; -@using SmartStore.Web.Framework.UI; @model PayPalPlusConfigurationModel @{ Layout = ""; - Html.AddCssFileParts(true, "~/Plugins/SmartStore.PayPal/Content/smartstore.paypal.css"); - - var hasCredentials = (Model.ClientId.HasValue() && Model.Secret.HasValue()); }
          @@ -39,114 +34,14 @@ { @Html.HiddenFor(model => model.TransactMode) - - - - - - - - - - - - - - - - - - - - - - - - - - - +
          -
          -
          - @Model.ConfigGroups.SafeGet(0) -
          -
          -
          - @Html.SmartLabelFor(model => model.UseSandbox) - - @Html.EditorFor(model => model.UseSandbox) - @Html.ValidationMessageFor(model => model.UseSandbox) -
          - @Html.SmartLabelFor(model => model.ClientId) - - @Html.SettingEditorFor(model => model.ClientId) - @Html.ValidationMessageFor(model => model.ClientId) -
          - @Html.SmartLabelFor(model => model.Secret) - - @Html.SettingEditorFor(model => model.Secret) - @Html.ValidationMessageFor(model => model.Secret) -
          -
          -
          - @Model.ConfigGroups.SafeGet(1) -
          -
          -
          - @Html.SmartLabelFor(model => model.ExperienceProfileId) - - @Html.SettingEditorFor(model => model.ExperienceProfileId) - - - - @T(Model.ExperienceProfileId.HasValue() ? "Common.Refresh" : "Common.AddNew") - + @Html.Partial("ApiConfiguration") - @if (Model.ExperienceProfileId.HasValue()) - { - - - @T("Admin.Common.Delete") - - } - - @Html.ValidationMessageFor(model => model.ExperienceProfileId) -
          - @Html.SmartLabelFor(model => model.WebhookId) - -
          - @* IPNs and webhook messages have no store context, so multistore configuration not possible here *@ - @Html.EditorFor(model => model.WebhookId) - -
          - @if (Model.WebhookId.HasValue()) - { - - - @T("Admin.Common.Delete") - - } - else - { - - - @T("Common.AddNew") - - } -
          -
          - @Html.ValidationMessageFor(model => model.WebhookId) -
          diff --git a/src/Plugins/SmartStore.PayPal/Views/Shared/Partials/ApiConfiguration.cshtml b/src/Plugins/SmartStore.PayPal/Views/Shared/Partials/ApiConfiguration.cshtml new file mode 100644 index 0000000000..b0d068e120 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/Shared/Partials/ApiConfiguration.cshtml @@ -0,0 +1,115 @@ +@using SmartStore.PayPal; +@using SmartStore.PayPal.Models; +@using SmartStore.Web.Framework; +@using SmartStore.Web.Framework.UI; +@model ApiConfigurationModel +@{ + Layout = ""; + + var hasCredentials = Model.ClientId.HasValue() && Model.Secret.HasValue(); +} + +
          - @Model.ConfigGroups.SafeGet(2) + @T("Common.Misc")
          + + + + + + + + + + + + + + + + + + + + + + + + + + +
          +
          +
          + @T("Plugins.SmartStore.PayPal.AccessData") +
          +
          +
          + @Html.SmartLabelFor(model => model.UseSandbox) + + @Html.EditorFor(model => model.UseSandbox) + @Html.ValidationMessageFor(model => model.UseSandbox) +
          + @Html.SmartLabelFor(model => model.ClientId) + + @Html.SettingEditorFor(model => model.ClientId) + @Html.ValidationMessageFor(model => model.ClientId) +
          + @Html.SmartLabelFor(model => model.Secret) + + @Html.SettingEditorFor(model => model.Secret) + @Html.ValidationMessageFor(model => model.Secret) +
          +
          +
          + @T("Admin.Common.DataExchange") +
          +
          +
          + @Html.SmartLabelFor(model => model.ExperienceProfileId) + + @Html.SettingEditorFor(model => model.ExperienceProfileId) + + + + @T(Model.ExperienceProfileId.HasValue() ? "Common.Refresh" : "Common.AddNew") + + + @if (Model.ExperienceProfileId.HasValue()) + { + + + @T("Admin.Common.Delete") + + } + + @Html.ValidationMessageFor(model => model.ExperienceProfileId) +
          + @Html.SmartLabelFor(model => model.WebhookId) + +
          + @* IPNs and webhook messages have no store context, so multistore configuration not possible here *@ + @Html.EditorFor(model => model.WebhookId) + +
          + @if (Model.WebhookId.HasValue()) + { + + + @T("Admin.Common.Delete") + + } + else + { + + + @T("Common.AddNew") + + } +
          +
          + @Html.ValidationMessageFor(model => model.WebhookId) +
          diff --git a/src/Plugins/SmartStore.PayPal/changelog.md b/src/Plugins/SmartStore.PayPal/changelog.md index 5c6598b0a5..ba84600675 100644 --- a/src/Plugins/SmartStore.PayPal/changelog.md +++ b/src/Plugins/SmartStore.PayPal/changelog.md @@ -1,5 +1,9 @@ #Release Notes +##Paypal 3.2.2.0 +###New Features +* New instalments provider. + ##Paypal 3.1.5.3 ###Improvements * PayPal PLUS: now up to 10 more third party payment methods are allowed by PayPal. diff --git a/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs b/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs index e55f1cdd0a..05761ba86c 100644 --- a/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs +++ b/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs @@ -71,6 +71,7 @@ public ActionResult Configure() model.LimitMethodsToCreated = _shippingByTotalSettings.LimitMethodsToCreated; model.SmallQuantityThreshold = _shippingByTotalSettings.SmallQuantityThreshold; model.SmallQuantitySurcharge = _shippingByTotalSettings.SmallQuantitySurcharge; + model.CalculateTotalIncludingTax = _shippingByTotalSettings.CalculateTotalIncludingTax; model.PrimaryStoreCurrencyCode = _services.StoreContext.CurrentStore.PrimaryStoreCurrency.CurrencyCode; model.GridPageSize = _adminAreaSettings.GridPageSize; @@ -162,6 +163,7 @@ public ActionResult Configure(ByTotalListModel model) _shippingByTotalSettings.LimitMethodsToCreated = model.LimitMethodsToCreated; _shippingByTotalSettings.SmallQuantityThreshold = model.SmallQuantityThreshold; _shippingByTotalSettings.SmallQuantitySurcharge = model.SmallQuantitySurcharge; + _shippingByTotalSettings.CalculateTotalIncludingTax = model.CalculateTotalIncludingTax; _services.Settings.SaveSetting(_shippingByTotalSettings); diff --git a/src/Plugins/SmartStore.Shipping/Description.txt b/src/Plugins/SmartStore.Shipping/Description.txt index 94ecae8f06..654d28c65a 100644 --- a/src/Plugins/SmartStore.Shipping/Description.txt +++ b/src/Plugins/SmartStore.Shipping/Description.txt @@ -2,8 +2,8 @@ Description: Provides shipping methods for fixed rate shipping and computation based on weight. SystemName: SmartStore.Shipping Group: Shipping -Version: 3.2.0 -MinAppVersion: 3.2.0 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 1 FileName: SmartStore.Shipping.dll ResourceRootKey: Plugins.SmartStore.Shipping \ No newline at end of file diff --git a/src/Plugins/SmartStore.Shipping/Localization/resources.de-de.xml b/src/Plugins/SmartStore.Shipping/Localization/resources.de-de.xml index c5d47af01d..de15e1946b 100644 --- a/src/Plugins/SmartStore.Shipping/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.Shipping/Localization/resources.de-de.xml @@ -84,6 +84,12 @@ Dem Kunden wird während des Checkouts kostenloser Versand als Fallback angeboten, wenn keine der festgelegten Bedingungen zutreffen. Aktivieren Sie diese Option, wenn das nicht gewünscht ist. + + Berechne Bestellwert inkl. Steuer + + + Legt fest, ob der Bestellwert zur Berechnung der Versandkosten inkl. oder exkl. Steuern berechnet werden soll. + Basisgebühr diff --git a/src/Plugins/SmartStore.Shipping/Localization/resources.en-us.xml b/src/Plugins/SmartStore.Shipping/Localization/resources.en-us.xml index 16cfa0bb5f..41e299ed1c 100644 --- a/src/Plugins/SmartStore.Shipping/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.Shipping/Localization/resources.en-us.xml @@ -85,6 +85,12 @@ If you check this option, then your customers will be limited to shipping options configured here. Otherwise, they'll be able to choose any existing shipping options even if they're not configured here (zero shipping fee in this case). + + Calculate order total incl. tax + + + Specifies whether to calculate the order total including or excluding taxes. + Base fee diff --git a/src/Plugins/SmartStore.Shipping/Models/ByTotalListModel.cs b/src/Plugins/SmartStore.Shipping/Models/ByTotalListModel.cs index 848836b1af..4816ca9b1b 100644 --- a/src/Plugins/SmartStore.Shipping/Models/ByTotalListModel.cs +++ b/src/Plugins/SmartStore.Shipping/Models/ByTotalListModel.cs @@ -63,6 +63,9 @@ public ByTotalListModel() [SmartResourceDisplayName("Plugins.Shipping.ByTotal.Fields.SmallQuantitySurcharge")] public decimal SmallQuantitySurcharge { get; set; } + [SmartResourceDisplayName("Plugins.Shipping.ByTotal.Fields.CalculateTotalIncludingTax")] + public bool CalculateTotalIncludingTax { get; set; } + public string PrimaryStoreCurrencyCode { get; set; } public int GridPageSize { get; set; } diff --git a/src/Plugins/SmartStore.Shipping/Providers/ByTotalProvider.cs b/src/Plugins/SmartStore.Shipping/Providers/ByTotalProvider.cs index 05bd66b8cc..f06e57d635 100644 --- a/src/Plugins/SmartStore.Shipping/Providers/ByTotalProvider.cs +++ b/src/Plugins/SmartStore.Shipping/Providers/ByTotalProvider.cs @@ -172,7 +172,6 @@ public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest get int countryId = 0; int stateProvinceId = 0; string zip = null; - var taxRate = decimal.Zero; decimal subTotal = decimal.Zero; int storeId = _storeContext.CurrentStore.Id; @@ -190,13 +189,19 @@ public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest get continue; } - var itemSubTotal = _priceCalculationService.GetSubTotal(shoppingCartItem, true); - var itemSubTotalInclTax = _taxService.GetProductPrice(shoppingCartItem.Item.Product, itemSubTotal, true, getShippingOptionRequest.Customer, out taxRate); - subTotal += itemSubTotalInclTax; + var itemSubTotalBase = _priceCalculationService.GetSubTotal(shoppingCartItem, true); + var itemSubTotal = _taxService.GetProductPrice( + shoppingCartItem.Item.Product, + itemSubTotalBase, + _shippingByTotalSettings.CalculateTotalIncludingTax, + getShippingOptionRequest.Customer, + out var _); + + subTotal += itemSubTotal; } - decimal sqThreshold = _shippingByTotalSettings.SmallQuantityThreshold; - decimal sqSurcharge = _shippingByTotalSettings.SmallQuantitySurcharge; + var sqThreshold = _shippingByTotalSettings.SmallQuantityThreshold; + var sqSurcharge = _shippingByTotalSettings.SmallQuantitySurcharge; var shippingMethods = _shippingService.GetAllShippingMethods(getShippingOptionRequest, storeId); foreach (var shippingMethod in shippingMethods) @@ -206,7 +211,7 @@ public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest get { if (rate > 0 && sqThreshold > 0 && subTotal <= sqThreshold) { - // Add small quantity surcharge (Mindermengenzuschlag) + // Add small quantity surcharge (Mindermengenzuschlag). rate += sqSurcharge; } diff --git a/src/Plugins/SmartStore.Shipping/Settings/ShippingByTotalSettings.cs b/src/Plugins/SmartStore.Shipping/Settings/ShippingByTotalSettings.cs index 3ba3e94ee9..ce7f882691 100644 --- a/src/Plugins/SmartStore.Shipping/Settings/ShippingByTotalSettings.cs +++ b/src/Plugins/SmartStore.Shipping/Settings/ShippingByTotalSettings.cs @@ -4,7 +4,13 @@ namespace SmartStore.Shipping { public class ShippingByTotalSettings : ISettings { + public ShippingByTotalSettings() + { + CalculateTotalIncludingTax = true; + } + public bool LimitMethodsToCreated { get; set; } + public bool CalculateTotalIncludingTax { get; set; } public decimal SmallQuantityThreshold { get; set; } public decimal SmallQuantitySurcharge { get; set; } diff --git a/src/Plugins/SmartStore.Shipping/SmartStore.Shipping.csproj b/src/Plugins/SmartStore.Shipping/SmartStore.Shipping.csproj index 5492feba91..8d59d2df62 100644 --- a/src/Plugins/SmartStore.Shipping/SmartStore.Shipping.csproj +++ b/src/Plugins/SmartStore.Shipping/SmartStore.Shipping.csproj @@ -1,5 +1,6 @@  - + + diff --git a/src/Plugins/SmartStore.Shipping/Views/ByTotal/Configure.cshtml b/src/Plugins/SmartStore.Shipping/Views/ByTotal/Configure.cshtml index d96bda0ff5..ee33ab492d 100644 --- a/src/Plugins/SmartStore.Shipping/Views/ByTotal/Configure.cshtml +++ b/src/Plugins/SmartStore.Shipping/Views/ByTotal/Configure.cshtml @@ -44,6 +44,15 @@ @Html.ValidationMessageFor(model => model.SmallQuantitySurcharge) + + + @Html.SmartLabelFor(model => model.CalculateTotalIncludingTax) + + + @Html.EditorFor(model => model.CalculateTotalIncludingTax) + @Html.ValidationMessageFor(model => model.CalculateTotalIncludingTax) + + @Html.SmartLabelFor(model => model.LimitMethodsToCreated) diff --git a/src/Plugins/SmartStore.ShippingByWeight/Description.txt b/src/Plugins/SmartStore.ShippingByWeight/Description.txt index 03dcf522b4..f73070d64c 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Description.txt +++ b/src/Plugins/SmartStore.ShippingByWeight/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Shipping by weight SystemName: SmartStore.ShippingByWeight Group: Shipping -Version: 3.2.0 -MinAppVersion: 3.2.0 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 1 FileName: SmartStore.ShippingByWeight.dll ResourceRootKey: Plugins.Shipping.ByWeight \ No newline at end of file diff --git a/src/Plugins/SmartStore.ShippingByWeight/SmartStore.ShippingByWeight.csproj b/src/Plugins/SmartStore.ShippingByWeight/SmartStore.ShippingByWeight.csproj index 0392e89152..30c483ab7c 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/SmartStore.ShippingByWeight.csproj +++ b/src/Plugins/SmartStore.ShippingByWeight/SmartStore.ShippingByWeight.csproj @@ -1,5 +1,6 @@  - + + diff --git a/src/Plugins/SmartStore.Tax/Description.txt b/src/Plugins/SmartStore.Tax/Description.txt index 3bc0f3b246..66eb94aab2 100644 --- a/src/Plugins/SmartStore.Tax/Description.txt +++ b/src/Plugins/SmartStore.Tax/Description.txt @@ -2,8 +2,8 @@ Description: Contains default tax providers like FixedRate, ByRegion etc. Group: Tax SystemName: SmartStore.Tax -Version: 3.2.0 -MinAppVersion: 3.2.0 +Version: 3.2.2 +MinAppVersion: 3.2.2 DisplayOrder: 0 FileName: SmartStore.Tax.dll ResourceRootKey: Plugins.SmartStore.Tax diff --git a/src/Plugins/SmartStore.Tax/Providers/ByRegionTaxProvider.cs b/src/Plugins/SmartStore.Tax/Providers/ByRegionTaxProvider.cs index 5752d63f6a..bd1765149e 100644 --- a/src/Plugins/SmartStore.Tax/Providers/ByRegionTaxProvider.cs +++ b/src/Plugins/SmartStore.Tax/Providers/ByRegionTaxProvider.cs @@ -1,37 +1,32 @@ -using System.Data.Entity.Migrations; +using System.Linq; using System.Web.Routing; +using SmartStore.Core.Domain.Tax; using SmartStore.Core.Plugins; -using SmartStore.Tax.Data; -using SmartStore.Tax.Data.Migrations; -using SmartStore.Tax.Services; -using SmartStore.Services.Localization; +using SmartStore.Services.Configuration; using SmartStore.Services.Tax; +using SmartStore.Tax.Services; namespace SmartStore.Tax { - [SystemName("Tax.CountryStateZip")] + [SystemName("Tax.CountryStateZip")] [FriendlyName("Tax By Region")] [DisplayOrder(10)] public class ByRegionTaxProvider : ITaxProvider, IConfigurable { private readonly ITaxRateService _taxRateService; - private readonly TaxRateObjectContext _objectContext; - private readonly ILocalizationService _localizationService; + private readonly ISettingService _settingService; + private readonly TaxSettings _taxSettings; - public ByRegionTaxProvider(ITaxRateService taxRateService, - TaxRateObjectContext objectContext, - ILocalizationService localizationService) + public ByRegionTaxProvider( + ITaxRateService taxRateService, + ISettingService settingService, + TaxSettings taxSettings) { - this._taxRateService = taxRateService; - this._objectContext = objectContext; - _localizationService = localizationService; + _taxRateService = taxRateService; + _settingService = settingService; + _taxSettings = taxSettings; } - /// - /// Gets tax rate - /// - /// Tax calculation request - /// Tax public CalculateTaxResult GetTaxRate(CalculateTaxRequest calculateTaxRequest) { var result = new CalculateTaxResult(); @@ -42,12 +37,26 @@ public CalculateTaxResult GetTaxRate(CalculateTaxRequest calculateTaxRequest) return result; } - var taxRates = _taxRateService.GetAllTaxRates(calculateTaxRequest.TaxCategoryId, - calculateTaxRequest.Address.Country != null ? calculateTaxRequest.Address.Country.Id: 0, - calculateTaxRequest.Address.StateProvince != null ? calculateTaxRequest.Address.StateProvince.Id : 0, + if (_taxSettings.EuVatEnabled) + { + if (!(calculateTaxRequest.Address.Country?.SubjectToVat ?? false)) + { + // Fallback to fixed rate (merchant country VAT rate). + result.TaxRate = _settingService.GetSettingByKey($"Tax.TaxProvider.FixedRate.TaxCategoryId{calculateTaxRequest.TaxCategoryId}"); + return result; + } + } + + var taxRates = _taxRateService.GetAllTaxRates( + calculateTaxRequest.TaxCategoryId, + calculateTaxRequest.Address.Country?.Id ?? 0, + calculateTaxRequest.Address.StateProvince?.Id ?? 0, calculateTaxRequest.Address.ZipPostalCode); - if (taxRates.Count > 0) + + if (taxRates.Any()) + { result.TaxRate = taxRates[0].Percentage; + } return result; } @@ -56,8 +65,7 @@ public void GetConfigurationRoute(out string actionName, out string controllerNa { actionName = "Configure"; controllerName = "TaxByRegion"; - routeValues = new RouteValueDictionary() { { "area", "SmartStore.Tax" } }; + routeValues = new RouteValueDictionary { { "area", "SmartStore.Tax" } }; } - } } diff --git a/src/Plugins/SmartStore.Tax/Providers/FixedRateTaxProvider.cs b/src/Plugins/SmartStore.Tax/Providers/FixedRateTaxProvider.cs index 55d3ab87b7..4fabac4b04 100644 --- a/src/Plugins/SmartStore.Tax/Providers/FixedRateTaxProvider.cs +++ b/src/Plugins/SmartStore.Tax/Providers/FixedRateTaxProvider.cs @@ -1,37 +1,35 @@ using System.Web.Routing; using SmartStore.Core.Plugins; using SmartStore.Services.Configuration; -using SmartStore.Services.Localization; using SmartStore.Services.Tax; namespace SmartStore.Tax { - [SystemName("Tax.FixedRate")] + [SystemName("Tax.FixedRate")] [FriendlyName("Fixed Tax Rate")] [DisplayOrder(5)] public class FixedRateTaxProvider : ITaxProvider, IConfigurable { private readonly ISettingService _settingService; - private readonly ILocalizationService _localizationService; - public FixedRateTaxProvider(ISettingService settingService, ILocalizationService localizationService) + public FixedRateTaxProvider(ISettingService settingService) { - this._settingService = settingService; - _localizationService = localizationService; + _settingService = settingService; } public CalculateTaxResult GetTaxRate(CalculateTaxRequest calculateTaxRequest) { - var result = new CalculateTaxResult() + var result = new CalculateTaxResult { TaxRate = GetTaxRate(calculateTaxRequest.TaxCategoryId) }; + return result; } protected decimal GetTaxRate(int taxCategoryId) { - decimal rate = this._settingService.GetSettingByKey(string.Format("Tax.TaxProvider.FixedRate.TaxCategoryId{0}", taxCategoryId)); + var rate = _settingService.GetSettingByKey($"Tax.TaxProvider.FixedRate.TaxCategoryId{taxCategoryId}"); return rate; } @@ -39,8 +37,7 @@ public void GetConfigurationRoute(out string actionName, out string controllerNa { actionName = "Configure"; controllerName = "TaxFixedRate"; - routeValues = new RouteValueDictionary() { { "area", "SmartStore.Tax" } }; + routeValues = new RouteValueDictionary { { "area", "SmartStore.Tax" } }; } - } } diff --git a/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj b/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj index 7b93e203b3..21dbe5eef1 100644 --- a/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj +++ b/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj @@ -1,5 +1,6 @@  - + + diff --git a/src/Presentation/SmartStore.Web.Framework/Controllers/AdminControllerBase.cs b/src/Presentation/SmartStore.Web.Framework/Controllers/AdminControllerBase.cs index c79c787de1..f04be13344 100644 --- a/src/Presentation/SmartStore.Web.Framework/Controllers/AdminControllerBase.cs +++ b/src/Presentation/SmartStore.Web.Framework/Controllers/AdminControllerBase.cs @@ -27,7 +27,7 @@ public void OnActionExecuted(ActionExecutedContext filterContext) } [AdminValidateIpAddress(Order = 100)] - [RequireHttpsByConfig(SslRequirement.Yes, Order = 110)] + [RewriteUrl(SslRequirement.Yes, Order = 110)] [CustomerLastActivity(Order = 100)] [StoreIpAddress(Order = 100)] [AdminThemed(Order = -1)] diff --git a/src/Presentation/SmartStore.Web.Framework/Controllers/ContollerExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Controllers/ContollerExtensions.cs index eec8c4eb38..1d17a3d790 100644 --- a/src/Presentation/SmartStore.Web.Framework/Controllers/ContollerExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Controllers/ContollerExtensions.cs @@ -126,7 +126,7 @@ public static string RenderPartialViewToString(this ControllerBase controller, s { ViewEngineResult viewResult = System.Web.Mvc.ViewEngines.Engines.FindPartialView(controller.ControllerContext, viewName.EmptyNull()); - ThrowIfViewNotFound(viewResult, viewName); + ThrowIfViewNotFound(viewResult, viewName); var viewContext = new ViewContext(controller.ControllerContext, viewResult.View, controller.ViewData, controller.TempData, sw); viewResult.View.Render(viewContext, sw); diff --git a/src/Presentation/SmartStore.Web.Framework/Controllers/PublicControllerBase.cs b/src/Presentation/SmartStore.Web.Framework/Controllers/PublicControllerBase.cs index 189014d893..fee48defb5 100644 --- a/src/Presentation/SmartStore.Web.Framework/Controllers/PublicControllerBase.cs +++ b/src/Presentation/SmartStore.Web.Framework/Controllers/PublicControllerBase.cs @@ -6,8 +6,7 @@ namespace SmartStore.Web.Framework.Controllers { - [CanonicalHostName(Order = 100)] - [RequireHttpsByConfigAttribute(SslRequirement.Retain, Order = 110)] + [RewriteUrl(SslRequirement.Retain)] [StoreClosed(Order = -1)] [PublicStoreAllowNavigation(Order = -1)] [LanguageSeoCode(Order = -1)] diff --git a/src/Presentation/SmartStore.Web.Framework/DependencyRegistrar.cs b/src/Presentation/SmartStore.Web.Framework/DependencyRegistrar.cs index 61da50b412..80526ac5c3 100644 --- a/src/Presentation/SmartStore.Web.Framework/DependencyRegistrar.cs +++ b/src/Presentation/SmartStore.Web.Framework/DependencyRegistrar.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using System.Web; @@ -201,7 +202,7 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); - builder.RegisterType().As().InstancePerRequest(); + builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().InstancePerRequest(); builder.RegisterType().As().InstancePerRequest(); @@ -610,10 +611,10 @@ protected override void Load(ContainerBuilder builder) { builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As().SingleInstance(); var consumerTypes = _typeFinder.FindClassesOfType(typeof(IConsumer)); foreach (var type in consumerTypes) @@ -706,8 +707,6 @@ static HttpContextBase HttpContextBaseFactory(IComponentContext ctx) return new HttpContextWrapper(HttpContext.Current); } - // TODO: determine store url - // register FakeHttpContext when HttpContext is not available return new FakeHttpContext("~/"); } @@ -715,19 +714,11 @@ static HttpContextBase HttpContextBaseFactory(IComponentContext ctx) static bool IsRequestValid() { if (HttpContext.Current == null) - return false; - - try - { - // The "Request" property throws at application startup on IIS integrated pipeline mode - var req = HttpContext.Current.Request; - } - catch { return false; } - return true; + return HttpContext.Current.SafeGetHttpRequest() != null; } } @@ -856,7 +847,7 @@ protected override void Load(ContainerBuilder builder) if (DataSettings.DatabaseIsInstalled()) { - builder.RegisterType().AsResultFilterFor(); + builder.RegisterType().AsResultFilterFor(0); } } } diff --git a/src/Presentation/SmartStore.Web.Framework/Extensions/HttpExtensions.cs b/src/Presentation/SmartStore.Web.Framework/Extensions/HttpExtensions.cs index 63eecdab18..56dba19bee 100644 --- a/src/Presentation/SmartStore.Web.Framework/Extensions/HttpExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/Extensions/HttpExtensions.cs @@ -215,15 +215,13 @@ internal static void SetUserThemeChoiceInCookie(this HttpContextBase context, st internal static HttpCookie GetPreviewModeCookie(this HttpContextBase context, bool createIfMissing) { - if (context?.Handler == null) - return null; - - var cookie = context.Request?.Cookies?.Get("sm.PreviewModeOverrides"); + var httpRequest = context.SafeGetHttpRequest(); + var cookie = httpRequest?.Cookies?.Get("sm.PreviewModeOverrides"); - if (cookie == null && createIfMissing) + if (cookie == null && createIfMissing && httpRequest != null) { cookie = new HttpCookie("sm.PreviewModeOverrides") { HttpOnly = true }; - context.Request.Cookies.Set(cookie); + httpRequest.Cookies.Set(cookie); } if (cookie != null) diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/CompressAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/CompressAttribute.cs index 24ba430c44..bdbde8447f 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/CompressAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/CompressAttribute.cs @@ -26,7 +26,7 @@ public override void OnActionExecuting(ActionExecutingContext filterContext) response.AppendHeader("Content-Encoding", "deflate"); } - //response.AppendHeader("Vary", "Content-Encoding"); + response.AppendHeader("Vary", "Content-Encoding"); } } } diff --git a/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs index 716cc5b6ef..8a65cd046a 100644 --- a/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs +++ b/src/Presentation/SmartStore.Web.Framework/Filters/StoreIpAddressAttribute.cs @@ -19,7 +19,7 @@ public virtual void OnActionExecuting(ActionExecutingContext filterContext) if (!DataSettings.DatabaseIsInstalled()) return; - if (filterContext == null || filterContext.HttpContext == null || filterContext.HttpContext.Request == null) + if (filterContext?.HttpContext?.Request == null) return; // Don't apply filter to child methods. diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/CachedFileResult.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/CachedFileResult.cs index 0dda117ee9..0967a4d685 100644 --- a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/CachedFileResult.cs +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/CachedFileResult.cs @@ -1,10 +1,6 @@ using System; -using System.Collections.Generic; +using System.Globalization; using System.IO; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading.Tasks; using System.Web; using System.Web.Hosting; using System.Web.Mvc; @@ -13,230 +9,226 @@ namespace SmartStore.Web.Framework.Modelling { - public class CachedFileResult : FileResult + public class CachedFileResult : ActionResult, IFileResponse { - // Default buffer size as defined in BufferedStream type - private const int BufferSize = 0x1000; - - private HttpContextBase _httpContext; - private string _etag; - - private readonly string _path; - private readonly Func _streamReader; - private readonly Func _bufferReader; + #region Ctor public CachedFileResult(string path, string contentType = null) - : base(contentType ?? MimeTypes.MapNameToMimeType(path)) + : this(GetFileInfo(path), contentType) { - _etag = CreateETag(path); - _path = path; } - public CachedFileResult(FileInfo file, string contentType = null, Func reader = null) - : this(CreateETag(file), contentType ?? MimeTypes.MapNameToMimeType(file.Name), reader ?? file.OpenRead) + public CachedFileResult(FileInfo file, string contentType = null) { + Guard.NotNull(file, nameof(file)); + + if (!file.Exists) + { + throw new FileNotFoundException(file.FullName); + } + + Transmitter = new FileStreamTransmitter(file.OpenRead); + ContentType = contentType.NullEmpty() ?? MimeTypes.MapNameToMimeType(file.Name); + FileLength = file.Length; LastModifiedUtc = file.LastWriteTimeUtc; } - public CachedFileResult(IFile file, string contentType = null, Func reader = null) - : this(CreateETag(file), contentType ?? MimeTypes.MapNameToMimeType(file.Name), reader ?? file.OpenRead) + public CachedFileResult(IFile file, string contentType = null) { + Guard.NotNull(file, nameof(file)); + + if (!file.Exists) + { + throw new FileNotFoundException(file.Path); + } + + Transmitter = new FileStreamTransmitter(file.OpenRead); + ContentType = contentType.NullEmpty() ?? MimeTypes.MapNameToMimeType(file.Name); + FileLength = file.Size; LastModifiedUtc = file.LastUpdated; } - public CachedFileResult(VirtualFile file, string contentType = null, Func reader = null) - : this(CreateETag(file), contentType ?? MimeTypes.MapNameToMimeType(file.Name), reader ?? file.Open) + public CachedFileResult(VirtualFile file, DateTime? lastModifiedUtc = null, string contentType = null) { + Guard.NotNull(file, nameof(file)); + + try + { + var fi = GetFileInfo(file.VirtualPath); + if (fi.Exists) + { + ContentType = contentType.NullEmpty() ?? MimeTypes.MapNameToMimeType(fi.Name); + FileLength = fi.Length; + LastModifiedUtc = fi.LastWriteTimeUtc; + } + } + finally + { + if (lastModifiedUtc == null) + { + throw new ArgumentNullException(nameof(lastModifiedUtc), "A modification date must be provided if the VirtualFile cannot be mapped to a physical path."); + } + + if (FileLength == null) + { + ContentType = contentType.NullEmpty() ?? MimeTypes.MapNameToMimeType(file.Name); + using (var stream = file.Open()) + { + FileLength = stream.Length; + } + } + } + + Transmitter = new FileStreamTransmitter(file.Open); + LastModifiedUtc = lastModifiedUtc.Value; } - public CachedFileResult(string etag, string contentType, Func reader) - : base(contentType) + public CachedFileResult(string contentType, DateTime lastModifiedUtc, Func factory, long? fileLength = null) { - Guard.NotNull(reader, nameof(reader)); - SanitizeETag(etag); + Guard.NotNull(factory, nameof(factory)); + Guard.NotEmpty(contentType, nameof(contentType)); - _streamReader = reader; + Transmitter = new FileStreamTransmitter(factory); + ContentType = contentType; + FileLength = fileLength; + LastModifiedUtc = lastModifiedUtc; } - public CachedFileResult(string etag, string contentType, Func reader) - : base(contentType) + public CachedFileResult(string contentType, DateTime lastModifiedUtc, Func factory, long? fileLength = null) { - Guard.NotNull(reader, nameof(reader)); - SanitizeETag(etag); + Guard.NotNull(factory, nameof(factory)); + Guard.NotEmpty(contentType, nameof(contentType)); - _bufferReader = reader; + Transmitter = new FileBufferTransmitter(factory); + ContentType = contentType; + FileLength = fileLength; + LastModifiedUtc = lastModifiedUtc; } - private void SanitizeETag(string etag) + private static FileInfo GetFileInfo(string path) { - Guard.NotEmpty(etag, nameof(etag)); + Guard.NotEmpty(path, nameof(path)); - if (etag[0] != '"') + if (!PathHelper.IsAbsolutePhysicalPath(path)) { - etag = string.Concat("\"", etag, "\""); + path = CommonHelper.MapPath(path); } - _etag = etag; + return new FileInfo(path); } - public DateTime? LastModifiedUtc { get; set; } + #endregion - public DateTime Expiration { get; set; } = DateTime.UtcNow.AddDays(7); + #region Properties - public TimeSpan MaxAge { get; set; } = TimeSpan.FromDays(7); + public string ContentType { get; private set; } + public long? FileLength { get; set; } - public static string CreateETag(string path) - { - Guard.NotEmpty(path, nameof(path)); + public DateTime LastModifiedUtc { get; set; } - if (!PathHelper.IsAbsolutePhysicalPath(path)) - { - path = CommonHelper.MapPath(path); - } + public TimeSpan MaxAge { get; set; } = TimeSpan.FromDays(1); - return CreateETag(new FileInfo(path)); - } + /// + /// If not set, will be auto-generated based on property. + /// + public string ETag { get; set; } - public static string CreateETag(VirtualFile file) - { - Guard.NotNull(file, nameof(file)); + public FileTransmitter Transmitter { get; private set; } - var fi = new FileInfo(CommonHelper.MapPath(file.VirtualPath)); - return CreateETag(fi); - } + /// + /// A callback that will be invoked on successful result execution + /// + public Action OnExecuted { get; set; } - public static string CreateETag(FileInfo file) - { - Guard.NotNull(file, nameof(file)); + #endregion - if (!file.Exists) + public override void ExecuteResult(ControllerContext context) + { + var httpContext = context.HttpContext; + var request = httpContext.Request; + var response = httpContext.Response; + + // Fix Last Modified Time. We might need it soon + // if we encounter a Range: and If-Range header + // Using UTC time to avoid daylight savings time bug 83230 + var lastModified = new DateTime(LastModifiedUtc.Year, + LastModifiedUtc.Month, + LastModifiedUtc.Day, + LastModifiedUtc.Hour, + LastModifiedUtc.Minute, + LastModifiedUtc.Second, + 0, + DateTimeKind.Utc); + + // Because we can't set a "Last-Modified" header to any time + // in the future, check the last modified time and set it to + // DateTime.Now if it's in the future. + // This is to fix VSWhidbey #402323 + var utcNow = DateTime.UtcNow; + if (lastModified > utcNow) { - throw new FileNotFoundException("File to create ETag for must exist.", file.FullName); + // use 1 second resolution + lastModified = new DateTime(utcNow.Ticks - (utcNow.Ticks % TimeSpan.TicksPerSecond), DateTimeKind.Utc); } - return CreateETag(file.FullName, file.Length.GetHashCode(), file.CreationTimeUtc, file.LastWriteTimeUtc); - } + LastModifiedUtc = lastModified; - public static string CreateETag(IFile file) - { - Guard.NotNull(file, nameof(file)); - - if (!file.Exists) + // Generate ETag if empty + if (ETag.IsEmpty()) { - throw new FileNotFoundException("File to create ETag for must exist.", file.Path); + ETag = GenerateETag(httpContext, lastModified, utcNow); } - return CreateETag(file.Path, file.Size.GetHashCode(), file.LastUpdated); - } - - public static string CreateETag(params object[] tokens) - { - var sb = new StringBuilder(); - var tag = string.Empty; + // Determine applicable file responder + var responder = ResolveResponder(request); - foreach (var token in tokens) + // Execute response (send file) + if (responder.TrySendHeaders(httpContext)) { - if (token is DateTime dt) - { - tag += dt.ToUnixTime().ToString(); - } - else - { - tag += token.Convert(); - } + responder.SendFile(httpContext); } - return "\"" + tag.Hash(Encoding.UTF8) + "\""; + // Finish: invoke the optional callback + OnExecuted?.Invoke(); } - public override void ExecuteResult(ControllerContext context) + private FileResponder ResolveResponder(HttpRequestBase request) { - _httpContext = context.HttpContext; - base.ExecuteResult(context); - } - - protected override void WriteFile(HttpResponseBase response) - { - var ifNoneMatch = _httpContext.Request.Headers["If-None-Match"]; - if (ifNoneMatch.HasValue() && _etag == ifNoneMatch) + // is this a request for an unmodified file? + var ifNoneMatch = request.Headers["If-None-Match"]; + if (ifNoneMatch.HasValue() && ETag == ifNoneMatch) { - // File hasn't changed, so return HTTP 304 without retrieving the data - response.StatusCode = (int)HttpStatusCode.NotModified; - response.StatusDescription = "Not Modified"; - - // Explicitly set the Content-Length header so the client doesn't wait for - // content but keeps the connection open for other requests - response.AddHeader("Content-Length", "0"); - - ApplyResponseHeaders(response, false); + return new UnmodifiedFileResponder(this); } - else - { - if (_path != null) - { - response.TransmitFile(_path); - } - else if (_streamReader != null) - { - var stream = _streamReader(); - if (stream == null) - { - throw new NullReferenceException("File stream cannot be NULL."); - } - // Grab chunks of data and write to the output stream - var outputStream = response.OutputStream; - using (stream) - { - var buffer = new byte[BufferSize]; - - while (true) - { - int bytesRead = stream.Read(buffer, 0, BufferSize); - if (bytesRead == 0) - { - // no more data - break; - } - - outputStream.Write(buffer, 0, bytesRead); - } - } - } - else if (_bufferReader != null) - { - var buffer = _bufferReader(); - if (buffer == null) - { - throw new NullReferenceException("File buffer cannot be NULL."); - } - - // Write buffer to output stream - response.OutputStream.Write(buffer, 0, buffer.Length); - } - - ApplyResponseHeaders(response, true); - - // Set ETag for served file (revalidated on subsequent requests) - response.Cache.SetETag(_etag); + // is this a Range request? + var rangeHeader = request.Headers["Range"]; + if (rangeHeader.HasValue() && rangeHeader.StartsWith("bytes", StringComparison.OrdinalIgnoreCase)) + { + return new RangeFileResponder(this, rangeHeader); } + + // Responder for sending the whole file + return new FullFileResponder(this); } - private void ApplyResponseHeaders(HttpResponseBase response, bool setLastModifiedDate) + private static string GenerateETag(HttpContextBase context, DateTime lastModified, DateTime now) { - var cache = response.Cache; - - cache.SetCacheability(System.Web.HttpCacheability.Public); - cache.VaryByHeaders["Accept-Encoding"] = true; - cache.SetExpires(Expiration); - cache.SetMaxAge(MaxAge); - cache.SetRevalidation(HttpCacheRevalidation.AllCaches); - - if (setLastModifiedDate && LastModifiedUtc.HasValue) - { - cache.SetLastModified(LastModifiedUtc.Value); - } + // Get 64-bit FILETIME stamp + var lastModFileTime = lastModified.ToFileTime(); + var nowFileTime = now.ToFileTime(); + var hexFileTime = lastModFileTime.ToString("X8", CultureInfo.InvariantCulture); + + //// Do what IIS does to determine if this is a weak ETag. + //// Compare the last modified time to now and if the difference is + //// less than or equal to 3 seconds, then it is weak + //if ((nowFileTime - lastModFileTime) <= 30000000) + //{ + // return "W/\"" + hexFileTime + "\""; + //} + + return "\"" + hexFileTime + "\""; } } } diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/FileResponder.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/FileResponder.cs new file mode 100644 index 0000000000..3b30853fe1 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/FileResponder.cs @@ -0,0 +1,561 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Net; +using System.Web; + +namespace SmartStore.Web.Framework.Modelling +{ + internal abstract class FileResponder + { + protected FileResponder(IFileResponse fileResponse) + { + Guard.NotNull(fileResponse, nameof(fileResponse)); + FileResponse = fileResponse; + } + + protected IFileResponse FileResponse { get; private set; } + + public virtual bool TrySendHeaders(HttpContextBase context) + { + var response = context.Response; + + var utcNow = DateTime.UtcNow; + + // Specify content type + response.ContentType = FileResponse.ContentType; + // We support byte ranges + response.AppendHeader("Accept-Ranges", "bytes"); + // Set the expires header for HTTP 1.0 cliets + response.Cache.SetExpires(utcNow.Add(FileResponse.MaxAge)); + // How often the browser should check that it has the latest version + response.Cache.SetMaxAge(FileResponse.MaxAge); + // The unique identifier for the entity + response.Cache.SetETag(FileResponse.ETag); + // Proxy and browser can cache response + response.Cache.SetCacheability(HttpCacheability.Public); + // Proxy cache should check with orginal server once cache has expired + response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches); + //// The date the file was last modified + //context.Response.Cache.SetLastModified(FileResponse.LastModifiedUtc); + + return true; + } + + public abstract void SendFile(HttpContextBase context); + } + + + + internal sealed class UnmodifiedFileResponder : FileResponder + { + public UnmodifiedFileResponder(IFileResponse fileResponse) + : base(fileResponse) + { + } + + public override bool TrySendHeaders(HttpContextBase context) + { + var response = context.Response; + + base.TrySendHeaders(context); + + response.StatusCode = (int)HttpStatusCode.NotModified; + response.StatusDescription = "Not Modified"; + + // Explicitly set the Content-Length header so the client doesn't wait for + // content but keeps the connection open for other requests + response.AddHeader("Content-Length", "0"); + + return true; + } + + public override void SendFile(HttpContextBase context) + { + // Don't send file, it is unmodified. Let browser fetch from its cache. + } + } + + + + internal sealed class FullFileResponder : FileResponder + { + public FullFileResponder(IFileResponse fileResponse) + : base(fileResponse) + { + } + + public override bool TrySendHeaders(HttpContextBase context) + { + base.TrySendHeaders(context); + + // The date the file was last modified + context.Response.Cache.SetLastModified(FileResponse.LastModifiedUtc); + + return true; + } + + public override void SendFile(HttpContextBase context) + { + var fileLength = FileResponse.FileLength ?? FileResponse.Transmitter.GetFileLength(); + FileResponse.Transmitter.TransmitFile(0, fileLength, fileLength, context); + } + } + + + + internal sealed class RangeFileResponder : FileResponder + { + internal struct ByteRange + { + internal long Offset; + internal long Length; + } + + private const string RANGE_BOUNDARY = ""; + private const string MULTIPART_CONTENT_TYPE = "multipart/byteranges; boundary="; + private const string MULTIPART_RANGE_DELIMITER = "--\r\n"; + private const string MULTIPART_RANGE_END = "----\r\n\r\n"; + private const string CONTENT_RANGE_FORMAT = "bytes {0}-{1}/{2}"; + private const int MAX_RANGE_ALLOWED = 5; + + private const int ERROR_ACCESS_DENIED = 5; + + private static readonly string[] _httpDateFormats = new string[] { "r", "dddd, dd-MMM-yy HH':'mm':'ss 'GMT'", "ddd MMM d HH':'mm':'ss yyyy" }; + + private readonly FileResponder _defaultResponder; + private readonly string _rangeHeader; + + public RangeFileResponder(IFileResponse fileResponse, string rangeHeader) + : base(fileResponse) + { + _defaultResponder = new FullFileResponder(fileResponse); + _rangeHeader = rangeHeader; + } + + public override bool TrySendHeaders(HttpContextBase context) + { + var fileLength = FileResponse.FileLength ?? FileResponse.Transmitter.GetFileLength(); + var etag = FileResponse.ETag; + var lastModified = FileResponse.LastModifiedUtc; + + var handled = ExecuteRangeRequest(context, fileLength, _rangeHeader, etag, lastModified); + if (!handled && _defaultResponder.TrySendHeaders(context)) + { + _defaultResponder.SendFile(context); + handled = true; + } + + return handled; + } + + public override void SendFile(HttpContextBase context) + { + // Do nothing here, we have handled everything in 'TrySendHeaders()' already + } + + #region Range Utils + + // Most of the helpers here were copied over from the internal StaticFileHandler.cs + + private bool ExecuteRangeRequest( + HttpContextBase context, + long fileLength, + string rangeHeader, + string etag, + DateTime lastModified) + { + var handled = false; + var request = context.Request; + var response = context.Response; + + // return "416 Requested range not satisfiable" if the file length is zero. + if (fileLength <= 0) + { + SendRangeNotSatisfiable(response, fileLength); + handled = true; + return handled; + } + + var ifRangeHeader = request.Headers["If-Range"]; + if (ifRangeHeader != null && ifRangeHeader.Length > 1) + { + // Is this an ETag or a Date? We only need to check two + // characters; an ETag either begins with W/ or it is quoted. + if (ifRangeHeader[0] == '"') + { + // it's a strong ETag + if (ifRangeHeader != etag) + { + // the etags do not match, and we will therefore return the entire response + return handled; + } + } + else if (ifRangeHeader[0] == 'W' && ifRangeHeader[1] == '/') + { + // it's a weak ETag, and is therefore not usable for sub-range retrieval and + // we will return the entire response + return handled; + } + else + { + // It's a date. If it is greater than or equal to the last-write time of the file, we can send the range. + if (IsOutDated(ifRangeHeader, lastModified)) + { + return handled; + } + } + } + + // the expected format is "bytes = [, , ...]" + // where is "-[]" or "-". + int indexOfEquals = rangeHeader.IndexOf('='); + if (indexOfEquals == -1 || indexOfEquals == rangeHeader.Length - 1) + { + // invalid syntax + return handled; + } + + int startIndex = indexOfEquals + 1; + bool isRangeHeaderSyntacticallyValid = true; + long offset; + long length; + bool isSatisfiable; + bool exceededMax = false; + ByteRange[] byteRanges = null; + int byteRangesCount = 0; + long totalBytes = 0; + while (startIndex < rangeHeader.Length && isRangeHeaderSyntacticallyValid) + { + isRangeHeaderSyntacticallyValid = GetNextRange(rangeHeader, ref startIndex, fileLength, out offset, out length, out isSatisfiable); + + if (!isRangeHeaderSyntacticallyValid) + { + break; + } + + if (!isSatisfiable) + { + continue; + } + + if (byteRanges == null) + { + byteRanges = new ByteRange[16]; + } + + if (byteRangesCount >= byteRanges.Length) + { + // grow byteRanges array + ByteRange[] buffer = new ByteRange[byteRanges.Length * 2]; + Array.Copy(byteRanges, buffer, byteRanges.Length); + byteRanges = buffer; + } + + byteRanges[byteRangesCount].Offset = offset; + byteRanges[byteRangesCount].Length = length; + byteRangesCount++; + + // IIS imposes this limitation too, and sends "400 Bad Request" if exceeded + totalBytes += length; + if (totalBytes > fileLength * MAX_RANGE_ALLOWED) + { + exceededMax = true; + break; + } + } + + if (!isRangeHeaderSyntacticallyValid) + { + return handled; + } + + if (exceededMax) + { + SendBadRequest(response); + handled = true; + return handled; + } + + if (byteRangesCount == 0) + { + // we parsed the Range header and found no satisfiable byte ranges, so return "416 Requested Range Not Satisfiable" + SendRangeNotSatisfiable(response, fileLength); + handled = true; + return handled; + } + + if (byteRangesCount == 1) + { + offset = byteRanges[0].Offset; + length = byteRanges[0].Length; + response.ContentType = FileResponse.ContentType; + string contentRange = String.Format(CultureInfo.InvariantCulture, CONTENT_RANGE_FORMAT, offset, offset + length - 1, fileLength); + response.AppendHeader("Content-Range", contentRange); + + SendResponseHeaders(); + FileResponse.Transmitter.TransmitFile(offset, length, fileLength, context); + } + else + { + response.ContentType = MULTIPART_CONTENT_TYPE; + string contentRange; + string partialContentType = "Content-Type: " + FileResponse.ContentType + "\r\n"; + + SendResponseHeaders(); + + for (int i = 0; i < byteRangesCount; i++) + { + offset = byteRanges[i].Offset; + length = byteRanges[i].Length; + response.Write(MULTIPART_RANGE_DELIMITER); + response.Write(partialContentType); + response.Write("Content-Range: "); + contentRange = String.Format(CultureInfo.InvariantCulture, CONTENT_RANGE_FORMAT, offset, offset + length - 1, fileLength); + response.Write(contentRange); + response.Write("\r\n\r\n"); + // Transmit + FileResponse.Transmitter.TransmitFile(offset, length, fileLength, context); + response.Write("\r\n"); + } + response.Write(MULTIPART_RANGE_END); + } + + handled = true; + return handled; + + void SendResponseHeaders() + { + // Send a "206 Partial Content" status + response.StatusCode = (int)HttpStatusCode.PartialContent; + response.AppendHeader("Accept-Ranges", "bytes"); + response.Cache.SetLastModified(lastModified); + response.Cache.SetETag(etag); + response.Cache.SetCacheability(HttpCacheability.Public); + } + } + + private void SendBadRequest(HttpResponseBase response) + { + response.StatusCode = (int)HttpStatusCode.BadRequest; + response.Write("Bad Request"); + } + + private void SendRangeNotSatisfiable(HttpResponseBase response, long fileLength) + { + response.StatusCode = (int)HttpStatusCode.RequestedRangeNotSatisfiable; + response.ContentType = null; + response.AppendHeader("Content-Range", "bytes */" + fileLength.ToString(NumberFormatInfo.InvariantInfo)); + } + + private static bool IsOutDated(string ifRangeHeader, DateTime lastModified) + { + try + { + var utcLastModified = lastModified.ToUniversalTime(); + var utc = UtcParse(ifRangeHeader); + return (utc < utcLastModified); + } + catch + { + return true; + } + } + + private static DateTime UtcParse(string time) + { + Guard.NotNull(time, nameof(time)); + + var dtStyles = DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal; + if (DateTime.TryParseExact(time, _httpDateFormats, null, dtStyles, out var utcDate)) + { + return utcDate; + } + + throw new FormatException($"{time} is an invalid date expression."); + } + + // initial space characters are skipped, and the string of digits up until the first non-digit + // are converted to a long. If digits are found the method returns true; otherwise false. + private static bool GetLongFromSubstring(string s, ref int startIndex, out long result) + { + result = 0; + + // get index of first digit + MovePastSpaceCharacters(s, ref startIndex); + int beginIndex = startIndex; + + // get index of last digit + MovePastDigits(s, ref startIndex); + int endIndex = startIndex - 1; + + // are there any digits? + if (endIndex < beginIndex) + { + return false; + } + + long multipleOfTen = 1; + for (int i = endIndex; i >= beginIndex; i--) + { + int digit = s[i] - '0'; + result += digit * multipleOfTen; + multipleOfTen *= 10; + // check for overflow + if (result < 0) + { + return false; + } + } + + return true; + } + + // The Range header consists of one or more byte range specifiers. E.g, "Range: bytes=0-1024,-1024" is a request + // for the first and last 1024 bytes of a file. Before this method is called, startIndex points to the beginning + // of a byte range specifier; and afterwards it points to the beginning of the next byte range specifier. + // If the current byte range specifier is syntactially inavlid, this function will return false indicating that the + // Range header must be ignored. If the function returns true, then the byte range specifier will be converted to + // an offset and length, and the startIndex will be incremented to the next byte range specifier. The byte range + // specifier (offset and length) returned by this function is satisfiable if and only if isSatisfiable is true. + private static bool GetNextRange(string rangeHeader, ref int startIndex, long fileLength, out long offset, out long length, out bool isSatisfiable) + { + // startIndex is first char after '=', or first char after ',' + Debug.Assert(startIndex < rangeHeader.Length, "startIndex < rangeHeader.Length"); + + offset = 0; + length = 0; + isSatisfiable = false; + + // A Range request to an empty file is never satisfiable, and will always receive a 416 status. + if (fileLength <= 0) + { + // put startIndex at end of string so we don't try to call GetNextRange again + startIndex = rangeHeader.Length; + return true; + } + + MovePastSpaceCharacters(rangeHeader, ref startIndex); + + if (startIndex < rangeHeader.Length && rangeHeader[startIndex] == '-') + { + // this range is of the form "-mmm" + startIndex++; + if (!GetLongFromSubstring(rangeHeader, ref startIndex, out length)) + { + return false; + } + + if (length > fileLength) + { + // send entire file + offset = 0; + length = fileLength; + } + else + { + // send last N bytes + offset = fileLength - length; + } + + isSatisfiable = IsRangeSatisfiable(offset, length, fileLength); + // we parsed the current range, but need to successfully move the startIndex to the next range + return IncrementToNextRange(rangeHeader, ref startIndex); + } + else + { + // this range is of the form "nnn-[mmm]" + if (!GetLongFromSubstring(rangeHeader, ref startIndex, out offset)) + { + return false; + } + + // increment startIndex past '-' + if (startIndex < rangeHeader.Length && rangeHeader[startIndex] == '-') + { + startIndex++; + } + else + { + return false; + } + + if (!GetLongFromSubstring(rangeHeader, ref startIndex, out long endPos)) + { + // assume range is of form "nnn-". If it isn't, + // the call to IncrementToNextRange will return false + length = fileLength - offset; + } + else + { + // if...greater than or equal to the current length of the entity-body, last-byte-pos + // is taken to be equal to one less than the current length of the entity- body in bytes. + if (endPos > fileLength - 1) + { + endPos = fileLength - 1; + } + + length = endPos - offset + 1; + + if (length < 1) + { + // the byte range specifier is syntactially invalid + // because the last-byte-pos < first-byte-pos + return false; + } + } + + isSatisfiable = IsRangeSatisfiable(offset, length, fileLength); + + // we parsed the current range, but need to successfully move the startIndex to the next range + return IncrementToNextRange(rangeHeader, ref startIndex); + } + } + + private static bool IncrementToNextRange(string s, ref int startIndex) + { + // increment startIndex until next token and return true, unless the syntax is invalid + MovePastSpaceCharacters(s, ref startIndex); + + if (startIndex < s.Length) + { + if (s[startIndex] != ',') + { + return false; + } + // move to first char after ',' + startIndex++; + } + + return true; + } + + private static bool IsRangeSatisfiable(long offset, long length, long fileLength) + { + return (offset < fileLength && length > 0); + } + + private static bool IsSecurityError(int ErrorCode) + { + return (ErrorCode == ERROR_ACCESS_DENIED); + } + + private static void MovePastSpaceCharacters(string s, ref int startIndex) + { + while (startIndex < s.Length && s[startIndex] == ' ') + { + startIndex++; + } + } + + private static void MovePastDigits(string s, ref int startIndex) + { + while (startIndex < s.Length && s[startIndex] <= '9' && s[startIndex] >= '0') + { + startIndex++; + } + } + + #endregion + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/FileTransmitter.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/FileTransmitter.cs new file mode 100644 index 0000000000..82b2399294 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/FileTransmitter.cs @@ -0,0 +1,116 @@ +using System; +using System.IO; +using System.Web; + +namespace SmartStore.Web.Framework.Modelling +{ + public abstract class FileTransmitter + { + public abstract long GetFileLength(); + public abstract void TransmitFile(long offset, long length, long fileLength, HttpContextBase context); + } + + public sealed class FileBufferTransmitter : FileTransmitter + { + private readonly Func _bufferFactory; + private byte[] _buffer; + + public FileBufferTransmitter(Func bufferFactory) + { + Guard.NotNull(bufferFactory, nameof(bufferFactory)); + _bufferFactory = bufferFactory; + } + + public override long GetFileLength() + { + return GetBuffer().LongLength; + } + + public override void TransmitFile(long offset, long length, long fileLength, HttpContextBase context) + { + context.Response.OutputStream.Write(GetBuffer(), (int)offset, (int)length); + } + + private byte[] GetBuffer() + { + if (_buffer == null) + { + _buffer = _bufferFactory(); + if (_buffer == null) + { + throw new NullReferenceException("File buffer cannot be NULL."); + } + } + + return _buffer; + } + } + + public sealed class FileStreamTransmitter : FileTransmitter + { + // Default buffer size as defined in BufferedStream type + const int DefaultBufferSize = 81920; + + private readonly Func _streamFactory; + private Stream _stream; + + public FileStreamTransmitter(Func streamFactory) + { + Guard.NotNull(streamFactory, nameof(streamFactory)); + _streamFactory = streamFactory; + } + + public override long GetFileLength() + { + return GetStream().Length; + } + + public override void TransmitFile(long offset, long length, long fileLength, HttpContextBase context) + { + var response = context.Response; + var stream = GetStream(); + + response.BufferOutput = false; + + using (stream) + { + int bufferSize = (int)Math.Min(DefaultBufferSize, length); + byte[] buffer = new byte[bufferSize]; + + int read; + long remaining = length; + + stream.Seek(offset, SeekOrigin.Begin); + while ((remaining > 0) && (read = stream.Read(buffer, 0, buffer.Length)) != 0) + { + if (response.IsClientConnected) + { + response.OutputStream.Write(buffer, 0, read); + //response.Flush(); + + remaining -= read; + } + else + { + remaining = 0; + break; + } + } + } + } + + private Stream GetStream() + { + if (_stream == null) + { + _stream = _streamFactory(); + if (_stream == null) + { + throw new NullReferenceException("File stream cannot be NULL."); + } + } + + return _stream; + } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Modelling/Results/IFileResponse.cs b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/IFileResponse.cs new file mode 100644 index 0000000000..f490fe8dac --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Modelling/Results/IFileResponse.cs @@ -0,0 +1,14 @@ +using System; + +namespace SmartStore.Web.Framework.Modelling +{ + public interface IFileResponse + { + string ContentType { get; } + long? FileLength { get; } + DateTime LastModifiedUtc { get; } + TimeSpan MaxAge { get; } + string ETag { get; } + FileTransmitter Transmitter { get; } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Security/RequireHttpsByConfigAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Security/RequireHttpsByConfigAttribute.cs deleted file mode 100644 index 4fa6c14d76..0000000000 --- a/src/Presentation/SmartStore.Web.Framework/Security/RequireHttpsByConfigAttribute.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Web.Mvc; -using SmartStore.Core; -using SmartStore.Core.Data; -using SmartStore.Core.Domain.Security; - -namespace SmartStore.Web.Framework.Security -{ - public class RequireHttpsByConfigAttribute : FilterAttribute, IAuthorizationFilter - { - public RequireHttpsByConfigAttribute(SslRequirement sslRequirement) - { - this.SslRequirement = sslRequirement; - } - - public Lazy SecuritySettings { get; set; } - public Lazy StoreContext { get; set; } - public Lazy WebHelper { get; set; } - - public virtual void OnAuthorization(AuthorizationContext filterContext) - { - // don't apply filter to child methods - if (filterContext.IsChildAction) - return; - - // only redirect for GET requests, - // otherwise the browser might not propagate the verb and request body correctly. - if (!String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) - return; - - if (!DataSettings.DatabaseIsInstalled()) - return; - - var securitySettings = SecuritySettings.Value; - var isLocalRequest = filterContext.HttpContext.Request.IsLocal; - - if (!securitySettings.UseSslOnLocalhost && isLocalRequest) - return; - - var webHelper = WebHelper.Value; - - var currentConnectionSecured = webHelper.IsCurrentConnectionSecured(); - var storeContext = StoreContext.Value; - var currentStore = storeContext.CurrentStore; - - if (currentStore.ForceSslForAllPages) - { - // all pages are forced to be SSL no matter of the specified value - this.SslRequirement = SslRequirement.Yes; - } - - switch (this.SslRequirement) - { - case SslRequirement.Yes: - { - if (!currentConnectionSecured) - { - if (currentStore != null && currentStore.GetSecurityMode() > HttpSecurityMode.Unsecured) - { - // redirect to HTTPS version of page - // string url = "https://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl; - - string url = webHelper.GetThisPageUrl(true, true); - filterContext.Result = new RedirectResult(url, !isLocalRequest); - } - } - } - break; - case SslRequirement.No: - { - if (currentConnectionSecured) - { - // 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, !isLocalRequest); - } - } - break; - case SslRequirement.Retain: - { - //do nothing - } - break; - default: - throw new SmartException("Unsupported SslRequirement parameter"); - } - } - - public SslRequirement SslRequirement { get; set; } - } -} diff --git a/src/Presentation/SmartStore.Web.Framework/Security/RewriteUrlAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Security/RewriteUrlAttribute.cs new file mode 100644 index 0000000000..f63ad4b5ba --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Security/RewriteUrlAttribute.cs @@ -0,0 +1,137 @@ +using System; +using System.Web.Mvc; +using SmartStore.Core; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Security; +using SmartStore.Core.Domain.Seo; + +namespace SmartStore.Web.Framework.Security +{ + public class RewriteUrlAttribute : FilterAttribute, IAuthorizationFilter + { + public RewriteUrlAttribute(SslRequirement sslRequirement) + { + this.SslRequirement = sslRequirement; + this.Order = 100; + } + + public Lazy SeoSettings { get; set; } + public Lazy SecuritySettings { get; set; } + public Lazy StoreContext { get; set; } + public Lazy WebHelper { get; set; } + + public virtual void OnAuthorization(AuthorizationContext filterContext) + { + // don't apply filter to child methods + if (filterContext.IsChildAction) + return; + + // only redirect for GET requests, + // otherwise the browser might not propagate the verb and request body correctly. + if (!String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) + return; + + if (!DataSettings.DatabaseIsInstalled()) + return; + + var securitySettings = SecuritySettings.Value; + var isLocalRequest = filterContext.HttpContext.Request.IsLocal; + + if (!securitySettings.UseSslOnLocalhost && isLocalRequest) + return; + + var webHelper = WebHelper.Value; + + var currentConnectionSecured = webHelper.IsCurrentConnectionSecured(); + var currentStore = StoreContext.Value.CurrentStore; + + if (currentStore.ForceSslForAllPages) + { + // all pages are forced to be SSL no matter of the specified value + this.SslRequirement = SslRequirement.Yes; + } + + var securityMode = currentStore.GetSecurityMode(); + + switch (this.SslRequirement) + { + case SslRequirement.Yes: + { + if (!currentConnectionSecured && securityMode > HttpSecurityMode.Unsecured) + { + // Redirect to HTTPS version of page + string url = webHelper.GetThisPageUrl(true, true); + filterContext.Result = new RedirectResult(url, !isLocalRequest); + } + } + break; + case SslRequirement.No: + { + if (currentConnectionSecured) + { + // Redirect to HTTP version of page + string url = webHelper.GetThisPageUrl(true, false); + filterContext.Result = new RedirectResult(url, !isLocalRequest); + } + } + break; + case SslRequirement.Retain: + { + // Do nothing + } + break; + default: + throw new SmartException("Unsupported SslRequirement parameter"); + } + + ApplyCanonicalHostNameRule(filterContext, isLocalRequest, securityMode); + } + + private void ApplyCanonicalHostNameRule(AuthorizationContext filterContext, bool isLocalRequest, HttpSecurityMode securityMode) + { + var rule = SeoSettings.Value.CanonicalHostNameRule; + if (rule == CanonicalHostNameRule.NoRule) + return; + + var uri = filterContext.Result != null + ? new Uri(((RedirectResult)filterContext.Result).Url) + : filterContext.HttpContext.Request.Url; + + if (uri.IsLoopback) + { + // allows testing of "localtest.me" + return; + } + + var url = uri.ToString(); + var protocol = uri.Scheme.ToLower(); + var isHttps = protocol.IsCaseInsensitiveEqual("https"); + var startsWith = "{0}://www.".FormatInvariant(protocol); + var hasPrefix = url.StartsWith(startsWith, StringComparison.OrdinalIgnoreCase); + + if (isHttps) + { + if (securityMode == HttpSecurityMode.SharedSsl) + { + // Don't attempt to redirect when shared SSL is being used and current request is secured. + // Redirecting from http://ssl.bla.com to https://www.ssl.bla.com will most probably fail. + return; + } + } + + if (hasPrefix && rule == CanonicalHostNameRule.OmitWww) + { + url = url.Replace("://www.", "://"); + filterContext.Result = new RedirectResult(url, !isLocalRequest); + } + + if (!hasPrefix && rule == CanonicalHostNameRule.RequireWww) + { + url = url.Replace("{0}://".FormatInvariant(protocol), startsWith); + filterContext.Result = new RedirectResult(url, !isLocalRequest); + } + } + + public SslRequirement SslRequirement { get; set; } + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Seo/CanonicalHostNameAttribute.cs b/src/Presentation/SmartStore.Web.Framework/Seo/CanonicalHostNameAttribute.cs deleted file mode 100644 index 2c9188b241..0000000000 --- a/src/Presentation/SmartStore.Web.Framework/Seo/CanonicalHostNameAttribute.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Web.Mvc; -using SmartStore.Core; -using SmartStore.Core.Data; -using SmartStore.Core.Domain.Seo; -using SmartStore.Core.Infrastructure; - -namespace SmartStore.Web.Framework.Seo -{ - public class CanonicalHostNameAttribute : FilterAttribute, IAuthorizationFilter - { - public Lazy SeoSettings { get; set; } - public Lazy WebHelper { get; set; } - public Lazy StoreContext { get; set; } - - public virtual void OnAuthorization(AuthorizationContext filterContext) - { - if (filterContext == null) - throw new ArgumentNullException("filterContext"); - - if (filterContext.IsChildAction) - return; - - // only redirect for GET requests - if (!String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) - return; - - if (!DataSettings.DatabaseIsInstalled()) - return; - - var rule = SeoSettings.Value.CanonicalHostNameRule; - if (rule == CanonicalHostNameRule.NoRule) - return; - - var uri = filterContext.HttpContext.Request.Url; - - if (uri.IsLoopback) - { - // allows testing of "localtest.me" - return; - } - - var url = uri.ToString(); - var protocol = uri.Scheme.ToLower(); - var isHttps = protocol.IsCaseInsensitiveEqual("https"); - var startsWith = "{0}://www.".FormatInvariant(protocol); - var hasPrefix = url.StartsWith(startsWith, StringComparison.OrdinalIgnoreCase); - - if (isHttps) - { - var securityMode = StoreContext.Value.CurrentStore.GetSecurityMode(); - if (securityMode == HttpSecurityMode.SharedSsl) - { - // Don't attempt to redirect when shared SSL is being used and current request is secured. - // Redirecting from http://ssl.bla.com to https://www.ssl.bla.com will most probably fail. - return; - } - } - - if (hasPrefix && rule == CanonicalHostNameRule.OmitWww) - { - url = url.Replace("://www.", "://"); - filterContext.Result = new RedirectResult(url, true); - } - - if (!hasPrefix && rule == CanonicalHostNameRule.RequireWww) - { - url = url.Replace("{0}://".FormatInvariant(protocol), startsWith); - filterContext.Result = new RedirectResult(url, true); - } - - } - - } -} diff --git a/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj b/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj index 6e1740f62d..bcf02806b0 100644 --- a/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj +++ b/src/Presentation/SmartStore.Web.Framework/SmartStore.Web.Framework.csproj @@ -1,5 +1,6 @@  - + + Debug AnyCPU @@ -227,6 +228,9 @@ + + + @@ -282,7 +286,6 @@ - @@ -290,6 +293,7 @@ + @@ -402,7 +406,7 @@ - + diff --git a/src/Presentation/SmartStore.Web.Framework/SmartUrlRoutingModule.cs b/src/Presentation/SmartStore.Web.Framework/SmartUrlRoutingModule.cs index 26e9c264c2..67097db01a 100644 --- a/src/Presentation/SmartStore.Web.Framework/SmartUrlRoutingModule.cs +++ b/src/Presentation/SmartStore.Web.Framework/SmartUrlRoutingModule.cs @@ -14,19 +14,10 @@ using SmartStore.Core.Infrastructure; using SmartStore.Core.Events; using SmartStore.Core.Data; +using SmartStore.Collections; namespace SmartStore.Web.Framework { - public class HttpModuleInitializedEvent - { - public HttpModuleInitializedEvent(HttpApplication application) - { - Application = application; - } - - public HttpApplication Application { get; private set; } - } - /// /// Request event sequence: /// - BeginRequest @@ -55,7 +46,12 @@ public HttpModuleInitializedEvent(HttpApplication application) public class SmartUrlRoutingModule : IHttpModule { private static readonly object _contextKey = new object(); - private static readonly ConcurrentBag _routes = new ConcurrentBag(); + + private static readonly ICollection> _actions = + new SyncedCollection>(new List>()) { ReadLockFree = true }; + + private static readonly ICollection _routes = + new SyncedCollection(new List()) { ReadLockFree = true }; static SmartUrlRoutingModule() { @@ -92,15 +88,32 @@ public void Init(HttpApplication application) // 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)); + foreach (var action in _actions) + { + action(application); + } + + // Set app to fully initialized state on very first request + EngineContext.Current.IsFullyInitialized = true; } } + /// + /// Registers an action that is called on application init. Call this to register HTTP request lifecycle callbacks / event handlers. + /// + /// Action + public static void RegisterAction(Action action) + { + Guard.NotNull(action, nameof(action)); + + _actions.Add(action); + } + /// /// Registers a path pattern which should be handled by the /// diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/BundlingVirtualPathProvider.cs b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/BundlingVirtualPathProvider.cs index 92248de62a..edb20c5c8b 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/BundlingVirtualPathProvider.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/BundlingVirtualPathProvider.cs @@ -77,80 +77,20 @@ public override VirtualFile GetFile(string virtualPath) return base.GetFile(virtualPath); } - public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies) - { - var styleResult = ThemeHelper.IsStyleSheet(virtualPath); - if (styleResult.IsPreprocessor && !(styleResult.IsThemeVars || styleResult.IsModuleImports) && virtualPathDependencies != null) - { - // Exclude the special imports from the file dependencies list - return base.GetFileHash(virtualPath, ThemeHelper.RemoveVirtualImports(virtualPathDependencies.Cast())); - } - - return base.GetFileHash(virtualPath, virtualPathDependencies); - } - public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart) { - var styleResult = ThemeHelper.IsStyleSheet(virtualPath); - - if (styleResult == null || styleResult.IsCss) - { - return base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart); - } - - if (styleResult.IsThemeVars || styleResult.IsModuleImports) - { - return null; - } - - // Is Sass Or Less Or StyleBundle - - var arrPathDependencies = virtualPathDependencies.Cast().ToArray(); - - - // Exclude the special imports from the file dependencies list, - // 'cause this one cannot be monitored by the physical file system - var fileDependencies = ThemeHelper.RemoveVirtualImports(arrPathDependencies); - - if (fileDependencies == arrPathDependencies) + if (ThemeHelper.IsStyleValidationRequest()) { - // No themevars or moduleimports import... so no special considerations here - return base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart); - } - - if (fileDependencies.Any()) - { - string cacheKey = null; - - var isThemeableAsset = (!styleResult.IsBundle && ThemeHelper.PathIsInheritableThemeFile(virtualPath)) - || (styleResult.IsBundle && fileDependencies.Any(x => ThemeHelper.PathIsInheritableThemeFile(x))); - - if (isThemeableAsset) + var themeVarsPath = virtualPathDependencies.Cast().FirstOrDefault(x => ThemeHelper.PathIsThemeVars(x)); + if (themeVarsPath.HasValue()) { - var theme = ThemeHelper.ResolveCurrentTheme(); - int storeId = ThemeHelper.ResolveCurrentStoreId(); - // invalidate the cache when variables change - cacheKey = FrameworkCacheConsumer.BuildThemeVarsCacheKey(theme.ThemeName, storeId); - - if (styleResult.IsSass && (ThemeHelper.IsStyleValidationRequest())) - { - // Special case: ensure that cached validation result gets nuked in a while, - // when ThemeVariableService publishes the entity changed messages. - return new CacheDependency(new string[0], new string[] { cacheKey }, utcStart); - } + return base.GetCacheDependency(virtualPath, new[] { themeVarsPath }, utcStart); } + } - var files = ThemingVirtualPathProvider.MapDependencyPaths(fileDependencies); - - return new CacheDependency( - files, - cacheKey == null ? new string[0] : new string[] { cacheKey }, - utcStart); - } - - return null; + return base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart); } - } + } internal class SassCheckedPathStack { diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/DefaultAssetCache.cs b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/DefaultAssetCache.cs index ede36d2153..f1eaf270da 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/DefaultAssetCache.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/DefaultAssetCache.cs @@ -81,10 +81,8 @@ public CachedAssetEntry GetAsset(string virtualPath) Guard.NotEmpty(virtualPath, nameof(virtualPath)); - string themeName; - int storeId; - var cacheDirectoryName = ResolveCacheDirectoryName(virtualPath, out themeName, out storeId); - + var cacheDirectoryName = ResolveCacheDirectoryName(virtualPath, out string themeName, out int storeId); + if (CacheFolder.DirectoryExists(cacheDirectoryName)) { try @@ -92,10 +90,7 @@ public CachedAssetEntry GetAsset(string virtualPath) var deps = CacheFolder.ReadFile(CacheFolder.Combine(cacheDirectoryName, "asset.dependencies")); var hash = CacheFolder.ReadFile(CacheFolder.Combine(cacheDirectoryName, "asset.hash")); - IEnumerable parsedDeps; - string currentHash; - - if (!TryValidate(virtualPath, deps, hash, out parsedDeps, out currentHash)) + if (!TryValidate(virtualPath, deps, hash, out IEnumerable parsedDeps, out string currentHash)) { Logger.DebugFormat("Invalidating cached asset for '{0}' because it is not valid anymore.", virtualPath); InvalidateAssetInternal(cacheDirectoryName, themeName, storeId); diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/ModuleImportsVirtualFile.cs b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/ModuleImportsVirtualFile.cs index 720b884ee4..34b4557fe5 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/Assets/ModuleImportsVirtualFile.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/Assets/ModuleImportsVirtualFile.cs @@ -5,18 +5,46 @@ using System.Web.Hosting; using SmartStore.Core.Plugins; using SmartStore.Core.Data; +using System; +using SmartStore.Utilities; namespace SmartStore.Web.Framework.Theming.Assets { - public class ModuleImportsVirtualFile : VirtualFile + public sealed class ModuleImport { - private static readonly HashSet _adminImports; - private static readonly HashSet _publicImports; + public string VirtualPath { get; internal set; } + public string PhysicalPath { get; internal set; } + public PluginDescriptor PluginDescriptor { get; internal set; } + public bool IsAdmin { get; internal set; } + + public override string ToString() + { + return VirtualPath; + } + + public override int GetHashCode() + { + return VirtualPath.GetHashCode(); + } + + public override bool Equals(object obj) + { + if (!(obj is ModuleImport other)) + return false; + + return string.Equals(this.VirtualPath, other.VirtualPath, StringComparison.OrdinalIgnoreCase); + } + } + + public class ModuleImportsVirtualFile : VirtualFile, IFileDependencyProvider + { + private static readonly HashSet _adminImports; + private static readonly HashSet _publicImports; static ModuleImportsVirtualFile() { - _adminImports = new HashSet(); - _publicImports = new HashSet(); + _adminImports = new HashSet(); + _publicImports = new HashSet(); if (DataSettings.DatabaseIsInstalled()) { @@ -35,24 +63,40 @@ private static void CollectModuleImports() if (!Directory.Exists(contentDir)) continue; - if (File.Exists(Path.Combine(contentDir, "public.scss"))) - { - _publicImports.Add($"{root}/{plugin.FolderName}/Content/public.scss"); - } + TryAddImport(plugin, _publicImports, "public.scss"); + TryAddImport(plugin, _adminImports, "admin.scss"); + } - if (File.Exists(Path.Combine(contentDir, "admin.scss"))) + void TryAddImport(PluginDescriptor plugin, HashSet imports, string name) + { + var physicalPath = Path.Combine(plugin.PhysicalPath, "Content", name); + if (File.Exists(physicalPath)) { - _adminImports.Add($"{root}/{plugin.FolderName}/Content/admin.scss"); + imports.Add(new ModuleImport + { + PhysicalPath = physicalPath, + VirtualPath = $"{root}/{plugin.FolderName}/Content/{name}", + PluginDescriptor = plugin, + IsAdmin = name.Contains("admin") + }); } } } - private readonly bool _isAdmin; + public static ModuleImport[] PublicImports + { + get => _publicImports.ToArray(); + } + + public static ModuleImport[] AdminImports + { + get => _adminImports.ToArray(); + } public ModuleImportsVirtualFile(string virtualPath, bool isAdmin) : base(virtualPath) { - _isAdmin = isAdmin; + IsAdmin = isAdmin; } public override bool IsDirectory @@ -60,19 +104,36 @@ public override bool IsDirectory get { return false; } } + public bool IsAdmin + { + get; + private set; + } + public override Stream Open() { var sb = new StringBuilder(); - var imports = _isAdmin ? _adminImports : _publicImports; + var imports = IsAdmin ? _adminImports : _publicImports; foreach (var imp in imports) { - sb.AppendLine($"@import '{imp}';"); + sb.AppendLine($"@import '{imp.VirtualPath}';"); } return GenerateStreamFromString(sb.ToString()); } + public void HashCombine(HashCodeCombiner combiner) + { + Guard.NotNull(combiner, nameof(combiner)); + + var imports = IsAdmin ? _adminImports : _publicImports; + foreach (var imp in imports) + { + combiner.Add(new FileInfo(imp.PhysicalPath)); + } + } + private Stream GenerateStreamFromString(string value) { var stream = new MemoryStream(); @@ -86,5 +147,13 @@ private Stream GenerateStreamFromString(string value) } } + public void AddFileDependencies(ICollection mappedPaths, ICollection cacheKeys) + { + var imports = IsAdmin ? _adminImports : _publicImports; + foreach (var imp in imports) + { + mappedPaths.Add(imp.PhysicalPath); + } + } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/CssHttpHandler.cs b/src/Presentation/SmartStore.Web.Framework/Theming/CssHttpHandler.cs index 3a1cc2ac7c..e2fd7aa151 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/CssHttpHandler.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/CssHttpHandler.cs @@ -69,8 +69,7 @@ protected override string GetCacheKey(string assetVirtualPath, string bundleVirt if (IsThemeableRequest()) { - var httpContext = HttpContext.Current; - if (httpContext != null && httpContext.Request != null) + if (HttpContext.Current?.Request != null) { var qs = QueryString.Current; if (qs.Count > 0) diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/IFileDependencyProvider.cs b/src/Presentation/SmartStore.Web.Framework/Theming/IFileDependencyProvider.cs new file mode 100644 index 0000000000..e36f9c9fd4 --- /dev/null +++ b/src/Presentation/SmartStore.Web.Framework/Theming/IFileDependencyProvider.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace SmartStore.Web.Framework.Theming +{ + internal interface IFileDependencyProvider + { + void AddFileDependencies(ICollection mappedPaths, ICollection cacheKeys); + } +} diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/InheritedVirtualThemeFile.cs b/src/Presentation/SmartStore.Web.Framework/Theming/InheritedVirtualThemeFile.cs index 37bb50a4b2..ad49bed1d2 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/InheritedVirtualThemeFile.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/InheritedVirtualThemeFile.cs @@ -1,11 +1,12 @@ using System; +using System.Collections.Generic; using System.IO; using System.Web.Hosting; using SmartStore.Core.Themes; namespace SmartStore.Web.Framework.Theming { - internal class InheritedVirtualThemeFile : VirtualFile + internal class InheritedVirtualThemeFile : VirtualFile, IFileDependencyProvider { public InheritedVirtualThemeFile(InheritedThemeFileResult resolveResult) : base(DetermineVirtualPath(resolveResult)) @@ -39,5 +40,9 @@ private static string DetermineVirtualPath(InheritedThemeFileResult resolveResul } } - } + public void AddFileDependencies(ICollection mappedPaths, ICollection cacheKeys) + { + mappedPaths.Add(ResolveResult.ResultPhysicalPath); + } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/SmartVirtualPathProvider.cs b/src/Presentation/SmartStore.Web.Framework/Theming/SmartVirtualPathProvider.cs index 0c9232dfb6..32c6d31fe8 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/SmartVirtualPathProvider.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/SmartVirtualPathProvider.cs @@ -146,7 +146,7 @@ private bool IsExtensionPath(string virtualPath, out string root, out string app } } - internal class DebugVirtualFile : VirtualFile + internal class DebugVirtualFile : VirtualFile, IFileDependencyProvider { public DebugVirtualFile(string virtualPath, string debugPath) : base(virtualPath) @@ -160,11 +160,16 @@ public override bool IsDirectory { get { return false; } } - + public override Stream Open() { var fileStream = new FileStream(PhysicalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); return fileStream; } + + public void AddFileDependencies(ICollection mappedPaths, ICollection cacheKeys) + { + mappedPaths.Add(PhysicalPath); + } } } diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHelper.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHelper.cs index eca7a0bf9f..074b0e48cb 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHelper.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeHelper.cs @@ -37,7 +37,7 @@ internal static IEnumerable RemoveVirtualImports(IEnumerable vir { Guard.NotNull(virtualPathDependencies, nameof(virtualPathDependencies)); - // determine the virtual themevarsscss import reference + // determine the virtual themevars scss import reference var themeVarsFile = virtualPathDependencies.Where(x => ThemeHelper.PathIsThemeVars(x)).FirstOrDefault(); var moduleImportsFile = virtualPathDependencies.Where(x => ThemeHelper.PathIsModuleImports(x)).FirstOrDefault(); diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeVarsVirtualFile.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeVarsVirtualFile.cs index 7ab2289ec8..b4bd7ab49b 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemeVarsVirtualFile.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemeVarsVirtualFile.cs @@ -1,15 +1,12 @@ +using System.Collections.Generic; using System.IO; using System.Text; using System.Web.Hosting; namespace SmartStore.Web.Framework.Theming { - public class ThemeVarsVirtualFile : VirtualFile + public class ThemeVarsVirtualFile : VirtualFile, IFileDependencyProvider { - private readonly string _extension; - private readonly string _themeName; - private readonly int _storeId; - public ThemeVarsVirtualFile(string virtualPath, string themeName, int storeId) : this(virtualPath, Path.GetExtension(virtualPath), themeName, storeId) { @@ -18,24 +15,28 @@ public ThemeVarsVirtualFile(string virtualPath, string themeName, int storeId) internal ThemeVarsVirtualFile(string virtualPath, string extension, string themeName, int storeId) : base(virtualPath) { - _extension = extension; - _themeName = themeName; - _storeId = storeId; + Extension = extension; + ThemeName = themeName; + StoreId = storeId; } public override bool IsDirectory { get { return false; } } - - public override Stream Open() + + public string Extension { get; private set; } + public string ThemeName { get; private set; } + public int StoreId { get; private set; } + + public override Stream Open() { var repo = new ThemeVarsRepository(); - if (_themeName.IsEmpty()) + if (ThemeName.IsEmpty()) return GenerateStreamFromString(string.Empty); - var css = repo.GetPreprocessorCss(_extension, _themeName, _storeId); + var css = repo.GetPreprocessorCss(Extension, ThemeName, StoreId); return GenerateStreamFromString(css); } @@ -52,5 +53,9 @@ private Stream GenerateStreamFromString(string value) } } - } + public void AddFileDependencies(ICollection mappedPaths, ICollection cacheKeys) + { + cacheKeys.Add(FrameworkCacheConsumer.BuildThemeVarsCacheKey(ThemeName, StoreId)); + } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/ThemingVirtualPathProvider.cs b/src/Presentation/SmartStore.Web.Framework/Theming/ThemingVirtualPathProvider.cs index af9671169e..cef62c8959 100644 --- a/src/Presentation/SmartStore.Web.Framework/Theming/ThemingVirtualPathProvider.cs +++ b/src/Presentation/SmartStore.Web.Framework/Theming/ThemingVirtualPathProvider.cs @@ -8,6 +8,7 @@ using SmartStore.Core.Infrastructure; using SmartStore.Core.Themes; using SmartStore.Utilities; +using SmartStore.Web.Framework.Theming.Assets; namespace SmartStore.Web.Framework.Theming { @@ -89,7 +90,7 @@ public override string GetFileHash(string virtualPath, IEnumerable virtualPathDe return _previous.GetFileHash(virtualPath, virtualPathDependencies); } - var fileNames = MapDependencyPaths(virtualPathDependencies.Cast()); + var fileNames = MapDependencyPaths(virtualPathDependencies.Cast(), out _); var combiner = HashCodeCombiner.Start(); foreach (var fileName in fileNames) @@ -107,40 +108,41 @@ public override CacheDependency GetCacheDependency(string virtualPath, IEnumerab return null; } - return new CacheDependency(MapDependencyPaths(virtualPathDependencies.Cast()), utcStart); + var mappedPaths = MapDependencyPaths(virtualPathDependencies.Cast(), out var cacheKeys); + + return new CacheDependency(mappedPaths, cacheKeys, utcStart); } - internal static string[] MapDependencyPaths(IEnumerable virtualPathDependencies) + /// + /// Maps virtual to physical paths. Used to compute cache dependecies and file hashes. + /// + internal string[] MapDependencyPaths(IEnumerable virtualPathDependencies, out string[] cacheKeys) { - // Maps virtual to physical paths. Used to compute cache dependecies and file hashes. + cacheKeys = null; - var fileNames = new List(); + var mappedPaths = new HashSet(StringComparer.OrdinalIgnoreCase); + var cacheKeySet = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var dep in virtualPathDependencies) { - string mappedPath = null; - var file = HostingEnvironment.VirtualPathProvider.GetFile(dep); + var file = GetFile(dep); - if (file is InheritedVirtualThemeFile file1) - { - mappedPath = file1.ResolveResult.ResultPhysicalPath; - } - else if (file is DebugVirtualFile file2) + if (file is IFileDependencyProvider provider) { - mappedPath = file2.PhysicalPath; + provider.AddFileDependencies(mappedPaths, cacheKeySet); } else if (file != null) { - mappedPath = HostingEnvironment.MapPath(file.VirtualPath); - } - - if (mappedPath.HasValue()) - { - fileNames.Add(mappedPath); + mappedPaths.Add(HostingEnvironment.MapPath(file.VirtualPath)); } } - return fileNames.ToArray(); + cacheKeys = cacheKeySet.ToArray(); + + var paths = mappedPaths.ToArray(); + Array.Sort(paths); + + return paths; } private static InheritedThemeFileResult GetResolveResult(string virtualPath) diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/HtmlHelperExtensions.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/HtmlHelperExtensions.cs index b9382d6e5b..2d8e269b44 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/HtmlHelperExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/HtmlHelperExtensions.cs @@ -41,8 +41,16 @@ private static void RenderAttrs(HtmlHelper html, IDictionary att } var first = true; - foreach (var pair in attrs.Where(x => x.Value != null)) + + foreach (var key in attrs.Keys) { + var value = attrs[key]; + + if (value == null) + { + continue; + } + if (!first) { output.Write(" "); @@ -50,8 +58,8 @@ private static void RenderAttrs(HtmlHelper html, IDictionary att first = false; - output.Write(pair.Key + "=\""); - HttpUtility.HtmlAttributeEncode(pair.Value.ToString(), output); + output.Write(key + "=\""); + HttpUtility.HtmlAttributeEncode(value.ToString(), output); output.Write("\""); } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuExtensions.cs b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuExtensions.cs index 94ae537fc3..6284b9b052 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuExtensions.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Web.Mvc; +using System.Web.Routing; +using Newtonsoft.Json; using SmartStore.Collections; using SmartStore.Core.Domain.Cms; using SmartStore.Core.Localization; @@ -92,6 +94,35 @@ public static NodePathState GetNodePathState(this TreeNode node, IList return state; } + /// + /// Applies serialized route informations to a tree node. + /// + /// Tree node. + /// JSON serialized route data. + public static void ApplyRouteData(this TreeNode node, string data) + { + if (data.HasValue()) + { + var routeValues = JsonConvert.DeserializeObject(data); + var routeName = string.Empty; + + if (routeValues.TryGetValue("routename", out var val)) + { + routeName = val as string; + routeValues.Remove("routename"); + } + + if (routeName.HasValue()) + { + node.Value.Route(routeName, routeValues); + } + else + { + node.Value.Action(routeValues); + } + } + } + /// /// Creates a menu model. /// 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 dd5f21f542..bccc320ead 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuItem.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Components/Menu/MenuItem.cs @@ -19,6 +19,8 @@ public MenuItem() public int EntityId { get; set; } public string EntityName { get; set; } + public int MenuItemId { get; set; } + /// /// The total count of contained elements (like the count of products within a category) /// diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Extensions/LayoutExtensions.cs b/src/Presentation/SmartStore.Web.Framework/UI/Extensions/LayoutExtensions.cs index d8e2cfa3fc..e6b1186294 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Extensions/LayoutExtensions.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Extensions/LayoutExtensions.cs @@ -13,13 +13,13 @@ public static class LayoutExtensions public static void AddTitleParts(this HtmlHelper html, params string[] parts) { - var pageAssetsBuilder = EngineContext.Current.Resolve(); + var pageAssetsBuilder = EngineContext.Current.Resolve(); pageAssetsBuilder.AddTitleParts(parts); } public static void AppendTitleParts(this HtmlHelper html, params string[] parts) { - var pageAssetsBuilder = EngineContext.Current.Resolve(); + var pageAssetsBuilder = EngineContext.Current.Resolve(); pageAssetsBuilder.AppendTitleParts(parts); } @@ -27,7 +27,7 @@ public static MvcHtmlString SmartTitle(this HtmlHelper html, bool addDefaultTitl { var pageAssetsBuilder = EngineContext.Current.Resolve(); html.AppendTitleParts(parts); - return MvcHtmlString.Create(html.Encode(pageAssetsBuilder.GenerateTitle(addDefaultTitle))); + return MvcHtmlString.Create(pageAssetsBuilder.GenerateTitle(addDefaultTitle)); } #endregion @@ -50,7 +50,7 @@ public static MvcHtmlString SmartMetaDescription(this HtmlHelper html, params st { var pageAssetsBuilder = EngineContext.Current.Resolve(); html.AppendMetaDescriptionParts(parts); - return MvcHtmlString.Create(html.Encode(pageAssetsBuilder.GenerateMetaDescription())); + return MvcHtmlString.Create(pageAssetsBuilder.GenerateMetaDescription()); } #endregion @@ -73,7 +73,7 @@ public static MvcHtmlString SmartMetaKeywords(this HtmlHelper html, params strin { var pageAssetsBuilder = EngineContext.Current.Resolve(); html.AppendMetaKeywordParts(parts); - return MvcHtmlString.Create(html.Encode(pageAssetsBuilder.GenerateMetaKeywords())); + return MvcHtmlString.Create(pageAssetsBuilder.GenerateMetaKeywords()); } #endregion @@ -309,7 +309,7 @@ public static void AddBodyCssClass(this HtmlHelper html, string cssClassName) public static MvcHtmlString BodyCssClass(this HtmlHelper html) { var pageAssetsBuilder = EngineContext.Current.Resolve(); - return MvcHtmlString.Create(html.Encode(pageAssetsBuilder.GenerateBodyCssClasses())); + return MvcHtmlString.Create(pageAssetsBuilder.GenerateBodyCssClasses()); } public static void SetHtmlId(this HtmlHelper html, string htmlId) @@ -321,7 +321,7 @@ public static void SetHtmlId(this HtmlHelper html, string htmlId) public static MvcHtmlString HtmlId(this HtmlHelper html) { var pageAssetsBuilder = EngineContext.Current.Resolve(); - var result = html.Encode(pageAssetsBuilder.GenerateHtmlId()); + var result = pageAssetsBuilder.GenerateHtmlId(); try { diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/CatalogMenuInvalidator.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/CatalogMenuInvalidator.cs index 8769f1891d..d9e8273392 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/CatalogMenuInvalidator.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/CatalogMenuInvalidator.cs @@ -40,7 +40,11 @@ public CatalogMenuInvalidator( public async Task HandleAsync(CategoryTreeChangedEvent eventMessage) { - var affectedMenuNames = await GetAffectedMenuNamesAsync(); + var affectedMenuNames = await _rs.TableUntracked + .Where(x => x.ProviderName == "catalog") + .Select(x => x.Menu.SystemName) + .Distinct() + .ToListAsync(); foreach (var menuName in affectedMenuNames) { @@ -57,15 +61,6 @@ public async Task HandleAsync(CategoryTreeChangedEvent eventMessage) } } - private async Task> GetAffectedMenuNamesAsync() - { - return await _rs.TableUntracked - .Where(x => x.ProviderName == "catalog") - .Select(x => x.Menu.SystemName) - .Distinct() - .ToListAsync(); - } - private void Invalidate(string menuName) { if (!_invalidated.Contains(menuName)) diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/CatalogMenuItemProvider.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/CatalogMenuItemProvider.cs index dfc8526743..259965448d 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/CatalogMenuItemProvider.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/CatalogMenuItemProvider.cs @@ -5,9 +5,11 @@ using SmartStore.Core.Localization; using SmartStore.Services; using SmartStore.Services.Catalog; +using SmartStore.Services.Cms; using SmartStore.Services.Localization; using SmartStore.Services.Media; using SmartStore.Services.Seo; +using SmartStore.Utilities; namespace SmartStore.Web.Framework.UI { @@ -17,15 +19,18 @@ public class CatalogMenuItemProvider : MenuItemProviderBase private readonly ICommonServices _services; private readonly ICategoryService _categoryService; private readonly IPictureService _pictureService; + private readonly ILinkResolver _linkResolver; public CatalogMenuItemProvider( ICommonServices services, ICategoryService categoryService, - IPictureService pictureService) + IPictureService pictureService, + ILinkResolver linkResolver) { _services = services; _categoryService = categoryService; _pictureService = pictureService; + _linkResolver = linkResolver; T = NullLocalizer.Instance; } @@ -47,6 +52,7 @@ public override TreeNode Append(MenuItemProviderRequest request) var tree = _categoryService.GetCategoryTree(0, false, _services.StoreContext.CurrentStore.Id); var allPictureIds = tree.Flatten().Select(x => x.PictureId.GetValueOrDefault()); var allPictureInfos = _pictureService.GetPictureInfos(allPictureIds); + var randomId = CommonHelper.GenerateRandomInteger(0, 1000000); if (request.Entity.BeginGroup) { @@ -60,7 +66,7 @@ public override TreeNode Append(MenuItemProviderRequest request) // Do not append the root itself. foreach (var child in tree.Children) { - AppendToParent(request, ConvertNode(request, child, allPictureInfos)); + AppendToParent(request, ConvertNode(request, child, allPictureInfos, ref randomId)); } } @@ -80,30 +86,52 @@ protected override void ApplyLink(MenuItemProviderRequest request, TreeNode ConvertNode( MenuItemProviderRequest request, TreeNode categoryNode, - IDictionary allPictureInfos) + IDictionary allPictureInfos, + ref int randomId) { var node = categoryNode.Value; var name = node.Id > 0 ? node.GetLocalized(x => x.Name) : null; var menuItem = new MenuItem { + Id = randomId++.ToString(), EntityId = node.Id, EntityName = nameof(Category), + MenuItemId = request.Entity.Id, Text = name?.Value ?? node.Name, Rtl = name?.CurrentLanguage?.Rtl ?? false, BadgeText = node.Id > 0 ? node.GetLocalized(x => x.BadgeText) : null, BadgeStyle = (BadgeStyle)node.BadgeStyle, - RouteName = node.Id > 0 ? "Category" : "HomePage" + RouteName = node.Id > 0 ? "Category" : "HomePage", }; - if (node.Id > 0) + // Handle external link + if (node.ExternalLink.HasValue()) { - menuItem.RouteValues.Add("SeName", node.GetSeName()); + var link = _linkResolver.Resolve(node.ExternalLink); + if (link.Status == LinkStatus.Ok) + { + menuItem.Url = link.Link; + } + } - if (node.ParentCategoryId == 0 && node.Published && node.PictureId != null) + if (menuItem.Url.IsEmpty()) + { + if (node.Id > 0) { - menuItem.ImageId = node.PictureId; + menuItem.RouteName = "Category"; + menuItem.RouteValues.Add("SeName", node.GetSeName()); } + else + { + menuItem.RouteName = "HomePage"; + } + } + + // Picture + if (node.Id > 0 && node.ParentCategoryId == 0 && node.Published && node.PictureId != null) + { + menuItem.ImageId = node.PictureId; } // Apply inheritable properties. @@ -129,7 +157,7 @@ private TreeNode ConvertNode( { foreach (var childNode in categoryNode.Children) { - convertedNode.Append(ConvertNode(request, childNode, allPictureInfos)); + convertedNode.Append(ConvertNode(request, childNode, allPictureInfos, ref randomId)); } } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/MenuItemProviderBase.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/MenuItemProviderBase.cs index e8b87fceb5..c46dca62ba 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/MenuItemProviderBase.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/MenuItemProviderBase.cs @@ -42,7 +42,7 @@ protected virtual TreeNode AppendToParent(MenuItemProviderRequest requ var root = request.Parent.Root; var providers = root.GetMetadata>("Providers"); var provider = request.Entity.ProviderName; - + node.SetMetadata("Provider", provider); if (providers == null) @@ -54,6 +54,11 @@ protected virtual TreeNode AppendToParent(MenuItemProviderRequest requ providers.Add(provider); } + if (node.Id == null && node.Value.Id.HasValue()) + { + node.Id = node.Value.Id; + } + return request.Parent.Append(node); } @@ -72,7 +77,9 @@ protected virtual MenuItem ConvertToMenuItem(MenuItemProviderRequest request) { EntityId = entity.Id, EntityName = nameof(MenuItemRecord), + MenuItemId = entity.Id, Text = title, + Summary = shortDescription, Visible = entity.Published, Rtl = title?.CurrentLanguage?.Rtl ?? false, PermissionNames = entity.PermissionNames @@ -101,6 +108,7 @@ protected virtual MenuItem ConvertToMenuItem(MenuItemProviderRequest request) if (entity.HtmlId.HasValue()) { menuItem.LinkHtmlAttributes.Add("id", entity.HtmlId); + menuItem.Id = entity.HtmlId; } if (entity.Icon.HasValue() && !request.IsEditMode) diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/RouteMenuItemProvider.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/RouteMenuItemProvider.cs index 0ff2fd185c..7d4e5690ca 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/RouteMenuItemProvider.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/Providers/RouteMenuItemProvider.cs @@ -1,6 +1,4 @@ -using System.Web.Routing; -using Newtonsoft.Json; -using SmartStore.Collections; +using SmartStore.Collections; using SmartStore.Core.Localization; namespace SmartStore.Web.Framework.UI @@ -19,26 +17,7 @@ protected override void ApplyLink(MenuItemProviderRequest request, TreeNode(request.Entity.Model); - var routeName = string.Empty; - - if (routeValues.TryGetValue("routename", out var val)) - { - routeName = val as string; - routeValues.Remove("routename"); - } - - if (routeName.HasValue()) - { - node.Value.Route(routeName, routeValues); - } - else - { - node.Value.Action(routeValues); - } - } + node.ApplyRouteData(request.Entity.Model); } catch { } diff --git a/src/Presentation/SmartStore.Web.Framework/UI/Menus/UserMenuFilter.cs b/src/Presentation/SmartStore.Web.Framework/UI/Menus/UserMenuFilter.cs index a762207bca..0fb3e9a3bb 100644 --- a/src/Presentation/SmartStore.Web.Framework/UI/Menus/UserMenuFilter.cs +++ b/src/Presentation/SmartStore.Web.Framework/UI/Menus/UserMenuFilter.cs @@ -1,4 +1,5 @@ -using System.Web.Mvc; +using System; +using System.Web.Mvc; using SmartStore.Services.Cms; namespace SmartStore.Web.Framework.UI @@ -14,15 +15,15 @@ public UserMenuFilter(IWidgetProvider widgetProvider, IMenuStorage menuStorage) _menuStorage = menuStorage; } - public void OnResultExecuting(ResultExecutingContext filterContext) + public void OnResultExecuting(ResultExecutingContext filterContext) { - if (filterContext.IsChildAction || !filterContext.Result.IsHtmlViewResult()) - { - return; - } + if (filterContext.IsChildAction || !filterContext.Result.IsHtmlViewResult()) + { + return; + } - ProcessUserMenus(); - } + ProcessUserMenus(); + } public void OnResultExecuted(ResultExecutedContext filterContext) { diff --git a/src/Presentation/SmartStore.Web.Framework/Validators/SmartBaseValidator.cs b/src/Presentation/SmartStore.Web.Framework/Validators/SmartBaseValidator.cs index 7d349f7e4a..2c33fc64e0 100644 --- a/src/Presentation/SmartStore.Web.Framework/Validators/SmartBaseValidator.cs +++ b/src/Presentation/SmartStore.Web.Framework/Validators/SmartBaseValidator.cs @@ -16,7 +16,7 @@ public void Validate(T model, ModelStateDictionary modelState) if (!result.IsValid) { - foreach (var error in result.Errors) + foreach (var error in result.Errors.DistinctBy(x => x.ErrorMessage)) { try { @@ -33,11 +33,14 @@ public bool Validate(T model, List warnings) if (!result.IsValid) { - foreach (var error in result.Errors) - warnings.Add(error.ErrorMessage); + foreach (var error in result.Errors.DistinctBy(x => x.ErrorMessage)) + { + warnings.Add(error.ErrorMessage); + } return false; } + return true; } } diff --git a/src/Presentation/SmartStore.Web.Framework/WebStoreContext.cs b/src/Presentation/SmartStore.Web.Framework/WebStoreContext.cs index 855a89a6e4..8ec5458aea 100644 --- a/src/Presentation/SmartStore.Web.Framework/WebStoreContext.cs +++ b/src/Presentation/SmartStore.Web.Framework/WebStoreContext.cs @@ -1,39 +1,74 @@ using System; using System.Linq; +using System.Collections.Generic; using System.Web; using SmartStore.Core; using SmartStore.Core.Domain.Stores; +using System.Collections.Concurrent; +using SmartStore.Core.Data; +using SmartStore.Core.Caching; +using SmartStore.Core.Data.Hooks; using SmartStore.Core.Infrastructure.DependencyManagement; -using SmartStore.Services.Stores; -using SmartStore.Core.Fakes; +using SmartStore.Core.Domain.Directory; namespace SmartStore.Web.Framework { - /// - /// Store context for web application - /// - public partial class WebStoreContext : IStoreContext + public partial class WebStoreContext : DbSaveHook, IStoreContext { + public class StoreEntityCache + { + public IDictionary Stores { get; set; } + public IDictionary HostMap { get; set; } + public int PrimaryStoreId { get; set; } + + internal Store GetPrimaryStore() + { + return Stores.Get(PrimaryStoreId); + } + + internal Store GetStoreById(int id) + { + return Stores.Get(id); + } + + internal Store GetStoreByHostName(string host) + { + if (!string.IsNullOrEmpty(host) && HostMap.TryGetValue(host, out var id)) + { + return Stores.Get(id); + } + + return null; + } + } + internal const string OverriddenStoreIdKey = "OverriddenStoreId"; + const string CacheKey = "stores:all"; - private readonly Work _storeService; - private readonly IWebHelper _webHelper; - private readonly HttpContextBase _httpContext; + private readonly Lazy> _rs; + private readonly Lazy _httpContext; + private readonly ICacheManager _cache; private Store _currentStore; - public WebStoreContext(Work storeService) + public WebStoreContext(Lazy> rs, Lazy httpContext, ICacheManager cache) { - _storeService = storeService; - _httpContext = HttpContext.Current != null ? (HttpContextBase)new HttpContextWrapper(HttpContext.Current) : new FakeHttpContext("~/"); - _webHelper = new WebHelper(_httpContext); + _rs = rs; + _httpContext = httpContext; + _cache = cache; + } + + public int? GetRequestStore() + { + return _httpContext.Value.SafeGetHttpRequest()?.RequestContext?.RouteData?.DataTokens?.Get(OverriddenStoreIdKey)?.Convert(); } public void SetRequestStore(int? storeId) { - try + var dataTokens = _httpContext.Value.SafeGetHttpRequest()?.RequestContext?.RouteData?.DataTokens; + + if (dataTokens != null) { - var dataTokens = _httpContext.Request.RequestContext.RouteData.DataTokens; if (storeId.GetValueOrDefault() > 0) { dataTokens[OverriddenStoreIdKey] = storeId.Value; @@ -45,61 +80,27 @@ public void SetRequestStore(int? storeId) _currentStore = null; } - catch { } } - public int? GetRequestStore() + public int? GetPreviewStore() { - try + var cookie = _httpContext.Value.GetPreviewModeCookie(false); + if (cookie != null) { - // Reduce thrown exceptions in console - if (_httpContext.Handler == null) - return null; - - var value = _httpContext.Request?.RequestContext?.RouteData?.DataTokens?.Get(OverriddenStoreIdKey); - if (value != null) + var value = cookie.Values[OverriddenStoreIdKey]; + if (value.HasValue()) { - return (int)value; + return value.ToInt(); } - - return null; - } - catch - { - return null; } + + return null; } public void SetPreviewStore(int? storeId) { - try - { - _httpContext.SetPreviewModeValue(OverriddenStoreIdKey, storeId.HasValue ? storeId.Value.ToString() : null); - _currentStore = null; - } - catch { } - } - - public int? GetPreviewStore() - { - try - { - var cookie = _httpContext.GetPreviewModeCookie(false); - if (cookie != null) - { - var value = cookie.Values[OverriddenStoreIdKey]; - if (value.HasValue()) - { - return value.ToInt(); - } - } - - return null; - } - catch - { - return null; - } + _httpContext.Value.SetPreviewModeValue(OverriddenStoreIdKey, storeId.HasValue ? storeId.Value.ToString() : null); + _currentStore = null; } /// @@ -111,28 +112,27 @@ public Store CurrentStore { if (_currentStore == null) { + var cachedStores = GetCachedStores(); + int? storeOverride = GetRequestStore() ?? GetPreviewStore(); if (storeOverride.HasValue) { - // the store to be used can be overwritten on request basis (e.g. for theme preview, editing etc.) - _currentStore = _storeService.Value.GetStoreById(storeOverride.Value); + // The store to be used can be overwritten on request basis (e.g. for theme preview, editing etc.) + _currentStore = cachedStores.GetStoreById(storeOverride.Value); } if (_currentStore == null) { - // ty to determine the current store by HTTP_HOST - var host = _webHelper.ServerVariables("HTTP_HOST"); - - var allStores = _storeService.Value.GetAllStores(); - var store = allStores.FirstOrDefault(s => s.ContainsHostValue(host)); - - if (store == null) - { - // Load the first found store - store = allStores.FirstOrDefault(); - } - - _currentStore = store ?? throw new Exception("No store could be loaded"); + // Try to determine the current store by HTTP_HOST + var hostName = _httpContext.Value.SafeGetHttpRequest()?.ServerVariables["HTTP_HOST"]; + + _currentStore = + // Try to resolve the current store by HTTP_HOST + cachedStores.GetStoreByHostName(hostName) ?? + // Then resolve primary store + cachedStores.GetPrimaryStore() ?? + // No way + throw new Exception("No store could be loaded."); } } @@ -144,16 +144,61 @@ public Store CurrentStore } } - /// - /// IsSingleStoreMode ? 0 : CurrentStore.Id - /// public int CurrentStoreIdIfMultiStoreMode { get { - return _storeService.Value.IsSingleStoreMode() ? 0 : CurrentStore.Id; + return GetCachedStores().Stores.Count <= 1 ? 0 : CurrentStore.Id; } } + protected StoreEntityCache GetCachedStores() + { + return _cache.Get(CacheKey, () => + { + var entry = new StoreEntityCache(); + + var allStores = _rs.Value.TableUntracked + .Expand(x => x.PrimaryStoreCurrency) + .Expand(x => x.PrimaryExchangeRateCurrency) + .OrderBy(x => x.DisplayOrder) + .ThenBy(x => x.Name) + .ToList(); + + // Detach all entities... you never know. + allStores.Each(x => _rs.Value.Context.DetachEntity(x)); + + entry.Stores = allStores.ToDictionary(x => x.Id); + entry.HostMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var store in allStores) + { + var hostValues = store.ParseHostValues(); + foreach (var host in hostValues) + { + entry.HostMap[host] = store.Id; + } + } + + if (allStores.Count > 0) + { + entry.PrimaryStoreId = allStores.FirstOrDefault().Id; + } + + return entry; + }); + } + + public override void OnAfterSave(IHookedEntity entry) + { + if (entry.Entity is Store || entry.Entity is Currency) + { + _cache.Remove(CacheKey); + } + else + { + throw new NotSupportedException(); + } + } } } diff --git a/src/Presentation/SmartStore.Web/Administration/Content/_admin.scss b/src/Presentation/SmartStore.Web/Administration/Content/_admin.scss index edeff9ea58..a1427d316c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Content/_admin.scss +++ b/src/Presentation/SmartStore.Web/Administration/Content/_admin.scss @@ -262,7 +262,7 @@ body { &.sticky { position: fixed; - z-index: 600; + z-index: $zindex-sticky; left: var(--content-padding-x); right: var(--content-padding-x); padding: calc(var(--content-padding-y) / 2) var(--content-padding-x); @@ -498,12 +498,16 @@ legend { left: 0; right: 0; top: 0; - width: 100%; height: 3px; - z-index: 999; + z-index: $zindex-sticky + 1; background-color: #ffab40; transition: opacity .05s ease-in; + .sticky-header & { + left: var(--content-padding-x); + right: var(--content-padding-x); + } + &.busy { opacity: 1; } diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerController.cs index 6228ace363..679533bd2e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/CustomerController.cs @@ -558,19 +558,21 @@ public ActionResult Create() public ActionResult Create(CustomerModel model, bool continueEditing, FormCollection form) { if (!_permissionService.Authorize(StandardPermissionProvider.ManageCustomers)) + { return AccessDeniedView(); + } - if (model.Email.HasValue()) + /// Validate unique user. + if (model.Email.HasValue() && _customerService.GetCustomerByEmail(model.Email) != null) { - var tempCust = _customerService.GetCustomerByEmail(model.Email); - if (tempCust != null) - ModelState.AddModelError("", "Email is already registered"); + ModelState.AddModelError("", T("Account.Register.Errors.EmailAlreadyExists")); } - if (model.Username.HasValue() && _customerSettings.CustomerLoginType != CustomerLoginType.Email) + + if (model.Username.HasValue() && + _customerSettings.CustomerLoginType != CustomerLoginType.Email && + _customerService.GetCustomerByUsername(model.Username) != null) { - var tempCust = _customerService.GetCustomerByEmail(model.Username); - if (tempCust != null) - ModelState.AddModelError("", "Username is already registered"); + ModelState.AddModelError("", T("Account.Register.Errors.UsernameAlreadyExists")); } // Validate customer roles. @@ -581,15 +583,19 @@ public ActionResult Create(CustomerModel model, bool continueEditing, FormCollec { foreach (var customerRole in allCustomerRoles) { - if (model.SelectedCustomerRoleIds.Contains(customerRole.Id)) - newCustomerRoles.Add(customerRole); + if (model.SelectedCustomerRoleIds.Contains(customerRole.Id)) + { + newCustomerRoles.Add(customerRole); + } } } var customerRolesError = ValidateCustomerRoles(newCustomerRoles); if (customerRolesError.HasValue()) + { ModelState.AddModelError("", customerRolesError); + } var allowManagingCustomerRoles = _permissionService.Authorize(StandardPermissionProvider.ManageCustomerRoles); @@ -660,7 +666,7 @@ public ActionResult Create(CustomerModel model, bool continueEditing, FormCollec _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.Fax, model.Fax); // Password. - if (!String.IsNullOrWhiteSpace(model.Password)) + if (model.Password.HasValue()) { var changePassRequest = new ChangePasswordRequest(model.Email, false, _customerSettings.DefaultPasswordFormat, model.Password); var changePassResult = _customerRegistrationService.ChangePassword(changePassRequest); diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/LanguageController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/LanguageController.cs index 7ffb83c3c0..72b56965a2 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/LanguageController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/LanguageController.cs @@ -177,7 +177,7 @@ private void PrepareAvailableLanguageModel( model.TranslatedPercentage = resources.TranslatedPercentage; model.IsDownloadRunning = state != null && state.Id == resources.Id; model.UpdatedOn = _dateTimeHelper.ConvertToUserTime(resources.UpdatedOn, DateTimeKind.Utc); - model.UpdatedOnString = model.UpdatedOn.RelativeFormat(false, "f"); + model.UpdatedOnString = resources.UpdatedOn.RelativeFormat(true, "f"); model.FlagImageFileName = GetFlagFileName(resources.Language.Culture); if (language != null && lastImportInfos.TryGetValue(language.Id, out LastResourcesImportInfo info)) diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/OnlineCustomerController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/OnlineCustomerController.cs index 3c1afff0f8..a65ccdc5c6 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/OnlineCustomerController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/OnlineCustomerController.cs @@ -68,7 +68,7 @@ public ActionResult List() Id = x.Id, CustomerInfo = x.IsRegistered() ? x.Email : T("Admin.Customers.Guest").Text, LastIpAddress = x.LastIpAddress, - Location = _geoCountryLookup.LookupCountryName(x.LastIpAddress), + Location = _geoCountryLookup.LookupCountry(x.LastIpAddress)?.Name.EmptyNull(), LastActivityDate = _dateTimeHelper.ConvertToUserTime(x.LastActivityDateUtc, DateTimeKind.Utc), LastVisitedPage = x.GetAttribute(SystemCustomerAttributeNames.LastVisitedPage) }; @@ -95,7 +95,7 @@ public ActionResult List(GridCommand command) Id = x.Id, CustomerInfo = x.IsRegistered() ? x.Email : T("Admin.Customers.Guest").Text, LastIpAddress = x.LastIpAddress, - Location = _geoCountryLookup.LookupCountryName(x.LastIpAddress), + Location = _geoCountryLookup.LookupCountry(x.LastIpAddress)?.Name.EmptyNull(), LastActivityDate = _dateTimeHelper.ConvertToUserTime(x.LastActivityDateUtc, DateTimeKind.Utc), LastVisitedPage = x.GetAttribute(SystemCustomerAttributeNames.LastVisitedPage) }; diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/OrderController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/OrderController.cs index 077a1e113f..36f63cceb1 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/OrderController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/OrderController.cs @@ -898,14 +898,14 @@ public ActionResult List(OrderListModel model) model.AvailablePaymentMethods = paymentMethods .Select(x => new SelectListItem { - Text = _pluginMediator.GetLocalizedFriendlyName(x.Metadata).NullEmpty() ?? x.Metadata.FriendlyName.NullEmpty(), + Text = _pluginMediator.GetLocalizedFriendlyName(x.Metadata).NullEmpty() ?? x.Metadata.FriendlyName.NullEmpty() ?? x.Metadata.SystemName, Value = x.Metadata.SystemName }) .ToList(); var paymentMethodsCounts = model.AvailablePaymentMethods .GroupBy(x => x.Text) - .Select(x => new { Name = x.Key, Count = x.Count() }) + .Select(x => new { Name = x.Key.EmptyNull(), Count = x.Count() }) .ToDictionarySafe(x => x.Name, x => x.Count); model.AvailablePaymentMethods = model.AvailablePaymentMethods @@ -1063,7 +1063,7 @@ public ActionResult GoToOrderId(OrderListModel model) #region Export / Import - [HttpPost, Compress] + [HttpPost] public ActionResult ExportPdf(bool all, string selectedIds = null) { if (!_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) @@ -1868,6 +1868,7 @@ public ActionResult AddProductToOrderDetails(int orderId, int productId) } [HttpPost] + [ValidateInput(false)] public ActionResult AddProductToOrderDetails(int orderId, int productId, bool adjustInventory, bool? updateTotals, ProductVariantQuery query, FormCollection form) { if (!_permissionService.Authorize(StandardPermissionProvider.ManageOrders)) diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/PictureController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/PictureController.cs index 2b8c3601b1..c831a228fd 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/PictureController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/PictureController.cs @@ -1,4 +1,6 @@ -using System.Web.Mvc; +using System; +using System.Linq; +using System.Web.Mvc; using SmartStore.Core.Domain.Media; using SmartStore.Data.Utilities; using SmartStore.Services.Media; @@ -38,8 +40,9 @@ public ActionResult AsyncUpload(bool isTransient = false, bool validate = true) } var picture = _pictureService.InsertPicture(postedFile.Buffer, postedFile.ContentType, null, true, isTransient, validate); - return Json( - new { + + return Json(new + { success = true, pictureId = picture.Id, imageUrl = _pictureService.GetUrl(picture, _mediaSettings.ProductThumbPictureSize, host: "") diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/QuantityUnitController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/QuantityUnitController.cs index 3649c5522f..6c429e4e48 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/QuantityUnitController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/QuantityUnitController.cs @@ -3,7 +3,6 @@ using System.Web.Mvc; using SmartStore.Admin.Models.Directory; using SmartStore.Core.Domain.Directory; -using SmartStore.Services; using SmartStore.Services.Directory; using SmartStore.Services.Localization; using SmartStore.Services.Security; @@ -17,45 +16,20 @@ namespace SmartStore.Admin.Controllers [AdminAuthorize] public class QuantityUnitController : AdminControllerBase { - #region Fields - private readonly IQuantityUnitService _quantityUnitService; private readonly ILocalizedEntityService _localizedEntityService; private readonly ILanguageService _languageService; - private readonly ICommonServices _services; - - #endregion - - #region Constructors public QuantityUnitController( IQuantityUnitService quantityUnitService, ILocalizedEntityService localizedEntityService, - ILanguageService languageService, - ICommonServices services) + ILanguageService languageService) { _quantityUnitService = quantityUnitService; _localizedEntityService = localizedEntityService; _languageService = languageService; - _services = services; } - #endregion - - #region Utilities - - [NonAction] - public void UpdateLocales(QuantityUnit quantityUnit, QuantityUnitModel model) - { - foreach (var localized in model.Locales) - { - _localizedEntityService.SaveLocalizedValue(quantityUnit, x => x.Name, localized.Name, localized.LanguageId); - _localizedEntityService.SaveLocalizedValue(quantityUnit, x => x.Description, localized.Description, localized.LanguageId); - } - } - - #endregion - #region Methods public ActionResult Index() @@ -65,20 +39,25 @@ public ActionResult Index() public ActionResult List() { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + { return AccessDeniedView(); + } - var quantityUnitModel = _quantityUnitService.GetAllQuantityUnits().Select(x => x.ToModel()).ToList(); + var quantityUnitModel = _quantityUnitService.GetAllQuantityUnits() + .Select(x => x.ToModel()) + .ToList(); var gridModel = new GridModel { Data = quantityUnitModel, Total = quantityUnitModel.Count() }; + return View(gridModel); } - //ajax + // Ajax. public ActionResult AllQuantityUnits(string label, int selectedId) { var quantityUnits = _quantityUnitService.GetAllQuantityUnits(); @@ -104,11 +83,12 @@ public ActionResult AllQuantityUnits(string label, int selectedId) public ActionResult Create() { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + { return AccessDeniedView(); + } var model = new QuantityUnitModel(); - //locales AddLocales(_languageService, model.Locales); return View(model); @@ -117,40 +97,46 @@ public ActionResult Create() [HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")] public ActionResult Create(QuantityUnitModel model, bool continueEditing) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + { return AccessDeniedView(); + } if (ModelState.IsValid) { var quantityUnit = model.ToEntity(); _quantityUnitService.InsertQuantityUnit(quantityUnit); - //locales + UpdateLocales(quantityUnit, model); NotifySuccess(T("Admin.Configuration.QuantityUnit.Added")); return continueEditing ? RedirectToAction("Edit", new { id = quantityUnit.Id }) : RedirectToAction("List"); } - //If we got this far, something failed, redisplay form return View(model); } public ActionResult Edit(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + { return AccessDeniedView(); + } var quantityUnit = _quantityUnitService.GetQuantityUnitById(id); if (quantityUnit == null) + { return RedirectToAction("List"); + } var model = quantityUnit.ToModel(); AddLocales(_languageService, model.Locales, (locale, languageId) => { locale.Name = quantityUnit.GetLocalized(x => x.Name, languageId, false, false); - locale.Description = quantityUnit.GetLocalized(x => x.Description, languageId, false, false); + locale.NamePlural = quantityUnit.GetLocalized(x => x.NamePlural, languageId, false, false); + locale.Description = quantityUnit.GetLocalized(x => x.Description, languageId, false, false); }); return View(model); @@ -159,12 +145,16 @@ public ActionResult Edit(int id) [HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")] public ActionResult Edit(QuantityUnitModel model, bool continueEditing) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + { return AccessDeniedView(); + } var quantityUnit = _quantityUnitService.GetQuantityUnitById(model.Id); if (quantityUnit == null) + { return RedirectToAction("List"); + } if (ModelState.IsValid) { @@ -181,39 +171,57 @@ public ActionResult Edit(QuantityUnitModel model, bool continueEditing) return View(model); } + [HttpPost] + public ActionResult Save(FormCollection formValues) + { + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + { + return AccessDeniedView(); + } + + return RedirectToAction("List", "QuantityUnit"); + } + [HttpPost, ActionName("Delete")] public ActionResult DeleteConfirmed(int id) { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) + { return AccessDeniedView(); + } var quantityUnit = _quantityUnitService.GetQuantityUnitById(id); if (quantityUnit == null) - //No delivery time found with the specified id + { return RedirectToAction("List"); + } try { - _quantityUnitService.DeleteQuantityUnit(quantityUnit); - NotifySuccess(T("Admin.Configuration.QuantityUnits.Deleted")); + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); return RedirectToAction("List"); } - catch (Exception exc) + catch (Exception ex) { - NotifyError(exc); + NotifyError(ex); return RedirectToAction("Edit", new { id = quantityUnit.Id }); } } - [HttpPost] - public ActionResult Save(FormCollection formValues) - { - if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageMeasures)) - return AccessDeniedView(); + #endregion - return RedirectToAction("List", "QuantityUnit"); + #region Utilities + + private void UpdateLocales(QuantityUnit quantityUnit, QuantityUnitModel model) + { + foreach (var localized in model.Locales) + { + _localizedEntityService.SaveLocalizedValue(quantityUnit, x => x.Name, localized.Name, localized.LanguageId); + _localizedEntityService.SaveLocalizedValue(quantityUnit, x => x.NamePlural, localized.NamePlural, localized.LanguageId); + _localizedEntityService.SaveLocalizedValue(quantityUnit, x => x.Description, localized.Description, localized.LanguageId); + } } #endregion diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/SettingController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/SettingController.cs index 6eadcc7fcf..521c96998a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/SettingController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/SettingController.cs @@ -1056,6 +1056,8 @@ public ActionResult GeneralCommon() // SEO. var seoSettings = _services.Settings.LoadSetting(storeScope); MiniMapper.Map(seoSettings, model.SeoSettings); + // Fix Disallows joined with comma in MiniMapper (we need NewLine). + model.SeoSettings.ExtraRobotsDisallows = string.Join(Environment.NewLine, seoSettings.ExtraRobotsDisallows); StoreDependingSettings.GetOverrideKeys(seoSettings, model.SeoSettings, storeScope, _services.Settings, false); @@ -1221,120 +1223,135 @@ public ActionResult GeneralCommon(GeneralCommonSettingsModel model, FormCollecti public ActionResult ChangeEnryptionKey(GeneralCommonSettingsModel model) { if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageSettings)) + { return AccessDeniedView(); + } // Set page timeout to 5 minutes. Server.ScriptTimeout = 300; var storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); var securitySettings = _services.Settings.LoadSetting(storeScope); + var oldEncryptionPrivateKey = securitySettings.EncryptionKey; + var newEncryptionPrivateKey = model.SecuritySettings.EncryptionKey.EmptyNull(); - try + if (newEncryptionPrivateKey.IsEmpty() || newEncryptionPrivateKey.Length != 16) { - if (model.SecuritySettings.EncryptionKey == null) - model.SecuritySettings.EncryptionKey = ""; - - model.SecuritySettings.EncryptionKey = model.SecuritySettings.EncryptionKey.Trim(); - - var newEncryptionPrivateKey = model.SecuritySettings.EncryptionKey; - if (newEncryptionPrivateKey.IsEmpty() || newEncryptionPrivateKey.Length != 16) - throw new SmartException(T("Admin.Configuration.Settings.GeneralCommon.EncryptionKey.TooShort")); + NotifyError(T("Admin.Configuration.Settings.GeneralCommon.EncryptionKey.TooShort")); + return RedirectToAction("GeneralCommon"); + } - var oldEncryptionPrivateKey = securitySettings.EncryptionKey; - if (oldEncryptionPrivateKey == newEncryptionPrivateKey) - throw new SmartException(T("Admin.Configuration.Settings.GeneralCommon.EncryptionKey.TheSame")); + if (oldEncryptionPrivateKey == newEncryptionPrivateKey) + { + NotifyError(T("Admin.Configuration.Settings.GeneralCommon.EncryptionKey.TheSame")); + return RedirectToAction("GeneralCommon"); + } + try + { // Update encrypted order info. - var orders = _orderService.LoadAllOrders(); - foreach (var order in orders) + var orderQuery = _orderService.GetOrders(0, 0, null, null, null, null, null, null, null); + orderQuery = orderQuery.OrderByDescending(x => x.CreatedOnUtc); + IPagedList orders = null; + var pageIndex = 0; + + do { - // New credit card encryption. - string decryptedCardType = _encryptionService.DecryptText(order.CardType, oldEncryptionPrivateKey); - string decryptedCardName = _encryptionService.DecryptText(order.CardName, oldEncryptionPrivateKey); - string decryptedCardNumber = _encryptionService.DecryptText(order.CardNumber, oldEncryptionPrivateKey); - string decryptedMaskedCreditCardNumber = _encryptionService.DecryptText(order.MaskedCreditCardNumber, oldEncryptionPrivateKey); - string decryptedCardCvv2 = _encryptionService.DecryptText(order.CardCvv2, oldEncryptionPrivateKey); - string decryptedCardExpirationMonth = _encryptionService.DecryptText(order.CardExpirationMonth, oldEncryptionPrivateKey); - string decryptedCardExpirationYear = _encryptionService.DecryptText(order.CardExpirationYear, oldEncryptionPrivateKey); - - string encryptedCardType = _encryptionService.EncryptText(decryptedCardType, newEncryptionPrivateKey); - string encryptedCardName = _encryptionService.EncryptText(decryptedCardName, newEncryptionPrivateKey); - string encryptedCardNumber = _encryptionService.EncryptText(decryptedCardNumber, newEncryptionPrivateKey); - string encryptedMaskedCreditCardNumber = _encryptionService.EncryptText(decryptedMaskedCreditCardNumber, newEncryptionPrivateKey); - string encryptedCardCvv2 = _encryptionService.EncryptText(decryptedCardCvv2, newEncryptionPrivateKey); - string encryptedCardExpirationMonth = _encryptionService.EncryptText(decryptedCardExpirationMonth, newEncryptionPrivateKey); - string encryptedCardExpirationYear = _encryptionService.EncryptText(decryptedCardExpirationYear, newEncryptionPrivateKey); - - order.CardType = encryptedCardType; - order.CardName = encryptedCardName; - order.CardNumber = encryptedCardNumber; - order.MaskedCreditCardNumber = encryptedMaskedCreditCardNumber; - order.CardCvv2 = encryptedCardCvv2; - order.CardExpirationMonth = encryptedCardExpirationMonth; - order.CardExpirationYear = encryptedCardExpirationYear; - - // New direct debit encryption. - string decryptedAccountHolder = _encryptionService.DecryptText(order.DirectDebitAccountHolder, oldEncryptionPrivateKey); - string decryptedAccountNumber = _encryptionService.DecryptText(order.DirectDebitAccountNumber, oldEncryptionPrivateKey); - string decryptedBankCode = _encryptionService.DecryptText(order.DirectDebitBankCode, oldEncryptionPrivateKey); - string decryptedBankName = _encryptionService.DecryptText(order.DirectDebitBankName, oldEncryptionPrivateKey); - string decryptedBic = _encryptionService.DecryptText(order.DirectDebitBIC, oldEncryptionPrivateKey); - string decryptedCountry = _encryptionService.DecryptText(order.DirectDebitCountry, oldEncryptionPrivateKey); - string decryptedIban = _encryptionService.DecryptText(order.DirectDebitIban, oldEncryptionPrivateKey); - - string encryptedAccountHolder = _encryptionService.EncryptText(decryptedAccountHolder, newEncryptionPrivateKey); - string encryptedAccountNumber = _encryptionService.EncryptText(decryptedAccountNumber, newEncryptionPrivateKey); - string encryptedBankCode = _encryptionService.EncryptText(decryptedBankCode, newEncryptionPrivateKey); - string encryptedBankName = _encryptionService.EncryptText(decryptedBankName, newEncryptionPrivateKey); - string encryptedBic = _encryptionService.EncryptText(decryptedBic, newEncryptionPrivateKey); - string encryptedCountry = _encryptionService.EncryptText(decryptedCountry, newEncryptionPrivateKey); - string encryptedIban = _encryptionService.EncryptText(decryptedIban, newEncryptionPrivateKey); - - order.DirectDebitAccountHolder = encryptedAccountHolder; - order.DirectDebitAccountNumber = encryptedAccountNumber; - order.DirectDebitBankCode = encryptedBankCode; - order.DirectDebitBankName = encryptedBankName; - order.DirectDebitBIC = encryptedBic; - order.DirectDebitCountry = encryptedCountry; - order.DirectDebitIban = encryptedIban; - - _orderService.UpdateOrder(order); + orders = new PagedList(orderQuery, pageIndex++, 1000); + + foreach (var order in orders) + { + // New credit card encryption. + string decryptedCardType = _encryptionService.DecryptText(order.CardType, oldEncryptionPrivateKey); + string decryptedCardName = _encryptionService.DecryptText(order.CardName, oldEncryptionPrivateKey); + string decryptedCardNumber = _encryptionService.DecryptText(order.CardNumber, oldEncryptionPrivateKey); + string decryptedMaskedCreditCardNumber = _encryptionService.DecryptText(order.MaskedCreditCardNumber, oldEncryptionPrivateKey); + string decryptedCardCvv2 = _encryptionService.DecryptText(order.CardCvv2, oldEncryptionPrivateKey); + string decryptedCardExpirationMonth = _encryptionService.DecryptText(order.CardExpirationMonth, oldEncryptionPrivateKey); + string decryptedCardExpirationYear = _encryptionService.DecryptText(order.CardExpirationYear, oldEncryptionPrivateKey); + + string encryptedCardType = _encryptionService.EncryptText(decryptedCardType, newEncryptionPrivateKey); + string encryptedCardName = _encryptionService.EncryptText(decryptedCardName, newEncryptionPrivateKey); + string encryptedCardNumber = _encryptionService.EncryptText(decryptedCardNumber, newEncryptionPrivateKey); + string encryptedMaskedCreditCardNumber = _encryptionService.EncryptText(decryptedMaskedCreditCardNumber, newEncryptionPrivateKey); + string encryptedCardCvv2 = _encryptionService.EncryptText(decryptedCardCvv2, newEncryptionPrivateKey); + string encryptedCardExpirationMonth = _encryptionService.EncryptText(decryptedCardExpirationMonth, newEncryptionPrivateKey); + string encryptedCardExpirationYear = _encryptionService.EncryptText(decryptedCardExpirationYear, newEncryptionPrivateKey); + + order.CardType = encryptedCardType; + order.CardName = encryptedCardName; + order.CardNumber = encryptedCardNumber; + order.MaskedCreditCardNumber = encryptedMaskedCreditCardNumber; + order.CardCvv2 = encryptedCardCvv2; + order.CardExpirationMonth = encryptedCardExpirationMonth; + order.CardExpirationYear = encryptedCardExpirationYear; + + // New direct debit encryption. + string decryptedAccountHolder = _encryptionService.DecryptText(order.DirectDebitAccountHolder, oldEncryptionPrivateKey); + string decryptedAccountNumber = _encryptionService.DecryptText(order.DirectDebitAccountNumber, oldEncryptionPrivateKey); + string decryptedBankCode = _encryptionService.DecryptText(order.DirectDebitBankCode, oldEncryptionPrivateKey); + string decryptedBankName = _encryptionService.DecryptText(order.DirectDebitBankName, oldEncryptionPrivateKey); + string decryptedBic = _encryptionService.DecryptText(order.DirectDebitBIC, oldEncryptionPrivateKey); + string decryptedCountry = _encryptionService.DecryptText(order.DirectDebitCountry, oldEncryptionPrivateKey); + string decryptedIban = _encryptionService.DecryptText(order.DirectDebitIban, oldEncryptionPrivateKey); + + string encryptedAccountHolder = _encryptionService.EncryptText(decryptedAccountHolder, newEncryptionPrivateKey); + string encryptedAccountNumber = _encryptionService.EncryptText(decryptedAccountNumber, newEncryptionPrivateKey); + string encryptedBankCode = _encryptionService.EncryptText(decryptedBankCode, newEncryptionPrivateKey); + string encryptedBankName = _encryptionService.EncryptText(decryptedBankName, newEncryptionPrivateKey); + string encryptedBic = _encryptionService.EncryptText(decryptedBic, newEncryptionPrivateKey); + string encryptedCountry = _encryptionService.EncryptText(decryptedCountry, newEncryptionPrivateKey); + string encryptedIban = _encryptionService.EncryptText(decryptedIban, newEncryptionPrivateKey); + + order.DirectDebitAccountHolder = encryptedAccountHolder; + order.DirectDebitAccountNumber = encryptedAccountNumber; + order.DirectDebitBankCode = encryptedBankCode; + order.DirectDebitBankName = encryptedBankName; + order.DirectDebitBIC = encryptedBic; + order.DirectDebitCountry = encryptedCountry; + order.DirectDebitIban = encryptedIban; + + _orderService.UpdateOrder(order); + } } + while (orders.HasNextPage); - // Update user information. - // Optimization - load only users with PasswordFormat.Encrypted. - - var query = _customerService.GetAllCustomersByPasswordFormat(PasswordFormat.Encrypted).SourceQuery; + // Update user information. + var customerQuery = _customerService.GetAllCustomersByPasswordFormat(PasswordFormat.Encrypted).SourceQuery; + PagedList customers = null; + pageIndex = 0; - for (var pageIndex = 0; pageIndex < 9999999; ++pageIndex) - { - var customers = new PagedList(query, pageIndex, 1000); - - foreach (var customer in customers) - { - string decryptedPassword = _encryptionService.DecryptText(customer.Password, oldEncryptionPrivateKey); - string encryptedPassword = _encryptionService.EncryptText(decryptedPassword, newEncryptionPrivateKey); + do + { + customers = new PagedList(customerQuery, pageIndex++, 1000); - customer.Password = encryptedPassword; - } + if (customers.Any()) + { + foreach (var customer in customers) + { + var decryptedPassword = _encryptionService.DecryptText(customer.Password, oldEncryptionPrivateKey); + var encryptedPassword = _encryptionService.EncryptText(decryptedPassword, newEncryptionPrivateKey); - Services.DbContext.SaveChanges(); + customer.Password = encryptedPassword; + } - if (!customers.HasNextPage) - break; - } + Services.DbContext.SaveChanges(); + } + } + while (customers.HasNextPage); securitySettings.EncryptionKey = newEncryptionPrivateKey; _services.Settings.SaveSetting(securitySettings); NotifySuccess(T("Admin.Configuration.Settings.GeneralCommon.EncryptionKey.Changed")); } - catch (Exception exc) + catch (Exception ex) { - NotifyError(exc); + NotifyError(ex); + Logger.Error(ex); } - return RedirectToAction("GeneralCommon"); + return RedirectToAction("GeneralCommon"); } [HttpPost] diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/SpecificationAttributeController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/SpecificationAttributeController.cs index c8f64aee7c..b4f0844db8 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/SpecificationAttributeController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/SpecificationAttributeController.cs @@ -497,18 +497,14 @@ public ActionResult OptionDelete(int optionId, int specificationAttributeId, Gri return OptionList(specificationAttributeId, command); } - //ajax + // Ajax. [AcceptVerbs(HttpVerbs.Get)] - public ActionResult GetOptionsByAttributeId(string attributeId) + public ActionResult GetOptionsByAttributeId(int attributeId) { if (!_permissionService.Authorize(StandardPermissionProvider.ManageCatalog)) return AccessDeniedView(); - // This action method gets called via an ajax request - if (string.IsNullOrEmpty(attributeId)) - throw new ArgumentNullException("attributeId"); - - var options = _specificationAttributeService.GetSpecificationAttributeOptionsBySpecificationAttribute(Convert.ToInt32(attributeId)); + var options = _specificationAttributeService.GetSpecificationAttributeOptionsBySpecificationAttribute(attributeId); var result = from o in options select new { id = o.Id, name = o.Name, text = o.Name }; diff --git a/src/Presentation/SmartStore.Web/Administration/Controllers/TopicController.cs b/src/Presentation/SmartStore.Web/Administration/Controllers/TopicController.cs index 1d6543ab6a..bf5b9fb78b 100644 --- a/src/Presentation/SmartStore.Web/Administration/Controllers/TopicController.cs +++ b/src/Presentation/SmartStore.Web/Administration/Controllers/TopicController.cs @@ -223,6 +223,7 @@ public ActionResult Create() } [HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")] + [ValidateInput(false)] public ActionResult Create(TopicModel model, bool continueEditing, FormCollection form) { if (!Services.Permissions.Authorize(StandardPermissionProvider.ManageTopics)) diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Catalog/CategoryModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Catalog/CategoryModel.cs index a6c12464b7..f8ff4c2c9e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Catalog/CategoryModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Catalog/CategoryModel.cs @@ -39,6 +39,10 @@ public CategoryModel() [AllowHtml] public string BottomDescription { get; set; } + [SmartResourceDisplayName("Admin.Catalog.Categories.Fields.ExternalLink")] + [AllowHtml, UIHint("Link")] + public string ExternalLink { get; set; } + [SmartResourceDisplayName("Admin.Catalog.Categories.Fields.BadgeText")] [AllowHtml] public string BadgeText { get; set; } diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Directory/QuantityUnitModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Directory/QuantityUnitModel.cs index f44da303ce..f87e50dae0 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Directory/QuantityUnitModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Directory/QuantityUnitModel.cs @@ -16,7 +16,10 @@ public QuantityUnitModel() [SmartResourceDisplayName("Admin.Configuration.QuantityUnit.Fields.Name")] public string Name { get; set; } - [SmartResourceDisplayName("Common.Description")] + [SmartResourceDisplayName("Admin.Configuration.QuantityUnit.Fields.NamePlural")] + public string NamePlural { get; set; } + + [SmartResourceDisplayName("Common.Description")] [AllowHtml] public string Description { get; set; } @@ -36,7 +39,10 @@ public class QuantityUnitLocalizedModel : ILocalizedModelLocal [SmartResourceDisplayName("Admin.Configuration.QuantityUnit.Fields.Name")] public string Name { get; set; } - [SmartResourceDisplayName("Common.Description")] + [SmartResourceDisplayName("Admin.Configuration.QuantityUnit.Fields.NamePlural")] + public string NamePlural { get; set; } + + [SmartResourceDisplayName("Common.Description")] public string Description { get; set; } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Menus/MenuItemRecordModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Menus/MenuItemRecordModel.cs index c4c5dd22f4..9c033c24c7 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Menus/MenuItemRecordModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Menus/MenuItemRecordModel.cs @@ -1,8 +1,12 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Web; using System.Web.Mvc; +using System.Web.Routing; using FluentValidation; using FluentValidation.Attributes; +using Newtonsoft.Json; +using SmartStore.Collections; using SmartStore.Core.Localization; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Localization; @@ -86,6 +90,29 @@ public partial class MenuItemRecordValidator : AbstractValidator x.ProviderName).NotEmpty(); + + RuleFor(x => x.Model) + .Must(x => + { + try + { + if (x.HasValue()) + { + var node = new TreeNode(new MenuItem()); + node.ApplyRouteData(x); + + var urlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext); + var result = node.Value.GenerateUrl(urlHelper); + + return result.HasValue(); + } + } + catch { } + + return true; + }) + .When(x => x.ProviderName.IsCaseInsensitiveEqual("route")) + .WithMessage(T("Admin.ContentManagement.Menus.Item.InvalidRouteValues")); } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Models/Messages/QueuedEmailModel.cs b/src/Presentation/SmartStore.Web/Administration/Models/Messages/QueuedEmailModel.cs index dff6e45f88..ce4275bf0a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Models/Messages/QueuedEmailModel.cs +++ b/src/Presentation/SmartStore.Web/Administration/Models/Messages/QueuedEmailModel.cs @@ -80,10 +80,10 @@ public partial class QueuedEmailValidator : AbstractValidator { public QueuedEmailValidator() { - RuleFor(x => x.Priority).NotEmpty().InclusiveBetween(0, 99999); + RuleFor(x => x.Priority).InclusiveBetween(0, 99999); RuleFor(x => x.From).NotEmpty(); RuleFor(x => x.To).NotEmpty(); - RuleFor(x => x.SentTries).NotEmpty().InclusiveBetween(0, 99999); + RuleFor(x => x.SentTries).InclusiveBetween(0, 99999); } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Scripts/_references.js b/src/Presentation/SmartStore.Web/Administration/Scripts/_references.js index a941beba51..6d51960f98 100644 Binary files a/src/Presentation/SmartStore.Web/Administration/Scripts/_references.js and b/src/Presentation/SmartStore.Web/Administration/Scripts/_references.js differ diff --git a/src/Presentation/SmartStore.Web/Administration/Scripts/admin.globalinit.js b/src/Presentation/SmartStore.Web/Administration/Scripts/admin.globalinit.js index bf57a8181b..71d0efd6c1 100644 --- a/src/Presentation/SmartStore.Web/Administration/Scripts/admin.globalinit.js +++ b/src/Presentation/SmartStore.Web/Administration/Scripts/admin.globalinit.js @@ -136,6 +136,7 @@ if (sectionHeaderHasButtons === true) { var y = $(this).scrollTop(); sectionHeader.toggleClass("sticky", y >= navbarHeight); + $(document.body).toggleClass("sticky-header", y >= navbarHeight); } }).trigger('resize'); } diff --git a/src/Presentation/SmartStore.Web/Administration/SmartStore.Admin.csproj b/src/Presentation/SmartStore.Web/Administration/SmartStore.Admin.csproj index 7857f08e68..44b280e563 100644 --- a/src/Presentation/SmartStore.Web/Administration/SmartStore.Admin.csproj +++ b/src/Presentation/SmartStore.Web/Administration/SmartStore.Admin.csproj @@ -1,5 +1,6 @@  - + + Debug diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Category/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Category/_CreateOrUpdate.cshtml index 7a0d9bfd6b..a5953500e0 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Category/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Category/_CreateOrUpdate.cshtml @@ -32,6 +32,62 @@ @Html.ValidationMessageFor(model => model.Id) + + + @Html.SmartLabelFor(model => model.ExternalLink) + + + @Html.EditorFor(model => model.ExternalLink) + @Html.ValidationMessageFor(model => model.ExternalLink) + + + + + @Html.SmartLabelFor(model => model.ParentCategoryId) + + + @Html.DropDownListFor(model => model.ParentCategoryId, + Model.ParentCategoryId == null ? Enumerable.Empty() : new List { + new SelectListItem { Text = Model.ParentCategoryBreadcrumb, Value = Model.ParentCategoryId.ToString() } + }, + new + { + placeholder = "[" + T("Common.Unspecified") + "]", + data_select = true, + data_select_lazy = true, + data_select_init_text = Model.ParentCategoryBreadcrumb, + data_select_url = Url.Action("Allcategories", "Category", new { selectedId = Model.ParentCategoryId }) + }) + @Html.ValidationMessageFor(model => model.ParentCategoryId) + + + + + @Html.SmartLabelFor(model => model.DisplayOrder) + + + @Html.EditorFor(model => model.DisplayOrder) + @Html.ValidationMessageFor(model => model.DisplayOrder) + + + + + @Html.SmartLabelFor(model => model.Published) + + + @Html.EditorFor(x => x.Published) + @Html.ValidationMessageFor(model => model.Published) + + + + + @Html.SmartLabelFor(model => model.ShowOnHomePage) + + + @Html.EditorFor(x => x.ShowOnHomePage) + @Html.ValidationMessageFor(model => model.ShowOnHomePage) + + } @@ -175,51 +231,7 @@ - - @Html.SmartLabelFor(model => model.ParentCategoryId) - - - @Html.DropDownListFor(model => model.ParentCategoryId, - Model.ParentCategoryId == null ? Enumerable.Empty() : new List { - new SelectListItem { Text = Model.ParentCategoryBreadcrumb, Value = Model.ParentCategoryId.ToString() } - }, - new - { - placeholder = "[" + T("Common.Unspecified") + "]", - data_select = true, - data_select_lazy = true, - data_select_init_text = Model.ParentCategoryBreadcrumb, - data_select_url = Url.Action("Allcategories", "Category", new { selectedId = Model.ParentCategoryId }) - }) - @Html.ValidationMessageFor(model => model.ParentCategoryId) - - - - - @Html.SmartLabelFor(model => model.ShowOnHomePage) - - - @Html.EditorFor(x => x.ShowOnHomePage) - @Html.ValidationMessageFor(model => model.ShowOnHomePage) - - - - - @Html.SmartLabelFor(model => model.Published) - - - @Html.EditorFor(x => x.Published) - @Html.ValidationMessageFor(model => model.Published) - - - - - @Html.SmartLabelFor(model => model.DisplayOrder) - - - @Html.EditorFor(model => model.DisplayOrder) - @Html.ValidationMessageFor(model => model.DisplayOrder) - +
          @@ -257,6 +269,9 @@ @Html.ValidationMessageFor(model => model.PageSizeOptions) + +
          + @Html.SmartLabelFor(model => model.Alias) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/EmailAccount/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/EmailAccount/_CreateOrUpdate.cshtml index accc2b0fff..ba662e30c9 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/EmailAccount/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/EmailAccount/_CreateOrUpdate.cshtml @@ -54,7 +54,7 @@ @Html.SmartLabelFor(model => model.Password) - @Html.EditorFor(model => model.Password) + @Html.EditorFor(model => model.Password, null, new { htmlAttributes = new { value = Model.Password } }) @Html.ValidationMessageFor(model => model.Password) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/_CreateOrUpdate.cshtml index 1d667bc4c1..3aab2a436c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Manufacturer/_CreateOrUpdate.cshtml @@ -336,10 +336,10 @@ { foreach (var discount in Model.AvailableDiscounts) { -
          - - +
          + +
          } } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Menu/ItemList.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Menu/ItemList.cshtml index 6024a71e89..3145879640 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Menu/ItemList.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Menu/ItemList.cshtml @@ -24,13 +24,14 @@ else var item = node.Value; var parentId = node.Parent != null ? node.Parent.Value.EntityId : 0; var indentStyle = " style=\"padding-left: {0}px;\"".FormatInvariant(padding); + var rtl = WorkContext.WorkingLanguage.Rtl;