diff --git a/.gitignore b/.gitignore index 3b997b1c64..a45279fb21 100644 --- a/.gitignore +++ b/.gitignore @@ -25,16 +25,6 @@ local.properties # PDT-specific .buildpath -########### -## Symlinks -########### - -*-sym.* -*-symdir/ -[Ee]xternal[Ff]iles/ -/src/Plugins/*-sym -/src/Presentation/SmartStore.Web/Themes/*-sym - ########################################################### ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. @@ -43,23 +33,48 @@ local.properties # User-specific files *.suo *.user +*.userosscache *.sln.docstates -# Build results +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs +# Build results [Dd]ebug/ +[Dd]ebugPublic/ [Rr]elease/ +[Rr]eleases/ x64/ -# build/ +x86/ +bld/ [Bb]in/ [Oo]bj/ +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + *_i.c *_p.c +*_i.h *.ilk *.meta *.obj @@ -79,6 +94,7 @@ x64/ *.vssscc .builds *.pidb +*.svclog *.scc # Visual C++ cache files @@ -93,6 +109,7 @@ ipch/ *.psess *.vsp *.vspx +*.sap # Guidance Automation Toolkit *.gpState @@ -108,8 +125,16 @@ _TeamCity* *.dotCover # NCrunch -*.ncrunch* +_NCrunch_* .*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ # Installshield output folder [Ee]xpress/ @@ -128,31 +153,58 @@ DocProject/Help/html publish/ # Publish Web Output -*.Publish.xml +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted *.pubxml - -# NuGet Packages Directory -## TODO: If you have NuGet Package Restore enabled, uncomment the next line -/src/packages +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets # Windows Azure Build Output csx *.build.csdef +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Microsoft Azure ApplicationInsights config file +ApplicationInsights.config + # Windows Store app package directory AppPackages/ +BundleArtifacts/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ # Others # sql/ -*.Cache ClientBin/ [Ss]tyle[Cc]op.* ~$* *~ *.dbmdl -*.[Pp]ublish.xml +*.dbproj.schemaview *.pfx *.publishsettings +node_modules/ +orleans.codegen.cs # RIA/Silverlight projects Generated_Code/ @@ -165,12 +217,42 @@ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files -App_Data/*.mdf -App_Data/*.ldf +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings -################### -## Windows detritus -################### +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe + +# FAKE - F# Make +.fake/ # Windows image file caches Thumbs.db @@ -185,6 +267,21 @@ $RECYCLE.BIN/ # Mac crap .DS_Store + +########### +## Symlinks +########### + +*-sym.* +*-symdir/ +[Ee]xternal[Ff]iles/ +/src/Plugins/*-sym +/src/Presentation/SmartStore.Web/Themes/*-sym + + +########### +## Specific +########### # Backups [Bb]ackup/ [Bb]ackups/ @@ -194,12 +291,11 @@ _[Bb]ackups/ *.orig Kopie von* -########### -## Specific -########### /build /src/Presentation/SmartStore.Web/Plugins /src/Presentation/SmartStore.Web/App_Data/_temp/ +/src/Presentation/SmartStore.Web/App_Data/ExportProfiles/ +/src/Presentation/SmartStore.Web/App_Data/ImportProfiles/ /src/Presentation/SmartStore.Web/App_Data/Settings.txt /src/Presentation/SmartStore.Web/App_Data/InstalledPlugins.txt /src/Presentation/SmartStore.Web/App_Data/Licenses.lic @@ -219,4 +315,3 @@ Kopie von* src/SmartStoreNET.Packager.sln Log.txt -.vs/ diff --git a/README.md b/README.md index 6fbee6e54f..a7a83aee3a 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A comprehensive set of tools for CRM & CMS, sales, marketing, payment & shipping 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/net](http://www.smartstore.com/net) +* **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) * **Documentation:** [SmartStore.NET Documentation in English](http://docs.smartstore.com/display/SMNET/SmartStore.NET+Documentation+Home) @@ -54,18 +54,18 @@ The state-of-the-art architecture of SmartStore.NET - with `ASP.NET 4.5` + `MVC * and many more... ## Project Status -SmartStore.NET V2.2.1 has been released on May 15, 2015. The highlights are: - -* Overall performance increase -* Added multistore support for forums -* New option to display product thumbnails in instant search -* New mobile theme: _MobileLight_ (a light variant of the default mobile theme) -* Quantity unit management -* Limit country settings to stores -* Web API: Support for file upload and multipart mime -* More reliable mobile device detection -* Performance: product list rendering up to 10x (!) faster now -* Lots of bug fixes +SmartStore.NET V2.5.0 has been released on March 03, 2016. The highlights are: + + * New import/export framework (profiles, filters, mapping, projections, scheduling, deployment... just everything!) + * TaskScheduler: Rewritten from scratch to be suitable for Web Farms (including support for cron expressions) + * Payment and shipping methods by customer roles + * Restrict payment methods to countries + * Restrict payment methods to shipping methods + * Email attachment support for message templates + * Attach order invoice PDF automatically to order notification emails + * Overall performance increase + * Lots of bug fixes + ##Try it online diff --git a/SmartStoreNET.Tasks.Targets b/SmartStoreNET.Tasks.Targets index 4cc8cafa8a..f184eaa7eb 100644 --- a/SmartStoreNET.Tasks.Targets +++ b/SmartStoreNET.Tasks.Targets @@ -2,7 +2,7 @@ - + @@ -44,7 +44,7 @@ x86 $(BUILD_NUMBER) - 2.2.2 + 2.6.0 $(StageFolder) .$(Version) diff --git a/build.bat b/build.bat index e017b455af..b58175809a 100644 --- a/build.bat +++ b/build.bat @@ -1,21 +1,24 @@ -set MSBuildPath=%windir%\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe +FOR %%b in ( + "%VS140COMNTOOLS%..\..\VC\vcvarsall.bat" + "%ProgramFiles(x86)%\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" + "%ProgramFiles%\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" -@IF NOT EXIST %MSBuildPath% @ECHO COULDN'T FIND MSBUILD: %MSBuildPath% (Is .NET 4 installed?) -ELSE GOTO END + "%VS120COMNTOOLS%..\..\VC\vcvarsall.bat" + "%ProgramFiles(x86)%\Microsoft Visual Studio 12.0\VC\vcvarsall.bat" + "%ProgramFiles%\Microsoft Visual Studio 12.0\VC\vcvarsall.bat" + + "%VS110COMNTOOLS%..\..\VC\vcvarsall.bat" + "%ProgramFiles(x86)%\Microsoft Visual Studio 11.0\VC\vcvarsall.bat" + "%ProgramFiles%\Microsoft Visual Studio 11.0\VC\vcvarsall.bat" + ) do ( + if exist %%b ( + call %%b x86 + goto build + ) +) + +echo "Unable to detect suitable environment. Build may not succeed." -:CheckOS -IF EXIST "%PROGRAMFILES(X86)%" (GOTO 64BIT) ELSE (GOTO 32BIT) +:build -:64BIT -echo 64-bit... -set MSBuildPath="%ProgramFiles(x86)%\MSBuild\12.0\Bin\MSBuild.exe" -GOTO END - -:32BIT -echo 32-bit... -set MSBuildPath="%ProgramFiles%\MSBuild\12.0\Bin\MSBuild.exe" -GOTO END - -:END - -%MSBuildPath% SmartStoreNET.proj /p:DebugSymbols=false /p:DebugType=None /P:SlnName=SmartStoreNET /maxcpucount %* \ No newline at end of file +msbuild SmartStoreNET.proj /p:DebugSymbols=false /p:DebugType=None /P:SlnName=SmartStoreNET /maxcpucount %* \ No newline at end of file diff --git a/changelog.md b/changelog.md index fb272219ce..0dac88a9fd 100644 --- a/changelog.md +++ b/changelog.md @@ -1,9 +1,87 @@ # Release Notes +## SmartStore.NET 2.6 + +### Highlights +* Major improvements in Importer & Exporter: better field mapping, higher performance, bug fixes etc. +* 'PayPal PLUS' payment plugin +* 'paydirekt' payment plugin +* 'Viveum' payment plugin +* 'BeezUP' export provider +* (Dev) Publishing SmartStore.Web from within Visual Studio now deploys the project correctly. No need to execute ClickToBuild.cmd anymore. Just publish directly to any target, including Azure. + +### New Features +* #961 Fix "Open Redirection Vulnerability" +* #571 Option to display another checkbox on confirm page to let the customer accept that his email address can be handed over to a third party +* #870 Implement address import for customers (both billing & shipping address) +* #886 Add setting to hide manufacturer images on product detail page and to hide default image for manufacturers +* Import localized SEO names for product and categories +* #477 Implement option to specify the number of exported and imported pictures +* #859 Make checkout attributes suitable for multi-stores +* Product details: Select attribute and gift card values by query string parameters +* #950 make contact form comply with current German law + +### Improvements +* Major improvements in Importer: better field mapping, higher performance, bug fixes etc. +* (Dev) Publishing SmartStore.Web from within Visual Studio now deploys the project correctly. No need to execute ClickToBuild.cmd anymore. Just publish directly to any target, including Azure. +* Localization & SEO: language switcher now takes language specific SEO slugs into account when building links +* Smarter import of plugin resource files with graceful fallbacks (de-DE > de > de-* > en-US > en > en-* > *) +* (Perf) Faster language resource file import +* Exports the product detail link including the attribute query string when exporting attribute combinations +* #918 Compare products: Display base price information +* Export email attachments needs to be stored in database because the temp file may not exist anymore when sending the email +* #913 Use HTML5 Input types (tel, email) +* Added paging to frontend order list +* Added paging to backend checkout attribute list +* #977 Show PAngV base/delivery amount also +* Updated LiveEngage LiveChat plugin + +### Bugfixes +* TaskScheduler could fail polling when primary store url is an external IP address +* Fixed ajax cache issue when saving payment or shipping restrictions. Internet Explorer showed the old data state (before storage). +* "The provider failed at the Execute method: Member 'CurrentValues' cannot be called for the entity of type 'Product'" when exporting product attribute combinations +* Bundles without selected attributes could throw an exception on product detail page +* GMC feed did not export the product type and Billiger did not export shop_cat (category path) +* The error message of a payment provider when capturing a payment was not displayed +* Adding new shipping method threw an exception +* Attribute Values: Assigning IsPreselected to more than one value causes an error +* BizImporter: fixed redirection bug when default file extension in .biz wasn't .html +* Fixed: Export deployment emails were always send manually +* Manually notifying gift card recipient threw an exception +* Loading shipping by weight grid failed under SQL CE +* #949 Import: ProcessSlugs does not process explicitly specified "SeName", if product name did not change in an update scenario +* Customer import: Creates customer role duplicates for inserted customers +* GMC feed does not generate the sale price if the sale price is set for a future date +* Mobile devices: Fixed "Uncaught Error: Syntax error, unrecognized expression: :nth-child" +* Download nor sample download were removable when editing product +* Copied product must not share sample download of source product. Could produce "The DELETE statement conflicted with the REFERENCE constraint 'FK_dbo.Product_dbo.Download_SampleDownloadId'". +* #921 Specification attribute options with single quotation marks are causing a Javascript error +* #971 Product is added to cart automatically if it has a non-required file upload attribute +* #973 Bundle item upload is nowhere linked +* Base price in product list ignored PriceDisplayType (catalog settings) and possibly displayed the wrong base price info +* Private messages: Fixes "No route in the route table matches the supplied values" +* Payone: Hash string incorrect for frontend API payments where the order has more than 9 products +* Export mail notification: Download link not working if SSL is enabled +* Discount rule has spent amount including sub total option can cause wrong discount calculation if the cart contains a product several times +* #986 File uploads possible through /content/filemanager/index.html + + ## SmartStore.NET 2.5 +### Highlights + * New import/export framework (profiles, filters, mapping, projections, scheduling, deployment... just everything!) + * TaskScheduler: Rewritten from scratch to be suitable for Web Farms (including support for cron expressions) + * Payment and shipping methods by customer roles + * Restrict payment methods to countries + * Restrict payment methods to shipping methods + * Email attachment support for message templates + * Attach order invoice PDF automatically to order notification emails + * Overall performance increase + * Lots of bug fixes + ### New Features -* #701 Implement new export framework +* New export and import framework +* Import of customer and category data * #141 Payment and shipping methods by customer roles * #67 Restrict payment methods to countries * #94 Restrict payment methods to shipping methods @@ -11,7 +89,9 @@ * #584 Email attachment support for message templates * Attach order invoice PDF automatically to order notification emails * #526 Min/Max amount option for which the payment method should be offered during checkout -* (Dev) New _SyncMapping_ service: enables easier entity synchronization with external systems +* (Dev) New _SyncMapping_ service: enables easier entity synchronization with external systems +* (Dev) #792 ViewEngine: Enable vbhtml views per configuration +* (Dev) Plugin developers can now render child actions into a dynamically created special tab called 'Plugins' * #718 ShopConnector: Import option for "Published" and "Disable buy\wishlist button" * #702 Facebook and Twitter external authentication suitable for multi-stores * New scheduled task: Clear e-mail queue @@ -26,6 +106,28 @@ * #202 Implement option for product list 'default sort order' * #360 Import & export product variant combinations * #722 System > SEO Names: Implement editing of an UrlRecord +* Admin > System > System Info shows used application memory (RAM) +* Added option to make VATIN mandatory during customer registration +* #840 Activity log: Have option to exclude search engine activity +* #841 Activity log for deleting an order +* More settings to control creation of SEO names +* GMC feed: Supporting fields multipack, bundle, adult, energy efficiency class and custom label (0 to 4) +* #760 Setting to set a customer role for new registered users +* #800 Multi-store: Option to display all orders of all stores for customer in frontend +* #457 Added option to hide the default image for categories and products +* #451 Add message token for product shipping surcharge +* #436 Make %Order.Product(s)% token to link the product detail page and a add product thumbnail +* #339 Meta robots setting for page indexing of search engines +* PayPal: Option for API security protocol +* Product filter: Option to sort filter results by their display order rather than by number of matches +* Elmar Shopinfo: Option to export delivery time as availability +* #654 Place user agreement for downloadable files in checkout process +* #398 EU law: add 'revocation' form and revocation waiver for ESD +* #738 Implement download of pictures via URLs in product import +* Web-API: Bridge to import framework: uploading import files to import profile directory +* Setting to round down calculated reward points +* #695 Implement checkbox in checkout to let customers subscribe to newsletters +* #495 Implement option to search product detail description by default ### Improvements * (Perf) Implemented static caches for URL aliases and localized properties. Increases app startup and request speed by up to 30%. @@ -49,12 +151,29 @@ * #62 free shipping info on product detail page * Display base price in CompactProductBox * Automatically redirect to referrer after login +* #826 Image gallery: the viewport height was fixed to 300 px, but now respects MediaSettings > ImageSize. * #249 Make UI editor for 'SeoSettings.ExtraRobotsDisallows' * Debitoor: Customer VAT number not transmitted anymore because it appears on the Debitoor invoice. * #778 Web-API: Increase MaxExpansionDepth for using expand pathes * #767 Remove assignments to a grouped product if the grouped product is deleted * #773 Reduce number of guest records created by search engine requests * #791 Preselected attributes or attribute combinations should always be appended as querystring to product page links +* Simplified handling of SEO names +* URLs are not converted to lower case anymore +* Product grid sortable by name, price and created on +* #26 Display company or name in order list +* Added inline editing of country grid +* #790 Improved language editing +* #843 Implement a product picker +* #850 Use new product picker for selecting required products +* Trusted Shops: badge will be displayed in mobile themes, payment info link replaced compare list link in footer +* Product filter: Specification attributes are sorted by display order rather than alphabetically by name +* #856 Don't route topics which are excluded from sitemap +* #851 Replace reCAPTCHA with "I'm not a robot" CAPTCHA +* #713 Display gift card remaining amount in frontend order details and order messages +* #736 Render PayPal Express Button in minibasket +* PayPal: Support for partial refunds +* Offline credit card payment: Option to exclude credit card types ### Bugfixes * #523 Redirecting to payment provider performed by core instead of plugin @@ -69,6 +188,19 @@ * #776 Preview: Manufacturer and Product in Multi Store * #755 Some methods still loading all products in one go * #796 Selected specification in product filter mask is displayed with default language (not localized) +* #805 Product filter is reset if 'product sorting' or 'view mode' or 'amount of displayed products per page' is changed +* Hide link to a topic page if it is limited to stores +* #829 Activity log: Searching by customer email out of function +* Product import: Store mappings were not applied when inserting new records +* Faulty permission handling in ajax grid actions (no message, infinite loading icon) +* Grouped products: Display order was not correct +* Deletion of a customer could delete all newsletter subscriptions +* PayPal: Fixed "The request was aborted: Could not create SSL/TLS secure channel." +* PayPal Express: Void and refund out of function ("The transaction id is not valid") +* Customer could not delete his avatar +* Facebook authentication: Email missing in verification +* Attribute with a product linkage throws exception if added to cart +* Number of products per product tag could be incorrect in a multi-store ## SmartStore.NET 2.2.2 diff --git a/lib/SmartStore.Licensing/SmartStore.Licensing.dll b/lib/SmartStore.Licensing/SmartStore.Licensing.dll index 51e391ce23..8acdf39c24 100644 Binary files a/lib/SmartStore.Licensing/SmartStore.Licensing.dll and b/lib/SmartStore.Licensing/SmartStore.Licensing.dll differ diff --git a/src/AssemblySharedInfo.cs b/src/AssemblySharedInfo.cs index f4e2566c4e..79225de4c7 100644 --- a/src/AssemblySharedInfo.cs +++ b/src/AssemblySharedInfo.cs @@ -1,6 +1,4 @@ using System.Reflection; -using System.Runtime.InteropServices; -using System.Security; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information @@ -9,6 +7,6 @@ [assembly: AssemblyDescription("SmartStore.NET")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("SmartStore AG")] -[assembly: AssemblyCopyright("Copyright © SmartStore AG 2015")] +[assembly: AssemblyCopyright("Copyright © SmartStore AG 2016")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/src/AssemblyVersionInfo.cs b/src/AssemblyVersionInfo.cs index 0c8dfa2cfb..f74f0edf78 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("2.2.0.0")] +[assembly: AssemblyVersion("2.6.0.0")] -[assembly: AssemblyFileVersion("2.2.0.0")] -[assembly: AssemblyInformationalVersion("2.2.2.0")] +[assembly: AssemblyFileVersion("2.6.0.0")] +[assembly: AssemblyInformationalVersion("2.6.0.0")] diff --git a/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs b/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs index 9ad87c3758..b02ea8dd58 100644 --- a/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs +++ b/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs @@ -5,7 +5,6 @@ using System.Web.Hosting; using Autofac; using SmartStore.Core.Infrastructure; -using SmartStore.Utilities; namespace SmartStore.Core.Async { @@ -23,9 +22,9 @@ public static CancellationToken AppShutdownCancellationToken } /// - /// Executes an async Task method which has a void return value synchronously + /// Executes an async Task method which has a void return value synchronously /// - /// Task method to execute + /// Task method to execute public static void RunSync(Func func) { var oldContext = SynchronizationContext.Current; @@ -53,10 +52,10 @@ public static void RunSync(Func func) } /// - /// Executes an async Task method which has a T return type synchronously + /// Executes an async Task method which has a TResult return type synchronously /// - /// Return Type - /// Task method to execute + /// Return Type + /// Task method to execute /// public static TResult RunSync(Func> func) { @@ -120,7 +119,7 @@ public static Task Run(Action action, Cancell var t = Task.Factory.StartNew(() => { var accessor = EngineContext.Current.ContainerManager.ScopeAccessor; - using (var scope = accessor.BeginContextAwareScope()) + using (accessor.BeginContextAwareScope()) { action(accessor.GetLifetimeScope(null), ct); } @@ -143,7 +142,7 @@ public static Task Run(Action action, var t = Task.Factory.StartNew((o) => { var accessor = EngineContext.Current.ContainerManager.ScopeAccessor; - using (var scope = accessor.BeginContextAwareScope()) + using (accessor.BeginContextAwareScope()) { action(accessor.GetLifetimeScope(null), ct, o); } @@ -192,7 +191,7 @@ public static Task Run(Func { var accessor = EngineContext.Current.ContainerManager.ScopeAccessor; - using (var scope = accessor.BeginContextAwareScope()) + using (accessor.BeginContextAwareScope()) { return function(accessor.GetLifetimeScope(null), ct); } @@ -215,7 +214,7 @@ public static Task Run(Func { var accessor = EngineContext.Current.ContainerManager.ScopeAccessor; - using (var scope = accessor.BeginContextAwareScope()) + using (accessor.BeginContextAwareScope()) { return function(accessor.GetLifetimeScope(null), ct, o); } @@ -229,10 +228,11 @@ public static Task Run(Func> _items = new Queue>(); + public Exception InnerException { get; set; } - readonly AutoResetEvent workItemsWaiting = new AutoResetEvent(false); - readonly Queue> items = new Queue>(); public override void Send(SendOrPostCallback d, object state) { @@ -241,28 +241,28 @@ public override void Send(SendOrPostCallback d, object state) public override void Post(SendOrPostCallback d, object state) { - lock (items) + lock (_items) { - items.Enqueue(Tuple.Create(d, state)); + _items.Enqueue(Tuple.Create(d, state)); } - workItemsWaiting.Set(); + _workItemsWaiting.Set(); } public void EndMessageLoop() { - Post(_ => done = true, null); + Post(_ => _done = true, null); } public void BeginMessageLoop() { - while (!done) + while (!_done) { Tuple task = null; - lock (items) + lock (_items) { - if (items.Count > 0) + if (_items.Count > 0) { - task = items.Dequeue(); + task = _items.Dequeue(); } } if (task != null) @@ -275,7 +275,7 @@ public void BeginMessageLoop() } else { - workItemsWaiting.WaitOne(); + _workItemsWaiting.WaitOne(); } } } @@ -290,8 +290,8 @@ public override SynchronizationContext CreateCopy() internal class BackgroundWorkHost : IRegisteredObject { - private CancellationTokenSource _shutdownCancellationTokenSource = new CancellationTokenSource(); - private int _numRunningWorkItems = 0; + private readonly CancellationTokenSource _shutdownCancellationTokenSource = new CancellationTokenSource(); + private int _numRunningWorkItems; public BackgroundWorkHost() { diff --git a/src/Libraries/SmartStore.Core/Async/AsyncState.cs b/src/Libraries/SmartStore.Core/Async/AsyncState.cs index 64d4e91e14..ce99542612 100644 --- a/src/Libraries/SmartStore.Core/Async/AsyncState.cs +++ b/src/Libraries/SmartStore.Core/Async/AsyncState.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; using System.Runtime.Caching; -using System.Text; using System.Threading; namespace SmartStore.Core.Async @@ -11,7 +8,7 @@ namespace SmartStore.Core.Async public class AsyncState { - private readonly static AsyncState s_instance = new AsyncState(); + private static readonly AsyncState s_instance = new AsyncState(); private readonly MemoryCache _cache = new MemoryCache("SmartStore.AsyncState"); private AsyncState() @@ -45,7 +42,7 @@ public IEnumerable GetAll() var value = kvp.Value as StateInfo; if (value != null && value.Progress != null) { - yield return (T)(value.Progress); + yield return (T)value.Progress; } } } @@ -68,7 +65,7 @@ private T Get(out CancellationTokenSource cancelTokenSource, string name = nu if (value != null) { cancelTokenSource = value.CancellationTokenSource; - return (T)(value.Progress); + return (T)value.Progress; } return default(T); @@ -77,7 +74,7 @@ private T Get(out CancellationTokenSource cancelTokenSource, string name = nu public void Set(T state, string name = null, bool neverExpires = false) { Guard.ArgumentNotNull(() => state); - this.Set(state, null, name, neverExpires); + Set(state, null, name, neverExpires); } public void Update(Action update, string name = null) @@ -101,7 +98,7 @@ public void SetCancelTokenSource(CancellationTokenSource cancelTokenSource, s { Guard.ArgumentNotNull(() => cancelTokenSource); - this.Set(default(T), cancelTokenSource, name); + Set(default(T), cancelTokenSource, name); } private void Set(T state, CancellationTokenSource cancelTokenSource, string name = null, bool neverExpires = false) @@ -129,8 +126,6 @@ private void Set(T state, CancellationTokenSource cancelTokenSource, string n key, value ?? new StateInfo { Progress = state, CancellationTokenSource = cancelTokenSource }, policy); - - var xx = _cache.Get(key); } public bool Remove(string name = null) diff --git a/src/Libraries/SmartStore.Core/BaseEntity.cs b/src/Libraries/SmartStore.Core/BaseEntity.cs index f555ef1e8d..0c90dc9a9a 100644 --- a/src/Libraries/SmartStore.Core/BaseEntity.cs +++ b/src/Libraries/SmartStore.Core/BaseEntity.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.Serialization; @@ -12,8 +13,7 @@ namespace SmartStore.Core /// [DataContract] public abstract partial class BaseEntity - { - + { /// /// Gets or sets the entity identifier /// @@ -21,7 +21,8 @@ public abstract partial class BaseEntity [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } - public Type GetUnproxiedType() + [SuppressMessage("ReSharper", "PossibleNullReferenceException")] + public Type GetUnproxiedType() { var t = GetType(); if (t.AssemblyQualifiedName.StartsWith("System.Data.Entity.")) @@ -57,16 +58,17 @@ protected virtual bool Equals(BaseEntity other) if (HasSameNonDefaultIds(other)) { var otherType = other.GetUnproxiedType(); - var thisType = this.GetUnproxiedType(); + var thisType = GetUnproxiedType(); return thisType.Equals(otherType); } return false; } - public override int GetHashCode() + [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] + public override int GetHashCode() { - if (this.IsTransientRecord()) + if (IsTransientRecord()) { return base.GetHashCode(); } @@ -77,7 +79,7 @@ public override int GetHashCode() // It's possible for two objects to return the same hash code based on // identically valued properties, even if they're of two different types, // so we include the object's type in the hash calculation - int hashCode = GetUnproxiedType().GetHashCode(); + var hashCode = GetUnproxiedType().GetHashCode(); return (hashCode * 31) ^ Id.GetHashCode(); } } diff --git a/src/Libraries/SmartStore.Core/Caching/AspNetCache.cs b/src/Libraries/SmartStore.Core/Caching/AspNetCache.cs index e75ef70b3b..4af855729a 100644 --- a/src/Libraries/SmartStore.Core/Caching/AspNetCache.cs +++ b/src/Libraries/SmartStore.Core/Caching/AspNetCache.cs @@ -4,14 +4,13 @@ using System.Linq; using System.Web; using System.Web.Caching; -using SmartStore.Core.Fakes; namespace SmartStore.Core.Caching { public partial class AspNetCache : ICache { - private const string REGION_NAME = "$$SmartStoreNET$$"; + private const string RegionName = "$$SmartStoreNET$$"; public IEnumerable> Entries { @@ -22,9 +21,9 @@ public IEnumerable> Entries return from entry in HttpRuntime.Cache.Cast() let key = entry.Key.ToString() - where key.StartsWith(REGION_NAME) + where key.StartsWith(RegionName) select new KeyValuePair( - key.Substring(REGION_NAME.Length), + key.Substring(RegionName.Length), entry.Value); } } @@ -72,7 +71,7 @@ public void Remove(string key) public static string BuildKey(string key) { - return key.HasValue() ? REGION_NAME + key : null; + return key.HasValue() ? RegionName + key : null; } public bool IsSingleton diff --git a/src/Libraries/SmartStore.Core/Caching/DefaultCacheManager.cs b/src/Libraries/SmartStore.Core/Caching/DefaultCacheManager.cs index ed49180ca8..6949aec542 100644 --- a/src/Libraries/SmartStore.Core/Caching/DefaultCacheManager.cs +++ b/src/Libraries/SmartStore.Core/Caching/DefaultCacheManager.cs @@ -12,7 +12,7 @@ public static class ICacheManagerExtensions { public static T Get(this ICacheManager cacheManager, string key) { - return cacheManager.Get(key, () => { return default(T); }); + return cacheManager.Get(key, () => default(T)); } } @@ -24,7 +24,7 @@ public partial class CacheManager : ICacheManager where TCache : ICache // Wwe put a special string into cache if value is null, // otherwise our 'Contains()' would always return false, // which is bad if we intentionally wanted to save NULL values. - private const string FAKE_NULL = "__[NULL]__"; + private const string FakeNull = "__[NULL]__"; public CacheManager(Func fn) { @@ -58,7 +58,7 @@ private T GetExisting(string key) { var value = _cache.Get(key); - if (value.Equals(FAKE_NULL)) + if (value.Equals(FakeNull)) return default(T); return (T)_cache.Get(key); @@ -70,7 +70,7 @@ public void Set(string key, object value, int? cacheTime = null) using (EnterWriteLock()) { - _cache.Set(key, value ?? FAKE_NULL, cacheTime); + _cache.Set(key, value ?? FakeNull, cacheTime); } } diff --git a/src/Libraries/SmartStore.Core/Caching/ICache.cs b/src/Libraries/SmartStore.Core/Caching/ICache.cs index a250d20bc0..b9fc134a8d 100644 --- a/src/Libraries/SmartStore.Core/Caching/ICache.cs +++ b/src/Libraries/SmartStore.Core/Caching/ICache.cs @@ -26,7 +26,7 @@ public interface ICache /// Adds the cache item with the specified key /// /// Key - /// Data + /// Data /// Cache time in minutes void Set(string key, object value, int? cacheTime); diff --git a/src/Libraries/SmartStore.Core/Caching/NullCache.cs b/src/Libraries/SmartStore.Core/Caching/NullCache.cs index ea96b38717..f02414d682 100644 --- a/src/Libraries/SmartStore.Core/Caching/NullCache.cs +++ b/src/Libraries/SmartStore.Core/Caching/NullCache.cs @@ -7,8 +7,7 @@ namespace SmartStore.Core.Caching /// public partial class NullCache : ICacheManager { - - private readonly static ICacheManager s_instance = new NullCache(); + private static readonly ICacheManager s_instance = new NullCache(); public static ICacheManager Instance { diff --git a/src/Libraries/SmartStore.Core/Caching/RequestCache.cs b/src/Libraries/SmartStore.Core/Caching/RequestCache.cs index a509f3d76b..7ab183061f 100644 --- a/src/Libraries/SmartStore.Core/Caching/RequestCache.cs +++ b/src/Libraries/SmartStore.Core/Caching/RequestCache.cs @@ -9,7 +9,7 @@ namespace SmartStore.Core.Caching public partial class RequestCache : ICache { - private const string REGION_NAME = "$$SmartStoreNET$$"; + private const string RegionName = "$$SmartStoreNET$$"; private readonly HttpContextBase _context; public RequestCache(HttpContextBase context) @@ -39,9 +39,9 @@ public IEnumerable> Entries string key = enumerator.Key as string; if (key == null) continue; - if (key.StartsWith(REGION_NAME)) + if (key.StartsWith(RegionName)) { - yield return new KeyValuePair(key.Substring(REGION_NAME.Length), enumerator.Value); + yield return new KeyValuePair(key.Substring(RegionName.Length), enumerator.Value); } } } @@ -90,7 +90,7 @@ public void Remove(string key) private string BuildKey(string key) { - return key.HasValue() ? REGION_NAME + key : null; + return key.HasValue() ? RegionName + key : null; } public bool IsSingleton diff --git a/src/Libraries/SmartStore.Core/Collections/LazyMultimap.cs b/src/Libraries/SmartStore.Core/Collections/LazyMultimap.cs index f7dac8f097..789c363da0 100644 --- a/src/Libraries/SmartStore.Core/Collections/LazyMultimap.cs +++ b/src/Libraries/SmartStore.Core/Collections/LazyMultimap.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace SmartStore.Collections @@ -10,25 +11,22 @@ namespace SmartStore.Collections /// public class LazyMultimap : Multimap { - private Func> _load; - private List _loaded; // to avoid database round trips with empty results + private readonly Func> _load; + private readonly List _loaded; // to avoid database round trips with empty results private List _collect; - private int _roundTripCount; + //private int _roundTripCount; /// /// Constructor /// - /// int[] keys like Entity.Id, Multimap> delegate to load data + /// int[] keys like Entity.Id, Multimap{int, T}> delegate to load data /// Keys of eager loaded data public LazyMultimap(Func> load, IEnumerable collect = null) { _load = load; _loaded = new List(); - if (collect == null) - _collect = new List(); - else - _collect = new List(collect); + _collect = collect == null ? new List() : new List(collect); } protected virtual void Load(IEnumerable keys) @@ -44,8 +42,7 @@ protected virtual void Load(IEnumerable keys) if (loadKeys.Any()) { - ++_roundTripCount; - + //++_roundTripCount; //Debug.WriteLine("Round trip {0} of {1}: {2}", _roundTripCount, typeof(T).Name, string.Join(",", loadKeys.OrderBy(x => x))); var items = _load(loadKeys); @@ -92,6 +89,7 @@ public virtual ICollection Load(int key) /// Collect keys for combined loading /// /// Data keys + [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] public virtual void Collect(IEnumerable keys) { if (keys != null && keys.Any()) @@ -101,7 +99,7 @@ public virtual void Collect(IEnumerable keys) } /// - /// Collect single key for combinde loading + /// Collect single key for combined loading /// /// Data key public virtual void Collect(int key) @@ -111,5 +109,14 @@ public virtual void Collect(int key) _collect.Add(key); } } + + public override void Clear() + { + _loaded.Clear(); + _collect.Clear(); + //_roundTripCount = 0; + + base.Clear(); + } } } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/MostRecentlyUsedList.cs b/src/Libraries/SmartStore.Core/Collections/MostRecentlyUsedList.cs similarity index 89% rename from src/Libraries/SmartStore.Core/Infrastructure/MostRecentlyUsedList.cs rename to src/Libraries/SmartStore.Core/Collections/MostRecentlyUsedList.cs index 1029a802bf..6d81d0bc3a 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/MostRecentlyUsedList.cs +++ b/src/Libraries/SmartStore.Core/Collections/MostRecentlyUsedList.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; -namespace SmartStore.Core.Infrastructure +namespace SmartStore.Collections { public class MostRecentlyUsedList : IEnumerable { @@ -26,7 +26,7 @@ public MostRecentlyUsedList(IEnumerable collection, int maxSize) public MostRecentlyUsedList(string collection, int maxSize) { _maxSize = maxSize; - _mru = (collection.SplitSafe(Delimiter) as IEnumerable).ToList(); + _mru = collection.SplitSafe(Delimiter).Cast().ToList(); Normalize(); } @@ -42,7 +42,7 @@ public MostRecentlyUsedList(IEnumerable collection, T newItem, int maxSize) public MostRecentlyUsedList(string collection, T newItem, int maxSize) { _maxSize = maxSize; - _mru = (collection.SplitSafe(Delimiter) as IEnumerable).ToList(); + _mru = collection.SplitSafe(Delimiter).Cast().ToList(); Add(newItem); } diff --git a/src/Libraries/SmartStore.Core/Collections/QuerystringBuilder.cs b/src/Libraries/SmartStore.Core/Collections/Querystring.cs similarity index 93% rename from src/Libraries/SmartStore.Core/Collections/QuerystringBuilder.cs rename to src/Libraries/SmartStore.Core/Collections/Querystring.cs index 76606ef24d..9df23fc868 100644 --- a/src/Libraries/SmartStore.Core/Collections/QuerystringBuilder.cs +++ b/src/Libraries/SmartStore.Core/Collections/Querystring.cs @@ -1,6 +1,7 @@ using System.Text; using System.Web; using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; namespace SmartStore.Collections { @@ -28,6 +29,7 @@ public static QueryString Current /// /// the string to extract the querystring from /// a string representing only the querystring + [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")] public static string ExtractQuerystring(string s) { if (!string.IsNullOrEmpty(s)) @@ -45,22 +47,23 @@ public static string ExtractQuerystring(string s) /// /// the string to parse /// the QueryString object - public QueryString FillFromString(string s) + public QueryString FillFromString(string s, bool urlDecode = false) { base.Clear(); if (string.IsNullOrEmpty(s)) { return this; } + foreach (string keyValuePair in ExtractQuerystring(s).Split('&')) { if (string.IsNullOrEmpty(keyValuePair)) { continue; } + string[] split = keyValuePair.Split('='); - base.Add(split[0], - split.Length == 2 ? split[1] : ""); + base.Add(split[0], split.Length == 2 ? (urlDecode ? HttpUtility.UrlDecode(split[1]) : split[1]) : ""); } return this; } @@ -73,7 +76,7 @@ public QueryString FromCurrent() { if (HttpContext.Current != null) { - return FillFromString(HttpContext.Current.Request.QueryString.ToString()); + return FillFromString(HttpContext.Current.Request.QueryString.ToString(), true); } base.Clear(); return this; @@ -182,7 +185,7 @@ public override string ToString() { if (!string.IsNullOrEmpty(base.Keys[i])) { - foreach (string val in base[base.Keys[i]].Split(',')) + foreach (string val in base[base.Keys[i]].EmptyNull().Split(',')) { builder.Append((builder.Length == 0) ? "?" : "&").Append( HttpUtility.UrlEncode(base.Keys[i])).Append("=").Append(val); diff --git a/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs b/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs new file mode 100644 index 0000000000..79a4362f30 --- /dev/null +++ b/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Linq.Expressions; +using System.Collections.Concurrent; + +namespace SmartStore.ComponentModel +{ + public class FastActivator + { + private static readonly ConcurrentDictionary _activatorsCache = new ConcurrentDictionary(); + + public FastActivator(ConstructorInfo constructorInfo) + { + Guard.ArgumentNotNull(() => constructorInfo); + + Constructor = constructorInfo; + Invoker = MakeFastInvoker(constructorInfo); + ParameterTypes = constructorInfo.GetParameters().Select(p => p.ParameterType).ToArray(); + } + + /// + /// Gets the backing . + /// + public ConstructorInfo Constructor { get; private set; } + + /// + /// Gets the parameter types from the backing + /// + public Type[] ParameterTypes { get; private set; } + + /// + /// Gets the constructor invoker. + /// + public Func Invoker { get; private set; } + + /// + /// Creates an instance of the type using the specified parameters. + /// + /// A reference to the newly created object. + public object Activate(params object[] parameters) + { + return Invoker(parameters); + } + + /// + /// Creates a single fast constructor invoker. The result is not cached. + /// + /// constructorInfo to create invoker for. + /// a fast invoker. + public static Func MakeFastInvoker(ConstructorInfo constructorInfo) + { + var paramsInfo = constructorInfo.GetParameters(); + + var parametersExpression = Expression.Parameter(typeof(object[]), "args"); + var argumentsExpression = new Expression[paramsInfo.Length]; + + for (int paramIndex = 0; paramIndex < paramsInfo.Length; paramIndex++) + { + var indexExpression = Expression.Constant(paramIndex); + var parameterType = paramsInfo[paramIndex].ParameterType; + + var parameterIndexExpression = Expression.ArrayIndex(parametersExpression, indexExpression); + var convertExpression = Expression.Convert(parameterIndexExpression, parameterType); + argumentsExpression[paramIndex] = convertExpression; + + if (!parameterType.GetTypeInfo().IsValueType) + continue; + + var nullConditionExpression = Expression.Equal(parameterIndexExpression, Expression.Constant(null)); + argumentsExpression[paramIndex] = Expression.Condition(nullConditionExpression, Expression.Default(parameterType), convertExpression); + } + + var newExpression = Expression.New(constructorInfo, argumentsExpression); + var lambda = Expression.Lambda>(newExpression, parametersExpression); + + return lambda.Compile(); + } + + #region Static + + /// + /// Creates and caches fast constructor invokers + /// + /// The type to extract fast constructor invokers for + /// A cached array of all public instance constructors from the given type. + /// The parameterless default constructor is always excluded from the list of activators + public static FastActivator[] GetActivators(Type type) + { + return GetActivatorsCore(type); + } + + private static FastActivator[] GetActivatorsCore(Type type) + { + FastActivator[] activators; + if (!_activatorsCache.TryGetValue(type, out activators)) + { + var candidates = GetCandidateConstructors(type); + activators = candidates.Select(c => new FastActivator(c)).ToArray(); + _activatorsCache.TryAdd(type, activators); + } + + return activators; + } + + /// + /// Creates an instance of the specified type using the constructor that best matches the specified parameters. + /// + /// The type of object to create. + /// + /// An array of arguments that match in number, order, and type the parameters of the constructor to invoke. + /// If args is an empty array or null, the constructor that takes no parameters (the default constructor) is invoked. + /// + /// A reference to the newly created object. + public static T CreateInstance(params object[] args) + { + return (T)CreateInstance(typeof(T), args); + } + + /// + /// Creates an instance of the specified type using the constructor that best matches the specified parameters. + /// + /// The type of object to create. + /// + /// An array of arguments that match in number, order, and type the parameters of the constructor to invoke. + /// If args is an empty array or null, the constructor that takes no parameters (the default constructor) is invoked. + /// + /// A reference to the newly created object. + public static object CreateInstance(Type type, params object[] args) + { + Guard.ArgumentNotNull(type, "type"); + + if (args == null || args.Length == 0) + { + // don't struggle with FastActivator: native reflection is really fast with default constructor! + return Activator.CreateInstance(type); + } + + var activators = GetActivatorsCore(type); + var matchingActivator = FindMatchingActivatorCore(activators, type, args); + + if (matchingActivator == null) + { + throw new ArgumentException("No matching contructor was found for the given arguments.", "args"); + } + + return matchingActivator.Activate(args); + } + + public static FastActivator FindMatchingActivator(Type type, params object[] args) + { + var activators = GetActivatorsCore(type); + var matchingActivator = FindMatchingActivatorCore(activators, type, args); + + return matchingActivator; + } + + private static FastActivator FindMatchingActivatorCore(FastActivator[] activators, Type type, object[] args) + { + if (activators.Length == 0) + { + return null; + } + + if (activators.Length == 1) + { + // this seems to be bad design, but it's on purpose for performance reasons. + // In nearly ALL cases there is only one constructor. + return activators[0]; + } + + var argTypes = args.Select(x => x.GetType()).ToArray(); + var constructor = type.GetConstructor( + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly, + null, + argTypes, + null); + + if (constructor != null) + { + var matchingActivator = activators.FirstOrDefault(a => a.Constructor == constructor); + return matchingActivator; + } + + return null; + } + + private static IEnumerable GetCandidateConstructors(Type type) + { + var constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + return constructors.Where(c => c.GetParameters().Length > 0); + } + + private static void CheckIsValidType(Type type) + { + if (type.IsAbstract || type.IsInterface) + { + throw new ArgumentException("The type to create activators for must be concrete. Type: {0}".FormatInvariant(type.ToString()), "type"); + } + } + + #endregion + } +} diff --git a/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs b/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs new file mode 100644 index 0000000000..068b84f008 --- /dev/null +++ b/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs @@ -0,0 +1,671 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace SmartStore.ComponentModel +{ + public enum PropertyCachingStrategy + { + /// + /// Don't cache FastProperty instances + /// + Uncached = 0, + /// + /// Always cache FastProperty instances + /// + Cached = 1, + /// + /// Always cache FastProperty instances. PLUS cache all other properties of the declaring type. + /// + EagerCached = 2 + } + + public class FastProperty + { + // Delegate type for a by-ref property getter + private delegate TValue ByRefFunc(ref TDeclaringType arg); + + private static readonly MethodInfo CallPropertyGetterOpenGenericMethod = typeof(FastProperty).GetTypeInfo().GetDeclaredMethod("CallPropertyGetter"); + private static readonly MethodInfo CallPropertyGetterByReferenceOpenGenericMethod = typeof(FastProperty).GetTypeInfo().GetDeclaredMethod("CallPropertyGetterByReference"); + private static readonly MethodInfo CallNullSafePropertyGetterOpenGenericMethod = typeof(FastProperty).GetTypeInfo().GetDeclaredMethod("CallNullSafePropertyGetter"); + private static readonly MethodInfo CallNullSafePropertyGetterByReferenceOpenGenericMethod = typeof(FastProperty).GetTypeInfo().GetDeclaredMethod("CallNullSafePropertyGetterByReference"); + private static readonly MethodInfo CallPropertySetterOpenGenericMethod = typeof(FastProperty).GetTypeInfo().GetDeclaredMethod("CallPropertySetter"); + + private static readonly ConcurrentDictionary _singlePropertiesCache = new ConcurrentDictionary(); + + // Using an array rather than IEnumerable, as target will be called on the hot path numerous times. + private static readonly ConcurrentDictionary> _propertiesCache = new ConcurrentDictionary>(); + private static readonly ConcurrentDictionary> _visiblePropertiesCache = new ConcurrentDictionary>(); + + private Action _valueSetter; + private bool? _isPublicSettable; + private bool? _isSequenceType; + + /// + /// Initializes a . + /// This constructor does not cache the helper. For caching, use . + /// + [SuppressMessage("ReSharper", "VirtualMemberCallInContructor")] + public FastProperty(PropertyInfo property) + { + Guard.ArgumentNotNull(() => property); + + Property = property; + Name = property.Name; + ValueGetter = MakeFastPropertyGetter(property); + } + + /// + /// Gets the backing . + /// + public PropertyInfo Property { get; private set; } + + /// + /// Gets (or sets in derived types) the property name. + /// + public virtual string Name { get; protected set; } + + /// + /// Gets the property value getter. + /// + public Func ValueGetter { get; private set; } + + public bool IsPublicSettable + { + get + { + if (!_isPublicSettable.HasValue) + { + _isPublicSettable = Property.CanWrite && Property.GetSetMethod(false) != null; + } + return _isPublicSettable.Value; + } + } + + public bool IsSequenceType + { + get + { + if (!_isSequenceType.HasValue) + { + _isSequenceType = Property.PropertyType != typeof(string) && Property.PropertyType.IsSubClass(typeof(IEnumerable<>)); + } + return _isSequenceType.Value; + } + } + + /// + /// Gets the property value setter. + /// + public Action ValueSetter + { + get + { + if (_valueSetter == null) + { + // We'll allow safe races here. + _valueSetter = MakeFastPropertySetter(Property); + } + + return _valueSetter; + } + } + + /// + /// Returns the property value for the specified . + /// + /// The object whose property value will be returned. + /// The property value. + public object GetValue(object instance) + { + return ValueGetter(instance); + } + + /// + /// Sets the property value for the specified . + /// + /// The object whose property value will be set. + /// The property value. + public void SetValue(object instance, object value) + { + ValueSetter(instance, value); + } + + /// + /// Creates and caches fast property helpers that expose getters for every public get property on the + /// underlying type. + /// + /// the instance to extract property accessors for. + /// A cached array of all public property getters from the underlying type of target instance. + /// + public static IReadOnlyDictionary GetProperties(object instance, PropertyCachingStrategy cachingStrategy = PropertyCachingStrategy.Cached) + { + return GetProperties(instance.GetType()); + } + + /// + /// Creates and caches fast property helpers that expose getters for every public get property on the + /// specified type. + /// + /// The type to extract property accessors for. + /// A cached array of all public property getters from the type of target instance. + /// + public static IReadOnlyDictionary GetProperties(Type type, PropertyCachingStrategy cachingStrategy = PropertyCachingStrategy.Cached) + { + var propertiesCache = cachingStrategy > PropertyCachingStrategy.Uncached ? _propertiesCache : CreateVolatileCache(); + + return (IReadOnlyDictionary)GetProperties(type, CreateInstance, propertiesCache); + } + + /// + /// + /// Creates and caches fast property helpers that expose getters for every non-hidden get property + /// on the specified type. + /// + /// + /// excludes properties defined on base types that have been + /// hidden by definitions using the new keyword. + /// + /// + /// The instance to extract property accessors for. + /// + /// A cached array of all public property getters from the instance's type. + /// + public static IReadOnlyDictionary GetVisibleProperties(object instance, PropertyCachingStrategy cachingStrategy = PropertyCachingStrategy.Cached) + { + var propertiesCache = cachingStrategy > PropertyCachingStrategy.Uncached ? _propertiesCache : CreateVolatileCache(); + var visiblePropertiesCache = cachingStrategy > PropertyCachingStrategy.Uncached ? _visiblePropertiesCache : CreateVolatileCache(); + + return (IReadOnlyDictionary)GetVisibleProperties(instance.GetType(), CreateInstance, propertiesCache, visiblePropertiesCache); + } + + /// + /// + /// Creates and caches fast property helpers that expose getters for every non-hidden get property + /// on the specified type. + /// + /// + /// excludes properties defined on base types that have been + /// hidden by definitions using the new keyword. + /// + /// + /// The type to extract property accessors for. + /// + /// A cached array of all public property getters from the type. + /// + public static IReadOnlyDictionary GetVisibleProperties(Type type, PropertyCachingStrategy cachingStrategy = PropertyCachingStrategy.Cached) + { + var propertiesCache = cachingStrategy > PropertyCachingStrategy.Uncached ? _propertiesCache : CreateVolatileCache(); + var visiblePropertiesCache = cachingStrategy > PropertyCachingStrategy.Uncached ? _visiblePropertiesCache : CreateVolatileCache(); + + return (IReadOnlyDictionary)GetVisibleProperties(type, CreateInstance, propertiesCache, visiblePropertiesCache); + } + + public static FastProperty GetProperty( + Expression> property, + PropertyCachingStrategy cachingStrategy = PropertyCachingStrategy.Cached) + { + return GetProperty(property.ExtractPropertyInfo(), cachingStrategy); + } + + public static FastProperty GetProperty( + Type type, + string propertyName, + PropertyCachingStrategy cachingStrategy = PropertyCachingStrategy.Cached) + { + Guard.ArgumentNotNull(() => type); + Guard.ArgumentNotEmpty(() => propertyName); + + FastProperty fastProperty = null; + + if (TryGetCachedProperty(type, propertyName, cachingStrategy == PropertyCachingStrategy.EagerCached, out fastProperty)) + { + return fastProperty; + } + + var key = new PropertyKey(type, propertyName); + if (!_singlePropertiesCache.TryGetValue(key, out fastProperty)) + { + var pi = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance | BindingFlags.IgnoreCase); + if (pi != null) + { + fastProperty = CreateInstance(pi); + if (cachingStrategy > PropertyCachingStrategy.Uncached) + { + _singlePropertiesCache.TryAdd(key, fastProperty); + } + } + } + + return fastProperty; + } + + public static FastProperty GetProperty( + PropertyInfo propertyInfo, + PropertyCachingStrategy cachingStrategy = PropertyCachingStrategy.Cached) + { + Guard.ArgumentNotNull(() => propertyInfo); + + FastProperty fastProperty = null; + + if (TryGetCachedProperty(propertyInfo.ReflectedType, propertyInfo.Name, cachingStrategy == PropertyCachingStrategy.EagerCached, out fastProperty)) + { + return fastProperty; + } + + var key = new PropertyKey(propertyInfo.ReflectedType, propertyInfo.Name); + if (!_singlePropertiesCache.TryGetValue(key, out fastProperty)) + { + fastProperty = CreateInstance(propertyInfo); + if (cachingStrategy > PropertyCachingStrategy.Uncached) + { + _singlePropertiesCache.TryAdd(key, fastProperty); + } + } + + return fastProperty; + } + + private static bool TryGetCachedProperty( + Type type, + string propertyName, + bool eagerCached, + out FastProperty fastProperty) + { + fastProperty = null; + IDictionary allProperties; + + if (eagerCached) + { + allProperties = (IDictionary)GetProperties(type); + allProperties.TryGetValue(propertyName, out fastProperty); + } + + if (fastProperty == null && _propertiesCache.TryGetValue(type, out allProperties)) + { + allProperties.TryGetValue(propertyName, out fastProperty); + } + + return fastProperty != null; + } + + /// + /// Creates a single fast property getter. The result is not cached. + /// + /// propertyInfo to extract the getter for. + /// a fast getter. + /// + /// This method is more memory efficient than a dynamically compiled lambda, and about the + /// same speed. + /// + public static Func MakeFastPropertyGetter(PropertyInfo propertyInfo) + { + Debug.Assert(propertyInfo != null); + + return MakeFastPropertyGetter( + propertyInfo, + CallPropertyGetterOpenGenericMethod, + CallPropertyGetterByReferenceOpenGenericMethod); + } + + /// + /// Creates a single fast property getter which is safe for a null input object. The result is not cached. + /// + /// propertyInfo to extract the getter for. + /// a fast getter. + /// + /// This method is more memory efficient than a dynamically compiled lambda, and about the + /// same speed. + /// + public static Func MakeNullSafeFastPropertyGetter(PropertyInfo propertyInfo) + { + Debug.Assert(propertyInfo != null); + + return MakeFastPropertyGetter( + propertyInfo, + CallNullSafePropertyGetterOpenGenericMethod, + CallNullSafePropertyGetterByReferenceOpenGenericMethod); + } + + private static Func MakeFastPropertyGetter( + PropertyInfo propertyInfo, + MethodInfo propertyGetterWrapperMethod, + MethodInfo propertyGetterByRefWrapperMethod) + { + Debug.Assert(propertyInfo != null); + + // Must be a generic method with a Func<,> parameter + Debug.Assert(propertyGetterWrapperMethod != null); + Debug.Assert(propertyGetterWrapperMethod.IsGenericMethodDefinition); + Debug.Assert(propertyGetterWrapperMethod.GetParameters().Length == 2); + + // Must be a generic method with a ByRefFunc<,> parameter + Debug.Assert(propertyGetterByRefWrapperMethod != null); + Debug.Assert(propertyGetterByRefWrapperMethod.IsGenericMethodDefinition); + Debug.Assert(propertyGetterByRefWrapperMethod.GetParameters().Length == 2); + + var getMethod = propertyInfo.GetMethod; + Debug.Assert(getMethod != null); + Debug.Assert(!getMethod.IsStatic); + Debug.Assert(getMethod.GetParameters().Length == 0); + + // Instance methods in the CLR can be turned into static methods where the first parameter + // is open over "target". This parameter is always passed by reference, so we have a code + // path for value types and a code path for reference types. + if (getMethod.DeclaringType.GetTypeInfo().IsValueType) + { + // Create a delegate (ref TDeclaringType) -> TValue + return MakeFastPropertyGetter( + typeof(ByRefFunc<,>), + getMethod, + propertyGetterByRefWrapperMethod); + } + else + { + // Create a delegate TDeclaringType -> TValue + return MakeFastPropertyGetter( + typeof(Func<,>), + getMethod, + propertyGetterWrapperMethod); + } + } + + private static Func MakeFastPropertyGetter( + Type openGenericDelegateType, + MethodInfo propertyGetMethod, + MethodInfo openGenericWrapperMethod) + { + var typeInput = propertyGetMethod.DeclaringType; + var typeOutput = propertyGetMethod.ReturnType; + + var delegateType = openGenericDelegateType.MakeGenericType(typeInput, typeOutput); + var propertyGetterDelegate = propertyGetMethod.CreateDelegate(delegateType); + + var wrapperDelegateMethod = openGenericWrapperMethod.MakeGenericMethod(typeInput, typeOutput); + var accessorDelegate = wrapperDelegateMethod.CreateDelegate( + typeof(Func), + propertyGetterDelegate); + + return (Func)accessorDelegate; + } + + /// + /// Creates a single fast property setter for reference types. The result is not cached. + /// + /// propertyInfo to extract the setter for. + /// a fast getter. + /// + /// This method is more memory efficient than a dynamically compiled lambda, and about the + /// same speed. This only works for reference types. + /// + public static Action MakeFastPropertySetter(PropertyInfo propertyInfo) + { + Debug.Assert(propertyInfo != null); + Debug.Assert(!propertyInfo.DeclaringType.GetTypeInfo().IsValueType); + + var setMethod = propertyInfo.SetMethod; + Debug.Assert(setMethod != null); + Debug.Assert(!setMethod.IsStatic); + Debug.Assert(setMethod.ReturnType == typeof(void)); + var parameters = setMethod.GetParameters(); + Debug.Assert(parameters.Length == 1); + + // Instance methods in the CLR can be turned into static methods where the first parameter + // is open over "target". This parameter is always passed by reference, so we have a code + // path for value types and a code path for reference types. + var typeInput = setMethod.DeclaringType; + var parameterType = parameters[0].ParameterType; + + // Create a delegate TDeclaringType -> { TDeclaringType.Property = TValue; } + var propertySetterAsAction = + setMethod.CreateDelegate(typeof(Action<,>).MakeGenericType(typeInput, parameterType)); + var callPropertySetterClosedGenericMethod = + CallPropertySetterOpenGenericMethod.MakeGenericMethod(typeInput, parameterType); + var callPropertySetterDelegate = + callPropertySetterClosedGenericMethod.CreateDelegate( + typeof(Action), propertySetterAsAction); + + return (Action)callPropertySetterDelegate; + } + + /// + /// Given an object, adds each instance property with a public get method as a key and its + /// associated value to a dictionary. + /// + /// If the object is already an + /// IDictionary{string, object} + /// + /// instance, then a copy + /// is returned. + /// + /// + /// The implementation of FastProperty will cache the property accessors per-type. This is + /// faster when the the same type is used multiple times with ObjectToDictionary. + /// + public static IDictionary ObjectToDictionary(object value, Func keySelector = null) + { + var dictionary = value as IDictionary; + if (dictionary != null) + { + return new Dictionary(dictionary, StringComparer.OrdinalIgnoreCase); + } + + keySelector = keySelector ?? new Func(key => key); + + dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (value != null) + { + foreach (var prop in GetProperties(value).Values) + { + dictionary[keySelector(prop.Name)] = prop.GetValue(value); + } + } + + return dictionary; + } + + private static FastProperty CreateInstance(PropertyInfo property) + { + return new FastProperty(property); + } + + // Called via reflection + private static object CallPropertyGetter( + Func getter, + object target) + { + return getter((TDeclaringType)target); + } + + // Called via reflection + private static object CallPropertyGetterByReference( + ByRefFunc getter, + object target) + { + var unboxed = (TDeclaringType)target; + return getter(ref unboxed); + } + + // Called via reflection + private static object CallNullSafePropertyGetter( + Func getter, + object target) + { + if (target == null) + { + return null; + } + + return getter((TDeclaringType)target); + } + + // Called via reflection + private static object CallNullSafePropertyGetterByReference( + ByRefFunc getter, + object target) + { + if (target == null) + { + return null; + } + + var unboxed = (TDeclaringType)target; + return getter(ref unboxed); + } + + private static void CallPropertySetter( + Action setter, + object target, + object value) + { + setter((TDeclaringType)target, (TValue)value); + } + + protected static IDictionary GetVisibleProperties( + Type type, + Func createPropertyHelper, + ConcurrentDictionary> allPropertiesCache, + ConcurrentDictionary> visiblePropertiesCache) + { + IDictionary result; + if (visiblePropertiesCache.TryGetValue(type, out result)) + { + return result; + } + + // The simple and common case, this is normal POCO object - no need to allocate. + var allPropertiesDefinedOnType = true; + var allProperties = GetProperties(type, createPropertyHelper, allPropertiesCache); + foreach (var propertyHelper in allProperties.Values) + { + if (propertyHelper.Property.DeclaringType != type) + { + allPropertiesDefinedOnType = false; + break; + } + } + + if (allPropertiesDefinedOnType) + { + result = allProperties; + visiblePropertiesCache.TryAdd(type, result); + return result; + } + + // There's some inherited properties here, so we need to check for hiding via 'new'. + var filteredProperties = new List(allProperties.Count); + foreach (var propertyHelper in allProperties.Values) + { + var declaringType = propertyHelper.Property.DeclaringType; + if (declaringType == type) + { + filteredProperties.Add(propertyHelper); + continue; + } + + // If this property was declared on a base type then look for the definition closest to the + // the type to see if we should include it. + var ignoreProperty = false; + + // Walk up the hierarchy until we find the type that actally declares this + // PropertyInfo. + var currentTypeInfo = type.GetTypeInfo(); + var declaringTypeInfo = declaringType.GetTypeInfo(); + while (currentTypeInfo != null && currentTypeInfo != declaringTypeInfo) + { + // We've found a 'more proximal' public definition + var declaredProperty = currentTypeInfo.GetDeclaredProperty(propertyHelper.Name); + if (declaredProperty != null) + { + ignoreProperty = true; + break; + } + + if (currentTypeInfo.BaseType != null) + { + currentTypeInfo = currentTypeInfo.BaseType.GetTypeInfo(); + } + + } + + if (!ignoreProperty) + { + filteredProperties.Add(propertyHelper); + } + } + + result = filteredProperties.ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase); + visiblePropertiesCache.TryAdd(type, result); + return result; + } + + protected static IDictionary GetProperties( + Type type, + Func createPropertyHelper, + ConcurrentDictionary> cache) + { + // Unwrap nullable types. This means Nullable.Value and Nullable.HasValue will not be + // part of the sequence of properties returned by this method. + type = Nullable.GetUnderlyingType(type) ?? type; + + IDictionary fastProperties; + if (!cache.TryGetValue(type, out fastProperties)) + { + var candidates = GetCandidateProperties(type); + fastProperties = candidates.Select(p => createPropertyHelper(p)).ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase); + cache.TryAdd(type, fastProperties); + } + + return fastProperties; + } + + private static IEnumerable GetCandidateProperties(Type type) + { + // We avoid loading indexed properties using the Where statement. + var properties = type.GetRuntimeProperties().Where(IsCandidateProperty); + + var typeInfo = type.GetTypeInfo(); + if (typeInfo.IsInterface) + { + // Reflection does not return information about inherited properties on the interface itself. + properties = properties.Concat(typeInfo.ImplementedInterfaces.SelectMany( + interfaceType => interfaceType.GetRuntimeProperties().Where(IsCandidateProperty))); + } + + return properties; + } + + // Indexed properties are not useful (or valid) for grabbing properties off an object. + private static bool IsCandidateProperty(PropertyInfo property) + { + return property.GetIndexParameters().Length == 0 && + property.GetMethod != null && + property.GetMethod.IsPublic && + !property.GetMethod.IsStatic; + } + + private static ConcurrentDictionary> CreateVolatileCache() + { + return new ConcurrentDictionary>(); + } + + class PropertyKey : Tuple + { + public PropertyKey(Type type, string propertyName) + : base(type, propertyName) + { + } + public Type Type { get { return base.Item1; } } + public string PropertyName { get { return base.Item2; } } + } + } +} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/ComponentModel/GenericListTypeConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/GenericListTypeConverter.cs deleted file mode 100644 index f5253dd01b..0000000000 --- a/src/Libraries/SmartStore.Core/ComponentModel/GenericListTypeConverter.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Globalization; -using System.Linq; - -namespace SmartStore.Core.ComponentModel -{ - public class GenericListTypeConverter : TypeConverter - { - protected readonly TypeConverter _typeConverter; - - public GenericListTypeConverter() - { - _typeConverter = TypeDescriptor.GetConverter(typeof(T)); - if (_typeConverter == null) - throw new InvalidOperationException("No type converter exists for type " + typeof(T).FullName); - } - - protected virtual string[] GetStringArray(string input) - { - if (!String.IsNullOrEmpty(input)) - { - string[] result = input.Split(','); - Array.ForEach(result, s => s.Trim()); - return result; - } - else - return new string[0]; - } - - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - - if (sourceType == typeof(string)) - { - string[] items = GetStringArray(sourceType.ToString()); - return (items.Count() > 0); - } - - return base.CanConvertFrom(context, sourceType); - } - - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - { - if (value is string) - { - string[] items = GetStringArray((string)value); - var result = new List(); - Array.ForEach(items, s => - { - object item = _typeConverter.ConvertFromInvariantString(s); - if (item != null) - { - result.Add((T)item); - } - }); - - return result; - } - return base.ConvertFrom(context, culture, value); - } - - public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) - { - if (destinationType == typeof(string)) - { - string result = string.Empty; - if (((IList)value) != null) - { - //we don't use string.Join() because it doesn't support invariant culture - for (int i = 0; i < ((IList)value).Count; i++) - { - var str1 = Convert.ToString(((IList)value)[i], CultureInfo.InvariantCulture); - result += str1; - //don't add comma after the last element - if (i != ((IList)value).Count - 1) - result += ","; - } - } - return result; - } - - return base.ConvertTo(context, culture, value, destinationType); - } - } -} diff --git a/src/Libraries/SmartStore.Core/ComponentModel/Expando.cs b/src/Libraries/SmartStore.Core/ComponentModel/HybridExpando.cs similarity index 51% rename from src/Libraries/SmartStore.Core/ComponentModel/Expando.cs rename to src/Libraries/SmartStore.Core/ComponentModel/HybridExpando.cs index 81c2ecb4f7..1049481e35 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/Expando.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/HybridExpando.cs @@ -36,7 +36,7 @@ using System.Linq; using System.Dynamic; using System.Reflection; -using Fasterflect; +using System.Collections; namespace SmartStore.ComponentModel { @@ -59,7 +59,7 @@ namespace SmartStore.ComponentModel /// Dictionary: Any of the extended properties are accessible via IDictionary interface /// [Serializable] - public class Expando : DynamicObject, IDynamicMetaObjectProvider + public class HybridExpando : DynamicObject, IDictionary { /// /// Instance of object passed in @@ -71,8 +71,6 @@ public class Expando : DynamicObject, IDynamicMetaObjectProvider /// private Type _instanceType; - private IList _instancePropertyInfo; - /// /// String Dictionary that contains the extra dynamic values /// stored on this object/instance @@ -86,7 +84,7 @@ public class Expando : DynamicObject, IDynamicMetaObjectProvider /// /// Note you can subclass Expando. /// - public Expando() + public HybridExpando() { Initialize(this); } @@ -99,30 +97,24 @@ public Expando() /// check native properties and only check the Dictionary! /// /// - public Expando(object instance) + public HybridExpando(object instance) { Initialize(instance); } - - protected virtual void Initialize(object instance) + protected void Initialize(object instance) { _instance = instance; if (instance != null) _instanceType = instance.GetType(); } - IList InstancePropertyInfo - { - get - { - if (_instancePropertyInfo == null && _instance != null) - _instancePropertyInfo = _instance.GetType().Properties(Flags.InstancePublicDeclaredOnly); - return _instancePropertyInfo; - } - } + protected object WrappedObject + { + get { return _instance; } + } - public override IEnumerable GetDynamicMemberNames() + public override IEnumerable GetDynamicMemberNames() { foreach (var prop in this.GetProperties(false)) yield return prop.Key; @@ -138,68 +130,76 @@ public override IEnumerable GetDynamicMemberNames() /// public override bool TryGetMember(GetMemberBinder binder, out object result) { - result = null; - - // first check the Properties collection for member - if (Properties.Keys.Contains(binder.Name)) - { - result = Properties[binder.Name]; - return true; - } - - - // Next check for Public properties via Reflection - if (_instance != null) - { - try - { - return GetProperty(_instance, binder.Name, out result); - } - catch { } - } - - // failed to retrieve a property - result = null; - return false; + return TryGetMemberCore(binder.Name, out result); } - - /// - /// Property setter implementation tries to retrieve value from instance - /// first then into this object - /// - /// - /// - /// - public override bool TrySetMember(SetMemberBinder binder, object value) + protected virtual bool TryGetMemberCore(string name, out object result) + { + result = null; + + // first check the Properties collection for member + if (Properties.Keys.Contains(name)) + { + result = Properties[name]; + return true; + } + + // Next check for public properties via Reflection + if (_instance != null) + { + try + { + return GetProperty(_instance, name, out result); + } + catch { } + } + + // failed to retrieve a property + result = null; + return false; + } + + + /// + /// Property setter implementation tries to retrieve value from instance + /// first then into this object + /// + /// + /// + /// + public override bool TrySetMember(SetMemberBinder binder, object value) { - - // first check to see if there's a native property to set - if (_instance != null) - { - try - { - bool result = SetProperty(_instance, binder.Name, value); - if (result) - return true; - } - catch { } - } - - // no match - set or add to dictionary - Properties[binder.Name] = value; - return true; + return TrySetMemberCore(binder.Name, value); } - /// - /// Dynamic invocation method. Currently allows only for Reflection based - /// operation (no ability to add methods dynamically). - /// - /// - /// - /// - /// - public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) + protected virtual bool TrySetMemberCore(string name, object value) + { + // first check to see if there's a native property to set + if (_instance != null) + { + try + { + bool result = SetProperty(_instance, name, value); + if (result) + return true; + } + catch { } + } + + // no match - set or add to dictionary + Properties[name] = value; + return true; + } + + /// + /// Dynamic invocation method. Currently allows only for Reflection based + /// operation (no ability to add methods dynamically). + /// + /// + /// + /// + /// + public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { if (_instance != null) { @@ -226,19 +226,16 @@ public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, o /// protected bool GetProperty(object instance, string name, out object result) { - if (instance == null) - instance = this; - - var pi = _instanceType.Property(name, Flags.InstancePublic); - if (pi != null) - { - result = instance.GetPropertyValue(pi.Name); - return true; - } - - result = null; + var fastProp = _instanceType != null ? FastProperty.GetProperty(_instanceType, name, PropertyCachingStrategy.EagerCached) : null; + if (fastProp != null) + { + result = fastProp.GetValue(instance ?? this); + return true; + } + + result = null; return false; - } + } /// /// Reflection helper method to set a property value @@ -249,16 +246,14 @@ protected bool GetProperty(object instance, string name, out object result) /// protected bool SetProperty(object instance, string name, object value) { - if (instance == null) - instance = this; - - var pi = _instanceType.Property(name, Flags.InstancePublic); - if (pi != null) - { - instance.SetPropertyValue(pi.Name, value); - return true; + var fastProp = _instanceType != null ? FastProperty.GetProperty(_instanceType, name, PropertyCachingStrategy.EagerCached) : null; + if (fastProp != null) + { + fastProp.SetValue(instance ?? this, value); + return true; } - return false; + + return false; } /// @@ -271,16 +266,12 @@ protected bool SetProperty(object instance, string name, object value) /// protected bool InvokeMethod(object instance, string name, object[] args, out object result) { - if (instance == null) - instance = this; - // Look at the instanceType - var mi = _instanceType.Method(name, Flags.InstancePublic); - + var mi = _instanceType != null ? _instanceType.GetMethod(name, BindingFlags.Instance | BindingFlags.Public) : null; if (mi != null) { - result = _instance.CallMethod(mi.Name, args); - return true; + result = mi.Invoke(instance ?? this, args); + return true; } result = null; @@ -288,78 +279,66 @@ protected bool InvokeMethod(object instance, string name, object[] args, out obj } - /// - /// Convenience method that provides a string Indexer - /// to the Properties collection AND the strongly typed - /// properties of the object by name. - /// - /// // dynamic - /// exp["Address"] = "112 nowhere lane"; - /// // strong - /// var name = exp["StronglyTypedProperty"] as string; - /// - /// - /// The getter checks the Properties dictionary first - /// then looks in PropertyInfo for properties. - /// The setter checks the instance properties before - /// checking the Properties dictionary. - /// - /// - /// - /// - public object this[string key] + /// + /// Convenience method that provides a string Indexer + /// to the Properties collection AND the strongly typed + /// properties of the object by name. + /// + /// // dynamic + /// exp["Address"] = "112 nowhere lane"; + /// // strong + /// var name = exp["StronglyTypedProperty"] as string; + /// + /// + /// The getter checks the Properties dictionary first + /// then looks in PropertyInfo for properties. + /// The setter checks the instance properties before + /// checking the Properties dictionary. + /// + /// + /// + /// + public object this[string key] + { + get + { + object result = null; + if (!TryGetMemberCore(key, out result)) + { + throw new KeyNotFoundException(); + } + + return result; + } + set + { + TrySetMemberCore(key, value); + } + } + + + /// + /// Returns all properties + /// + /// + /// + public IEnumerable> GetProperties(bool includeInstanceProperties = false) { - get + foreach (var key in this.Properties.Keys) + { + yield return new KeyValuePair(key, this.Properties[key]); + } + + if (includeInstanceProperties && _instance != null) { - try - { - // try to get from properties collection first - return Properties[key]; - } - catch (KeyNotFoundException) - { - // try reflection on instanceType - object result = null; - if (GetProperty(_instance, key, out result)) - return result; - - // no doesn't exist - throw; - } + foreach (var prop in FastProperty.GetProperties(_instance).Values) + { + if (!this.Properties.ContainsKey(prop.Name)) + { + yield return new KeyValuePair(prop.Name, prop.GetValue(_instance)); + } + } } - set - { - if (Properties.ContainsKey(key)) - { - Properties[key] = value; - return; - } - - // check instance for existance of type first - var pi = _instanceType.Property(key, Flags.Public); - if (pi != null) - SetProperty(_instance, key, value); - else - Properties[key] = value; - } - } - - - /// - /// Returns and the properties of - /// - /// - /// - public IEnumerable> GetProperties(bool includeInstanceProperties = false) - { - if (includeInstanceProperties && _instance != null) - { - foreach (var prop in this.InstancePropertyInfo) - yield return new KeyValuePair(prop.Name, prop.GetValue(_instance, null)); - } - - foreach (var key in this.Properties.Keys) - yield return new KeyValuePair(key, this.Properties[key]); } @@ -383,34 +362,135 @@ public bool Contains(KeyValuePair item, bool includeInstanceProp /// public bool Contains(string propertyName, bool includeInstanceProperties = false) { - bool res = Properties.ContainsKey(propertyName); - if (res) - return true; + if (Properties.ContainsKey(propertyName)) + { + return true; + } if (includeInstanceProperties && _instance != null) { - foreach (var prop in this.InstancePropertyInfo) - { - if (prop.Name == propertyName) - return true; - } + return FastProperty.GetProperties(_instance).ContainsKey(propertyName); } return false; } - public bool TryGetValue(string propertyName, out object value) - { - value = null; - - if (this.Contains(propertyName, true)) - { - value = this[propertyName]; - return true; - } - - return false; + #region IDictionary + + ICollection IDictionary.Keys + { + get + { + return GetProperties(true).Select(x => x.Key).AsReadOnly(); + } + } + + ICollection IDictionary.Values + { + get + { + return GetProperties(true).Select(x => x.Value).AsReadOnly(); + } + } + + int ICollection>.Count + { + get + { + var count = Properties.Count; + if (_instanceType != null) + { + count += FastProperty.GetProperties(_instanceType).Count; + } + + return count; + } + } + + bool ICollection>.IsReadOnly + { + get + { + return false; + } + } + + object IDictionary.this[string key] + { + get + { + return this[key]; + } + + set + { + this[key] = value; + } + } + + bool IDictionary.ContainsKey(string key) + { + return Contains(key, true); + } + + void IDictionary.Add(string key, object value) + { + throw new NotImplementedException(); + } + + bool IDictionary.Remove(string key) + { + throw new NotImplementedException(); + } + + public bool TryGetValue(string key, out object value) + { + value = null; + + if (this.Contains(key, true)) + { + value = this[key]; + return true; + } + + return false; + } + + void ICollection>.Add(KeyValuePair item) + { + throw new NotImplementedException(); + } + + void ICollection>.Clear() + { + throw new NotImplementedException(); + } + + bool ICollection>.Contains(KeyValuePair item) + { + return Contains(item.Key, true); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + bool ICollection>.Remove(KeyValuePair item) + { + throw new NotImplementedException(); + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + return GetProperties(true).GetEnumerator(); } - } + IEnumerator IEnumerable.GetEnumerator() + { + return GetProperties(true).GetEnumerator(); + } + + #endregion + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/ComponentModel/PropertyBag.cs b/src/Libraries/SmartStore.Core/ComponentModel/PropertyBag.cs index fb2ab77767..bec72dcea5 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/PropertyBag.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/PropertyBag.cs @@ -181,7 +181,7 @@ public void WriteXml(System.Xml.XmlWriter writer) writer.WriteStartElement("item"); writer.WriteStartElement("key"); - writer.WriteString(key as string); + writer.WriteString(key); writer.WriteEndElement(); writer.WriteStartElement("value"); @@ -306,8 +306,7 @@ public bool FromXml(string xml) if (string.IsNullOrEmpty(xml)) return true; - var result = SerializationUtils.DeSerializeObject(xml, - this.GetType()) as PropertyBag; + var result = SerializationUtils.DeSerializeObject(xml, this.GetType()) as PropertyBag; if (result != null) { foreach (var item in result) diff --git a/src/Libraries/SmartStore.Core/ComponentModel/SerializationUtils.cs b/src/Libraries/SmartStore.Core/ComponentModel/SerializationUtils.cs index 263e516ef5..e36995facd 100644 --- a/src/Libraries/SmartStore.Core/ComponentModel/SerializationUtils.cs +++ b/src/Libraries/SmartStore.Core/ComponentModel/SerializationUtils.cs @@ -34,20 +34,13 @@ using System; using System.IO; using System.Text; -//using System.Reflection; -using Fasterflect; - using System.Xml; using System.Xml.Serialization; using System.Runtime.Serialization.Formatters.Binary; using System.Diagnostics; -using System.Runtime.Serialization; namespace SmartStore.ComponentModel { - - // Serialization specific code - internal static class SerializationUtils { /// @@ -194,25 +187,25 @@ public static bool SerializeObject(object instance, out string xmlResultString, return true; } - - /// - /// Serializes an object instance to a file. - /// - /// the object instance to serialize - /// - /// determines whether XML serialization or binary serialization is used - /// - public static bool SerializeObject(object instance, out byte[] resultBuffer, bool throwExceptions = false) + /// + /// Serializes an object instance to a file. + /// + /// the object instance to serialize + /// + /// + /// + public static bool SerializeObject(object instance, out byte[] resultBuffer, bool throwExceptions = false) { bool retVal = true; + resultBuffer = null; - MemoryStream ms = null; + var ms = new MemoryStream(); try { - BinaryFormatter serializer = new BinaryFormatter(); - ms = new MemoryStream(); + var serializer = new BinaryFormatter(); serializer.Serialize(ms, instance); - } + resultBuffer = ms.ToArray(); + } catch (Exception ex) { Debug.Write("SerializeObject failed with : " + ex.GetBaseException().Message, "West Wind"); @@ -223,12 +216,9 @@ public static bool SerializeObject(object instance, out byte[] resultBuffer, boo } finally { - if (ms != null) - ms.Close(); + ms.Close(); } - resultBuffer = ms.ToArray(); - return retVal; } @@ -419,17 +409,17 @@ public static object DeSerializeObject(byte[] buffer, Type objectType, bool thro /// public static string ObjectToString(object instance, string separator, ObjectToStringTypes type) { - var fi = instance.GetType().Fields(); + var fi = instance.GetType().GetFields(); string output = string.Empty; if (type == ObjectToStringTypes.Properties || type == ObjectToStringTypes.PropertiesAndFields) { - foreach (var property in instance.GetType().Properties()) + foreach (var property in instance.GetType().GetProperties()) { try { - output += property.Name + ":" + instance.GetPropertyValue(property.Name).ToString() + separator; + output += property.Name + ":" + property.GetValue(instance, null).ToString() + separator; } catch { @@ -444,7 +434,7 @@ public static string ObjectToString(object instance, string separator, ObjectToS { try { - output = output + field.Name + ": " + instance.GetFieldValue(field.Name).ToString() + separator; + output = output + field.Name + ": " + field.GetValue(instance).ToString() + separator; } catch { diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/BooleanConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/BooleanConverter.cs new file mode 100644 index 0000000000..11e6c6c941 --- /dev/null +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/BooleanConverter.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmartStore.ComponentModel +{ + [SuppressMessage("ReSharper", "CanBeReplacedWithTryCastAndCheckForNull")] + public class BooleanConverter : TypeConverterBase + { + private readonly HashSet _trueValues; + private readonly HashSet _falseValues; + + public BooleanConverter(string[] trueValues, string[] falseValues) + : base(typeof(bool)) + { + _trueValues = new HashSet(trueValues, StringComparer.OrdinalIgnoreCase); + _falseValues = new HashSet(falseValues, StringComparer.OrdinalIgnoreCase); + } + + public ICollection TrueValues + { + get { return _trueValues; } + } + + public ICollection FalseValues + { + get { return _falseValues; } + } + + public override object ConvertFrom(CultureInfo culture, object value) + { + if (value is short) + { + if ((short)value == 0) + { + return false; + } + if ((short)value == 1) + { + return true; + } + } + + if (value is string) + { + var str = (string)value; + + bool b; + if (bool.TryParse(str, out b)) + { + return b; + } + + short sh; + if (short.TryParse(str, out sh)) + { + if (sh == 0) + { + return false; + } + if (sh == 1) + { + return true; + } + } + + str = (str.NullEmpty() ?? string.Empty).Trim(); + if (_trueValues.Contains(str)) + { + return true; + } + + if (_falseValues.Contains(str)) + { + return false; + } + } + + return base.ConvertFrom(culture, value); + } + } +} diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/DateTimeConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/DateTimeConverter.cs new file mode 100644 index 0000000000..b357dcddb8 --- /dev/null +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/DateTimeConverter.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmartStore.ComponentModel +{ + [SuppressMessage("ReSharper", "CanBeReplacedWithTryCastAndCheckForNull")] + public class DateTimeConverter : TypeConverterBase + { + public DateTimeConverter() + : base(typeof(DateTime)) + { + } + + public override bool CanConvertFrom(Type type) + { + return type == typeof(string) + || type == typeof(long) + || type == typeof(double) + || type == typeof(TimeSpan) + || base.CanConvertFrom(type); + } + + public override bool CanConvertTo(Type type) + { + return type == typeof(string) + || type == typeof(long) + || type == typeof(double) + || type == typeof(DateTimeOffset) + || type == typeof(TimeSpan) + || base.CanConvertTo(type); + } + + public override object ConvertFrom(CultureInfo culture, object value) + { + if (value is TimeSpan) + { + var span = (TimeSpan)value; + return new DateTime(span.Ticks); + } + + if (value is string) + { + var str = (string)value; + + DateTime time; + if (DateTime.TryParse(str, culture, DateTimeStyles.None, out time)) + { + return time; + } + + long lng; + if (long.TryParse(str, NumberStyles.None, culture, out lng)) + { + return lng.FromUnixTime(); + } + + double dbl; + if (double.TryParse(str, NumberStyles.AllowDecimalPoint, culture, out dbl)) + { + return DateTime.FromOADate(dbl); + } + } + + if (value is long) + { + return ((long)value).FromUnixTime(); + } + + if (value is double) + { + return DateTime.FromOADate((double)value); + } + + return base.ConvertFrom(culture, value); + } + + public override object ConvertTo(CultureInfo culture, string format, object value, Type to) + { + var time = (DateTime)value; + + if (to == typeof(DateTimeOffset)) + { + return new DateTimeOffset(time); + } + + if (to == typeof(TimeSpan)) + { + return new TimeSpan(time.Ticks); + } + + if (to == typeof(double)) + { + return time.ToOADate(); + } + + if (to == typeof(long)) + { + return time.ToUnixTime(); + } + + return base.ConvertTo(culture, format, value, to); + } + } +} diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs new file mode 100644 index 0000000000..1980693205 --- /dev/null +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; + +namespace SmartStore.ComponentModel +{ + [SuppressMessage("ReSharper", "CanBeReplacedWithTryCastAndCheckForNull")] + public class EnumerableConverter : TypeConverterBase + { + private readonly Func, object> _activator; + private readonly ITypeConverter _elementTypeConverter; + + public EnumerableConverter(Type sequenceType) + : base(typeof(object)) + { + _elementTypeConverter = TypeConverterFactory.GetConverter(); + if (_elementTypeConverter == null) + throw new InvalidOperationException("No type converter exists for type " + typeof(T).FullName); + + _activator = CreateSequenceActivator(sequenceType); + } + + [SuppressMessage("ReSharper", "RedundantLambdaSignatureParentheses")] + private static Func, object> CreateSequenceActivator(Type sequenceType) + { + // Default is IEnumerable + Func, object> activator = null; + + var t = sequenceType; + + if (t == typeof(IEnumerable)) + { + activator = (x) => x; + } + else if (t == (typeof(IReadOnlyCollection)) || t == (typeof(IReadOnlyList))) + { + activator = (x) => x.AsReadOnly(); + } + else if (t.IsAssignableFrom(typeof(List))) + { + activator = (x) => x.ToList(); + } + else if (t.IsAssignableFrom(typeof(HashSet))) + { + activator = (x) => new HashSet(x); + } + else if (t.IsAssignableFrom(typeof(Queue))) + { + activator = (x) => new Queue(x); + } + else if (t.IsAssignableFrom(typeof(Stack))) + { + activator = (x) => new Stack(x); + } + else if (t.IsAssignableFrom(typeof(LinkedList))) + { + activator = (x) => new LinkedList(x); + } + else if (t.IsAssignableFrom(typeof(ConcurrentBag))) + { + activator = (x) => new ConcurrentBag(x); + } + else if (t.IsAssignableFrom(typeof(ArraySegment))) + { + activator = (x) => new ArraySegment(x.ToArray()); + } + + if (activator == null) + { + throw new InvalidOperationException("'{0}' is not a valid type for enumerable conversion.".FormatInvariant(sequenceType.FullName)); + } + + return activator; + } + + public override bool CanConvertFrom(Type type) + { + return type == typeof(string) || typeof(IConvertible).IsAssignableFrom(type); + } + + public override bool CanConvertTo(Type type) + { + return type == typeof(string); + } + + public override object ConvertFrom(CultureInfo culture, object value) + { + if (value == null) + { + return _activator(Enumerable.Empty()); + } + + if (value is string) + { + var items = GetStringArray((string)value); + + var result = items + .Select(x => _elementTypeConverter.ConvertFrom(culture, x)) + .Where(x => x != null) + .Cast(); + + return _activator(result); + } + + if (value is IConvertible) + { + var result2 = (new object[] { value }) + .Select(x => Convert.ChangeType(value, typeof(T))) + .Cast(); + + return _activator(result2); + } + + return base.ConvertFrom(culture, value); + } + + public override object ConvertTo(CultureInfo culture, string format, object value, Type to) + { + if (to == typeof(string)) + { + string result = string.Empty; + var enumerable = value as IEnumerable; + + if (enumerable != null) + { + // 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)); + result += str + ","; + } + + result = result.TrimEnd(','); + } + + return result; + } + + return base.ConvertTo(culture, format, value, to); + } + + protected virtual string[] GetStringArray(string input) + { + if (!String.IsNullOrEmpty(input)) + { + var splitChar = '|'; + + if (input.IndexOf(splitChar) < 0) + { + if (input.IndexOf(';') > -1) + { + splitChar = ';'; + } + else if (input.IndexOf(',') > -1) + { + splitChar = ','; + } + } + + var result = input.Split(new char[] { splitChar }, StringSplitOptions.RemoveEmptyEntries); + // ReSharper disable once ReturnValueOfPureMethodIsNotUsed + Array.ForEach(result, s => s.Trim()); + return result; + } + + return new string[0]; + } + } +} diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ITypeConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ITypeConverter.cs new file mode 100644 index 0000000000..c76ecba218 --- /dev/null +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ITypeConverter.cs @@ -0,0 +1,83 @@ +using System; +using System.Globalization; + +namespace SmartStore.ComponentModel +{ + /// + /// Converts objects. + /// + public interface ITypeConverter + { + /// + /// Returns whether this converter can convert an object of the given type to the type of this converter. + /// + /// A Type that represents the type you want to convert from. + /// true if this converter can perform the conversion; otherwise, false. + bool CanConvertFrom(Type type); + + /// + /// Returns whether this converter can convert the object to the specified type. + /// + /// A Type that represents the type you want to convert to. + /// true if this converter can perform the conversion; otherwise, false. + bool CanConvertTo(Type type); + + /// + /// Converts the given value to the type of this converter. + /// + /// The to use as the current culture. If null is passed, the invariant culture is assumed. + /// The object to convert. + /// An object that represents the converted value. + object ConvertFrom(CultureInfo culture, object value); + + /// + /// Converts the given value object to the specified type, using the arguments. + /// + /// The to use as the current culture. If null is passed, the invariant culture is assumed. + /// A standard or custom format expression. + /// The object to convert. + /// The type to convert the value parameter to. + /// An Object that represents the converted value. + object ConvertTo(CultureInfo culture, string format, object value, Type to); + } + + public static class ITypeConverterExtensions + { + public static object ConvertFrom(this ITypeConverter converter, object value) + { + return converter.ConvertFrom(CultureInfo.InvariantCulture, value); + } + + public static object ConvertTo(this ITypeConverter converter, object value, Type to) + { + return converter.ConvertTo(CultureInfo.InvariantCulture, null, value, to); + } + + public static object SafeConvert(this ITypeConverter converter, string value) + { + try + { + if (converter != null && value.HasValue() && converter.CanConvertFrom(typeof(string))) + { + return converter.ConvertFrom(value); + } + } + catch (Exception exc) + { + exc.Dump(); + } + + return null; + } + + public static bool IsEqual(this ITypeConverter converter, string value, object compareWith) + { + object convertedObject = converter.SafeConvert(value); + + if (convertedObject != null && compareWith != null) + return convertedObject.Equals(compareWith); + + return false; + } + } +} diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/NullableConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/NullableConverter.cs new file mode 100644 index 0000000000..d11c53409f --- /dev/null +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/NullableConverter.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmartStore.ComponentModel +{ + [SuppressMessage("ReSharper", "TryCastAlwaysSucceeds")] + public class NullableConverter : TypeConverterBase + { + private readonly bool _underlyingTypeIsConvertible; + + public NullableConverter(Type type) + : base(type) + { + NullableType = type; + UnderlyingType = Nullable.GetUnderlyingType(type); + + if (UnderlyingType == null) + { + throw Error.Argument("type", "Type is not a nullable type."); + } + + _underlyingTypeIsConvertible = typeof(IConvertible).IsAssignableFrom(UnderlyingType) && !UnderlyingType.IsEnum; + UnderlyingTypeConverter = TypeConverterFactory.GetConverter(UnderlyingType); + } + + public Type NullableType + { + get; + private set; + } + + public Type UnderlyingType + { + get; + private set; + } + + public ITypeConverter UnderlyingTypeConverter + { + get; + private set; + } + + public override bool CanConvertFrom(Type type) + { + if (type == this.UnderlyingType) + { + return true; + } + + if (UnderlyingTypeConverter.CanConvertFrom(type)) + { + return true; + } + + if (_underlyingTypeIsConvertible && type != typeof(string) && typeof(IConvertible).IsAssignableFrom(type)) + { + return true; + } + + return false; + } + + public override bool CanConvertTo(Type type) + { + Console.WriteLine("NullableConverter can convert to {0}: {1}".FormatInvariant(type.Name, UnderlyingTypeConverter.CanConvertTo(type))); + + if (type == this.UnderlyingType) + { + return true; + } + + return UnderlyingTypeConverter.CanConvertTo(type); + } + + public override object ConvertFrom(CultureInfo culture, object value) + { + if ((value == null) || (value.GetType() == this.UnderlyingType)) + { + return value; + } + + if ((value is string) && string.IsNullOrEmpty(value as string)) + { + return null; + } + + if (_underlyingTypeIsConvertible && !(value is string) && value is IConvertible) + { + // num > num? + return Convert.ChangeType(value, UnderlyingType, culture); + } + + return UnderlyingTypeConverter.ConvertFrom(culture, value); + } + + public override object ConvertTo(CultureInfo culture, string format, object value, Type to) + { + if ((to == this.UnderlyingType) && this.NullableType.IsInstanceOfType(value)) + { + return value; + } + + if (value == null) + { + if (to == typeof(string)) + { + return string.Empty; + } + } + + return UnderlyingTypeConverter.ConvertTo(culture, format, value, to); + } + } +} diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ProductBundleDataConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ProductBundleDataConverter.cs new file mode 100644 index 0000000000..599431116f --- /dev/null +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ProductBundleDataConverter.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml.Serialization; +using SmartStore.Core.Domain.Catalog; + +namespace SmartStore.ComponentModel +{ + [SuppressMessage("ReSharper", "TryCastAlwaysSucceeds")] + public class ProductBundleDataConverter : TypeConverterBase + { + private readonly bool _forList; + + public ProductBundleDataConverter(bool forList) + : base(typeof(object)) + { + _forList = forList; + } + + public override bool CanConvertFrom(Type type) + { + return type == typeof(string); + } + + public override bool CanConvertTo(Type type) + { + return type == typeof(string); + } + + public override object ConvertFrom(CultureInfo culture, object value) + { + if (value is string) + { + object result = null; + string str = value as string; + if (!string.IsNullOrEmpty(str)) + { + try + { + using (var tr = new StringReader(str)) + { + var serializer = new XmlSerializer(_forList ? typeof(List) : typeof(ProductBundleItemOrderData)); + result = serializer.Deserialize(tr); + } + } + catch + { + // xml error + } + } + + return result; + } + + return base.ConvertFrom(culture, value); + } + + public override object ConvertTo(CultureInfo culture, string format, object value, Type to) + { + if (to == typeof(string)) + { + if (value != null && (value is ProductBundleItemOrderData || value is IList)) + { + var sb = new StringBuilder(); + using (var tw = new StringWriter(sb)) + { + var serializer = new XmlSerializer(_forList ? typeof(List) : typeof(ProductBundleItemOrderData)); + serializer.Serialize(tw, value); + return sb.ToString(); + } + } + else + { + return string.Empty; + } + } + + return base.ConvertTo(culture, format, value, to); + } + } +} diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ShippingOptionConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ShippingOptionConverter.cs new file mode 100644 index 0000000000..b79c9a5f2f --- /dev/null +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ShippingOptionConverter.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Text; +using System.Xml.Serialization; +using SmartStore.Core.Domain.Shipping; + +namespace SmartStore.ComponentModel +{ + [SuppressMessage("ReSharper", "TryCastAlwaysSucceeds")] + public class ShippingOptionConverter : TypeConverterBase + { + private readonly bool _forList; + + public ShippingOptionConverter(bool forList) + : base(typeof(object)) + { + _forList = forList; + } + + public override bool CanConvertFrom(Type type) + { + return type == typeof(string); + } + + public override bool CanConvertTo(Type type) + { + return type == typeof(string); + } + + public override object ConvertFrom(CultureInfo culture, object value) + { + if (value is string) + { + object result = null; + string str = value as string; + if (!String.IsNullOrEmpty(str)) + { + try + { + using (var tr = new StringReader(str)) + { + var serializer = new XmlSerializer(_forList ? typeof(List) : typeof(ShippingOption)); + result = serializer.Deserialize(tr); + } + } + catch + { + // xml error + } + } + + return result; + } + + return base.ConvertFrom(culture, value); + } + + public override object ConvertTo(CultureInfo culture, string format, object value, Type to) + { + if (to == typeof(string)) + { + if (value != null && (value is ShippingOption || value is IList)) + { + var sb = new StringBuilder(); + using (var tw = new StringWriter(sb)) + { + var serializer = new XmlSerializer(_forList ? typeof(List) : typeof(ShippingOption)); + serializer.Serialize(tw, value); + return sb.ToString(); + } + } + else + { + return string.Empty; + } + } + + return base.ConvertTo(culture, format, value, to); + } + } +} diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TimeSpanConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TimeSpanConverter.cs new file mode 100644 index 0000000000..656355d8e8 --- /dev/null +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TimeSpanConverter.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmartStore.ComponentModel +{ + [SuppressMessage("ReSharper", "CanBeReplacedWithTryCastAndCheckForNull")] + public class TimeSpanConverter : TypeConverterBase + { + public TimeSpanConverter() + : base(typeof(TimeSpan)) + { + } + + public override bool CanConvertFrom(Type type) + { + return type == typeof(string) + || type == typeof(DateTime) + || base.CanConvertFrom(type); + } + + public override object ConvertFrom(CultureInfo culture, object value) + { + if (value is DateTime) + { + var time = (DateTime)value; + return new TimeSpan(time.Ticks); + } + + if (value is string) + { + var str = (string)value; + + TimeSpan span; + if (TimeSpan.TryParse(str, culture, out span)) + { + return span; + } + + long lng; + if (long.TryParse(str, NumberStyles.None, culture, out lng)) + { + return new TimeSpan(lng.FromUnixTime().Ticks); + } + + double dbl; + if (double.TryParse(str, NumberStyles.None, culture, out dbl)) + { + return new TimeSpan(DateTime.FromOADate(dbl).Ticks); + } + } + + try + { + return (TimeSpan)System.Convert.ChangeType(value, typeof(TimeSpan), culture); + } + catch { } + + return base.ConvertFrom(culture, value); + } + } +} diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterAdapter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterAdapter.cs new file mode 100644 index 0000000000..b7ed897169 --- /dev/null +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterAdapter.cs @@ -0,0 +1,37 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +namespace SmartStore.ComponentModel +{ + internal class TypeConverterAdapter : TypeConverterBase + { + private readonly TypeConverter _converter; + + public TypeConverterAdapter(TypeConverter converter) + : base(typeof(object)) + { + _converter = converter; + } + + public override bool CanConvertFrom(Type type) + { + return _converter != null && _converter.CanConvertFrom(type); + } + + public override bool CanConvertTo(Type type) + { + return _converter != null && _converter.CanConvertTo(type); + } + + public override object ConvertFrom(CultureInfo culture, object value) + { + return _converter.ConvertFrom(null, culture, value); + } + + public override object ConvertTo(CultureInfo culture, string format, object value, Type to) + { + return _converter.ConvertTo(null, culture, value, to); + } + } +} diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterBase.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterBase.cs new file mode 100644 index 0000000000..958cd9274c --- /dev/null +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterBase.cs @@ -0,0 +1,68 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +namespace SmartStore.ComponentModel +{ + public abstract class TypeConverterBase : ITypeConverter + { + private readonly Lazy _systemConverter; + private readonly Type _type; + + protected TypeConverterBase(Type type) + { + Guard.ArgumentNotNull(() => type); + + _type = type; + _systemConverter = new Lazy(() => TypeDescriptor.GetConverter(type), true); + } + + public TypeConverter SystemConverter + { + get + { + if (_type == typeof(object)) + { + return null; + } + + return _systemConverter.Value; + } + } + + public virtual bool CanConvertFrom(Type type) + { + return SystemConverter != null && SystemConverter.CanConvertFrom(type); + } + + public virtual bool CanConvertTo(Type type) + { + return type == typeof(string) || (SystemConverter != null && SystemConverter.CanConvertTo(type)); + } + + public virtual object ConvertFrom(CultureInfo culture, object value) + { + if (SystemConverter != null) + { + return SystemConverter.ConvertFrom(null, culture, value); + } + + throw Error.InvalidCast(value.GetType(), _type); + } + + public virtual object ConvertTo(CultureInfo culture, string format, object value, Type to) + { + if (SystemConverter != null) + { + return SystemConverter.ConvertTo(null, culture, value, to); + } + + if (value == null) + { + return string.Empty; + } + + return value.ToString(); + } + } +} diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterFactory.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterFactory.cs new file mode 100644 index 0000000000..4f25272dd8 --- /dev/null +++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterFactory.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Shipping; + +namespace SmartStore.ComponentModel +{ + public static class TypeConverterFactory + { + private static readonly ConcurrentDictionary _typeConverters = new ConcurrentDictionary(); + + static TypeConverterFactory() + { + CreateDefaultConverters(); + } + + private static void CreateDefaultConverters() + { + _typeConverters.TryAdd(typeof(DateTime), new DateTimeConverter()); + _typeConverters.TryAdd(typeof(TimeSpan), new TimeSpanConverter()); + _typeConverters.TryAdd(typeof(bool), new BooleanConverter( + new [] { "yes", "y", "on", "wahr" }, + new [] { "no", "n", "off", "falsch" })); + + ITypeConverter converter = new ShippingOptionConverter(true); + _typeConverters.TryAdd(typeof(IList), converter); + _typeConverters.TryAdd(typeof(List), converter); + _typeConverters.TryAdd(typeof(ShippingOption), new ShippingOptionConverter(false)); + + converter = new ProductBundleDataConverter(true); + _typeConverters.TryAdd(typeof(IList), converter); + _typeConverters.TryAdd(typeof(List), converter); + _typeConverters.TryAdd(typeof(ProductBundleItemOrderData), new ProductBundleDataConverter(false)); + } + + public static void RegisterConverter(ITypeConverter typeConverter) + { + RegisterConverter(typeof(T), typeConverter); + } + + public static void RegisterConverter(Type type, ITypeConverter typeConverter) + { + Guard.ArgumentNotNull(() => type); + Guard.ArgumentNotNull(() => typeConverter); + + _typeConverters.TryAdd(type, typeConverter); + } + + public static ITypeConverter RemoveConverter(ITypeConverter typeConverter) + { + return RemoveConverter(typeof(T)); + } + + public static ITypeConverter RemoveConverter(Type type) + { + Guard.ArgumentNotNull(() => type); + + ITypeConverter converter = null; + _typeConverters.TryRemove(type, out converter); + return converter; + } + + public static ITypeConverter GetConverter() + { + return GetConverter(typeof(T)); + } + + public static ITypeConverter GetConverter(object component) + { + Guard.ArgumentNotNull(() => component); + + return GetConverter(component.GetType()); + } + + public static ITypeConverter GetConverter(Type type) + { + Guard.ArgumentNotNull(() => type); + + ITypeConverter converter; + if (_typeConverters.TryGetValue(type, out converter)) + { + return converter; + } + + var isGenericType = type.IsGenericType; + if (isGenericType) + { + var definition = type.GetGenericTypeDefinition(); + + // Nullables + if (definition == typeof(Nullable<>)) + { + converter = new NullableConverter(type); + RegisterConverter(type, converter); + return converter; + } + + // Sequence types + var genericArgs = type.GetGenericArguments(); + var isEnumerable = genericArgs.Length == 1 && type.IsSubClass(typeof(IEnumerable<>)); + if (isEnumerable) + { + converter = (ITypeConverter)Activator.CreateInstance(typeof(EnumerableConverter<>).MakeGenericType(genericArgs[0]), type); + RegisterConverter(type, converter); + return converter; + } + } + + // default fallback + converter = new TypeConverterAdapter(TypeDescriptor.GetConverter(type)); + RegisterConverter(type, converter); + return converter; + } + } +} diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/HooksEventConsumer.cs b/src/Libraries/SmartStore.Core/Data/Hooks/HooksEventConsumer.cs index ad567f4e7c..2372dbba00 100644 --- a/src/Libraries/SmartStore.Core/Data/Hooks/HooksEventConsumer.cs +++ b/src/Libraries/SmartStore.Core/Data/Hooks/HooksEventConsumer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using Autofac.Features.Metadata; @@ -50,9 +51,9 @@ public HooksEventConsumer( public void HandleEvent(PreActionHookEvent eventMessage) { - var entries = eventMessage.ModifiedEntries; + var entries = eventMessage.ModifiedEntries.ToArray(); - if (!entries.Any() || !_preHooks.Any()) + if (entries.Length == 0 || !_preHooks.Any()) return; foreach (var entry in entries) @@ -79,6 +80,7 @@ public void HandleEvent(PreActionHookEvent eventMessage) } } + [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] private IEnumerable GetPreHookInstancesFor(Type hookedType) { if (_preHooksCache.ContainsKey(hookedType)) @@ -93,9 +95,9 @@ private IEnumerable GetPreHookInstancesFor(Type hookedType) public void HandleEvent(PostActionHookEvent eventMessage) { - var entries = eventMessage.ModifiedEntries; + var entries = eventMessage.ModifiedEntries.ToArray(); - if (!entries.Any() || !_postHooks.Any()) + if (entries.Length == 0 || !_postHooks.Any()) return; foreach (var entry in entries) diff --git a/src/Libraries/SmartStore.Core/Data/IDbContext.cs b/src/Libraries/SmartStore.Core/Data/IDbContext.cs index cec8a1a2ce..cb276a03ac 100644 --- a/src/Libraries/SmartStore.Core/Data/IDbContext.cs +++ b/src/Libraries/SmartStore.Core/Data/IDbContext.cs @@ -87,12 +87,20 @@ IList ExecuteStoredProcedureList(string commandText, params ob /// true when the entity is attched already, false otherwise bool IsAttached(TEntity entity) where TEntity : BaseEntity; - /// - /// Detaches an entity from the current object context if it's attached - /// - /// Type of entity - /// The entity instance to detach - void DetachEntity(TEntity entity) where TEntity : BaseEntity; + /// + /// Attaches an entity to the context or returns an already attached entity (if it was already attached) + /// + /// Type of entity + /// Entity + /// Attached entity + TEntity Attach(TEntity entity) where TEntity : BaseEntity; + + /// + /// Detaches an entity from the current object context if it's attached + /// + /// Type of entity + /// The entity instance to detach + void DetachEntity(TEntity entity) where TEntity : BaseEntity; /// /// Detaches all entities of type TEntity from the current object context diff --git a/src/Libraries/SmartStore.Core/Data/IDbContextExtensions.cs b/src/Libraries/SmartStore.Core/Data/IDbContextExtensions.cs index 0889cd6684..f3afa01d07 100644 --- a/src/Libraries/SmartStore.Core/Data/IDbContextExtensions.cs +++ b/src/Libraries/SmartStore.Core/Data/IDbContextExtensions.cs @@ -32,6 +32,7 @@ public static void DetachEntities(this IDbContext ctx, IEnumerable /// Type of entity + /// /// The entity instance /// true on success, false on failure public static bool SetToUnchanged(this IDbContext ctx, TEntity entity) where TEntity : BaseEntity @@ -167,5 +168,10 @@ public static void LoadReference( } } + public static void AttachRange(this IDbContext ctx, IEnumerable entities) where TEntity : BaseEntity + { + entities.Each(x => ctx.Attach(x)); + } + } } diff --git a/src/Libraries/SmartStore.Core/Data/IRepository.cs b/src/Libraries/SmartStore.Core/Data/IRepository.cs index ca867c4e82..cae856f654 100644 --- a/src/Libraries/SmartStore.Core/Data/IRepository.cs +++ b/src/Libraries/SmartStore.Core/Data/IRepository.cs @@ -70,7 +70,7 @@ public partial interface IRepository where T : BaseEntity /// /// Marks the changes of existing entities to be saved to the store. /// - /// A list of entity instances that should be updated in the database. + /// A list of entity instances that should be updated in the database. /// Implementors should delegate this to the current void UpdateRange(IEnumerable entities); diff --git a/src/Libraries/SmartStore.Core/Data/Impex/ImportModeFlags.cs b/src/Libraries/SmartStore.Core/Data/Impex/ImportModeFlags.cs deleted file mode 100644 index f7c40437d9..0000000000 --- a/src/Libraries/SmartStore.Core/Data/Impex/ImportModeFlags.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace SmartStore.Core.Data -{ - - [Flags] - public enum ImportModeFlags - { - Insert = 1, - Update = 2 - } - -} diff --git a/src/Libraries/SmartStore.Core/Data/Impex/ImportProgressInfo.cs b/src/Libraries/SmartStore.Core/Data/Impex/ImportProgressInfo.cs deleted file mode 100644 index 9ddf010991..0000000000 --- a/src/Libraries/SmartStore.Core/Data/Impex/ImportProgressInfo.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SmartStore.Core.Data -{ - - public class ImportProgressInfo - { - - public int TotalRecords - { - get; - set; - } - - public int TotalProcessed - { - get; - set; - } - - public double ProcessedPercent - { - get - { - if (TotalRecords == 0) - return 0; - - return ((double)TotalProcessed / (double)TotalRecords) * 100; - } - } - - public int NewRecords - { - get; - set; - } - - public int ModifiedRecords - { - get; - set; - } - - public TimeSpan ElapsedTime - { - get; - set; - } - - public int TotalWarnings - { - get; - set; - } - - public int TotalErrors - { - get; - set; - } - } - -} diff --git a/src/Libraries/SmartStore.Core/Data/Impex/ImportResult.cs b/src/Libraries/SmartStore.Core/Data/Impex/ImportResult.cs deleted file mode 100644 index 1856607135..0000000000 --- a/src/Libraries/SmartStore.Core/Data/Impex/ImportResult.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace SmartStore.Core.Data -{ - - public class ImportResult - { - - public ImportResult() - { - this.Messages = new List(); - this.StartDateUtc = DateTime.UtcNow; - } - - public DateTime StartDateUtc - { - get; - set; - } - - public DateTime EndDateUtc - { - get; - set; - } - - public int TotalRecords - { - get; - set; - } - - public int NewRecords - { - get; - set; - } - - public int ModifiedRecords - { - get; - set; - } - - public int AffectedRecords - { - get { return NewRecords + ModifiedRecords; } - } - - public bool Cancelled - { - get; - set; - } - - public ImportMessage AddInfo(string message, ImportRowInfo affectedRow = null, string affectedField = null) - { - return this.AddMessage(message, ImportMessageType.Info, affectedRow, affectedField); - } - - public ImportMessage AddWarning(string message, ImportRowInfo affectedRow = null, string affectedField = null) - { - return this.AddMessage(message, ImportMessageType.Warning, affectedRow, affectedField); - } - - public ImportMessage AddError(string message, ImportRowInfo affectedRow = null, string affectedField = null) - { - return this.AddMessage(message, ImportMessageType.Error, affectedRow, affectedField); - } - - public ImportMessage AddError(Exception exception, int? affectedBatch = null, string stage = null) - { - var ex = exception; - while (true) - { - if (ex.InnerException == null) - break; - ex = ex.InnerException; - } - - var prefix = new List(); - if (affectedBatch.HasValue) - { - prefix.Add("Batch: " + affectedBatch.Value); - } - if (stage.HasValue()) - { - prefix.Add("Stage: " + stage); - } - - string msg = string.Empty; - if (prefix.Any()) - { - msg = "[{0}] ".FormatCurrent(String.Join(", ", prefix)); - } - - msg += ex.Message; - - return this.AddMessage(msg, ImportMessageType.Error); - } - - public ImportMessage AddMessage(string message, ImportMessageType severity, ImportRowInfo affectedRow = null, string affectedField = null) - { - var msg = new ImportMessage(message, severity); - msg.AffectedItem = affectedRow; - msg.AffectedField = affectedField; - this.Messages.Add(msg); - return msg; - } - - public IList Messages - { - get; - private set; - } - - public bool HasWarnings - { - get { return this.Messages.Any(x => x.MessageType == ImportMessageType.Warning); } - } - - public bool HasErrors - { - get { return this.Messages.Any(x => x.MessageType == ImportMessageType.Error); } - } - - } - -} diff --git a/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs b/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs index f177597e75..ed034c1730 100644 --- a/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs +++ b/src/Libraries/SmartStore.Core/Data/RepositoryExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Text; @@ -9,22 +10,6 @@ namespace SmartStore.Core.Data public static class RepositoryExtensions { - - public static IEnumerable LoadAll(this IRepository rs) where T : BaseEntity - { - return rs.Table.AsEnumerable(); - } - - public static IEnumerable Where(this IRepository rs, Func predicate) where T : BaseEntity - { - return rs.Table.Where(predicate); - } - - public static T GetSingle(this IRepository rs, Func predicate) where T : BaseEntity - { - return rs.Table.SingleOrDefault(predicate); - } - public static T GetFirst(this IRepository rs, Func predicate) where T : BaseEntity { return rs.Table.FirstOrDefault(predicate); @@ -51,7 +36,9 @@ public static void Delete(this IRepository rs, int id) where T : BaseEntit entity.Id = id; rs.Attach(entity); - rs.Context.ChangeState(entity, System.Data.Entity.EntityState.Deleted); + + // must downcast 'cause of Rhino mocks stub + rs.Context.ChangeState((BaseEntity)entity, System.Data.Entity.EntityState.Deleted); } public static void DeleteRange(this IRepository rs, IEnumerable ids) where T : BaseEntity @@ -75,6 +62,7 @@ public static void DeleteRange(this IRepository rs, IEnumerable ids) /// /// This method turns off auto detection, validation and hooking. /// + [SuppressMessage("ReSharper", "UnusedVariable")] public static int DeleteAll(this IRepository rs, Expression> predicate = null, bool cascade = false) where T : BaseEntity { var count = 0; diff --git a/src/Libraries/SmartStore.Core/Data/SqlFileTokenizer.cs b/src/Libraries/SmartStore.Core/Data/SqlFileTokenizer.cs index 343dad1287..e85c9bac4c 100644 --- a/src/Libraries/SmartStore.Core/Data/SqlFileTokenizer.cs +++ b/src/Libraries/SmartStore.Core/Data/SqlFileTokenizer.cs @@ -49,10 +49,10 @@ public IEnumerable Tokenize() using (var reader = ReadSqlFile()) { - var statement = string.Empty; + string statement; while ((statement = ReadNextSqlStatement(reader)) != null) { - yield return statement; + yield return statement.EmptyNull(); } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Blogs/BlogExtensions.cs b/src/Libraries/SmartStore.Core/Domain/Blogs/BlogExtensions.cs index d28f41b9c9..e5cbfc2f5c 100644 --- a/src/Libraries/SmartStore.Core/Domain/Blogs/BlogExtensions.cs +++ b/src/Libraries/SmartStore.Core/Domain/Blogs/BlogExtensions.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace SmartStore.Core.Domain.Blogs { public static class BlogExtensions { - public static string[] ParseTags(this BlogPost blogPost) + [SuppressMessage("ReSharper", "LoopCanBeConvertedToQuery")] + public static string[] ParseTags(this BlogPost blogPost) { if (blogPost == null) throw new ArgumentNullException("blogPost"); @@ -13,8 +15,8 @@ public static string[] ParseTags(this BlogPost blogPost) var parsedTags = new List(); if (!String.IsNullOrEmpty(blogPost.Tags)) { - string[] tags2 = blogPost.Tags.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - foreach (string tag2 in tags2) + var tags2 = blogPost.Tags.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var tag2 in tags2) { var tmp = tag2.Trim(); if (!String.IsNullOrEmpty(tmp)) diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs index f021ce4069..12f9fa8587 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs @@ -33,6 +33,7 @@ public CatalogSettings() CompareProductsEnabled = true; FilterEnabled = true; MaxFilterItemsToDisplay = 4; + SortFilterResultsByMatches = true; SubCategoryDisplayType = SubCategoryDisplayType.AboveProductList; ProductSearchAutoCompleteEnabled = true; ShowProductImagesInSearchAutoComplete = true; @@ -41,6 +42,7 @@ public CatalogSettings() NumberOfBestsellersOnHomepage = 6; ShowManufacturersOnHomepage = true; ShowManufacturerPictures = false; + ShowManufacturerPicturesInProductDetail = true; SearchPageProductsPerPage = 6; ProductsAlsoPurchasedEnabled = true; ProductsAlsoPurchasedNumber = 6; @@ -175,7 +177,12 @@ public CatalogSettings() /// Gets or sets a value indicating whether all filter criterias should be expanded /// public bool ExpandAllFilterCriteria { get; set; } - + + /// + /// Gets or sets a value indicating whether filter results should be sorted by matches + /// + public bool SortFilterResultsByMatches { get; set; } + /// /// Gets or sets a value indicating whether and where to display a list of subcategories /// @@ -301,10 +308,30 @@ public CatalogSettings() /// public bool ShowManufacturerPictures { get; set; } - /// - /// Gets or sets a number of products per page on search products page - /// - public int SearchPageProductsPerPage { get; set; } + /// + /// Gets or sets a value indicating whether to hide manufacturer pictures in product detail + /// + public bool ShowManufacturerPicturesInProductDetail { get; set; } + + /// + /// Gets or sets a value indicating whether to hide maufacturer default pictures + /// + public bool HideManufacturerDefaultPictures { get; set; } + + /// + /// Gets or sets a value indicating whether to hide category default pictures + /// + public bool HideCategoryDefaultPictures { get; set; } + + /// + /// Gets or sets a value indicating whether to hide product default pictures + /// + public bool HideProductDefaultPictures { get; set; } + + /// + /// Gets or sets a number of products per page on search products page + /// + public int SearchPageProductsPerPage { get; set; } /// /// Gets or sets "List of products purchased by other customers who purchased the above" option is enable @@ -366,10 +393,15 @@ public CatalogSettings() public bool SuppressSkuSearch { get; set; } - /// - /// Gets or sets the available customer selectable default page size options - /// - public string DefaultPageSizeOptions { get; set; } + /// + /// Gets or sets a value indicating whether to search long description + /// + public bool SearchDescriptions { get; set; } + + /// + /// Gets or sets the available customer selectable default page size options + /// + public string DefaultPageSizeOptions { get; set; } /// /// Gets or sets the price display type for prices in product lists diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs index a815e7d4b8..999f66bad3 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs @@ -18,7 +18,7 @@ namespace SmartStore.Core.Domain.Catalog /// [DataContract] public partial class Product : BaseEntity, ISoftDeletable, ILocalizedEntity, ISlugSupported, IAclSupported, IStoreMappingSupported, IMergedData - { + { private ICollection _productCategories; private ICollection _productManufacturers; private ICollection _productPictures; @@ -191,6 +191,7 @@ public string Sku /// Gets or sets the manufacturer part number /// [DataMember] + [Index] public string ManufacturerPartNumber { [DebuggerStepThrough] @@ -208,6 +209,7 @@ public string ManufacturerPartNumber /// Gets or sets the Global Trade Item Number (GTIN). These identifiers include UPC (in North America), EAN (in Europe), JAN (in Japan), and ISBN (for books). /// [DataMember] + [Index] public string Gtin { [DebuggerStepThrough] @@ -393,7 +395,7 @@ public int StockQuantity [DebuggerStepThrough] get { - return this.GetMergedDataValue("StockQuantity", _stockQuantity); + return this.GetMergedDataValue("StockQuantity", _stockQuantity); } set { @@ -1046,5 +1048,5 @@ public virtual ICollection ProductBundleItems get { return _productBundleItems ?? (_productBundleItems = new HashSet()); } protected set { _productBundleItems = value; } } - } + } } diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleData.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleData.cs index 5582074c65..354720c7ea 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleData.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductBundleData.cs @@ -34,58 +34,4 @@ public partial class ProductBundleItemOrderData public string AttributesInfo { get; set; } public bool PerItemShoppingCart { get; set; } } - - public class ProductBundleDataListTypeConverter : TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - return true; - - return base.CanConvertFrom(context, sourceType); - } - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - { - if (value is string) - { - List bundleData = null; - string rawValue = value as string; - - if (rawValue.HasValue()) - { - try - { - using (var reader = new StringReader(rawValue)) - { - var xml = new XmlSerializer(typeof(List)); - bundleData = (List)xml.Deserialize(reader); - } - } - catch { } - } - return bundleData; - } - return base.ConvertFrom(context, culture, value); - } - - public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) - { - if (destinationType == typeof(string)) - { - var bundleData = value as List; - - if (bundleData == null) - return ""; - - var sb = new StringBuilder(); - using (var writer = new StringWriter(sb)) - { - var xml = new XmlSerializer(typeof(List)); - xml.Serialize(writer, value); - return sb.ToString(); - } - } - return base.ConvertTo(context, culture, value, destinationType); - } - } } diff --git a/src/Libraries/SmartStore.Core/Domain/Common/CommonSettings.cs b/src/Libraries/SmartStore.Core/Domain/Common/CommonSettings.cs index 9ecb41ae4f..84ceca5428 100644 --- a/src/Libraries/SmartStore.Core/Domain/Common/CommonSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Common/CommonSettings.cs @@ -1,5 +1,4 @@ - -using SmartStore.Core.Configuration; +using SmartStore.Core.Configuration; namespace SmartStore.Core.Domain.Common { @@ -14,6 +13,7 @@ public CommonSettings() SitemapIncludeTopics = true; FullTextMode = FulltextSearchMode.ExactMatch; AutoUpdateEnabled = true; + EntityPickerPageSize = 48; } public bool UseSystemEmailForContactUsForm { get; set; } @@ -29,21 +29,25 @@ public CommonSettings() public bool SitemapIncludeTopics { get; set; } /// - /// Gets a sets a value indicating whether to display a warning if java-script is disabled + /// Gets or sets a value indicating whether to display a warning if java-script is disabled /// public bool DisplayJavaScriptDisabledWarning { get; set; } /// - /// Gets a sets a value indicating whether to full-text search is supported + /// Gets or sets a value indicating whether to full-text search is supported /// public bool UseFullTextSearch { get; set; } /// - /// Gets a sets a Full-Text search mode + /// Gets or sets a Full-Text search mode /// public FulltextSearchMode FullTextMode { get; set; } public bool AutoUpdateEnabled { get; set; } - } + /// + /// Gets or sets the page size for the entity picker + /// + public int EntityPickerPageSize { get; set; } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Common/RestrictionContextTypes.cs b/src/Libraries/SmartStore.Core/Domain/Common/RestrictionContextTypes.cs deleted file mode 100644 index 98a1590c7e..0000000000 --- a/src/Libraries/SmartStore.Core/Domain/Common/RestrictionContextTypes.cs +++ /dev/null @@ -1,15 +0,0 @@ - -namespace SmartStore.Core.Domain.Common -{ - public enum CountryRestrictionContextType - { - BillingAddress = 0, - ShippingAddress - } - - public enum AmountRestrictionContextType - { - SubtotalAmount = 0, - TotalAmount - } -} diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerNameFormat.cs b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerNameFormat.cs index 227eba65cf..c10aa440f4 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerNameFormat.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerNameFormat.cs @@ -3,7 +3,7 @@ namespace SmartStore.Core.Domain.Customers /// /// Represents the customer name fortatting enumeration /// - public enum CustomerNameFormat : int + public enum CustomerNameFormat { /// /// Show emails diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerNumberMethod.cs b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerNumberMethod.cs index 6e32e611b1..f333486399 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerNumberMethod.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerNumberMethod.cs @@ -3,7 +3,7 @@ namespace SmartStore.Core.Domain.Customers /// /// Represents the customer number method /// - public enum CustomerNumberMethod : int + public enum CustomerNumberMethod { /// /// no customer number will be saved diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerNumberVisibility.cs b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerNumberVisibility.cs index 05d37b7b5a..d05e588e97 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerNumberVisibility.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerNumberVisibility.cs @@ -3,7 +3,7 @@ namespace SmartStore.Core.Domain.Customers /// /// Represents the customer visibility in the frontend /// - public enum CustomerNumberVisibility : int + public enum CustomerNumberVisibility { /// /// Customer number won't be displayed in the frontend diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs index 83380a63a3..e6d7f56f56 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs @@ -24,6 +24,7 @@ public CustomerSettings() NewsletterEnabled = true; OnlineCustomerMinutes = 20; StoreLastVisitedPage = true; + DisplayPrivacyAgreementOnContactUs = false; } /// @@ -151,7 +152,11 @@ public CustomerSettings() /// public bool StoreLastVisitedPage { get; set; } - + /// + /// Gets or sets a value indicating whether to display a checkbox to the customer where he can agree to privacy terms + /// + public bool DisplayPrivacyAgreementOnContactUs { get; set; } + #region Form fields /// @@ -246,8 +251,12 @@ public CustomerSettings() #endregion - // codehint: sm-add (no ui, only db edit) public string PrefillLoginUsername { get; set; } public string PrefillLoginPwd { get; set; } - } + + /// + /// Identifier of a customer role that new registered customers will be assigned to + /// + public int RegisterCustomerRoleId { get; set; } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/PasswordFormat.cs b/src/Libraries/SmartStore.Core/Domain/Customers/PasswordFormat.cs index b84ab3a661..4b35797ec7 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/PasswordFormat.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/PasswordFormat.cs @@ -1,7 +1,7 @@  namespace SmartStore.Core.Domain.Customers { - public enum PasswordFormat : int + public enum PasswordFormat { Clear = 0, Hashed = 1, diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/RewardPointsSettings.cs b/src/Libraries/SmartStore.Core/Domain/Customers/RewardPointsSettings.cs index 1d6ec48df5..f0e99e9502 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/RewardPointsSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/RewardPointsSettings.cs @@ -24,10 +24,15 @@ public RewardPointsSettings() /// public decimal ExchangeRate { get; set; } - /// - /// Gets or sets a number of points awarded for registration - /// - public int PointsForRegistration { get; set; } + /// + /// Gets or sets a value whether to round down reward points + /// + public bool RoundDownRewardPoints { get; set; } + + /// + /// Gets or sets a number of points awarded for registration + /// + public int PointsForRegistration { get; set; } /// /// Gets or sets a number of points awarded for a product review diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/UserRegistrationType.cs b/src/Libraries/SmartStore.Core/Domain/Customers/UserRegistrationType.cs index dc89589020..1f950092db 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/UserRegistrationType.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/UserRegistrationType.cs @@ -3,7 +3,7 @@ namespace SmartStore.Core.Domain.Customers /// /// Represents the customer registration type fortatting enumeration /// - public enum UserRegistrationType : int + public enum UserRegistrationType { /// /// Standard account creation diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeEnums.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeEnums.cs new file mode 100644 index 0000000000..7a3ab1f8ae --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeEnums.cs @@ -0,0 +1,26 @@ +namespace SmartStore.Core.Domain.DataExchange +{ + public delegate void ProgressValueSetter(int value, int maximum, string message); + + + /// + /// Data exchange abortion types + /// + public enum DataExchangeAbortion + { + /// + /// No abortion. Go on with processing. + /// + None = 0, + + /// + /// Break item processing but not the rest of the execution. Typically used for demo limitations. + /// + Soft, + + /// + /// Break processing immediately + /// + Hard + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeSettings.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeSettings.cs index 742e379df5..4b0bc6d497 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeSettings.cs @@ -7,11 +7,22 @@ public class DataExchangeSettings : ISettings public DataExchangeSettings() { MaxFileNameLength = 50; + ImageDownloadTimeout = 10; } /// /// The maximum length of file names (in characters) of files created by the export framework /// public int MaxFileNameLength { get; set; } + + /// + /// Relative path to a folder with images to be imported + /// + public string ImageImportFolder { get; set; } + + /// + /// The timeout for image download per entity in minutes + /// + public int ImageDownloadTimeout { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportDeployment.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportDeployment.cs index 3b8190d6a4..b9e323c670 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportDeployment.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportDeployment.cs @@ -21,14 +21,9 @@ public class ExportDeployment : BaseEntity, ICloneable public bool Enabled { get; set; } /// - /// Whether to deploy in a folder that can be reached from the internet + /// XML with information about the last deployment result /// - public bool IsPublic { get; set; } - - /// - /// Whether to create a zip archive with the content of the export - /// - public bool CreateZip { get; set; } + public string ResultInfo { get; set; } /// /// The deployment type identifier @@ -84,6 +79,11 @@ public ExportHttpTransmissionType HttpTransmissionType /// public string FileSystemPath { get; set; } + /// + /// Path of a subfolder + /// + public string SubFolder { get; set; } + /// /// Multiple email addresses can be separated by commas /// @@ -117,14 +117,13 @@ public ExportDeployment Clone() { Name = this.Name, Enabled = this.Enabled, - IsPublic = this.IsPublic, - CreateZip = this.CreateZip, DeploymentTypeId = this.DeploymentTypeId, Username = this.Username, Password = this.Password, Url = this.Url, HttpTransmissionTypeId = this.HttpTransmissionTypeId, FileSystemPath = this.FileSystemPath, + SubFolder = this.SubFolder, EmailAddresses = this.EmailAddresses, EmailSubject = this.EmailSubject, EmailAccountId = this.EmailAccountId, diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportCore.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs similarity index 76% rename from src/Libraries/SmartStore.Core/Domain/DataExchange/ExportCore.cs rename to src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs index f9a5dcc18e..6ea68ec73c 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportCore.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs @@ -1,4 +1,5 @@ - +using System; + namespace SmartStore.Core.Domain.DataExchange { /// @@ -22,7 +23,8 @@ public enum ExportDeploymentType FileSystem = 0, Email, Http, - Ftp + Ftp, + PublicFolder } /// @@ -59,64 +61,77 @@ public enum ExportAttributeValueMerging } /// - /// Controls data processing and projection items supported by export provider + /// Controls data processing and projection items supported by an export provider /// - public enum ExportSupport + [Flags] + public enum ExportFeatures { + None = 0, + /// /// Whether to automatically create a file based public deployment when an export profile is created /// - CreateInitialPublicDeployment = 0, + CreatesInitialPublicDeployment = 1, /// /// Whether to offer option to include\exclude grouped products /// - ProjectionNoGroupedProducts, + CanOmitGroupedProducts = 1 << 2, /// /// Whether to offer option to export attribute combinations as products /// - ProjectionAttributeCombinationAsProduct, + CanProjectAttributeCombinations = 1 << 3, /// /// Whether to offer further options to manipulate the product description /// - ProjectionDescription, + CanProjectDescription = 1 << 4, /// /// Whether to offer option to enter a brand fallback /// - ProjectionBrand, + OffersBrandFallback = 1 << 5, /// /// Whether to offer option to set a picture size and to get the URL of the main image /// - ProjectionMainPictureUrl, + CanIncludeMainPicture = 1 << 6, /// /// Whether to use SKU as manufacturer part number if MPN is empty /// - ProjectionUseOwnProductNo, - + UsesSkuAsMpnFallback = 1 << 7, + /// /// Whether to offer option to enter a shipping time fallback /// - ProjectionShippingTime, + OffersShippingTimeFallback = 1 << 8, /// /// Whether to offer option to enter a shipping costs fallback and a free shipping threshold /// - ProjectionShippingCosts, + OffersShippingCostsFallback = 1 << 9, /// /// Whether to get the calculated old product price /// - ProjectionOldPrice, + UsesOldPrice = 1 << 10, + + /// + /// Whether to get the calculated special and regular price (ignoring special offers) + /// + UsesSpecialPrice = 1 << 11, + + /// + /// Whether to not automatically send a completion email + /// + CanOmitCompletionMail = 1 << 12, /// - /// Whether to get the calculated special and regular (ignoring special offers) price + /// Whether to provide additional data of attribute combinations /// - ProjectionSpecialPrice + UsesAttributeCombination = 1 << 13 } /// @@ -128,25 +143,4 @@ public enum ExportOrderStatusChange Processing, Complete } - - /// - /// Export abortion types - /// - public enum ExportAbortion - { - /// - /// No abortion. Go on with processing. - /// - None = 0, - - /// - /// Break item processing but not the rest of the execution. Typically used for demo limitations. - /// - Soft, - - /// - /// Break processing immediately - /// - Hard - } } diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportFilter.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportFilter.cs index 7f22f55302..23d08fd03c 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportFilter.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportFilter.cs @@ -1,9 +1,5 @@ using System; -using System.Collections.Generic; using SmartStore.Core.Domain.Catalog; -using SmartStore.Core.Domain.Orders; -using SmartStore.Core.Domain.Payments; -using SmartStore.Core.Domain.Shipping; namespace SmartStore.Core.Domain.DataExchange { @@ -116,11 +112,64 @@ public class ExportFilter /// public int[] ShippingStatusIds { get; set; } + #endregion + + #region Customer + + /// + /// Filter by active or inactive customers + /// + public bool? IsActiveCustomer { get; set; } + + /// + /// Filter by tax exempt customers + /// + public bool? IsTaxExempt { get; set; } + /// /// Identifiers of customer roles /// public int[] CustomerRoleIds { get; set; } + /// + /// Filter by billing country identifiers + /// + public int[] BillingCountryIds { get; set; } + + /// + /// Filter by shipping country identifiers + /// + public int[] ShippingCountryIds { get; set; } + + /// + /// Filter by last activity date from + /// + public DateTime? LastActivityFrom { get; set; } + + /// + /// Filter by last activity date to + /// + public DateTime? LastActivityTo { get; set; } + + /// + /// Filter by at least spent amount + /// + public decimal? HasSpentAtLeastAmount { get; set; } + + /// + /// Filter by at least placed orders + /// + public int? HasPlacedAtLeastOrders { get; set; } + + #endregion + + #region Newsletter Subscription + + /// + /// Filter by active or inactive subscriber + /// + public bool? IsActiveSubscriber { get; set; } + #endregion } } diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProfile.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProfile.cs index 75331d4a2c..3161d1ea94 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProfile.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProfile.cs @@ -23,7 +23,7 @@ public ExportProfile() public string Name { get; set; } /// - /// The name of the folder (file system) + /// The root path of the export folder /// public string FolderName { get; set; } @@ -32,11 +32,21 @@ public ExportProfile() /// public string FileNamePattern { get; set; } + /// + /// The system name of the profile + /// + public string SystemName { get; set; } + /// /// The system name of the export provider /// public string ProviderSystemName { get; set; } + /// + /// Whether the profile is an unremovable system profile + /// + public bool IsSystemProfile { get; set; } + /// /// Whether the export profile is enabled /// @@ -88,7 +98,7 @@ public ExportProfile() public bool PerStore { get; set; } /// - /// Identifier of an email account used to send a notification message of the completion of the export + /// Email Account identifier used to send a notification message an export completes /// public int EmailAccountId { get; set; } diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProjection.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProjection.cs index abe979107d..493acda473 100644 --- a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProjection.cs +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProjection.cs @@ -7,7 +7,9 @@ namespace SmartStore.Core.Domain.DataExchange /// /// Settings projected onto an export /// - /// Note possible projection controlling: a) developer controls, b) merchant controls, c) developer controls what the merchant can control + /// + /// Note possible projection controlling: a) developer controls, b) merchant controls, c) developer controls what the merchant can control + /// [Serializable] public class ExportProjection { @@ -93,6 +95,11 @@ public ExportDescriptionMerging DescriptionMerging /// public string Brand { get; set; } + /// + /// Number of images per object to be exported + /// + public int? NumberOfPictures { get; set; } + /// /// Picture size /// diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportEnums.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportEnums.cs new file mode 100644 index 0000000000..94bbd69246 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportEnums.cs @@ -0,0 +1,28 @@ +using System; + +namespace SmartStore.Core.Domain.DataExchange +{ + /// + /// Supported entity types + /// + public enum ImportEntityType + { + Product = 0, + Category, + Customer, + NewsLetterSubscription + } + + public enum ImportFileType + { + CSV = 0, + XLSX + } + + [Flags] + public enum ImportModeFlags + { + Insert = 1, + Update = 2 + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportProfile.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportProfile.cs new file mode 100644 index 0000000000..1139496b4a --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ImportProfile.cs @@ -0,0 +1,113 @@ +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Domain.Tasks; + +namespace SmartStore.Core.Domain +{ + public class ImportProfile : BaseEntity + { + /// + /// The name of the profile + /// + public string Name { get; set; } + + /// + /// The name of the folder (file system) + /// + public string FolderName { get; set; } + + /// + /// The identifier of the file type + /// + public int FileTypeId { get; set; } + + /// + /// The file type + /// + public ImportFileType FileType + { + get + { + return (ImportFileType)FileTypeId; + } + set + { + FileTypeId = (int)value; + } + } + + /// + /// The identifier of the entity type + /// + public int EntityTypeId { get; set; } + + /// + /// The entity type + /// + public ImportEntityType EntityType + { + get + { + return (ImportEntityType)EntityTypeId; + } + set + { + EntityTypeId = (int)value; + } + } + + /// + /// Whether the profile is enabled + /// + public bool Enabled { get; set; } + + /// + /// Number of records to bypass + /// + public int Skip { get; set; } + + /// + /// Maximum number of records to return + /// + public int Take { get; set; } + + /// + /// Whether to only update existing data + /// + public bool UpdateOnly { get; set; } + + /// + /// Name of key fields to identify existing records during import + /// + public string KeyFieldNames { get; set; } + + /// + /// File type specific configuration + /// + public string FileTypeConfiguration { get; set; } + + /// + /// XML with extra data + /// + public string ExtraData { get; set; } + + /// + /// Mapping of import columns + /// + public string ColumnMapping { get; set; } + + /// + /// XML with information about the last import + /// + public string ResultInfo { get; set; } + + /// + /// The scheduling task identifier + /// + public int SchedulingTaskId { get; set; } + + /// + /// The scheduling task + /// + public virtual ScheduleTask ScheduleTask { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/Localization/LocalizedProperty.cs b/src/Libraries/SmartStore.Core/Domain/Localization/LocalizedProperty.cs index ffd8d2fc22..f0d0406fb4 100644 --- a/src/Libraries/SmartStore.Core/Domain/Localization/LocalizedProperty.cs +++ b/src/Libraries/SmartStore.Core/Domain/Localization/LocalizedProperty.cs @@ -13,28 +13,28 @@ public partial class LocalizedProperty : BaseEntity /// Gets or sets the entity identifier /// [DataMember] - [Index("IX_LocalizedProperty_Compound", 1)] + [Index("IX_LocalizedProperty_Compound", Order = 1)] public int EntityId { get; set; } /// /// Gets or sets the language identifier /// [DataMember] - [Index("IX_LocalizedProperty_Compound", 4)] + [Index("IX_LocalizedProperty_Compound", Order = 4)] public int LanguageId { get; set; } /// /// Gets or sets the locale key group /// [DataMember] - [Index("IX_LocalizedProperty_Compound", 3)] + [Index("IX_LocalizedProperty_Compound", Order = 3)] public string LocaleKeyGroup { get; set; } /// /// Gets or sets the locale key /// [DataMember] - [Index("IX_LocalizedProperty_Compound", 2)] + [Index("IX_LocalizedProperty_Compound", Order = 2)] public string LocaleKey { get; set; } /// diff --git a/src/Libraries/SmartStore.Core/Domain/Media/MediaSettings.cs b/src/Libraries/SmartStore.Core/Domain/Media/MediaSettings.cs index 6d1deff20f..b3072c0912 100644 --- a/src/Libraries/SmartStore.Core/Domain/Media/MediaSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Media/MediaSettings.cs @@ -1,5 +1,4 @@ - -using SmartStore.Core.Configuration; +using SmartStore.Core.Configuration; namespace SmartStore.Core.Domain.Media { @@ -11,6 +10,7 @@ public MediaSettings() ProductThumbPictureSize = 100; ProductDetailsPictureSize = 300; ProductThumbPictureSizeOnProductDetailsPage = 70; + MessageProductThumbPictureSize = 70; AssociatedProductPictureSize = 125; BundledProductPictureSize = 70; CategoryThumbPictureSize = 125; @@ -18,7 +18,7 @@ public MediaSettings() CartThumbPictureSize = 80; CartThumbBundleItemPictureSize = 32; MiniCartThumbPictureSize = 32; - AutoCompleteSearchThumbPictureSize = 20; + VariantValueThumbPictureSize = 20; MaximumImageSize = 1280; DefaultPictureZoomEnabled = true; PictureZoomType = "window"; @@ -30,16 +30,17 @@ public MediaSettings() public int ProductThumbPictureSize { get; set; } public int ProductDetailsPictureSize { get; set; } public int ProductThumbPictureSizeOnProductDetailsPage { get; set; } - public int AssociatedProductPictureSize { get; set; } + public int MessageProductThumbPictureSize { get; set; } + public int AssociatedProductPictureSize { get; set; } public int BundledProductPictureSize { get; set; } - public int CategoryThumbPictureSize { get; set; } + public int CategoryThumbPictureSize { get; set; } public int ManufacturerThumbPictureSize { get; set; } public int CartThumbPictureSize { get; set; } public int CartThumbBundleItemPictureSize { get; set; } public int MiniCartThumbPictureSize { get; set; } - public int AutoCompleteSearchThumbPictureSize { get; set; } + public int VariantValueThumbPictureSize { get; set; } - public bool DefaultPictureZoomEnabled { get; set; } + public bool DefaultPictureZoomEnabled { get; set; } public string PictureZoomType { get; set; } public int MaximumImageSize { get; set; } diff --git a/src/Libraries/SmartStore.Core/Domain/Media/PictureType.cs b/src/Libraries/SmartStore.Core/Domain/Media/PictureType.cs index 8fdb9118df..3da89ff17a 100644 --- a/src/Libraries/SmartStore.Core/Domain/Media/PictureType.cs +++ b/src/Libraries/SmartStore.Core/Domain/Media/PictureType.cs @@ -3,7 +3,7 @@ namespace SmartStore.Core.Domain.Media /// /// Represents a picture item type /// - public enum PictureType : int + public enum PictureType { /// /// Entities (products, categories, manufacturers) diff --git a/src/Libraries/SmartStore.Core/Domain/Messages/EmailAttachmentStorageLocation.cs b/src/Libraries/SmartStore.Core/Domain/Messages/EmailAttachmentStorageLocation.cs index 34d369c1cf..3dada46a59 100644 --- a/src/Libraries/SmartStore.Core/Domain/Messages/EmailAttachmentStorageLocation.cs +++ b/src/Libraries/SmartStore.Core/Domain/Messages/EmailAttachmentStorageLocation.cs @@ -1,4 +1,5 @@ - +using SmartStore.Core.Domain.Media; + namespace SmartStore.Core.Domain.Messages { public enum EmailAttachmentStorageLocation diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutAttribute.cs b/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutAttribute.cs index a317879baa..63cbf58ea8 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutAttribute.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutAttribute.cs @@ -1,14 +1,15 @@ using System.Collections.Generic; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Stores; namespace SmartStore.Core.Domain.Orders { - /// - /// Represents a checkout attribute - /// - public partial class CheckoutAttribute : BaseEntity, ILocalizedEntity - { + /// + /// Represents a checkout attribute + /// + public partial class CheckoutAttribute : BaseEntity, ILocalizedEntity, IStoreMappingSupported + { private ICollection _checkoutAttributeValues; public CheckoutAttribute() @@ -60,11 +61,16 @@ public CheckoutAttribute() /// Gets or sets whether the checkout attribute is active /// public bool IsActive { get; set; } - - /// - /// Gets the attribute control type - /// - public AttributeControlType AttributeControlType + + /// + /// Gets or sets a value indicating whether the entity is limited/restricted to certain stores + /// + public bool LimitedToStores { get; set; } + + /// + /// Gets the attribute control type + /// + public AttributeControlType AttributeControlType { get { @@ -75,6 +81,7 @@ public AttributeControlType AttributeControlType this.AttributeControlTypeId = (int)value; } } + /// /// Gets the checkout attribute values /// diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutEnums.cs b/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutEnums.cs new file mode 100644 index 0000000000..4ce792b336 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/Orders/CheckoutEnums.cs @@ -0,0 +1,45 @@ +namespace SmartStore.Core.Domain.Orders +{ + /// + /// Setting for newsletter subscription in checkout + /// + public enum CheckoutNewsLetterSubscription + { + /// + /// No newsletter subscription checkbox + /// + None = 0, + + /// + /// Deactivated newsletter subscription checkbox + /// + Deactivated, + + /// + /// Activated newsletter subscription checkbox + /// + Activated + } + + + /// + /// Setting to hand over customer email to third party + /// + public enum CheckoutThirdPartyEmailHandOver + { + /// + /// No third party email hand over checkbox + /// + None = 0, + + /// + /// Deactivated third party email hand over checkbox + /// + Deactivated, + + /// + /// Activated third party email hand over checkbox + /// + Activated + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/Order.cs b/src/Libraries/SmartStore.Core/Domain/Orders/Order.cs index b48ef9fcf6..9db5f14733 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/Order.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/Order.cs @@ -451,19 +451,25 @@ protected virtual SortedDictionary ParseTaxRates(string taxRat public int? RewardPointsRemaining { get; set; } /// - /// Gets or sets a value indicating whether a new payment notification (IPN) arrived + /// Gets or sets a value indicating whether a new payment notification arrived (IPN, webhook, callback etc.) /// [DataMember] public bool HasNewPaymentNotification { get; set; } - #endregion + /// + /// Gets or sets a value indicating whether the customer accepted to hand over email address to third party + /// + [DataMember] + public bool AcceptThirdPartyEmailHandOver { get; set; } - #region Navigation properties + #endregion - /// - /// Gets or sets the customer - /// - [DataMember] + #region Navigation properties + + /// + /// Gets or sets the customer + /// + [DataMember] public virtual Customer Customer { get; set; } /// diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/OrderSettings.cs b/src/Libraries/SmartStore.Core/Domain/Orders/OrderSettings.cs index 7a55c36f70..05e9a49fd4 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/OrderSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/OrderSettings.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using SmartStore.Core.Configuration; +using SmartStore.Core.Configuration; using SmartStore.Core.Domain.Localization; namespace SmartStore.Core.Domain.Orders @@ -16,6 +15,7 @@ public OrderSettings() ReturnRequestReasons = "Received Wrong Product,Wrong Product Ordered,There Was A Problem With The Product"; NumberOfDaysReturnRequestAvailable = 365; MinimumOrderPlacementInterval = 30; + OrderListPageSize = 10; } /// @@ -83,5 +83,14 @@ public OrderSettings() /// public int MinimumOrderPlacementInterval { get; set; } - } + /// + /// Gets or sets a value indicating whether to display all orders of all stores to a customer + /// + public bool DisplayOrdersOfAllStores { get; set; } + + /// + /// Page size of the order list + /// + public int OrderListPageSize { get; set; } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/OrderStatus.cs b/src/Libraries/SmartStore.Core/Domain/Orders/OrderStatus.cs index cbff14a755..bf41213405 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/OrderStatus.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/OrderStatus.cs @@ -3,7 +3,7 @@ namespace SmartStore.Core.Domain.Orders /// /// Represents an order status enumeration /// - public enum OrderStatus : int + public enum OrderStatus { /// /// Pending diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/RecurringPayment.cs b/src/Libraries/SmartStore.Core/Domain/Orders/RecurringPayment.cs index dd4896bb5b..9861dc9302 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/RecurringPayment.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/RecurringPayment.cs @@ -62,12 +62,12 @@ public DateTime? NextPaymentDate DateTime? result = null; if (!this.IsActive) - return result; + return null; var historyCollection = this.RecurringPaymentHistory; if (historyCollection.Count >= this.TotalCycles) { - return result; + return null; } //set another value to change calculation method diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/ReturnRequestStatus.cs b/src/Libraries/SmartStore.Core/Domain/Orders/ReturnRequestStatus.cs index 8cbb36ee80..02fea117d8 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/ReturnRequestStatus.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/ReturnRequestStatus.cs @@ -4,7 +4,7 @@ namespace SmartStore.Core.Domain.Orders /// /// Represents a return status /// - public enum ReturnRequestStatus : int + public enum ReturnRequestStatus { /// /// Pending diff --git a/src/Libraries/SmartStore.Core/Domain/Orders/ShoppingCartSettings.cs b/src/Libraries/SmartStore.Core/Domain/Orders/ShoppingCartSettings.cs index 7398790f67..e403d7cdb1 100644 --- a/src/Libraries/SmartStore.Core/Domain/Orders/ShoppingCartSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Orders/ShoppingCartSettings.cs @@ -1,9 +1,10 @@ using SmartStore.Core.Configuration; +using SmartStore.Core.Domain.Localization; namespace SmartStore.Core.Domain.Orders { - public class ShoppingCartSettings : ISettings - { + public class ShoppingCartSettings : BaseEntity, ISettings, ILocalizedEntity + { public ShoppingCartSettings() { MaximumShoppingCartItems = 1000; @@ -17,6 +18,7 @@ public ShoppingCartSettings() ShowDiscountBox = true; ShowGiftCardBox = true; ShowCommentBox = true; + ShowEsdRevocationWaiverBox = true; CrossSellsNumber = 8; EmailWishlistEnabled = true; MiniShoppingCartEnabled = true; @@ -97,11 +99,31 @@ public ShoppingCartSettings() /// Gets or sets a value indicating whether to show a comment box on shopping cart page /// public bool ShowCommentBox { get; set; } - - /// - /// Gets or sets a number of "Cross-sells" on shopping cart page - /// - public int CrossSellsNumber { get; set; } + + /// + /// Gets or sets a value indicating whether to show a revocation waiver checkbox box for ESD products + /// + public bool ShowEsdRevocationWaiverBox { get; set; } + + /// + /// Gets or sets a value indicating whether to show a checkbox to subscribe to newsletters + /// + public CheckoutNewsLetterSubscription NewsLetterSubscription { get; set; } + + /// + /// Gets or sets a value indicating whether to show a checkbox to let the customer accept to hand over email address to third party + /// + public CheckoutThirdPartyEmailHandOver ThirdPartyEmailHandOver { get; set; } + + /// + /// Gets or sets the label to accept to hand over the email to third party + /// + public string ThirdPartyEmailHandOverLabel { get; set; } + + /// + /// Gets or sets a number of "Cross-sells" on shopping cart page + /// + public int CrossSellsNumber { get; set; } /// /// Gets or sets a value indicating whether "email a wishlist" feature is enabled diff --git a/src/Libraries/SmartStore.Core/Domain/Payments/PaymentMethod.cs b/src/Libraries/SmartStore.Core/Domain/Payments/PaymentMethod.cs index 5b81853884..e078434cc6 100644 --- a/src/Libraries/SmartStore.Core/Domain/Payments/PaymentMethod.cs +++ b/src/Libraries/SmartStore.Core/Domain/Payments/PaymentMethod.cs @@ -1,5 +1,4 @@ using System.Runtime.Serialization; -using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Localization; namespace SmartStore.Core.Domain.Payments @@ -16,80 +15,6 @@ public partial class PaymentMethod : BaseEntity, ILocalizedEntity [DataMember] public string PaymentMethodSystemName { get; set; } - /// - /// Gets or sets identifiers of customer roles (comma separated) to be excluded in checkout - /// - [DataMember] - public string ExcludedCustomerRoleIds { get; set; } - - /// - /// Gets or sets identifiers of countries (comma separated) to be excluded in checkout - /// - [DataMember] - public string ExcludedCountryIds { get; set; } - - /// - /// Gets or sets shipping methods (comma separated) to be excluded in checkout - /// - [DataMember] - public string ExcludedShippingMethodIds { get; set; } - - /// - /// Gets or sets the context identifier for country exclusion - /// - [DataMember] - public int CountryExclusionContextId { get; set; } - - /// - /// Gets or sets the country exclusion context - /// - [DataMember] - public CountryRestrictionContextType CountryExclusionContext - { - get - { - return (CountryRestrictionContextType)this.CountryExclusionContextId; - } - set - { - this.CountryExclusionContextId = (int)value; - } - } - - /// - /// Gets or sets the minimum order amount for which to offer the payment method - /// - [DataMember] - public decimal? MinimumOrderAmount { get; set; } - - /// - /// Gets or sets the maximum order amount for which to offer the payment method - /// - [DataMember] - public decimal? MaximumOrderAmount { get; set; } - - /// - /// Gets or sets the context identifier for amount restriction - /// - [DataMember] - public int AmountRestrictionContextId { get; set; } - - /// - /// Gets or sets the amount restriction context - /// - [DataMember] - public AmountRestrictionContextType AmountRestrictionContext - { - get - { - return (AmountRestrictionContextType)this.AmountRestrictionContextId; - } - set - { - this.AmountRestrictionContextId = (int)value; - } - } - /// /// Gets or sets the full description /// diff --git a/src/Libraries/SmartStore.Core/Domain/Payments/PaymentStatus.cs b/src/Libraries/SmartStore.Core/Domain/Payments/PaymentStatus.cs index ddd4e0bcaa..44f3dc262e 100644 --- a/src/Libraries/SmartStore.Core/Domain/Payments/PaymentStatus.cs +++ b/src/Libraries/SmartStore.Core/Domain/Payments/PaymentStatus.cs @@ -4,7 +4,7 @@ namespace SmartStore.Core.Domain.Payments /// /// Represents a payment status enumeration /// - public enum PaymentStatus : int + public enum PaymentStatus { /// /// Pending diff --git a/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs b/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs index f6ec324336..1f06b5aba6 100644 --- a/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs @@ -1,35 +1,80 @@ - +using System; using System.Collections.Generic; using SmartStore.Core.Configuration; namespace SmartStore.Core.Domain.Seo { - public class SeoSettings : ISettings + public class SeoSettings : ISettings { public SeoSettings() { PageTitleSeparator = ". "; PageTitleSeoAdjustment = PageTitleSeoAdjustment.PagenameAfterStorename; - DefaultTitle = "Your store"; + DefaultTitle = "Shop"; DefaultMetaKeywords = ""; DefaultMetaDescription = ""; - AllowUnicodeCharsInUrls = true; - CanonicalHostNameRule = Seo.CanonicalHostNameRule.NoRule; - ReservedUrlRecordSlugs = new List { "admin", "install", "recentlyviewedproducts", "newproducts", "compareproducts", "clearcomparelist", "setproductreviewhelpfulness", "login", "register", "logout", "cart", "wishlist", "emailwishlist", "checkout", "contactus", "passwordrecovery", "subscribenewsletter", "blog", "boards", "inboxupdate", "sentupdate", "news", "sitemap", "sitemapseo", "search", "config", "api", "odata" }; - ExtraRobotsDisallows = new List { "/blog/tag/", "/blog/month/", "/producttags/" }; + AllowUnicodeCharsInUrls = false; + CanonicalHostNameRule = CanonicalHostNameRule.NoRule; LoadAllUrlAliasesOnStartup = true; + + ExtraRobotsDisallows = new List { "/blog/tag/", "/blog/month/", "/producttags/" }; + + ReservedUrlRecordSlugs = new List + { + "admin", + "install", + "recentlyviewedproducts", + "newproducts", + "compareproducts", + "clearcomparelist", + "setproductreviewhelpfulness", + "login", + "register", + "logout", + "cart", + "wishlist", + "emailwishlist", + "checkout", + "contactus", + "passwordrecovery", + "subscribenewsletter", + "blog", + "boards", + "inboxupdate", + "sentupdate", + "news", + "sitemap", + "sitemapseo", + "search", + "config", + "api", + "odata" + }; + + SeoNameCharConversion = string.Join(Environment.NewLine, new List + { + "ä;ae", + "ö;oe", + "ü;ue", + "Ä;Ae", + "Ö;Oe", + "Ü;Ue", + "ß;ss" + }); } - + public string PageTitleSeparator { get; set; } public PageTitleSeoAdjustment PageTitleSeoAdjustment { get; set; } public string DefaultTitle { get; set; } public string DefaultMetaKeywords { get; set; } public string DefaultMetaDescription { get; set; } + public string MetaRobotsContent { get; set; } - public bool ConvertNonWesternChars { get; set; } + public bool ConvertNonWesternChars { get; set; } public bool AllowUnicodeCharsInUrls { get; set; } + public string SeoNameCharConversion { get; set; } - public bool CanonicalUrlsEnabled { get; set; } + public bool CanonicalUrlsEnabled { get; set; } public CanonicalHostNameRule CanonicalHostNameRule { get; set; } /// diff --git a/src/Libraries/SmartStore.Core/Domain/Shipping/Shipment.cs b/src/Libraries/SmartStore.Core/Domain/Shipping/Shipment.cs index d0841d1dd6..e189e22b12 100644 --- a/src/Libraries/SmartStore.Core/Domain/Shipping/Shipment.cs +++ b/src/Libraries/SmartStore.Core/Domain/Shipping/Shipment.cs @@ -59,6 +59,7 @@ public partial class Shipment : BaseEntity /// /// Gets or sets the shipment items /// + [DataMember] public virtual ICollection ShipmentItems { get { return _shipmentItems ?? (_shipmentItems = new HashSet()); } diff --git a/src/Libraries/SmartStore.Core/Domain/Shipping/ShipmentItem.cs b/src/Libraries/SmartStore.Core/Domain/Shipping/ShipmentItem.cs index 48db08b9e1..fc77fb18b2 100644 --- a/src/Libraries/SmartStore.Core/Domain/Shipping/ShipmentItem.cs +++ b/src/Libraries/SmartStore.Core/Domain/Shipping/ShipmentItem.cs @@ -1,29 +1,35 @@ +using System.Runtime.Serialization; namespace SmartStore.Core.Domain.Shipping { /// /// Represents a shipment order product variant /// + [DataContract] public partial class ShipmentItem : BaseEntity { /// /// Gets or sets the shipment identifier /// + [DataMember] public int ShipmentId { get; set; } /// /// Gets or sets the order item identifier /// + [DataMember] public int OrderItemId { get; set; } /// /// Gets or sets the quantity /// + [DataMember] public int Quantity { get; set; } /// /// Gets the shipment /// + [DataMember] public virtual Shipment Shipment { get; set; } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingMethod.cs b/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingMethod.cs index 86d5390e85..078b12a20c 100644 --- a/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingMethod.cs +++ b/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingMethod.cs @@ -1,15 +1,14 @@ using System.Collections.Generic; using System.Runtime.Serialization; -using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Localization; namespace SmartStore.Core.Domain.Shipping { - /// - /// Represents a shipping method (used for offline shipping rate computation methods) - /// - [DataContract] + /// + /// Represents a shipping method (used for offline shipping rate computation methods) + /// + [DataContract] public partial class ShippingMethod : BaseEntity, ILocalizedEntity { private ICollection _restrictedCountries; @@ -35,34 +34,6 @@ public partial class ShippingMethod : BaseEntity, ILocalizedEntity [DataMember] public bool IgnoreCharges { get; set; } - /// - /// Gets or sets identifiers of customer roles (comma separated) to be excluded in checkout - /// - [DataMember] - public string ExcludedCustomerRoleIds { get; set; } - - /// - /// Gets or sets the context identifier for country exclusion - /// - [DataMember] - public int CountryExclusionContextId { get; set; } - - /// - /// Gets or sets the country exclusion context - /// - [DataMember] - public CountryRestrictionContextType CountryExclusionContext - { - get - { - return (CountryRestrictionContextType)this.CountryExclusionContextId; - } - set - { - this.CountryExclusionContextId = (int)value; - } - } - /// /// Gets or sets the restricted countries /// diff --git a/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingOption.cs b/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingOption.cs index ad29aba1aa..5edb06f415 100644 --- a/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingOption.cs +++ b/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingOption.cs @@ -38,134 +38,4 @@ public partial class ShippingOption /// public string Description { get; set; } } - - - public class ShippingOptionTypeConverter : TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - - return base.CanConvertFrom(context, sourceType); - } - - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - { - if (value is string) - { - ShippingOption shippingOption = null; - string valueStr = value as string; - if (!String.IsNullOrEmpty(valueStr)) - { - try - { - using (var tr = new StringReader(valueStr)) - { - var xmlS = new XmlSerializer(typeof(ShippingOption)); - shippingOption = (ShippingOption)xmlS.Deserialize(tr); - } - } - catch - { - //xml error - } - } - return shippingOption; - } - return base.ConvertFrom(context, culture, value); - } - - public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) - { - if (destinationType == typeof(string)) - { - var shippingOption = value as ShippingOption; - if (shippingOption != null) - { - var sb = new StringBuilder(); - using (var tw = new StringWriter(sb)) - { - var xmlS = new XmlSerializer(typeof(ShippingOption)); - xmlS.Serialize(tw, value); - string serialized = sb.ToString(); - return serialized; - } - } - else - { - return ""; - } - } - - return base.ConvertTo(context, culture, value, destinationType); - } - } - - - public class ShippingOptionListTypeConverter : TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - - return base.CanConvertFrom(context, sourceType); - } - - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - { - if (value is string) - { - List shippingOptions = null; - string valueStr = value as string; - if (!String.IsNullOrEmpty(valueStr)) - { - try - { - using (var tr = new StringReader(valueStr)) - { - var xmlS = new XmlSerializer(typeof(List)); - shippingOptions = (List)xmlS.Deserialize(tr); - } - } - catch - { - //xml error - } - } - return shippingOptions; - } - return base.ConvertFrom(context, culture, value); - } - - public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) - { - if (destinationType == typeof(string)) - { - var shippingOptions = value as List; - if (shippingOptions != null) - { - var sb = new StringBuilder(); - using (var tw = new StringWriter(sb)) - { - var xmlS = new XmlSerializer(typeof(List)); - xmlS.Serialize(tw, value); - string serialized = sb.ToString(); - return serialized; - } - } - else - { - return ""; - } - } - - return base.ConvertTo(context, culture, value, destinationType); - } - } } diff --git a/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingStatus.cs b/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingStatus.cs index 64c1cde1e2..18bf8246d6 100644 --- a/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingStatus.cs +++ b/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingStatus.cs @@ -3,7 +3,7 @@ namespace SmartStore.Core.Domain.Shipping /// /// Represents the shipping status enumeration /// - public enum ShippingStatus : int + public enum ShippingStatus { /// /// Shipping not required diff --git a/src/Libraries/SmartStore.Core/Domain/Stores/StoreExtensions.cs b/src/Libraries/SmartStore.Core/Domain/Stores/StoreExtensions.cs index fe7812464c..3273ab21da 100644 --- a/src/Libraries/SmartStore.Core/Domain/Stores/StoreExtensions.cs +++ b/src/Libraries/SmartStore.Core/Domain/Stores/StoreExtensions.cs @@ -17,16 +17,12 @@ public static string[] ParseHostValues(this Store store) throw new ArgumentNullException("store"); var parsedValues = new List(); - if (!String.IsNullOrEmpty(store.Hosts)) + if (!string.IsNullOrEmpty(store.Hosts)) { - string[] hosts = store.Hosts.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - foreach (string host in hosts) - { - var tmp = host.Trim(); - if (!String.IsNullOrEmpty(tmp)) - parsedValues.Add(tmp); - } + var hosts = store.Hosts.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + parsedValues.AddRange(hosts.Select(host => host.Trim()).Where(tmp => !string.IsNullOrEmpty(tmp))); } + return parsedValues.ToArray(); } diff --git a/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs b/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs index 7a342abf17..5a69937cac 100644 --- a/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Tax/TaxSettings.cs @@ -137,5 +137,10 @@ public TaxSettings() /// Gets or sets a value indicating whether we should notify a store owner when a new VAT number is submitted /// public bool EuVatEmailAdminWhenNewVatSubmitted { get; set; } + + /// + /// Gets or sets a value indicating whether a VAT-ID is required + /// + public bool VatRequired { get; set; } } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Email/EmailMessage.cs b/src/Libraries/SmartStore.Core/Email/EmailMessage.cs index 426f83e100..683071e498 100644 --- a/src/Libraries/SmartStore.Core/Email/EmailMessage.cs +++ b/src/Libraries/SmartStore.Core/Email/EmailMessage.cs @@ -2,15 +2,9 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Net.Mail; -using System.Net.Mime; -using System.Xml.Serialization; -using System.Linq; using System.IO; using System.Text; -using System.Xml; -using System.Web; using System.Net; -using System.Threading.Tasks; namespace SmartStore.Core.Email { @@ -80,11 +74,11 @@ public EmailMessage(EmailAddress to, string subject, string body, EmailAddress f public async void BodyFromFile(string filePathOrUrl) { - StreamReader sr = null; + StreamReader sr; if (filePathOrUrl.ToLower().StartsWith("http")) { - WebClient wc = new WebClient(); + var wc = new WebClient(); sr = new StreamReader(await wc.OpenReadTaskAsync(filePathOrUrl)); } else diff --git a/src/Libraries/SmartStore.Core/Email/SmtpContext.cs b/src/Libraries/SmartStore.Core/Email/SmtpContext.cs index 796e642109..054487610f 100644 --- a/src/Libraries/SmartStore.Core/Email/SmtpContext.cs +++ b/src/Libraries/SmartStore.Core/Email/SmtpContext.cs @@ -73,6 +73,8 @@ public SmtpClient ToSmtpClient() smtpClient.UseDefaultCredentials = this.UseDefaultCredentials; smtpClient.EnableSsl = this.EnableSsl; + smtpClient.Timeout = 10000; + if (this.UseDefaultCredentials) { smtpClient.Credentials = CredentialCache.DefaultNetworkCredentials; diff --git a/src/Libraries/SmartStore.Core/Extensions/CollectionExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/CollectionExtensions.cs index a4a602abc5..b6fec3780a 100644 --- a/src/Libraries/SmartStore.Core/Extensions/CollectionExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/CollectionExtensions.cs @@ -7,10 +7,8 @@ namespace SmartStore { - public static class CollectionExtensions { - public static void AddRange(this ICollection initial, IEnumerable other) { if (other == null) @@ -31,31 +29,5 @@ public static bool IsNullOrEmpty(this ICollection source) { return (source == null || source.Count == 0); } - - //public static bool HasItems(this IEnumerable source) - //{ - // return source != null && source.GetEnumerator().MoveNext(); - //} - - public static bool EqualsAll(this IList a, IList b) - { - if (a == null || b == null) - return (a == null && b == null); - - if (a.Count != b.Count) - return false; - - EqualityComparer comparer = EqualityComparer.Default; - - for (int i = 0; i < a.Count; i++) - { - if (!comparer.Equals(a[i], b[i])) - return false; - } - - return true; - } - } - } diff --git a/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs index d7d8629f56..a97d4862bf 100644 --- a/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs @@ -1,207 +1,83 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.IO; -using System.ComponentModel; +using System.Diagnostics; using System.Globalization; -using System.Collections; -using System.Reflection; +using System.IO; using System.Runtime.Serialization.Formatters.Binary; -using SmartStore.Utilities; -using System.Diagnostics; using System.Security.Cryptography; -using SmartStore.Core.ComponentModel; -using SmartStore.Core.Domain.Shipping; -using SmartStore.Core.Domain.Catalog; +using System.Text; +using System.Text.RegularExpressions; +using SmartStore.ComponentModel; +using SmartStore.Utilities; namespace SmartStore { - - public static class ConversionExtensions + public static class ConversionExtensions { - private readonly static IDictionary s_customTypeConverters; - - static ConversionExtensions() - { - var intConverter = new GenericListTypeConverter(); - var decConverter = new GenericListTypeConverter(); - var stringConverter = new GenericListTypeConverter(); - var soListConverter = new ShippingOptionListTypeConverter(); - var bundleDataListConverter = new ProductBundleDataListTypeConverter(); - - s_customTypeConverters = new Dictionary(); - s_customTypeConverters.Add(typeof(List), intConverter); - s_customTypeConverters.Add(typeof(IList), intConverter); - s_customTypeConverters.Add(typeof(List), decConverter); - s_customTypeConverters.Add(typeof(IList), decConverter); - s_customTypeConverters.Add(typeof(List), stringConverter); - s_customTypeConverters.Add(typeof(IList), stringConverter); - s_customTypeConverters.Add(typeof(ShippingOption), new ShippingOptionTypeConverter()); - s_customTypeConverters.Add(typeof(List), soListConverter); - s_customTypeConverters.Add(typeof(IList), soListConverter); - s_customTypeConverters.Add(typeof(List), bundleDataListConverter); - s_customTypeConverters.Add(typeof(IList), bundleDataListConverter); - } - #region Object public static T Convert(this object value) { - return (T)Convert(value, typeof(T)); - } - - public static T Convert(this object value, CultureInfo culture) - { - return (T)Convert(value, typeof(T), culture); - } + return (T)(Convert(value, typeof(T)) ?? default(T)); + } - public static object Convert(this object value, Type to) - { - return value.Convert(to, CultureInfo.InvariantCulture); - } + public static T Convert(this object value, T defaultValue) + { + return (T)(Convert(value, typeof(T)) ?? defaultValue); + } - public static object Convert(this object value, Type to, CultureInfo culture) + public static T Convert(this object value, CultureInfo culture) { - Guard.ArgumentNotNull(to, "to"); - - if (value == null || to.IsInstanceOfType(value)) - { - return value; - } - - // array conversion results in four cases, as below - Array valueAsArray = value as Array; - if (to.IsArray) - { - Type destinationElementType = to.GetElementType(); - if (valueAsArray != null) - { - // case 1: both destination + source type are arrays, so convert each element - IList valueAsList = (IList)valueAsArray; - IList converted = Array.CreateInstance(destinationElementType, valueAsList.Count); - for (int i = 0; i < valueAsList.Count; i++) - { - converted[i] = valueAsList[i].Convert(destinationElementType, culture); - } - return converted; - } - else - { - // case 2: destination type is array but source is single element, so wrap element in array + convert - object element = value.Convert(destinationElementType, culture); - IList converted = Array.CreateInstance(destinationElementType, 1); - converted[0] = element; - return converted; - } - } - else if (valueAsArray != null) - { - // case 3: destination type is single element but source is array, so extract first element + convert - IList valueAsList = (IList)valueAsArray; - if (valueAsList.Count > 0) - { - value = valueAsList[0]; - } - // .. fallthrough to case 4 - } - // case 4: both destination + source type are single elements, so convert - - Type fromType = value.GetType(); - - //if (to.IsInterface || to.IsGenericTypeDefinition || to.IsAbstract) - // throw Error.Argument("to", "Target type '{0}' is not a value type or a non-abstract class.", to.FullName); - - // use Convert.ChangeType if both types are IConvertible - if (value is IConvertible && typeof(IConvertible).IsAssignableFrom(to)) - { - if (to.IsEnum) - { - if (value is string) - return Enum.Parse(to, value.ToString(), true); - else if (fromType.IsInteger()) - return Enum.ToObject(to, value); - } + return (T)(Convert(value, typeof(T), culture) ?? default(T)); + } - return System.Convert.ChangeType(value, to, culture); - } + public static T Convert(this object value, T defaultValue, CultureInfo culture) + { + return (T)(Convert(value, typeof(T), culture) ?? defaultValue); + } - if (value is DateTime && to == typeof(DateTimeOffset)) - return new DateTimeOffset((DateTime)value); + public static object Convert(this object value, Type to) + { + return value.Convert(to, CultureInfo.InvariantCulture); + } - if (value is string && to == typeof(Guid)) - return new Guid((string)value); + public static object Convert(this object value, Type to, CultureInfo culture) + { + Guard.ArgumentNotNull(to, "to"); - // see if source or target types have a TypeConverter that converts between the two - TypeConverter toConverter = GetTypeConverter(fromType); + if (value == null || value == DBNull.Value || to.IsInstanceOfType(value)) + { + return value == DBNull.Value ? null : value; + } - Type nonNullableTo = to.GetNonNullableType(); - bool isNullableTo = to != nonNullableTo; + Type from = value.GetType(); - if (toConverter != null && toConverter.CanConvertTo(nonNullableTo)) - { - object result = toConverter.ConvertTo(null, culture, value, nonNullableTo); - return isNullableTo ? Activator.CreateInstance(typeof(Nullable<>).MakeGenericType(nonNullableTo), result) : result; + if (culture == null) + { + culture = CultureInfo.InvariantCulture; } - TypeConverter fromConverter = GetTypeConverter(nonNullableTo); - - if (fromConverter != null && fromConverter.CanConvertFrom(fromType)) - { - object result = fromConverter.ConvertFrom(null, culture, value); - return isNullableTo ? Activator.CreateInstance(typeof(Nullable<>).MakeGenericType(nonNullableTo), result) : result; - } - - // TypeConverter doesn't like Double to Decimal - if (fromType == typeof(double) && nonNullableTo == typeof(decimal)) + // get a converter for 'to' (value -> to) + var converter = TypeConverterFactory.GetConverter(to); + if (converter != null && converter.CanConvertFrom(from)) { - decimal result = new Decimal((double)value); - return isNullableTo ? Activator.CreateInstance(typeof(Nullable<>).MakeGenericType(nonNullableTo), result) : result; + return converter.ConvertFrom(culture, value); } - throw Error.InvalidCast(fromType, to); - - #region OBSOLETE - // TypeConverter converter = TypeDescriptor.GetConverter(to); - // bool canConvertFrom = converter.CanConvertFrom(value.GetType()); - // if (!canConvertFrom) - // { - // converter = TypeDescriptor.GetConverter(value.GetType()); - // } - // if (!(canConvertFrom || converter.CanConvertTo(to))) - // { - // throw Error.InvalidOperation(@"The parameter conversion from type '{0}' to type '{1}' failed - // because no TypeConverter can convert between these types.", - // value.GetType().FullName, - // to.FullName); - // } - - // try - // { - // CultureInfo cultureToUse = culture ?? CultureInfo.CurrentCulture; - // object convertedValue = (canConvertFrom) ? - // converter.ConvertFrom(null /* context */, cultureToUse, value) : - // converter.ConvertTo(null /* context */, cultureToUse, value, to); - // return convertedValue; - // } - // catch (Exception ex) - // { - // throw Error.InvalidOperation(@"The parameter conversion from type '{0}' to type '{1}' failed. - // See the inner exception for more information.", ex, - // value.GetType().FullName, - // to.FullName); - // } - #endregion - } + // try the other way round with a 'from' converter (to <- from) + converter = TypeConverterFactory.GetConverter(from); + if (converter != null && converter.CanConvertTo(to)) + { + return converter.ConvertTo(culture, null, value, to); + } - internal static TypeConverter GetTypeConverter(Type type) - { - TypeConverter converter; - if (s_customTypeConverters.TryGetValue(type, out converter)) + // use Convert.ChangeType if both types are IConvertible + if (value is IConvertible && typeof(IConvertible).IsAssignableFrom(to)) { - return converter; + return System.Convert.ChangeType(value, to, culture); } - return TypeDescriptor.GetConverter(type); + + throw Error.InvalidCast(from, to); } #endregion @@ -217,187 +93,64 @@ public static char ToHex(this int value) return (char)((value - 10) + 97); } - /// - /// Returns kilobytes - /// - /// - /// - public static int ToKb(this int value) - { - return value * 1024; - } - - /// - /// Returns megabytes - /// - /// - /// - public static int ToMb(this int value) - { - return value * 1024 * 1024; - } - - /// Returns a that represents a specified number of minutes. - /// number of minutes - /// A that represents a value. - /// 3.Minutes() - public static TimeSpan ToMinutes(this int minutes) - { - return TimeSpan.FromMinutes(minutes); - } - - /// - /// Returns a that represents a specified number of seconds. - /// - /// number of seconds - /// A that represents a value. - /// 2.Seconds() - public static TimeSpan ToSeconds(this int seconds) - { - return TimeSpan.FromSeconds(seconds); - } - - /// - /// Returns a that represents a specified number of milliseconds. - /// - /// milliseconds for this timespan - /// A that represents a value. - public static TimeSpan ToMilliseconds(this int milliseconds) - { - return TimeSpan.FromMilliseconds(milliseconds); - } - - /// - /// Returns a that represents a specified number of days. - /// - /// Number of days. - /// A that represents a value. - public static TimeSpan ToDays(this int days) - { - return TimeSpan.FromDays(days); - } - - - /// - /// Returns a that represents a specified number of hours. - /// - /// Number of hours. - /// A that represents a value. - public static TimeSpan ToHours(this int hours) - { - return TimeSpan.FromHours(hours); - } - - #endregion - - #region double - - /// Returns a that represents a specified number of minutes. - /// number of minutes - /// A that represents a value. - /// 3D.Minutes() - public static TimeSpan ToMinutes(this double minutes) - { - return TimeSpan.FromMinutes(minutes); - } - - - /// Returns a that represents a specified number of hours. - /// number of hours - /// A that represents a value. - /// 3D.Hours() - public static TimeSpan ToHours(this double hours) - { - return TimeSpan.FromHours(hours); - } - - /// Returns a that represents a specified number of seconds. - /// number of seconds - /// A that represents a value. - /// 2D.Seconds() - public static TimeSpan ToSeconds(this double seconds) - { - return TimeSpan.FromSeconds(seconds); - } - - /// Returns a that represents a specified number of milliseconds. - /// milliseconds for this timespan - /// A that represents a value. - public static TimeSpan ToMilliseconds(this double milliseconds) - { - return TimeSpan.FromMilliseconds(milliseconds); - } - - /// - /// Returns a that represents a specified number of days. - /// - /// Number of days, accurate to the milliseconds. - /// A that represents a value. - public static TimeSpan ToDays(this double days) - { - return TimeSpan.FromDays(days); - } - #endregion #region String public static T ToEnum(this string value, T defaultValue) where T : IComparable, IFormattable { - T convertedValue = defaultValue; - - if (!string.IsNullOrEmpty(value)) - { - try - { - convertedValue = (T)Enum.Parse(typeof(T), value.Trim(), true); - } - catch (ArgumentException) - { - } - } - - return convertedValue; - } + Guard.ArgumentIsEnumType(typeof(T), "T"); - public static string[] ToArray(this string value) - { - return value.ToArray(new char[] { ',' }); - } + T result; + if (CommonHelper.TryConvert(value, out result)) + { + return result; + } - public static string[] ToArray(this string value, params char[] separator) - { - return value.Trim().Split(separator, StringSplitOptions.RemoveEmptyEntries); + return defaultValue; } public static int ToInt(this string value, int defaultValue = 0) { int result; - if (int.TryParse(value, out result)) - { - return result; - } - return defaultValue; + if (CommonHelper.TryConvert(value, out result)) + { + return result; + } + + return defaultValue; } - public static float ToFloat(this string value, float defaultValue = 0) + public static char ToChar(this string value, bool unescape = false, char defaultValue = '\0') + { + char result; + if (value.HasValue() && char.TryParse(unescape ? Regex.Unescape(value) : value, out result)) + { + return result; + } + return defaultValue; + } + + public static float ToFloat(this string value, float defaultValue = 0) { float result; - if (float.TryParse(value, out result)) - { - return result; - } - return defaultValue; + if (CommonHelper.TryConvert(value, out result)) + { + return result; + } + + return defaultValue; } public static bool ToBool(this string value, bool defaultValue = false) { bool result; - if (bool.TryParse(value, out result)) - { - return result; - } - return defaultValue; + if (CommonHelper.TryConvert(value, out result)) + { + return result; + } + + return defaultValue; } public static DateTime? ToDateTime(this string value, DateTime? defaultValue) @@ -450,25 +203,6 @@ public static bool ToBool(this string value, bool defaultValue = false) return null; } - public static Guid ToGuid(this string value) - { - if ((!String.IsNullOrEmpty(value)) && (value.Trim().Length == 22)) - { - string encoded = string.Concat(value.Trim().Replace("-", "+").Replace("_", "/"), "=="); - - byte[] base64 = System.Convert.FromBase64String(encoded); - - return new Guid(base64); - } - - return Guid.Empty; - } - - public static byte[] ToByteArray(this string value) - { - return Encoding.Default.GetBytes(value); - } - [DebuggerStepThrough] public static Version ToVersion(this string value, Version defaultVersion = null) { @@ -484,12 +218,6 @@ public static Version ToVersion(this string value, Version defaultVersion = null #endregion - #region DateTime - - // [...] - - #endregion - #region Stream public static byte[] ToByteArray(this Stream stream) @@ -521,7 +249,11 @@ public static string AsString(this Stream stream, Encoding encoding) // convert stream to string string result; - stream.Position = 0; + + if (stream.CanSeek) + { + stream.Position = 0; + } using (StreamReader sr = new StreamReader(stream, encoding)) { @@ -556,19 +288,13 @@ public static Stream ToStream(this byte[] bytes) return new MemoryStream(bytes); } - public static string AsString(this byte[] bytes) - { - return Encoding.Default.GetString(bytes); - } - - - /// - /// Computes the MD5 hash of a byte array - /// - /// The byte array to compute the hash for - /// The hash value - //[DebuggerStepThrough] - public static string Hash(this byte[] value, bool toBase64 = false) + /// + /// Computes the MD5 hash of a byte array + /// + /// The byte array to compute the hash for + /// The hash value + [DebuggerStepThrough] + public static string Hash(this byte[] value, bool toBase64 = false) { Guard.ArgumentNotNull(value, "value"); diff --git a/src/Libraries/SmartStore.Core/Extensions/DateTimeExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/DateTimeExtensions.cs index 0ae64d902a..3070172cdc 100644 --- a/src/Libraries/SmartStore.Core/Extensions/DateTimeExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/DateTimeExtensions.cs @@ -5,24 +5,10 @@ namespace SmartStore { - public static class DateTimeExtensions { - private static readonly DateTime MinDate = new DateTime(1900, 1, 1); - private static readonly DateTime MaxDate = new DateTime(9999, 12, 31, 23, 59, 59, 999); public static readonly DateTime BeginOfEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - public static bool IsValid(this DateTime value) - { - return (value >= MinDate) && (value <= MaxDate); - } - - public static string GetLocalOffset(this DateTime value) - { - TimeSpan utcOffset = TimeZoneInfo.Local.GetUtcOffset(value); - return utcOffset.Hours.ToString("+00;-00", CultureInfo.InvariantCulture) + ":" + utcOffset.Minutes.ToString("00;00", CultureInfo.InvariantCulture); - } - /// /// Converts a nullable date/time value to UTC. /// @@ -33,38 +19,6 @@ public static string GetLocalOffset(this DateTime value) return dateTime.HasValue ? dateTime.Value.ToUniversalTime() : (DateTime?)null; } - /// - /// Returns a copy of a date/time value with its kind - /// set to but does not perform - /// any time-zone adjustment. - /// - /// - /// This method is useful when obtaining date/time values from sources - /// that might not correctly set the UTC flag. - /// - /// The date/time - /// The same date/time with the UTC flag set - public static DateTime AssumeUniversalTime(this DateTime dateTime) - { - return new DateTime(dateTime.Ticks, DateTimeKind.Utc); - } - - /// - /// Returns a copy of a nullable date/time value with its kind - /// set to but does not perform - /// any time-zone adjustment. - /// - /// - /// This method is useful when obtaining date/time values from sources - /// that might not correctly set the UTC flag. - /// - /// The nullable date/time - /// The same nullable date/time with the UTC flag set - public static DateTime? AssumeUniversalTime(this DateTime? dateTime) - { - return dateTime.HasValue ? AssumeUniversalTime(dateTime.Value) : (DateTime?)null; - } - /// /// Converts a nullable UTC date/time value to local time. /// @@ -143,260 +97,6 @@ public static DateTime GetEvenMinuteDateBefore(this DateTime? dateTime) return new DateTime(d.Year, d.Month, d.Day, d.Hour, d.Minute, 0); } - /// - /// Returns a date that is rounded to the next even second above the given - /// date. - /// - /// the Date to round, if the current time will - /// be used - /// the new rounded date - public static DateTime GetEvenSecondDate(this DateTime? dateTime) - { - if (!dateTime.HasValue) - { - dateTime = DateTime.UtcNow; - } - DateTime d = dateTime.Value; - d = d.AddSeconds(1); - return new DateTime(d.Year, d.Month, d.Day, d.Hour, d.Minute, d.Second); - } - - /// - /// Returns a date that is rounded to the previous even second below the - /// given date. - ///

- /// For example an input date with a time of 08:13:54.341 would result in a - /// date with the time of 08:13:00.000. - ///

- ///
- /// the Date to round, if the current time will - /// be used - /// the new rounded date - public static DateTime GetEvenSecondDateBefore(this DateTime? dateTime) - { - if (!dateTime.HasValue) - { - dateTime = DateTime.UtcNow; - } - DateTime d = dateTime.Value; - return new DateTime(d.Year, d.Month, d.Day, d.Hour, d.Minute, d.Second); - } - - /// - /// Returns a date that is rounded to the next even multiple of the given - /// minute. - ///

- /// For example an input date with a time of 08:13:54, and an input - /// minute-base of 5 would result in a date with the time of 08:15:00. The - /// same input date with an input minute-base of 10 would result in a date - /// with the time of 08:20:00. But a date with the time 08:53:31 and an - /// input minute-base of 45 would result in 09:00:00, because the even-hour - /// is the next 'base' for 45-minute intervals. - ///

- /// - ///

- /// More examples: - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - ///
Input TimeMinute-BaseResult Time
11:16:412011:20:00
11:36:412011:40:00
11:46:412012:00:00
11:26:413011:30:00
11:36:413012:00:00
11:16:411711:17:00
11:17:411711:34:00
11:52:411712:00:00
11:52:41511:55:00
11:57:41512:00:00
11:17:41012:00:00
11:17:41111:08:00
- ///

- /// - ///
- /// the Date to round, if the current time willbe used - /// the base-minute to set the time on - /// The new rounded date - public static DateTime GetNextGivenMinuteDate(this DateTime? dateTime, int minuteBase) - { - if (minuteBase < 0 || minuteBase > 59) - { - throw new ArgumentException("minuteBase must be >=0 and <= 59"); - } - - if (!dateTime.HasValue) - { - dateTime = DateTime.UtcNow; - } - DateTime d = dateTime.Value; - - if (minuteBase == 0) - { - d = d.AddHours(1); - return new DateTime(d.Year, d.Month, d.Day, d.Hour, 0, 0); - } - - int minute = d.Minute; - int arItr = minute / minuteBase; - int nextMinuteOccurance = minuteBase * (arItr + 1); - - if (nextMinuteOccurance < 60) - { - return new DateTime(d.Year, d.Month, d.Day, d.Hour, nextMinuteOccurance, 0); - } - else - { - d = d.AddHours(1); - return new DateTime(d.Year, d.Month, d.Day, d.Hour, 0, 0); - } - } - - /// - /// Returns a date that is rounded to the next even multiple of the given - /// minute. - ///

- /// The rules for calculating the second are the same as those for - /// calculating the minute in the method - /// . - ///

- ///
- /// The date. - /// The second base. - /// - public static DateTime GetNextGivenSecondDate(this DateTime? dateTime, int secondBase) - { - if (secondBase < 0 || secondBase > 59) - { - throw new ArgumentException("secondBase must be >=0 and <= 59"); - } - - if (!dateTime.HasValue) - { - dateTime = DateTime.UtcNow; - } - - DateTime d = dateTime.Value; - - if (secondBase == 0) - { - d = d.AddMinutes(1); - return new DateTime(d.Year, d.Month, d.Day, d.Hour, d.Minute, 0); - } - - int second = d.Second; - int arItr = second / secondBase; - int nextSecondOccurance = secondBase * (arItr + 1); - - if (nextSecondOccurance < 60) - { - return new DateTime(d.Year, d.Month, d.Day, d.Hour, d.Minute, nextSecondOccurance); - } - else - { - d = d.AddMinutes(1); - return new DateTime(d.Year, d.Month, d.Day, d.Hour, d.Minute, 0); - } - } - - - /// - /// Translate a date and time from a users timezone to the another - /// (probably server) timezone to assist in creating a simple trigger with - /// the right date and time. - /// - /// the date to translate - /// the original time-zone - /// the destination time-zone - /// the translated UTC date - public static DateTime TranslateTime(this DateTime date, TimeZone src, TimeZone dest) - { - DateTime newDate = DateTime.UtcNow; - double offset = (GetOffset(date, dest) - GetOffset(date, src)); - - newDate = newDate.AddMilliseconds(-1 * offset); - - return newDate; - } - - /// - /// Gets the offset from UT for the given date in the given timezone, - /// taking into account daylight savings. - /// - /// the date that is the base for the offset - /// the time-zone to calculate to offset to - /// the offset - public static double GetOffset(this DateTime date, TimeZone tz) - { - if (tz.IsDaylightSavingTime(date)) - { - // TODO - return tz.BaseUtcOffset.TotalMilliseconds + 0; - } - - return tz.BaseUtcOffset.TotalMilliseconds; - } - - /// - /// This functions determines if the TimeZone uses daylight saving time - /// - /// TimeZone instance to validate - /// True or false depending if daylight savings time is used - public static bool UseDaylightTime(this TimeZone timezone) - { - return timezone.SupportsDaylightSavingTime; - } - public static long ToJavaScriptTicks(this DateTime dateTime) { DateTimeOffset utcDateTime = dateTime.ToUniversalTime(); @@ -404,28 +104,6 @@ public static long ToJavaScriptTicks(this DateTime dateTime) return javaScriptTicks; } - //public static long ToJavaScriptTicks(this DateTimeOffset offset) - //{ - // DateTimeOffset utcDateTime = offset.ToUniversalTime(); - // long javaScriptTicks = (utcDateTime.Ticks - InitialJavaScriptDateTicks) / (long)10000; - // return javaScriptTicks; - //} - - public static XmlDateTimeSerializationMode ToSerializationMode(DateTimeKind kind) - { - switch (kind) - { - case DateTimeKind.Local: - return XmlDateTimeSerializationMode.Local; - case DateTimeKind.Unspecified: - return XmlDateTimeSerializationMode.Unspecified; - case DateTimeKind.Utc: - return XmlDateTimeSerializationMode.Utc; - default: - throw new ArgumentOutOfRangeException("kind", kind, "Unexpected DateTimeKind value."); - } - } - /// /// Get the first day of the month for /// any full date submitted diff --git a/src/Libraries/SmartStore.Core/Extensions/DecimalExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/DecimalExtensions.cs index 9d2540f9f5..aa6dd811e3 100644 --- a/src/Libraries/SmartStore.Core/Extensions/DecimalExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/DecimalExtensions.cs @@ -17,7 +17,7 @@ public static string FormatInvariant(this decimal value, int decimals = 2) } /// - /// Calculates the tax (percental) from a gross and a net value. + /// Calculates the tax (percentage) from a gross and a net value. /// /// Gross value /// Net value diff --git a/src/Libraries/SmartStore.Core/Extensions/DictionaryExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/DictionaryExtensions.cs index 6b91605920..6e4c8976e8 100644 --- a/src/Libraries/SmartStore.Core/Extensions/DictionaryExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/DictionaryExtensions.cs @@ -5,14 +5,13 @@ using System.Web.Routing; using System.Globalization; using System.Dynamic; +using SmartStore.Utilities; namespace SmartStore { - public static class DictionaryExtensions { - - public static void AddRange(this IDictionary values, IEnumerable> other) + public static void AddRange(this IDictionary values, IEnumerable> other) { foreach (var kvp in other) { @@ -34,16 +33,16 @@ public static void Merge(this IDictionary instance, string key, public static void Merge(this IDictionary instance, object values, bool replaceExisting = true) { - instance.Merge(new RouteValueDictionary(values), replaceExisting); + instance.Merge(CommonHelper.ObjectToDictionary(values), replaceExisting); } - public static void Merge(this IDictionary instance, IDictionary from, bool replaceExisting = true) + public static void Merge(this IDictionary instance, IDictionary from, bool replaceExisting = true) { - foreach (KeyValuePair keyValuePair in from) + foreach (var kvp in from) { - if (replaceExisting || !instance.ContainsKey(keyValuePair.Key)) + if (replaceExisting || !instance.ContainsKey(kvp.Key)) { - instance[keyValuePair.Key] = keyValuePair.Value; + instance[kvp.Key] = kvp.Value; } } } @@ -58,30 +57,22 @@ public static void PrependInValue(this IDictionary instance, str instance[key] = !instance.ContainsKey(key) ? value.ToString() : (value + separator + instance[key]); } - public static string ToAttributeString(this IDictionary instance) - { - StringBuilder builder = new StringBuilder(); - foreach (KeyValuePair pair in instance) - { - object[] args = new object[] { HttpUtility.HtmlAttributeEncode(pair.Key), HttpUtility.HtmlAttributeEncode(pair.Value.ToString()) }; - builder.Append(" {0}=\"{1}\"".FormatWith(args)); - } - return builder.ToString(); - } - - public static T GetValue(this IDictionary instance, K key) + public static TValue GetValue(this IDictionary instance, TKey key) { try { object val; if (instance != null && instance.TryGetValue(key, out val) && val != null) - return (T)Convert.ChangeType(val, typeof(T), CultureInfo.InvariantCulture); + { + return (TValue)Convert.ChangeType(val, typeof(TValue), CultureInfo.InvariantCulture); + } } - catch (Exception exc) + catch (Exception ex) { - exc.Dump(); + ex.Dump(); } - return default(T); + + return default(TValue); } public static ExpandoObject ToExpandoObject(this IDictionary source, bool castIfPossible = false) diff --git a/src/Libraries/SmartStore.Core/Extensions/EnumExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/EnumExtensions.cs deleted file mode 100644 index 6097509595..0000000000 --- a/src/Libraries/SmartStore.Core/Extensions/EnumExtensions.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; - -namespace SmartStore -{ - - public static class EnumExtensions - { - - /// - /// Gets the of an - /// type value. - /// - /// The type value. - /// A string containing the text of the - /// . - public static string GetFriendlyName(this Enum value) - { - Guard.ArgumentNotNull(value, "value"); - - string friendlyName = value.ToString(); - FieldInfo fieldInfo = value.GetType().GetField(friendlyName); - - var attributes = (EnumFriendlyNameAttribute[])fieldInfo.GetCustomAttributes(typeof(EnumFriendlyNameAttribute), false); - - if (attributes != null && attributes.Length == 1) - { - friendlyName = attributes[0].FriendlyName; - } - return friendlyName; - } - - /// - /// Gets the of an - /// type value. - /// - /// The type value. - /// A string containing the text of the - /// . - public static string GetDescription(this Enum value) - { - Guard.ArgumentNotNull(value, "value"); - - string description = value.ToString(); - FieldInfo fieldInfo = value.GetType().GetField(description); - - EnumDescriptionAttribute[] attributes = - (EnumDescriptionAttribute[]) - fieldInfo.GetCustomAttributes(typeof(EnumDescriptionAttribute), false); - - if (attributes != null && attributes.Length == 1) - { - description = attributes[0].Description; - } - return description; - } - - /// - /// Checks if the specified enum flag is set on a flagged enumeration type. - /// - /// - /// - /// - /// - public static bool IsSet(this T value, T flags) where T : struct - { - Type type = typeof(T); - - // only works with enums - if (!type.IsEnum) - throw Error.Argument("T", "The type parameter T must be an enum type."); - - // handle each underlying type - Type numberType = Enum.GetUnderlyingType(type); - - if (numberType.Equals(typeof(int))) - { - return BoxUnbox(value, flags, (a, b) => (a & b) == b); - } - else if (numberType.Equals(typeof(sbyte))) - { - return BoxUnbox(value, flags, (a, b) => (a & b) == b); - } - else if (numberType.Equals(typeof(byte))) - { - return BoxUnbox(value, flags, (a, b) => (a & b) == b); - } - else if (numberType.Equals(typeof(short))) - { - return BoxUnbox(value, flags, (a, b) => (a & b) == b); - } - else if (numberType.Equals(typeof(ushort))) - { - return BoxUnbox(value, flags, (a, b) => (a & b) == b); - } - else if (numberType.Equals(typeof(uint))) - { - return BoxUnbox(value, flags, (a, b) => (a & b) == b); - } - else if (numberType.Equals(typeof(long))) - { - return BoxUnbox(value, flags, (a, b) => (a & b) == b); - } - else if (numberType.Equals(typeof(ulong))) - { - return BoxUnbox(value, flags, (a, b) => (a & b) == b); - } - else if (numberType.Equals(typeof(char))) - { - return BoxUnbox(value, flags, (a, b) => (a & b) == b); - } - else - { - throw new ArgumentException("Unknown enum underlying type " + - numberType.Name + "."); - } - } - - /// - /// Helper function for handling the value types. Boxes the params to - /// object so that the cast can be called on them. - /// - private static bool BoxUnbox(object value, object flags, Func op) - { - return op((T)value, (T)flags); - } - - } - -} diff --git a/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs index ad2d91cd13..948cfddc6a 100644 --- a/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; @@ -7,28 +6,30 @@ using System.Text; using System.Web; using SmartStore.Collections; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace SmartStore { - - public static class EnumerableExtensions + [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] + public static class EnumerableExtensions { - #region Nested classes private static class DefaultReadOnlyCollection { private static ReadOnlyCollection defaultCollection; + [SuppressMessage("ReSharper", "ConvertIfStatementToNullCoalescingExpression")] internal static ReadOnlyCollection Empty { get { - if (EnumerableExtensions.DefaultReadOnlyCollection.defaultCollection == null) + if (defaultCollection == null) { - EnumerableExtensions.DefaultReadOnlyCollection.defaultCollection = new ReadOnlyCollection(new T[0]); + defaultCollection = new ReadOnlyCollection(new T[0]); } - return EnumerableExtensions.DefaultReadOnlyCollection.defaultCollection; + return defaultCollection; } } } @@ -75,15 +76,15 @@ public static IEnumerable> Chunk(this IEnumerable items, in } - /// - /// Performs an action on each item while iterating through a list. - /// This is a handy shortcut for foreach(item in list) { ... } - /// - /// The type of the items. - /// The list, which holds the objects. - /// The action delegate which is called on each item while iterating. - //[DebuggerStepThrough] - public static void Each(this IEnumerable source, Action action) + /// + /// Performs an action on each item while iterating through a list. + /// This is a handy shortcut for foreach(item in list) { ... } + /// + /// The type of the items. + /// The list, which holds the objects. + /// The action delegate which is called on each item while iterating. + [DebuggerStepThrough] + public static void Each(this IEnumerable source, Action action) { foreach (T t in source) { @@ -98,7 +99,7 @@ public static void Each(this IEnumerable source, Action action) /// The type of the items. /// The list, which holds the objects. /// The action delegate which is called on each item while iterating. - //[DebuggerStepThrough] + [DebuggerStepThrough] public static void Each(this IEnumerable source, Action action) { int i = 0; @@ -108,11 +109,6 @@ public static void Each(this IEnumerable source, Action action) } } - public static IEnumerable CastValid(this IEnumerable source) - { - return source.Cast().Where(o => o is T).Cast(); - } - public static ReadOnlyCollection AsReadOnly(this IEnumerable source) { if (source == null || !source.Any()) @@ -133,20 +129,76 @@ public static ReadOnlyCollection AsReadOnly(this IEnumerable source) return new ReadOnlyCollection(source.ToArray()); } - public static IEnumerable OrderByOrdinal(this IEnumerable source) - where T : IOrdered - { - return source.OrderByOrdinal(false); - } + /// + /// Converts an enumerable to a dictionary while tolerating duplicate entries (last wins) + /// + /// source + /// keySelector + /// Result as dictionary + public static Dictionary ToDictionarySafe( + this IEnumerable source, + Func keySelector) + { + return source.ToDictionarySafe(keySelector, new Func(src => src), null); + } - public static IEnumerable OrderByOrdinal(this IEnumerable source, bool descending) - where T : IOrdered - { - if (!descending) - return source.OrderBy(x => x.Ordinal); - else - return source.OrderByDescending(x => x.Ordinal); - } + /// + /// Converts an enumerable to a dictionary while tolerating duplicate entries (last wins) + /// + /// source + /// keySelector + /// comparer + /// Result as dictionary + public static Dictionary ToDictionarySafe( + this IEnumerable source, + Func keySelector, + IEqualityComparer comparer) + { + return source.ToDictionarySafe(keySelector, new Func(src => src), comparer); + } + + /// + /// Converts an enumerable to a dictionary while tolerating duplicate entries (last wins) + /// + /// source + /// keySelector + /// elementSelector + /// Result as dictionary + public static Dictionary ToDictionarySafe( + this IEnumerable source, + Func keySelector, + Func elementSelector) + { + return source.ToDictionarySafe(keySelector, elementSelector, null); + } + + /// + /// Converts an enumerable to a dictionary while tolerating duplicate entries (last wins) + /// + /// source + /// keySelector + /// elementSelector + /// comparer + /// Result as dictionary + public static Dictionary ToDictionarySafe( + this IEnumerable source, + Func keySelector, + Func elementSelector, + IEqualityComparer comparer) + { + Guard.ArgumentNotNull(() => source); + Guard.ArgumentNotNull(() => keySelector); + Guard.ArgumentNotNull(() => elementSelector); + + var dictionary = new Dictionary(comparer); + + foreach (var local in source) + { + dictionary[keySelector(local)] = elementSelector(local); + } + + return dictionary; + } #endregion @@ -178,6 +230,7 @@ public static Multimap ToMultimap( public static void AddRange(this NameValueCollection initial, NameValueCollection other) { Guard.ArgumentNotNull(initial, "initial"); + if (other == null) return; @@ -190,7 +243,7 @@ public static void AddRange(this NameValueCollection initial, NameValueCollectio /// /// Builds an URL query string /// - /// Namer value collection + /// Name value collection /// Encoding type. Can be null. /// Whether to encode keys and values /// The query string without leading a question mark @@ -227,62 +280,5 @@ public static string BuildQueryString(this NameValueCollection nvc, Encoding enc } #endregion - - #region AsSerializable - - /// - /// Convenience API to allow an IEnumerable[T] (such as returned by Linq2Sql, NHibernate, EF etc.) - /// to be serialized by DataContractSerializer. - /// - /// The type of item. - /// The original collection. - /// A serializable enumerable wrapper. - public static IEnumerable AsSerializable(this IEnumerable source) where T : class - { - return new IEnumerableWrapper(source); - } - - /// - /// This wrapper allows IEnumerable to be serialized by DataContractSerializer. - /// It implements the minimal amount of surface needed for serialization. - /// - /// Type of item. - class IEnumerableWrapper : IEnumerable - where T : class - { - IEnumerable _collection; - - // The DataContractSerilizer needs a default constructor to ensure the object can be - // deserialized. We have a dummy one since we don't actually need deserialization. - public IEnumerableWrapper() - { - throw new NotImplementedException(); - } - - internal IEnumerableWrapper(IEnumerable collection) - { - this._collection = collection; - } - - // The DataContractSerilizer needs an Add method to ensure the object can be - // deserialized. We have a dummy one since we don't actually need deserialization. - public void Add(T item) - { - throw new NotImplementedException(); - } - - public IEnumerator GetEnumerator() - { - return this._collection.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return ((IEnumerable)this._collection).GetEnumerator(); - } - } - - #endregion } - } diff --git a/src/Libraries/SmartStore.Core/Extensions/HtmlTextWriterExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/HtmlTextWriterExtensions.cs index 26edc0d0e3..acd2f75109 100644 --- a/src/Libraries/SmartStore.Core/Extensions/HtmlTextWriterExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/HtmlTextWriterExtensions.cs @@ -4,14 +4,12 @@ using System.Web.UI; namespace SmartStore -{ - +{ public static class HtmlTextWriterExtensions { - public static void AddAttributes(this HtmlTextWriter writer, IDictionary attributes) { - if (attributes.Any>()) + if (attributes.Any()) { foreach (var pair in attributes) { @@ -20,8 +18,5 @@ public static void AddAttributes(this HtmlTextWriter writer, IDictionary webRequest); Guard.ArgumentNotNull(() => httpRequest); var authCookie = httpRequest.Cookies[FormsAuthentication.FormsCookieName]; + if (authCookie == null) + return; + var sendCookie = new Cookie(authCookie.Name, authCookie.Value, authCookie.Path, httpRequest.Url.Host); if (webRequest.CookieContainer == null) { webRequest.CookieContainer = new CookieContainer(); } + webRequest.CookieContainer.Add(sendCookie); } } diff --git a/src/Libraries/SmartStore.Core/Extensions/JsonExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/JsonExtensions.cs deleted file mode 100644 index 4c192adc74..0000000000 --- a/src/Libraries/SmartStore.Core/Extensions/JsonExtensions.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Net; -using System.Threading.Tasks; -using Newtonsoft.Json; - - -/////////////////////////////////////////////////////////////////////// -// Needs JSon.Net (Newtonsoft.Json.dll) from http://json.codeplex.com/ -////////////////////////////////////////////////////////////////////// - -namespace SmartStore -{ - public static class JsonExtensions - { - public static async Task GetDynamicJsonObject(this Uri uri) - { - using (WebClient wc = new WebClient()) - { - wc.Encoding = System.Text.Encoding.UTF8; - wc.Headers["User-Agent"] = "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 2.0.50727; .NET4.0C; .NET4.0E)"; - var response = await wc.DownloadStringTaskAsync(uri); - return JsonConvert.DeserializeObject(response); - } - } - } -} - - - diff --git a/src/Libraries/SmartStore.Core/Extensions/LinqExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/LinqExtensions.cs index 06a3b2dc6d..c0731e7538 100644 --- a/src/Libraries/SmartStore.Core/Extensions/LinqExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/LinqExtensions.cs @@ -1,15 +1,14 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; namespace SmartStore { - public static class LinqExtensions { - public static PropertyInfo ExtractPropertyInfo(this LambdaExpression propertyAccessor) { return propertyAccessor.ExtractMemberInfo() as PropertyInfo; @@ -20,7 +19,8 @@ public static FieldInfo ExtractFieldInfo(this LambdaExpression propertyAccessor) return propertyAccessor.ExtractMemberInfo() as FieldInfo; } - public static MemberInfo ExtractMemberInfo(this LambdaExpression propertyAccessor) + [SuppressMessage("ReSharper", "CanBeReplacedWithTryCastAndCheckForNull")] + public static MemberInfo ExtractMemberInfo(this LambdaExpression propertyAccessor) { Guard.ArgumentNotNull(() => propertyAccessor); diff --git a/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs index 865d5f8ed4..033c5d4230 100644 --- a/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs @@ -11,12 +11,12 @@ namespace SmartStore { public static class MiscExtensions { - public static void Dump(this Exception exc) + public static void Dump(this Exception exception) { try { - exc.StackTrace.Dump(); - exc.Message.Dump(); + exception.StackTrace.Dump(); + exception.Message.Dump(); } catch { } } @@ -24,7 +24,7 @@ public static void Dump(this Exception exc) public static string ToAllMessages(this Exception exception) { var sb = new StringBuilder(); - + while (exception != null) { if (!sb.ToString().EmptyNull().Contains(exception.Message)) @@ -46,52 +46,6 @@ public static string ToElapsedSeconds(this Stopwatch watch) return "{0:0.0}".FormatWith(TimeSpan.FromMilliseconds(watch.ElapsedMilliseconds).TotalSeconds); } - public static bool HasColumn(this DataView dv, string columnName) - { - dv.RowFilter = "ColumnName='" + columnName + "'"; - return dv.Count > 0; - } - - public static string GetDataType(this DataTable dt, string columnName) - { - dt.DefaultView.RowFilter = "ColumnName='" + columnName + "'"; - return dt.Rows[0]["DataType"].ToString(); - } - - public static int CountExecute(this OleDbConnection conn, string sqlCount) - { - using (OleDbCommand cmd = new OleDbCommand(sqlCount, conn)) - { - return (int)cmd.ExecuteScalar(); - } - } - - public static object SafeConvert(this TypeConverter converter, string value) - { - try - { - if (converter != null && value.HasValue() && converter.CanConvertFrom(typeof(string))) - { - return converter.ConvertFromString(value); - } - } - catch (Exception exc) - { - exc.Dump(); - } - return null; - } - - public static bool IsEqual(this TypeConverter converter, string value, object compareWith) - { - object convertedObject = converter.SafeConvert(value); - - if (convertedObject != null && compareWith != null) - return convertedObject.Equals(compareWith); - - return false; - } - public static bool IsNullOrDefault(this T? value) where T : struct { return default(T).Equals(value.GetValueOrDefault()); @@ -128,14 +82,6 @@ public static T GetMergedDataValue(this IMergedData mergedData, string key, T return defaultValue; } - public static bool IsRouteEqual(this RouteData routeData, string controller, string action) - { - if (routeData == null) - return false; - - return routeData.GetRequiredString("controller").IsCaseInsensitiveEqual(controller) && routeData.GetRequiredString("action").IsCaseInsensitiveEqual(action); - } - /// /// Append grow if string builder is empty. Append delimiter and grow otherwise. /// diff --git a/src/Libraries/SmartStore.Core/Extensions/StreamExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/StreamExtensions.cs index 31bddf144d..f958bdaf73 100644 --- a/src/Libraries/SmartStore.Core/Extensions/StreamExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/StreamExtensions.cs @@ -10,26 +10,35 @@ namespace SmartStore { public static class StreamExtensions { - - public static bool ToFile(this Stream srcStream, string path) + public static StreamReader ToStreamReader(this Stream stream, bool leaveOpen) + { + return new StreamReader(stream, Encoding.UTF8, true, 0x400, leaveOpen); + } + + public static StreamReader ToStreamReader(this Stream stream, Encoding encoding, bool detectEncoding, int bufferSize, bool leaveOpen) + { + return new StreamReader(stream, encoding, detectEncoding, bufferSize, leaveOpen); + } + + public static bool ToFile(this Stream srcStream, string path) { if (srcStream == null) return false; const int BuffSize = 32768; bool result = true; - int len = 0; Stream dstStream = null; byte[] buffer = new Byte[BuffSize]; try { - using (dstStream = File.OpenWrite(path)) - { + using (dstStream = File.OpenWrite(path)) + { + int len; while ((len = srcStream.Read(buffer, 0, BuffSize)) > 0) dstStream.Write(buffer, 0, len); } - } + } catch { result = false; diff --git a/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs index 159d1922e3..9131705215 100644 --- a/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs @@ -7,11 +7,11 @@ using System.Text.RegularExpressions; using System.IO; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace SmartStore { - public static class StringExtensions { public const string CarriageReturnLineFeed = "\r\n"; @@ -112,37 +112,37 @@ public static string NullEmpty(this string value) return (string.IsNullOrEmpty(value)) ? null : value; } - /// - /// Formats a string to an invariant culture - /// - /// The format string. - /// The objects. - /// - [DebuggerStepThrough] + /// + /// Formats a string to an invariant culture + /// + /// The format string. + /// The objects. + /// + [DebuggerStepThrough] public static string FormatInvariant(this string format, params object[] objects) { return string.Format(CultureInfo.InvariantCulture, format, objects); } - /// - /// Formats a string to the current culture. - /// - /// The format string. - /// The objects. - /// - [DebuggerStepThrough] + /// + /// Formats a string to the current culture. + /// + /// The format string. + /// The objects. + /// + [DebuggerStepThrough] public static string FormatCurrent(this string format, params object[] objects) { return string.Format(CultureInfo.CurrentCulture, format, objects); } - /// - /// Formats a string to the current UI culture. - /// - /// The format string. - /// The objects. - /// - [DebuggerStepThrough] + /// + /// Formats a string to the current UI culture. + /// + /// The format string. + /// The objects. + /// + [DebuggerStepThrough] public static string FormatCurrentUI(this string format, params object[] objects) { return string.Format(CultureInfo.CurrentUICulture, format, objects); @@ -160,29 +160,29 @@ public static string FormatWith(this string format, IFormatProvider provider, pa return string.Format(provider, format, args); } - /// - /// Determines whether this instance and another specified System.String object have the same value. - /// - /// The string to check equality. - /// The comparing with string. - /// - /// true if the value of the comparing parameter is the same as this string; otherwise, false. - /// - [DebuggerStepThrough] + /// + /// Determines whether this instance and another specified System.String object have the same value. + /// + /// The string to check equality. + /// The comparing with string. + /// + /// true if the value of the comparing parameter is the same as this string; otherwise, false. + /// + [DebuggerStepThrough] public static bool IsCaseSensitiveEqual(this string value, string comparing) { return string.CompareOrdinal(value, comparing) == 0; } - /// - /// Determines whether this instance and another specified System.String object have the same value. - /// - /// The string to check equality. - /// The comparing with string. - /// - /// true if the value of the comparing parameter is the same as this string; otherwise, false. - /// - [DebuggerStepThrough] + /// + /// Determines whether this instance and another specified System.String object have the same value. + /// + /// The string to check equality. + /// The comparing with string. + /// + /// true if the value of the comparing parameter is the same as this string; otherwise, false. + /// + [DebuggerStepThrough] public static bool IsCaseInsensitiveEqual(this string value, string comparing) { return string.Compare(value, comparing, StringComparison.OrdinalIgnoreCase) == 0; @@ -197,14 +197,14 @@ public static bool IsEmpty(this string value) return string.IsNullOrWhiteSpace(value); } - /// - /// Determines whether the string is all white space. Empty string will return false. - /// - /// The string to test whether it is all white space. - /// - /// true if the string is all white space; otherwise, false. - /// - [DebuggerStepThrough] + /// + /// Determines whether the string is all white space. Empty string will return false. + /// + /// The string to test whether it is all white space. + /// + /// true if the string is all white space; otherwise, false. + /// + [DebuggerStepThrough] public static bool IsWhiteSpace(this string value) { Guard.ArgumentNotNull(value, "value"); @@ -340,26 +340,6 @@ public static string Truncate(this string value, int maxLength, string suffix = } } - /// - /// Determines whether the string contains white space. - /// - /// The string to test for white space. - /// - /// true if the string contains white space; otherwise, false. - /// - [DebuggerStepThrough] - public static bool ContainsWhiteSpace(this string value) - { - Guard.ArgumentNotNull(value, "value"); - - for (int i = 0; i < value.Length; i++) - { - if (char.IsWhiteSpace(value[i])) - return true; - } - return false; - } - /// /// Ensure that a string starts with a string. /// @@ -375,13 +355,13 @@ public static string EnsureStartsWith(this string value, string startsWith) return value.StartsWith(startsWith) ? value : (startsWith + value); } - /// - /// Ensures the target string ends with the specified string. - /// - /// The target. - /// The value. - /// The target string with the value string at the end. - [DebuggerStepThrough] + /// + /// Ensures the target string ends with the specified string. + /// + /// The target. + /// The value. + /// The target string with the value string at the end. + [DebuggerStepThrough] public static string EnsureEndsWith(this string value, string endWith) { Guard.ArgumentNotNull(value, "value"); @@ -401,15 +381,6 @@ public static string EnsureEndsWith(this string value, string endWith) return value + endWith; } - [DebuggerStepThrough] - public static int? GetLength(this string value) - { - if (value == null) - return null; - else - return value.Length; - } - [DebuggerStepThrough] public static string UrlEncode(this string value) { @@ -477,13 +448,13 @@ private static string RemoveHtmlInternal(string s, ICollection removeTag }); } - /// - /// Replaces pascal casing with spaces. For example "CustomerId" would become "Customer Id". - /// Strings that already contain spaces are ignored. - /// - /// String to split - /// The string after being split - [DebuggerStepThrough] + /// + /// Replaces pascal casing with spaces. For example "CustomerId" would become "Customer Id". + /// Strings that already contain spaces are ignored. + /// + /// String to split + /// The string after being split + [DebuggerStepThrough] public static string SplitPascalCase(this string value) { //return Regex.Replace(input, "([A-Z][a-z])", " $1", RegexOptions.Compiled).Trim(); @@ -518,111 +489,23 @@ public static string[] SplitSafe(this string value, string separator) /// Splits a string into two strings /// true: success, false: failure [DebuggerStepThrough] - public static bool SplitToPair(this string value, out string strLeft, out string strRight, string delimiter) { + [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")] + public static bool SplitToPair(this string value, out string strLeft, out string strRight, string delimiter) + { int idx = -1; - if (value.IsEmpty() || delimiter.IsEmpty() || (idx = value.IndexOf(delimiter)) == -1) + if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(delimiter) || (idx = value.IndexOf(delimiter)) == -1) { strLeft = value; strRight = ""; return false; } + strLeft = value.Substring(0, idx); strRight = value.Substring(idx + delimiter.Length); + return true; } - [DebuggerStepThrough] - public static string ToCamelCase(this string instance) - { - char ch = instance[0]; - return (ch.ToString().ToLowerInvariant() + instance.Substring(1)); - } - - [DebuggerStepThrough] - public static string ReplaceNewLines(this string value, string replacement) - { - StringReader sr = new StringReader(value); - StringBuilder sb = new StringBuilder(); - - bool first = true; - - string line; - while ((line = sr.ReadLine()) != null) - { - if (first) - first = false; - else - sb.Append(replacement); - - sb.Append(line); - } - - return sb.ToString(); - } - - /// - /// Indents the specified string. - /// - /// The string to indent. - /// The number of characters to indent by. - /// - [DebuggerStepThrough] - public static string Indent(this string value, int indentation) - { - return Indent(value, indentation, ' '); - } - - /// - /// Indents the specified string. - /// - /// The string to indent. - /// The number of characters to indent by. - /// The indent character. - /// - [DebuggerStepThrough] - public static string Indent(this string value, int indentation, char indentChar) - { - Guard.ArgumentNotNull(value, "value"); - Guard.ArgumentIsPositive(indentation, "indentation"); - - StringReader sr = new StringReader(value); - StringWriter sw = new StringWriter(CultureInfo.InvariantCulture); - - ActionTextReaderLine(sr, sw, delegate(TextWriter tw, string line) - { - tw.Write(new string(indentChar, indentation)); - tw.Write(line); - }); - - return sw.ToString(); - } - - /// - /// Numbers the lines. - /// - /// The string to number. - /// - public static string NumberLines(this string value) - { - Guard.ArgumentNotNull(value, "value"); - - StringReader sr = new StringReader(value); - StringWriter sw = new StringWriter(CultureInfo.InvariantCulture); - - int lineNumber = 1; - - ActionTextReaderLine(sr, sw, delegate(TextWriter tw, string line) - { - tw.Write(lineNumber.ToString(CultureInfo.InvariantCulture).PadLeft(4)); - tw.Write(". "); - tw.Write(line); - - lineNumber++; - }); - - return sw.ToString(); - } - [DebuggerStepThrough] public static string EncodeJsString(this string value) { @@ -632,7 +515,7 @@ public static string EncodeJsString(this string value) [DebuggerStepThrough] public static string EncodeJsString(this string value, char delimiter, bool appendDelimiters) { - StringBuilder sb = new StringBuilder(value.GetLength() ?? 16); + StringBuilder sb = new StringBuilder(value != null ? value.Length : 16); using (StringWriter w = new StringWriter(sb, CultureInfo.InvariantCulture)) { EncodeJsString(w, value, delimiter, appendDelimiters); @@ -720,9 +603,12 @@ public static void Dump(this string value, bool appendMarks = false) Debug.WriteLine(value); Debug.WriteLineIf(appendMarks, "------------------------------------------------"); } - - /// Smart way to create a HTML attribute with a leading space. - /// Name of the attribute. + + /// Smart way to create a HTML attribute with a leading space. + /// Name of the attribute. + /// + /// + [SuppressMessage("ReSharper", "StringCompareIsCultureSpecific.3")] public static string ToAttribute(this string value, string name, bool htmlEncode = true) { if (name.IsEmpty()) @@ -778,7 +664,31 @@ public static string Replace(this string value, int x1, int x2, string replaceBy return value; } - [DebuggerStepThrough] + [DebuggerStepThrough] + public static string Replace(this string value, string oldValue, string newValue, StringComparison comparisonType) + { + try + { + int startIndex = 0; + while (true) + { + startIndex = value.IndexOf(oldValue, startIndex, comparisonType); + if (startIndex == -1) + break; + + value = value.Substring(0, startIndex) + newValue + value.Substring(startIndex + oldValue.Length); + + startIndex += newValue.Length; + } + } + catch (Exception exc) + { + exc.Dump(); + } + return value; + } + + [DebuggerStepThrough] public static string TrimSafe(this string value) { return (value.HasValue() ? value.Trim() : value); @@ -963,14 +873,15 @@ public static string RemoveInvalidXmlChars(this string s) [DebuggerStepThrough] public static string ReplaceCsvChars(this string s) { - if (s.HasValue()) + if (s.IsEmpty()) { - s = s.Replace(';', ','); - s = s.Replace('\r', ' '); - s = s.Replace('\n', ' '); - return s.Replace("'", ""); + return ""; } - return ""; + + s = s.Replace(';', ','); + s = s.Replace('\r', ' '); + s = s.Replace('\n', ' '); + return s.Replace("'", ""); } #endregion @@ -1040,22 +951,6 @@ private static void EncodeJsString(TextWriter writer, string value, char delimit writer.Write(delimiter); } - - private static void ActionTextReaderLine(TextReader textReader, TextWriter textWriter, ActionLine lineAction) - { - string line; - bool firstLine = true; - while ((line = textReader.ReadLine()) != null) - { - if (!firstLine) - textWriter.WriteLine(); - else - firstLine = false; - - lineAction(textWriter, line); - } - } - #endregion } diff --git a/src/Libraries/SmartStore.Core/Extensions/TypeDescriptorExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/TypeDescriptorExtensions.cs deleted file mode 100644 index f9467d851a..0000000000 --- a/src/Libraries/SmartStore.Core/Extensions/TypeDescriptorExtensions.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.ComponentModel; - -using SmartStore.Linq; - -namespace SmartStore -{ - - public static class TypeDescriptorExtensions - { - - public static IEnumerable GetAttributes(this ICustomTypeDescriptor td) where TAttribute : Attribute - { - var attributes = td.GetAttributes().OfType(); - return TypeExtensions.SortAttributesIfPossible(attributes); - } - - public static IEnumerable GetAttributes(this PropertyDescriptor pd) where TAttribute : Attribute - { - var attributes = pd.Attributes.OfType(); - return TypeExtensions.SortAttributesIfPossible(attributes); - } - - public static IEnumerable GetAttributes(this PropertyDescriptor pd, - Func predicate) - where TAttribute : Attribute - { - Guard.ArgumentNotNull(predicate, "predicate"); - - var attributes = pd.Attributes.OfType().Where(predicate); - return TypeExtensions.SortAttributesIfPossible(attributes); - } - - public static PropertyDescriptor GetProperty(this ICustomTypeDescriptor td, string name) - { - Guard.ArgumentNotEmpty(name, "name"); - return td.GetProperties().Find(name, true); - //.Cast() - //.FirstOrDefault(p => p.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)); - } - - public static IEnumerable GetPropertiesWith(this ICustomTypeDescriptor td) - where TAttribute : Attribute - { - return td.GetPropertiesWith(x => true); - } - - public static IEnumerable GetPropertiesWith( - this ICustomTypeDescriptor td, - Func predicate) - where TAttribute : Attribute - { - Guard.ArgumentNotNull(predicate, "predicate"); - - return td.GetProperties() - .Cast() - .Where(p => p.GetAttributes().Any(predicate)); - - //Expression> expression = (TAttribute a) => typeof(TAttribute).IsAssignableFrom(a.GetType()); - - //return td.GetProperties() - // .Cast() - // .Where(p => p.Attributes.Cast() - // .Any(expression.And(predicate).Compile())); - - //var query = from p in this.EntityTypeDescriptor.GetProperties().Cast() - // let attributes = p.Attributes.Cast() - // where attributes.Any(a => typeof(TAttribute).IsAssignableFrom(a.GetType())) - // //where attributes.Any(a => typeof(TAttribute).IsAssignableFrom(a.GetType())) - // select p; - - //return query - // .Select(p => new EntityProperty(p, this)) - // .AsReadOnly(); - } - - } - -} diff --git a/src/Libraries/SmartStore.Core/Extensions/TypeExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/TypeExtensions.cs index 69752367c2..baf9195b18 100644 --- a/src/Libraries/SmartStore.Core/Extensions/TypeExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/TypeExtensions.cs @@ -4,26 +4,26 @@ using System.Reflection; using System.Collections; using System.Diagnostics; -using Fasterflect; +using System.Diagnostics.CodeAnalysis; namespace SmartStore { - public static class TypeExtensions { - private static Type[] s_predefinedTypes; - private static Type[] s_predefinedGenericTypes; - - static TypeExtensions() - { - s_predefinedTypes = new Type[] { typeof(string), typeof(decimal), typeof(DateTime), typeof(TimeSpan), typeof(Guid) }; - s_predefinedGenericTypes = new Type[] { typeof(Nullable<>) }; - } + private static readonly Type[] s_predefinedTypes = new Type[] { typeof(string), typeof(decimal), typeof(DateTime), typeof(TimeSpan), typeof(Guid) }; + private static readonly Type[] s_predefinedGenericTypes = new Type[] { typeof(Nullable<>) }; public static string AssemblyQualifiedNameWithoutVersion(this Type type) { - string[] strArray = type.AssemblyQualifiedName.Split(new char[] { ',' }); - return string.Format("{0}, {1}", strArray[0], strArray[1]); + Guard.ArgumentNotNull(() => type); + + if (type.AssemblyQualifiedName != null) + { + var strArray = type.AssemblyQualifiedName.Split(new char[] { ',' }); + return string.Format("{0}, {1}", strArray[0], strArray[1]); + } + + return null; } public static bool IsSequenceType(this Type seqType) @@ -45,15 +45,8 @@ public static bool IsPredefinedSimpleType(this Type type) { return true; } + return s_predefinedTypes.Any(t => t == type); - //foreach (Type type2 in s_predefinedTypes) - //{ - // if (type2 == type) - // { - // return true; - // } - //} - //return false; } public static bool IsStruct(this Type type) @@ -75,15 +68,8 @@ public static bool IsPredefinedGenericType(this Type type) { return false; } + return s_predefinedGenericTypes.Any(t => t == type); - //foreach (Type type2 in s_predefinedGenericTypes) - //{ - // if (type2 == type) - // { - // return true; - // } - //} - //return false; } public static bool IsPredefinedType(this Type type) @@ -97,7 +83,6 @@ public static bool IsPredefinedType(this Type type) public static bool IsInteger(this Type type) { - switch (Type.GetTypeCode(type)) { case TypeCode.SByte: @@ -119,11 +104,6 @@ public static bool IsNullable(this Type type) return type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); } - public static bool IsNullAssignable(this Type type) - { - return !type.IsValueType || type.IsNullable(); - } - public static bool IsConstructable(this Type type) { Guard.ArgumentNotNull(type, "type"); @@ -224,246 +204,6 @@ private static bool IsSubClassInternal(Type initialType, Type currentType, Type return IsSubClassInternal(initialType, currentType.BaseType, check, out implementingType); } - public static bool IsIndexed(this PropertyInfo property) - { - Guard.ArgumentNotNull(property, "property"); - return !property.GetIndexParameters().IsNullOrEmpty(); - } - - /// - /// Determines whether the member is an indexed property. - /// - /// The member. - /// - /// true if the member is an indexed property; otherwise, false. - /// - public static bool IsIndexed(this MemberInfo member) - { - Guard.ArgumentNotNull(member, "member"); - - PropertyInfo propertyInfo = member as PropertyInfo; - - if (propertyInfo != null) - return propertyInfo.IsIndexed(); - else - return false; - } - - /// - /// Checks to see if the specified type is assignable. - /// - /// - /// - public static bool IsType(this Type type) - { - return typeof(TType).IsAssignableFrom(type); - } - - - - public static MemberInfo GetSingleMember(this Type type, string name, MemberTypes memberTypes) - { - return type.GetSingleMember( - name, - memberTypes, - BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static); - } - - public static MemberInfo GetSingleMember(this Type type, string name, MemberTypes memberTypes, BindingFlags bindingAttr) - { - return type.GetMember( - name, - memberTypes, - bindingAttr).SingleOrDefault(); - } - - public static string GetNameAndAssemblyName(this Type type) - { - Guard.ArgumentNotNull(type, "type"); - return type.FullName + ", " + type.Assembly.GetName().Name; - } - - public static IEnumerable GetFieldsAndProperties(this Type type, BindingFlags bindingAttr) - { - foreach (var fi in type.GetFields(bindingAttr)) - { - yield return fi; - } - - foreach (var pi in type.GetProperties(bindingAttr)) - { - yield return pi; - } - } - - public static MemberInfo GetFieldOrProperty(this Type type, string name, bool ignoreCase) - { - BindingFlags flags = BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance; - if (ignoreCase) - flags |= BindingFlags.IgnoreCase; - - return type.GetSingleMember( - name, - MemberTypes.Field | MemberTypes.Property, - flags); - } - - public static List FindMembers(this Type targetType, MemberTypes memberType, BindingFlags bindingAttr, MemberFilter filter, object filterCriteria) - { - Guard.ArgumentNotNull(targetType, "targetType"); - - List memberInfos = new List(targetType.FindMembers(memberType, bindingAttr, filter, filterCriteria)); - - // fix weirdness with FieldInfos only being returned for the current Type - // find base type fields and add them to result - if ((memberType & MemberTypes.Field) != 0 - && (bindingAttr & BindingFlags.NonPublic) != 0) - { - // modify flags to not search for public fields - BindingFlags nonPublicBindingAttr = bindingAttr ^ BindingFlags.Public; - - while ((targetType = targetType.BaseType) != null) - { - memberInfos.AddRange(targetType.FindMembers(MemberTypes.Field, nonPublicBindingAttr, filter, filterCriteria)); - } - } - - return memberInfos; - } - - //public static Type MakeGenericType(this Type genericTypeDefinition, params Type[] innerTypes) - //{ - // Guard.ArgumentNotNull(genericTypeDefinition, "genericTypeDefinition"); - // Guard.ArgumentNotEmpty(innerTypes, "innerTypes"); - // Guard.Argument.IsTrue(genericTypeDefinition.IsGenericTypeDefinition, "genericTypeDefinition", "Type '{0}' must be a generic type definition.".FormatInvariant(genericTypeDefinition)); - - // return genericTypeDefinition.MakeGenericType(innerTypes); - //} - - public static object CreateGeneric(this Type genericTypeDefinition, Type innerType, params object[] args) - { - return CreateGeneric(genericTypeDefinition, new Type[] { innerType }, args); - } - - public static object CreateGeneric(this Type genericTypeDefinition, Type[] innerTypes, params object[] args) - { - return CreateGeneric(genericTypeDefinition, innerTypes, (t, a) => Activator.CreateInstance(t, args)); - } - - public static object CreateGeneric(this Type genericTypeDefinition, Type[] innerTypes, Func instanceCreator, params object[] args) - { - Guard.ArgumentNotNull(() => genericTypeDefinition); - Guard.ArgumentNotNull(() => innerTypes); - Guard.ArgumentNotNull(() => instanceCreator); - if (innerTypes.Length == 0) - throw Error.Argument("innerTypes", "The sequence must contain at least one entry."); - - Type specificType = genericTypeDefinition.MakeGenericType(innerTypes); - - return instanceCreator(specificType, args); - } - - public static IList CreateGenericList(this Type listType) - { - Guard.ArgumentNotNull(listType, "listType"); - return (IList)typeof(List<>).CreateGeneric(listType); - } - - //public static Type RemoveNullable(this Type type) - //{ - // if (type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(Nullable<>))) - // { - // return type.GetGenericArguments()[0]; - // } - // return type; - //} - - public static bool IsEnumerable(this Type type) - { - Guard.ArgumentNotNull(type, "type"); - return type.IsAssignableFrom(typeof(IEnumerable)); - } - - public static bool IsGenericDictionary(this Type type) - { - if (type.IsInterface && type.IsGenericType) - { - return typeof(IDictionary<,>).Equals(type.GetGenericTypeDefinition()); - } - return (type.GetInterface(typeof(IDictionary<,>).Name) != null); - } - - //public static bool IsListType(this Type type) - //{ - // Guard.ArgumentNotNull(type, "type"); - - // if (type.IsArray) - // return true; - // else if (typeof(IList).IsAssignableFrom(type)) - // return true; - // else if (type.IsSubClass(typeof(IList<>))) - // return true; - // else - // return false; - //} - - /// - /// Gets the member's value on the object. - /// - /// The member. - /// The target object. - /// The member's value on the object. - public static object GetValue(this MemberInfo member, object target) - { - Guard.ArgumentNotNull(member, "member"); - Guard.ArgumentNotNull(target, "target"); - - var type = target.GetType(); - - switch (member.MemberType) - { - case MemberTypes.Field: - return target.GetFieldValue(member.Name); - //return ((FieldInfo)member).GetValue(target); - case MemberTypes.Property: - return target.GetPropertyValue(member.Name); - default: - throw new ArgumentException("MemberInfo '{0}' is not of type FieldInfo or PropertyInfo".FormatInvariant(member.Name), "member"); - } - } - - /// - /// Sets the member's value on the target object. - /// - /// The member. - /// The target. - /// The value. - public static void SetValue(this MemberInfo member, object target, object value) - { - Guard.ArgumentNotNull(member, "member"); - Guard.ArgumentNotNull(target, "target"); - - switch (member.MemberType) - { - case MemberTypes.Field: - target.SetFieldValue(member.Name, value); - break; - //return ((FieldInfo)member).GetValue(target); - case MemberTypes.Property: - try - { - target.SetPropertyValue(member.Name, value); - } - catch (TargetParameterCountException e) - { - throw new ArgumentException("PropertyInfo '{0}' has index parameters".FormatInvariant(member.Name), "member", e); - } - break; - default: - throw new ArgumentException("MemberInfo '{0}' is not of type FieldInfo or PropertyInfo".FormatInvariant(member.Name), "member"); - } - } - /// /// Gets the underlying type of a type. /// @@ -476,62 +216,15 @@ public static Type GetNonNullableType(this Type type) return type.GetGenericArguments()[0]; } - /// - /// Determines whether the specified MemberInfo can be read. - /// - /// The MemberInfo to determine whether can be read. - /// - /// true if the specified MemberInfo can be read; otherwise, false. - /// - /// - /// For methods this will return true if the return type - /// is not void and the method is parameterless. - /// - public static bool CanReadValue(this MemberInfo member) - { - switch (member.MemberType) - { - case MemberTypes.Field: - return true; - case MemberTypes.Property: - return ((PropertyInfo)member).CanRead; - case MemberTypes.Method: - MethodInfo mi = (MethodInfo)member; - return mi.ReturnType != typeof(void) && mi.GetParameters().Length == 0; - default: - return false; - } - } - - /// - /// Determines whether the specified MemberInfo can be set. - /// - /// The MemberInfo to determine whether can be set. - /// - /// true if the specified MemberInfo can be set; otherwise, false. - /// - public static bool CanSetValue(this MemberInfo member) - { - switch (member.MemberType) - { - case MemberTypes.Field: - return true; - case MemberTypes.Property: - return ((PropertyInfo)member).CanWrite; - default: - return false; - } - } - - /// - /// Returns single attribute from the type - /// - /// Attribute to use - /// Attribute provider - /// - /// Null if the attribute is not found - /// If there are 2 or more attributes - public static TAttribute GetAttribute(this ICustomAttributeProvider target, bool inherits) where TAttribute : Attribute + /// + /// Returns single attribute from the type + /// + /// Attribute to use + /// Attribute provider + /// + /// Null if the attribute is not found + /// If there are 2 or more attributes + public static TAttribute GetAttribute(this ICustomAttributeProvider target, bool inherits) where TAttribute : Attribute { if (target.IsDefined(typeof(TAttribute), inherits)) { @@ -551,15 +244,15 @@ public static bool HasAttribute(this ICustomAttributeProvider target return target.IsDefined(typeof(TAttribute), inherits); } - /// - /// Given a particular MemberInfo, return the custom attributes of the - /// given type on that member. - /// - /// Type of attribute to retrieve. - /// The member to look at. - /// True to include attributes inherited from base classes. - /// Array of found attributes. - public static TAttribute[] GetAttributes(this ICustomAttributeProvider target, bool inherits) where TAttribute : Attribute + /// + /// Given a particular MemberInfo, return the custom attributes of the + /// given type on that member. + /// + /// Type of attribute to retrieve. + /// The member to look at. + /// True to include attributes inherited from base classes. + /// Array of found attributes. + public static TAttribute[] GetAttributes(this ICustomAttributeProvider target, bool inherits) where TAttribute : Attribute { if (target.IsDefined(typeof(TAttribute), inherits)) { @@ -568,30 +261,9 @@ public static TAttribute[] GetAttributes(this ICustomAttributeProvid .Cast(); return SortAttributesIfPossible(attributes).ToArray(); - - #region Obsolete - //return target - // .GetCustomAttributes(typeof(TAttribute), inherits) - // .ToArray(a => (TAttribute)a); - #endregion } - return new TAttribute[0]; - #region Obsolete - //// OBSOLETE 1 - //return target.GetCustomAttributes(typeof(TAttribute), inherits).Cast().ToArray(); - - //// OBSOLETE 2 - //object[] attributesAsObjects = member.GetCustomAttributes(typeof(TAttribute), inherits); - //TAttribute[] attributes = new TAttribute[attributesAsObjects.Length]; - //int index = 0; - //Array.ForEach(attributesAsObjects, - // delegate(object o) - // { - // attributes[index++] = (TAttribute)o; - // }); - //return attributes; - #endregion + return new TAttribute[0]; } /// @@ -626,7 +298,8 @@ public static TAttribute[] GetAllAttributes(this MemberInfo member, return attributes.ToArray(); } - internal static IEnumerable SortAttributesIfPossible(IEnumerable attributes) + [SuppressMessage("ReSharper", "SuspiciousTypeConversion.Global")] + internal static IEnumerable SortAttributesIfPossible(IEnumerable attributes) where TAttribute : Attribute { if (typeof(IOrdered).IsAssignableFrom(typeof(TAttribute))) @@ -672,15 +345,16 @@ internal static Type FindIEnumerable(this Type seqType) return typeof(IEnumerable<>).MakeGenericType(seqType.GetElementType()); if (seqType.IsGenericType) { - foreach (Type arg in seqType.GetGenericArguments()) + var args = seqType.GetGenericArguments(); + foreach (var arg in args) { - Type ienum = typeof(IEnumerable<>).MakeGenericType(arg); + var ienum = typeof(IEnumerable<>).MakeGenericType(arg); if (ienum.IsAssignableFrom(seqType)) return ienum; } } Type[] ifaces = seqType.GetInterfaces(); - if (ifaces != null && ifaces.Length > 0) + if (ifaces.Length > 0) { foreach (Type iface in ifaces) { diff --git a/src/Libraries/SmartStore.Core/Extensions/XPathExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/XPathExtensions.cs index a770ab6007..d44109c7f2 100644 --- a/src/Libraries/SmartStore.Core/Extensions/XPathExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/XPathExtensions.cs @@ -27,7 +27,7 @@ public static class XPathExtensions if (xpath.IsEmpty()) { if (node.Value.HasValue()) - return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFromString(null, culture == null ? CultureInfo.InvariantCulture : culture, node.Value); + return node.Value.Convert(culture); return defaultValue; } @@ -38,7 +38,7 @@ public static class XPathExtensions var n = node.SelectSingleNode(xpath); if (n != null && n.Value.HasValue()) - return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFromString(null, culture == null ? CultureInfo.InvariantCulture : culture, n.Value); + return n.Value.Convert(culture); } } catch (Exception exc) diff --git a/src/Libraries/SmartStore.Core/Extensions/XmlNodeExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/XmlNodeExtensions.cs index b167b43c85..601c4cef6d 100644 --- a/src/Libraries/SmartStore.Core/Extensions/XmlNodeExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/XmlNodeExtensions.cs @@ -17,7 +17,7 @@ public static class XmlNodeExtensions XmlAttribute attr = node.Attributes[attributeName]; if (attr != null) { - return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFromString(attr.InnerText); + return attr.InnerText.Convert(); } } } @@ -43,13 +43,13 @@ public static string GetAttributeText(this XmlNode node, string attributeName) if (node != null) { if (xpath.IsEmpty()) - return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFromString(node.InnerText); + return node.InnerText.Convert(); var n = node.SelectSingleNode(xpath); if (n != null && n.InnerText.HasValue()) - return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFromString(null, culture == null ? CultureInfo.InvariantCulture : culture, n.InnerText); - } + return n.InnerText.Convert(culture); + } } catch (Exception exc) { diff --git a/src/Libraries/SmartStore.Core/Extensions/XmlWriterExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/XmlWriterExtensions.cs index 5ead943146..ca4e1c2c71 100644 --- a/src/Libraries/SmartStore.Core/Extensions/XmlWriterExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/XmlWriterExtensions.cs @@ -21,16 +21,6 @@ public static void WriteCData(this XmlWriter writer, string name, string value, } } - public static void WriteNode(this XmlWriter writer, string name, Action content) - { - if (name.HasValue() && content != null) - { - writer.WriteStartElement(name); - content(); - writer.WriteEndElement(); - } - } - /// /// Created a simple or CData node element /// diff --git a/src/Libraries/SmartStore.Core/Fakes/FakeController.cs b/src/Libraries/SmartStore.Core/Fakes/FakeController.cs new file mode 100644 index 0000000000..1ff087bb77 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Fakes/FakeController.cs @@ -0,0 +1,8 @@ +using System.Web.Mvc; + +namespace SmartStore.Core.Fakes +{ + public class FakeController : Controller + { + } +} diff --git a/src/Libraries/SmartStore.Core/Financial/Currency.cs b/src/Libraries/SmartStore.Core/Financial/Currency.cs deleted file mode 100644 index a30be2bf3f..0000000000 --- a/src/Libraries/SmartStore.Core/Financial/Currency.cs +++ /dev/null @@ -1,307 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Globalization; - -namespace SmartStore.Financial -{ - - [Serializable] - public struct Currency : IEquatable - { - #region Fields - - internal readonly RegionInfo Region; - public static readonly Currency Empty = new Currency(); - - #endregion - - #region Ctor - - public Currency(RegionInfo region) - : this() - { - Guard.ArgumentNotNull(() => region); - - Region = region; - - this.ThreeLetterISOCode = region.ISOCurrencySymbol; - this.Symbol = region.CurrencySymbol; - this.NativeName = region.CurrencyNativeName; - this.EnglishName = region.CurrencyEnglishName; - } - - public Currency(Currency currency) - : this() - { - this.ThreeLetterISOCode = currency.ThreeLetterISOCode; - this.Symbol = currency.Symbol; - this.NativeName = currency.NativeName; - this.EnglishName = currency.EnglishName; - this.Region = currency.Region; - } - - #endregion - - #region Properties - - /// - /// Gets the ISO standard three-letter name. - /// - public string ThreeLetterISOCode { get; private set; } - - ///// - ///// Gets the integer-based ISO code. - ///// - //public int NumericISOCode { get; private set; } - - /// - /// Gets the prefixing symbol of the currency. - /// - public string Symbol { get; private set; } - - /// - /// Gets the native name of the currency. - /// - public string NativeName { get; private set; } - - /// - /// Gets the english name of the currency. - /// - public string EnglishName { get; private set; } - - // TODO: (?) DecimalDigits, Separator, GroupSeparator, GroupSizes, NegativePatterm, PositivePattern - - #endregion - - #region Methods - - public static Currency Default - { - get - { - return new Currency(RegionInfo.CurrentRegion); - } - } - - public static Currency GetCurrency(string threeLetterISOCode) - { - Guard.ArgumentNotEmpty(threeLetterISOCode, "threeLetterISOCode"); - - if (threeLetterISOCode.Length != 3) - throw new ArgumentException("The currency ISO code must be 3 letters in length.", "threeLetterISOCode"); - - var query = - from r in GetValidRegions() - where r.ISOCurrencySymbol.Equals(threeLetterISOCode, StringComparison.InvariantCultureIgnoreCase) - select r; - var region = query.First(); - - return new Currency(region); - } - - public static Currency GetCurrency(RegionInfo region) - { - return new Currency(region); - } - - public static bool TryGetCurrency(string threeLetterISOCode, out Currency currency) - { - currency = Currency.Empty; - - try - { - currency = Currency.GetCurrency(threeLetterISOCode); - return (currency.ThreeLetterISOCode != null); - } - catch - { - return false; - } - } - - public static IEnumerable AllCurrencies - { - get - { - return from r in GetValidRegions() - select new Currency(r); - } - } - - internal static IEnumerable GetValidRegions() - { - return - from c in - (from c in CultureInfo.GetCultures(CultureTypes.InstalledWin32Cultures) - where !c.IsNeutralCulture && c.LCID != 0x7f - select c) - select new RegionInfo(c.Name); - } - - #endregion - - #region Comparison - - public override int GetHashCode() - { - return this.ThreeLetterISOCode.ToUpperInvariant().GetHashCode(); - } - - public override bool Equals(object obj) - { - if (obj == null) - return false; - - if (obj.GetType() != typeof(Currency)) - return false; - - return this.Equals((Currency)obj); - } - - public bool Equals(Currency other) - { - return (this.ThreeLetterISOCode.Equals(other.ThreeLetterISOCode, StringComparison.InvariantCultureIgnoreCase)); - } - - public override string ToString() - { - return "{0} ({1})".FormatCurrent( - this.Symbol ?? this.ThreeLetterISOCode, - this.NativeName ?? this.EnglishName ?? this.ThreeLetterISOCode); - } - - #endregion - - #region Implicit operators - - public static implicit operator Currency(string threeLetterISOCode) - { - return Currency.GetCurrency(threeLetterISOCode); - } - - public static implicit operator Currency(Money value) - { - return new Currency(value.Currency); - } - - #endregion - - #region Operator overloads - - public static bool operator ==(Currency left, Currency right) - { - return left.Equals(right); - } - - public static bool operator !=(Currency left, Currency right) - { - return !left.Equals(right); - } - - #endregion - - #region Temp/Cargo - - // ISOCode, NumericISOCode, Symbol, EnglishName, DecimalDigits - - //new CurrencyInfo("ARS", 32, "", "Argentine peso", 2, false), - //new CurrencyInfo("AUD", 36, "", "Australian dollar", 2, false), - //new CurrencyInfo("ATS", 40, "", "Austrian schilling", 2, false), // Gibt es nicht mehr - //new CurrencyInfo("BSD", 44, "", "Bahamian dollar", 2, false), - //new CurrencyInfo("BHD", 48, "", "Bahraini dinar", 2, false), - //new CurrencyInfo("BDT", 50, "", "Bangladesh taka", 2, false), - //new CurrencyInfo("BEF", 56, "", "Belgian franc", 2, false), // Gibt es nicht mehr - //new CurrencyInfo("BWP", 72, "", "Botswanan pula", 2, false), - //new CurrencyInfo("BRL", 986, "", "Brazilian real", 2, false), - //new CurrencyInfo("GBP", 826, "£", "British pound", 2, true), - //new CurrencyInfo("BND", 96, "", "Brunei dollar", 2, false), - //new CurrencyInfo("BGN", 975, "", "Bulgarian lev", 2, false), - //new CurrencyInfo("CAD", 124, "", "Canadian dollar", 2, false), - //new CurrencyInfo("CLP", 152, "", "Chilean peso", 2, false), - //new CurrencyInfo("CNY", 156, "", "Chinese yuan renminbi", 2, false), - //new CurrencyInfo("COP", 170, "", "Colombian peso", 2, false), - //new CurrencyInfo("HRK", 191, "", "Croatian kuna", 2, false), - //new CurrencyInfo("CYP", 196, "", "Cyprus pound", 2, false), - //new CurrencyInfo("CZK", 203, "", "Czech koruna", 2, false), - //new CurrencyInfo("DKK", 208, "DK", "Danische Krone", 2, true), - //new CurrencyInfo("DEM", 276, "DM", "Deutsche Mark", 2, false), // Gibt es nicht mehr - //new CurrencyInfo("EGP", 818, "", "Egyptian pound", 2, false), - //new CurrencyInfo("EEK", 233, "", "Estonian kroon", 2, false), - //new CurrencyInfo("EUR", 978, "", "Euro", 2, true), - //new CurrencyInfo("XEU", 954, "", "European Currency Unit", 2, false), // Gibt es nicht mehr - //new CurrencyInfo("FJD", 242, "", "Fiji dollar", 2, false), - //new CurrencyInfo("FIM", 246, "", "Finnish markka", 2, false), // Gibt es nicht mehr - //new CurrencyInfo("FRF", 250, "", "French franc", 2, false), // Gibt es nicht mehr - //new CurrencyInfo("GHC", 288, "", "Ghana cedi", 2, false), - //new CurrencyInfo("GRD", 300, "", "Greek drachma", 2, false), // Gibt es nicht mehr - //new CurrencyInfo("HNL", 340, "", "Honduras lempira", 2, false), - //new CurrencyInfo("HKD", 344, "", "Hong Kong dollar", 2, false), - //new CurrencyInfo("HUF", 348, "", "Hungarian forint", 2, false), - //new CurrencyInfo("ISK", 352, "", "Iceland krona", 2, false), - //new CurrencyInfo("INR", 356, "", "Indian rupee", 2, false), - //new CurrencyInfo("IDR", 360, "", "Indonesian rupiah", 2, false), - //new CurrencyInfo("IRR", 364, "", "Iranian rial", 2, false), - //new CurrencyInfo("IQD", 368, "", "Iraqi dinar", 2, false), - //new CurrencyInfo("IEP", 372, "", "Irish pound", 2, false), // Gibt es nicht mehr - //new CurrencyInfo("ILS", 376, "Shekel", "Israeli shekel", 2, false), - //new CurrencyInfo("ITL", 380, "", "Italian lira", 2, false), // Gibt es nicht mehr - //new CurrencyInfo("JMD", 388, "", "Jamaican dollar", 2, false), - //new CurrencyInfo("JPY", 392, "¥", "Japanese yen", 2, true), - //new CurrencyInfo("KWD", 414, "", "Kuwaiti dinar", 2, false), - //new CurrencyInfo("LVL", 428, "", "Latvian lats", 2, false), - //new CurrencyInfo("LYD", 434, "", "Libyan dinar", 2, false), - //new CurrencyInfo("LTL", 440, "", "Lithuanian litas", 2, false), - //new CurrencyInfo("LUF", 442, "", "Luxembourg franc", 2, false), // Gibt es nicht mehr - //new CurrencyInfo("MYR", 458, "", "Malaysian ringgit", 2, false), - //new CurrencyInfo("MTL", 470, "", "Maltese lira", 2, false), - //new CurrencyInfo("MUR", 480, "", "Mauritius rupee", 2, false), - //new CurrencyInfo("MXP", 484, "", "Mexican peso", 2, false), // Gibt es nicht mehr - //new CurrencyInfo("MXN", 484, "", "Mexican peso", 2, false), - //new CurrencyInfo("MAD", 504, "", "Moroccan dirham", 2, false), - //new CurrencyInfo("MMK", 104, "", "Myanmar kyat", 2, false), - //new CurrencyInfo("NPR", 524, "", "Nepalese rupee", 2, false), - //new CurrencyInfo("ANG", 532, "", "Netherlands Antillian guilder", 2, false), - //new CurrencyInfo("NLG", 528, "", "Netherlands guilder", 2, false), // Gibt es nicht mehr - //new CurrencyInfo("NZD", 554, "", "New Zealand dollar", 2, false), - //new CurrencyInfo("NOK", 578, "NK", "Norwegische Krone", 2, true), - //new CurrencyInfo("OMR", 512, "", "Omani rial", 2, false), - //new CurrencyInfo("PKR", 586, "", "Pakistan rupee", 2, false), - //new CurrencyInfo("PAB", 590, "", "Panamanian balboa", 2, false), - //new CurrencyInfo("PEN", 604, "", "Peruvian sol nuevo", 2, false), - //new CurrencyInfo("PHP", 608, "", "Philippine peso", 2, false), - //new CurrencyInfo("PLN", 985, "Zloty", "Polish zloty", 2, true), - //new CurrencyInfo("PTE", 620, "", "Portuguese escudo", 2, false), // Gibt es nicht mehr - //new CurrencyInfo("QAR", 634, "", "Qatari riyal", 2, false), - //new CurrencyInfo("ROL", 642, "", "Romanian leu", 2, false), - //new CurrencyInfo("RON", 0, "", "Romanian new leu", 2, false), - //new CurrencyInfo("RUB", 643, "Rubel", "Russian ruble", 2, true), - //new CurrencyInfo("SAR", 682, "", "Saudi riyal", 2, false), - //new CurrencyInfo("SGD", 702, "", "Singapore dollar", 2, false), - //new CurrencyInfo("SKK", 703, "", "Slovak koruna", 2, false), - //new CurrencyInfo("SIT", 705, "", "Slovenia tolar", 2, false), - //new CurrencyInfo("ZAR", 710, "Rand", "South African rand", 2, true), - //new CurrencyInfo("ZAL", 991, "", "South African rand (financial)", 2, false), // Gibt es nicht mehr - //new CurrencyInfo("KRW", 410, "", "South Korean won", 2, false), - //new CurrencyInfo("ESP", 724, "", "Spanish peseta", 2, false), // Gibt es nicht mehr - //new CurrencyInfo("XDR", 960, "", "Special Drawing Right", 2, false), - //new CurrencyInfo("LKR", 144, "", "Sri Lankan rupee", 2, false), - //new CurrencyInfo("SEK", 752, "SK", "Schwedische Krone", 2, true), - //new CurrencyInfo("CHF", 756, "Franken", "Swiss franc", 2, true), - //new CurrencyInfo("TWD", 901, "", "Taiwan new dollar", 2, false), - //new CurrencyInfo("THB", 764, "", "Thai baht", 2, false), - //new CurrencyInfo("TTD", 780, "", "Trinidad and Tobago dollar", 2, false), - //new CurrencyInfo("TND", 788, "", "Tunisian dinar", 2, false), - //new CurrencyInfo("TRL", 792, "", "Turkish lira", 2, false), // Gibt es nicht mehr - //new CurrencyInfo("TRY", 949, "", "Turkish new lira", 2, true), - //new CurrencyInfo("AED", 784, "", "U.A.E. dirham", 2, false), - //new CurrencyInfo("USD", 840, "$", "US Dollar", 2, true), - //new CurrencyInfo("VEB", 862, "", "Venezuelan bolivar", 2, false) - - - #endregion - - } - -} diff --git a/src/Libraries/SmartStore.Core/Financial/Money.cs b/src/Libraries/SmartStore.Core/Financial/Money.cs deleted file mode 100644 index 9166aaa4c1..0000000000 --- a/src/Libraries/SmartStore.Core/Financial/Money.cs +++ /dev/null @@ -1,652 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Globalization; - -namespace SmartStore.Financial -{ - - [Serializable] - public struct Money : IComparable, IComparable, IFormattable, IConvertible - { - #region Fields - - public static readonly Money Zero = new Money(0, Currency.Empty); - - #endregion - - #region Ctor - - public Money(decimal amount) - : this(amount, Currency.Default) - { - } - - public Money(decimal amount, string currencyISOCode) - : this(amount, Currency.GetCurrency(currencyISOCode)) - { - } - - //public Money(decimal amount, int currencyCode) - //{ - // // TODO - //} - - public Money(decimal amount, Currency currency) - : this() - { - this.Value = amount; - this.Currency = currency; - } - - public Money(Money money) - : this() - { - this.Value = money.Value; - this.Currency = money.Currency; - } - - #endregion - - #region Properties - - public decimal Value { get; private set; } - - public Currency Currency { get; private set; } - - #endregion - - #region Methods - - public override int GetHashCode() - { - if (this.Value == 0) - return 0; - - return this.Value.GetHashCode() | this.Currency.GetHashCode(); - } - - public override bool Equals(object obj) - { - if (obj == null) - return false; - - if (obj.GetType() != typeof(Money)) - return false; - - Money other = (Money)obj; - - if (other.Value == 0 && this.Value == 0) - return true; - - return other.Value == this.Value && other.Currency == this.Currency; - } - - public int CompareTo(Money other) - { - return ((IComparable)this).CompareTo(other); - } - - int IComparable.CompareTo(object obj) - { - if (obj == null || !(obj is Money)) - return 1; - - Money other = (Money)obj; - - if (this.Value == other.Value) - return 0; - if (this.Value < other.Value) - return -1; - return 1; - } - - private static void GuardCurrenciesAreEqual(Money a, Money b) - { - if (a.Currency != b.Currency) - throw new InvalidOperationException("Cannot operate on money values with different currencies."); - } - - #endregion - - #region ToString - - /// - /// Returns a string-representation of the money value. - /// - /// The string value of the money. - public override string ToString() - { - return this.ToString("C", null, false); - } - - public string ToString(bool useISOCodeAsSymbol) - { - return this.ToString("C", null, useISOCodeAsSymbol); - } - - public string ToString(string format) - { - return this.ToString(format, null, false); - } - - public string ToString(string format, bool useISOCodeAsSymbol) - { - return this.ToString(format, null, useISOCodeAsSymbol); - } - - public string ToString(IFormatProvider provider) - { - return this.ToString("C", provider, false); - } - - public string ToString(IFormatProvider provider, bool useISOCodeAsSymbol) - { - return this.ToString("C", provider, useISOCodeAsSymbol); - } - - public string ToString(string format, IFormatProvider provider) - { - return this.ToString(format, provider, false); - } - - public string ToString(string format, IFormatProvider provider, bool useISOCodeAsSymbol) - { - // REVIEW: was ist mit Digits, Groups, Decimal usw. Müssen wir die nicht von DB oder sonstwo beziehen? - - Guard.ArgumentNotEmpty(format, "format"); - - NumberFormatInfo info = null; - - CultureInfo ci; - - if (provider == null) - provider = CultureInfo.GetCultureInfo(this.Currency.Region.Name).NumberFormat; - - info = provider as NumberFormatInfo; - if (info == null) - { - ci = provider as CultureInfo; - if (ci != null) - info = ci.NumberFormat; - } - - if (info != null) - { - info = (NumberFormatInfo)info.Clone(); - - if (Currency != Currency.Empty) - { - info.CurrencySymbol = useISOCodeAsSymbol ? - Currency.ThreeLetterISOCode : - Currency.Symbol.NullEmpty() ?? Currency.ThreeLetterISOCode; - } - else - { - info.CurrencySymbol = "?"; - } - } - - return Value.ToString(format, info); - } - - #endregion - - #region Implicit/explicit operator overloads - - public static implicit operator Money(byte value) - { - return new Money(System.Convert.ToDecimal(value)); - } - - public static implicit operator Money(int value) - { - return new Money(System.Convert.ToDecimal(value)); - } - - public static implicit operator Money(long value) - { - return new Money(System.Convert.ToDecimal(value)); - } - - public static implicit operator Money(sbyte value) - { - return new Money(System.Convert.ToDecimal(value)); - } - - public static implicit operator Money(short value) - { - return new Money(System.Convert.ToDecimal(value)); - } - - public static implicit operator Money(uint value) - { - return new Money(System.Convert.ToDecimal(value)); - } - - public static implicit operator Money(ulong value) - { - return new Money(System.Convert.ToDecimal(value)); - } - - public static implicit operator Money(ushort value) - { - return new Money(System.Convert.ToDecimal(value)); - } - - public static implicit operator Money(decimal value) - { - return new Money(value); - } - - public static implicit operator Money(double value) - { - return new Money(System.Convert.ToDecimal(value)); - } - - public static implicit operator Money(float value) - { - return new Money(System.Convert.ToDecimal(value)); - } - - public static explicit operator byte(Money money) - { - return System.Convert.ToByte(money.Value); - } - - public static explicit operator decimal(Money money) - { - return money.Value; - } - - public static explicit operator double(Money money) - { - return System.Convert.ToDouble(money.Value); - } - - public static explicit operator float(Money money) - { - return System.Convert.ToSingle(money.Value); - } - - public static explicit operator int(Money money) - { - return System.Convert.ToInt32(money.Value); - } - - public static explicit operator long(Money money) - { - return System.Convert.ToInt64(money.Value); - } - - public static explicit operator sbyte(Money money) - { - return System.Convert.ToSByte(money.Value); - } - - public static explicit operator short(Money money) - { - return System.Convert.ToInt16(money.Value); - } - - public static explicit operator ushort(Money money) - { - return System.Convert.ToUInt16(money.Value); - } - - public static explicit operator uint(Money money) - { - return System.Convert.ToUInt32(money.Value); - } - - public static explicit operator ulong(Money money) - { - return System.Convert.ToUInt64(money.Value); - } - - #endregion - - #region Equality/Comparison - - public static bool operator ==(Money a, Money b) - { - return a.Equals(b); - } - - public static bool operator !=(Money a, Money b) - { - return !a.Equals(b); - } - - public static bool operator >(Money a, Money b) - { - if (b.Value == 0 && a.Value > 0) - return true; - - if (b.Value < 0 && a.Value >= 0) - return true; - - if (a == Money.Zero) - return b.Value < 0; - - if (b == Money.Zero) - return a.Value > 0; - - GuardCurrenciesAreEqual(a, b); - - return a.Value > b.Value; - - } - - public static bool operator <(Money a, Money b) - { - if (a.Value == 0 && b.Value > 0) - return true; - - if (a.Value < 0 && b.Value >= 0) - return true; - - if (b == Money.Zero) - return a.Value < 0; - - if (a == Money.Zero) - return b.Value > 0; - - GuardCurrenciesAreEqual(a, b); - - return a.Value < b.Value; - } - - public static bool operator <=(Money a, Money b) - { - if (a.Value < 0 && b.Value >= 0) - return true; - - if (a.Value == 0 && b.Value >= 0) - return true; - - GuardCurrenciesAreEqual(a, b); - - return a.Value < b.Value || a.Equals(b); - - } - - public static bool operator >=(Money a, Money b) - { - if (b.Value < 0 && a.Value >= 0) - return true; - - if (b.Value == 0 && a.Value >= 0) - return true; - - GuardCurrenciesAreEqual(a, b); - - return a.Value > b.Value || a.Equals(b); - - } - - #endregion - - #region Addition - - public static Money operator ++(Money a) - { - a.Value++; - - return a; - } - - public static Money operator +(Money a, Money b) - { - if (a == Money.Zero) - return b; - - if (b == Money.Zero) - return a; - - GuardCurrenciesAreEqual(a, b); - - return new Money(a.Value + b.Value, a.Currency); - } - - #endregion - - #region Subtraction - - public static Money operator --(Money a) - { - a.Value--; - - return a; - } - - public static Money operator -(Money a, Money b) - { - if (a == Money.Zero) - return new Money(0 - b.Value, a.Currency); - - if (b == Money.Zero) - return a; - - GuardCurrenciesAreEqual(a, b); - - return new Money(a.Value - b.Value, a.Currency); - } - - #endregion - - #region Multiplication - - public static Money operator *(Money a, Money b) - { - GuardCurrenciesAreEqual(a, b); - return new Money(a.Value * b.Value, a.Currency); - } - - public static Money operator *(Money a, decimal b) - { - return new Money(a.Value * b, a.Currency); - } - - public static Money operator *(decimal a, Money b) - { - return new Money(b.Value * a, b.Currency); - } - - public static Money operator *(Money a, int b) - { - return new Money(a.Value * b, a.Currency); - } - - public static Money operator *(int a, Money b) - { - return new Money(b.Value * a, b.Currency); - } - - #endregion - - #region Division - - public static Money operator /(Money a, Money b) - { - GuardCurrenciesAreEqual(a, b); - return new Money(a.Value / b.Value, a.Currency); - } - - public static Money operator /(Money a, decimal b) - { - return new Money(a.Value / b, a.Currency); - } - - public static Money operator /(decimal a, Money b) - { - return new Money(a / b.Value, b.Currency); - } - - public static Money operator /(Money a, int b) - { - return new Money(a.Value / b, a.Currency); - } - - public static Money operator /(int a, Money b) - { - return new Money(a / b.Value, b.Currency); - } - - #endregion - - #region Parse - - // [...] TODO - - #endregion - - #region Exchange & Math - - /// - /// Gets the ratio of one money to another. - /// - /// The numerator of the operation. - /// The denominator of the operation. - /// A decimal from 0.0 to 1.0 of the ratio between the two money values. - public static decimal GetRatio(Money numerator, Money denominator) - { - if (numerator == Money.Zero) - return 0; - - if (denominator == Money.Zero) - throw new DivideByZeroException("Attempted to divide by zero!"); - - GuardCurrenciesAreEqual(numerator, denominator); - - return numerator.Value / denominator.Value; - } - - /// - /// Gets the smallest money, given the two values. - /// - /// The first money to compare. - /// The second money to compare. - /// The smallest money value of the arguments. - public static Money Min(Money m1, Money m2) - { - if (m1 == m2) // This will check currency. - return m1; - - return new Money(Math.Min(m1.Value, m2.Value), m1.Currency); - } - - /// - /// Gets the largest money, given the two values. - /// - /// The first money to compare. - /// The second money to compare. - /// The largest money value of the arguments. - public static Money Max(Money m1, Money m2) - { - if (m1 == m2) // This will check currency. - return m1; - - return new Money(Math.Max(m1.Value, m2.Value), m1.Currency); - } - - /// - /// Gets the absolute value of the . - /// - /// The value of money to convert. - /// The money value as an absolute value. - public static Money Abs(Money value) - { - return new Money(Math.Abs(value.Value), value.Currency); - } - - #endregion - - #region IConvertible Members - - public TypeCode GetTypeCode() - { - return TypeCode.Decimal; - } - - public bool ToBoolean(IFormatProvider provider) - { - throw Error.InvalidCast(typeof(Money), typeof(bool)); - } - - public byte ToByte(IFormatProvider provider) - { - return (byte)this.Value; - } - - public char ToChar(IFormatProvider provider) - { - throw Error.InvalidCast(typeof(Money), typeof(bool)); - } - - public DateTime ToDateTime(IFormatProvider provider) - { - throw Error.InvalidCast(typeof(Money), typeof(bool)); - } - - public decimal ToDecimal(IFormatProvider provider) - { - return this.Value; - } - - public double ToDouble(IFormatProvider provider) - { - return (double)this.Value; - } - - public short ToInt16(IFormatProvider provider) - { - return (short)this.Value; - } - - public int ToInt32(IFormatProvider provider) - { - return (int)this.Value; - } - - public long ToInt64(IFormatProvider provider) - { - return (long)this.Value; - } - - public sbyte ToSByte(IFormatProvider provider) - { - return (sbyte)this.Value; - } - - public float ToSingle(IFormatProvider provider) - { - return (float)this.Value; - } - - public object ToType(Type conversionType, IFormatProvider provider) - { - return System.Convert.ChangeType(this.Value, conversionType, provider); - } - - public ushort ToUInt16(IFormatProvider provider) - { - return (ushort)this.Value; - } - - public uint ToUInt32(IFormatProvider provider) - { - return (uint)this.Value; - } - - public ulong ToUInt64(IFormatProvider provider) - { - return (ulong)this.Value; - } - - #endregion - - } - -} diff --git a/src/Libraries/SmartStore.Core/Html/CodeFormatter/CodeFormatHelper.cs b/src/Libraries/SmartStore.Core/Html/CodeFormatter/CodeFormatHelper.cs index d2de15007c..a840a6ed53 100644 --- a/src/Libraries/SmartStore.Core/Html/CodeFormatter/CodeFormatHelper.cs +++ b/src/Libraries/SmartStore.Core/Html/CodeFormatter/CodeFormatHelper.cs @@ -17,31 +17,6 @@ public partial class CodeFormatHelper #region Utilities - /// - /// Code evaluator method - /// - /// Match - /// Formatted text - private static string CodeEvaluator(Match match) - { - if (!match.Success) - return match.Value; - - var options = new HighlightOptions(); - - options.Language = match.Groups["lang"].Value; - options.Code = match.Groups["code"].Value; - options.DisplayLineNumbers = match.Groups["linenumbers"].Value == "on" ? true : false; - options.Title = match.Groups["title"].Value; - options.AlternateLineNumbers = match.Groups["altlinenumbers"].Value == "on" ? true : false; - - string result = match.Value.Replace(match.Groups["begin"].Value, ""); - result = result.Replace(match.Groups["end"].Value, ""); - result = Highlight(options, result); - return result; - - } - /// /// Code evaluator method /// diff --git a/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs b/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs index 51d36bfc7c..f5f5527993 100644 --- a/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs +++ b/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs @@ -44,9 +44,9 @@ private static string EnsureOnlyAllowedHtml(string text) private static bool IsValidTag(string tag, string tags) { string[] allowedTags = tags.Split(','); - if (tag.IndexOf("javascript") >= 0) return false; - if (tag.IndexOf("vbscript") >= 0) return false; - if (tag.IndexOf("onclick") >= 0) return false; + if (tag.IndexOf("javascript", StringComparison.OrdinalIgnoreCase) >= 0) return false; + if (tag.IndexOf("vbscript", StringComparison.OrdinalIgnoreCase) >= 0) return false; + if (tag.IndexOf("onclick", StringComparison.OrdinalIgnoreCase) >= 0) return false; var endchars = new char[] { ' ', '>', '/', '\t' }; @@ -54,12 +54,7 @@ private static bool IsValidTag(string tag, string tags) if (pos > 0) tag = tag.Substring(0, pos); if (tag[0] == '/') tag = tag.Substring(1); - foreach (string aTag in allowedTags) - { - if (tag == aTag) return true; - } - - return false; + return allowedTags.Any(aTag => tag == aTag); } #endregion @@ -87,12 +82,12 @@ public static string FormatText(string text, bool stripTags, { if (stripTags) { - text = HtmlUtils.StripTags(text); + text = StripTags(text); } if (allowHtml) { - text = HtmlUtils.EnsureOnlyAllowedHtml(text); + text = EnsureOnlyAllowedHtml(text); } else { @@ -101,7 +96,7 @@ public static string FormatText(string text, bool stripTags, if (convertPlainTextToHtml) { - text = HtmlUtils.ConvertPlainTextToHtml(text); + text = ConvertPlainTextToHtml(text); } if (allowBBCode) @@ -208,7 +203,6 @@ public static string ConvertHtmlToPlainText(string text, /// Converts an attribute string spec to a html table putting each new line in a TR and each attr name/value in a TD. /// /// The text to convert - /// A value indicating whether to encode (HTML) values /// The formatted (html) string public static string ConvertPlainTextToTable(string text, string tableCssClass = null) { diff --git a/src/Libraries/SmartStore.Core/Html/ResolveLinksHelper.cs b/src/Libraries/SmartStore.Core/Html/ResolveLinksHelper.cs index d0e72938fe..c58f9540ba 100644 --- a/src/Libraries/SmartStore.Core/Html/ResolveLinksHelper.cs +++ b/src/Libraries/SmartStore.Core/Html/ResolveLinksHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text.RegularExpressions; @@ -23,6 +24,8 @@ public partial class ResolveLinksHelper /// /// Shortens any absolute URL to a specified maximum length /// + [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")] + [SuppressMessage("ReSharper", "StringLastIndexOfIsCultureSpecific.1")] private static string ShortenUrl(string url, int max) { if (url.Length <= max) diff --git a/src/Libraries/SmartStore.Core/IHideObjectMembers.cs b/src/Libraries/SmartStore.Core/IHideObjectMembers.cs index 7228c572b6..97947ea881 100644 --- a/src/Libraries/SmartStore.Core/IHideObjectMembers.cs +++ b/src/Libraries/SmartStore.Core/IHideObjectMembers.cs @@ -1,13 +1,9 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; namespace SmartStore { - [EditorBrowsable(EditorBrowsableState.Never)] public interface IHideObjectMembers { diff --git a/src/Libraries/SmartStore.Core/IO/Media/FileSystemStorageProvider.cs b/src/Libraries/SmartStore.Core/IO/Media/FileSystemStorageProvider.cs index 8a047c9110..636c7fbee5 100644 --- a/src/Libraries/SmartStore.Core/IO/Media/FileSystemStorageProvider.cs +++ b/src/Libraries/SmartStore.Core/IO/Media/FileSystemStorageProvider.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Web.Hosting; @@ -12,9 +13,10 @@ public class FileSystemStorageProvider : IStorageProvider private readonly string _storagePath; private readonly string _publicPath; - public FileSystemStorageProvider(FileSystemSettings settings) + [SuppressMessage("ReSharper", "PossibleNullReferenceException")] + public FileSystemStorageProvider(FileSystemSettings settings) { - string mediaPath = CommonHelper.MapPath("~/Media/", false); + var mediaPath = CommonHelper.MapPath("~/Media/", false); _storagePath = Path.Combine(mediaPath, settings.DirectoryName); var appPath = ""; @@ -29,11 +31,13 @@ public FileSystemStorageProvider(FileSystemSettings settings) _publicPath = appPath + "Media/" + settings.DirectoryName + "/"; } - string Map(string path) { + private string Map(string path) + { return string.IsNullOrEmpty(path) ? _storagePath : Path.Combine(_storagePath, path); } - static string Fix(string path) { + static string Fix(string path) + { return string.IsNullOrEmpty(path) ? "" : Path.DirectorySeparatorChar != '/' diff --git a/src/Libraries/SmartStore.Core/IO/Media/IStorageProvider.cs b/src/Libraries/SmartStore.Core/IO/Media/IStorageProvider.cs index a43de9e8d6..38a8958457 100644 --- a/src/Libraries/SmartStore.Core/IO/Media/IStorageProvider.cs +++ b/src/Libraries/SmartStore.Core/IO/Media/IStorageProvider.cs @@ -1,4 +1,4 @@ - +using System; using System.Collections.Generic; namespace SmartStore.Core.IO.Media @@ -48,12 +48,12 @@ public interface IStorageProvider /// If the folder doesn't exist. void DeleteFolder(string path); - /// - /// Renames a folder in the storage provider. - /// - /// The relative path to the folder to be renamed. - /// The relative path to the new folder. - void RenameFolder(string path, string newPath); + /// + /// Renames a folder in the storage provider. + /// + /// The relative path to the folder to be renamed. + /// The relative path to the new folder. + void RenameFolder(string path, string newPath); /// /// Deletes a file in the storage provider. @@ -62,12 +62,12 @@ public interface IStorageProvider /// If the file doesn't exist. void DeleteFile(string path); - /// - /// Renames a file in the storage provider. - /// - /// The relative path to the file to be renamed. - /// The relative path to the new file. - void RenameFile(string path, string newPath); + /// + /// Renames a file in the storage provider. + /// + /// The relative path to the file to be renamed. + /// The relative path to the new file. + void RenameFile(string path, string newPath); /// /// Creates a file in the storage provider. diff --git a/src/Libraries/SmartStore.Core/IO/MimeTypes.cs b/src/Libraries/SmartStore.Core/IO/MimeTypes.cs index afecc35c0c..e44c81b0a2 100644 --- a/src/Libraries/SmartStore.Core/IO/MimeTypes.cs +++ b/src/Libraries/SmartStore.Core/IO/MimeTypes.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Security; using System.Text; @@ -24,13 +25,14 @@ public static string MapNameToMimeType(string fileNameOrExtension) /// /// The mime type /// The corresponding file extension (without dot) + [SuppressMessage("ReSharper", "RedundantAssignment")] public static string MapMimeTypeToExtension(string mimeType) { if (mimeType.IsEmpty()) return null; return _mimeMap.GetOrAdd(mimeType, k => { - string result = null; + string result; try { diff --git a/src/Libraries/SmartStore.Core/IO/VirtualPath/DefaultVirtualPathProvider.cs b/src/Libraries/SmartStore.Core/IO/VirtualPath/DefaultVirtualPathProvider.cs index 5e2164b149..102649e1b2 100644 --- a/src/Libraries/SmartStore.Core/IO/VirtualPath/DefaultVirtualPathProvider.cs +++ b/src/Libraries/SmartStore.Core/IO/VirtualPath/DefaultVirtualPathProvider.cs @@ -92,7 +92,7 @@ public bool IsMalformedVirtualPath(string virtualPath) if (string.IsNullOrEmpty(virtualPath)) return true; - if (virtualPath.IndexOf("..") >= 0) + if (virtualPath.IndexOf("..", StringComparison.OrdinalIgnoreCase) >= 0) { virtualPath = virtualPath.Replace(Path.DirectorySeparatorChar, '/'); string rootPrefix = virtualPath.StartsWith("~/") ? "~/" : virtualPath.StartsWith("/") ? "/" : ""; diff --git a/src/Libraries/SmartStore.Core/IWebHelper.cs b/src/Libraries/SmartStore.Core/IWebHelper.cs index 03b4f128c8..2d089bd64d 100644 --- a/src/Libraries/SmartStore.Core/IWebHelper.cs +++ b/src/Libraries/SmartStore.Core/IWebHelper.cs @@ -13,11 +13,21 @@ public partial interface IWebHelper /// URL referrer string GetUrlReferrer(); - /// - /// Get context IP address - /// - /// URL referrer - string GetCurrentIpAddress(); + /// + /// Gets a unique client identifier + /// + /// A unique identifier + /// + /// The client identifier is a hashed combination of client ip address and user agent. + /// This method returns null if IP or user agent (or both) cannot be determined. + /// + string GetClientIdent(); + + /// + /// Get context IP address + /// + /// URL referrer + string GetCurrentIpAddress(); /// /// Gets this page name diff --git a/src/Libraries/SmartStore.Core/IWorkContext.cs b/src/Libraries/SmartStore.Core/IWorkContext.cs index d3a54cf702..e137ac98bd 100644 --- a/src/Libraries/SmartStore.Core/IWorkContext.cs +++ b/src/Libraries/SmartStore.Core/IWorkContext.cs @@ -59,10 +59,5 @@ public interface IWorkContext /// Gets or sets a value indicating whether we're in admin area /// bool IsAdmin { get; set; } - - ///// - ///// Gets a value indicating whether we're in the public shop - ///// - //bool IsPublic { get; } - } + } } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/AppDomainTypeFinder.cs b/src/Libraries/SmartStore.Core/Infrastructure/AppDomainTypeFinder.cs index 8e2e1b1498..79a7a41f22 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/AppDomainTypeFinder.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/AppDomainTypeFinder.cs @@ -21,12 +21,12 @@ public class AppDomainTypeFinder : ITypeFinder private static object s_lock = new object(); - private string _assemblySkipLoadingPattern = @"^System|^mscorlib|^Microsoft|^CppCodeProvider|^VJSharpCodeProvider|^WebDev|^Nuget|^Castle|^Iesi|^log4net|^Autofac|^AutoMapper|^EntityFramework|^EPPlus|^Fasterflect|^nunit|^TestDriven|^MbUnit|^Rhino|^QuickGraph|^TestFu|^Telerik|^Antlr3|^Recaptcha|^FluentValidation|^ImageResizer|^itextsharp|^MiniProfiler|^Newtonsoft|^Pandora|^WebGrease|^Noesis|^DotNetOpenAuth|^Facebook|^LinqToTwitter|^PerceptiveMCAPI|^CookComputing|^GCheckout|^Mono\.Math|^Org\.Mentalis|^App_Web|^BundleTransformer|^ClearScript|^JavaScriptEngineSwitcher|^MsieJavaScriptEngine|^Glimpse|^Ionic|^App_GlobalResources|^AjaxMin|^MaxMind|^NReco|^OffAmazonPayments|^UAParser"; + private string _assemblySkipLoadingPattern = @"^System|^mscorlib|^Microsoft|^CppCodeProvider|^VJSharpCodeProvider|^WebDev|^Nuget|^Castle|^Iesi|^log4net|^Autofac|^AutoMapper|^EntityFramework|^EPPlus|^nunit|^TestDriven|^MbUnit|^Rhino|^QuickGraph|^TestFu|^Telerik|^Antlr3|^Recaptcha|^FluentValidation|^ImageResizer|^itextsharp|^MiniProfiler|^Newtonsoft|^Pandora|^WebGrease|^Noesis|^DotNetOpenAuth|^Facebook|^LinqToTwitter|^PerceptiveMCAPI|^CookComputing|^GCheckout|^Mono\.Math|^Org\.Mentalis|^App_Web|^BundleTransformer|^ClearScript|^JavaScriptEngineSwitcher|^MsieJavaScriptEngine|^Glimpse|^Ionic|^App_GlobalResources|^AjaxMin|^MaxMind|^NReco|^OffAmazonPayments|^UAParser"; private string _assemblyRestrictToLoadingPattern = ".*"; private readonly IDictionary _assemblyMatchTable = new Dictionary(); - private Regex _assemblySkipLoadingRegex = null; - private Regex _assemblyRestrictToLoadingRegex = null; + private Regex _assemblySkipLoadingRegex; + private Regex _assemblyRestrictToLoadingRegex; private bool _ignoreReflectionErrors = true; private bool _loadAppDomainAssemblies = true; @@ -34,15 +34,6 @@ public class AppDomainTypeFinder : ITypeFinder #endregion - #region Constructors - - /// Creates a new instance of the AppDomainTypeFinder. - public AppDomainTypeFinder() - { - } - - #endregion - #region Properties /// The app domain to look for types in. @@ -102,16 +93,6 @@ public string AssemblyRestrictToLoadingPattern #endregion - #region Internal Attributed Assembly class - - private class AttributedAssembly - { - internal Assembly Assembly { get; set; } - internal Type PluginAttributeType { get; set; } - } - - #endregion - #region ITypeFinder public IEnumerable FindClassesOfType(Type assignTypeFrom, IEnumerable assemblies, bool onlyConcreteClasses = true) @@ -174,16 +155,6 @@ public IEnumerable FindClassesOfType(Type assignTypeFrom, IEnumerable - /// Caches attributed assembly information so they don't have to be re-read - /// - private readonly List _attributedAssemblies = new List(); - - /// - /// Caches the assembly attributes that have been searched for - /// - private readonly List _assemblyAttributesSearched = new List(); - /// /// Gets the assemblies related to the current implementation. /// diff --git a/src/Libraries/SmartStore.Core/Infrastructure/ComparableObject.cs b/src/Libraries/SmartStore.Core/Infrastructure/ComparableObject.cs index 7a0c3d9dbb..a53cc21140 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/ComparableObject.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/ComparableObject.cs @@ -3,38 +3,22 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; -using System.ComponentModel; -using Fasterflect; +using SmartStore.Utilities; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using SmartStore.ComponentModel; namespace SmartStore { - /// /// Provides a standard base class for facilitating sophisticated comparison of objects. /// [Serializable] public abstract class ComparableObject { - /// - /// To help ensure hashcode uniqueness, a carefully selected random number multiplier - /// is used within the calculation. Goodrich and Tamassia's Data Structures and - /// Algorithms in Java asserts that 31, 33, 37, 39 and 41 will produce the fewest number - /// of collissions. See http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/ - /// for more information. - /// - protected const int HashMultiplier = 31; + private readonly HashSet _extraSignatureProperties = new HashSet(StringComparer.OrdinalIgnoreCase); - private readonly List _extraSignatureProperties = new List(); - - /// - /// This static member caches the domain signature properties to avoid looking them up for - /// each instance of the same type. - /// - /// A description of the ThreadStatic attribute may be found at - /// http://www.dotnetjunkies.com/WebLog/chris.taylor/archive/2005/08/18/132026.aspx - /// - [ThreadStatic] - private static IDictionary> s_signatureProperties; + private static readonly ConcurrentDictionary _signaturePropertyNames = new ConcurrentDictionary(); public override bool Equals(object obj) { @@ -53,28 +37,32 @@ public override bool Equals(object obj) /// if at all, in an object's lifetime; it's important that properties are carefully /// selected which truly represent the signature of an object. /// + [SuppressMessage("ReSharper", "BaseObjectGetHashCodeCallInGetHashCode")] + [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] public override int GetHashCode() { unchecked { - var signatureProperties = GetSignatureProperties(); + var signatureProperties = GetSignatureProperties().ToArray(); Type t = this.GetType(); - // It's possible for two objects to return the same hash code based on - // identically valued properties, even if they're of two different types, - // so we include the object's type in the hash calculation - int hashCode = t.GetHashCode(); + var combiner = HashCodeCombiner.Start(); - foreach (var pi in signatureProperties) + // It's possible for two objects to return the same hash code based on + // identically valued properties, even if they're of two different types, + // so we include the object's type in the hash calculation + combiner.Add(t.GetHashCode()); + + foreach (var prop in signatureProperties) { - object value = this.GetPropertyValue(pi.Name); // pi.GetValue(this); + var value = prop.GetValue(this); if (value != null) - hashCode = (hashCode * HashMultiplier) ^ value.GetHashCode(); + combiner.Add(value.GetHashCode()); } - if (signatureProperties.Any()) - return hashCode; + if (signatureProperties.Length > 0) + return combiner.CombinedHash; // If no properties were flagged as being part of the signature of the object, // then simply return the hashcode of the base object as the hashcode. @@ -102,8 +90,8 @@ protected virtual bool HasSameSignatureAs(ComparableObject compareTo) foreach (var pi in signatureProperties) { - object thisValue = this.GetPropertyValue(pi.Name); - object thatValue = compareTo.GetPropertyValue(pi.Name); + object thisValue = pi.GetValue(this); + object thatValue = pi.GetValue(compareTo); if (thisValue == null && thatValue == null) continue; @@ -123,56 +111,46 @@ protected virtual bool HasSameSignatureAs(ComparableObject compareTo) /// /// - public IEnumerable GetSignatureProperties() + public IEnumerable GetSignatureProperties() { - IEnumerable properties; - - // Init the signaturePropertiesDictionary here due to reasons described at - // http://blogs.msdn.com/jfoscoding/archive/2006/07/18/670497.aspx - if (s_signatureProperties == null) - s_signatureProperties = new Dictionary>(); - - var t = GetType(); - - if (s_signatureProperties.TryGetValue(t, out properties)) - return properties; - - return (s_signatureProperties[t] = GetSignaturePropertiesCore()); + var type = GetType(); + var propertyNames = GetSignaturePropertyNamesCore(); + + foreach (var name in propertyNames) + { + var fastProperty = FastProperty.GetProperty(type, name); + if (fastProperty != null) + { + yield return fastProperty; + } + } } /// /// Enforces the template method pattern to have child objects determine which specific /// properties should and should not be included in the object signature comparison. /// - protected virtual IEnumerable GetSignaturePropertiesCore() + protected virtual string[] GetSignaturePropertyNamesCore() { - Type t = this.GetType(); - //var properties = TypeDescriptor.GetProvider(t).GetTypeDescriptor(t) - // .GetPropertiesWith(); + Type type = this.GetType(); + string[] names; - //if (_extraSignatureProperties.Count > 0) - //{ - // properties = properties.Union(_extraSignatureProperties); - //} + if (!_signaturePropertyNames.TryGetValue(type, out names)) + { + names = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => Attribute.IsDefined(p, typeof(ObjectSignatureAttribute), true)) + .Select(p => p.Name) + .ToArray(); - //return new PropertyDescriptorCollection(properties.ToArray(), true); + _signaturePropertyNames.TryAdd(type, names); + } - var properties = t.GetProperties() - .Where(p => Attribute.IsDefined(p, typeof(ObjectSignatureAttribute), true)); + if (_extraSignatureProperties.Count == 0) + { + return names; + } - return properties.Union(_extraSignatureProperties).ToList(); - } - - /// - /// Adds an extra property to the type specific signature properties list. - /// - /// The property to add. - /// Both lists are unioned, so - /// that no duplicates can occur within the global descriptor collection. - protected void RegisterSignatureProperty(PropertyInfo propertyInfo) - { - Guard.ArgumentNotNull(() => propertyInfo); - _extraSignatureProperties.Add(propertyInfo); + return names.Union(_extraSignatureProperties).ToArray(); } /// @@ -185,13 +163,7 @@ protected void RegisterSignatureProperty(string propertyName) { Guard.ArgumentNotEmpty(() => propertyName); - Type t = GetType(); - - var pi = t.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance); - if (pi == null) - throw Error.Argument("propertyName", "Could not find property '{0}' on type '{1}'.", propertyName, t); - - RegisterSignatureProperty(pi); + _extraSignatureProperties.Add(propertyName); } } @@ -214,7 +186,7 @@ protected void RegisterSignatureProperty(Expression> expression) { Guard.ArgumentNotNull(() => expression); - base.RegisterSignatureProperty(expression.ExtractPropertyInfo()); + base.RegisterSignatureProperty(expression.ExtractPropertyInfo().Name); } public virtual bool Equals(T other) diff --git a/src/Libraries/SmartStore.Core/Infrastructure/DateRange.cs b/src/Libraries/SmartStore.Core/Infrastructure/DateRange.cs deleted file mode 100644 index bb331223be..0000000000 --- a/src/Libraries/SmartStore.Core/Infrastructure/DateRange.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace SmartStore -{ - - public class DateRange : ValueObject, ICloneable - { - - public DateRange() - { - } - - public DateRange(DateTime to) - : this(default(DateTime), to) - { - } - - public DateRange(DateTime from, DateTime to) - { - DateFrom = from; - DateTo = to; - } - - [ObjectSignature] - public DateTime? DateFrom { get; set; } - - [ObjectSignature] - public DateTime? DateTo { get; set; } - - public bool IsDefined - { - get { return DateFrom.HasValue || DateTo.HasValue; } - } - - //public override int GetHashCode() - //{ - // return SystemUtil.GetHashCode(this.DateFrom, this.DateTo); - //} - - //public override bool Equals(object obj) - //{ - // if (ReferenceEquals(null, obj)) return false; - // if (ReferenceEquals(this, obj)) return true; - - // DateRange other = (DateRange)obj; - // return this.DateFrom.Equals(other.DateFrom) && this.DateTo.Equals(other.DateTo); - //} - - #region ICloneable Members - - public DateRange Clone() - { - return new DateRange() - { - DateFrom = this.DateFrom, - DateTo = this.DateTo - }; - } - - object ICloneable.Clone() - { - return this.Clone(); - } - - #endregion - - } - -} diff --git a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs index e15832df7f..f39bbeedeb 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/ContainerManager.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Reflection; using Autofac; using Autofac.Builder; +using SmartStore.ComponentModel; using SmartStore.Core.Caching; namespace SmartStore.Core.Infrastructure.DependencyManagement @@ -10,6 +13,7 @@ namespace SmartStore.Core.Infrastructure.DependencyManagement public class ContainerManager { private readonly IContainer _container; + private readonly ConcurrentDictionary _cachedActivators = new ConcurrentDictionary(); public ContainerManager(IContainer container) { @@ -61,30 +65,67 @@ public T ResolveUnregistered(ILifetimeScope scope = null) where T : class public object ResolveUnregistered(Type type, ILifetimeScope scope = null) { - var constructors = type.GetConstructors(); - foreach (var constructor in constructors) - { - try - { - var parameters = constructor.GetParameters(); - var parameterInstances = new List(); - foreach (var parameter in parameters) - { - var service = Resolve(parameter.ParameterType, scope); - if (service == null) - throw new SmartException("Unkown dependency"); - parameterInstances.Add(service); - } - return Activator.CreateInstance(type, parameterInstances.ToArray()); - } - catch (SmartException) - { + FastActivator activator; + object[] parameterInstances = null; + + if (!_cachedActivators.TryGetValue(type, out activator)) + { + var constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + foreach (var constructor in constructors) + { + var parameterTypes = constructor.GetParameters().Select(p => p.ParameterType).ToArray(); + if (TryResolveAll(parameterTypes, out parameterInstances, scope)) + { + activator = new FastActivator(constructor); + _cachedActivators.TryAdd(type, activator); + break; + } + } + } + if (activator != null) + { + if (parameterInstances == null) + { + TryResolveAll(activator.ParameterTypes, out parameterInstances, scope); } - } - throw new SmartException("No contructor was found that had all the dependencies satisfied."); + if (parameterInstances != null) + { + return activator.Activate(parameterInstances); + } + } + + throw new SmartException("No constructor for {0} was found that had all the dependencies satisfied.".FormatInvariant(type.Name.NaIfEmpty())); } + private bool TryResolveAll(Type[] types, out object[] instances, ILifetimeScope scope = null) + { + instances = null; + + try + { + var instances2 = new object[types.Length]; + + for (int i = 0; i < types.Length; i++) + { + var service = Resolve(types[i], scope); + if (service == null) + { + return false; + } + + instances2[i] = service; + } + + instances = instances2; + return true; + } + catch + { + return false; + } + } + public bool TryResolve(Type serviceType, ILifetimeScope scope, out object instance) { return (scope ?? Scope()).TryResolve(serviceType, out instance); diff --git a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeAccessor.cs b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeAccessor.cs index 79626e5775..03e0fbbba7 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeAccessor.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/DependencyManagement/DefaultLifetimeScopeAccessor.cs @@ -71,17 +71,17 @@ public ILifetimeScope GetLifetimeScope(Action configurationAct return scope; } - private void OnScopeBeginning(object sender, LifetimeScopeBeginningEventArgs args) - { - bool isWeb = System.Web.HttpContext.Current != null; - Debug.WriteLine("Scope Begin, Web: " + isWeb); - } + //private void OnScopeBeginning(object sender, LifetimeScopeBeginningEventArgs args) + //{ + // bool isWeb = System.Web.HttpContext.Current != null; + // Debug.WriteLine("Scope Begin, Web: " + isWeb); + //} - private void OnScopeEnding(object sender, LifetimeScopeEndingEventArgs args) - { - bool isWeb = System.Web.HttpContext.Current != null; - Debug.WriteLine("Scope END, Web: " + isWeb); - } + //private void OnScopeEnding(object sender, LifetimeScopeEndingEventArgs args) + //{ + // bool isWeb = System.Web.HttpContext.Current != null; + // Debug.WriteLine("Scope END, Web: " + isWeb); + //} private ILifetimeScope BeginLifetimeScope(Action configurationAction) { diff --git a/src/Libraries/SmartStore.Core/Infrastructure/DisposableObject.cs b/src/Libraries/SmartStore.Core/Infrastructure/DisposableObject.cs index 1c7aa25811..e3b01f2e42 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/DisposableObject.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/DisposableObject.cs @@ -8,18 +8,18 @@ namespace SmartStore public abstract class DisposableObject : IDisposable { - private bool _disposed = false; + private bool _isDisposed; public virtual bool IsDisposed { [DebuggerStepThrough] - get { return _disposed; } + get { return _isDisposed; } } [DebuggerStepThrough] protected void CheckDisposed() { - if (IsDisposed) + if (_isDisposed) { throw Error.ObjectDisposed(GetType().FullName); } @@ -28,7 +28,7 @@ protected void CheckDisposed() [DebuggerStepThrough] protected void CheckDisposed(string errorMessage) { - if (IsDisposed) + if (_isDisposed) { throw Error.ObjectDisposed(GetType().FullName, errorMessage); } @@ -37,17 +37,20 @@ protected void CheckDisposed(string errorMessage) [DebuggerStepThrough] public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); + if (!_isDisposed) + { + Dispose(true); + GC.SuppressFinalize(this); + } } protected void Dispose(bool disposing) { - if (!_disposed) + if (!_isDisposed) { OnDispose(disposing); } - _disposed = true; + _isDisposed = true; } protected abstract void OnDispose(bool disposing); diff --git a/src/Libraries/SmartStore.Core/Infrastructure/EnumFriendlyNameAttribute.cs b/src/Libraries/SmartStore.Core/Infrastructure/EnumFriendlyNameAttribute.cs deleted file mode 100644 index 1be47733be..0000000000 --- a/src/Libraries/SmartStore.Core/Infrastructure/EnumFriendlyNameAttribute.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace SmartStore -{ - - /// - /// Provides a friendly display name for an enumerated type value. - /// - [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] - public sealed class EnumFriendlyNameAttribute : Attribute - { - public EnumFriendlyNameAttribute(string friendlyName) - { - FriendlyName = friendlyName; - } - - public string FriendlyName { get; private set; } - } - - /// - /// Provides a description for an enumerated type. - /// - [AttributeUsage(AttributeTargets.Enum | AttributeTargets.Field, AllowMultiple = false)] - public sealed class EnumDescriptionAttribute : Attribute - { - - /// - /// Initializes a new instance of the - /// class. - /// - /// The description to store in this attribute. - /// - public EnumDescriptionAttribute(string description) - { - Description = description; - } - - /// - /// Gets the description stored in this attribute. - /// - /// The description stored in the attribute. - public string Description { get; private set; } - } - -} diff --git a/src/Libraries/SmartStore.Core/Infrastructure/Error.cs b/src/Libraries/SmartStore.Core/Infrastructure/Error.cs index d8752d8169..47f03f6f2e 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/Error.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/Error.cs @@ -2,8 +2,6 @@ using System.Globalization; using System.Diagnostics; -using Fasterflect; - namespace SmartStore { public static class Error @@ -77,7 +75,7 @@ public static Exception Argument(Func arg, string message, params object[] [DebuggerStepThrough] public static Exception InvalidOperation(string message, params object[] args) { - return Error.InvalidOperation(message, null, args); + return InvalidOperation(message, null, args); } [DebuggerStepThrough] diff --git a/src/Libraries/SmartStore.Core/Infrastructure/Guard.cs b/src/Libraries/SmartStore.Core/Infrastructure/Guard.cs index f90ae96153..5a15aba9f3 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/Guard.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/Guard.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; @@ -113,7 +114,7 @@ public static void ArgumentNotEmpty(Func arg) { if (arg().IsEmpty()) { - string argName = GetParamName(arg); + var argName = GetParamName(arg); throw Error.Argument(argName, "String parameter '{0}' cannot be null or all whitespace.", argName); } } @@ -303,7 +304,7 @@ public static void ArgumentNotDisposed(DisposableObject arg, string argName) [DebuggerStepThrough] public static void PagingArgsValid(int indexArg, int sizeArg, string indexArgName, string sizeArgName) { - ArgumentNotNegative(indexArg, indexArgName, "PageIndex cannot be below 0"); + ArgumentNotNegative(indexArg, indexArgName, "PageIndex cannot be below 0"); if (indexArg > 0) { // if pageIndex is specified (> 0), PageSize CANNOT be 0 @@ -317,9 +318,10 @@ public static void PagingArgsValid(int indexArg, int sizeArg, string indexArgNam } [DebuggerStepThrough] + [SuppressMessage("ReSharper", "UnusedMember.Local")] private static string GetParamName(Expression> expression) { - string name = string.Empty; + var name = string.Empty; MemberExpression body = expression.Body as MemberExpression; if (body != null) diff --git a/src/Libraries/SmartStore.Core/Infrastructure/IEngine.cs b/src/Libraries/SmartStore.Core/Infrastructure/IEngine.cs index 7053d65252..43d05ecce3 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/IEngine.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/IEngine.cs @@ -18,7 +18,6 @@ public interface IEngine /// /// Initialize components and plugins in the SmartStore environment. /// - /// Config void Initialize(); T Resolve(string name = null) where T : class; diff --git a/src/Libraries/SmartStore.Core/Infrastructure/Misc.cs b/src/Libraries/SmartStore.Core/Infrastructure/Misc.cs deleted file mode 100644 index c09656ec4b..0000000000 --- a/src/Libraries/SmartStore.Core/Infrastructure/Misc.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.Reflection; -using System.Text; -using System.Globalization; -using System.IO; - -namespace SmartStore -{ - - //internal delegate T Creator(); - - internal static class Misc - { - - public static bool TryAction(Func func, out T output) - { - Guard.ArgumentNotNull(() => func); - - try - { - output = func(); - return true; - } - catch - { - output = default(T); - return false; - } - } - - /// - /// Perform an action if the string is not null or empty. - /// - /// The value. - /// The action to perform. - public static void IfNotNullOrEmpty(string value, Action action) - { - IfNotNullOrEmpty(value, action, null); - } - - private static void IfNotNullOrEmpty(string value, Action trueAction, Action falseAction) - { - if (!string.IsNullOrEmpty(value)) - { - if (trueAction != null) - trueAction(value); - } - else - { - if (falseAction != null) - falseAction(value); - } - } - - } -} diff --git a/src/Libraries/SmartStore.Core/Infrastructure/ObjectSignatureAttribute.cs b/src/Libraries/SmartStore.Core/Infrastructure/ObjectSignatureAttribute.cs index ec0292c390..42b53f80c4 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/ObjectSignatureAttribute.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/ObjectSignatureAttribute.cs @@ -1,11 +1,9 @@ using System; namespace SmartStore -{ - +{ [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public sealed class ObjectSignatureAttribute : Attribute { } - } diff --git a/src/Libraries/SmartStore.Core/Infrastructure/ValueObject.cs b/src/Libraries/SmartStore.Core/Infrastructure/ValueObject.cs deleted file mode 100644 index 066c47f810..0000000000 --- a/src/Libraries/SmartStore.Core/Infrastructure/ValueObject.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Reflection; -using Fasterflect; - -namespace SmartStore -{ - - /// - /// Base class for complex value objects, which do not have - /// identifiers. - /// - /// Type of the entity, which is the value obect. - public abstract class ValueObject : ComparableObject> - where T : ValueObject - { - - /// - /// Registers all properties of the subclass as signature properties. - /// - protected void RegisterProperties() - { - RegisterProperties((pd) => true); - } - - /// - /// Registers any properties of the subclass matching - /// the given as signature properties. - /// - protected void RegisterProperties(Func filter) - { - foreach (var pi in this.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public)) - { - if (filter(pi)) - RegisterSignatureProperty(pi); - } - } - - public override bool Equals(object obj) - { - return base.Equals(obj); - } - - public override int GetHashCode() - { - return base.GetHashCode(); - } - - public override string ToString() - { - var sb = new StringBuilder(); - var properties = GetSignatureProperties(); - int propsCount = properties.Count(); - - int i = 1; - - foreach (var pi in properties) - { - object value = this.GetPropertyValue(pi.Name); - if (value == null) - continue; - - sb.Append(pi.Name + ": " + value.ToString()); - if (i < propsCount) - sb.Append(", "); - - i++; - } - - return sb.ToString(); - } - - public static bool operator ==(ValueObject x, ValueObject y) - { - if ((object)x == null) - return (object)y == null; - - return x.Equals(y); - } - - public static bool operator !=(ValueObject x, ValueObject y) - { - return !(x == y); - } - - } - -} diff --git a/src/Libraries/SmartStore.Core/Infrastructure/WebAppTypeFinder.cs b/src/Libraries/SmartStore.Core/Infrastructure/WebAppTypeFinder.cs index 689a3f9e62..90dc70012f 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/WebAppTypeFinder.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/WebAppTypeFinder.cs @@ -15,8 +15,8 @@ public class WebAppTypeFinder : AppDomainTypeFinder { #region Fields - private bool _ensureBinFolderAssembliesLoaded = true; - private bool _binFolderAssembliesLoaded = false; + private bool _ensureBinFolderAssembliesLoaded; + private bool _binFolderAssembliesLoaded; #endregion diff --git a/src/Libraries/SmartStore.Core/Linq/Expanders/LambdaPathExpander.cs b/src/Libraries/SmartStore.Core/Linq/Expanders/LambdaPathExpander.cs index 185b8a946e..4c7c1ec45d 100644 --- a/src/Libraries/SmartStore.Core/Linq/Expanders/LambdaPathExpander.cs +++ b/src/Libraries/SmartStore.Core/Linq/Expanders/LambdaPathExpander.cs @@ -57,16 +57,14 @@ public virtual void Expand(Type type, string path) var tokenizer = new StringTokenizer(path, ".", false); foreach (string member in tokenizer) { - // Property oder Field des Members ermitteln - MemberInfo prop = t.GetFieldOrProperty(member, true); - //MemberInfo prop = t.GetProperty(member); + // Property ermitteln + //MemberInfo prop = t.GetFieldOrProperty(member, true); + var prop = t.GetProperty(member, BindingFlags.Public | BindingFlags.IgnoreCase | BindingFlags.Instance); if (prop == null) throw new ArgumentException("The property or member '{0}' does not exist in type '{1}'.".FormatInvariant(member, t.FullName)); - Type memberType = (prop.MemberType == MemberTypes.Property) - ? ((PropertyInfo)prop).PropertyType - : ((FieldInfo)prop).FieldType; + Type memberType = prop.PropertyType; DoExpand(t, member); diff --git a/src/Libraries/SmartStore.Core/Linq/Expressions/ExpressionVisitor.cs b/src/Libraries/SmartStore.Core/Linq/Expressions/ExpressionVisitor.cs index b1a66d8885..63cf7761fd 100644 --- a/src/Libraries/SmartStore.Core/Linq/Expressions/ExpressionVisitor.cs +++ b/src/Libraries/SmartStore.Core/Linq/Expressions/ExpressionVisitor.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; @@ -21,6 +22,7 @@ namespace SmartStore.Linq.Expressions /// for anyone outside of Microsoft to be using... /// [DebuggerStepThrough, DebuggerNonUserCode] + [SuppressMessage("ReSharper", "PossibleUnintendedReferenceComparison")] public abstract class ExpressionVisitor { /// diff --git a/src/Libraries/SmartStore.Core/Localization/LocalizationHelper.cs b/src/Libraries/SmartStore.Core/Localization/LocalizationHelper.cs index d4c940ee36..ed9beed2ad 100644 --- a/src/Libraries/SmartStore.Core/Localization/LocalizationHelper.cs +++ b/src/Libraries/SmartStore.Core/Localization/LocalizationHelper.cs @@ -1,11 +1,10 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Globalization; using System.Linq; namespace SmartStore.Core.Localization { - public static class LocalizationHelper + public static class LocalizationHelper { private readonly static HashSet _cultureCodes = new HashSet( @@ -18,5 +17,36 @@ public static bool IsValidCultureCode(string cultureCode) return _cultureCodes.Contains(cultureCode); } - } + public static string GetLanguageNativeName(string locale) + { + try + { + if (locale.HasValue()) + { + var info = CultureInfo.GetCultureInfoByIetfLanguageTag(locale); + if (info != null) + return info.NativeName; + } + } + catch { } + + return null; + } + + public static string GetCurrencySymbol(string locale) + { + try + { + if (locale.HasValue()) + { + var info = new RegionInfo(locale); + if (info != null) + return info.CurrencySymbol; + } + } + catch { } + + return null; + } + } } diff --git a/src/Libraries/SmartStore.Core/Logging/ICustomerActivityService.cs b/src/Libraries/SmartStore.Core/Logging/ICustomerActivityService.cs index 0c32d85236..3ff8526a96 100644 --- a/src/Libraries/SmartStore.Core/Logging/ICustomerActivityService.cs +++ b/src/Libraries/SmartStore.Core/Logging/ICustomerActivityService.cs @@ -1,15 +1,14 @@ using System; using System.Collections.Generic; -using SmartStore.Core; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Logging; namespace SmartStore.Core.Logging { - /// - /// Customer activity service interface - /// - public partial interface ICustomerActivityService + /// + /// Customer activity service interface + /// + public partial interface ICustomerActivityService { /// /// Inserts an activity log type item @@ -75,19 +74,27 @@ ActivityLog InsertActivity(string systemKeyword, /// Activity log void DeleteActivity(ActivityLog activityLog); - /// - /// Gets all activity log items - /// - /// Log item creation from; null to load all customers - /// Log item creation to; null to load all customers - /// Customer identifier; null to load all customers - /// Activity log type identifier - /// Page index - /// Page size - /// Activity log collection - IPagedList GetAllActivities(DateTime? createdOnFrom, - DateTime? createdOnTo, int? customerId, - int activityLogTypeId, int pageIndex, int pageSize); + /// + /// Gets all activity log items + /// + /// Log item creation from; null to load all customers + /// Log item creation to; null to load all customers + /// Customer identifier; null to load all customers + /// Activity log type identifier + /// Page index + /// Page size + /// Customer email + /// Customer system name + /// Activity log collection + IPagedList GetAllActivities( + DateTime? createdOnFrom, + DateTime? createdOnTo, + int? customerId, + int activityLogTypeId, + int pageIndex, + int pageSize, + string email = null, + bool? customerSystemAccount = null); /// /// Gets an activity log item @@ -96,9 +103,16 @@ IPagedList GetAllActivities(DateTime? createdOnFrom, /// Activity log item ActivityLog GetActivityById(int activityLogId); - /// - /// Clears activity log - /// - void ClearAllActivities(); + /// + /// Gets activity logs be identifier + /// + /// Activity log identifiers + /// List of activity logs + IList GetActivityByIds(int[] activityLogIds); + + /// + /// Clears activity log + /// + void ClearAllActivities(); } } diff --git a/src/Libraries/SmartStore.Core/Logging/LoggingExtensions.cs b/src/Libraries/SmartStore.Core/Logging/LoggingExtensions.cs index de1266efd4..fb8a10be35 100644 --- a/src/Libraries/SmartStore.Core/Logging/LoggingExtensions.cs +++ b/src/Libraries/SmartStore.Core/Logging/LoggingExtensions.cs @@ -9,35 +9,49 @@ public static class LoggingExtensions public static void Debug(this ILogger logger, string message, Exception exception = null, Customer customer = null) { - FilteredLog(logger, LogLevel.Debug, message, exception, customer); + FilteredLog(logger, LogLevel.Debug, message, null, exception, customer); } public static void Information(this ILogger logger, string message, Exception exception = null, Customer customer = null) { - FilteredLog(logger, LogLevel.Information, message, exception, customer); + FilteredLog(logger, LogLevel.Information, message, null, exception, customer); } public static void Warning(this ILogger logger, string message, Exception exception = null, Customer customer = null) { - FilteredLog(logger, LogLevel.Warning, message, exception, customer); + FilteredLog(logger, LogLevel.Warning, message, null, exception, customer); } public static void Error(this ILogger logger, string message, Exception exception = null, Customer customer = null) { - FilteredLog(logger, LogLevel.Error, message, exception, customer); + FilteredLog(logger, LogLevel.Error, message, null, exception, customer); } - public static void Fatal(this ILogger logger, string message, Exception exception = null, Customer customer = null) + public static void Error(this ILogger logger, string message, string fullMessage, Exception exception = null, Customer customer = null) + { + FilteredLog(logger, LogLevel.Error, message, fullMessage, exception, customer); + } + + public static void Fatal(this ILogger logger, string message, Exception exception = null, Customer customer = null) { - FilteredLog(logger, LogLevel.Fatal, message, exception, customer); + FilteredLog(logger, LogLevel.Fatal, message, null, exception, customer); } public static void Error(this ILogger logger, Exception exception, Customer customer = null) { - FilteredLog(logger, LogLevel.Error, exception.ToAllMessages(), exception, customer); + FilteredLog(logger, LogLevel.Error, exception.ToAllMessages(), null, exception, customer); } - private static void FilteredLog(ILogger logger, LogLevel level, string message, Exception exception = null, Customer customer = null) + public static void ErrorsAll(this ILogger logger, Exception exception, Customer customer = null) + { + while (exception != null) + { + FilteredLog(logger, LogLevel.Error, exception.Message, exception.StackTrace, exception, customer); + exception = exception.InnerException; + } + } + + private static void FilteredLog(ILogger logger, LogLevel level, string message, string fullMessage, Exception exception = null, Customer customer = null) { // don't log thread abort exception if ((exception != null) && (exception is System.Threading.ThreadAbortException)) @@ -45,7 +59,11 @@ private static void FilteredLog(ILogger logger, LogLevel level, string message, if (logger.IsEnabled(level)) { - string fullMessage = exception == null ? string.Empty : exception.StackTrace; + if (exception != null && fullMessage.IsEmpty()) + { + fullMessage = "{0}\n{1}\n{2}".FormatCurrent(exception.Message, new String('-', 20), exception.StackTrace); + } + logger.InsertLog(level, message, fullMessage, customer); } } diff --git a/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs b/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs index 7cb7b6e7de..593a39ddb0 100644 --- a/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs +++ b/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; +using System.IO; using System.Text; -using SmartStore.Core.Domain.Logging; using SmartStore.Core.Domain.Customers; -using System.Diagnostics; +using SmartStore.Core.Domain.Logging; using SmartStore.Utilities; namespace SmartStore.Core.Logging @@ -12,6 +12,7 @@ namespace SmartStore.Core.Logging public class TraceLogger : DisposableObject, ILogger { private readonly TraceSource _traceSource; + private readonly StreamWriter _streamWriter; public TraceLogger() : this(CommonHelper.MapPath("~/App_Data/SmartStore.log")) { @@ -24,23 +25,37 @@ public TraceLogger(string fileName) _traceSource = new TraceSource("SmartStore"); _traceSource.Switch = new SourceSwitch("LogSwitch", "Error"); _traceSource.Listeners.Remove("Default"); - + var console = new ConsoleTraceListener(false); console.Filter = new EventTypeFilter(SourceLevels.All); console.Name = "console"; + _traceSource.Listeners.Add(console); + var textListener = new TextWriterTraceListener(fileName); textListener.Filter = new EventTypeFilter(SourceLevels.All); textListener.TraceOutputOptions = TraceOptions.DateTime; - _traceSource.Listeners.Add(console); - _traceSource.Listeners.Add(textListener); - - // Allow the trace source to send messages to - // listeners for all event types. Currently only + try + { + // force UTF-8 encoding (even if the text just contains ANSI characters) + var append = File.Exists(fileName); + _streamWriter = new StreamWriter(fileName, append, Encoding.UTF8); + + textListener.Writer = _streamWriter; + + _traceSource.Listeners.Add(textListener); + } + catch (IOException) + { + // file is locked by another process + } + + // Allow the trace source to send messages to + // listeners for all event types. Currently only // error messages or higher go to the listeners. - // Messages must get past the source switch to - // get to the listeners, regardless of the settings + // Messages must get past the source switch to + // get to the listeners, regardless of the settings // for the listeners. _traceSource.Switch.Level = SourceLevels.All; } @@ -133,6 +148,12 @@ protected override void OnDispose(bool disposing) { _traceSource.Flush(); _traceSource.Close(); + + if (_streamWriter != null) + { + _streamWriter.Close(); + _streamWriter.Dispose(); + } } } } diff --git a/src/Libraries/SmartStore.Core/Packaging/FolderUpdater.cs b/src/Libraries/SmartStore.Core/Packaging/FolderUpdater.cs index 314eb5d61c..d02cc71cce 100644 --- a/src/Libraries/SmartStore.Core/Packaging/FolderUpdater.cs +++ b/src/Libraries/SmartStore.Core/Packaging/FolderUpdater.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.IO; using SmartStore.Core.Logging; @@ -14,6 +15,7 @@ public interface IFolderUpdater void Restore(DirectoryInfo backupfolder, DirectoryInfo existingFolder); } + [SuppressMessage("ReSharper", "NotAccessedField.Local")] public class FolderUpdater : IFolderUpdater { public class FolderContent @@ -83,6 +85,7 @@ private FolderContent GetFolderContent(DirectoryInfo folder, IEnumerable files, IEnumerable ignores) { if (!folder.Exists) diff --git a/src/Libraries/SmartStore.Core/Packaging/NuGet/ExtensionReferenceRepository.cs b/src/Libraries/SmartStore.Core/Packaging/NuGet/ExtensionReferenceRepository.cs index e010cc1507..f5ca4be0fe 100644 --- a/src/Libraries/SmartStore.Core/Packaging/NuGet/ExtensionReferenceRepository.cs +++ b/src/Libraries/SmartStore.Core/Packaging/NuGet/ExtensionReferenceRepository.cs @@ -53,14 +53,12 @@ public override bool SupportsPrereleasePackages /// internal class PluginReferenceRepository : ExtensionReferenceRepository { - private readonly IPluginFinder _pluginFinder; private readonly IList _descriptors; public PluginReferenceRepository(IProjectSystem project, IPackageRepository sourceRepository, IPluginFinder pluginFinder) : base(project, sourceRepository) { - _pluginFinder = pluginFinder; - _descriptors = _pluginFinder.GetPluginDescriptors().ToList(); + _descriptors = pluginFinder.GetPluginDescriptors().ToList(); } public override IQueryable GetPackages() @@ -83,14 +81,12 @@ public override IQueryable GetPackages() /// internal class ThemeReferenceRepository : ExtensionReferenceRepository { - private readonly IThemeRegistry _themeRegistry; private readonly ICollection _themeManifests; public ThemeReferenceRepository(IProjectSystem project, IPackageRepository sourceRepository, IThemeRegistry themeRegistry) : base(project, sourceRepository) { - _themeRegistry = themeRegistry; - _themeManifests = _themeRegistry.GetThemeManifests(true); + _themeManifests = themeRegistry.GetThemeManifests(true); } public override IQueryable GetPackages() diff --git a/src/Libraries/SmartStore.Core/Packaging/NuGet/FileBasedProjectSystem.cs b/src/Libraries/SmartStore.Core/Packaging/NuGet/FileBasedProjectSystem.cs index f6294a4e68..a32cae4e91 100644 --- a/src/Libraries/SmartStore.Core/Packaging/NuGet/FileBasedProjectSystem.cs +++ b/src/Libraries/SmartStore.Core/Packaging/NuGet/FileBasedProjectSystem.cs @@ -8,7 +8,7 @@ namespace SmartStore.Core.Packaging { - internal class FileBasedProjectSystem : PhysicalFileSystem, IProjectSystem, IFileSystem + internal class FileBasedProjectSystem : PhysicalFileSystem, IProjectSystem { public FileBasedProjectSystem(string root) diff --git a/src/Libraries/SmartStore.Core/Packaging/NuGet/NullSourceRepository.cs b/src/Libraries/SmartStore.Core/Packaging/NuGet/NullSourceRepository.cs index 95cd8ca76b..5d98420378 100644 --- a/src/Libraries/SmartStore.Core/Packaging/NuGet/NullSourceRepository.cs +++ b/src/Libraries/SmartStore.Core/Packaging/NuGet/NullSourceRepository.cs @@ -12,10 +12,6 @@ namespace SmartStore.Core.Packaging /// internal class NullSourceRepository : PackageRepositoryBase { - public NullSourceRepository() - { - } - public override IQueryable GetPackages() { return Enumerable.Empty().AsQueryable(); diff --git a/src/Libraries/SmartStore.Core/Packaging/PackageBuilder.cs b/src/Libraries/SmartStore.Core/Packaging/PackageBuilder.cs index 53e322fe59..ee78bbc8fd 100644 --- a/src/Libraries/SmartStore.Core/Packaging/PackageBuilder.cs +++ b/src/Libraries/SmartStore.Core/Packaging/PackageBuilder.cs @@ -18,12 +18,10 @@ namespace SmartStore.Core.Packaging public class PackageBuilder : IPackageBuilder { private readonly IWebSiteFolder _webSiteFolder; - private readonly IVirtualPathProvider _virtualPathProvider; - public PackageBuilder(IWebSiteFolder webSiteFolder, IVirtualPathProvider virtualPathProvider) + public PackageBuilder(IWebSiteFolder webSiteFolder) { this._webSiteFolder = webSiteFolder; - this._virtualPathProvider = virtualPathProvider; } private static readonly string[] _ignoredThemeExtensions = new[] { @@ -38,18 +36,18 @@ private static bool IgnoreFile(string filePath) { return String.IsNullOrEmpty(filePath) || _ignoredThemePaths.Any(filePath.Contains) || - _ignoredThemeExtensions.Contains(Path.GetExtension(filePath) ?? ""); + _ignoredThemeExtensions.Contains(Path.GetExtension(filePath).NullEmpty() ?? ""); } public Stream BuildPackage(PluginDescriptor pluginDescriptor) { - return BuildPackage(PackagingUtils.ConvertToExtensionDescriptor(pluginDescriptor)); + return BuildPackage(pluginDescriptor.ConvertToExtensionDescriptor()); } public Stream BuildPackage(ThemeManifest themeManifest) { - return BuildPackage(PackagingUtils.ConvertToExtensionDescriptor(themeManifest)); + return BuildPackage(themeManifest.ConvertToExtensionDescriptor()); } private Stream BuildPackage(ExtensionDescriptor extensionDescriptor) @@ -58,7 +56,7 @@ private Stream BuildPackage(ExtensionDescriptor extensionDescriptor) BeginPackage(context); try { - EstablishPaths(context, _webSiteFolder, extensionDescriptor.Location, extensionDescriptor.Id, extensionDescriptor.ExtensionType); + EstablishPaths(context, _webSiteFolder, extensionDescriptor.Id, extensionDescriptor.ExtensionType); SetCoreProperties(context, extensionDescriptor); EmbedFiles(context); } @@ -100,7 +98,7 @@ private static void SetCoreProperties(BuildContext context, ExtensionDescriptor } } - private static void EstablishPaths(BuildContext context, IWebSiteFolder webSiteFolder, string locationPath, string extensionName, string extensionType = "Plugin") + private static void EstablishPaths(BuildContext context, IWebSiteFolder webSiteFolder, string extensionName, string extensionType = "Plugin") { context.SourceFolder = webSiteFolder; if (extensionType.IsCaseInsensitiveEqual("theme")) @@ -129,11 +127,11 @@ private static void EmbedFiles(BuildContext context) // the package that way (the package itself is the logical base path). // Get it by stripping the basePath off including the slash. var relativePath = virtualPath.Replace(basePath, ""); - EmbedVirtualFile(context, relativePath, MediaTypeNames.Application.Octet); + EmbedVirtualFile(context, relativePath); } } - private static void EmbedVirtualFile(BuildContext context, string relativePath, string contentType) + private static void EmbedVirtualFile(BuildContext context, string relativePath) { var file = new VirtualPackageFile( context.SourceFolder, @@ -153,8 +151,6 @@ private class BuildContext public IWebSiteFolder SourceFolder { get; set; } public string SourcePath { get; set; } public string TargetPath { get; set; } - - public XDocument Project { get; set; } } #endregion diff --git a/src/Libraries/SmartStore.Core/Packaging/PackageInstaller.cs b/src/Libraries/SmartStore.Core/Packaging/PackageInstaller.cs index 5039a7b331..5a0664ef51 100644 --- a/src/Libraries/SmartStore.Core/Packaging/PackageInstaller.cs +++ b/src/Libraries/SmartStore.Core/Packaging/PackageInstaller.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using NuGet; @@ -62,7 +63,7 @@ public PackageInfo Install(Stream packageStream, string location, string applica { Guard.ArgumentNotNull(() => packageStream); - IPackage package = null; + IPackage package; try { package = new ZipPackage(packageStream); @@ -215,6 +216,7 @@ public void Uninstall(string packageId, string applicationFolder) } } + [SuppressMessage("ReSharper", "UnusedMethodReturnValue.Local")] private bool RestoreExtensionFolder(string extensionFolder, string extensionId) { var virtualSource = _virtualPathProvider.Combine("~", extensionFolder, extensionId); diff --git a/src/Libraries/SmartStore.Core/Packaging/Updater/AppUpdater.cs b/src/Libraries/SmartStore.Core/Packaging/Updater/AppUpdater.cs index 2a77aef5a3..f54398153e 100644 --- a/src/Libraries/SmartStore.Core/Packaging/Updater/AppUpdater.cs +++ b/src/Libraries/SmartStore.Core/Packaging/Updater/AppUpdater.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Threading; @@ -26,6 +27,7 @@ public sealed class AppUpdater : DisposableObject #region Package update + [SuppressMessage("ReSharper", "RedundantAssignment")] public bool InstallablePackageExists() { string packagePath = null; @@ -112,15 +114,13 @@ private IPackage FindPackage(bool createLogger, out string path) var files = Directory.GetFiles(dir, "SmartStore.*.nupkg", SearchOption.TopDirectoryOnly); // TODO: allow more than one package in folder and return newest - if (files == null || files.Length == 0 || files.Length > 1) + if (files.Length == 0 || files.Length > 1) return null; - IPackage package = null; - try { path = files[0]; - package = new ZipPackage(files[0]); + IPackage package = new ZipPackage(files[0]); if (createLogger) { _logger = CreateLogger(package); diff --git a/src/Libraries/SmartStore.Core/PagedList.cs b/src/Libraries/SmartStore.Core/PagedList.cs index c7e05f617a..4f4689496e 100644 --- a/src/Libraries/SmartStore.Core/PagedList.cs +++ b/src/Libraries/SmartStore.Core/PagedList.cs @@ -4,7 +4,6 @@ namespace SmartStore.Core { - public abstract class PagedListBase : IPageable { @@ -32,10 +31,10 @@ protected PagedListBase(int pageIndex, int pageSize, int totalItemsCount) // only here for compat reasons with nc public void LoadPagedList(IPagedList pagedList) { - this.Init(pagedList as IPageable); + this.Init(pagedList); } - public virtual void Init(IPageable pageable) + public void Init(IPageable pageable) { Guard.ArgumentNotNull(pageable, "pageable"); diff --git a/src/Libraries/SmartStore.Core/Plugins/BasePlugin.cs b/src/Libraries/SmartStore.Core/Plugins/BasePlugin.cs index 16c8e45a85..80ae85fd5a 100644 --- a/src/Libraries/SmartStore.Core/Plugins/BasePlugin.cs +++ b/src/Libraries/SmartStore.Core/Plugins/BasePlugin.cs @@ -2,10 +2,6 @@ { public abstract class BasePlugin : IPlugin { - protected BasePlugin() - { - } - /// /// Gets or sets the plugin descriptor /// diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs b/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs index 235c4756ce..0409454f60 100644 --- a/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs +++ b/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; using System.Xml; @@ -52,7 +53,7 @@ public string BrandImageFileName if (_brandImageFileName == null) { // "null" means we haven't checked yet! - var filesToCheck = new string[] { "branding.png", "branding.gif", "branding.jpg", "branding.jpeg" }; + var filesToCheck = new [] { "branding.png", "branding.gif", "branding.jpg", "branding.jpeg" }; var dir = this.PhysicalPath; foreach (var file in filesToCheck) { @@ -234,7 +235,8 @@ public IPlugin Instance() return Instance(); } - public int CompareTo(PluginDescriptor other) + [SuppressMessage("ReSharper", "StringCompareToIsCultureSpecific")] + public int CompareTo(PluginDescriptor other) { if (DisplayOrder != other.DisplayOrder) return DisplayOrder.CompareTo(other.DisplayOrder); diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs b/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs index dfa85d8ef7..7fd04123aa 100644 --- a/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs +++ b/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs @@ -13,7 +13,7 @@ using System.Web.Hosting; using Microsoft.Web.Infrastructure; using Microsoft.Web.Infrastructure.DynamicModuleHelper; -using SmartStore.Core.ComponentModel; +using SmartStore.ComponentModel; using SmartStore.Core.Infrastructure.DependencyManagement; using SmartStore.Core.Plugins; using SmartStore.Core.Packaging; @@ -178,7 +178,6 @@ public static void Initialize() } IncompatiblePlugins = incompatiblePlugins.AsReadOnly(); - } } diff --git a/src/Libraries/SmartStore.Core/Plugins/Providers/ProviderMetadata.cs b/src/Libraries/SmartStore.Core/Plugins/Providers/ProviderMetadata.cs index b1da4a1304..d07a628170 100644 --- a/src/Libraries/SmartStore.Core/Plugins/Providers/ProviderMetadata.cs +++ b/src/Libraries/SmartStore.Core/Plugins/Providers/ProviderMetadata.cs @@ -74,9 +74,9 @@ public class ProviderMetadata public bool IsHidden { get; set; } /// - /// Gets or sets an array of values that reflects what export data processing is supported by a provider + /// Gets or sets flags that reflects what features of export data processing is supported by a provider /// - public ExportSupport[] ExportSupport { get; set; } + public ExportFeatures ExportFeatures { get; set; } /// /// Gets or sets an array of widget system names, which depend on the current provider diff --git a/src/Libraries/SmartStore.Core/RouteInfo.cs b/src/Libraries/SmartStore.Core/RouteInfo.cs index af8c5c7a18..b6856d1a0f 100644 --- a/src/Libraries/SmartStore.Core/RouteInfo.cs +++ b/src/Libraries/SmartStore.Core/RouteInfo.cs @@ -5,7 +5,6 @@ namespace SmartStore { - public class RouteInfo { public RouteInfo(RouteInfo cloneFrom) @@ -14,22 +13,35 @@ public RouteInfo(RouteInfo cloneFrom) Guard.ArgumentNotNull(() => cloneFrom); } - public RouteInfo(string action, string controller, object routeValues) + public RouteInfo(string action, object routeValues) + : this(action, null, routeValues) + { + } + + public RouteInfo(string action, string controller, object routeValues) : this(action, controller, new RouteValueDictionary(routeValues)) { - Guard.ArgumentNotNull(() => routeValues); } - public RouteInfo(string action, string controller, IDictionary routeValues) + public RouteInfo(string action, IDictionary routeValues) + : this(action, null, routeValues) + { + } + + public RouteInfo(string action, string controller, IDictionary routeValues) : this(action, controller, new RouteValueDictionary(routeValues)) { Guard.ArgumentNotNull(() => routeValues); } - public RouteInfo(string action, string controller, RouteValueDictionary routeValues) + public RouteInfo(string action, RouteValueDictionary routeValues) + : this(action, null, routeValues) + { + } + + public RouteInfo(string action, string controller, RouteValueDictionary routeValues) { Guard.ArgumentNotEmpty(() => action); - Guard.ArgumentNotEmpty(() => controller); Guard.ArgumentNotNull(() => routeValues); this.Action = action; @@ -56,5 +68,4 @@ public RouteValueDictionary RouteValues } } - } diff --git a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj index ab5fd94130..a14afb2ba4 100644 --- a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj +++ b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj @@ -62,12 +62,13 @@ MinimumRecommendedRules.ruleset - - ..\..\packages\Autofac.3.4.1\lib\net40\Autofac.dll + + ..\..\packages\Autofac.3.5.2\lib\net40\Autofac.dll + True - False - ..\..\packages\Autofac.Mvc5.3.3.1\lib\net45\Autofac.Integration.Mvc.dll + ..\..\packages\Autofac.Mvc5.3.3.4\lib\net45\Autofac.Integration.Mvc.dll + True False @@ -77,10 +78,6 @@ False ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll - - False - ..\..\packages\fasterflect.2.1.3\lib\net40\Fasterflect.dll - True ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll @@ -88,9 +85,9 @@ ..\..\packages\Microsoft.Web.Xdt.1.0.0\lib\net40\Microsoft.Web.XmlTransform.dll - - False - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True False @@ -153,37 +150,52 @@ - + + + + + + + + + + + + + + + - - + + + - + @@ -215,10 +227,6 @@ - - - - @@ -303,7 +311,7 @@ - + @@ -347,14 +355,12 @@ - - @@ -373,7 +379,6 @@ - @@ -435,23 +440,17 @@ - - - - - - @@ -566,18 +565,19 @@ - - + - + + + @@ -598,6 +598,7 @@ + diff --git a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs index e35d4026a1..38750e3069 100644 --- a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs +++ b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs @@ -9,7 +9,6 @@ namespace SmartStore.Core public static class SmartStoreVersion { private static readonly Version s_infoVersion = new Version("1.0.0.0"); - private static readonly Version s_version = Assembly.GetExecutingAssembly().GetName().Version; private static readonly List s_breakingChangesHistory = new List { // IMPORTANT: Add app versions from low to high @@ -20,12 +19,14 @@ public static class SmartStoreVersion new Version("1.2.1"), // MC: had to be :-( new Version("2.0"), new Version("2.1"), - new Version("2.2") + new Version("2.2"), + new Version("2.5") }; - private const string HELP_BASEURL = "http://docs.smartstore.com/display/SMNET25/"; + //private const string HELP_BASEURL = "http://docs.smartstore.com/display/SMNET25/"; + private const string HELP_BASEURL = "http://docs.smartstore.com/display/"; - static SmartStoreVersion() + static SmartStoreVersion() { s_breakingChangesHistory.Reverse(); @@ -67,9 +68,18 @@ public static Version Version } } - public static string GenerateHelpUrl(string path) + public static string GenerateHelpUrl(string languageCode, string path) { - return HELP_BASEURL + path.Trim().TrimStart('/', '\\'); + return String.Concat( + HELP_BASEURL, + GetUserGuideSpaceKey(languageCode), + "/", + path.EmptyNull().Trim().TrimStart('/', '\\')); + } + + public static string GetUserGuideSpaceKey(string languageCode) + { + return "SMNET25"; } /// diff --git a/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs b/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs index a9f59ab407..82ad8b0d52 100644 --- a/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs +++ b/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -35,7 +36,7 @@ public partial class DefaultThemeRegistry : DisposableObject, IThemeRegistry public DefaultThemeRegistry(IEventPublisher eventPublisher, bool? enableMonitoring, string themesBasePath, bool autoLoadThemes) { - this._enableMonitoring = enableMonitoring ?? CommonHelper.GetAppSetting("sm:MonitorThemesFolder", true); + this._enableMonitoring = enableMonitoring ?? CommonHelper.GetAppSetting("sm:MonitorThemesFolder", true); this._themesBasePath = themesBasePath.NullEmpty() ?? CommonHelper.GetAppSetting("sm:ThemesBasePath", "~/Themes/").EnsureEndsWith("/"); this._eventPublisher = eventPublisher; @@ -102,10 +103,9 @@ private void AddThemeManifestInternal(ThemeManifest manifest, bool isInit) { Guard.ArgumentNotNull(() => manifest); - bool removed = false; if (!isInit) { - removed = TryRemoveManifest(manifest.ThemeName); + TryRemoveManifest(manifest.ThemeName); } ThemeManifest baseManifest = null; @@ -131,7 +131,8 @@ private void AddThemeManifestInternal(ThemeManifest manifest, bool isInit) } } - private bool TryRemoveManifest(string themeName) + [SuppressMessage("ReSharper", "AssignmentInConditionalExpression")] + private bool TryRemoveManifest(string themeName) { bool result; ThemeManifest existing; diff --git a/src/Libraries/SmartStore.Core/Themes/ThemeFolderData.cs b/src/Libraries/SmartStore.Core/Themes/ThemeFolderData.cs index 50055a93a3..3bac8d4084 100644 --- a/src/Libraries/SmartStore.Core/Themes/ThemeFolderData.cs +++ b/src/Libraries/SmartStore.Core/Themes/ThemeFolderData.cs @@ -23,7 +23,7 @@ string[] ITopologicSortable.DependsOn if (BaseTheme == null) return null; - return new string[] { BaseTheme }; + return new [] { BaseTheme }; } } } diff --git a/src/Libraries/SmartStore.Core/Themes/ThemeManifestMaterializer.cs b/src/Libraries/SmartStore.Core/Themes/ThemeManifestMaterializer.cs index f88e133528..7162c5e172 100644 --- a/src/Libraries/SmartStore.Core/Themes/ThemeManifestMaterializer.cs +++ b/src/Libraries/SmartStore.Core/Themes/ThemeManifestMaterializer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Xml; @@ -29,7 +30,7 @@ public ThemeManifest Materialize() { var root = _manifest.ConfigurationNode; - _manifest.ThemeTitle = root.GetAttribute("title") ?? _manifest.ThemeName; + _manifest.ThemeTitle = root.GetAttribute("title").NullEmpty() ?? _manifest.ThemeName; _manifest.SupportRtl = root.GetAttribute("supportRTL").ToBool(); _manifest.MobileTheme = root.GetAttribute("mobileTheme").ToBool(); _manifest.PreviewImageUrl = root.GetAttribute("previewImageUrl").NullEmpty() ?? "~/Themes/{0}/preview.png".FormatCurrent(_manifest.ThemeName); @@ -44,7 +45,9 @@ public ThemeManifest Materialize() return _manifest; } - private Multimap MaterializeSelects() + [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute")] + [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] + private Multimap MaterializeSelects() { var selects = new Multimap(); var root = _manifest.ConfigurationNode; diff --git a/src/Libraries/SmartStore.Core/Utilities/CollectionHelper.cs b/src/Libraries/SmartStore.Core/Utilities/CollectionHelper.cs deleted file mode 100644 index 2497ef9349..0000000000 --- a/src/Libraries/SmartStore.Core/Utilities/CollectionHelper.cs +++ /dev/null @@ -1,266 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Reflection; -using System.Linq; -using System.Text; -using System.Collections; -using System.Globalization; -using System.Collections.Specialized; -using System.ComponentModel; - -using Fasterflect; - -namespace SmartStore.Utilities -{ - public static class CollectionHelper - { - - /// - /// Determines whether the collection is null, empty or its contents are uninitialized values. - /// - /// The list. - /// - /// true if the collection is null or empty or its contents are uninitialized values; otherwise, false. - /// - public static bool IsNullOrEmptyOrDefault(IList value) - { - if (value.IsNullOrEmpty()) - return true; - - return TypeHelper.ItemsUnitializedValue(value); - } - - - public static List> Flatten(params IList[] lists) - { - List> flattened = new List>(); - Dictionary currentList = new Dictionary(); - - Recurse(new List>(lists), 0, currentList, flattened); - - return flattened; - } - - private static void Recurse(IList> global, int current, Dictionary currentSet, List> flattenedResult) - { - IList currentArray = global[current]; - - for (int i = 0; i < currentArray.Count; i++) - { - currentSet[current] = currentArray[i]; - - if (current == global.Count - 1) - { - List items = new List(); - - for (int k = 0; k < currentSet.Count; k++) - { - items.Add(currentSet[k]); - } - - flattenedResult.Add(items); - } - else - { - Recurse(global, current + 1, currentSet, flattenedResult); - } - } - } - - public static IList Minus(IList list, IList minus) - { - Guard.ArgumentNotNull(list, "list"); - - List result = new List(list.Count); - foreach (T t in list) - { - if (minus == null || !minus.Contains(t)) - result.Add(t); - } - - return result; - } - - public static T[] CreateArray(IEnumerable enumerable) - { - Guard.ArgumentNotNull(enumerable, "enumerable"); - - if (enumerable is T[]) - return (T[])enumerable; - - return enumerable.ToArray(); - } - - - public static IList CreateAndPopulateList(Type listType, Action populateList) - { - Guard.ArgumentNotNull(listType, "listType"); - Guard.ArgumentNotNull(populateList, "populateList"); - - IList list; - Type collectionType; - bool isReadOnlyOrFixedSize = false; - - if (listType.IsArray) - { - // have to use an arraylist when creating array - // there is no way to know the size until it is finised - list = new ArrayList(); - isReadOnlyOrFixedSize = true; - } - else if (listType.IsSubClass(typeof(ReadOnlyCollection<>), out collectionType)) - { - Type readOnlyCollectionContentsType = collectionType.GetGenericArguments()[0]; - Type genericEnumerable = typeof(IEnumerable<>).MakeGenericType(readOnlyCollectionContentsType); - bool suitableConstructor = false; - - foreach (ConstructorInfo constructor in listType.GetConstructors()) - { - IList parameters = constructor.GetParameters(); - - if (parameters.Count == 1) - { - if (genericEnumerable.IsAssignableFrom(parameters[0].ParameterType)) - { - suitableConstructor = true; - break; - } - } - } - - if (!suitableConstructor) - throw new Exception("Read-only type {0} does not have a public constructor that takes a type that implements {1}.".FormatWith(CultureInfo.InvariantCulture, listType, genericEnumerable)); - - // can't add or modify a readonly list - // use List and convert once populated - list = (IList)readOnlyCollectionContentsType.CreateGenericList(); - isReadOnlyOrFixedSize = true; - } - else if (typeof(IList).IsAssignableFrom(listType)) - { - if (listType.IsConstructable()) - list = (IList)Activator.CreateInstance(listType); - else if (listType == typeof(IList)) - list = new List(); - else - list = null; - } - else if (listType.IsGenericType && listType.GetGenericTypeDefinition() == typeof(IList<>)) - { - list = TypeHelper.GetElementType(listType).CreateGenericList(); //TypeHelper.GetListItemType(listType).CreateGenericList(); - } - else - { - list = null; - } - - if (list == null) - throw new Exception("Cannot create and populate list type {0}.".FormatWith(CultureInfo.InvariantCulture, listType)); - - populateList(list); - - // create readonly and fixed sized collections using the temporary list - if (isReadOnlyOrFixedSize) - { - if (listType.IsArray) - list = ((ArrayList)list).ToArray(TypeHelper.GetElementType(listType)); - else if (listType.IsSubClass(typeof(ReadOnlyCollection<>))) - list = (IList)Activator.CreateInstance(listType, list); - } - - return list; - } - - public static string ToUnsortedHtmlList(ICollection data) - { - return ToUnsortedHtmlList(data, null, null); - } - - public static string ToUnsortedHtmlList(ICollection data, string ulClassName) - { - return ToUnsortedHtmlList(data, ulClassName, null); - } - - public static string ToUnsortedHtmlList(ICollection data, string ulClassName, string liClassName) - { - if (data.IsNullOrEmpty()) - return String.Empty; - - StringBuilder ul = new StringBuilder(); - if (!String.IsNullOrEmpty(ulClassName)) - { - ul.Append(string.Format("
    \n", ulClassName)); - } - else - { - ul.Append("
      \n"); - } - - foreach (var m in data) - { - if (!String.IsNullOrEmpty(liClassName)) - { - ul.Append(string.Format("
    • ", liClassName)); - } - else - { - ul.Append("
    • "); - } - ul.Append(m).AppendLine("
    • "); - } - - return ul.AppendLine("
    ").ToString(); - ; - - } - - public static IDictionary ObjectToDictionary(object obj) - { - Guard.ArgumentNotNull(() => obj); - - Type t = obj.GetType(); - - return t.GetProperties(BindingFlags.Instance | BindingFlags.Public) - .ToDictionary(k => k.Name.Replace("_", "-"), - v => obj.GetPropertyValue(v.Name), - StringComparer.OrdinalIgnoreCase); - } - - public static NameValueCollection ObjectToNameValueCollection(object obj) - { - var result = new NameValueCollection(StringComparer.OrdinalIgnoreCase); - foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(obj)) - { - object value = descriptor.GetValue(obj); - result.Add(descriptor.Name, value.ToString()); - } - - return result; - } - - /// - /// Converts the type to an - /// compatible object. - /// - /// The type. - /// An containing the enumerated - /// type value and description. - public static IDictionary EnumToDictionary(Type type) - { - Guard.ArgumentIsEnumType(type, "type"); - - IDictionary dict = new Dictionary(); - Array enumValues = Enum.GetValues(type); - - foreach (Enum value in enumValues) - { - object key = Convert.ChangeType(value, Enum.GetUnderlyingType(type)); - dict[key] = value.GetDescription(); - } - - return dict; - } - - } -} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs b/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs index 5b5b60f58c..cdcfe4e431 100644 --- a/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs @@ -1,14 +1,13 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Configuration; using System.Dynamic; using System.Globalization; using System.IO; using System.Security.Cryptography; -using System.Text; using System.Web.Hosting; using System.Web.Mvc; +using SmartStore.ComponentModel; namespace SmartStore.Utilities { @@ -136,7 +135,7 @@ public static bool TryConvert(object value, out T convertedValue) public static bool TryConvert(object value, CultureInfo culture, out T convertedValue) { - return Misc.TryAction(delegate + return TryAction(delegate { return value.Convert(culture); }, out convertedValue); @@ -149,7 +148,7 @@ public static bool TryConvert(object value, Type to, out object convertedValue) public static bool TryConvert(object value, Type to, CultureInfo culture, out object convertedValue) { - return Misc.TryAction(delegate { return value.Convert(to, culture); }, out convertedValue); + return TryAction(delegate { return value.Convert(to, culture); }, out convertedValue); } public static ExpandoObject ToExpando(object value) @@ -165,9 +164,13 @@ public static ExpandoObject ToExpando(object value) return (ExpandoObject)expando; } - public static TypeConverter GetTypeConverter(Type type) + public static IDictionary ObjectToDictionary(object obj) { - return ConversionExtensions.GetTypeConverter(type); + Guard.ArgumentNotNull(() => obj); + + return FastProperty.ObjectToDictionary( + obj, + key => key.Replace("_", "-")); } /// @@ -191,5 +194,21 @@ public static TypeConverter GetTypeConverter(Type type) return setting.Convert(); } - } + private static bool TryAction(Func func, out T output) + { + Guard.ArgumentNotNull(() => func); + + try + { + output = func(); + return true; + } + catch + { + output = default(T); + return false; + } + } + + } } diff --git a/src/Libraries/SmartStore.Core/Utilities/DateTimeConvert.cs b/src/Libraries/SmartStore.Core/Utilities/DateTimeConvert.cs deleted file mode 100644 index 5e3cdabc64..0000000000 --- a/src/Libraries/SmartStore.Core/Utilities/DateTimeConvert.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; - -namespace SmartStore.Utilities -{ - /// - /// Converts between s and s. - /// - /// Accepted formats for parsing are "dd MMM yyyy HH:mm:ss.ff", "yyyy-MM-ddTHH:mm:ss", "dd MMM yyyy hh:mm tt", "dd MMM yyyy hh:mm:ss tt", "dd MMM yyyy HH:mm:ss", "dd MMM yyyy HH:mm" and "dd MMM yyyy". - public static class DateTimeConvert - { - #region Properties - - /// - /// The default format used by and . - /// - [SuppressMessage("Microsoft.Performance", "CA1802:UseLiteralsWhereAppropriate")] - public static readonly string DateTimeFormat = "dd MMM yyyy HH:mm:ss.ff"; - - /// - /// The supported formats used to parse strings. - /// - private static readonly string[] parseFormats = new string[] { DateTimeFormat, "s", "dd MMM yyyy hh:mm tt", "dd MMM yyyy hh:mm:ss tt", "dd MMM yyyy HH:mm:ss", "dd MMM yyyy HH:mm", "dd MMM yyyy" }; - - #endregion - - #region Methods - - /// - /// Converts the specified string representation of a date and time to its equivalent. - /// - /// - /// Accepted formats for parsing are "dd MMM yyyy HH:mm:ss.ff", "yyyy-MM-ddTHH:mm:ss", "dd MMM yyyy hh:mm tt", "dd MMM yyyy hh:mm:ss tt", "dd MMM yyyy HH:mm:ss", "dd MMM yyyy HH:mm" and "dd MMM yyyy". is used to attempt to parse . - /// - /// A string containing a date and (optionally) time to convert. - /// A equivalent to the date and time contained in . - /// cannot be parsed. - /// is a null reference. - /// is . - public static DateTime Parse(string date) - { - try - { - return DateTime.ParseExact(date, parseFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None); - } - catch (FormatException formatException) - { - throw new FormatException(string.Format("{0}. Value = {1}", formatException.Message, date), formatException); - } - } - - - /// - /// Converts the specified string representation of a date and time to its equivalent. - /// - /// A string containing a date and (optionally) time to convert. - /// if is ; otherwise the value returned from . - public static DateTime? ParseNullable(string s) - { - if (string.IsNullOrEmpty(s)) - { - return null; - } - else - { - return Parse(s); - } - } - - - /// - /// Converts a to its equivalent string representation. - /// - /// The to convert. - /// A string representation . - public static string ToString(DateTime dateTime) - { - return dateTime.ToString(DateTimeFormat); - } - - - /// - /// Converts a to its equivalent string representation. - /// - /// The to convert. - /// if A string representation . - public static string ToString(DateTime? dateTime) - { - if (dateTime.HasValue) - { - return ToString(dateTime.Value); - } - else - { - return string.Empty; - } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Utilities/DictionaryConverter.cs b/src/Libraries/SmartStore.Core/Utilities/DictionaryConverter.cs index bcb4706ad6..1df2547ea0 100644 --- a/src/Libraries/SmartStore.Core/Utilities/DictionaryConverter.cs +++ b/src/Libraries/SmartStore.Core/Utilities/DictionaryConverter.cs @@ -1,12 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; -using System.ComponentModel; using System.Text; using System.Collections; using System.Reflection; using System.Runtime.Serialization; -using Fasterflect; +using SmartStore.ComponentModel; namespace SmartStore.Utilities { @@ -93,7 +92,7 @@ public static object CreateAndPopulate(Type targetType, IDictionary targetType); - var target = targetType.CreateInstance(); //Activator.CreateInstance(targetType); + var target = Activator.CreateInstance(targetType); Populate(source, target, out problems); @@ -141,15 +140,14 @@ public static void Populate(IDictionary source, object target, o if (source != null) { - // TODO: Metadaten aus einem TypeCache ziehen zwecks Performance! - foreach (var pi in t.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + foreach (var fastProp in FastProperty.GetProperties(t).Values) { - object value; + var pi = fastProp.Property; if (!pi.PropertyType.IsPredefinedSimpleType() && source.TryGetValue(pi.Name, out value) && value is IDictionary) { - var nestedValue = target.GetPropertyValue(pi.Name); + var nestedValue = fastProp.GetValue(target); ICollection nestedProblems; populated = populated.Concat(new object[] { target }).ToArray(); @@ -159,17 +157,17 @@ public static void Populate(IDictionary source, object target, o { problems.AddRange(nestedProblems); } - WriteToProperty(target, pi, nestedValue, problems); + WriteToProperty(target, fastProp, nestedValue, problems); } else if (pi.PropertyType.IsArray && !source.ContainsKey(pi.Name)) { - WriteToProperty(target, pi, RetrieveArrayValues(pi, source, problems), problems); + WriteToProperty(target, fastProp, RetrieveArrayValues(pi, source, problems), problems); } else { if (source.TryGetValue(pi.Name, out value)) { - WriteToProperty(target, pi, value, problems); + WriteToProperty(target, fastProp, value, problems); } } } @@ -182,21 +180,19 @@ private static object RetrieveArrayValues(PropertyInfo arrayProp, IDictionary).MakeGenericType(elemType)); - var elements = (IList)typeof(List<>).MakeGenericType(elemType).CreateInstance(); + var elements = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elemType)); - // TODO: Metadaten aus einem TypeCache ziehen zwecks Performance! - var properties = elemType.GetProperties(BindingFlags.Public | BindingFlags.Instance); + var properties = FastProperty.GetProperties(elemType); while (anyValuesFound) { object curElement = null; anyValuesFound = false; // false until proven otherwise - foreach (var pi in properties) + foreach (var prop in properties.Values) { //var key = string.Format("_{0}{1}_{2}", idx, arrayProp.Name, pd.Name); - var key = string.Format("{0}[{1}].{2}", arrayProp.Name, idx, pi.Name); + var key = string.Format("{0}[{1}].{2}", arrayProp.Name, idx, prop.Name); object value; if (source.TryGetValue(key, out value)) @@ -205,11 +201,11 @@ private static object RetrieveArrayValues(PropertyInfo arrayProp, IDictionary problems) + private static void SetPropFromValue(object value, object item, FastProperty prop, ICollection problems) { - WriteToProperty(item, pi, value, problems); + WriteToProperty(item, prop, value, problems); } - private static void WriteToProperty(object item, PropertyInfo pi, object value, ICollection problems) + private static void WriteToProperty(object item, FastProperty prop, object value, ICollection problems) { - if (!pi.CanWrite) + var pi = prop.Property; + + if (!pi.CanWrite) return; try @@ -246,8 +244,7 @@ private static void WriteToProperty(object item, PropertyInfo pi, object value, if (pi.PropertyType.IsAssignableFrom(value.GetType())) { - //pi.SetValue(item, value); - item.SetPropertyValue(pi.Name, value); + prop.SetValue(item, value); return; } @@ -256,8 +253,7 @@ private static void WriteToProperty(object item, PropertyInfo pi, object value, destType = pi.PropertyType.GetGenericArguments()[0]; } - //pi.SetValue(item, value.Convert(destType)); - item.SetPropertyValue(pi.Name, value.Convert(destType)); + prop.SetValue(item, value.Convert(destType)); } } catch (Exception ex) diff --git a/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs b/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs index de2db7a3e9..d3fcef7874 100644 --- a/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs +++ b/src/Libraries/SmartStore.Core/Utilities/FileDownloadManager.cs @@ -31,14 +31,24 @@ public FileDownloadManager(HttpRequestBase httpRequest) /// The URL to download the file from (either a fully qualified URL or an app relative/absolute path) /// Specifies whether the FormsAuthenticationCookie should be sent /// Timeout in milliseconds - public FileDownloadResponse DownloadFile(string url, bool sendAuthCookie = false, int? timeout = null) + /// Specifiers whether the file is located on the local server + public FileDownloadResponse DownloadFile(string url, bool sendAuthCookie = false, int? timeout = null, bool isLocal = false) { Guard.ArgumentNotEmpty(() => url); url = WebHelper.GetAbsoluteUrl(url, _httpRequest); - var req = (HttpWebRequest)WebRequest.Create(url); - req.UserAgent = "SmartStore.NET"; + HttpWebRequest req; + + if (isLocal) + { + req = WebHelper.CreateHttpRequestForSafeLocalCall(new Uri(url)); + } + else + { + req = WebRequest.CreateHttp(url); + req.UserAgent = "SmartStore.NET"; + } if (timeout.HasValue) { @@ -51,14 +61,32 @@ public FileDownloadResponse DownloadFile(string url, bool sendAuthCookie = false } using (var resp = (HttpWebResponse)req.GetResponse()) + using (var stream = resp.GetResponseStream()) { - using (var stream = resp.GetResponseStream()) + if (resp.StatusCode == HttpStatusCode.OK) { - if (resp.StatusCode == HttpStatusCode.OK) + var data = stream.ToByteArray(); + if (data != null && data.Length != 0) { - var data = stream.ToByteArray(); - var cd = new ContentDisposition(resp.Headers["Content-Disposition"]); - var fileName = cd.FileName; + string fileName = null; + + var contentDisposition = resp.Headers["Content-Disposition"]; + if (contentDisposition.HasValue()) + { + var cd = new ContentDisposition(contentDisposition); + fileName = cd.FileName; + } + + if (fileName.IsEmpty()) + { + try + { + var uri = new Uri(url); + fileName = Path.GetFileName(uri.LocalPath); + } + catch { } + } + return new FileDownloadResponse(data, fileName, resp.ContentType); } } @@ -79,31 +107,40 @@ public async Task DownloadAsync(FileDownloadManagerContext context, IEnumerable< private async Task DownloadFiles(FileDownloadManagerContext context, IEnumerable items) { - var client = new HttpClient(); - - client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue(); - client.DefaultRequestHeaders.CacheControl.NoCache = true; - client.DefaultRequestHeaders.Add("Connection", "Keep-alive"); + try + { + using (var client = new HttpClient()) + { + client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue(); + client.DefaultRequestHeaders.CacheControl.NoCache = true; + client.DefaultRequestHeaders.Add("Connection", "Keep-alive"); - if (context.Timeout != null) - client.Timeout = context.Timeout; + if (context.Timeout.TotalMilliseconds > 0 && context.Timeout != Timeout.InfiniteTimeSpan) + client.Timeout = context.Timeout; - IEnumerable downloadTasksQuery = - from item in items - select ProcessUrl(context, client, item); + IEnumerable downloadTasksQuery = + from item in items + select ProcessUrl(context, client, item); - // now execute the bunch - List downloadTasks = downloadTasksQuery.ToList(); + // now execute the bunch + List downloadTasks = downloadTasksQuery.ToList(); - while (downloadTasks.Count > 0) - { - // identify the first task that completes - Task firstFinishedTask = await Task.WhenAny(downloadTasks); + while (downloadTasks.Count > 0) + { + // identify the first task that completes + Task firstFinishedTask = await Task.WhenAny(downloadTasks); - // process only once - downloadTasks.Remove(firstFinishedTask); + // process only once + downloadTasks.Remove(firstFinishedTask); - await firstFinishedTask; + await firstFinishedTask; + } + } + } + catch (Exception exception) + { + if (context.Logger != null) + context.Logger.ErrorsAll(exception); } } @@ -111,7 +148,10 @@ private async Task ProcessUrl(FileDownloadManagerContext context, HttpClient cli { try { - Task task = client.GetStreamAsync(item.Url); + //HttpResponseMessage response = await client.GetAsync(item.Url, HttpCompletionOption.ResponseHeadersRead); + //Task task = response.Content.ReadAsStreamAsync(); + + Task task = client.GetStreamAsync(item.Url); await task; int count; @@ -132,29 +172,31 @@ private async Task ProcessUrl(FileDownloadManagerContext context, HttpClient cli item.Success = (!task.IsFaulted && !canceled); } - catch (Exception exc) + catch (Exception exception) { - item.Success = false; - item.ErrorMessage = exc.ToAllMessages(); - - var webExc = exc.InnerException as WebException; - if (webExc != null) - item.ExceptionStatus = webExc.Status; + try + { + item.Success = false; + item.ErrorMessage = exception.ToAllMessages(); - if (context.Logger != null) - context.Logger.Error(item.ToString(), exc); + var webExc = exception.InnerException as WebException; + if (webExc != null) + item.ExceptionStatus = webExc.Status; + + if (context.Logger != null) + context.Logger.Error(item.ToString(), exception); + } + catch { } } } - } + public class FileDownloadResponse { public FileDownloadResponse(byte[] data, string fileName, string contentType) { Guard.ArgumentNotNull(() => data); - Guard.ArgumentNotEmpty(() => fileName); - Guard.ArgumentNotEmpty(() => contentType); this.Data = data; this.FileName = fileName; @@ -182,12 +224,12 @@ public class FileDownloadManagerContext /// /// Optional logger to log errors /// - public TraceLogger Logger { get; set; } + public ILogger Logger { get; set; } /// /// Cancellation token /// - public CancellationTokenSource CancellationToken { get; set; } + public CancellationToken CancellationToken { get; set; } /// /// Timeout for the HTTP client diff --git a/src/Libraries/SmartStore.Core/Utilities/FileSystemHelper.cs b/src/Libraries/SmartStore.Core/Utilities/FileSystemHelper.cs index f9e3c8c928..3fbb4f83f8 100644 --- a/src/Libraries/SmartStore.Core/Utilities/FileSystemHelper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/FileSystemHelper.cs @@ -14,7 +14,7 @@ public static class FileSystemHelper /// Name of a sub directory to be created and returned (optional) public static string TempDir(string subDirectory = null) { - string path = CommonHelper.GetAppSetting("sm:TempDirectory", "~/App_Data/_temp"); + string path = CommonHelper.GetAppSetting("sm:TempDirectory", "~/App_Data/_temp"); path = CommonHelper.MapPath(path); if (!Directory.Exists(path)) @@ -31,6 +31,28 @@ public static string TempDir(string subDirectory = null) return path; } + /// + /// Ensures that path is a valid root path + /// + /// Relative path + /// Valid root path + public static string ValidateRootPath(string path) + { + if (path.HasValue()) + { + path = path.Replace('\\', '/'); + + if (!path.StartsWith("~/")) + { + if (path.StartsWith("~")) + path = path.Substring(1); + + path = (path.StartsWith("/") ? "~" : "~/") + path; + } + } + return path; + } + /// /// Safe way to cleanup the temp directory. Should be called via scheduled task. /// @@ -38,26 +60,25 @@ public static void TempCleanup() { try { - string dir = FileSystemHelper.TempDir(); + var dir = TempDir(); if (Directory.Exists(dir)) { - FileInfo fi; var oldestDate = DateTime.Now.Subtract(new TimeSpan(0, 5, 0, 0)); var files = Directory.EnumerateFiles(dir); foreach (string file in files) { - fi = new FileInfo(file); + var fi = new FileInfo(file); - if (fi != null && fi.LastWriteTime < oldestDate) - FileSystemHelper.Delete(file); + if (fi.LastWriteTime < oldestDate) + Delete(file); } } } - catch (Exception exc) + catch (Exception ex) { - exc.Dump(); + ex.Dump(); } } @@ -84,6 +105,7 @@ public static bool Delete(string path) result = false; exc.Dump(); } + return result; } @@ -114,8 +136,10 @@ public static bool Copy(string sourcePath, string destinationPath, bool overwrit /// Source directory /// Target directory /// Whether to override existing files - public static void CopyDirectory(DirectoryInfo source, DirectoryInfo target, bool overwrite = true) + public static bool CopyDirectory(DirectoryInfo source, DirectoryInfo target, bool overwrite = true) { + var result = true; + foreach (FileInfo fi in source.GetFiles()) { try @@ -124,6 +148,7 @@ public static void CopyDirectory(DirectoryInfo source, DirectoryInfo target, boo } catch (Exception exc) { + result = false; exc.Dump(); } } @@ -137,9 +162,12 @@ public static void CopyDirectory(DirectoryInfo source, DirectoryInfo target, boo } catch (Exception exc) { + result = false; exc.Dump(); } } + + return result; } /// @@ -209,6 +237,30 @@ public static void ClearDirectory(string directoryPath, bool selfToo, List + /// Creates a non existing directory name + /// + /// Path of a directory + /// Default name for directory. null to use a guid. + /// Non existing directory name + public static string CreateNonExistingDirectoryName(string directoryPath, string defaultName) + { + if (defaultName.IsEmpty()) + defaultName = Guid.NewGuid().ToString(); + + if (directoryPath.IsEmpty() || !Directory.Exists(directoryPath)) + return defaultName; + + var newName = defaultName; + + for (int i = 1; i < 999999 && Directory.Exists(Path.Combine(directoryPath, newName)); ++i) + { + newName = defaultName + i.ToString(); + } + + return newName; + } + /// /// Safe way to count files in a directory /// diff --git a/src/Libraries/SmartStore.Core/Utilities/HashCodeCombiner.cs b/src/Libraries/SmartStore.Core/Utilities/HashCodeCombiner.cs new file mode 100644 index 0000000000..b69d640a05 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Utilities/HashCodeCombiner.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +/* + Copied over from Microsoft.Framework.Internal +*/ + +namespace SmartStore.Utilities +{ + internal struct HashCodeCombiner + { + private long _combinedHash64; + + public int CombinedHash + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get { return _combinedHash64.GetHashCode(); } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private HashCodeCombiner(long seed) + { + _combinedHash64 = seed; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HashCodeCombiner Add(IEnumerable e) + { + if (e == null) + { + Add(0); + } + else + { + var count = 0; + foreach (object o in e) + { + Add(o); + count++; + } + Add(count); + } + return this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator int (HashCodeCombiner self) + { + return self.CombinedHash; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HashCodeCombiner Add(int i) + { + _combinedHash64 = ((_combinedHash64 << 5) + _combinedHash64) ^ i; + return this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HashCodeCombiner Add(string s) + { + var hashCode = (s != null) ? s.GetHashCode() : 0; + return Add(hashCode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HashCodeCombiner Add(object o) + { + var hashCode = (o != null) ? o.GetHashCode() : 0; + return Add(hashCode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HashCodeCombiner Add(TValue value, IEqualityComparer comparer) + { + var hashCode = value != null ? comparer.GetHashCode(value) : 0; + return Add(hashCode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HashCodeCombiner Start() + { + return new HashCodeCombiner(0x1505L); + } + } +} diff --git a/src/Libraries/SmartStore.Core/Utilities/ObjectDumper.cs b/src/Libraries/SmartStore.Core/Utilities/ObjectDumper.cs new file mode 100644 index 0000000000..8ccba41fbb --- /dev/null +++ b/src/Libraries/SmartStore.Core/Utilities/ObjectDumper.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace SmartStore.Core.Utilities +{ + public static class ObjectDumper + { + public static void Dump(object value, TextWriter writer) + { + writer.WriteLine(Dump(value)); + } + + public static string Dump(object value) + { + return JsonConvert.SerializeObject(value, Formatting.Indented); + } + + public static void ToConsole(object value) + { + Console.WriteLine(Dump(value)); + } + + public static void ToDebug(object value) + { + Debug.WriteLine(Dump(value)); + } + } +} diff --git a/src/Libraries/SmartStore.Core/Utilities/Prettifier.cs b/src/Libraries/SmartStore.Core/Utilities/Prettifier.cs index d833e3276b..d0d4cc2307 100644 --- a/src/Libraries/SmartStore.Core/Utilities/Prettifier.cs +++ b/src/Libraries/SmartStore.Core/Utilities/Prettifier.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text; @@ -7,11 +8,10 @@ namespace SmartStore.Utilities { - public static class Prettifier { - - public static string BytesToString(long bytes) + [SuppressMessage("ReSharper", "RedundantAssignment")] + public static string BytesToString(long bytes) { double result = bytes; double dsize = bytes; diff --git a/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs b/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs index 0c71d1d6ba..3dec92a6d4 100644 --- a/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/SeoHelper.cs @@ -1,105 +1,109 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics.CodeAnalysis; using System.Text; namespace SmartStore.Utilities { - + public static class SeoHelper { + private static readonly string _okChars = "abcdefghijklmnopqrstuvwxyz1234567890 _-/"; + private static readonly object _lock = new object(); private static Dictionary _seoCharacterTable; - private static readonly object s_lock = new object(); + private static Dictionary _userSeoCharacterTable; /// - /// Get SE name + /// Get SEO friendly string /// - /// Name + /// String to be converted /// A value indicating whether non western chars should be converted /// A value indicating whether Unicode chars are allowed - /// Result - public static string GetSeName(string name, bool convertNonWesternChars, bool allowUnicodeCharsInUrls) + /// Raw data of semicolon separated char conversions + /// SEO friendly string + [SuppressMessage("ReSharper", "PossibleNullReferenceException")] + public static string GetSeName(string name, bool convertNonWesternChars, bool allowUnicodeCharsInUrls, string charConversions = null) { if (String.IsNullOrEmpty(name)) return name; - string okChars = "abcdefghijklmnopqrstuvwxyz1234567890 _-/"; + name = name.Trim().ToLowerInvariant(); - if (convertNonWesternChars) + if (charConversions != null && _userSeoCharacterTable == null) { - if (_seoCharacterTable == null) - InitializeSeoCharacterTable(); + InitializeUserSeoCharacterTable(charConversions); + } + + if (convertNonWesternChars && _seoCharacterTable == null) + { + InitializeSeoCharacterTable(); } var sb = new StringBuilder(); - foreach (char c in name.ToCharArray()) + foreach (char c in name) { - string c2 = c.ToString(); - if (convertNonWesternChars) + var c2 = c.ToString(); + + if (charConversions != null && _userSeoCharacterTable != null && _userSeoCharacterTable.ContainsKey(c2)) { - if (_seoCharacterTable.ContainsKey(c2)) - c2 = _seoCharacterTable[c2]; + sb.Append(_userSeoCharacterTable[c2]); + continue; } - switch (c2) + if (convertNonWesternChars && _seoCharacterTable.ContainsKey(c2)) { - case "ä": - sb.Append("ae"); - break; - case "ö": - sb.Append("oe"); - break; - case "ü": - sb.Append("ue"); - break; - case "ß": - sb.Append("ss"); - break; - case "Ä": - sb.Append("AE"); - break; - case "Ö": - sb.Append("OE"); - break; - case "Ü": - sb.Append("UE"); - break; + c2 = _seoCharacterTable[c2]; + } - case "é": - case "è": - case "ê": - sb.Append('e'); - break; - case "á": - case "à": - case "â": - sb.Append('a'); - break; - case "ú": - case "ù": - case "û": - sb.Append('u'); - break; - case "ó": - case "ò": - case "ô": - sb.Append('o'); - break; + if (_okChars.Contains(c2) || (allowUnicodeCharsInUrls && char.IsLetterOrDigit(c))) + { + sb.Append(c2); + } + } + + sb.Replace(" ", "-"); - default: - if (okChars.Contains(c2)) - sb.Append(c2); - break; - } // switch + var result = sb.ToString(); + + while (result.Contains("--")) + { + result = result.Replace("--", "-"); } - string name2 = sb.ToString(); - name2 = name2.Replace(" ", "-"); - while (name2.Contains("--")) - name2 = name2.Replace("--", "-"); - while (name2.Contains("__")) - name2 = name2.Replace("__", "_"); + while (result.Contains("__")) + { + result = result.Replace("__", "_"); + } + return result.Trim('/'); + } + + public static void ResetUserSeoCharacterTable() + { + if (_userSeoCharacterTable != null) + { + _userSeoCharacterTable.Clear(); + _userSeoCharacterTable = null; + } + } - return name2.Trim('/'); + private static void InitializeUserSeoCharacterTable(string charConversions) + { + lock (_lock) + { + if (_userSeoCharacterTable == null) + { + _userSeoCharacterTable = new Dictionary(); + + string strLeft, strRight; + + foreach (var conversion in charConversions.SplitSafe(Environment.NewLine)) + { + if (conversion.SplitToPair(out strLeft, out strRight, ";") && strLeft.HasValue() && !_userSeoCharacterTable.ContainsKey(strLeft)) + { + _userSeoCharacterTable.Add(strLeft, strRight); + } + } + } + } } /// @@ -113,7 +117,7 @@ public static string GetSeName(string name, bool convertNonWesternChars, bool al /// private static void InitializeSeoCharacterTable() { - lock (s_lock) + lock (_lock) { if (_seoCharacterTable == null) { diff --git a/src/Libraries/SmartStore.Core/Utilities/SmartSyndicationFeed.cs b/src/Libraries/SmartStore.Core/Utilities/SmartSyndicationFeed.cs index f93e4e6ea3..a81a4301cc 100644 --- a/src/Libraries/SmartStore.Core/Utilities/SmartSyndicationFeed.cs +++ b/src/Libraries/SmartStore.Core/Utilities/SmartSyndicationFeed.cs @@ -52,7 +52,7 @@ public SyndicationItem CreateItem(string title, string synopsis, string url, Dat if (contentEncoded != null) { - item.ElementExtensions.Add("encoded", SmartSyndicationFeed.UrlPurlContent, contentEncoded.RemoveInvalidXmlChars().EmptyNull()); + item.ElementExtensions.Add("encoded", UrlPurlContent, contentEncoded.RemoveInvalidXmlChars().EmptyNull()); } return item; diff --git a/src/Libraries/SmartStore.Core/Utilities/StringTokenizer.cs b/src/Libraries/SmartStore.Core/Utilities/StringTokenizer.cs index 50819eb98f..dd1a76e28b 100644 --- a/src/Libraries/SmartStore.Core/Utilities/StringTokenizer.cs +++ b/src/Libraries/SmartStore.Core/Utilities/StringTokenizer.cs @@ -11,7 +11,6 @@ public class StringTokenizer : IEnumerable { private const string _defaultDelim = " \t\n\r\f"; private readonly string _delim; - private readonly string[] _delims = new string[0]; private readonly string _text; private readonly bool _returnDelims; @@ -36,8 +35,8 @@ public StringTokenizer(string text, string delim, bool returnDelims) #region IEnumerable[] Members - private int _pos = 0; - private string _token = null; + private int _pos; + private string _token; private string GetNext() { diff --git a/src/Libraries/SmartStore.Core/Utilities/TypeHelper.cs b/src/Libraries/SmartStore.Core/Utilities/TypeHelper.cs index a887d720c1..b5db831c2c 100644 --- a/src/Libraries/SmartStore.Core/Utilities/TypeHelper.cs +++ b/src/Libraries/SmartStore.Core/Utilities/TypeHelper.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Globalization; using System.IO; -using Fasterflect; namespace SmartStore.Utilities { @@ -14,34 +13,6 @@ public static class TypeHelper { public delegate T Creator(); - public static bool IsUnitializedValue(object value) - { - if (value == null) - { - return true; - } - else - { - object unitializedValue = CreateUnitializedValue(value.GetType()); - return value.Equals(unitializedValue); - } - } - - public static object CreateUnitializedValue(Type type) - { - Guard.ArgumentNotNull(type, "type"); - - if (type.IsGenericTypeDefinition) - throw new ArgumentException("Type {0} is a generic type definition and cannot be instantiated.".FormatWith(CultureInfo.InvariantCulture, type), "type"); - - if (type.IsClass || type.IsInterface || type == typeof(void)) - return null; - else if (type.IsValueType) - return Activator.CreateInstance(type); - else - throw new ArgumentException("Type {0} cannot be instantiated.".FormatWith(CultureInfo.InvariantCulture, type), "type"); - } - public static Type GetElementType(Type type) { if (!type.IsPredefinedSimpleType()) @@ -61,286 +32,9 @@ public static Type GetElementType(Type type) return GetElementType(type3); } } - return type; - } - - public static void GetDictionaryKeyValueTypes(Type dictionaryType, out Type keyType, out Type valueType) - { - Guard.ArgumentNotNull(dictionaryType, "type"); - - Type genericDictionaryType; - if (dictionaryType.IsSubClass(typeof(IDictionary<,>), out genericDictionaryType)) - { - if (genericDictionaryType.IsGenericTypeDefinition) - throw new Exception("Type {0} is not a dictionary.".FormatInvariant(dictionaryType)); - - Type[] dictionaryGenericArguments = genericDictionaryType.GetGenericArguments(); - - keyType = dictionaryGenericArguments[0]; - valueType = dictionaryGenericArguments[1]; - return; - } - else if (typeof(IDictionary).IsAssignableFrom(dictionaryType)) - { - keyType = null; - valueType = null; - return; - } - else - { - throw new Exception("Type {0} is not a dictionary.".FormatInvariant(dictionaryType)); - } - } - - public static Type GetDictionaryValueType(Type dictionaryType) - { - Type keyType; - Type valueType; - GetDictionaryKeyValueTypes(dictionaryType, out keyType, out valueType); - - return valueType; - } - - public static Type GetDictionaryKeyType(Type dictionaryType) - { - Type keyType; - Type valueType; - GetDictionaryKeyValueTypes(dictionaryType, out keyType, out valueType); - - return keyType; - } - - /// - /// Tests whether the list's items are their unitialized value. - /// - /// The list. - /// Whether the list's items are their unitialized value - public static bool ItemsUnitializedValue(IList list) - { - Guard.ArgumentNotNull(list, "list"); - - Type elementType = GetElementType(list.GetType()); // GetListItemType(list.GetType()); - - if (elementType.IsValueType) - { - object unitializedValue = CreateUnitializedValue(elementType); - - for (int i = 0; i < list.Count; i++) - { - if (!list[i].Equals(unitializedValue)) - return false; - } - } - else if (elementType.IsClass) - { - for (int i = 0; i < list.Count; i++) - { - object value = list[i]; - - if (value != null) - return false; - } - } - else - { - throw new Exception("Type {0} is neither a ValueType or a Class.".FormatWith(CultureInfo.InvariantCulture, elementType)); - } - - return true; - } - - /// - /// Gets the member's underlying type. - /// - /// The member. - /// The underlying type of the member. - public static Type GetMemberUnderlyingType(MemberInfo member) - { - Guard.ArgumentNotNull(member, "member"); - - switch (member.MemberType) - { - case MemberTypes.Field: - return ((FieldInfo)member).FieldType; - case MemberTypes.Property: - return ((PropertyInfo)member).PropertyType; - case MemberTypes.Event: - return ((EventInfo)member).EventHandlerType; - default: - throw new ArgumentException("MemberInfo must be if type FieldInfo, PropertyInfo or EventInfo", "member"); - } - } - - public static IList GetPropertyNames(object target) - { - return target.GetType().GetProperties().Select(z => z.Name).ToList(); - } - - public static IEnumerable GetFieldsAndProperties(BindingFlags bindingAttr) - { - return typeof(T).GetFieldsAndProperties(bindingAttr); - } - - public static T GetAttribute(ICustomAttributeProvider attributeProvider) where T : Attribute - { - return GetAttribute(attributeProvider, true); - } - public static T GetAttribute(ICustomAttributeProvider attributeProvider, bool inherit) where T : Attribute - { - T[] attributes = GetAttributes(attributeProvider, inherit); - return attributes.FirstOrDefault(); - } - - public static T[] GetAttributes(ICustomAttributeProvider attributeProvider, bool inherit) where T : Attribute - { - Guard.ArgumentNotNull(attributeProvider, "attributeProvider"); - - return (T[])attributeProvider.GetCustomAttributes(typeof(T), inherit); - } - - /// - /// Gets the value of a property through reflection. - /// - /// The to get the value from. - /// The name of the property to extract the value for. - /// The value of the property. - public static object GetPropertyValue(object from, string propertyName) - { - Guard.ArgumentNotNull(from, "value"); - var propertyInfo = from.GetType().GetProperty(propertyName, - BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic); - return propertyInfo.GetValue(from, null); - } - - public static bool TryAction(Creator creator, out T output) - { - Guard.ArgumentNotNull(creator, "creator"); - - try - { - output = creator(); - return true; - } - catch - { - output = default(T); - return false; - } - } - - public static bool TryGetDescription(object value, out string description) - { - return TryAction(delegate { return GetDescription(value); }, out description); - } - - public static string GetDescription(object o) - { - Guard.ArgumentNotNull(o, "o"); - - ICustomAttributeProvider attributeProvider = o as ICustomAttributeProvider; - - // object passed in isn't an attribute provider - // if value is an enum value, get value field member, otherwise get values type - if (attributeProvider == null) - { - Type valueType = o.GetType(); - - if (valueType.IsEnum) - attributeProvider = valueType.GetField(o.ToString()); - else - attributeProvider = valueType; - } - - DescriptionAttribute descriptionAttribute = TypeHelper.GetAttribute(attributeProvider); - - if (descriptionAttribute != null) - return descriptionAttribute.Description; - else - throw new Exception("No DescriptionAttribute on '{0}'.".FormatWith(CultureInfo.InvariantCulture, o.GetType())); - } - - public static IList GetDescriptions(IList values) - { - Guard.ArgumentNotNull(values, "values"); - - string[] descriptions = new string[values.Count]; - - for (int i = 0; i < values.Count; i++) - { - descriptions[i] = GetDescription(values[i]); - } - - return descriptions; - } - - /// - /// - /// - /// The type from which to infer namespace name. - /// The relative name of the resource, without (default) namespace name. - /// - public static string GetStringFromEmbeddedResource(Type type, string resourceName) - { - Assembly assembly = type.Assembly; - resourceName = String.Concat(type.Namespace, ".", resourceName); - return GetStringFromEmbeddedResource(assembly, resourceName); - } - - public static string GetStringFromEmbeddedResource(Assembly assembly, string resourceFullName) - { - using (Stream stream = assembly.GetManifestResourceStream(resourceFullName)) - { - try - { - return stream.AsString(); - //using (StreamReader reader = new StreamReader(stream)) - //{ - // return reader.ReadToEnd(); - //} - } - catch (Exception e) - { - throw new Exception("Error retrieving from Resources. Tried '" - + resourceFullName + "'\r\n" + e.ToString()); - } - } - } - - public static Type GetSequenceType(Type elementType) - { - return typeof(IEnumerable<>).MakeGenericType(elementType); - } - - public static Type GetMemberType(MemberInfo mi) - { - FieldInfo fi = mi as FieldInfo; - if (fi != null) - return fi.FieldType; - PropertyInfo pi = mi as PropertyInfo; - if (pi != null) - return pi.PropertyType; - EventInfo ei = mi as EventInfo; - if (ei != null) - return ei.EventHandlerType; - return null; - } - - public static void RegisterTypeConverter() where TC : TypeConverter - { - TypeDescriptor.AddAttributes(typeof(T), new TypeConverterAttribute(typeof(TC))); - } - - public static T CreateInstance(params object[] parameters) where T : class - { - return (T)typeof(T).CreateInstance(parameters); - } - - public static object CreateInstance(Type type, params object[] parameters) - { - return type.CreateInstance(parameters); + return type; } - } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Utilities/Wildcard.cs b/src/Libraries/SmartStore.Core/Utilities/Wildcard.cs index 690e2624d5..6a31c4dd38 100644 --- a/src/Libraries/SmartStore.Core/Utilities/Wildcard.cs +++ b/src/Libraries/SmartStore.Core/Utilities/Wildcard.cs @@ -75,14 +75,14 @@ private static string WildcardToRegex(string pattern) { m_isForward = true; //escape and beginning - pattern = "^" + Regex.Escape(pattern); + pattern = "^" + Escape(pattern); //replace * with .* pattern = pattern.Replace("\\*", ".*"); //$ is for end position and replace ? with a . pattern = pattern.Replace("\\?", ".") + "$"; //convert the number ranges into regular expression - Regex re = new Regex("[0-9]+-[0-9]+"); + var re = new Regex("[0-9]+-[0-9]+"); MatchCollection collection = re.Matches(pattern); foreach (Match match in collection) { diff --git a/src/Libraries/SmartStore.Core/WebHelper.cs b/src/Libraries/SmartStore.Core/WebHelper.cs index f7bee23a4c..edef7863b7 100644 --- a/src/Libraries/SmartStore.Core/WebHelper.cs +++ b/src/Libraries/SmartStore.Core/WebHelper.cs @@ -1,6 +1,8 @@ using System; using System.Configuration; +using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Web; @@ -10,22 +12,29 @@ using SmartStore.Core.Domain.Stores; using SmartStore.Core.Infrastructure; using SmartStore.Utilities; +using System.Net; +using System.Text; +using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Net.Sockets; namespace SmartStore.Core { - public partial class WebHelper : IWebHelper { - private static bool? s_optimizedCompilationsEnabled = null; - private static AspNetHostingPermissionLevel? s_trustLevel = null; + private static object s_lock = new object(); + private static bool? s_optimizedCompilationsEnabled; + private static AspNetHostingPermissionLevel? s_trustLevel; private static readonly Regex s_staticExts = new Regex(@"(.*?)\.(css|js|png|jpg|jpeg|gif|bmp|html|htm|xml|pdf|doc|xls|rar|zip|ico|eot|svg|ttf|woff|otf|axd|ashx|less)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex s_htmlPathPattern = new Regex(@"(?<=(?:href|src)=(?:""|'))(?!https?://)(?[^(?:""|')]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Multiline); private static readonly Regex s_cssPathPattern = new Regex(@"url\('(?.+)'\)", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Multiline); + private static ConcurrentDictionary s_safeLocalHostNames = new ConcurrentDictionary(); private readonly HttpContextBase _httpContext; private bool? _isCurrentConnectionSecured; private string _storeHost; private string _storeHostSsl; + private string _ipAddress; private bool? _appPathPossiblyAppended; private bool? _appPathPossiblyAppendedSsl; @@ -48,17 +57,71 @@ public virtual string GetUrlReferrer() return referrerUrl; } + public virtual string GetClientIdent() + { + var ipAddress = this.GetCurrentIpAddress(); + var userAgent = _httpContext.Request != null ? _httpContext.Request.UserAgent : string.Empty; + + if (ipAddress.HasValue() && userAgent.HasValue()) + { + return (ipAddress + userAgent).GetHashCode().ToString(); + } + + return null; + } + public virtual string GetCurrentIpAddress() { + if (_ipAddress != null) + { + return _ipAddress; + } + + if (_httpContext == null && _httpContext.Request == null) + { + return string.Empty; + } + + var vars = _httpContext.Request.ServerVariables; + + var keysToCheck = new string[] + { + "HTTP_CLIENT_IP", + "HTTP_X_FORWARDED_FOR", + "HTTP_X_FORWARDED", + "HTTP_X_CLUSTER_CLIENT_IP", + "HTTP_FORWARDED_FOR", + "HTTP_FORWARDED", + "REMOTE_ADDR", + "HTTP_CF_CONNECTING_IP" + }; + string result = null; - if (_httpContext != null && _httpContext.Request != null) - result = _httpContext.Request.UserHostAddress; + foreach (var key in keysToCheck) + { + var ipString = vars[key]; + if (ipString.HasValue()) + { + 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; + } + } + } if (result == "::1") + { result = "127.0.0.1"; + } - return result.EmptyNull(); + return (_ipAddress = result.EmptyNull()); } public virtual string GetThisPageUrl(bool includeQueryString) @@ -78,7 +141,7 @@ public virtual string GetThisPageUrl(bool includeQueryString, bool useSsl) bool appPathPossiblyAppended; string storeHost = GetStoreHost(useSsl, out appPathPossiblyAppended).TrimEnd('/'); - string rawUrl = string.Empty; + string rawUrl; if (appPathPossiblyAppended) { string temp = _httpContext.Request.AppRelativeCurrentExecutionFilePath.TrimStart('~'); @@ -99,7 +162,7 @@ public virtual string GetThisPageUrl(bool includeQueryString, bool useSsl) } } - return url.ToLowerInvariant(); + return url; } public virtual bool IsCurrentConnectionSecured() @@ -137,7 +200,8 @@ public virtual string ServerVariables(string name) return result; } - private string GetHostPart(string url) + [SuppressMessage("ReSharper", "UnusedMember.Local")] + private string GetHostPart(string url) { var uri = new Uri(url); var host = uri.GetComponents(UriComponents.Scheme | UriComponents.Host, UriFormat.Unescaped); @@ -189,12 +253,12 @@ private string GetStoreHost(bool useSsl, out bool appPathPossiblyAppended) if (_currentStore != null) { - var securityMode = _currentStore.GetSecurityMode(useSsl); + var securityMode = _currentStore.GetSecurityMode(); if (httpHost.IsEmpty()) { - //HTTP_HOST variable is not available. - //It's possible only when HttpContext is not available (for example, running in a schedule task) + // HTTP_HOST variable is not available. + // It's possible only when HttpContext is not available (for example, running in a schedule task) result = _currentStore.Url.EnsureEndsWith("/"); appPathPossiblyAppended = true; @@ -239,7 +303,7 @@ private string GetStoreHost(bool useSsl, out bool appPathPossiblyAppended) } // cache results for request - result = result.EnsureEndsWith("/").ToLowerInvariant(); + result = result.EnsureEndsWith("/"); if (useSsl) { _storeHostSsl = result; @@ -288,7 +352,7 @@ public virtual string GetStoreLocation(bool useSsl) result += "/"; } - return result.ToLowerInvariant(); + return result; } public virtual bool IsStaticResource(HttpRequest request) @@ -316,9 +380,8 @@ public virtual string MapPath(string path) public virtual string ModifyQueryString(string url, string queryStringModification, string anchor) { - // TODO: routine should not return a query string in lowercase (unless the caller is telling him to do so). - url = url.EmptyNull().ToLower(); - queryStringModification = queryStringModification.EmptyNull().ToLower(); + url = url.EmptyNull(); + queryStringModification = queryStringModification.EmptyNull(); string curAnchor = null; @@ -338,13 +401,19 @@ public virtual string ModifyQueryString(string url, string queryStringModificati current.Add(nv, modify[nv], true); } - var result = "{0}{1}{2}".FormatCurrent(parts[0], current.ToString(), anchor.NullEmpty() == null ? (curAnchor == null ? "" : "#" + curAnchor.ToLower()) : "#" + anchor.ToLower()); + var result = string.Concat( + parts[0], + current.ToString(), + anchor.NullEmpty() == null ? (curAnchor == null ? "" : "#" + curAnchor) : "#" + anchor + ); + return result; } public virtual string RemoveQueryString(string url, string queryString) { - var parts = url.EmptyNull().ToLower().Split(new[] { '?' }); + var parts = url.SplitSafe("?"); + var current = new QueryString(parts.Length == 2 ? parts[1] : ""); if (current.Count > 0 && queryString.HasValue()) @@ -352,13 +421,14 @@ public virtual string RemoveQueryString(string url, string queryString) current.Remove(queryString); } - var result = "{0}{1}".FormatCurrent(parts[0], current.ToString()); + var result = string.Concat(parts[0], current.ToString()); return result; } public virtual T QueryString(string name) { string queryParam = null; + if (_httpContext != null && _httpContext.Request.QueryString[name] != null) queryParam = _httpContext.Request.QueryString[name]; @@ -389,7 +459,7 @@ public virtual void RestartAppDomain(bool makeRedirect = false, string redirectU { if (_httpContext.Request.RequestType == "GET") { - if (String.IsNullOrEmpty(redirectUrl)) + if (string.IsNullOrEmpty(redirectUrl)) { redirectUrl = GetThisPageUrl(true); } @@ -405,7 +475,8 @@ public virtual void RestartAppDomain(bool makeRedirect = false, string redirectU } } - private bool TryWriteWebConfig() + [SuppressMessage("ReSharper", "UnusedMember.Local")] + private bool TryWriteWebConfig() { try { @@ -420,7 +491,8 @@ private bool TryWriteWebConfig() } } - private bool TryWriteGlobalAsax() + [SuppressMessage("ReSharper", "UnusedMember.Local")] + private bool TryWriteGlobalAsax() { try { @@ -487,7 +559,7 @@ public static AspNetHostingPermissionLevel GetTrustLevel() //determine maximum foreach (AspNetHostingPermissionLevel trustLevel in - new AspNetHostingPermissionLevel[] { + new [] { AspNetHostingPermissionLevel.Unrestricted, AspNetHostingPermissionLevel.High, AspNetHostingPermissionLevel.Medium, @@ -564,6 +636,7 @@ public static string MakeAllUrlsAbsolute(string html, string protocol, string ho /// /// Prepends protocol and host to the given (relative) url /// + [SuppressMessage("ReSharper", "AccessToModifiedClosure")] public static string GetAbsoluteUrl(string url, HttpRequestBase request) { Guard.ArgumentNotEmpty(() => url); @@ -584,15 +657,180 @@ public static string GetAbsoluteUrl(string url, HttpRequestBase request) url = VirtualPathUtility.ToAbsolute(url); } - url = String.Format("{0}://{1}{2}", request.Url.Scheme, request.Url.Authority, url); + url = string.Format("{0}://{1}{2}", request.Url.Scheme, request.Url.Authority, url); return url; } - private class StoreHost - { - public string Host { get; set; } - public bool ExpectingDirtySecurityChannelMove { get; set; } - } + public static string GetPublicIPAddress() + { + string result = string.Empty; + + try + { + using (var client = new WebClient()) + { + client.Headers["User-Agent"] = "Mozilla/4.0 (Compatible; Windows NT 5.1; MSIE 6.0) (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)"; + try + { + byte[] arr = client.DownloadData("http://checkip.amazonaws.com/"); + string response = Encoding.UTF8.GetString(arr); + result = response.Trim(); + } + catch { } + } + } + catch { } + + var checkers = new string[] + { + "https://ipinfo.io/ip", + "https://api.ipify.org", + "https://icanhazip.com", + "https://wtfismyip.com/text", + "http://bot.whatismyipaddress.com/" + }; + + if (string.IsNullOrEmpty(result)) + { + using (var client = new WebClient()) + { + foreach (var checker in checkers) + { + try + { + result = client.DownloadString(checker).Replace("\n", ""); + if (!string.IsNullOrEmpty(result)) + { + break; + } + } + catch { } + } + } + } - } + if (string.IsNullOrEmpty(result)) + { + try + { + var url = "http://checkip.dyndns.org"; + var req = WebRequest.Create(url); + using (var resp = req.GetResponse()) + { + using (var sr = new StreamReader(resp.GetResponseStream())) + { + var response = sr.ReadToEnd().Trim(); + var a = response.Split(':'); + var a2 = a[1].Substring(1); + var a3 = a2.Split('<'); + result = a3[0]; + } + } + } + catch { } + } + + return result; + } + + public static HttpWebRequest CreateHttpRequestForSafeLocalCall(Uri requestUri) + { + Guard.ArgumentNotNull(() => requestUri); + + var safeHostName = GetSafeLocalHostName(requestUri); + + var uri = requestUri; + + if (!requestUri.Host.Equals(safeHostName, StringComparison.OrdinalIgnoreCase)) + { + var url = String.Format("{0}://{1}{2}", + requestUri.Scheme, + requestUri.IsDefaultPort ? safeHostName : safeHostName + ":" + requestUri.Port, + requestUri.PathAndQuery); + uri = new Uri(url); + } + + var request = WebRequest.CreateHttp(uri); + request.ServerCertificateValidationCallback += (sender, cert, chain, errors) => true; + request.ServicePoint.Expect100Continue = false; + request.UserAgent = "SmartStore.NET {0}".FormatInvariant(SmartStoreVersion.CurrentFullVersion); + + return request; + } + + private static string GetSafeLocalHostName(Uri requestUri) + { + return s_safeLocalHostNames.GetOrAdd(requestUri.Port, (port) => + { + // first try original host + if (TestHost(requestUri, requestUri.Host, 5000)) + { + return requestUri.Host; + } + + // try loopback + var hostName = Dns.GetHostName(); + var hosts = new List { "localhost", hostName, "127.0.0.1" }; + foreach (var host in hosts) + { + if (TestHost(requestUri, host, 500)) + { + return host; + } + } + + // try local IP addresses + hosts.Clear(); + var ipAddresses = Dns.GetHostAddresses(hostName).Where(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.ToString()); + hosts.AddRange(ipAddresses); + + foreach (var host in hosts) + { + if (TestHost(requestUri, host, 500)) + { + return host; + } + } + + // None of the hosts are callable. WTF? + return requestUri.Host; + }); + } + + private static bool TestHost(Uri originalUri, string host, int timeout) + { + var url = String.Format("{0}://{1}/taskscheduler/noop", + originalUri.Scheme, + originalUri.IsDefaultPort ? host : host + ":" + originalUri.Port); + var uri = new Uri(url); + + var request = WebRequest.CreateHttp(uri); + request.ServerCertificateValidationCallback += (sender, cert, chain, errors) => true; + request.ServicePoint.Expect100Continue = false; + request.UserAgent = "SmartStore.NET"; + request.Timeout = timeout; + + HttpWebResponse response = null; + + try + { + response = (HttpWebResponse)request.GetResponse(); + if (response.StatusCode == HttpStatusCode.OK) + { + return true; + } + } + catch + { + // try the next host + } + finally + { + if (response != null) + response.Dispose(); + } + + return false; + } + } } diff --git a/src/Libraries/SmartStore.Core/XmlHelper.cs b/src/Libraries/SmartStore.Core/XmlHelper.cs index b73c914f03..3b3e3f2e75 100644 --- a/src/Libraries/SmartStore.Core/XmlHelper.cs +++ b/src/Libraries/SmartStore.Core/XmlHelper.cs @@ -123,17 +123,7 @@ public static DateTime DeserializeDateTime(string dateTime) /// XML string public static string Serialize(T instance) { - if (instance != null) - { - using (var writer = new StringWriter()) - { - var xmlSerializer = new XmlSerializer(typeof(T)); - - xmlSerializer.Serialize(writer, instance); - return writer.ToString(); - } - } - return null; + return Serialize(instance, typeof(T)); } /// @@ -144,17 +134,18 @@ public static string Serialize(T instance) /// XML string public static string Serialize(object instance, Type type) { - if (instance != null) + if (instance == null) + return null; + + Guard.NotNull(() => type); + + using (var writer = new StringWriter()) { - using (var writer = new StringWriter()) - { - var xmlSerializer = new XmlSerializer(type); + var xmlSerializer = new XmlSerializer(type); - xmlSerializer.Serialize(writer, instance); - return writer.ToString(); - } + xmlSerializer.Serialize(writer, instance); + return writer.ToString(); } - return null; } /// @@ -165,16 +156,7 @@ public static string Serialize(object instance, Type type) /// Object instance public static T Deserialize(string xml) { - if (xml.HasValue()) - { - using (var reader = new StringReader(xml)) - { - var serializer = new XmlSerializer(typeof(T)); - - return (T)serializer.Deserialize(reader); - } - } - return (T)Activator.CreateInstance(typeof(T)); + return (T)Deserialize(xml, typeof(T)); } /// @@ -185,15 +167,21 @@ public static T Deserialize(string xml) /// Object instance public static object Deserialize(string xml, Type type) { - if (xml.HasValue()) + Guard.NotNull(() => type); + + try { - using (var reader = new StringReader(xml)) + if (xml.HasValue()) { - var serializer = new XmlSerializer(type); - - return serializer.Deserialize(reader); + using (var reader = new StringReader(xml)) + { + var serializer = new XmlSerializer(type); + return serializer.Deserialize(reader); + } } } + catch { } + return Activator.CreateInstance(type); } diff --git a/src/Libraries/SmartStore.Core/app.config b/src/Libraries/SmartStore.Core/app.config new file mode 100644 index 0000000000..6d58373dd6 --- /dev/null +++ b/src/Libraries/SmartStore.Core/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/packages.config b/src/Libraries/SmartStore.Core/packages.config index 923c1183a2..61985f795a 100644 --- a/src/Libraries/SmartStore.Core/packages.config +++ b/src/Libraries/SmartStore.Core/packages.config @@ -1,15 +1,14 @@  - - + + - - + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/EfRepository.cs b/src/Libraries/SmartStore.Data/EfRepository.cs index 860551f723..5afd5e5d41 100644 --- a/src/Libraries/SmartStore.Data/EfRepository.cs +++ b/src/Libraries/SmartStore.Data/EfRepository.cs @@ -161,7 +161,7 @@ public virtual void UpdateRange(IEnumerable entities) private void SetEntityStateToModifiedIfApplicable(T entity) { var entry = InternalContext.Entry(entity); - if (entry.State == System.Data.Entity.EntityState.Detached || (this.AutoCommitEnabledInternal && !InternalContext.Configuration.AutoDetectChangesEnabled)) + if (entry.State < System.Data.Entity.EntityState.Added || (this.AutoCommitEnabledInternal && !InternalContext.Configuration.AutoDetectChangesEnabled)) { entry.State = System.Data.Entity.EntityState.Modified; } diff --git a/src/Libraries/SmartStore.Data/Extensions/DataReaderExtensions.cs b/src/Libraries/SmartStore.Data/Extensions/DataReaderExtensions.cs index 9581cf4ce4..09aa2eadbb 100644 --- a/src/Libraries/SmartStore.Data/Extensions/DataReaderExtensions.cs +++ b/src/Libraries/SmartStore.Data/Extensions/DataReaderExtensions.cs @@ -4,112 +4,141 @@ using System.Collections.Generic; using System.Data; using System.Reflection; +using System.Linq; +using System.Dynamic; +using SmartStore.ComponentModel; namespace SmartStore.Data { public static class DataReaderExtensions { - /// - /// Creates a list of a given type from all the rows in a DataReader. - /// - /// Note this method uses Reflection so this isn't a high performance - /// operation, but it can be useful for generic data reader to entity - /// conversions on the fly and with anonymous types. - /// - /// - /// An open DataReader that's in position to read - /// Optional - comma delimited list of fields that you don't want to update - /// - /// Optional - Cached PropertyInfo dictionary that holds property info data for this object. - /// Can be used for caching hte PropertyInfo structure for multiple operations to speed up - /// translation. If not passed automatically created. - /// - /// - public static List DataReaderToObjectList(this IDataReader reader, string fieldsToSkip = null, Dictionary piList = null) - where TType : new() - { - if (reader == null) - return null; - - var items = new List(); - - // Create lookup list of property info objects - if (piList == null) - { - piList = new Dictionary(); - var props = typeof(TType).GetProperties(BindingFlags.Instance | BindingFlags.Public); - foreach (var prop in props) - piList.Add(prop.Name.ToLower(), prop); - } + public static object GetValue(this IDataReader reader, string columnName) + { + try + { + if (reader != null && !reader.IsClosed && columnName.HasValue()) + { + int ordinal = reader.GetOrdinal(columnName); + return reader.GetValue(ordinal); + } + } + catch (Exception ex) + { + ex.Dump(); + } + + return null; + } + + public static IEnumerable MapSequence(this IDataReader reader, params string[] fieldsToSkip) + where T : new() + { + Guard.ArgumentNotNull(() => reader); + while (reader.Read()) + { + yield return reader.Map(fieldsToSkip); + } + } + + public static IEnumerable MapSequence(this IDataReader reader, params string[] fieldsToSkip) + { + Guard.ArgumentNotNull(() => reader); + while (reader.Read()) - { - var inst = new TType(); - DataReaderToObject(reader, inst, fieldsToSkip, piList); - items.Add(inst); - } + { + yield return reader.Map(fieldsToSkip); + } + } - return items; - } - - /// - /// Populates the properties of an object from a single DataReader row using - /// Reflection by matching the DataReader fields to a public property on - /// the object passed in. Unmatched properties are left unchanged. - /// - /// You need to pass in a data reader located on the active row you want - /// to serialize. - /// - /// This routine works best for matching pure data entities and should - /// be used only in low volume environments where retrieval speed is not - /// critical due to its use of Reflection. - /// - /// Instance of the DataReader to read data from. Should be located on the correct record (Read() should have been called on it before calling this method) - /// Instance of the object to populate properties on - /// Optional - A comma delimited list of object properties that should not be updated - /// Optional - Cached PropertyInfo dictionary that holds property info data for this object - public static void DataReaderToObject(this IDataReader reader, object instance, string fieldsToSkip = null, Dictionary piList = null) - { - if (reader.IsClosed) - throw new InvalidOperationException("Data reader cannot be used because it's already closed"); - - if (string.IsNullOrEmpty(fieldsToSkip)) - fieldsToSkip = string.Empty; - else - fieldsToSkip = "," + fieldsToSkip + ","; - - fieldsToSkip = fieldsToSkip.ToLower(); - - // create a dictionary of properties to look up - // we can pass this in so we can cache the list once - // for a list operation - if (piList == null) - { - piList = new Dictionary(); - var props = instance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public); - foreach (var prop in props) - piList.Add(prop.Name.ToLower(), prop); - } + /// + /// Populates an object from a single DataReader row using + /// fast reflection routines by matching the DataReader fields to a public property on + /// the object passed in. Unmatched properties are left unchanged. + /// + /// You need to pass in a data reader located on the active row you want + /// to serialize. + /// + /// Instance of the DataReader to read data from. Should be located on the correct record (Read() should have been called on it before calling this method) + /// An array of reader field names to ignore + public static T Map(this IDataReader reader, params string[] fieldsToSkip) + where T : new() + { + if (reader.IsClosed) + throw new InvalidOperationException("Data reader cannot be used because it's already closed"); + + var instance = new T(); + MapObject(reader, instance, fieldsToSkip); + return instance; + } + + public static dynamic Map(this IDataReader reader, params string[] fieldsToSkip) + { + if (reader.IsClosed) + throw new InvalidOperationException("Data reader cannot be used because it's already closed"); + + dynamic instance = new ExpandoObject(); + MapDictionary(reader, instance, fieldsToSkip); + return instance; + } + + public static void Map(this IDataReader reader, object instance, params string[] fieldsToSkip) + { + Guard.ArgumentNotNull(instance, "instance"); - for (int index = 0; index < reader.FieldCount; index++) - { - string name = reader.GetName(index).ToLower(); - if (piList.ContainsKey(name)) - { - var prop = piList[name]; - - if (fieldsToSkip.Contains("," + name + ",")) - continue; - - if ((prop != null) && prop.CanWrite) - { - var val = reader.GetValue(index); - prop.SetValue(instance, (val == DBNull.Value) ? null : val, null); - } - } + if (reader.IsClosed) + throw new InvalidOperationException("Data reader cannot be used because it's already closed"); + + var dict = instance as IDictionary; + + if (dict != null) + { + MapDictionary(reader, dict, fieldsToSkip); } + else + { + MapObject(reader, instance, fieldsToSkip); + } + } + + private static void MapObject(IDataReader reader, object instance, params string[] fieldsToSkip) + { + var fastProperties = FastProperty.GetProperties(instance); + + if (fastProperties.Count == 0) + return; + + for (int i = 0; i < reader.FieldCount; i++) + { + string name = reader.GetName(i); + if (fastProperties.ContainsKey(name)) + { + var fastProp = fastProperties[name]; - return; - } - } + if (fieldsToSkip.Contains(name)) + continue; + + if ((fastProp != null) && fastProp.Property.CanWrite) + { + var dbValue = reader.GetValue(i); + fastProp.SetValue(instance, (dbValue == DBNull.Value) ? null : dbValue); + } + } + } + } + + private static void MapDictionary(IDataReader reader, IDictionary instance, params string[] fieldsToSkip) + { + for (int i = 0; i < reader.FieldCount; i++) + { + string name = reader.GetName(i); + + if (fieldsToSkip.Contains(name)) + continue; + + var dbValue = reader.GetValue(i); + instance[name] = (dbValue == DBNull.Value) ? null : dbValue; + } + } + } } diff --git a/src/Libraries/SmartStore.Data/Extensions/MiscExtensions.cs b/src/Libraries/SmartStore.Data/Extensions/MiscExtensions.cs index ba396e84fe..70efa11f18 100644 --- a/src/Libraries/SmartStore.Data/Extensions/MiscExtensions.cs +++ b/src/Libraries/SmartStore.Data/Extensions/MiscExtensions.cs @@ -1,49 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Data.OleDb; -using System.Data.SqlClient; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Linq; namespace SmartStore { - public static class MiscExtensions { - public static bool IsDatabaseAvailable(this SqlConnectionStringBuilder sb) - { - bool res = false; - try - { - using (SqlConnection conn = new SqlConnection(sb.ToString())) - { - conn.Open(); - res = (conn.State == System.Data.ConnectionState.Open); - conn.Close(); - } - } - catch (Exception) - { - } - return res; - } - - public static T GetValue(this OleDbDataReader reader, string name, T defaultValue = default(T)) + public static bool IsEntityFrameworkProvider(this IQueryProvider provider) { - try - { - object value; - if (reader != null && name.HasValue() && (value = reader[name]) != null && value != DBNull.Value) - { - return (T)Convert.ChangeType(value, typeof(T)); - } - } - catch (Exception exc) - { - exc.Dump(); - } - return defaultValue; + return provider.GetType().FullName == "System.Data.Objects.ELinq.ObjectQueryProvider"; } } diff --git a/src/Libraries/SmartStore.Data/Extensions/SqlDataReaderExtensions.cs b/src/Libraries/SmartStore.Data/Extensions/SqlDataReaderExtensions.cs deleted file mode 100644 index becda17803..0000000000 --- a/src/Libraries/SmartStore.Data/Extensions/SqlDataReaderExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data.SqlClient; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SmartStore -{ - - public static class SqlDataReaderExtensions - { - public static object GetValue(this SqlDataReader reader, string columnName) - { - try - { - if (reader != null && columnName.HasValue()) - { - int ordinal = reader.GetOrdinal(columnName); - return reader.GetValue(ordinal); - } - } - catch (Exception exc) - { - exc.Dump(); - } - return null; - } - } -} diff --git a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportDeploymentMap.cs b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportDeploymentMap.cs index 0a57740d6e..2c49aa7d7a 100644 --- a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportDeploymentMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportDeploymentMap.cs @@ -11,10 +11,12 @@ public ExportDeploymentMap() this.HasKey(x => x.Id); this.Property(x => x.Name).IsRequired().HasMaxLength(100); + this.Property(x => x.ResultInfo).IsMaxLength(); this.Property(x => x.Username).HasMaxLength(400); this.Property(x => x.Password).HasMaxLength(400); this.Property(x => x.Url).HasMaxLength(4000); this.Property(x => x.FileSystemPath).HasMaxLength(400); + this.Property(x => x.SubFolder).HasMaxLength(400); this.Property(x => x.EmailAddresses).HasMaxLength(4000); this.Property(x => x.EmailSubject).HasMaxLength(400); diff --git a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportProfileMap.cs b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportProfileMap.cs index d54bf034e3..adacc3c4c6 100644 --- a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportProfileMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ExportProfileMap.cs @@ -11,7 +11,8 @@ public ExportProfileMap() this.HasKey(x => x.Id); this.Property(x => x.Name).IsRequired().HasMaxLength(100); - this.Property(x => x.FolderName).IsRequired().HasMaxLength(100); + this.Property(x => x.FolderName).IsRequired().HasMaxLength(400); + this.Property(x => x.SystemName).HasMaxLength(400); this.Property(x => x.ProviderSystemName).IsRequired().HasMaxLength(4000); this.Property(x => x.Filtering).IsMaxLength(); this.Property(x => x.Projection).IsMaxLength(); diff --git a/src/Libraries/SmartStore.Data/Mapping/DataExchange/ImportProfileMap.cs b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ImportProfileMap.cs new file mode 100644 index 0000000000..02515fd268 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Mapping/DataExchange/ImportProfileMap.cs @@ -0,0 +1,30 @@ +using System.Data.Entity.ModelConfiguration; +using SmartStore.Core.Domain; + +namespace SmartStore.Data.Mapping.DataExchange +{ + public partial class ImportProfileMap : EntityTypeConfiguration + { + public ImportProfileMap() + { + this.ToTable("ImportProfile"); + this.HasKey(x => x.Id); + + this.Property(x => x.Name).IsRequired().HasMaxLength(100); + this.Property(x => x.FolderName).IsRequired().HasMaxLength(100); + this.Property(x => x.KeyFieldNames).HasMaxLength(1000); + this.Property(x => x.FileTypeConfiguration).IsMaxLength(); + this.Property(x => x.ExtraData).IsMaxLength(); + this.Property(x => x.ColumnMapping).IsMaxLength(); + this.Property(x => x.ResultInfo).IsMaxLength(); + + this.Ignore(x => x.FileType); + this.Ignore(x => x.EntityType); + + this.HasRequired(x => x.ScheduleTask) + .WithMany() + .HasForeignKey(x => x.SchedulingTaskId) + .WillCascadeOnDelete(false); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Mapping/Payments/PaymentMethodMap.cs b/src/Libraries/SmartStore.Data/Mapping/Payments/PaymentMethodMap.cs index 59821c61a8..4af965451a 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Payments/PaymentMethodMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Payments/PaymentMethodMap.cs @@ -12,17 +12,7 @@ public PaymentMethodMap() this.Property(x => x.PaymentMethodSystemName).IsRequired().HasMaxLength(4000); - this.Property(x => x.ExcludedCustomerRoleIds).HasMaxLength(500); - this.Property(x => x.ExcludedCountryIds).HasMaxLength(2000); - this.Property(x => x.ExcludedShippingMethodIds).HasMaxLength(500); - - this.Property(x => x.MinimumOrderAmount).HasPrecision(18, 4); - this.Property(x => x.MaximumOrderAmount).HasPrecision(18, 4); - this.Property(x => x.FullDescription).HasMaxLength(4000); - - this.Ignore(x => x.CountryExclusionContext); - this.Ignore(x => x.AmountRestrictionContext); } } } diff --git a/src/Libraries/SmartStore.Data/Mapping/Shipping/ShippingMethodMap.cs b/src/Libraries/SmartStore.Data/Mapping/Shipping/ShippingMethodMap.cs index e1c566088a..94cf2af062 100644 --- a/src/Libraries/SmartStore.Data/Mapping/Shipping/ShippingMethodMap.cs +++ b/src/Libraries/SmartStore.Data/Mapping/Shipping/ShippingMethodMap.cs @@ -11,13 +11,10 @@ public ShippingMethodMap() this.HasKey(sm => sm.Id); this.Property(sm => sm.Name).IsRequired().HasMaxLength(400); - this.Property(sm => sm.ExcludedCustomerRoleIds).HasMaxLength(500); this.HasMany(sm => sm.RestrictedCountries) .WithMany(c => c.RestrictedShippingMethods) .Map(m => m.ToTable("ShippingMethodRestrictions")); - - this.Ignore(sm => sm.CountryExclusionContext); } } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201403262015448_ImportExcel.cs b/src/Libraries/SmartStore.Data/Migrations/201403262015448_ImportExcel.cs index 3cf4f4c6b5..aa992d333a 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201403262015448_ImportExcel.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201403262015448_ImportExcel.cs @@ -44,7 +44,7 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "{0} von {1} Zeilen verarbeitet."); builder.AddOrUpdate("Admin.Common.ImportFromExcel.QuickStats", - "{0} new, {1} updated - with {2} warning(s) und {3} error(s).", + "{0} new, {1} updated - with {2} warning(s) and {3} error(s).", "{0} neu, {1} aktualisiert - bei {2} Warnung(en) und {3} Fehler(n)."); builder.AddOrUpdate("Admin.Common.ImportFromExcel.ActiveSince", diff --git a/src/Libraries/SmartStore.Data/Migrations/201506181858349_AclRecordCustomerRole.cs b/src/Libraries/SmartStore.Data/Migrations/201506181858349_AclRecordCustomerRole.cs index d6c9752fbd..dbc7b21d2b 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201506181858349_AclRecordCustomerRole.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201506181858349_AclRecordCustomerRole.cs @@ -40,12 +40,10 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.AddOrUpdate("Admin.Configuration.Category.Acl.AssignToSubCategoriesAndProducts.Hint", @"This function assigns the ACL configuration of this category to all subcategories and products included in this category.
    - Please keep in mind you have to save changes in the ACL configuration
    - before you can assign them to all subcategories and products.
    - Attention: Please keep in mind that existing ACL records will be deleted", + Please keep in mind you have to save changes in the ACL configuration before you can assign them to all subcategories and products.
    + Attention: Please keep in mind that existing ACL records will be deleted.", @"Diese Funktion bernimmt die Zugriffsrecht-Konfiguration dieser Warengruppe fr alle Unterwarengruppen und Produkte.
    - Bitte beachten Sie, dass die nderungen der Zugriffsrechte zunchst gespeichert werden mssen,
    - bevor diese fr Unterkategorien und Produkte bernommen werden knnen.
    + Bitte beachten Sie, dass die nderungen der Zugriffsrechte zunchst gespeichert werden mssen, bevor diese fr Unterkategorien und Produkte bernommen werden knnen.
    Vorsicht: Bitte beachten Sie, dass vorhandene Zugriffsrechte berschrieben bzw. gelscht werden."); builder.AddOrUpdate("Admin.Configuration.Category.Stores.AssignToSubCategoriesAndProducts", @@ -54,12 +52,10 @@ bevor diese f builder.AddOrUpdate("Admin.Configuration.Category.Stores.AssignToSubCategoriesAndProducts.Hint", @"This function assigns the store configuration of this category to all subcategories and products included in this category.
    - Please keep in mind you have to save changes in the store configuration
    - before you can assign them to all subcategories and products.
    - Attention: Please keep in mind that existing store mappings will be deleted", + Please keep in mind you have to save changes in the store configuration before you can assign them to all subcategories and products.
    + Attention: Please keep in mind that existing store mappings will be deleted.", @"Diese Funktion bernimmt die Shop-Konfiguration dieser Warengruppe fr alle Unterwarengruppen und Produkte.
    - Bitte beachten Sie, dass die nderungen an der Store-Konfiguration zunchst gespeichert werden mssen,
    - bevor diese fr Unterkategorien und Produkte bernommen werden knnen.
    + Bitte beachten Sie, dass die nderungen an der Store-Konfiguration zunchst gespeichert werden mssen, bevor diese fr Unterkategorien und Produkte bernommen werden knnen.
    Vorsicht: Bitte beachten Sie, dass vorhandene Store-Konfiguration berschrieben bzw. gelscht werden."); builder.AddOrUpdate("Admin.Configuration.Acl.NoRolesDefined", @@ -123,26 +119,35 @@ bevor diese f builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.ShowManufacturersOnHomepage", "Show manufacturers on homepage", "Zeige Hersteller auf der Homepage", - "Specifies whether manufacturers will be displayed on the homepage.", - "Bestimmt ob Hersteller auf der Homepage angezeigt werden."); + "Specifies whether manufacturers are displayed on the homepage.", + "Legt fest, ob Hersteller auf der Homepage angezeigt werden."); builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.ShowManufacturerPictures", "Show manufacturer pictures on homepage", "Zeige Herstellerbilder auf der Homepage", - "Specifies whether manufacturers will be displayed as images or textual links on the homepage.", - "Bestimmt ob Hersteller auf der Homepage als Bilder oder textuelle Links angezeigt werden."); + "Specifies whether manufacturers are displayed as images or textual links on the homepage.", + "Legt fest, ob Hersteller auf der Homepage als Bilder oder textuelle Links angezeigt werden."); builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CustomerNumberEnabled", "Customers can enter a customer number", "Kunden knnen Kundennummer hinterlegen", "Specifies whether customers can enter a customer number.", - "Bestimmt ob Kunden eine Kundennummer hinterlegen knnen."); + "Legt fest, ob Kunden eine Kundennummer hinterlegen knnen."); builder.AddOrUpdate("Account.Fields.CustomerNumber", "Customer number", "Kundennummer"); - + builder.AddOrUpdate("Admin.Configuration.Settings.Tax.VatRequired", + "Customers must enter a VATIN", + "Kunden mssen eine Steuernummer angeben", + "Specifies whether customers must enter a VAT identification number.", + "Legt fest, ob Kunden bei der Registrierung eine Steuernummer angeben mssen."); + + builder.AddOrUpdate("Account.Fields.Vat.Required", + "Please enter your VATIN", + "Bitte geben Sie Ihre Steuernummer an"); + } } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201506211043073_PaymentShippingRestrictions.cs b/src/Libraries/SmartStore.Data/Migrations/201506211043073_PaymentShippingRestrictions.cs index 5cd8878827..7f4f7300a3 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201506211043073_PaymentShippingRestrictions.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201506211043073_PaymentShippingRestrictions.cs @@ -79,88 +79,6 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "Die Sitemap fr den Shop ist nicht erreichbar."); - builder.AddOrUpdate("Admin.Configuration.Payment.Methods.RestrictionNote", - "Select features for which you do not want to offer this payment method.", - "Whlen Sie Merkmale, bei denen Sie diese Zahlungsmethode nicht anbieten mchten."); - - builder.AddOrUpdate("Admin.Configuration.Shipping.Methods.RestrictionNote", - "Select features for which you do not want to offer this shipping method.", - "Whlen Sie Merkmale, bei denen Sie diese Versandart nicht anbieten mchten."); - - - builder.AddOrUpdate("Admin.Configuration.Payment.Methods.ExcludedCustomerRole", - "Customer roles", - "Kundengruppen", - "Specifies customer roles for which the payment method should not be offered.", - "Legt Kundengruppen fest, fr die die Zahlungsmethode nicht angeboten werden soll."); - - builder.AddOrUpdate("Admin.Configuration.Payment.Methods.ExcludedShippingMethod", - "Shipping method", - "Versandarten", - "Specifies shipping methods for which the payment method should not be offered.", - "Legt Versandarten fest, fr die die Zahlungsmethode nicht angeboten werden soll."); - - builder.AddOrUpdate("Admin.Configuration.Payment.Methods.ExcludedCountry", - "Countries", - "Lnder", - "Specifies countries for which the payment method should not be offered.", - "Legt Lnder fest, fr die die Zahlungsmethode nicht angeboten werden soll."); - - builder.AddOrUpdate("Admin.Configuration.Payment.Methods.MinimumOrderAmount", - "Minimum order amount", - "Mindestbestellwert", - "Specifies the minimum order amount from which on the payment method should be offered.", - "Legt den Mindestbestellwert fest, ab dem die Zahlungsmethode angeboten werden soll."); - - builder.AddOrUpdate("Admin.Configuration.Payment.Methods.MaximumOrderAmount", - "Maximum amount", - "Maximalbestellwert", - "Specifies the maximum order amount up to which the payment methods should be offered.", - "Legt den maximalen Bestellwert fest, bis zu dem die Zahlungsmethode angeboten werden soll."); - - - builder.AddOrUpdate("Admin.Configuration.Shipping.Methods.ExcludedCustomerRole", - "Customer roles", - "Kundengruppen", - "Specifies customer roles for which the shipping method should not be offered.", - "Legt Kundengruppen fest, fr die die Versandart nicht angeboten werden soll."); - - builder.AddOrUpdate("Admin.Configuration.Shipping.Methods.ExcludedCountry", - "Countries", - "Lnder", - "Specifies countries for which the shipping method should not be offered.", - "Legt Lnder fest, fr die die Versandart nicht angeboten werden soll."); - - - builder.AddOrUpdate("Admin.Configuration.Restrictions.AmountRestrictionContext", - "Amount related to", - "Betrag bezieht sich auf", - "Specifies the amount to which the minimum and maximum order amounts are related to.", - "Legt den Betrag fest, auf den sich die Mindest- und Maximalbestellwerte beziehen."); - - builder.AddOrUpdate("Admin.Configuration.Restrictions.CountryExclusionContext", - "Countries related to", - "Lnder beziehen sich auf", - "Specifies the address to which the selected countries are related to.", - "Legt die Adresse fest, auf den sich die gewhlten Lnder beziehen."); - - - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Common.CountryRestrictionContextType.BillingAddress", - "Billing address", - "Rechnungsadresse"); - - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Common.CountryRestrictionContextType.ShippingAddress", - "Shipping address", - "Versandadresse"); - - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Common.AmountRestrictionContextType.SubtotalAmount", - "Subtotal", - "Zwischensumme"); - - builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Common.AmountRestrictionContextType.TotalAmount", - "Order total", - "Gesamtsumme"); - builder.Delete("Admin.Configuration.Shipping.Restrictions.Updated"); builder.Delete("Admin.Configuration.Shipping.Restrictions.Description"); builder.Delete("Admin.Configuration.Shipping.Restrictions.Country"); diff --git a/src/Libraries/SmartStore.Data/Migrations/201508042207146_RemoveKeepAlive.cs b/src/Libraries/SmartStore.Data/Migrations/201508042207146_RemoveKeepAlive.cs index 5955672869..8c0dc62ad6 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201508042207146_RemoveKeepAlive.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201508042207146_RemoveKeepAlive.cs @@ -1,14 +1,19 @@ namespace SmartStore.Data.Migrations { - using System; - using System.Data.Entity.Migrations; + using System; + using System.Data.Entity.Migrations; + using System.Web.Hosting; + using Core.Data; using SmartStore.Data.Setup; public partial class RemoveKeepAlive : DbMigration, ILocaleResourcesProvider, IDataSeeder { public override void Up() { - Sql("DELETE FROM [dbo].[ScheduleTask] WHERE [Type] = 'SmartStore.Services.Common.KeepAliveTask, SmartStore.Services'"); + if (HostingEnvironment.IsHosted && DataSettings.Current.IsSqlServer) + { + Sql("DELETE FROM [dbo].[ScheduleTask] WHERE [Type] = 'SmartStore.Services.Common.KeepAliveTask, SmartStore.Services'"); + } } public override void Down() diff --git a/src/Libraries/SmartStore.Data/Migrations/201508091512101_ExportFramework.cs b/src/Libraries/SmartStore.Data/Migrations/201508091512101_ExportFramework.cs index 64bb41f4d8..5203e70451 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201508091512101_ExportFramework.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201508091512101_ExportFramework.cs @@ -124,10 +124,10 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "Legt den Export-Provider fest. Er ist fr die individuelle Formatierung der zu exportierenden Daten zustndig."); builder.AddOrUpdate("Admin.DataExchange.Export.EntityType", - "Entity", - "Entitt", - "The entity type the provider processes.", - "Der Entittstyp, den der Provider verarbeitet."); + "Object", + "Objekt", + "The object type the provider processes.", + "Der Objekttyp, den der Provider verarbeitet."); builder.AddOrUpdate("Admin.DataExchange.Export.Name", @@ -142,25 +142,8 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "The file type of the exported data.", "Der Dateityp der exportierten Daten."); - builder.AddOrUpdate("Admin.DataExchange.Export.LastExecution", - "Last execution", - "Letzte Ausfhrung", - "Information about the last execution of the export.", - "Informationen zur letzten Ausfhrung des Exports."); - builder.AddOrUpdate("Admin.DataExchange.Export.Offset", - "Skip", - "berspringen", - "Specifies the number of records to be skipped.", - "Legt die Anzahl der zu berspringenden Datenstze fest."); - - builder.AddOrUpdate("Admin.DataExchange.Export.Limit", - "Limit", - "Begrenzen", - "Specifies how many records to be loaded at maximum.", - "Legt die maximale Anzahl der zu exportierenden Datenstze fest."); - builder.AddOrUpdate("Admin.DataExchange.Export.BatchSize", "Batch size", "Stapelgre", @@ -482,9 +465,9 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.EmailAddresses", "Email addresses to", - "E-Mail-Addressen an", + "E-Mail Adressen an", "Specifies the email addresses where to send the data.", - "Legt die E-Mail Addressen fest, an die die Daten verschickt werden soll."); + "Legt die E-Mail Adressen fest, an die die Daten verschickt werden soll."); builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.EmailSubject", "Email subject", diff --git a/src/Libraries/SmartStore.Data/Migrations/201508121735397_AddSyncMapping.cs b/src/Libraries/SmartStore.Data/Migrations/201508121735397_AddSyncMapping.cs index b3499775ea..ce062a376e 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201508121735397_AddSyncMapping.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201508121735397_AddSyncMapping.cs @@ -96,7 +96,7 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.ExtraRobotsDisallows", "Extra Disallows for robots.txt", "Extra Disallows fr robots.txt", - "Enter additional paths that should be included as Disallow entries in your robots.txt. Each entry has to entered in a new line.", + "Enter additional paths that should be included as Disallow entries in your robots.txt. Each entry has to be entered in a new line.", "Geben Sie hier zustzliche Pfade an, die als Disallow-Eintrge zur robots.txt hinzugefgt werden sollen. Jeder Eintrag muss in einer neuen Zeile erfolgen."); builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.DefaultSortOrderMode", diff --git a/src/Libraries/SmartStore.Data/Migrations/201509021536425_ExportFramework1.cs b/src/Libraries/SmartStore.Data/Migrations/201509021536425_ExportFramework1.cs index d8e9b8ab9d..09f2dc6314 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201509021536425_ExportFramework1.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201509021536425_ExportFramework1.cs @@ -81,17 +81,17 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.AddOrUpdate("Admin.DataExchange.Export.CompletedEmailAddresses", "Email addresses to", - "E-Mail-Addressen an", + "E-Mail Adressen an", "Specifies the email addresses where to send the notification message.", - "Legt die E-Mail Addressen fest, an die die Benachrichtigung geschickt werden soll."); + "Legt die E-Mail Adressen fest, an die die Benachrichtigung geschickt werden soll."); builder.AddOrUpdate("Admin.DataExchange.Export.CompletedEmail.Subject", "Export of profile \"{0}\" has been finished", - "Export von Profile \"{0}\" ist abgeschlossen"); + "Export von Profil \"{0}\" ist abgeschlossen"); builder.AddOrUpdate("Admin.DataExchange.Export.CompletedEmail.Body", - "This is an automatic notification of store \"{0}\" about a recent data export. You can disable the sending of this message in the details of the export profile.", - "Dies ist eine automatische Benachrichtung von Shop \"{0}\" ber einen erfolgten Datenexport. Sie knnen den Versand dieser Mitteilung in den Details des Exportprofils deaktivieren."); + "This is an automatic notification of store \"{0}\" about a recent data export.", + "Dies ist eine automatische Benachrichtung von Shop \"{0}\" ber einen erfolgten Datenexport."); builder.AddOrUpdate("Admin.DataExchange.Export.FolderName", "Folder name", @@ -100,8 +100,8 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "Legt den Namen des Ordners fest, in den die Daten exportiert werden."); builder.AddOrUpdate("Admin.DataExchange.Export.FolderAndFileName.Validate", - "Please enter a valid folder and file name. Example for file names: %Misc.FileNumber%-%ExportProfile.Id%-gmc-%Store.Name%", - "Bitte einen gltigen Ordner- und Dateinamen eingeben. Beispiel fr Dateinamen: %Misc.FileNumber%-%ExportProfile.Id%-gmc-%Store.Name%"); + "Please enter a valid folder and file name. Example for file names: %File.Index%-%Profile.Id%-gmc-%Store.Name%", + "Bitte einen gltigen Ordner- und Dateinamen eingeben. Beispiel fr Dateinamen: %File.Index%-%Profile.Id%-gmc-%Store.Name%"); builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.CreateZip", @@ -184,8 +184,8 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "Das Exportprofil ist deaktiviert. Fr eine Exportvorschau muss das Exportprofil aktiviert sein."); builder.AddOrUpdate("Admin.DataExchange.Export.NoProfilesForProvider", - "There was no export profile of type \"{0}\" found. Create now a new export profile.", - "Es wurde kein Exportprofil vom Typ \"{0}\" gefunden. Jetzt ein neues Exportprofil anlegen."); + "There was no export profile of type {0} found. Create now a new export profile.", + "Es wurde kein Exportprofil vom Typ {0} gefunden. Jetzt ein neues Exportprofil anlegen."); builder.AddOrUpdate("Admin.DataExchange.Export.ProfileForProvider", "Export profile", diff --git a/src/Libraries/SmartStore.Data/Migrations/201509150931528_ExportFramework2.cs b/src/Libraries/SmartStore.Data/Migrations/201509150931528_ExportFramework2.cs index 6de44115cc..5bab264538 100644 --- a/src/Libraries/SmartStore.Data/Migrations/201509150931528_ExportFramework2.cs +++ b/src/Libraries/SmartStore.Data/Migrations/201509150931528_ExportFramework2.cs @@ -38,8 +38,10 @@ public void Seed(SmartObjectContext context) public void MigrateLocaleResources(LocaleResourcesBuilder builder) { builder.AddOrUpdate("Common.Example", "Example", "Beispiel"); + builder.AddOrUpdate("Common.ShowAll", "Show all", "Alle anzeigen"); builder.AddOrUpdate("Admin.Common.Selected", "Selected", "Ausgewhlte"); - builder.AddOrUpdate("Admin.Common.Entity", "Entity", "Entitt"); + builder.AddOrUpdate("Admin.Common.Entity", "Object", "Objekt"); + builder.AddOrUpdate("Admin.Common.Placeholder", "Placeholder", "Platzhalter"); builder.AddOrUpdate("Admin.Common.FilesDeleted", @@ -54,9 +56,13 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "Cannot load the provider {0}.", "Der Provider {0} konnte nicht geladen werden."); - builder.AddOrUpdate("ActivityLog.EditPaymentMethod", - "Edited payment method '{0}' ({1})", - "Zahlungsart '{0}' ({1}) bearbeitet"); + builder.AddOrUpdate("Admin.Common.NoEntriesSelected", + "No entries have been selected.", + "Es wurden keine Eintrge ausgewhlt."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Blog.ShowHeaderRSSUrl.Hint", + "Check to enable the blog RSS feed link in customers browser address bar.", + "Legt fest, ob der RSS-Feed-Link in der Adressleiste des Browsers angezeigt werden soll."); builder.AddOrUpdate("Admin.System.SeNames", "SEO Names", "SEO Namen"); @@ -69,16 +75,16 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "Legt den SEO Namen fest."); builder.AddOrUpdate("Admin.System.SeNames.EntityId", - "Entity ID", - "ID der Entitt", - "Specifies the ID of the associated entity.", - "Legt die ID der zugehrigen Entitt fest."); + "Object ID", + "Objekt-ID", + "Specifies the ID of the associated object.", + "Legt die ID des zugehrigen Objektes fest."); builder.AddOrUpdate("Admin.System.SeNames.EntityName", - "Entity", - "Entitt", - "Specifies the name of the associated entity.", - "Legt den Namen der zugehrigen Entitt fest."); + "Object", + "Objekt", + "Specifies the name of the associated object.", + "Legt den Namen der zugehrigen Objektes fest."); builder.AddOrUpdate("Admin.System.SeNames.IsActive", "Is active", @@ -93,11 +99,19 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) "Legt die Sprache des SEO Namens fest."); builder.AddOrUpdate("Admin.System.SeNames.SlugsPerEntity", - "Names per entity", - "Namen pro Entitt", - "The number of SEO names per entity.", - "Die Anzahl der SEO Namen pro Entitt."); + "Names per object", + "Namen pro Objekt", + "The number of SEO names per object.", + "Die Anzahl der SEO Namen pro Objekt."); + + builder.AddOrUpdate("Admin.System.SeNames.ActiveSlugAlreadyExist", + "Only one active SEO name should be set per language.", + "Pro Sprache darf nur ein aktiver SEO Name festgelegt werden."); + + builder.AddOrUpdate("Admin.DataExchange.Export.FileNamePatternDescriptions", + "ID of export profil;Folder name of export profil;SEO name of export profil;Store ID;SEO name of store;One based file index;Random number;UTC timestamp", + "ID des Exportprofils;Ordername des Exportprofils;SEO Name des Exportprofils;Shop ID;SEO Name des Shops;Mit 1 beginnender Dateiindex;Zufallszahl;UTC Zeitstempel"); builder.AddOrUpdate("Admin.DataExchange.Export.NotPreviewCompatible", "This option is not taken into account in the preview.", @@ -116,8 +130,8 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.AddOrUpdate("Admin.DataExchange.Export.Projection.NoGroupedProducts", "Do not export grouped products", "Keine Gruppenprodukte exportieren", - "Specifies whether to export grouped products. If this option is deactivated, then the associated products will be deactivated.", - "Legt fest, ob Gruppenprodukte exportiert werden sollen. Ist diese Option deaktiviert, so werden die zur Gruppe gehrenden Produkte exportiert."); + "Specifies whether to export grouped products. If this option is activated, then the associated products will be exported.", + "Legt fest, ob Gruppenprodukte exportiert werden sollen. Ist diese Option aktiviert, so werden die zur Gruppe gehrenden Produkte exportiert."); builder.AddOrUpdate("Admin.DataExchange.Export.NoFiltering", "There is no filtering available.", @@ -222,6 +236,41 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder) builder.Delete("Plugins.Widgets.OpenTrans.IsLexwareCompatibe"); builder.Delete("Admin.System.Maintenance.DeleteExportedFolders.TotalDeleted"); + + // Common + builder.AddOrUpdate("StoreClosed", + "We'll be back.", + "Wir sind bald wieder da."); + builder.AddOrUpdate("StoreClosed.Hint", + "We're busy updating our online store for you and will be back soon.", + "Wir aktualisieren gerade das Angebot in unserem Online-Shop. Die Seite ist demnchst wieder verfgbar."); + + builder.AddOrUpdate("Admin.System.SystemInfo.UsedMemorySize", + "Used memory (RAM)", + "Benutzter Speicher (RAM)"); + builder.AddOrUpdate("Admin.System.SystemInfo.GarbageCollect", + "Collect", + "Aufrumen"); + builder.AddOrUpdate("Admin.System.SystemInfo.GarbageCollectSuccessful", + "The memory has been successfully cleaned up.", + "Der Arbeitsspeicher wurde erfolgreich aufgerumt."); + + builder.AddOrUpdate("Admin.Configuration.Themes.NoConfigurationRequired", + "Theme requires no configuration", + "Theme bentigt keine Konfiguration"); + + + builder.AddOrUpdate("Tax.LegalInfoFooter2", + "* All prices {0}, plus shipping", + "* Alle Preise {0}, zzgl. Versandkosten"); + + builder.AddOrUpdate("Tax.LegalInfoProductDetail2", + "{0} {1} {2}plus shipping", + "{0} {1} {2} zzgl. Versandkosten"); + + builder.AddOrUpdate("ShoppingCart.ShippingInfoLink", + "For a complete listing of all shipping costs please click here.", + "Eine vollstndige Liste aller Versandkosten finden Sie hier."); } } } diff --git a/src/Libraries/SmartStore.Data/Migrations/201511271019577_ExportFramework3.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201511271019577_ExportFramework3.Designer.cs new file mode 100644 index 0000000000..766e90a409 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201511271019577_ExportFramework3.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class ExportFramework3 : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(ExportFramework3)); + + string IMigrationMetadata.Id + { + get { return "201511271019577_ExportFramework3"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201511271019577_ExportFramework3.cs b/src/Libraries/SmartStore.Data/Migrations/201511271019577_ExportFramework3.cs new file mode 100644 index 0000000000..ffbf880d57 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201511271019577_ExportFramework3.cs @@ -0,0 +1,142 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using Core.Domain.Customers; + using Core.Domain.Security; + using Setup; + + public partial class ExportFramework3 : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.ExportProfile", "SystemName", c => c.String(maxLength: 400)); + AddColumn("dbo.ExportProfile", "IsSystemProfile", c => c.Boolean(nullable: false)); + DropColumn("dbo.PaymentMethod", "ExcludedCustomerRoleIds"); + DropColumn("dbo.PaymentMethod", "ExcludedCountryIds"); + DropColumn("dbo.PaymentMethod", "ExcludedShippingMethodIds"); + DropColumn("dbo.PaymentMethod", "CountryExclusionContextId"); + DropColumn("dbo.PaymentMethod", "MinimumOrderAmount"); + DropColumn("dbo.PaymentMethod", "MaximumOrderAmount"); + DropColumn("dbo.PaymentMethod", "AmountRestrictionContextId"); + DropColumn("dbo.ShippingMethod", "ExcludedCustomerRoleIds"); + DropColumn("dbo.ShippingMethod", "CountryExclusionContextId"); + } + + public override void Down() + { + AddColumn("dbo.ShippingMethod", "CountryExclusionContextId", c => c.Int(nullable: false)); + AddColumn("dbo.ShippingMethod", "ExcludedCustomerRoleIds", c => c.String(maxLength: 500)); + AddColumn("dbo.PaymentMethod", "AmountRestrictionContextId", c => c.Int(nullable: false)); + AddColumn("dbo.PaymentMethod", "MaximumOrderAmount", c => c.Decimal(precision: 18, scale: 4)); + AddColumn("dbo.PaymentMethod", "MinimumOrderAmount", c => c.Decimal(precision: 18, scale: 4)); + AddColumn("dbo.PaymentMethod", "CountryExclusionContextId", c => c.Int(nullable: false)); + AddColumn("dbo.PaymentMethod", "ExcludedShippingMethodIds", c => c.String(maxLength: 500)); + AddColumn("dbo.PaymentMethod", "ExcludedCountryIds", c => c.String(maxLength: 2000)); + AddColumn("dbo.PaymentMethod", "ExcludedCustomerRoleIds", c => c.String(maxLength: 500)); + DropColumn("dbo.ExportProfile", "IsSystemProfile"); + DropColumn("dbo.ExportProfile", "SystemName"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + + var permissionMigrator = new PermissionMigrator(context); + + permissionMigrator.AddPermission(new PermissionRecord + { + Name = "Admin area. Manage Url Records", + SystemName = "ManageUrlRecords", + Category = "Configuration" + }, new string[] { SystemCustomerRoleNames.Administrators }); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.Common.Export.PDF", "PDF Export", "PDF Export"); + builder.AddOrUpdate("Admin.Common.TemporaryFiles", "Temporary files", "Temporre Dateien"); + builder.AddOrUpdate("Admin.Common.PublicFiles", "Public files", "ffentliche Dateien"); + builder.AddOrUpdate("Admin.Common.Of", "of", "von"); + + builder.AddOrUpdate("Admin.Common.NoTempFilesFound", + "There are no temporary files.", + "Es sind keine temporren Dateien vorhanden."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportEntityType.NewsLetterSubscription", + "Newsletter Subscribers", + "Newsletter Abonnenten"); + + builder.AddOrUpdate("Admin.DataExchange.Export.SystemName", + "System name of profile", + "Systemname des Profils", + "The system name of the export profile.", + "Der Systemname des Exportprofils."); + + builder.AddOrUpdate("Admin.DataExchange.Export.IsSystemProfile", + "System profile", + "Systemprofil", + "Indicates whether the export profile is a system profile. System profiles cannot be removed.", + "Gibt an, ob es sich bei dem Exportprofil um eine Systemprofil handelt. Systemprofile knnen nicht entfernt werden."); + + builder.AddOrUpdate("Admin.DataExchange.Export.CannotDeleteSystemProfile", + "Cannot delete a system export profile.", + "Ein System-Exportprofil kann nicht gelscht werden."); + + builder.AddOrUpdate("Admin.DataExchange.Export.MissingSystemProfile", + "The system export profile {0} was not found.", + "Das System-Exportprofil {0} wurde nicht gefunden."); + + builder.AddOrUpdate("Admin.DataExchange.Export.ExportFiles", + "Export files", + "Exportdateien"); + + + builder.AddOrUpdate("Admin.Configuration.Payment.Methods.RestrictionNote", + "There were no possibilities found to restrict payment methods.", + "Es wurden keine Mglichkeiten zur Einschrnkung von Zahlungsarten gefunden."); + + builder.AddOrUpdate("Admin.Configuration.Shipping.Methods.RestrictionNote", + "There were no possibilities found to restrict shipping methods.", + "Es wurden keine Mglichkeiten zur Einschrnkung von Versandarten gefunden."); + + + + builder.Delete( + "Admin.Configuration.Payment.Methods.ExcludedCustomerRole", + "Admin.Configuration.Payment.Methods.ExcludedShippingMethod", + "Admin.Configuration.Payment.Methods.ExcludedCountry", + "Admin.Configuration.Payment.Methods.MinimumOrderAmount", + "Admin.Configuration.Payment.Methods.MaximumOrderAmount", + "Admin.Configuration.Restrictions.AmountRestrictionContext", + "Enums.SmartStore.Core.Domain.Common.AmountRestrictionContextType.SubtotalAmount", + "Enums.SmartStore.Core.Domain.Common.AmountRestrictionContextType.TotalAmount", + "Enums.SmartStore.Core.Domain.Common.CountryRestrictionContextType.BillingAddress", + "Enums.SmartStore.Core.Domain.Common.CountryRestrictionContextType.ShippingAddress", + "Admin.Configuration.Shipping.Methods.ExcludedCustomerRole", + "Admin.Configuration.Shipping.Methods.ExcludedCountry", + "Admin.Configuration.Restrictions.CountryExclusionContext", + "Admin.Common.ExportToXml.All", + "Admin.Common.ExportToXml.Selected", + "Admin.Common.ExportToCsv.All", + "Admin.Common.ExportToCsv.Selected", + "Admin.Common.ExportToCsv", + "Admin.Common.ExportToPdf.TocTitle", + "PDFProductCatalog.SKU", + "PDFProductCatalog.Price", + "PDFProductCatalog.Manufacturer", + "PDFProductCatalog.Weight", + "PDFProductCatalog.Length", + "PDFProductCatalog.Width", + "PDFProductCatalog.Height", + "PDFProductCatalog.SpecificationAttributes", + "PDFProductCatalog.BundledItems", + "PDFProductCatalog.AssociatedProducts" + ); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201511271019577_ExportFramework3.resx b/src/Libraries/SmartStore.Data/Migrations/201511271019577_ExportFramework3.resx new file mode 100644 index 0000000000..9c408d2cf0 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201511271019577_ExportFramework3.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/201512151526290_ImportFramework.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201512151526290_ImportFramework.Designer.cs new file mode 100644 index 0000000000..a112acac53 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201512151526290_ImportFramework.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class ImportFramework : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(ImportFramework)); + + string IMigrationMetadata.Id + { + get { return "201512151526290_ImportFramework"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201512151526290_ImportFramework.cs b/src/Libraries/SmartStore.Data/Migrations/201512151526290_ImportFramework.cs new file mode 100644 index 0000000000..a7d561921c --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201512151526290_ImportFramework.cs @@ -0,0 +1,421 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using Core.Domain.Customers; + using Core.Domain.Security; + using Core.Domain.Seo; + using Setup; + + public partial class ImportFramework : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + CreateTable( + "dbo.ImportProfile", + c => new + { + Id = c.Int(nullable: false, identity: true), + Name = c.String(nullable: false, maxLength: 100), + FolderName = c.String(nullable: false, maxLength: 100), + FileTypeId = c.Int(nullable: false), + EntityTypeId = c.Int(nullable: false), + Enabled = c.Boolean(nullable: false), + Skip = c.Int(nullable: false), + Take = c.Int(nullable: false), + FileTypeConfiguration = c.String(), + ColumnMapping = c.String(), + SchedulingTaskId = c.Int(nullable: false), + }) + .PrimaryKey(t => t.Id) + .ForeignKey("dbo.ScheduleTask", t => t.SchedulingTaskId) + .Index(t => t.SchedulingTaskId); + + } + + public override void Down() + { + DropForeignKey("dbo.ImportProfile", "SchedulingTaskId", "dbo.ScheduleTask"); + DropIndex("dbo.ImportProfile", new[] { "SchedulingTaskId" }); + DropTable("dbo.ImportProfile"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + + var permissionMigrator = new PermissionMigrator(context); + var activityLogMigrator = new ActivityLogTypeMigrator(context); + + permissionMigrator.AddPermission(new PermissionRecord + { + Name = "Admin area. Manage Imports", + SystemName = "ManageImports", + Category = "Configuration" + }, new string[] { SystemCustomerRoleNames.Administrators }); + + activityLogMigrator.AddActivityLogType("DeleteOrder", "Delete order", "Auftrag gelscht"); + + context.MigrateSettings(x => + { + var seoSettings = new SeoSettings(); + x.Add("seosettings.seonamecharconversion", seoSettings.SeoNameCharConversion); + }); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.Common.RecordsSkip", + "Skip", + "berspringen", + "Specifies the number of records to be skipped.", + "Legt die Anzahl der zu berspringenden Datenstze fest."); + + builder.AddOrUpdate("Common.Unknown", "Unknown", "Unbekannt"); + builder.AddOrUpdate("Common.Unavailable", "Unavailable", "Nicht verfgbar"); + builder.AddOrUpdate("Common.Language", "Language", "Sprache"); + builder.AddOrUpdate("Admin.Common.ImportFile", "Import file", "Importdatei"); + builder.AddOrUpdate("Admin.Common.ImportFiles", "Import files", "Importdateien"); + builder.AddOrUpdate("Admin.Common.CsvConfiguration", "CSV Configuration", "CSV Konfiguration"); + + builder.AddOrUpdate("Admin.Common.RecordsTake", + "Limit", + "Begrenzen", + "Specifies the maximum number of records to be processed.", + "Legt die maximale Anzahl der zu verarbeitenden Datenstze fest."); + + builder.AddOrUpdate("Admin.Common.FileTypeMustEqual", + "The file must be of the type {0}.", + "Die Datei muss vom Typ {0} sein."); + + builder.AddOrUpdate("Admin.DataExchange.Import.NoProfiles", + "There were no import profiles found.", + "Es wurden keine Importprofile gefunden."); + + builder.AddOrUpdate("Admin.DataExchange.Import.Name", + "Name of profile", + "Name des Profils", + "Specifies the name of the import profile.", + "Legt den Namen des Importprofils fest."); + + builder.AddOrUpdate("Admin.DataExchange.Import.ProgressInfo", + "{0} of {1} records processed", + "{0} von {1} Datenstzen verarbeitet"); + + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ImportEntityType.Product", "Product", "Produkt"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ImportEntityType.Customer", "Customer", "Kunde"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ImportEntityType.NewsLetterSubscription", "Newsletter Subscriber", "Newsletter Abonnent"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ImportEntityType.Category", "Category", "Warengruppe"); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ImportFileType.CSV", "Delimiter separated values (.csv, .txt, .tab)", "Trennzeichen getrennte Werte (.csv, .txt, .tab)"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ImportFileType.XLSX", "Excel (.xlsx)", "Excel (.xlsx)"); + + builder.AddOrUpdate("Admin.DataExchange.Import.FileUpload", + "Upload import file...", + "Importdatei hochladen..."); + + builder.AddOrUpdate("Admin.DataExchange.Import.MissingImportFile", + "Please upload an import file.", + "Bitte laden Sie eine Importdatei hoch."); + + builder.AddOrUpdate("Admin.DataExchange.Csv.QuoteAllFields", + "Quote all fields", + "Alle Felder in Anfhrungszeichen", + "Specifies whether to set quotation marks around all field values.", + "Legt fest, ob die Werte aller Felder in Anfhrungszeichen gestellt werden sollen."); + + builder.AddOrUpdate("Admin.DataExchange.Csv.TrimValues", + "Trim values", + "berflssige Leerzeichen entfernen", + "Specifies whether to remove space characters at start and end of a field value.", + "Legt fest, ob Leerzeichen am Anfang und am Ende eines Feldwertes entfernt werden sollen."); + + builder.AddOrUpdate("Admin.DataExchange.Csv.SupportsMultiline", + "Supports multilines", + "Mehrzeilen erlaubt", + "Specifies whether field values with multilines are supported.", + "Legt fest, ob mehrzeilige Feldwerte untersttzt werden."); + + builder.AddOrUpdate("Admin.DataExchange.Csv.Delimiter", + "Delimiter", + "Trennzeichen", + "Specifies the field separator.", + "Legt das zu verwendende Trennzeichen fr die Felder fest."); + + builder.AddOrUpdate("Admin.DataExchange.Csv.Quote", + "Quote character", + "Anfhrungszeichen", + "Specifies the quotation character.", + "Legt das zu verwendende Anfhrungszeichen fest."); + + builder.AddOrUpdate("Admin.DataExchange.Csv.Escape", + "Inner quote character", + "Inneres Anfhrungszeichen", + "Specifies the inner quote character used for escaping.", + "Legt das innere Anfhrungszeichen (Escaping) fest."); + + builder.AddOrUpdate("Admin.DataExchange.Csv.Delimiter.Validation", + "Please enter a valid delimiter.", + "Geben Sie bitte ein gltiges Trennzeichen ein."); + + builder.AddOrUpdate("Admin.DataExchange.Csv.Quote.Validation", + "Please enter a valid quote character.", + "Geben Sie bitte ein gltiges Anfhrungszeichen ein."); + + builder.AddOrUpdate("Admin.DataExchange.Csv.Escape.Validation", + "Please enter a valid inner quote character (escaping).", + "Geben Sie bitte ein gltiges, inneres Anfhrungszeichen (Escaping) ein."); + + builder.AddOrUpdate("Admin.DataExchange.Csv.EscapeDelimiter.Validation", + "Delimiter and inner quote character cannot be equal in CSV files.", + "Trennzeichen und inneres Anfhrungszeichen knnen in CSV Dateien nicht gleich sein."); + + builder.AddOrUpdate("Admin.DataExchange.Csv.QuoteDelimiter.Validation", + "Delimiter and quote character cannot be equal in CSV files.", + "Trennzeichen und Anfhrungszeichen knnen in CSV Dateien nicht gleich sein."); + + + builder.AddOrUpdate("Admin.Catalog.Products.Fields.BasePriceMeasureUnit", "Base price measure unit", "Grundpreis Maeinheit"); + builder.AddOrUpdate("Admin.Catalog.Products.Fields.ApprovedRatingSum", "Approved rating sum", "Summe genehmigter Bewertungen"); + builder.AddOrUpdate("Admin.Catalog.Products.Fields.NotApprovedRatingSum", "Not approved rating sum", "Summe nicht genehmigter Bewertungen"); + builder.AddOrUpdate("Admin.Catalog.Products.Fields.ApprovedTotalReviews", "Approved total reviews", "Summe genehmigter Rezensionen"); + builder.AddOrUpdate("Admin.Catalog.Products.Fields.NotApprovedTotalReviews", "Not approved total reviews", "Summe nicht genehmigter Rezensionen"); + builder.AddOrUpdate("Admin.Catalog.Products.Fields.HasTierPrices", "Has tier prices", "Hat Staffelpreise"); + builder.AddOrUpdate("Admin.Catalog.Products.Fields.LowestAttributeCombinationPrice", "Lowest attribute combination price", "Niedrigster Attributkombinationspreis"); + builder.AddOrUpdate("Admin.Catalog.Products.Fields.HasDiscountsApplied", "Has discounts applied", "Hat angewendete Rabatte"); + + builder.AddOrUpdate("Admin.Catalog.Categories.Fields.ParentCategory", "Parent category", "bergeordnete Warengruppe"); + + builder.AddOrUpdate("Admin.Customers.Customers.Fields.CustomerGuid", "Customer GUID", "Kunden GUID"); + builder.AddOrUpdate("Admin.Customers.Customers.Fields.PasswordSalt", "Password salt", "Passwort Salt"); + builder.AddOrUpdate("Admin.Customers.Customers.Fields.IsSystemAccount", "Is system account", "Ist Systemkonto"); + builder.AddOrUpdate("Admin.Customers.Customers.Fields.LastLoginDateUtc", "Last login date", "Letztes Login-Datum"); + + builder.AddOrUpdate("Admin.Promotions.NewsLetterSubscriptions.Fields.NewsLetterSubscriptionGuid", "Subscription GUID", "Abonnement GUID"); + + builder.AddOrUpdate("Admin.DataExchange.ColumnMapping.Note", + "You can optionally set for each field of the import file whether and for which object property the data should be imported. Fields with equal names are always imported as long as they are not explicitly ignored. Not yet selected properties are highlighted in the selection list. It is also possible to define a default value which is applied when the import field is empty. Stored assignments becomes invalid and reset when the delimiter changes.", + "Sie knnen optional fr jedes Feld der Importdatei festlegen, ob und nach welcher Objekteigenschaft dessen Daten zu importieren sind. Gleichnamige Felder werden grundstzlich immer importiert, sofern sie nicht explizit ignoriert werden sollen. Noch nicht ausgewhlte Eigenschaften sind in der Auswahlliste hervorgehoben. Zudem ist die Angabe eines Standardwertes mglich, der angewendet wird, wenn das Importfeld leer ist. Durch nderung des Trennzeichens werden gespeicherte Zuordnungen ungltig und zurckgesetzt."); + + builder.AddOrUpdate("Admin.DataExchange.ColumnMapping.ImportField", "Import Field", "Importfeld"); + builder.AddOrUpdate("Admin.DataExchange.ColumnMapping.EntityProperty", "Object property", "Eigenschaft des Objektes"); + builder.AddOrUpdate("Admin.DataExchange.ColumnMapping.DefaultValue", "Default Value", "Standardwert"); + + + builder.Delete( + "Admin.DataExchange.Export.LastExecution", + "Admin.DataExchange.Export.Offset", + "Admin.DataExchange.Export.Limit", + "Admin.Promotions.NewsLetterSubscriptions.ImportEmailsSuccess", + "Admin.Common.ImportFromCsv", + "Admin.Common.CsvFile", + + "Admin.Common.ImportFromExcel", + "Admin.Common.ExcelFile", + "Admin.Common.ImportFromExcel.InProgress", + "Admin.Common.ImportFromExcel.LastResultTitle", + "Admin.Common.ImportFromExcel.ProcessedCount", + "Admin.Common.ImportFromExcel.QuickStats", + "Admin.Common.ImportFromExcel.ActiveSince", + "Admin.Common.ImportFromExcel.CancelPrompt", + "Admin.Common.ImportFromExcel.Cancel", + "Admin.Common.ImportFromExcel.Cancelled", + "Admin.Common.ImportFromExcel.DownloadReport", + "Admin.Common.ImportFromExcel.NoReportAvailable", + + "Admin.Configuration.ActivityLog.ActivityLog.Fields.ActivityLogTypeColumn", + "Plugins.ExchangeRate.EcbExchange.SetCurrencyToEURO" + ); + + builder.AddOrUpdate("ActivityLog.DeleteOrder", "Deleted order {0}", "Auftrag {0} gelscht"); + + builder.AddOrUpdate("Admin.System.SystemCustomerNames.SearchEngine", "Search Engine", "Suchmaschine"); + builder.AddOrUpdate("Admin.System.SystemCustomerNames.BackgroundTask", "Background Task", "Geplante Aufgabe"); + builder.AddOrUpdate("Admin.System.SystemCustomerNames.PdfConverter", "PDF Converter", "PDF-Konvertierer"); + + builder.AddOrUpdate("Admin.Configuration.ActivityLog.ActivityLog.Fields.CustomerSystemAccount", + "Customer system account", + "Kundensystemkonto", + "Filters results by customer system accounts.", + "Filtert Ergebnisse nach Kundenystemkonten."); + + builder.AddOrUpdate("Admin.Configuration.ActivityLog.ActivityLog.Fields.CustomerEmail", + "Customer Email", + "Kunden-E-Mail", + "Filters results by customer email address.", + "Filtert Ergebnisse nach E-Mail-Adresse der Kunden."); + + builder.AddOrUpdate("Admin.Configuration.Plugins.UnknownError", + "An unknown error occurred when calling a plugin. Please refer to the following message for details.", + "Beim Aufruf eines Plugins ist ein unbekannter Fehler aufgetreten. Details entnehmen Sie bitte der folgenden Meldung."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.AllowUnicodeCharsInUrls", + "Allow unicode characters", + "Unicode-Zeichen erlauben", + "Check whether SEO names can contain letters that are classified as unicode characters.", + "Legt fest, ob als Unicode-Zeichen eingestufte Buchstaben in SEO relevanten Namen erlaubt sind."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.SeoNameCharConversion", + "Characters to be converted", + "Zu konvertierende Zeichen", + "Allows an individual conversion of characters for SEO name creation. Enter the old and the new character separated by a semicolon, e.g. ;ae. Each entry has to be entered in a new line.", + "Ermglicht das individuelle Konvertieren von Zeichen bei der Erstellung SEO Namen. Geben Sie hier durch Semikolon getrennt das alte und das neue Zeichen ein, z.B. ;ae. Jeder Eintrag muss in einer neuen Zeile erfolgen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.TestSeoNameCreation", + "Check string", + "Zeichenkette prfen", + "Enter any string to check the SEO name creation. Changed settings must be saved before.", + "Geben Sie eine beliebige Zeichenkette ein, um daraus den SEO Namen zu erstellen. Genderte Einstellungen mssen zuvor gespeichert werden."); + + + builder.AddOrUpdate("Admin.System.Warnings.NoPermissionsDefined", + "There are no permissions defined.", + "Es sind keine Zugriffsrechte festgelegt."); + + builder.AddOrUpdate("Admin.System.Warnings.NoCustomerRolesDefined", + "There are no customer roles defined.", + "Es sind keine Kundengruppen festgelegt."); + + builder.AddOrUpdate("Admin.System.Warnings.AccessDeniedToAnonymousRequest", + "Access denied to anonymous request on {0}.", + "Zugriffsverweigerung durch anonyme Anfrage bei {0}."); + + builder.AddOrUpdate("Admin.System.Warnings.AccessDeniedToUser", + "Access denied to user #{0} '{1}' on {2}.", + "Zugriffsverweigerung durch Kunde #{0} '{1}' bei {2}."); + + builder.AddOrUpdate("Admin.Configuration.Countries.CannotDeleteDueToAssociatedAddresses", + "The country cannot be deleted because it has associated addresses.", + "Das Land kann nicht gelscht werden, weil ihm Adressen zugeordnet sind."); + + builder.AddOrUpdate("Admin.Configuration.Countries.States.CantDeleteWithAddresses", + "The state\\province cannot be deleted because it has associated addresses.", + "Das Bundesland\\Region kann nicht gelscht werden, weil ihm Adressen zugeordnet sind."); + + builder.AddOrUpdate("Admin.Configuration.Shipping.Methods.NoMethodsLoaded", + "No shipping methods could be loaded.", + "Es konnten keine Versandarten geladen werden."); + + builder.AddOrUpdate("Admin.System.Warnings.NoShipmentItems", + "No shipment items", + "Keine Versand-Artikel"); + + builder.AddOrUpdate("Admin.System.Warnings.DigitsOnly", + "Please enter digits only.", + "Bitte nur Ziffern eingeben."); + + builder.AddOrUpdate("Account.Register.Errors.CannotRegisterSearchEngine", + "A search engine can't be registered.", + "Eine Suchmaschine kann nicht registriert werden."); + + builder.AddOrUpdate("Account.Register.Errors.CannotRegisterTaskAccount", + "A background task account can't be registered.", + "Das Konto einer geplanten Aufgabe kann nicht registriert werden."); + + builder.AddOrUpdate("Account.Register.Errors.AlreadyRegistered", + "The customer is already registered.", + "Der Kunde ist bereits registriert."); + + builder.AddOrUpdate("Admin.Customers.CustomerRoles.CannotFoundRole", + "The customer role \"{0}\" cannot be found.", + "Die Kundengruppe \"{0}\" wurde nicht gefunden."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.RegisterCustomerRole", + "Customer role at registrations", + "Kundengruppe bei Registrierungen", + "Specifies a customer role that will be assigned to newly registered customers.", + "Legt eine Kundengruppe fest, die neu registrierten Kunden zugeordnet wird."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Order.DisplayOrdersOfAllStores", + "Display orders of all stores", + "Auftrge aller Shops anzeigen", + "Specifies whether to display the orders of all stores to the customer. If this option is disabled, only the orders of the current store are displayed.", + "Legt fest, ob dem Kunden die Auftrge aller Shops angezeigt werden sollen. Ist diese Option deaktiviert, so werden nur die Auftrge des aktuellen Shops angezeigt."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Order.GiftCards_Deactivated") + .Value("de", "Geschenkgutschein wird deaktiviert, wenn Auftragsstatus..."); + + builder.AddOrUpdate("Admin.Configuration.Languages.Fields.UniqueSeoCode.Required", + "Please select a SEO language code.", + "Bitte legen Sie einen SEO Sprach-Code fest."); + + builder.AddOrUpdate("Admin.Configuration.Languages.Fields.FlagImageFileName", + "Flag image", + "Flaggenbild", + "Specifies the flag image. The files for the flag images must be stored in /Content/Images/flags/.", + "Legt das Flaggenbild fest. Die Dateien der Flaggenbilder mssen in /Content/Images/flags/ liegen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.HideCategoryDefaultPictures", + "Hide default picture for categories", + "Standardbild bei Warengruppen ausblenden", + "Specifies whether to hide the default image for categories. The default image is shown when no image is assigned to a category.", + "Legt fest, ob das Standardbild bei Warengruppen ausgeblendet werden soll. Das Standardbild wird angezeigt, wenn der Warengruppe kein Bild zugeordnet ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.HideProductDefaultPictures", + "Hide default picture for products", + "Standardbild bei Produkten ausblenden", + "Specifies whether to hide the default image for products. The default image is shown when no image is assigned to a product.", + "Legt fest, ob das Standardbild bei Produkten ausgeblendet werden soll. Das Standardbild wird angezeigt, wenn dem Produkt kein Bild zugeordnet ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Media.MessageProductThumbPictureSize", + "Thumbnail size of products in emails", + "Thumbnail-Gre von Produkten in E-Mails", + "Specifies the thumbnail image size (pixels) of products in emails. Enter 0 to not display thumbnails.", + "Legt die Thumbnail-Bildgre (in Pixel) von Produkten in E-Mails fest. Geben Sie 0 ein, um keine Thumbnails anzuzeigen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.MetaRobotsContent", + "Meta robots", + "Meta Robots", + "Specifies if and how search engines indexing the pages of your store.", + "Legt fest, ob und wie Suchmaschinen die Seiten Ihres Shops indexieren."); + + builder.AddOrUpdate("Providers.ExchangeRate.EcbExchange.SetCurrencyToEURO", + "You can use ECB (European central bank) exchange rate provider only when exchange rate currency code is set to EURO.", + "Der EZB-Wechselkursdienst kann nur genutzt werden, wenn der Wechselkurs-Whrungscode auf EUR gesetzt ist."); + + + builder.AddOrUpdate("Common.Loading", "Loading", "Lade"); + builder.AddOrUpdate("Common.ShowMore", "Show more", "Mehr anzeigen"); + builder.AddOrUpdate("Common.Published", "Published", "Verffentlicht"); + builder.AddOrUpdate("Common.Unpublished", "Unpublished", "Unverffentlicht"); + builder.AddOrUpdate("Common.NotSelectable", "Not selectable", "Nicht auswhlbar"); + + builder.AddOrUpdate("Common.EntityPicker.SinglePickNote", + "Click on an item to select it and OK to apply it.", + "Klicken Sie auf ein Element, um es auszuwhlen und OK, um es zu bernehmen."); + + builder.AddOrUpdate("Common.EntityPicker.MultiPickNote", + "Click on an item to select or deselect it and OK to apply the selection.", + "Klicken Sie auf ein Element, um es aus- bzw. abzuwhlen und OK, um die Auswahl zu bernehmen."); + + builder.AddOrUpdate("Common.EntityPicker.NoMoreItemsFound", + "There were no more items found.", + "Es wurden keine weiteren Elemente gefunden."); + + builder.AddOrUpdate("Admin.Catalog.Products.BundleItems.NotesOnProductBundles", + "Notes on product bundles", + "Hinweise zu Produkt-Bundles"); + + builder.AddOrUpdate("Admin.Catalog.Products.RelatedProducts.AddNew", + "Add cross-selling product", + "Cross-Selling-Produkt hinzufgen"); + + builder.AddOrUpdate("Admin.Catalog.Products.RelatedProducts.SaveBeforeEdit", + "You need to save the product before you can add cross-selling products for this product page.", + "Sie mssen das Produkt speichern, bevor Sie Cross-Selling-Produkte hinzufgen knnen."); + + builder.AddOrUpdate("Admin.Catalog.Products.CrossSells.AddNew", + "Add checkout-selling product", + "Checkout-Selling-Produkt hinzufgen"); + + builder.AddOrUpdate("Admin.Catalog.Products.CrossSells.SaveBeforeEdit", + "You need to save the product before you can add checkout-selling products for this product page.", + "Sie mssen das Produkt speichern, bevor Sie Checkout-Selling-Produkte hinzufgen knnen."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201512151526290_ImportFramework.resx b/src/Libraries/SmartStore.Data/Migrations/201512151526290_ImportFramework.resx new file mode 100644 index 0000000000..fa9e8cc53c --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201512151526290_ImportFramework.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 + + + H4sIAAAAAAAEAOy923LcuLIo+D4R8w8OP50zsY/ddq+OWLuje05IsmQrti9akmzvvV4UFAuSuMwiq3nRpSfmy+ZhPun8wgHAGwgk7iBZpa4Xu0QkEkAiM5FIJBL/6//7/3/7n4/r9MU9Ksokz35/+ebVTy9foCzOV0l2+/vLurr5H39/+T//7//z//jteLV+fPGtg/uZwOGaWfn7y7uq2vz6+nUZ36F1VL5aJ3GRl/lN9SrO16+jVf767U8//fvrN29eI4ziJcb14sVv53VWJWtE/8B/HuVZjDZVHaWf8hVKy/Y7LrmgWF98jtao3EQx+v3lxToqqosqL9Crd1EVvXxxkCYR7sYFSm9evoiyLK+iCnfy168luqiKPLu92OAPUXr5tEEY7iZKS9R2/tcB3HQcP70l43g9VOxQxXVZ5WtLhG9+bgnzmq/uRN6XPeEw6Y4xiasnMmpKvt9fXuabJH75gm/p16O0IFAj0h5R+mKwJHtF65XNf//2ggP6t54p3r56++qnVz/924ujOq3qAv2eoboqovTfXpzV12kS/wd6usx/oOz3rE5Ttqe4r7hs9AF/OivyDSqqp3N00/b/dPXyxetxvdd8xb4aU6cZ3GlW/fz25YvPuPHoOkU9IzCEoKN6jzJURBVanUVVhYqM4ECUlELrXFsXT2WF1uR31ybmPyxHL198ih4/ouy2uvv9Jf758sVJ8ohW3Ze2H1+zBIsdrlQVNdI1dZrFab1Cp9lFgpuMNl2Dh3meoigDhqnBV55FZfmQFytcUKEYD98XZYdwclpcJlU6PcUP89XT5I18QlWEOZqQrZylsXeojItk0yicGdqbZ64+JmssFqvLnAp06cvJ5yhboeKg/J6sblHli63B8s88m54OTVPfi2iDF9gKKzGh7yb1L+7yh9G8+Y38EDM3KgLolyLJC6qV1frdQHlcRreB5+K318Pqq16To8cjvNjc5sWTy8ocPb5iMOwXZ3lbmmX5bz/9ZDTJluz1Lik3afT0hbC8DaMa888Fqio6FmvewSrhJrmtCwr9qsWz5yBnDno7DQd9i9I6xEph2SyllZ66Tjz7MY+jNPkTrbo2Hbi3xdEwr4Bwz8bytprpsJtawMSKsts6urVkEQAPmTqE6fO+yOvN/Aq6b3+ppueS78/RfXJLGUgyky9fnKOUApR3yabxp4iSdTWAnxT5+jxPIYHuoa4u8rqIyfhyLehlVFDz2k2n9N3yVCUtnr0GkbelWQjfTCQu7cy0VFeuxFO0j2v/UaMLlB9hHKrWA2zcTtLo9nSNB3uSpEhD7l/MRqvZ4lap734s8KabylJ5579PdDXBlTqTqu5mMs5RSXVcKVegHKRch8oAe904UqNS6E7pultnHOogBhqHc69h9brO17rqaL3M1qVrfaEtzGlJpOssrW+TTFAiuqqXeR1DyiecVeWvFEDbSqtCnJTCGSrWSUmE8xzF1KlvrRAuUFwTf90rHtdeEcjbCnSYZLv5NznIevvLL1O0PXhDJ29ZKrxHlLdRQcQKXtZ5Hr7iqgwirIYUZFgD7iXELCoXh2FbvXzFItpLr7P0TiRBJwVCF5hVN7Q9P+P5Mno8fkTrjfepF0bUGuIEDzcF6qoHcZXcex8+nZaNVmuY3w9XUP1oqpR4zTCxYuJ3HIZ6zM26yLH82yskUq2k/+6VkLytUHuJJU2RNiZi8gPzYE4Hcmb+JfuA5eOMmvR+2A7SNH94X6OywvuSb3nljdDDJyKcE2G5e4cZ82sVd5jIn5fJWlv3OFs51vT1Nblt24imAbdpowLRpBuVQhacUu3j2gdZ+YDVmbRTTflVo0bH3WKKRJXOlXvr8AaZlyZvUOz1uUJHYSrtpi7/XK+vUfHlhmiw0m8AUzh1G/HxEzFI9iERtOkTJhd16CiMPg7qihXGcWclYKBukMF66wkWsZe2YBEF0xkvDqMStR0g1O0M3S6GTiudDZmsZdRgCZhq9iG21XGKlwsiiPthv0rI2+po9L5O+lab37bHniVuV3cCGeII8hjPcjp5KwZR6WEbOsmLdVT5LtgdtosorSbv+sFqnWRH+XrNRAxPd5OhDOZjOri5SdIEi4svtcN4nN6hFAW4R9E5rg7iOK+BEO4pfFdh+OhjVFanm4PVCm/RVNcZTANGNBqvQERTfsnA/aR1sElZfcxvk8x1g4rrUy7CqlqKwtkgaEkqOZvoVP8VAzaYAWKpsPoDILZ262GSpnia+7lXdZOHBfo6BpF3mIOz7TVv6am63cJcDRaN2G8eRrCypYCQie13YtWjljqEYQg5sfUnU6oeHz8SgyZKD+rqjpg0MQVS7XJUNcBpMKogzIlZLdsJwmZAvT7LywoeW18MDkQsFXoNgDh1sbnrKe8jLZd3clwM95KDse0m3fPDPaRFYOfGJUK/uGLbLp0jvMfALPIHddGCXRuBgF2EIYSuSsDsu/wQFauzPMmq8kOCkZATd7DfApyk93I4YAwKYNuRdEedRmuNAAyoPw5GrgB5QFsVeHGX0/pHeBN7io0yuO88FEh+KZBAezmkl1unJ6jDPaT1Os9etQj2e3p5WydJUVaBXLd683WWhnSb/jCtYBbbRNn017ePyAatEO/WaI/QKoS/3SdZLG5dNS0yF2AnG1Yrmm/maujt5A39M9kQWylKNbH8gU6V7/IMNYcfk7d1Ej3O1JLfflu+k2lkCFwKu4W2hxkWQK5IWLT5cuu1mhVSZec4SLGLIwBpR8dQfk73jlzWa/O7pEAxMc9etTj2y7O8Lc2COdG1KBobUraOjyCBJmWw4MGH/CMiRDwt57g0dXlXIGTa4M8BGsSKFhVJzDXmGEdTX/8Li9pl/i3ydvFuw9WpuQJ2zvEuGM8Bxt1x7SdU3eUSF9IY5mqo3Ki3ZOwQ00MDuyttFev9LbsQSIY1WiuA1REEEDsPQnltC8f0cLk10iLgpm6/DinUknodmirTyYyZpMLppdPbDNP66I5IwiRaiVEsc+gj3pq0UGFu8j02dj0MzBGmvXjL25I5GcKGAE5lrV5fF+g+iTRqIsyJ8DbYQU77XMf1XBB+5arvFb/Vn5AGCONqce1lXiHzLal8hd4kGsP0MrguauVgs8Gs5y98QeM6vm5Wkzit+iOZ0BEDssMnaWiBk1gfpvltH/BlLdKkdvmKwbEdobhtZy7R4/RRbGTwxKMcLui3wwiyFEPrqwFwYCeoXGAlEMibjZquePAQQbBfC+RthbrPFyoVr2WzgfIouzg2W2b3Tk3UrWwtviM2LNPRGM+r8Egvo9vpk0gvc//PME+2qatD39i25ckOM7LAKbsmOhFk1inYi+K0GvLOT+WSGeT6aI8VukIqFILL9RjCL6NPka/quDrHu3H04LKPi6oI9+jVCM92GH5tl7ZlgVS30hBuFiP1PKqYUz03onxA6eamTv8LlZeYU9IgyD7nLrjk1+Wa6YfvyrHcetVDMrfkIADxfhwIZX25k8XS0iLDm/NjhcdYVueKk2fJiKQ1xAugZtX87oJ29PfVPvv9glYbkub8b6EVuE2aqZkm8A2hZb8lZYKhT7NVcp+s6ihNn3ztkGUOwC7ucmwJz2gnnuD+zdnerNcCO65F600a4EZf2HwsHZ5w55D7DU2Ym6t0m9/dS6IrVLDdfmM9XdTrYFv9QBg7dNSI4gbt3cdwSPsIp4N423JNX/yopxe6KKtvopiYIAVeRyttlG6YZt9XiUrAwzRyWr5PbqqjqPA+7OnwhLBWyB2ppEBfqjtM8WY5CfAUGMU5GD+ae81BlFpNbGNyERHbRgerFdcH7zGdlu/yhyzNI/9z8haP78x9zdJGwDuE3mP81IXFf7kRcDpmIGrRHD9ukuYNpXfRE4/TDAW9KE5RhGD7D1F5EWGrCYWa1TE2ywsquDckg8jBbYEQazi6dmaEbBavyWl5TjJXFwEContER09xippO+eo4FuMZKpLcW/p6nHTtp4g9ZeWUhpQfZ6RKgHwYIRPcYn2aEMmL0g5jEwzY+69RnKyJb+qswL/aR5L//vLFBUm1jtdPh+4Hy3dyWh6X3uRkng/0ZRxs4pBzyeweiyZG18Qceu/cqjz+8Y86an0dXiq72a5RjAf3UYLrJimD1TM8DOyp84KVZAFH/jF/aEbdpiTxDh7Mq+TmiToETvKi6+MhwrsvP8SHUfyDvg9K3gH3TuNDdoME42lDS7wD6Te93hYF3fTjWUrW9TrMJDUYo8dwGCkB0KpFlqAZjFUsC6SDh/XTYV1Vg3PBQ7YI9PekvEuTsgqDtBX+FGHmxXp95L9xPvzE1jlFl8Te/qURkuAr0Jd0NW0D7cbkiB7DTtTGxQbjiVLLgZjj7OMayOG1Q4QCi6uNc3DE1LmzjrMKFWUQ/mrV1ggzmpgpWsU2a5t483GZoEYmvRU+XkNRWR1UWHVe1xU6ytfXSdYe6wVkQtxnrPNoqjcSQpsm/hbzd5Tc3k0niuN9THD035PVhNg/TEubfqXx1Sc9Ij9lEu7EItzlijBpErcoTFwcYHKPiidS2dJ70pmB2AQTD1p1lnSJ2tUnyLa7x/cJRWVdINIlhTkZ5FXKvs2DNRsO6atie7Tkxxi1GWnrbJUieqKl8UCFca837Z2hguRrCuX4GCEl1AiNk000ZY1YnvqyWRL7JRIMkulKr1roy3zwlQ+BMXIoIRhGAWobIshqA1WEz9UYUIjnGZXLApTGQI7xSc2syhOUCWBXLR9Io6tkoLIgJCm8LfG7nUnj8NLEWHVuMUWYGA8iG4AA59hx9hBR2XcWUNF/CEw2BhDWcRxnCcWjHEILo+g9ByHrOA/m2Of+EDxcWKEy5s63v3QHetOmL+23Lcr+w1UU41FXkI1PU8txvM11B4VCHcGJ+pQplqpTFsZRm36LigQbddA+Utl9RT3FBBnUks2SSVXHqeJRWw3cYrS2Q3QeF2unK3lwDCgyIVsu5cIRkHUiM+5sWNFZHlTs7hhC2mEOzLbLrA8H6G5fDHGGUCjwggjhFdPcmYpOyTBao7b/tY9rlrelifKd6E3zbmZCRGp8LcleKcbDDRCT2nVMxBjcqdU1Zbsrtz75XeJqYRtdVR7l9SbP2MRuzp4pAdNkGU+6maHxkVTKfHl0wESIZxsHotvGX+aarZi48WYrqPbyA5ywLCmBbRcnwMUwhUdCPwbBeWHskWhRtZwvv2fZtwnXAMYBAcqHAkL7pavpH5N3vmHUodgvxfK2NEvxRBduyP2XQC3rHPjz3ZU4zCvMp7Ne60mTaIbXmVoxCnWhZ39hJtArdeRGX6gAwNbTZnlSRW5SXSR/Ir+2R1d/ysscr1gornjczhE5LZ4v4+CxydJn0Q3peZTdKmO2wvBA4Ltx4eMXtvjm0Ladgoc739/m8/SbCBta3xL08GmS5xtCHz46bFn0B5DA/sbYC9uoaThZXX9S1QExSeq4MjE5HQ/gl6Gk64G17f4JrZLoVVt/b7gr1FdDosMki4ohZL39y0SQdHGGa5rERbmETXIXH+UnSYoy9e7g50CXHz+jB1+9fVpeFlFWJiFuSE2T4tLl5FZUITCE9uSW0zdu6mSE0z/nyF69mKYemWpbMZ9t5LSUOkuD5BhNJjSWEhw25kLXV0VqJOsjNcYvGOR0jcG3l2Kl3FByBZA/nuzndYqMHiQPlKxygyY/tWpfYgqUpb+/lBoG3fD+A95AMM83W3psBjS9hp8+0yjTaPREmKd/6Ge2hrvZ0HuBQrfc+iss5+n4ESsqbPtFM+6Rh/iH6U6QpLtj5XmT29ozisL1eN+ERbRfbeRtaRYCw3sF1kf0aV58QI/forSev/XWMP2YkzVn6jsVrlaw7T5riNn232oNuPaSI28r0G5rFGHviyxY4oY45AUgeazUwikntQc1M2eYTFLMgmyYrufZULJCl3f1+jpjXv12RdYmDt2eE5znePQiPwrpmKLhEcObQVytK3aRUNwSklfT3xhS1LU9N+GuNU17FUriWNFenQroDWLaMhqAedeDuIV0Ux3S7uBQ780QeVsD0bzz93RED4aI2vfej6iV2OxowkscVg2Ndml04TK6VCu3BnrYR5IDRGtymPZyKm8rkIUfKnbstDzBVk895DVd0B6Tv/PXc6jBteEBWH5vuP8hkT0RcIJ13uWis66/IVd49uaxv25gse31w+T6gSX3X0JHjLnV8Ir+uJL6nv7oD4kcyitMoD98kg2Y9D+ILgmkRPbaY5uvhMwapM9wwv5Ww3bdatjfQ9jBewj70Pxn5B9WBcaNF3cxLA4qFwwFECjEbgPOTuO/74Dx7m0IlfIJsgOBCd9o0CApxhtvWID8ec1NrHbYIS5jTRLyqt8QhM4jJdkkGGafMh2Yik8cRqtGZ0oCFRZLuihReWlO9VCdFacK7V5v2io87zcfwkTobo8GC5cgbwZRt0ulZ6Q1/PQiSCZ1uxI8A30cqgs60AXHBMovuNrbK7y/gmpScS+smRYQOV4X+Yitz07tMrr135ZhJHvRchatUH5c3VodLLOrZB0Fs7/68KYie6o/zyqQ73lZ3tYcz/zO8t7uQm8JB33nxvbVGDU2y0diNGHKlm/CaOLhtvMRi4OyTG4zLEXdWckcj8Mt8hLKaUmfPfQ/ewnjjhx2mv+5TgNYrRqdF+4RSerr/FJXX24oUmr32p/SeD+MocpLrnkzw7SqzLFmXH+CIIvp8r87DNbVxWqaJ13VtiaFumlVl2FrE68bW4njQXhcK2UR7W0/eVuafcxEFzuNoitCXuvc7juk0NrcZnOzW0d8d2Pht2B72ZO3FchwatEEO9Agb7fhT8xD7JNZYadlm5HAO4qEWZeyqshTgm0rEyPZWzQuD7sYLuLOBgvPcy7jAf2upnVsR6g+1DAYKo+YXpmyeqCH1riSal4tDZT1TZ/yUSMJ4eELuJjsVxGDVWSePOULHSVNFobraym1GVFCm0sU7Z7b5W1J6OYfr7ZDQqSJgiU5ey7+qKMClee315MPqvHhrv6FJWGNJkwg17zWPkNDfveYJ9sSUtUQwqz9mGQ/mAx1iySysbW2tsfOMrQ7zYw1pxVpeHjQMQF4h2C/zijktqXR+zrpW21+294jKPsHHr8W3sl9AFSTLSRdW1NlQSc7ddS8GDj5WAyynodp6PgRj6kMb69Cy9Szya9urPr6N0k9LO8ex179ydsKZB3AyVltr3W5J+QNc8BrGd/hnkWDGajmRd8xKPSsLwshGC0SsJDHs5avD8t7qHJR2u7imxfkP6B0c1OnGSpL/x28gDKYWnlBIljartAnaNqpatfNlyYC3PTOVwK/R2U7wHBhDaMOqsx7gcBXXFXBoNfUkJnwumpeDIjX6vIoXzvm6Ce1XzEotoPH2s7o7q6Hcms0jaHH6Y+lCKWdErVJub3DCDI6M7FXA+DA1lC5wMQgkDfLOuYp7vl1n5tY44uJsts6uvU/sw0kg/YXc4sQ4mjZ7AlVXXO3GixxwjIPuDcJOBoN4Z1E4mCzKfJ7tGrxHQFhtrbXKPMqPNLAqTOCJpjY57RxG5l0je10qXSNpUviADVeYEeF4Oo6hrDdKzHrMxw14GQF8Kf+SlPByQogDq0ii9KDuroja1pzYe0cxZhvXXZPXQagVyrEe5NBoYRaCvqaDMdrJtn8pP5QMsvN6G4S5XWcsE22R28ztvyF8DLlvnmaOohjvE2dp0H8532yQsWUz5JpPWOg4lQpkquh5qBJjSoIS4BZLa8d10le1OuzvHRxEdC65asexV6Fytu6zDdJHMotHSLcdv7NzOnZwWpVUA/oxPEhu5CVTalfepEClYlYKmgOAMTWeqQoKNtqusgCQp0cyhXdZID89VnbGS+FRnHsNZq8LUqlrdFoZLZCRCpd1Nf/QrFKO/5tmgtKnxtBKP26/y3BOzBPB0ZUVqQn3jFfLZ5QU9zha/SunSPqeSwIjXqUrgjjYljXcjBOa4Kugy0Q2Dv6U9W1BsB/CfDU/nvFL2+LEuh9kdebifPYmeZN3poIf1CrU7721MlhFgeiQYPsQ57jEvHXygk9yLBcm1+xQJw2Z8pgbc4C+GvzthNeKp3i2Ot1hZJ59tr4Wcp44PNHtcqAz5Gs1AV/fiTVJ07qop1ba03RpNBp/tsrCXlblEDaJGeBDhxIW963IYIGBHu/6J2kKSZW6wj1dlZgEdwo0BmQ9wKzRR2sI2GwnUVP5DQ5KLJPqLrLV1OeJEk45qguCpTFT0e45gyNNo2dR9VgAKujx//uLAuX0WO7oIZwvH2L9LkTA6qVi/q6wioxPc3i9JKgDRVqr2rs+HGexi5JY3huYhLONNcIR43OM9JW68wzwraxWUeGG7IQZfvGRsoRryIJMRGi9AShqWkqb3lqAstbnpraLX7V+WdADTc5k3ayPm0rVK1M1gQ2vOtshVa2CWGtm3mIitVZnmRV+R0VCLOdf0zv0R2Kf+T1cMd7zq200PgsKVQ70yNUDPvBzU2SJgFei+z3B5vJaUCDqsmmBqM/KhDWX0eYt8a2kzNLYUwEw/QTSbo8ixku0GbCV0jLH2glm5JJR3h0f/92loaOHzdJQR0Jn/JsSPM9U5v/haLp6cnKV5Pc9R26TgK8b92jOojpuvkhT1cz8IfY8EyMyTR8GGU/ZtkAc23OomLYNk+P5myOXlcZ8mTM0eTpdTSDcdGuptQA7K+wTi33Nd4QFMmfVNPQFBVRTH4OpsHsTc8iMrLGz1HJJAOeUMNviCN9XoKLjc402ov6urfR5x3yWV3Ed1GJ5nTgn0WJ69XBzgPSbNOnn5e2ObI/xwpnUzenJbN7jQM9cL29x5PsTvgckaM35rFeo2OLDxHJQtR6cT7nVf+iWbijzvEpDXjkScXoigccjj2hciFSAgSyjclTxg02LUAhg+MSSdecAwU739DXEu/SPyQleSAB7CAEeNUe4g6dlUMJx8kKUNvXNt8nN3Qjpx0EBHj1tUSr70l1JwxGDy0MyqCK7eBoLXJXU8Hf9Cqn0H+uSOgsX+7UMyzYklTXfbGkZ0MR3DOm3LZn52iF0BqtWCV23FjgQEdZKC1TaIGFwehrWL8ui/suvyzclYpkH5eIb1KOi116tTHSxgIkr+04AInS46H8MmeCitDhlZkGTQkqzH3Airyt/hjU08PbaDVfN7GXXaZdbc1X2KGGZpHtPgqyooa2tRdanTKFgWDU8bHmcpJ02FpwDUSDsO3FXN5WRy9fCR2toCGQtVn7JzpYnEihdNQ0t3aHGhpDt/soyKUa2lahcIbWHEa70YBgm85L4fgrmb1ikbfV+c0YrQCl6TPTTiEC5SYOVTgtu87Sp1qjAN6oDuFRXm9mcmqfY2JsSIrpWbx2fWvzJLi5QBnZx84xsqapeYb1CW+2aJasidshL8B13EHdh0v7WydyQNkvyUZuJ2EBNx0NrEmV47mS1RFHJAGVrsoyeK8lmemh1w2UfQ5Tk804JpL37ZAgdv7Zog/b8JZvllQ0jfnUIbx9Q5NH7M4xmllGMnWEcefbaGzEqUk2bm1q2i0S5jpveGv3/E2ANLSnZYcsmBn/EQtINjwtZLkFItq6eU/NnEM0d+nqbJUibGZF0wcyNAr+iObJC8XfUkPpoCzzmEQlrzpjBT71CGglySw/nVXl79+1OGgEj2uAg0hjg1TxlsiA+0x8S0QoVHTtLMRbIv3BpaddSVDs7Up5W0GswWaevDWS/WJMr2LmQ7DHVu5pNZrA7GAfFDfg4N9J2ICDda+s0wC+vQwq+C7QVfuG4CFwHEZplA3PcbmfBU3sup3LgTaRZlAGskHRLlBYmwpOUBtK4PAnToEDgkxGE/C4qQvjcdCFXURNH0O013/ytoLYIJdFFP/AFJ8p0Jte0g27u6M8g1zDx9+hNLlHxZNj9dltH+PgOl7oJbF3tiF/8kBQFuJq0AFiB0cA0iDAMZRXqicWZQiltHe66yWS0ilIJJ3LW3dh/OVaYQgpBzJ5haXFcb+C2TE7R3/UyOkZh9ZBMEKzlwOFHIRIKBZMCEJtl8IcPp2jqMyzk7xouGl+N0jLv4i6vYMcEPh1QH15ymxq+Rf0pov2qKKbm/bmxdQHK6t1kumv9v7tpyAPgox0W5i8clt0xc51R83QRLKXhiCAfScI5rnZzKlxdoQXIb94Ch7TfmVTyX+Ale0sKlr7xvKEsDnPc6jITnGICMtgHsgwYSHLpFjCIokKzEMkLGIyj2UYo2MXVPHA3JLdxVhLXbHw7B5DCgbsNOSwQe8YCw1Ba4kUSN9vZ4+s6nxXaAU45pXB6Lsc5NBXSLHmvggKqParoLwtTWC16RvCth5b9FjhT+vN9ElISAD0H3VSBHhWnPjQCHzL8KHwnpaX0ePxI2Ko4YoKIzrCbHCbF0/BFuKjPKuKPA1ha4R7kuC0pJFeyJpgcq3Paw16lw12E8OwV4AOG/SraR3BmWxc0cu/DLcSUAlTfHtNLG9LoNjED05NpNqpDX2w+hfmG9bdEdyabg7gZmjotMSYsNijOEBYqYcGNNdc8+ss3ka0VnaOZwFxXRC+bvMjed9DlyDcay15WzzJnnPmCX6sEk8jyENXYmXW92hWB/BGGlYMKmfhBGwvWQomfopT1CzNntJAEJ2hIsm9E0bQ0BeKzzPW8KLC0y6NV5lrMyDG0YTIR3iaJVUSpdusydguGmmxq3ENueoaAWr11Rja1uUlXf/nVstitjJLfe6klinT408f81sHjYxr3ZKAIAbLXhvL22LItE2nLuHyRW+HYuLIDIoyA3MlwA/SqwAT9JIKNujxAdsQdHIAlSt7G+YEWiBjCHVC4PcqRd5Wk2kZ9+ghL1RJn99M46jRuIcmegH3OCPAljaWMR/7LYX7JVDZFibPR3SPUv/3M/OiCnd9x7L1Eww+W/KdTZ+d1VvOHO0JXfjKLfpaTB9ngZkcFQUq5mhrxgAJDacVJEosi30DPvKsInuIqFS9QPQ3Jy5yM2JkxovSaAlnrNDXs9m3JRxUfvO+9ysB1V7/y9ti6eSdiijUBojOoL9ba5PEW+qqUUqiwL+gXMqhBClVgHrJ7FlB07L0C6+rwI7x7KVVIa0hwlsJD4WS1CFZg7cBWV//C8XKSPpfJgtbmt9iJRFMUYAoo9a9ffjUPIIVEGGfYtIX50Q6lGVjUI+O1crVGH5QpAowQZOqYG1dSmyeE33vWWhp3wcgXc8ZSD8fU/f8rIP27+uWw8+98pe31W47veP1whyIhQ7VUz2O0rPHFfAwilAoulYFCL+bPfEdWtUpuozKHw5sT6qVr1gke6aXt6VxaP4yjUPzALOLyr9j2qxuYcyz48cN4Uj1Rc83Ye4QNqcA0lb+vkXOYdD03XzJjovC38j5jC2+89rF+/Mxopcvi8qx7nG2cm21jmPMJ67tsmSbjsFOyw/JCgu27wThP2+JVJwhrMeFPKJmdfXu4ECDPs8fWm3dDzvJIhKxcJRnJDSAeAs/UXS0rbHMwR1wUKjNm59o5WrIxSk2+HOntzguSAQERveqR7Jf0eRtNaT3teIaLMtEg3dbB2Jd+Y7jFNuzaUAzku2b5LC+5dGrMSh7XA9BAAf2IJiXafm18JHC/FVffy+Az1kAL9L6dv5WQ0Vkfoyy2xovzXYzYP6oFGGMJPa5c0oCsPLsFY9pL1RTCxUe0fsirzfzMzduef5GRy/rzed7djhFMJa+yzu0Rt+iIiGoXLwjpH75aoRmL3fytiihAnDuLLcO/aThbZj92pTcT3G72G7kS9n8t+f2yfnQNl5IGeA0lY1XpqE8Z2R7rokICyPBH/JSmeMtkMflY36bnyUxkYDtSV7woVqnh/mKMYGmC4Zrgse6DMGfUfWQFz8mn92zIsGK6YmK8FHr1/JPMEVxHj/Gd3hXgMjTUs6oFVl0pI3AmXXICK+UtZgUOzpgMdeOtoZ9miBxZvQj48AlQxpBqccyBvXLHNR3y3otfZcUKCY3s151SPYrqrwt7fnaNA7EZmI0D+n+Mkl2U/PX6/7uupx8zAkCf7KauGFP8mKNJ522MG17H5N1glnsMm+sVe9DHiJg5d2i6TeWizK3HSPVbcfZCs9scAvLbnvzKaKZ4jx3OS2WvWqWt/UM/NQTbrUPo/jHaYZbiH94Xhs4iqoozW9fSTDuWTToBIMn9iGSzAa7fb1AyL+E9cDAfx2sYKNrK4TMSipr7ExMTqoBNR7HWYhUpZ+irL6JqE+huERrbFC4nRy1ugRCt1ck8raWWZ++JegBd1V9BW+Sll1tVvMNdJsxNAAv86j2fLzn49n4uFXuAdiYw7Tn4j0Xz8bF1FAiTxG0RpAzE48R7XlY3la/q3gTaHfydpmTJvMVv8jLEtvgqT+X8aj2fLatfGa+jjbJ6T6h6i53iS5t65evRoj2jKGYUJZQTVoo/bo6yfJGsuO8Q4yLzL8HTjz4jxrVaHWM+Sk9qKoovnPMO9te7ShfgQj3PClviyGY93193BM8CeSoa8RUxAnEzYgAaqsa1UZhoKiSkwS4zaBZ0/F6yV/6MYn28LK+J8rh9ilZI9LmDC3Lr/QnkksboKBfNeCDQ1EOJfgSFaC23lAGlUXfR7V0Q2C+Go6EreHlFx31M4im3utnhaYrkrzwfk2OsNP8d6ZJq9pokiDXmPP5B3eZzzK0c7RJn4KMz6idWcZ0dDR5E4dxPHkb+vxEgewQEkQ6fQhpyEiXC6zcLovE+/kAjMYpJ2Oz+MVxXvs/VYI7sSKHd1Ga9rMQILlJvzLDCU4C2wh86npzq8J0QCzJdSO6GgODAxnBqGycMaCXaTPulrttw+LZGzcaMVVtL36ZJOCzdbzqdlWGjesvQMw/wrO84HNk2MYqlngmwxDIem9flppU6RO13NyuuShTazUvEu8duomwVONVlcoCrmWH1OJYf72JkluXMLdeX3U49rpK3pZGW0x1MVdrY07UcCCbc8lwa6/odPMwsUaIPMIRejHkUO2l0VkaJ/KI4l1lY9o1SQPR9An/n9ceM1S+kJDbu8B3WLx2i9I94huHo4ih9luv2j9raxvrys/oofyIiKh7Buj3KhPGuNec8rZginkn+l9oKxdGn4T1fU14xeUTikrMtM2j2F43YUeY9vKikBe1pTHRq10LPxp2Tkg39Z3Y6QLfWuZ+h8U3K91WGEFSemR7YdkLy3MSltP1Ji8q3OpN4pSfa1R/LxzbJhwneYo5Z6G2MUeQNsPcWA6DKUwCpR/Jxq8jl9EP5IehI+5Rnt0kt3UxDrqbLtNQWq+z/sb/xK21byJg5ORVhHD5f8YPNgCHlSOldjUGH44r5VDCgaUC1O/piacsds+/QMIVu3xDrxhUezWuU0beUQWUP9Q5Rt9Mk2vHKP/DRCsCTVT2WC1kJ1KaT/LCJj9MKoinlg8VNLW6Pk2tyGljZAEUFkOdtZ3Fbr4S8ziMR6Is36FNmj853gjgUew1mrytdlXyVWnLCHUgi+60bCY7jCvxn4N56JzDqmfdEHavQThHoJSQ+uiNMA1ps5cGSV9aVZvLIsrKdUJfZAoxFRDO8V0ZqrpgMIc9QuMdCXSN2uRczuBwNND00PYMngkNOLgwh45ESpJ79InJdugRa+QSsaRK5XMju3TDL6pXPfCwJ5LBCDsiKaBfCOejnzdrVH9vNMjb+mt6s8ivllSTKxzTG8JBEjnn9wkm7KKXkk/LdqnqhNczSCSQq8/PBSXyEOadOfZ3+E+yLM7hE+y4p/FEsldgJ2vyHJVY755mN6qbUWGa+nJzUyLPaGoaZOSH4jCq4ruL5E9Pr/EZFvLmSYrtCcEiDxXSF4ht7MdgV57wXu2giO9ChJGQWrX9zk9qiw3GEXxJyMse468EaQ23YM71kY0lda7LoSSmZADner1mTUlNzoLT8iSNbst+Vn3vC8laCmaFYunAWjp9ImsZI67jaf2E1teo6JROml+/fEGf4fn95U8CC4xgiXmEzVdE8rijvtIbdaVmW9rCAmGFzYSoJkmxWfaeoNHxiLylBSfoIiGq84xeMjKbpk+4I8kGd5ekiScDHFV+YzIFB2WZxwklYbe+0fT2jaLGSzMVz6vuVT+u/8fZ6kUjtspag5APnhGoAl4X6IgwMbFI//7y/xKGb9pgrxCZBrshcI28GY8JN/Ile4fIGvbigNpc5F5OGUcrUW1iiq7GX1qhIZeEsAVVYvZIskrcICZZjOcttRkKh8Rwn0k62TfHl+D1AWVki2gzhyb9YN+AFPvTN8sRU0e7314zzGrAw9i+WnV9M2RgsIqUe1loa9aFm9o9vlWOYy6mVc7bTnAstuXbVQh4driUcq26GsS5fA0bxtW0BjDv+BVmZUs2xMrTVC/RIyiQFDlZpy2GP0K4U6IKdn0G6QTnYDcEEvf8ICsfUHFF+UTFFAycjM8aEFtuYxED/AYx8HbwGtDxmbgNmAuTlgn8Yrx2gffTJFSrySZ5hW0vbIPFFVodEZcMzb0i4xJ9VYgjx7VsuNKgPWgZoIU6I8mGYnjXh6ivEm9Qrzr00k5D0CBdWEArsoAtmFNiealVjmAG2VXOkUn7bZXFhLj1dWqZkYOD2LAFsWFAHqs56/306pXo2HFiIUkfZmAeCU13iW3Gqkc3zWNpCctCY9wAIym1ZHh2AvszI1OBtDZpf1RxMQbrH4IaDmNkHCCCQqw1PFtlzlsAZoCxzJjWZeyHSUqOfbsGtN0cwwenAofenBSidDlQo31henh/TNdfvoKKHi2sC1mEZhQOhe0zoHSjmEFj6ebLaD1kHtpbRF8dpvktOcfQO3gESIgvOyAbhhQR75SzR9r9GVhQOic74fQhvT/K1/TIvGccFZfwwDIObOFsmVBAD/ChjMG3gw9lI5iJFWXzY9J8V2c5F2TzaMxZQl+avGr/l3siQXDQITmCtHJKwm1AvkkY+fJcqR7CHF5K5TwZOSubKlvDmO2TVKZMw78AOwVjcq/Gim1sP2OOh7AAY47nyYgxh8eel9nrto+ZanUlDwjuaFoYq60Mj9dcM4ZznMg6MccORELXXdBq75KyyTt+sMGTQtIItqNRnb+oKkFM1cHbMJWyDWiPbMa4XqRpJd2GMF2V6cjSt2Cu9gPQpPtxjv6okwI1Ib7aTkO1bCgTdL0y7R9AVwBOP4lOas2IdDPoOCMSmfSjq7+0HXeF+Sa5R8UTvfevsbBGwAobzsF4G6OGeI3t52TLp6o381lhIJ2NuIqptzRnHdbZKkWnFVofVFWRXNcVam5NXQ0lOoYzwaHgQ2l1BwY16op87WHGvK2bD5sRzicLNixg5Nnpa22PgLRDMdxLy+oZCYIX53Pt7eAGWzeWJfgankVzXl564y0OyJ6R52NhF+YNaVrIu7II7+2go6dtv3c39FteDRMIFRTc5uL/kTZjsU3fGkUpHcV8XCqdL5MudHW2hksNdSIPPzGP7vBSLhvDAgy6g0qUvG1yE1Gna6H1mEPAEGOycDZcCeJfwnuu6sgMfKWi8y540VumHg1jxBMatSStqNCCrjynbRLgP1VbW6MXtSOaT0Fq59OkK2y9reJswxUdqjMTP+/w6q4ax0IMvIOrfNv+xQbFyU3SpCHp3WimDKyurWBluKIDU2t6sIPsbTai+RjdbI53geXhkXyhr6NdSThSxn4OuMCbhQo0VhcNHboDXeExEsvlRcVjuDMIjgdvmPQOxrClC4mSwd10u5q8iy05ym4Zyxos+stLnP/YF1+xTPjGXf4aPEtLYbcsX0a3iuwwImzgiA0Ws9wEw8UBk780OL9FRRJlVT8tR/n6OskooFU8iykeBeEUKBxoatyhpQNkbDs6n16wnVOTnm1TWI1qfIYbOgMUW8HxO7y/sxjWdojGDu70DEb1jzqiGWK/ZomXVLB4tkI0Rh0C5GM08CUXA6ij28Hx0Jya9Iytt22877oCOKj9AAz9nBT89mj1Z6TKu6HoPXWmCJZhcKU7ThjjrrH8Et432/m2EIKtcbbxg6Opva9k3GrJmkpkFlJC8QQQFXV/5GKjk9qtlR6jAS8nSUb8YSFVPIqlhctqa2Bq/7t40bbKkl/YXH8WNvnVRUTecniXP2RpHq20vDUGD8xdHHLIbdj1c3LegvsyI3fBlDY6DRjVXIzDLhNU4FGTHKlsbnUZE8DgEIf1kDY8JkFvmQme8tnSa7R6KDOwqHqqTDrA1tsCBtU5IgTICdhyF90L0t7PyoS760I4R/cJejD1g42hFWtvA+iwAnMt7BIrKkcw37INz9HOseQHlG5u6jQjOV/HTGXEQdLqWqZlajrzr7x1OUPDIrNlbK0d2Nx8rp1nC8ZvKi7G/p/RQ0nvmGqTuQqQEFN3QDZMLCLeqWSu0u7PwJXSOTFpe/FkrqT3Xf7PnnFUXMIDyzjQIZkriB7gQxmDbwcfykYwEyvK5sek+a7O8mn4zR52g8GDJ6Jf6AG348cKFVmUHtTVHSFvE2LJPSonpY1RbYhUqoo25DPrwE5lr7ca0gzybjXHNr6RxRTASV7Ua5qDWsvgIijEzT2UDesCqK34NIg3WN6JGThLTtzdYaPLfJPEhnw0hpUyEgWz5iQO+UKsBPdiLl6CCbw7zHRF/31f5PVGzUkMoJSNrDmIRQqwD9O3rVszZf2fi/GA+TBpeqi1DUqs4RoDJdMMeQr11WCWMd+W8h3Q9XkV3mg+jPluC8wvhl/0VhIz4PAmGINcxn0gX28JC0rGMKsNJ86PSfO0wmKs+KVYmT9JBwFDrEjhbNgQRGz+El0g+03VixkYSUVdk+bHNRfmKO1+YAwWkIt20+cB9302rtvJHUOXs/5rGd2iDwnuTfHUJ8KXMp6ylurVA7aCy9sQcIOKZwy2j0uNhjID0xrNoUk/Fn/2ABxJo/ms+KkR47m4t2kNYF1QZ28p344GsRTTjubNpBO0wrKLOz3vUvMoBydd3m0P0Hm8u8OCkp7PtcKLc7FLzKYLnxMgJ2C4XQyZk/Z+VrbbwVC598lNdRQVq6sz3OW7qESr70l1N3CQjF009SC27KrYcKWuGZlahLg/3MUKw17NwHuG02DEiSCGxRlzZET0LKTjF7CWiildrUZ1gwB7yqRgeR1qNJQZeVo5hyb96OpsFw9/ZUXMjpFHVWfj5nGru2OImg9mKaYG59OkM6OKy5qtn/MKmeyRBjipyUpArE1WBu/usKak53MZq+JcbP8e6Rw9YPE5yzGCspMfre9dVQliQwDehiGVze2Ul95kJDNwq8n87YQHHxqImSGgrWms9sD9j01DbgIjNmuTVPwu2dDwczWRxmBgKvAWwirt9xjr7iwvcMdnkFd4HrZ/cen63eyZO1bR8cUIWsV0tr44uAEoy7WEq7eHBcEhzMiJ4ByZtN8jWDa8gHRjYxyxwkEHDDbgMZvHrIRzuyl7MpftLKOxKU9ttiBy5RxVdZGdoz9qZHIzAgaHzQEG0s5yBpvYMZtZNYZZrGXVPO2EnTx02lDvySoEv7QXUgFaWSY5bfYoKirmzWrl2YyiDmyljMHtLBV5U/LzQ/mT0uEWC4OezWKGaKfCpBdDrQVNZG4k2mVDWmNyJtzN9UM7jCX4dSdXEWEUurAKWYXJOXUXYy10g1iCTXcw8uIsT9NveYVH0V6wJh8OsvJB9VyvvA6YjogDt0pDpGgK4tah81vHsAZDmYFnDebOiG37WssZ6Xco/pHXfM5i4bPcaDdEABrxYF0rk960dch6EMa4ddxuO7wZWN92vo2MDL7ygt6UuC4wKW7PoifqZTzNEoJXd66jqAX7VsYV7NwrqsbMDzaC7MyMOjOLu8RgBoz6wdTbGi7sjvAEtjHlERkCE950Ojs3bB7gVp1oLK+UbUe3APvr5tukS3zdxaSBTOs9nvuP+e0V85vwjFQAFHUgnmdAbPhc1QrkU+Q6v3WcbTCeGZjZYO5MesFV3Qr21frZIOCJGHY3HWuqEczMmzvpTjPiQh33WXKdK7dtxasGCzHazjIYzSdyUV+XcZE0D4abZVkDq0hTxrDQ1qlj4KYWSr2m7MwMjKYn/k6wHR7vfVShT6gkQflXJ0W+1vKdog6cEJ4Ft0sDL29ofrYz6M0cLlQ98U16wdbbFua7zG1Zb6gxKeMxzSzOdmJf5mc6kewmfRhqLbenuLlJUvwFXeliagRIcDfRAVntJQTMs+e+knZhjp2AjLBGe9OFgwYP4pTLBE0GpdiVQuDwvjS1P56UoLdMpr78VkE9jll2p6p5srHjSL3lQj6qvCDPZyXrqHg6fozvouwWnWNRO6oL3ET8JI/90NUEg0BIJavID20rIOu2fZ9GFRr3aQY2NJ4Fk74o0GwHg9I/7DhzVCU8S47RL8yLYGfmZkKQ4BbcN6q/GNsdRvGP0wz3Jf5h52DRVYRYUFLHhim1ze6U59l0NDPwtul87oTnRjYYXZynpt7MPL2LwZ+GY1mQoXcwFPQfNarR6ngdJelBVUXxHT13P0kU2yt5FYiJQWgbFlY0Z/u0+dIcrB/KDMyrnz4jR2ay4I6LGcJVM5BYnY9YVkHDsI5sOm4CYNJRn7dOz+pGMi+PgvNl0gW23jZwKiNsLIvZqT2WMPMpW7ZVgJ0VErNV3Kwe0WKqF5hTk74w1RZj79P1Ji8q3LcbunzEd2hVp+gyKn9I+VpeBWLoEbQNIyuagZJZsD2fxsug79AMDKgnvkkn2npJdktqLsZ8x4/WzCevAj8f6sh8imaWYT59h2ZgPj3xd475cENp3oQqd2yi5gmxgpzxBlh73gPagexQFYMvv3TrhjIbz8pnzXCLfzPfbukY16mecJ0K10BFJzXrqKi+XP8LxRUpQo+YEWLqSouyLK8oll+/lugoLQh/lL+/rIpaZGaC+gJV7Kta5csXzXeGr9pHzAQe5apHj0dRhW7zIkEglr78SYsL/yLXGyE0bZEWxcc8jtLkT7RqZxDuFA+l79rHKLuto1sYW1tm1jl0URX0DmdJmU/aPQ5Oi/wMFeukLJPuvWUIMQ+jRTp+9lxEOD4b1/UwT1OwV/i7UeXm2qoMRXd72HBIquFokbSxFCBN+vATXUfInlYiNU2ZgcS0SWU+oeouB6d8DKFHiLUIwlJxj1Ur2LMRgDGxqbrKKhXNWxAtysM0vyWPCEK4ujI9NzUOW5CVOs+5BkX3SA2EY3iXSkcfleo01ptnSVzVBYijLTIliArTCMKYPCSbVFKgtWT2ATA9apQm96h4ukzWYE/ZctOBDwlyFGNn0w7Zou2vGZ8kaSVRYpo6po0qmWoMY8BbDfynKKtvIjr1qr6zYFrUOpxWyNoOXGxQnNwkMbWFevopegxX0OtJsNoXeoIEqk0FvGNj5s2YEu8yAm2vodQU0beoSKJsuFp/lK+vkyySEUdfS9vwP+qIfvmaJaCeYctdR2HRddMmTHBbI+U7Q5MlWHS+TYih08LtGR2ogfvzO93uIUEF3sbBpkZfqEXzGT2UMt3dlWmRHD9iHZtF6UFd3ZE9WyNEcmNaBa9trH8LGcLMPFBtgka6g2PfotYjimQYzHpB36aX9oKWahHRDAgQjjadhKHNwbyRAS+C8FOPGuzACxwwdvgRFUPsKoRm9JMJAvOulQka8saDFE3z0oYGjZjbHaYXmAPeYOMjsymHpNaGSGQUG6ff1o6WSZ4KD3OU21bbt3GGMrh/fCY53XaDz3kDbjvEFEW2aKUrjiz5kpa2UKoJGTdJUolYtmGCW+8nGK5ug64C9ka9OSrq3FOja3I9aP1RYK9MeiPc4JRq/nHQm9aCYe9KwTbL+F6ajmrdjRmQXsMVJJ00MuccoCSOTn+0M5nKrQrmMosGzddCgaYv1C88KEPYyFKqBB5Gb9fd4a08tSuvYcfhCMDAK5XDTok2Zl3rhaJB1BIvSx+XbtKJTxHVuNK+tOV6LxYcfAg6tWQBpBbb60u03qQSKYDgTB1XTyrEPIzxNlSBkwMx0OoEbKXw940h9AMv8rLEFVMFSh5GP/BmOZF7cUcABpthIEIF3hWDkUbm6DVI9XsuJrwL3GONYuy0PLneRMktKERdmV5qmoVFKTBjEKPd6UdU4d2iTtRhSIM+RyWW3u8oub0DyTgCMEX3LsHcUEp6ysNokY6CViCMXJCQThk/ZbFKFw/FBvv+8ekwvNfnz/KNkCqGy53ac+iYY2H18eHVcPjI1FGcIw4V+FNsNneOol4f6tAPWXqwKRyUmzbRRTmwTQwHsHycwphYpoRkj3/1VIShNeMDK0npx59a66gHY5+YdPwJ8hV3QiyST1NDPkh1RYiMwBG4goga/AAhubH6EzNPUyXrjQEUQ2HhQMo0R+8qaoxQTM1FQ+bq5vAfHjoLou45Aykbfh88oCECiwogA0hJBxKMz+WvsA7EujDGhigTFiBSxaCWfHT6yhDthBADBf0MWoDEahixP1nZ8ISrPowCoCQIqBgaBA/Si4ugUJELxDkxhbrMIQra8CDyEXCQED2YUBkFKXhEMxGBi2aRk2IMqB/HeGq9yTJGBxBHzXUOFOqv9TIdFckDQMkHIwJDhGHisxSEAXABVJES2Ycgh0maMg+LqajCgRoMZ1wjAH04hDMRqQ3mGrIEKKgkwOpHxVdR0WkIPTMgl4BYYQWGoFcXsKa0AkUg+UAEWIg0TAidgiYiqomtQtLgUb6mEdpDJB9MDwFOPQ4e3JthQKQAfaSkdrGaRxF4V30kHmA8w5AKwxesAJrSfKCgypyGsUJWtQRdACp1Llg9laDcD8rxcEkfAlGJS+ogYg2iqFvfvIqLBBiF/uRAQfliwhJVqphHNS27dKEgVwebTZqg1WXO9lMkihJePipVNYhYTHSxglZKrNDSJZ2CIJQb+NOEbj20zfi6SiFp1uOcVuT6huHQaAXJwAoGI4TqBSAciBagHTzOUAr+ahzmLVXvYzitGh6BK1S7kU4fI4NINA5lD0UbaQg5+/qvnGRG1bWDN8GiILAqdF5PeaPG5fI+ug4QflbaTuhtE2kVCwKMaxpR3JLEXAvTqlGxdSsyuhBwStLNSrTethquf0hpJsJqhyVUUVDM0BqUYp7auuEb1jOZAGo+Kj2DOZJrHu5iI4hUGwoQTj4YCBwiEXePSEEeEOMs29FRy+OLTVKGktfRMoC0qoLFzMmobQQgqRJ7ILrqZRQEtxuoXlb9CDnrggDfFzMgpKaidsjq+griSu/C6cmsaXNagqsuAF7Jru0Bx5YOaBQHkPbYwCNP9WVI1QmoQweg0y8zpgguJ+pbnbbCo8Tmyt1qCs8oZsqOGM+phKk83AhdRDG9+yr3IrBg+n0/A+3tQ2BxyXVU0/9QVFFcxTX1uxij0FLAFJOC0uoLyfpJMO7CfD4eVZf067dJbS+q6FfyKedkViNK1ZHxvXOn6Rih8KIKi2mhiRl1AZgd7h7/VFPkICHuYuEgC050Xpbru9aV5qtxXefBKw3VKaitNEx5mAnpT2/SXkkKLeZCjceaTEp0FnPUXSm2nyh1D+STpuUSD6PTcFmw1P2mCt7M7lxIVV9dROtNioYUInLqcJD6IY0reFOIQwfZfkOSFG/69OlQxu/nAfSRQMoHBFeA6MMmbFFQSIJwhjsVQ8uKJV4EMhmKYhm3JsusS/U5uk/Qg4HNwwFqJWAM7x1UB2OdkUQfULq5qdOMRMqOCrQ0k9c0HK4UQViqyptRyKasGQdyd9mYlAGvIpB8dAIsRC8mP5SCUCKqiQNeSYNdEOmQpgqmhwCnHgcP7s1FIFKAPlJS+4Tcay8ZSiAVUYZghQDx9rNfIlSlIVPG35tVlA/ZqD5EUU2iNQWVzZqcOGa/z86mpC4AJR+XCAzRjc0ZpyASgGwOitA8c3qScGCaYYyhpUTpMuDpqMKhm4MsV2z6OwlNWBjNCBhQCTUiPRlYJAANRun8QjJHm01QyRkNjNE8NmMJwxMNLpgYAkW9lAablFGlNhg4E1lnxhJCdTDoZBwCE9iBNjSjoMG1OhBOPhgIHKJNl3RSQRcQ1cRX6Zo2VfqUg9B1X6VFjWkwl+6EMnReDWnV5cHycAX5sJT1VMHyfBpQg8h5uA1F5PxklGzzqRqSsYG2HF/DLxMSsGkAoB7MzK4iSDc6UnrxIBr5GSClUqjfp/KY5iGBwnskApl0XuE1sibEPL6iLg/v1Rnu8V1UotX3pLpj8uqKpNFVkQ9OUxMiG5NDWEE1HWIZO4Xa1UPpka+GFMdyGsIV9AMF66noZ6GZ1G0ApJTOUShKfmUn1Jic41qW4x1VnpKw44Ym13sky7VG9TMgGj01QEo1XptyW6fxGExTkgBIy600R5Xw8iGpqkGUkmQVVxBN2cLEdizUtlZC9ZXsBusoNpZ4XefKMXkW9URLSchByMc1BpQlwALvrqrwTCmYbFr6qyHZvZwIY0D9GEbwKpLoTTQYJRTuK6Oy89a5S0am9SjwgLo9MAfvtZ/mcU3sVBi9QaDR5iCkSkFAFWCtMH4pQal1QKQTK+2h83oWksIanBUZMJLl6dPM7MQ/QKG5Xq4CV6kQaS1YNwkPZyj1kxz5XNfDhT6o5FIObDFIlXR60m8uGRUaVvgnpLAW41J4KzwpNlOcS56m3/KKplumx6Xsk6lAcIsCXBFqIq/lH8aiwC3J0ypJ+eqyIoDv1lwBT+UAK4RpXYV+N0QBEln2OI9qPTFtDxJy4I2hAHbL+OGdq9MsIa9qK7ZQqgoqg0NRDzZmhGeDlPaMCv20u3nwWaQr8UkjPTGldc0HLkNhQmLTXb9hiwDVtZPqknJ2eJzpSnioSaS5Clw+aEUtMAnt6HkqBTFVeCEbU3irKij5VPYRCGc2MJVV5ESpuWwhHU0MaaGjgXbsc45ZeFtMH3oEQ2uiIcBK0hAL7oEXXagFjHxiyo1fULs6KfK1inQqcIW1Jq8FX7vg3n1TxjPLUc9LusvcgnAMsPHYhjqBicYgnphk/Rt7VwonigikULA8LKiumWf/VMpawDWxw6R/3U97h0cCqVp4oArwWpaaBNtKEM4Q0nxBXtLD++JkHRVPx4/xXZTdonM8TcNbfcAmX1tJsSfX1YUfesh1qX31eEFqDq8VhiUl/cOYhmNow0GOKoWg2hjh1OSSvLWotC20deQj1VUFk7dLX45UkFTb0NR57iXtK5x2uir2g1W48IISdR6HHvh249VJAi8nCmj5AOWVIBrKXp1UUFDRwMSXaZmWr8aPTCppN4Y1GtioioZuxtQaIwVoxT2sORG3jZ76NGU6tpIta7AkmJIF2XbAq+/yKXMg7ujpy6vxW9IiVRXQ8mHKK0F05B/rVNBPgRg65R+/ox3g8psN5RTQ8gHKK8F324wpp0A8G+WGF02vzrqnSGV0A2B1gxOryGk2eoBVSzYAM6QFlbOhoNpvrxss5GQqSjJU9GW/vSYzsY7aD7+9xiAx2lR1lH7KVygtu4LuddqhZvvlxcUmiskhy/+4ePnicZ1m5e8v76pq8+vr1yVFXb5aJ3GRl/lN9SrO16+jVf767U8//fvrN29erxscr+PRFvk3rrd9S9h4xtt9rpQ8pbZCJ0lRVu+iKrqOSjwfR6u1AHaBDfDqy/W/UFzRI7pHbsn7rSd112Cbg6G5GSTOIYEmDuEOnPxu9ymkKWrrvyJ9egVefBpoeIKHRSafjhAxMy6ph2texFEaFWftg6md5l3hkedpvc6Gv3nek9e+eCortCa/x1jY7+bYTrM4rVcIW5QJrh1tuJ4JpRaYy7OoLB/wVh4XVIg8yMghhwDM8XeVx0iHr+aYLpMq5YjZfjLHcZivnsYomi/mGD6hKvoP9PTQuEhYTOMSO4zvEPOiN490VGiHF6AZ89kc18dkjVlrdZl3W3QWo1BojvccZStUHJTfkxXVzyxavswca1Pjn3nGDZ39bovtexFt2lgECOmo2Bb3xV3+AMyUUGiL9zAnJ8S8QPNlFrJcJHmBtSkny/1XS1m+jG4BcaZfRUy/vebUO7+CvBaWEG5B5xcks+UqelS+4WSxavWYBIeZydqlqj3NCiauXbar1ruk3KTRUxuIwWIal2zNbOMPJIjIb6JbJA6TLK25rRNMQ3/GKNpPFoYSIQI/kP7j1rDGxxx3PvkTrdru+6oDHp+LUjDAMQ3nNH3gcQxfLQyLNmUSj4v9boGNEARhI6zNqTHCyJU5YJUgtMcFyM2oYHu4vs9o5cXrklRdJiwurbqtOrHr8VGdNm/dQGzdF5rj/Zolf9ToAuVkgz7GyhWZ4zxJo9vTNe4PcauLQweKLUz7KuXsefJh+S3HWX2dJuUdbxUzn5+xgdNomYuqoKHSJfW3BVjHOIyuS5kWzTQyH3YN6novitO4xB4jsGpwRTZuHxIbdZbWtwnncRiX2GC8zOtYkCvm89ZIwRkq1klZJkNiOR8J4LE5cL8exbaudmHdnMPTgyyu4evWcJAulaQ596giuAw4R119W7nmpECou47ImRyjEguHUvR4/IjWG845x3y2wtUu300APodwVGaOlQZ/c9i6bzZatpGsJjRwrGjZkqkleCnNnaepp7bGGFw0NFhtF+yRUDq+PdqAmKQvWsYKJx7yL9kHrAbPaNLoUQe5Mgt5TdP84T29hn6Zf8srXnTF4jn2DXIvGmZzzODoaxXzrjS2xMbHswLxsd/n280tqG+6m6K+Wge+IWuoe2SVp9FApEUeQ/dtTs3zuV5fo+LLzbcm+dEI1bjoGe/ZhZvQvozI3pR2ZEc1iumYshEDiDWHkvATp6Ex3uLi319u/pvMtm9n7r972Pfd2fJMtO6a5bGw3y2M1k1/v2fUpeGzjQF8sNkU+b3oZxi+W4yzQHgtW33JhGVuXGLhpt2sJBjHJbNzKc+ch2l+2z7a4MCXytoT8WTT3CUJNRtPFVtgEQuEh0DyWfO9Yr8vPkvK92JMlLW6/kSaumlUUNPD53mjvprRi4zDfrfAFlWC26L7Zo6lfWznvxDePlQRd1QiFFrj/ZzL0fZl28XdzPNDvoyuRDUpzzftSzh/KLSI5IrKdjRcFBfzffF5ZB4Acpg6Ze1p1xJRuYxLlludureS+HGy37duixLGFe5hJs9tH7+vE4mF3JRY2I0leb2I3zAPXy0cN82Vq5HPpvm0RNR2V+ckL9aRaBIIpfaYL6K0grE2JRYuv9U6yTpNNPb2jUqsDkXhg4lRgUUPu6QEPCFHBXMfSrxDKRLuDfQf7Q83+vuW0PlGX7jUIeXHCO8N4B0tV7TkPpR05WN+m2SgE1cstcPcZTKSIhcAtmatGlJ2+KxVklwkBkuVtOY0KxW9yiWyPvPZbuZFVMPXedcorI43UcbHCnQfbfBgdVII0azMZ6tjmArhb/dJFgNBzVyhRR+FWxdHljcuWrZ7w69r3VdrTG9BTG9tMP0z2RBHS5SKIY1ckYVVcJdnqDkc4IwCtsBCfqJHCBvzeQ4tv5RdT2XAN9S9lSQXs15WcxpdKeo2W71GT4XL9jE44MB4KLLFCYfL8GUWfrWH/COqKlSclkBEsVhqgfmuQEiFGyi3OhJERRKDmPkyC71d05vMl/m3iDM7xyW7Fmb8zI7jO0b/hKq73DNsc4zL5baWBsG26ijpXWPHe8bhufP0NiOJve5I0gb+zHBctD2cydp2nozJonLhS3X9qRyq4YzoAMvw9XWB7pvkHdyCOSrZNXW+EHN3J6J+fN1hcTxthqtOw81hIzC3I3EFNRVbF2YJWJFDkQXONuSirXskeu1gCAtdkFf6RqRANjHftxxRmi/PJwJzn7gk5C3CndvKn3XZJQOc17sfz898Gk9GIjmL74pszpsKPDJ6e5zeuAdDXCQw5q18S8rkOkWn2Sq5T1Z1lKac3gcBZr0OcEdzrEnkXiy18LXVaSpFLBQuea7XMRFaY3tNPI0Dipe+ztDVkRufMMR+gdHwFrWdult4NMAHtK54CHsjq4kxu6jXsIXFFDuZVxL0MIR972nIGUwfEMJpDPJGpEAOPsKDmDu6Gpcsb5xc/Ki5DpIPFvIRZfVNFJMMFQVe0SroAEQGY97K+4q/et58sYkXGJ6GH4cKDN8t+tPWgYwGvswm9vSPOinQl+oOFb0NxkWhQhDWLQzmBox/VG4hvzXWW1jwY2JoHKxWHDZelrXQNrPbJfXmZ3f4buF0aevwM8t+t4jWytJGPJm846O4LaDcRv4eu+tNEvwwhD01jh83SUGdYe+ipxKmDA9j3woNBqEYINmSQ1lYN1F5EWFjC8EsAxRbaNNRTeFQXyi16jUJ8Du4LRASbVOx1C6asK8oxqQCxTZy2T+BxwsmU2Cjv9pKR09xij6i7La64zUYBOHawhkqklyYRxmMQyvUwKBoBE0MQVjFx90lm+Mswvs/QSmOimxwypMx8GVWMSIJkeQo7Wo3RzZCzIgEarkIy9PyuBRoSz/ZOBP7ZJ08m3FFVjYZcTxn91hiceXm0JHHLgWycWPm8Y9/1BF13fB+zFGR9YEHrX9wHyVpdJ2kAno5lFtL8CBgCIt5SDIFdrHUYjeQPzRjb0MnhcMHoNxql5TcPFF/x0ledP07RHhvKuyU5IAWBxZR/IOmGCaZ74Urdnyh5X5b/iSPsPE2f71H3iZ1heCpTdb1Gp53GMK2hehR1wIPYUk3tGqrJvzSBBRbSR1Zdw7rp8O6qoSwAqHUGvP3pLxLk7JSoOdBLCjT6JoUYXY/w/sw0TMGQ1gcFuDdEK2axPxto1GJjf9RQGWN40u6AtAMX629oUfkhBbygzYFFmvQBsVJlAK9G5e4YeyP4y6TNXBYp4R0a7E9sNO2x8NZcFjrXjzOKlSUEKNBAFarHlE8IywIYh8loNUO2LA9FaDVTuwyQY0ccpqRK7Jaz1E5PHV/lK+vk4zub4FxaIGtxoJ1YvO618Fmkyb8XgEEMMf/HSW3d/wjCO03C+oA+zz7nd33ZMUjaT9Z0AsYzwfr8fRrhFq9KMAc2lIpFinQkpF9QcOoAt0626YcIIqxJveoeCKTKPjPuDJzrJ0d+TVLhJNsvszG8i9Ru5wBjgqx1AHzJxSVdYFI3yTYRxAOLRysxTAjodABL/mhxM0CWOCvs1WK6OGl6O0TCm3xnqGCXDmHnUQSEMc2CA3UTfQQzqPIG1cT1r/KkbBgWxPG0y/cXnE8HRaHQB551Wkieci/nvHubY/BsweuzMrDjjklxiQSgjS4IvueyhBD5fbYIfXDlz2fsMf2BLI8yutNnol3hqByq7B3EKsbtm4W6NE/Nf5l/DqGsIw4aM7YsLUARRywhVuj9QI9weXx/tbuPb5F4uVELMNXG+Mz7IWhw7zCm3UpVqDYxs2ZRIJrk36ycdc1cy2L7oPK92FyalxNYKzsVEwstcCc0CAkAeXw2aaXt+gi+ZP3rPZfHcMCy8v8Au9L4wrGr4O17/8X6DhEKLR0N59HmXAdb1SweIjrxI6v3QkA3EY3S3hn0m44bm6iOq2+Jejhk2AlCoVbY3C12tPztkiDxOW2iKzmNMZW29xhkkX82zNckc2JyRqJz4cMXy3UDsrJ+2uZYMuNCmwiXD4j7sy//WQVdVNEWZkIQWujgjkEb9nrVGHkZITL/XLV3FITMMtvSCNu565XdzZJ64pobuGGcO8xCD08fUos07BW17TM/eGGjRnIeZ0iWVZAA3CrMADY4zYqsNg7N/mDJOkWxFIbm7wNwYRRA8U2TriywjsFas2yL5aJQbhyOJfWzqRXIiAIpxaiJ8IgXUIaWSsclEtL3QSAezsFmNssga9ygwAWjt3HqoiIguL8usPn7VHJzJmqpy5mMLkoYWX1bfU94rp58QE9Ai+WckXWy3rzUCy4rndFz9hUaNVVc1JJjimDWJ4DOnfjU4Vj++3P9ugXxigU2kdYwLEVLkYMbKtszymjv/KYML9Aklao6IPsuDVULLVwPSYrdHlXr68zIa8tV2SOs030MMbWf1zIGfisnXjbotR7HmxYMrCO57CHUPlalNOsAEMHYJUtfydDemzTDUTIr88WOOCj5o4UaV9q44E7K1BzUiTeBRwVbRufBzrMH2NzOdPXYth+u0V2oOp2lHpanmCVWw/39Xm+Eoqfv6nN5rkIwrAsQnemVWPZfsZl+w/cYh2V7RnYiYHDca4nyy7Bq/7bkNBRTywVZJFFMph9dJEa1z4GaPdigPaxL/tt87jczUCjF2hvkpgGLjPemwCmGoza3Wgzxbf95hs8kkarCCeHGlhLhdq4GIQ7QnyZbTBiF0MhiUccFT9jo1E1WZ7PGigwu7xyYIVuGomC+2DG/wvcgnoOnDgFDwbjvh3a3OwaN7T6lyblD7CyYzzuyzhYeemZXnZevkVFEmVg/oog86XA7z6PVkgnWkE8M/X659CdJ9dviHxFzy1jyK7mGjgoy+Q2Q6veu8LftwPKrbbcO5Pp4rSE3s8evi6zNxtspf9cc7EIXJGFnpogKyTdsX2pqy83FAW1OKCsdCLI1qx+LOv4rXMsJocVTV19adtkLhf7doUIyrVGe++LVxv9563hb4mhNIlJF86O20WHWltJ6kSAys2xk7Q/+JOQI5n9bsPAXVp/noOH7w7L1VGeVUWeQmlaZDDPf8MbVuYCCNvsUhYio8NyS9V2KW4aZzaJ9m7C+YOpcAm6SfU43weJCobAdo2b6SWMiz/qqEDl+e01cEODLbTc5h+s/lWXlfiehVBosWmnm2sZYrF0njDK+UxGKg7QwjgqsDl7z35I3zAUCqe87rDU/a7+0RwfXdi/JmOv9+RVp9FxXXvv64R3qoxKLOIFyv7Vm68F52fgy+z7KaD0xAflduDLbPRnVqEmayCvO5kCq4spQH4Hl+QOx4+4/VKwUpjPNrpxnyhCLLfUNEzucB9V06Nx0DWKutu/MQYvIjtcPlZdO3e/ah7ufp/NCclCrIxFv/S/c9phcWBkedVp+PhjlN3W5EEl3kJivls4YcRgV+tAV3r7EbgQabXY1Cm3tjZflgmx3Pb8sU1gbvNCNPiwb19kgbN9ibateyQe0cEQFlvE4blbeSNSoOWDeMOHtz7vwPiFVgRiVhZZlB7U1R1utI2HOkcxpaXPKqHC7LBy2KGbZjXprAyZ9WGZTGUt3DRvP9ltFghVTleEJjcJ766Ayu2xt04PXSMAmHlbX8jEXuY/ECeG7HdLbAcxttlLGc5RqZVlfJ+sUCHLPQWVb420n+RFvT7LS8+j7h6Ngxwr6k4jtJf5Jol5FP3HpYRffOPD+hHfs4PVCi/K3FrIfN7fRbEVDcoWAWSD4nEVDknlaaSDtsij6D8uJh2EBJC3fFRgsUVpsjtzu5Puo4VR3mnPsRXef7U4D0jwTpg7CWg+2Wxuy4o0LG5uh+/22GQzCZXbY6dZ0EC8TcleZ9nprMhfWbnqqVlV1PsirzegnupLdiGYQKFY2rWH1yzd5yWUFBFN0HwaFewVliHP7K8QT2S2URUQwGyjeFzVoaTyNDpx+zTY8+buqbynC0lNG4bvIzDNdNrLiqTeNGJCG4PuQ40KLPGJARjM5+WORcPsvNr8460PQUjKJ5TaHHw1aa0lqIFiy3m5qKKqFvByRfb9hdGKpRYuxCaXOIxYKLTG2+Qol/onZUD2HHdUFwXK4ifg8UQQwqaFpt45Vso8ZrbEvs+X0WO7HkHuBTmUTaQfeAOT+WzL1/V1lVdReprFKe4YxN48hGMLx4+6FnoI+xYuSf3++QfVWGBIzxaVY4MhbVtsVYJibDyEYwuKsfAQji3guqLswRCO+gnr+YTYmFF6ghBIMgPwEG2DxDQAD9E2SGYDcAtPalOFs0yHr5b8AXOdC6fB2dC5ItveETGG+td8N8d2jm7qbIVW0N1rvswG60NUrM7yJKvK76hAeG75ABwJiMU6d4fiH3k9XG6Q7vPUkB4tipeWJSD2q7csnAsqtwjmublJ0gRI5jgqcLDINxKLfGMdvkT2BVjp4o0vVgdHmEUgE0MNadF/XAsIjO6/2mESTdDhqyUmYMxuI/wUlT/QSk1NGYxdn4/u79+KPW6+2mE6ftwkBQ16+ZRnfAIMEMAV/3+hCKAyX+7Gwe+SAsXVO3Sd8PFyMiAbF1Rf7SCmK8iHPAXcUTIon5YgDpJDObV0GGU/xK0WCOCMXxRWEMAN/+mRHDUpc8LavmYmxdyXO2E/vY549yhfaL8uUJukjdeEV4gxhIWk1dhmLJI/qZjS2xlRDCV0VMH5tyYyqRrSv8VzVArZGHSwNtpxQ9LgKOgJQ/i0AI1IDmUVCdFbeYoBKcBsgsyL+C4qkdQLCwLY7KsSOE58VGDv8Wt2XrC/ryuzx0o2XFikN3XVrNFKL51xJZsDnzCZkHfhWIbdRp2jdZRkQrZZCYh5Gx8icpOu3ax/zqs+meS4HQXY1hwHdRvuryXeO31Iysr/bRcApcsDL2Zopjk8CvuELdVx4OGE5VLhLH4LMdf75IbunwIyF4DShbnM0EzDXF3bPBb2u4UGLdHqe1LdgUwmFNrhBV4AZT7/BRg3DK968Odsl35bc4zhFuBhNjmUPfdDZ3B8mcXuAPDW2ntpT8uuBzQJZARkFQEA7MeON6YbaK8EldtYPnGyIVfyRZuSK3LACdxY4sss7GKUEatfNH2Z77bYgA6OCiw8hKgshYcE+o823DSQnRp/UMo1AeAZa9VeYwQIP3K8hK6oO2EYEmlQEjo0FM1vT54FzPQQLpcCyYJKkyeAJ8NiqQNm8NxXLLWhpKy/rn2V99O1j+CBs8OxcrcHapZXcNASENc2QDJIQCxMBu0xqe/x6BS5nLu0R0BGAK7IZqHqqkrNHgDAJlI2RtmQVEpMWSYUW/Qd68/vQEZ39rtFCCR9eZisMlzwI/PdWr8ekRuvkIZtCrZrecaGiOcOvUfjujzDdadcnv2XUtrncVB+bq1WadBh3h0EgZH0bPEzthRZD20Yv5GI0cVtZIRlGk4NG+vdDIHTSu03WyyHuCQT0l+Ni+z8TtBunv0+/15u5ySInCI1qY18pKbD4vLIlrTqdmvyyyKKf+CBQYeWfJkFVhKGCNkpowLLk0UEH4HyZVbnhPSJFhCtUPgXkB5/ZwWLyUOK5nRZ9G0C5+DddwcHCCyb1v7jnclSfI6qusjIowrIN+nOCJWT2aKsPxEbBbrkFpqBwhpS4fxe5ygq8+wkL5rZ4n3tXKENXjrtiO7deReHUOiMVx7JoQS0nzc4naNYasOp0c1Ns/HlmHX4buH1Wa2TDAzxG5fYUJoRX/jqoARkN0N7Flv0c/rnEVa+IRb+MTanxV+HYrs191lUtKaLeNOVLbH1z0EYxyU2xs5AY+hIGipfbNO+5e8MdqPCLIwK8giFkBQbhlhitd3rO+BqlpfCE9A5aDwDHNOoPPIv57+N7KIQtvmVNuoRiK5T1KoKGLccyqbfl9Hj8SMSyDAqsDq0PMKyc5sXT0I+wHGRg+rb0jfr5LS1fbB2WxRLgJfLYJwhVMysr5UJrQuWAQQwpx7bvwLmJ72LeZviuiAXKNvrBqFOzSCsbidnhpimkTu+eXH/LJbvA/ytuS4stwVgs9n0+lOcoo8ouxVuZ7MFlvjOUJHkQrgMV2R5BkVr8ylB2AIrt1ngx1/sLRzFSVaQG2+nWVIlUQoKOF/2jOWcTgAu+Jjf+ok4g8hBupW1pxFspklwnyAWL+Udgm+V23ubd5k5CeZgDEoznngxKYxhIqctvRncPoXEqeNx0Zy7ieOMOBS4/vQft4aFvPWamz6bUY/hpj6ie5QKwbfMdyuveVGBsVXjEnOM5BU7EOGowGLh3sBPpGxcnkgJ68XHIxEeyO0/2mxpblBRoELANSrYTY+4lEsKcsKZxdwBAPPZZr2kr/1+iEreZGcLtkZF0UTjbG6OAEnPWXSuuc/VOCZa65g2xXtVYulSoh7qhZ1g71jtmml3VtDbJq3+9+P4MS4Hdtch2O7D+JMiX8u4my+z4UwZznGJlWwHea8pwCtr5TmKgPO3yPJErHU7HD41aamEq1V8sRPu/iaxFD0D8Yw1Rp+303Mb2KFx2QDK607ko4DT6jul0w/lOIMceVt3UHkR36FVnaLLqPzhGe3FYHKJ9FJWn4Zr/Df3B5jLhVe16ScbBZNnx48bwqhimmKuzEL5CylsbdPX2jouFMv35kt2XBS84h8VWMwaXsTOa1Eds98tvAIRjRYtKgHfuMQO43G2AvF13y37V9PXeuEeMmWWfRRnhPlsswR/SFYr/pXh4atVdN8tYfUzVMTAwTtXaI8X9K4IhRb+h/zhGypEqWW/b42mP4jTEA+b92ic/MLSutMo+KZtHsfw1RaTuGCw3+332Od5Ks3m3pXZCOLpKhVODptvW8OGX4sgbNijcWBDRd2/FhtepDWXF7T5ssRxtexxA/WjBkslx0MZKpI4UNgwj80lWZ4WxbZz9n+gp+YNyRGm4asVJgGJTX0g5aN1ukdbl9VCfHx5h9boW1QkxKj3Y+IRKgcO1tSfhn1po9wuqfk05ybyL8Rw7VugXn4G8tPFwQDX21bPgnCmaXmWeVGm4Aae/W6BjQQVime2zGdzXB/E57k/WL/Nnd/mZ0lMHgQAjvPZoiWvK3yo1ulhvhLWR/a79Wltl4TiM6oe8uIHeHArwFgFv2OBe6LS0j03KV63g2GsWzl+jO+wfYdoon91YzLQrVFtbad84827sbnc65BW3VYlp3pD1e3tVDG/o0NyRyLsH3NcKLwGNCqy3e+f5MU6qqqEf4xBLLVQhJ5PcEsFtL5Ok/KOXz2Yz0sq1l0I1HmXk2c1jrMVnlFuXriirVFhlE3aTwGMtBbM1VaTVt/2rWxgJ81uWPaHUfzjNMP9in+EC9GSIHXgKGNM0zBXsHwSAfMfBI6937UwiU9RVt9EdJ9QXKI1Xod8/XgQRgdWNUOzrSbdt4S898TfARu+zmE2LLUDaO/ih+EmHpvLhkCLYs9FW8dFrS4Pw0QcMqeQTw2GPQttHQudIzJVq3bqfG8ks7ic7iOrEUzDP71F9EZiKb1xwvZWgs3qPeud46ejIi/LC5SmQTiKx+aysGlRPF+uWmpZarIJNG+wei5KLCqXJUldf6KpZxuVPVwrBbK7BSh9jkUo3Br2+EeNarSiD40dVFUU3/nnwgBROrCLIZ5p2IZpnEfEFdk5E6JbRNzQIosIhTYszltLtpbSSSLGs3XfLNZH4fEZ22dn/K3HT8kaiSHUw9dtFLxg4uYnZPMtxkleJHwmzOGr3cUw8TqYLQaR6YavNtfJ+EtkdrXFXnTfLA7R0CZ94jvSf7TGI3ZpVGDhYzziPItHNrUPY84xST/Mfy2PnPxzHaFfljz+usAyfUlzpXHxHf1nO1xA15jPFkcpdNWOwZfI+TKrHq6IlzVK0yehk0zJ1ih5dqh+Wp7F5KDm1dUnOpgTn7W1ftC23ViLmmhUYBfDJIYwWa1eecFfcKFfLI6sS1RkwoCGrzaGX1mKOXOGr7Z3xi5Kfr6Gz1bje4duojqtsFZbYW5MorQUBguBbI3cHkXrTZTcep6CdlicTgBkVbfVbfucV1nfYJ2lTkqbm3GBDknHyFzOR3UYtpW1sb3ZrJ/NrXSeAYDiXRSbcFeCpjH+poqYczcqZRgHb9UbyLECFLvgfqvG/dYH989q3D/LcS+k6j6jh/IjqipUhAtignE6KD5TRBPpP7B1MfeUCm5eo98uAceMm+3diOj7hKKyLlCTl9530WdQOS35yvrbuuBPkZD0nLj1hQj3xMovt3MH0O38v0twk6W3QuaxuTOkAsWeJ585T56uN3lB3jW6SXzvzo5QOXCjpv62suJJnuIpFfGw3+3OHqH85Ox32xh+CN+4xNY1FiCd0o+EuyTffLE4JIp+8EdE9Is9rY/y7Ca5rQvgHFoCYnPdkSDrLoFwtxxHRRa0a5KM4VokzZhgggmlW6NsLp6yOMzFnAGRy70cVe2JvP/BruVc5HURIyE7BPN5qSs+9NbuYyWiGxXYjlRM68x+t70nciq8qzB8tsV1URWSS5BdiS3GwzxPIXzNdxvrKIvBvd6oYGvUwvEjWfjfoU2aB3h8iMfmcj6oRTGNlmhtH+Be1I21+8rfsAm11J+W9L4tx4rDV1snxj95y4H5bGGp97MLmUdi6W6faPrm4fhQVZvLIsrKdUJzeEI0k8H4taJvw9bkazaIYpggX2br4ofPQ9wPQ2hN8ERkXLL0UQThx+QefRKyH4wKrORHOHrvvm3ZmhVk3zxC5bxa7ffNlLxYdCusaEXJHhXaOprEXrqFxeM/7xM8OmnkPVBus9C26qtlBW695QoX2PN771sVs48nVrDGmc9Wc0T0quAKYL/bz3jjQBBjwqFym9DUsk6r0+xGiHIdvptj+3JzUyJuqem+WZ5MA+fRVuf3URXfXSR/cjzMfLaYASxONGnamO7916WXz6N8vaFZ+VVWhBTIwXg+KOI74URRLLXAnKIo43Ne9h/nX7IPyjKPE+qqEy+AoaI1HZvkuVdsomJMT8V9L01N4X4XB8+CA/bCSmRbRXNXjRMEsBaM1noBOcSyhMx9r7w7fBkVtwjajBt1mMVl2dnfXoP8YM4yF3cJ/bO5D3eFdWtVJOQd8iMi7E1cucJVaVBbcFCO6jSAQj40YAr0bXnyDddAAK4x6LIn5zTUm5tpOoa9YnS1Kl2eCC3mxWtguncP9bQVsXpOf48wwMQDnfOb6BbPYhN9mKTEeO2fpTSYbb6KbMpt5nqM05OmHLKQ086h9mNM645hc3uVkPl8cVp+rtP095c3UVry5qZu9N7M8y4pqcV4dbDZpAmJ2GyT3WgWFXU9no066C6RjgE7qRrwnKsedQBuUnbTc91oiTW3PhGHdNalq7BiiKGWjB3O4KwTRmTusG81L/Sd9OOEFs3cjNA12/5/Gd2qtycQuCQN0gBjshEREfvuPpwIaty5ILONMS1mSRhvSWU1ZHaE5RYURr+NNuRz2Wqeo4eoWJ3lSVaVHxLcD7z+fC3R6ntS3bWRg6ocV9rKYlYroYoBX2gb8pyBMa4AfKLv8DaanzoyhFM43SbcZu8i1AmxeeGQevIRjy2kxuFxbyMD6cevZ6HO10piuKIkQwUP0jtz2y/932X3oc3lQ46B03KoR8571hElSLmJYuqqWaGTpCgrwmnXUYkakJcvukOS7tit9az9kR6lzTvIHcCnKEtuUFld5j9Q9vvLtz+9efvyBX3KlNxfS29evnhcp1n5a0ynMcqyvKJD//3lXVVtfn39uqQtlq/WSVzkZX5TvYrz9etolb/GuH5+/ebNa7Rav+art2iNsPz07x2WslyNsswwh8ZdiCt5GX7MU7/9BxKYoWOSc3TzQsZPv73mK/4G8CRp+/eXCSEpFWf6IBfNe9icnBIoRHv58gVhO3IE2LPeayV69kizaSa7j4r4Lir+2zp6/O8svqoQXw0SepvFab1Cp9lFgtFGmw7pNTlosuzaadmF1OCCCsX0hWR3dEN8ToBxXiZVGoZizQ3YAIg+oSpqr1eUwRCO8sEFwhmOdsKFWnfuOEcZVlQH5fdkRZY2D0wNhn/mWZgxNui+F9GmfX5H0jdzXBd3+cNoDtxHeZgTI8hTLvucWoyas8RBh0P2zfYUZ0811Vo/eoQ9dTur+yGt//LFp+jxI8puq7vfX/7tp5+skY4vVJlOqfEsYGuoEi4IPN8ZeOswA+3zfhpJMLMOunvDweeRPnOU/Il6U/qZzOhwd4RphDdMfz3Fi83j7y//H1rp1xen/3kl0OOKxJbkdbb6txdUln598ebF/2vdHfaB2+Ad+ptLh+jzVsPLqzzvh+nZz6Rnnoqs7+lUnXwbrJPOEm8uri0fPRMp1erdNy5z0dLoqE7J8yoavW6N/muW/FGjC5Q3r9apkNvahSdpdHu6xl3vInKV6H/5yRb/eZX6WIgBzXzmiTl3JBObOI1MN5fVzlEJeLB2Vu48lqO+Zq8/f3JYfjp6TmJ4dcgDGmCnJUnze5bWt0nmsfE7LS/zOpazvce2iI8vfCacauQR8/SwjRnul1+sUff7UT/ExnMtP6h8VvPsPS8nBUKdl99nobmMHo8f0Xrj5Y/CSNoFq7mVB65XJkqky27l49FthIHyjwceN5HyUXJ5mj4Thl98CQ6tWfuUQgGcrUHMQ+Jf/ZJ9yMnFtlsvPj9I0/zhfY3KCq/f3/LKC5mb1Qr5hKKCHAUimq2gwUMeMa4SMq929D7OVoEwOe8RrHTAQVY+8EfnO6sJyIDstUBTa0s0wOd6fY2KLzdENkofpp54S9dHerVHOM+Dgdi3fO2YaKjpxUinm1FojNlWzmy/dLDZFPm930IwTuEp129m7p/RS/FWyKzZ9JnxZ5Metmmopl60hKK8SYiw285DnzBE6SKzZrk2AW1YpLKYh1B4T/JiHVU2B0RyXBdRWoXu58FqnWRH+XrNnJv7xbeUQfZkBzc3SZpgLvcjnf+O7B2i15dHKOT620FDtzu+7lWXiTZ9yi77sdDHqKzM1pk39siDrhCkpx/z2yQLZVJjfJTDsO41QGnUR+Dulp3lICCQGRAmI4QiMe26I2Iw74/xsgxeLd3ZVZmGkhptAUyZNBgycA12wkQOe6MsTGBfe1XawcruKnrJCEaPaCBvFjs4jbjqPj05YoLEvAjaytObkMjeBkH2z2RzlpdVlEJnvG4erbs8Q802OQi+k+gxIDaP9c98UwPladhZ7TlJ5AL1M5bt0urtsCyDHH085M07H6flBAEPl3cFQub4f7bFj0UEFUnM4XbytjbpBC/zb5GX9bxg4MMSztpx/pK/iuw7xesGjvAPxSOntxmm3tEduWo2CYewhskzYZDpLMUF16br6wLdJxHEoL7b712IBjtM81tiEz4TFl38+Nns9pGZM8Pk6pa53dT6Kb3iG7tjixbXEet0c9HCn/MqNMohY4bnSrOdB9Gqa3iqxXrrbuT5djZg1O4c28QzKO3OzqrZdjSkBd/TmQJ3gV7WoHcZCFY/jN+SMsHQWMEn98mqfbfUnTcmMYov7uhDE2El7QRDh8YZ/ICrY5z2/WO/qQ4XCdXhCLW72GvpkeXRxfViQxs9BDFAyBN92e1FvQ5kfQTB1yG7xFZmyg3Ws3+hUPb+noN4W+7MXPyog4tIlNU3UUyuSRV4halgl7JvK++rRL1ldMB5Wr5PbqqjqPDaM3Y4/Nfnc/RHnRToS3WHirNRRkLXPAgU37DUa8+87VVOTbZ6VRKTpf9gteKa9Or+afkuf8jSPPLb0rc4/Kbma5Y2Qtih8xrZp+7g5cuNgM8p4rFFcvy4SZr3E99FTzKMRo6/FiENGaAI/bn7Q1ReRCTFfohZHWNyOMrk6vucZeKBkUCyg9sCIdZ2cxnXCNElegwV6XSO4rooPM9xeiRHT3GKGrXhp+9YfGeoSHJPMe0x0iWcovUSrFN6/tW/w+Kjy0LdI8JKlmYli9IOW+NX7/fSKE7WUUoSduFfJc289ebveFdKLqLiZdKh60HC5E7L49KLhEzOFj8mwQYLcT1m91jEMLLmWMlzc1Tl8Y9/1FG7wffQ5M2eiOI7uI8SXDdJGZweTm2wj06rV5IFG+/H/KEZaxub5jcN2IZPbp7oPvokL7r+HSK8LfJBexjFP2hyJJJfzzPKk2zRCL7ThoZ4i9BvLr1MCrp84ZlJ1vU6xMQ0+KLHUPjowNGqRZWg8KYoZnMCfFg/HdZVlcuuWJuKDIH+npR3aVJW/ghbWU4R5kusmEc+DycvKra3Kaok9vLFjBAEXzq+pKtpG2i3GUfkTG2qNi42GE+UOg/E6PiDaaM/CrlM1iEOMVjc7dFIIMydq+k4q1BRevNiq71GWNHEDNRquFnbxPuFywQ1suul8/HiicrqoKqK5Lqu0FG+vk4yuuOZlFlx/7uHAMr2IQCfUXxHye3ddOI73qYER/89WU2I/cO0tOlXpdA6p0ccVuGEOjEIEysy8XWbbblxCI48uUfFE5lVe9fLuLaP46WzS79mCX+OadCPcW2ffhxGJWqXV29HQY/rE4rKukCkd0pT2T6DV9/EwZqN/wi9TPTNkB/jphw8kod1tkpRkxsWcIv5HgU06M9QcVqhdQg3zQghIUNIfBd3eeP5wVp7gnDKbnV/JvET2qgCl/xgHY38/eJfSzKtMR6c57l6/+iUgC244dA15a5Bdjg9SXvGVR7l9SbP2EsBTjt5AUuoiNhukujxMV0F/Rh1wEOWbgu/uvn1nn1Obi1SEnlki9iIWwJHuRzmFbbAgsdIkYdGQgtKx3ZhAqX24UijcMMwZzZnCQ0wcUhx1FX0MfBJtNpF8ifyGcEoOKu8zC/w9jGueMxuj6E0OL6MTxFC5c+hNvx5lN1qfPYOHBIwpjCsT2oLI7d2wmcRykGzzb6Pm6hOq28Jevjkdq/bPIq9UV3PxBhqR3OYZNGQ5BZT7Zp+cFu58AQSPBq1ZC/9KCfJ0zOdlfWzS8jhZ/TgI8Gn5WURZWXCxxkZLIW9HFwxSLxepFBLlneX9FfCbG+EPDORAq9wmBhFXUWv23fT2WTbcFezMyfa3f76+aR67Ebm4Lrva/rOHE/a8zpFgZ8vvNigyR1UbZoN+K627arThfOFQHaOygrb7dT2ZDO9S7GaOb46pGfSiHKnqWIQR0+EHZqYvNDIOwpLNkqe2Fsj34vCx49VEZGXV6e0K9kDuGei0rQOsV/srcCjPM2LD+gRfAHEF3m7eDXPwwQ+35t4YWxlvzmQIqdRz4SHFram2gM+105w1b26EiiQOZ7ydFl+2DXHxWND59YE946TFLN5H/Ll5VH6kKzQ5V29vs6YTIkuiNpb4Mt7t56Z98ldJfcM0vDLM9HQw/hc9WNT10s59qT1vAjQoaHmhR+u0xIr1eY8wUaAbNnrmZ3OLrzeS87FTBJ89TX9HlwoT7Caq4fLuvOrXlsOZK+a77kwBBeyFLXvw7j2X4YbnyEbThKqEjxSgaH7PlYjbKzGPr5C27vdi6/Yhx/8tTaA9KrfTRJTQvY7jGeyRC1sKcG0bXSCQ/4RJTa/DSpRhM2+3/POQROe1R2d+0enT2mTqQj6TAQAHmIo3tuadyAXYZRnwiI7MwutUrmMbp8x5S03ULbE+xYVSZSB97+fCVEnyFY4RR7BeTIgTnqn3/2WvAl250vyRukInO/IP9vLsgdlmdxmWAK7zfkE2Xb2d8A5/6X/A54L73CGU+X/XId5OS9YmjC6p/lSV19uKEo6yCkekmH54ZmsoVOERincmmFil7YoLgoS9fYSyHRHrbxp90x40VfBdf/3dLn6FNGIzkEDXrGz7HW5gW/Ludehttgk7wX+xOTh9E4K2yRr9lmzmN1HVhV5SjD5nclIxXQGPnkb/MbJcxPhSS5gB3RbeB24+arpJlT4eUy0ZIjOWhCklawwpB5fjmPNHoRO8+LijzoqUHl+ex26n83WdfUvPE1savLwafDoJnyGhhxj3SZZZoKwtH7JEftOG/JfaT8m2Y9AD0HZb/LMr6t0zwU8D73aDed9nfQN1VnyR40SivImIfwY8lT8a9k/dPC18Ar0BtD4vS/b4gt5KZzYoajJyxUq4h68Ce6E7PgR960MFQ20v0y+zGXyPnvvM1FJS0dWKK9NmgXsyK+4mgRUj2qH8Eb7jCVk8mljlsaKpHxGd/me08vE9B5ZGFQkd1wYTEHCAbczveNf6f3m7XjXFzhR/2vGZRura2JHFlmUHtTVHVGRTTTTOYoxwZ6JCu8WZffl3FOFH6+Za7HeVj+ZrVNmgxcQbetWCIz9C+Gty/wHCiMhFN1BHKOyDIcU/3mPd82FV+YYY6E7yYt6fUaemnkeEnaZb5LYXrzaan6XH5cWbqOnFc320GcHqxVeO8Nnpd21SwhUPih3PBMBoQOyZ9G22o4LCJlHfxdzm0hVbRY5RKB8bjSx1+Oe33zf9/4YlRXphacXvsUimXJHbDS5k+dOaDc10HNSPu+LvN44aqC2bvBbHf5PZwTefH1uVx0vWQ6hUIjsQUbVXq1MEoe3m+qJiuUz0VE7oR6eG7sF9OIZc25DvOfBtHQs8BUdN88Gwac+zV/uQGyx/UObGbfdGzvk6+Lq+50wtolfnXsjIPA5JKQ1L3BbNd8Tx2GFwNUm3g2IqsnhGzipc8edR3VRoCx+gt7NckTcIDzHSsbw8PXvzlJ5GT22q5L/DvtbJLlv6K7M8Ka9wrKQnmZxirs6WSzfqLHjx3kauySN9YnMZxrhqNF5RtrqhnlG2DY268hwQxbCat/YSI1hxZ+QtSFKTxCamqbylqcmsLzlqand4g+TlJ3yyeSM6JPi2LwVqjomawJb6jW2dlYTv5Nwjh6iYnWW4wWu/I4KhFnLL3Tk6A7FP/J6CK8OvWsUGgh2KbYzAiQxSrZBJjc3SZp4J8Lrrf5NkDHSGBqyjSHvWRQI65EjPP9jO8Vp2jEWUjvMRJAuBTNMhfF5pjosf6CVjHTePT26v38bDNnx4yYp6M7pU54NeR4C4v0vFIUZO8uX7xKs3Kp36Hp4YN3tHkmP5iCm68GHPF0FmisReUBGYJAfRtmPYFspDm8wEWPxnh6FRtk+uxMa7el1FGhBajU0NQra+L8wMlFjY69I/mxediZx81E8Tmo3Cfpg7CZr4ByVzB19T220IXlUwhNHRByw13jf2dtE4bt+VpO6JQrtTj2LklCRvN1OsdnOhKFpi5LsVbAQburGZTiJr2vqbKnbe6DBbhfO0TpKMiaTpUM+pA8RuSnVbm4/51Wf/2+Co5JuV/i1xLb9hwRP17N5MWHxhwNpVfvW22p+AVnuwmLMOu+TG2rsP0PW6YZmP3lDTa/5+1qi1fekunNkIa66d1dGj8oFd7fMyazPhEE7c4aZZad3hmR4fA4MO1L7HxZN7OY7Lbuu0mR4kWeKhg4Z3pptAm4YzvFgN+RucDBLrccY7s7IBcqIXR2qhw26cN37hMqSSQjunYiqmxFqm3m6hmdQfr1gPxPt148naBzLguba2bJX2kPcAyc5KOld8KmPG/uGJj9dnGM0s4xk6tPQbr/UrNdTk2zc2tS0m+zILvxRXZefxvue9WnZoQpiGH3ELJ4NWX28nhwnWv/7tCm06dObjq+aS1X7EbmIGYZH7ZZ9bKM8l8QvCy7QlIo6drBJ5HaZdyck2248st7N5+XlWTzquCGqV/AqxXAYpVE2ZOFxMt/KyaNqQm7Cwh4GsP6PK09v25WN88T8GaC7ZEOPUJ+H5C2oyS+LKP6RZLcBzwBpzNu0Rgk9yEOhThq79ywCoZtjFeoE4Bl5MfohOd3vaGoGOJ8S/caTew8sbI+qLjKS+B09mzwmAa5LBZq4xe2fEH6ncxSVeXaSFw2vhDHSW45DdP9rsN93QWoagmB1z4jLA+f5HE50c0M2PmHQHazWSSaJBOPzjllnnxkpihC3wrYl2sRigcxpqM9RVDyrRdJfW55FRWs4eHmeGr+Q2zErW9fnaJWdZP/j1cX1/8LHHtNdlsDSiQqSqz5cbtxpVs+dU3PCRZdnouego3PLB1mBjek2PsFEt7IEvhXiEDhPy8vo8fgRMSN1QYORHOHpvM2LJ8/AlfmembKmlO3Dj+6i+ZzeFxIG57BoiiimzXblojL2L/CElUALV0dcF+SiVxv3/byOWvjR2UuPiCF4bPZWHr6NR/1M2OHoKU7R+BFvpxkgaM5QkeTyoAKzhZ948Ck2r9M40xcCJlqpQUe/cCfHthtZUiXR/27v2nrjxpX0XxnM42JxspnFARaLnAVsx5kYSCYe28lg90VQuum2ELXUK6mT+AD735eibrwUryJ1S15m4maxWFX8xGuxKnW8sWFrL/5JA7E4/uldftjIl0ZpBCxCDXpQYLDuQMGmT1GXCMn6143Asnl72ObMUC5iXzosYrUrY5dAj9dZTWwxlhp38XZGG6zJO/QVpWMDxeVFZeotYsSxziTk9Q3AySSK/m8OUfQDjo9mz5kPyFe6RIxUVBSo8MVvlrNKo5yLRX3vlO3GncQ2iSDfxqUQkoTb1HtyKSLhfelH9xsZg2iVvL4PmX3tAiV4sN7ojI73PcWi57YgXufdkL0RYI6/ynxT5Ed3GLK1R+bbcBeDrjsuTm6gbB0es+2UdygeebPR7uEvn5vIMZ6Y9W8Yl+5+3keM28go4BzU2U9saQ8nQsEukO53T2h/TtFDXH7ZSG9rd6J/d9iJXuDvQb3z+LtL/q48u/5+qiEGeLzxW3Jr9uTsQmBqMlvU+Pg/pv3/8LR7N2j9DzwV3J2zqK0+KmM4xtbpQ3ZdFOMG8VYk17TjMo0cNot1lhVy5O0qS8+A/Os6Gy0NZuFflpeOljmT9JEe9pdEIBo23r7Km/Jtst8ji8BS0I1xfqhHjVtU7Kj1hIOHXcdJclIzWtu7/NsnVNDDW5F/+9r9Ihn6m7Bp9cjh6zw33VTi20bBcduahkcQf4Zuq3GXpy4uG0ztUQuvG7xyS0Osmj4WPwE1IaDu0/PBO1Mfd70uAbrNw2fV/ZvstuZ16QNrWIUmSZpvUGDG3nkygdzGnW1YH2cZg+3hCR3Rp7hIalYbQRrRyaY7jVJJWg5wJjxBhPDXWtZcw4GFcN4ISIJMWMD12/hJsEytr8MBLrVzG3TdOBbDb+nUqN62D+/yQ36b7OoI2Mvw5X5bHdPLfE/NX+PuV5sbyO5x+B+o+pYXX3z3zW2RHOPimXw6XRYzlwdAEJeRj5EIy+vvWMvsgEjI7LHywcwsxDR3vm+5/ygjocO1jioJH8vb3r05ZBq+drh4l9cMLIxi7m/yJi+OuMuoqOWe2DunegU+z/PnNCmf5vDh93rxBUzQPmPRv87r+PPX2R535tgp0G4N9j4mT1M3MgAt/RQi2Hr6Mt59uckw/92XLToHeXhOPu+j5dm9kKa49H8fZ+fHmCy1iwd0xGP2Zo6cggwHn5I6X4iYzGw048CP3rrXtj/7eLt93A57P7t4u118h1Ji1aanN9LD/XT90stq4bcxXEIPw0VelvcoTX/2oJ8eNB8c6QzWWzE7rZM8yZwy5pfxqx5FVHbLoGLGffbnGZ3RniQnuaiqePe0oYfZlG72Gwym8jjPbqxWfED1kRfds27RvzTzqctVwJvExZukqTXGx42OSo91+ZxkcfHsdNiv/SJdnki+x1tA2NHTirHLl7iR7++2SPJiZEyv+pmHd5/jmqnBoby9X3DuXdSHPISgd+iUPttJa8E2hMRXQtLpsRwvdzvfLE3e8LiM0PW9qJ9bUZ9H/fd4WHgokpFRRjATL+9WmyXMzjFJKlt73HSPsn195Benad9pHj0kaUk3Mk+AqfHYafbv9teY7TZTvzZgeZu6ZHiX9zYvZP7mZjdvJe4FS2UNF51lqY1s4cK48bq5L1PzzwRU+zV6jM9phQc3gkBcK8B3dxUfT3Fy2MrdEfRZOLpOwpOeGzOjmW76W23nq3/zS6LmmciPdrDssjHDC7dmEmzekIoR3cfGLVn8Ms6Hh71ivTTVx+Fr7STyGY6yXgKnG3bGHnj95pHXv1vyMh5I/kDfyneo/nA36PEAK+c1MIpkMWr0GPVbmQrCNVuLqPXVYN9hjlw/jR8E/O4HAXcU32b7zZ+T6XsUl+cCtamuNvJ96OZblwB0YcPb3dW4CO1uGuquscXQa/y5ZOWGhtmfMJoURjfHE9784zYfk828EQuCoTd5WmcBDsIam75m6cNL1gef8Q+SviSnMSI8xF/QmPqdQa/y7DE5nIsYutZ2fNuTno9Z56Ltg2MbrAezq8P12K9h+PohHsXcP2e7H8At3cTagyWiy+eGy2Dxf/3lpvxIdgH/+csD7nuXI/z8XOwQ+GTZWryGl1o8euhyeAukcs0fa86XorymqgJVR4/S5Fnf9yqMrr+56wpUHb9gIk2MjqEKGJHY6WZUeJuGx31V+BqCG4aXeS67DTAax3EfBfbmv/5er9Veo1OabymJRbv4dHoI8qh0BZpvtehhEXVTkqd6uzE8mkOW/xlWY27JLzrAjV9cGt0UuryGNrkodOBr8Kje/lV9VZ0eijgrjwmJETjeqhDHsevnZs9s60Vv7i5hdIPiYl7C3iikravoPm4uasAmX9F76g214xW01SW25Vzz81xg7nOB+l+t5r7BbO5Sbj0aF/nXBFslpNP6TdmOUC1Ex9xHejjxmG8XD+IG48XX6hj/WQ+lvk5QOmw0ZzO0I/bIJBnlOa1uskfBs9SJ3YfHxxKN8okiV9NjGFzG1e7pPvnnqJn8Fn+EJLDTIq7r6xigJGy23RLAzdUUr3ovMLux15QpirOzxfrZ/MkSKtrlWhN2ko6R6TLh8vwicQYG69HtAnX0szbYsOVOTmTh5XF/r5OdOFz1IEeqdfZmPEQ3r7uu6m/LLUkpy8i405sGnfobaNFysuMZjMxEN6hi2c19zTCRpBoQdRmuHPq2Y2Haq21TTr3KtOX2uYztSVp8OwmGmiF68nVSksmvi7Lg0pUdD+MPtG3LqS+ZxuwsSVUd91XS8lvCaagasjdv3d9o23Zm29Ra+5IW3/qEtq0Zoidb7u3/HmKny0l536jIcWNua6VpTSkVoJPfSYamcsg5c+xa2HbiHL0GXsDsufw1722epp/yOjzirIH2TdJtj/zIsKIXWfnNJboYXTdEJ9Q+r13y5q3av9XvIanEQJ+Obj8NQ5OEdkZRA3An3FSIT0Jp6LLc1AwBjss0P/wo4PDVl7XNbvPS4eXvUDPg6ugOfU3Qt7coPT2e08xxI7qKjmUUdl7cdNVHifJXXLYWD3FwSAu69d6cKzyov7mj6Sdvw03tbT7c7Dh5CzTI/G9UPmArph5Y/ZFbcpJh/aIs811Cera7RiHRuhtnrztUEr+0qEvRw4H/Otv/Ui9fhxw+nUT3KH382/Dj+3NaJac02WER/vHry1/5T+ZD1qT3/OWC3HvVpxnlLt6L5sBq7KUyAJKz8oAErGz/IjRJssE3b82v8qysihibW/zmk2yXnOKUtwdHaDg81Jr2LPmS1+iEsvpKXqW3Sbt04iWx/b4Zrgd09nj1ggKVAdaSf5IAjES2FQGNFltEGVu6DYgxOq0CX8ItF73nLqN74FOhupmvzfSyWDgJ9JR3mCr5WMIggBRMMgEwze90Je2bXeIuAKwPcXFA/CZxAIYUCKqO/wFBag2QuQGqOSmdCpx5mq5jcq4lZUFGflj9FEzUWMes2x+oRqLQtj0VECWNjIIE3c9h5kjTXvSAllYRo1kQk8+GF847BW9h8FZmV6HWSSfRr9RYDkyP8kWToAv2NZKL1VMEwRxnggnQZ+BrJWnYxLlqIZjULciAXpf08w+BQhsUzAY/uZfeVLir4goRj+1shyLYS3FRGKPlZbHFlqweU4w6FliaDUqdi55HEP3b3/72Uui5gVPneElz6n9bOwBAr9KFd70CtOO/4cWBwf4TnRASjHCzAaN3feqfguiW1l0N8ExpokmGd+yGRAk8znQKT4AqpRu7pEm1+91MyNIskC0GiM3iyqaPZ4CV/HnD1Ki6TNL6rSb8tMMJU5rpy2rcWy8ajNpijT8/GkhgoKyKYCUWOnm1QoOi9GWbmbw6jWwmr9lw1bkFruOCo5OWkWH4cfUXHb0qJm3Nf9nRPolJSM7nqP2//M6jLWcuHbrfprn5YARmBeGKwtyAQBYKgyROH5MWW9rFwAl8Mid0JtSLPwqcIAstB06DT+08K6buZafHcUm3cO7e7DILnf7HtY8p8JNkWffPPJr071EvTtjkddqOVnyDO9WuLtOPw4+TDC7CO3BIlsDY6lWeAFzqd++SNtVvo2eHme6a1Ga42DDMrLp8Dpgp4iPMBrN2el3VWAYt2YSyzYxkNsuzJQ5kPcI0w9j8C/H58TXhUtwFXsq4E5Oiq/vHHfrfc1Kg+nGs/Eh7SWMXJTAoDlO+mTGM1spmHJv7ECHCPZ98RcXzQx0SXdq3NBHTqUyB1R7QYjT0hQupqmGwYTWvUbLNjYnLc7ZPUR3P4KKqiuTzuUJN8NJoKNFNchQl0MF06ZQnUFLN1EIKxCGnRpmNg2JUrquJDEPt5UC3xepqDkddP5j1rcvccM5054Jg5hVg2tnyJ0rUjS4EH/05iCQ45bKOtjihIWRt6GCL18jmXGsxuFrNtDYjqKYfrOzufWYequq06o8xuXsqJrz6o5tl2LEFa78CZLQxgsMynAoYVMiVGNuhYQcdqUhgeRCoWfe/nxHIulm6wqKwt5oZbgFgm36msx/elrEwvz+hXfKY7EhRf9SxHrDB8kNyySg3AkCJemuAIiz6h1P9Z2SiF/0sT48HMyCEevqr0NVAyo4wzFNAdwiNfSesUtakfZjBQgdXc21DA+eHHJtHg232AVulwdyYp7Iv6GMFLmUVMcgM+mJTpRtZLVAqWawQZoykBoDLzLlI0qdAZ/4IEDPt8DlRBietmRZon+IiibOqH1uv8uPnJCOEszuIKGSDoKUk35JbiUpREymW5HGiwt9q9uaLB+r08+5YjM69VTeA55/nmATN/5glcowyRDQW2IJNDo9yAy0aerTYS8PfesdEE0hudvRb85DXQU5/JCkQAl0+9THkYtE46bGjB1gu5qyRV+JTnJ57kKo1DIOLaZFL1DURsyUMiWEnPIWBcqOtBZ55BnPDevbl5ITPMWZaGK5u9Rfdx8dTil7n37I0j/n8rNQZTEfAnL/0Py4cDbB68yOBNf5sWHhIUIH1q+M9y7MwU27FLpleCCaCzGK99Iws1K+bSOMy6GPSGC3dAmC1/C3lPCCacNtoh5+5N4pdzuN1BMTrpGVkGH5cfUC8XhWTtmYPiNeHbLXKtbeoSJ26CXbCNGWThLSC1LKZ5uaPCLveXHlLBN1Ui6rRwKsrzAe+6+8VKrI4vThXTzXHxomJS9646BFPpQEjl5pw9SOgUj0bQM6GxTd5cT6SGMa+gSc/SOjbZDhRv64eF4Mu6wHBQ35KdlOjgDQqwqD9eRs4aJRZDxAi8t/fi/x8kqKAIhE6r/15komINCiKEAg6MsMEBI9RQ4NcSxhCALnteywkXmYYdMz7ctoRh5AvYPEhE3tM14WE0OSrF8t+nXT9QsSaDUQfin2QPD+qtQtpk2HS/rL29D6NGiYNsQafufdXsWmeFjRTLnPNUTP7CreLsfmxjA/obYKlKZ4jODjoQmOp0pKD8rAEm4mmyqhl0u7s4VRBrAHfiv0Y8QNAzHxMmQtfRMJ5Jz5y17pkRPVSihIEvGSdDDuDImsCzPLdOuaBzYRuHXbAmdut4/fksbqKi310ey52T3GJ9n8l1ZNEB/du1PgfdlIwzIYfw40kU8Vd7nUxwgTYFbNDhFnrwAq5dmmgsQaSHJRngkWPFQI8oc163dNVXBbWPtKfwkJXQ0uC2mRrJGecMT0677Lpj7xCy19n11KKEjS/rhtDgyLLX2ffoW8Y7bc5ZlB2g9MqzicBwRlxwPLVn11CWq3iJBPCmddJULMcXw5cJhuGXLHCdMt8fof3T8mpTka16JmsE5IN6Nf/uG4A9XosfxrrRCUnRrDcrr0WGDnCgQNbECagok3HegKR8bFEX2HeS9pajJPfO3rNFPXzmv5Xzugzrlaqc5HVGRFRAF/jQOthSmRuacOUbGANTOuzitVv/4Bn+lHFCpvrG1usgMdZf861bU4EuYqLikr1FzItpQYmvETckoQv3E76SEE3kzYXkC5SgNAqZqklwGzKucoJXbNPVwK2ln8HvwRgTXgj74SruS/mr57Q7kt+5kOeCT/LRzCBkhnKxNJp3jaDaqlFCxnUTGPPMICUaGg03PFVZ9z27c4F1vhwGz+To8ebLKn5ejqBVJ1Osw1z+ze+cN3HioI+Rm1SPbEYfHQXGWqNfPVzsIMDUCmlaGFvSNwA4heUNhcmfN3Z8FnD4CvuiXf5IaL+XXek/KSBo2NOHPiySQBJtSqTJtS5hcpmYXBHK2XSHCfiIqC2ip3nfKiacr9pC6fZt5r+8RMuBiQPnW1AZjVQIc9878+fy12RNKkmJw7/QbctPqdmS1cPC1GnVYAE6/c1rtB7VNYOnNGbIj9OhxK2ce40jC1aPT44hUxapDtjKQB5yH/CYyHwGLpivlXt42OS4l9QNFFohr5BltHw69rvZwdVjHY3M/t9XOxSLrBhrYV2ZCBEM0fY7EXndjfpJOELRTMFglOvj81qpK4732ValRd1cPLkGBfP1993T3F2QHf4i7g6F7iJ3bMCXi0BC63uR/NRhojA3og1vwTCBKRXGDw0epg0pOiAZUCD/PETEzNggrH8bGC4jHdfbjIsy+5L0E1ukNlHIjwjkpRm9YtdmWar2DHLcLd8n4/lgW5CD5AxmJvbEeTPMzqj/fUxTtKLqop3T+Sy/U2iWGrbp4YKAjlQci6BGUix+oxTsF5Gxz7JjKtvSuyoEX6nDp7GEDHB4ZmCSQY4SngZxgIhS26q4Ogyao6WbwnYooYxqSrjOvaHGNBsYTDbmEbVnA1+N8dTXlRYtkc8wEb3uye0P6foIS6/yB8+0kTM3o8pMN9FMjIwHLmSMO8YpTqHwQurk0mDrYRJdqhlnDGHy/xQYWTg0q5sDyqsTquDCm4ozRsfWFCH8f0aLNcPKz4gEF0YZvFk3fdewEbpZbgJfJxzZX6bp+mnvMJ4b69c6o4amJKr+/7wC/9e1WB8yCO+nvZcrK0Lpxvryixucfn2mQMQodDgFd6o861Ogwlgpra8VZtz4Kv+4SIrvymOUikSvle7nycZxUZhzNdZlsRcC8LWIOKsOWSv8iOZKQ0HMKrK1GMX3TSfRrb/fUMjltTUVs1NDCM4Ea5rJuCA2YgtkeRpXLLKEzwxfjrZ5rvMSfOD5XBEVZl6OKKbZi5t6N83NBxJTW3V3MQwqv8tJlfielHIMDX8OM1VoD2SPA1HsHmWgZ9OthmdbclF4x36mqBvb1F6ejynWR15xXSvJ6k/+Z5PJgdw+Q0QbWgIM+sRq7bnxCFToHN1aKmkfT4bovx6L0DGWCCYmMrLQJbTsDbrWGYO5u2MWisaqlbghOUIp/U5XNljaFo3q2tcp3quPypcAxWde2G+R2+Soqxex1X8OS7Fe5y61j2q+kc4JDdt8zPVme3v9SXVMf7Hr/vPOe7r+HM6VBEGHI5x/P0qrtCBhI0Q2dOlYCM0gaYp/K/6IBFopi+BmugLNezf5bs4Tf6J9l2PAw0BNFCTAJmu8Tg7nMkTMbHNvghsqi81UQ/dVwU5iy3zc7EDWwPJpEoKlBopblFxTMoSY7w75BYkEEmg1kUqTcvs2x2hVbYYapGl0OmZpymkG/kZ1IeUGHDtLixA3l2hrIWu3NBW/TJEaq6eQmWxnsiwWUV76oa0LfRP+YQG+hKIf1+oU6D2WwMHwr4EFL8r1A2AbZzT96h6yqFPhycAh0OORtdmhYdnPIx9xdMp9N1w5WCLLImmweGESWhrKIKaGUp1X1G3kBI/oa4E/H66Qg37ISOvwH8oghoYSnUwk0+46tnWeKq9TXbVuYD6uy8BTdQVmvWAohWOQNEfUUsUvY8Jro37p440nRToCA9wIJWq1xhCnQgoTb6i4vkhOULas8VgowyFmbXpGMIyg9M0CpvTZLaN9+EA3yRpBU9k2ipGogm1zCRVfFwChQqWHZUxLtuK7+Ps/BgTSCuMw1Kp5KApjWXRCKFvnaUw0/z+hHbJY7IjWyAq2qfMBjJ6lTXgOsZ2gat/aN8eiZOjkhycK5U1nKQzlstGItM+fYih/RpdqOgtUm7Wzqe4SOJsiDV6lR8/J1ks6ReTSgq5lPU08v55jskvH7MEmnbYYkgGlsLNOuYm0Uy9zf/tvyO+olwgM0msccnr2QbuNTVLS26BkbaGbmHQv3wTFwV9Ebgg6Et15zUJKm6LBFzLU2XgWc1QrGlkcFoR2hiKoCbqUi336+94Os/i9OJcPdUnas3QJD1PUJNDUqhraKQjAbYkGxiqDGqXFJeR0SaG0MpO9ehCRUNmJ3yEWNaIkn9LYcL/9yI/n2SNtIWKlloKTUtt/GahkfZ3iH9bZLilYDPmSvcULJlqU8FSaqSA8/YKUsBkkBQwpaEUipbVrZl1o2R4ocqk3Wm0b6Gy1sKNNGXSRppiTSNgdkqhOZAKahgkNDhTkmyBhyLZOZLRZpdNfCdtRd5pLIXWpExWLMCYTDlsRoZEqx6fhAJQkSeB1eSpdMdAYmIE8TxIpAEPhkQy28ZlKygZoZEYZqsmaRhzoPcllDAOJMSW4hjIYSaA/nibDnosHnHTpeAxN01g3lQTQ1zVXEOhabIh0t5iQZpJNTLRBIjyCi9DWBrFaoQl1G5I2BCRwBaEJVCtHDlSXR8O8QjF3hvKwH4binXjI/OuVBwbmWJwXGQotLBMpRsBqgyGYmq4xP9YyBuhyqBGqGLdOgplCG+2VIO7SAKuqwQq3RYR80Bk//oZvCrlysGtIkuivWPKwVuA9nf4Tik3uGcYAqiJ01JfBF8odqUmovenHrAGfbFUEdNjE2nUI6FhKSV4cyYjtjglfkDHUwoPJTCZ7tR4oDS8DVNIIJKobseMW+6OKeUNCxSq007TZu9QTbaXX2PyBPDagqXRGbnIyxIzT+WtiiSgkQUqnZGbtY/0zpsrBw3MkmhPaMHAKMBRLUgHn9mCpOaCqJvXNqo/UmOCIIlHaEwxeGTGUGi/2OMpTg7QuDUUwV9oV6obmZoVkGpQ4inA8YgnMjj3fIeqChWa4VlGKDsThWi1JohLPI7+hZLDE9SnXDmsPkNi1uDrBIO7hNUWSRTNUlSalrmYNEKzXDnUJkeim/Wfs51i0qdLwTmfJtAedvOxLYADbp4EPtTmqYxalluVK5e3aWpV3qVP47GnJjfxGYxqUvO7X8ZxSu7qJaHT+2LdoRIv0YnPtoGrWaut3KNNoFB5zrVESN9wdzSt8JMQSVRn3NHF6ZQmaP+Qt/SJhRTy5YhAYSZDS66XoHNRMrjnNrzujgY6Y1D2LpZm3q3mXq5MpHzze10hhoVoE4FE5qzKUhlMvf2LUHC+7Utlk2xPYOChKG+KKZX5KZo2JX8pJsMaQKqAHEBtI5FODIO2hQapZxBq5/VocH2n6ijc2IcK/KsNKhcY54ePRZD72LM1Ff71DRcTf/kXrPqmpqHfGujtAlMHMwr0pmKwiPKZhL05hDmeHsfK6L41u2gZs4pyVWUvG4imutcKSk7Q2A5yVb5Q8G7Kh7g4oMrBlG1FuQGkCqsUXKYJ8TSm/B5ZghCfIP0SpVEZemniplrznCRqOMLK0SQjhRRqsE9h+nqSRy72KrLL86hbn6N2UZ8oxxPzynIl4QcfRFH1Mw4FF27jAjCTPUcJYkD5KGJeWTGSAMpK1FuKyejHMlHPFzAPSBjAFNADoMYEync99qq3e1CV0jyJf3W5/TSpJ3v+5a4iZzq5oiyh916aUPV+Y9kfNSgGTzmxftUArhiUikvOUhgGQQ0hHwTlxF76cQlmuEzSOjpyz1lhBI40nAmMMDRC6S5oyMBarrVAG/IT4F78Mgxkz3jtzdA91VQujUWiEMtj/rkpqSl9SuqwTGZeMkb9i0ZgtQxTKpbA7PPJZv0Lv8Bk64GvL5vqXJF39bvDWL36cAQQUQ1I/sWo3z8CVPS7QOO/x/mz+uazlj0GtldTdZSvmOVNqsmV4t9ZE6Vkb6jBmqBZxMKw5pHP/SbVPPX5wszTXcJYYYevFBI50NgjlIU0jA1q+Er+h9SZzAI8vQfXj+oKIXECxBhgmKiCBjhPthEbHUA61bJ0CmWAaAWNFoooBK64cldb+vI/osMVSK1hVF2rohhkgVZWHjdBzUsSMwFmrYt74MHArUn0qzlplWDLOtcO8GIWK4Ns0BT9UnZYHUgtIdKGWMtIgnrQhvC4kuFV0+NBIA2GholMwIQbUex6QDr/Ox8okAmprAxQ4tzzXLAVugkpBOR1PKsFoULKCCwPYib9RwKSB/tQ5jGJJDCN3jiaisHMpA42Q3MzjCDjcFemCJYjifQD3qQ5sFHcxOjtYmYQA65s4CEFb0kAId9oVTduC2Elt4lNtdAvQPQcVbo4yanDHbMK/rHMSStVGsQYCiclObVWKYk2gBrLMIkiYJXpYYExiwnOEQyCfdEWtQnXFcTE+kncpHawT3Rt5mQjoDnZlGEhNxAUrI2YQhWEba0mdoDp9Ng0sVZ4EymXk8Z1tcortTZbNy7SjCSqSyRt1tikaj6TGMWELxMZR8UcjnbjvhAyHCgnHw3D3gNE9/HxlKIhIqJccY5SsXTh4jI2yxZJzMUJVe5DMkas47qosoRS7xHk7oQvhJok1eVRJMeor5i7RCL/s1V4Vbu4mUqXKZFILrK7yxQf3ZPUHH705xlo+nRHXUEP8RHucrqPxPN7EommBv6jkz/BmctEquCtSn9Ls4oh0GQS0JZwswpMa2+6Pjyt0k4AVQijCHF0SVXqVz/qkhi4en05smAKMxF9B43hSL2OKkd0UF2JvjSNRmQmdO8gMhiSV6wtVvTbsy1LZbc2NKMFnac3yZdIs1V9sRSdR9En/m5JfF2DZwMgnVxw1zcDTFRpUgsOGu2qqGps4ihCDEvh1IOCXEeDe53cjw+uoNi9jfbjg0JqM1yUIbI9maa1u6FdGurR/boIc/Rhu+U24En8Ki6EHB/qedr0DPIrtrIikf+tbHhVu0Dv0S3enz3FJdr/lVRPVAui4roq3tRh6vKh7ElVaZh6d0Mw3+3AX24GuIInRcCa0i/eKEeAJ9N8pPvd2D5sLb/jwlzm6VMMaAZEiiTAgEinRxjqgZkP7FUEkhkol0JK+hALI0XyBsLEJBuDH7Novwt9Jb/omMc0XZ4I+TfBUfhVms+kcd+FifD0uoNOgxENfOVasoSeBAdrCvOrMmWH60aoC7mh3fLxhGva8zHJRzQDHkgZZqgDkqq0X7IqV8qIA26DzpbS+u/v0HEB+NwvmidLKnLtjsDLEyVZ1pv269dksvFgINWXIScO8XHMbgrFhlFK63/fOLUZ4FxBEZDJCBg5TOsq8CJLvtQAR5dPSc1L9BgxzIzkMt+wOYaimyypkjhVrCVVFXyvI+FMSu3Uo8mONN4Y3VpZbEpvF2ndQOoqOcGLcLP0Vg5RroakTpGQ4Em0nIpcMZXDCaiaKV2dVUrGR8bDs0lUUxdIF2LWmkZtnbrB1eTV86OWkA5Mf4EMUwe7R4byTwwXccpEEi6Od3TasehNkR9V9lCRhzAInGGtXdgoE6aNNsVDbmEIinjtZujTw0WK/ZtI5H/jJqS4a2pKs9e5jOwpEOUYHttBSn1fuzthCenw2jFeloTOJV5pTqJ1Jce4eL7+vnuKswO6w6YdUqgB2xJtJZVR2JxurUHgfG3sDoVOM9dsS8AsciONQP4w1p6lXpfakrxxyslQWyfE0KfJm0cYmSbB82cmxb5dV8X/9n1eE4EJ0qI3CTyQKqgVvgvOLvzKhHD3zQMIk/xuo8wSsRnWlEZhaeWKQVnfiD6qbG4y08gMEgwdTBumIKEreVVrIYhh8phFbOpc0UYKasV8AiT0baYVRaJehgOYr42wUKdhc3EGtzGHgjqkOcBEa813qMyf5mqOIQdc1LOWGQOg9a4IwEPMd0exUeSwc0tjQSeaiuoDo6s8K6siTupZBe9O+8VJF4r5IY/EBFXA/s4Xb/2ayCFuM7tskCTuatYPumRcHsxOJ+4wsCRFrlbKKkfIrCahUpCZAoXJWiZabSTH8KgDkrbdd4/JZCnSxhl2eKamNldPpxbe+BncDGpTeeZMe59JTQfsSsZxDI8nIDNfs3FRpNwbZ9ghUrzaXD2dWnjjSPQzqN3uIoWUgcazmqS+4oG2/7YmmEmlaoo7cn36RY/dxBRYGZ2taai6XN/1mM8JblrTunGdGLnm3TfayPq4Mxyh/9Mv/6q/etEwqQ2PexkVfdmrF01S1PYH/GeVF/EBvc/3KC3Jr69e3J1x7SNq/nqNyuQwsHiFeWaI5IkemHY0N9ljHfCdpNPkJOpIuuI+b3sV7+MqviiqpI5riYt3+Fsi6X6Jl0l9HvQZ7W+yD+fqdK6wyuj4OWUOnV+9ULf/6oUg86sm0F7pQwUsZoJVQB+yy3OS7nu538RpyW03ZSyusPV/R/j3pi/xp1mhw3PP6Y88M2TUmg9vUlG2x5/cAzqeUsys/JDdx1+Ri2wfS/QOHeLd822dxos4zciY6DuCNfur10l8KOJj2fIY6uM/MYb3x+//9f/ykBwdGq8IAA== + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/201601262000441_ImportFramework1.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201601262000441_ImportFramework1.Designer.cs new file mode 100644 index 0000000000..b8d27247a3 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201601262000441_ImportFramework1.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class ImportFramework1 : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(ImportFramework1)); + + string IMigrationMetadata.Id + { + get { return "201601262000441_ImportFramework1"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201601262000441_ImportFramework1.cs b/src/Libraries/SmartStore.Data/Migrations/201601262000441_ImportFramework1.cs new file mode 100644 index 0000000000..8c61a2784f --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201601262000441_ImportFramework1.cs @@ -0,0 +1,478 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using Setup; + + public partial class ImportFramework1 : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.ImportProfile", "UpdateOnly", c => c.Boolean(nullable: false)); + AddColumn("dbo.ImportProfile", "KeyFieldNames", c => c.String(maxLength: 1000)); + AddColumn("dbo.ImportProfile", "ResultInfo", c => c.String()); + } + + public override void Down() + { + DropColumn("dbo.ImportProfile", "ResultInfo"); + DropColumn("dbo.ImportProfile", "KeyFieldNames"); + DropColumn("dbo.ImportProfile", "UpdateOnly"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.DataExchange.Import.MultipleFilesSameFileTypeNote", + "For multiple import files please make sure that they are of the same file type and that the content follows the same pattern (e.g. same column headings).", + "Bei mehreren Importdateien ist darauf zu achten, dass diese vom selben Dateityp sind und deren Inhalt demselben Schema folgt (z.B. gleiche Spaltenberschriften)."); + + builder.AddOrUpdate("Admin.DataExchange.Import.ProfileEntitySelectNote", + "Please select an object that you want to import.", + "Whlen Sie bitte ein Objekt aus, das Sie importieren mchten."); + + builder.AddOrUpdate("Admin.DataExchange.Import.ProfileCreationNote", + "Please upload an import file, enter a meaningful name for the import profile and save.", + "Laden Sie bitte eine Importdatei hoch, legen Sie einen aussagekrftigen Namen fr das Importprofil fest und speichern Sie."); + + builder.AddOrUpdate("Admin.DataExchange.Import.AddAnotherFile", + "Add import file...", + "Importdatei hinzufgen..."); + + builder.AddOrUpdate("Admin.System.ScheduleTasks.RunNow.Progress.DataImportTask", + "The task is now running in the background. You will receive an email as soon as it is completed. The progress can be tracked in the import profile list.", + "Die Aufgabe wird jetzt im Hintergrund ausgefhrt. Sie erhalten eine E-Mail, sobald sie abgeschlossen ist. Den Fortschritt knnen Sie in der Importprofilliste verfolgen."); + + builder.AddOrUpdate("Admin.System.ScheduleTasks.RunNow.Progress.DataExportTask", + "The task is now running in the background. You will receive an email as soon as it is completed. The progress can be tracked in the export profile list.", + "Die Aufgabe wird jetzt im Hintergrund ausgefhrt. Sie erhalten eine E-Mail, sobald sie abgeschlossen ist. Den Fortschritt knnen Sie in der Exportprofilliste verfolgen."); + + builder.AddOrUpdate("Admin.DataExchange.Import.DefaultProfileNames", + "My product import;My category import;My customer import;My newsletter subscription import", + "Mein Produktimport;Mein Warengruppenimport;Mein Kundenimport;Mein Newsletter-Abonnement-Import"); + + builder.AddOrUpdate("Admin.DataExchange.Import.LastImportResult", + "Last import result", + "Letztes Importergebnis"); + + builder.AddOrUpdate("Admin.Common.TotalRows", "Total rows", "Zeilen insgesamt"); + builder.AddOrUpdate("Admin.Common.Skipped", "Skipped", "Ausgelassen"); + builder.AddOrUpdate("Admin.Common.NewRecords", "New records", "Neue Datenstze"); + builder.AddOrUpdate("Admin.Common.Updated", "Updated", "Aktualisiert"); + builder.AddOrUpdate("Admin.Common.Warnings", "Warnings", "Warnungen"); + builder.AddOrUpdate("Admin.Common.Errors", "Errors", "Fehler"); + builder.AddOrUpdate("Admin.Common.UnsupportedEntityType", "Unsupported entity type '{0}'", "Nicht untersttzter Entittstyp '{0}'"); + builder.AddOrUpdate("Admin.Common.DataExchange", "Data exchange", "Datenaustausch"); + + builder.AddOrUpdate("Admin.DataExchange.Import.CompletedEmail.Body", + "This is an automatic notification of store \"{0}\" about a recent data import. Summary:", + "Dies ist eine automatische Benachrichtung von Shop \"{0}\" ber einen erfolgten Datenimport. Zusammenfassung:"); + + builder.AddOrUpdate("Admin.DataExchange.Import.CompletedEmail.Subject", + "Import of \"{0}\" has been finished", + "Import von \"{0}\" ist abgeschlossen"); + + builder.AddOrUpdate("Admin.DataExchange.Import.ColumnMapping", + "Assignment of import fields", + "Zuordnung der Importfelder"); + + builder.AddOrUpdate("Admin.DataExchange.Import.SelectTargetProperty", + "Create new assignment here", + "Hier neue Zuordnung vornehmen"); + + builder.AddOrUpdate("Admin.DataExchange.Import.UpdateOnly", + "Only update", + "Nur aktualisieren", + "If this option is enabled, only existing data is updated but no new records are added.", + "Ist diese Option aktiviert, werden nur vorhandene Daten aktualisiert, aber keine neue Datenstze hinzugefgt."); + + builder.AddOrUpdate("Admin.DataExchange.Import.KeyFieldNames", + "Key fields", + "Schlsselfelder", + "Existing records can be identified for updates on the basis of key fields. The key fields are processed in the order how they are defined here.", + "Anhand von Schlsselfeldern knnen vorhandene Datenstze zwecks Aktualisierung identifiziert werden. Die Schlsselfelder werden in der hier festgelegten Reihenfolge verarbeitet."); + + builder.AddOrUpdate("Admin.DataExchange.Import.Validate.OneKeyFieldRequired", + "At least one key field is required.", + "Es ist mindestens ein Schlsselfeld erforderlich."); + + builder.AddOrUpdate("Admin.DataExchange.ColumnMapping.Validate.MultipleMappedIgnored", + "The following object properties were multiple assigned and thus ignored: {0}", + "Die folgenden Objekteigenschaft wurden mehrfach zugeodnet und deshalb ignoriert: {0}"); + + builder.AddOrUpdate("Admin.DataExchange.ColumnMapping.Validate.MappingsReset", + "The stored field assignments are invalid due to the change of the delimiter and were reset.", + "Die gespeicherten Feldzuordnungen sind aufgrund der nderung des Trennzeichens ungltig und wurden zurckgesetzt."); + + + builder.AddOrUpdate("Common.Download.NoDataAvailable", + "Download data is not available anymore.", + "Es sind keine Daten zum Herunterladen mehr verfgbar."); + + builder.AddOrUpdate("Common.Download.NotAvailable", + "Download is not available any more.", + "Der Download ist nicht mehr verfgbar."); + + builder.AddOrUpdate("Common.Download.SampleNotAvailable", + "Sample download is not available anymore.", + "Der Download einer Beispieldatei ist nicht mehr verfgbar."); + + builder.AddOrUpdate("Common.Download.HasNoSample", + "The product variant doesn't have a sample download.", + "Fr die Produktvariante ist der Download einer Beispieldatei nicht verfgbar."); + + builder.AddOrUpdate("Common.Download.NotAllowed", + "Downloads are not allowed.", + "Downloads sind nicht gestattet."); + + builder.AddOrUpdate("Common.Download.MaxNumberReached", + "You have reached the maximum number of downloads {0}.", + "Sie haben die maximale Anzahl an Downloads {0} erreicht."); + + builder.AddOrUpdate("Account.CustomerOrders.NotYourOrder", + "This is not your order.", + "Dieser Auftrag konnte Ihnen nicht zugeordnet werden."); + + builder.AddOrUpdate("Shipping.CouldNotLoadMethod", + "The shipping rate computation method could not be loaded.", + "Die Berechnungsmethode fr Versandkosten konnte nicht geladen werden."); + + builder.AddOrUpdate("Shipping.OneActiveMethodProviderRequired", + "At least one shipping rate computation method provider is required to be active.", + "Mindestens ein Provider zur Berechnung von Versandkosten muss aktiviert sein."); + + builder.AddOrUpdate("Payment.CouldNotLoadMethod", + "The payment method could not be loaded.", + "Die Zahlungsart konnte nicht geladen werden."); + + builder.AddOrUpdate("Payment.MethodNotAvailable", + "The payment method is not available.", + "Die Zahlungsart steht nicht zur Verfgung."); + + builder.AddOrUpdate("Payment.OneActiveMethodProviderRequired", + "At least one payment method provider is required to be active.", + "Mindestens ein Zahlungsart-Provider muss aktiviert sein."); + + builder.AddOrUpdate("Payment.RecurringPaymentNotSupported", + "Recurring payments are not supported by selected payment method.", + "Wiederkehrende Zahlungen sind fr die gewhlte Zahlungsart nicht mglich."); + + builder.AddOrUpdate("Payment.RecurringPaymentNotActive", + "Recurring payment is not active.", + "Wiederkehrende Zahlung ist inaktiv."); + + builder.AddOrUpdate("Payment.RecurringPaymentTypeUnknown", + "The recurring payment type is not supported.", + "Der Typ von wiederkehrender Zahlung wird nicht untersttzt."); + + builder.AddOrUpdate("Payment.CannotCalculateNextPaymentDate", + "The next payment date could not be calculated.", + "Das Datum der nchsten Zahlung kann nicht ermittelt werden."); + + builder.AddOrUpdate("Payment.PayingFailed", + "Unfortunately we can not handle this purchasing via your preferred payment method. Please select an alternate payment option to complete your order.", + "Leider knnen wir diesen Einkauf nicht ber die gewnschte Zahlungsart abwickeln. Bitte whlen Sie eine alternative Zahlungsoption aus, um Ihre Bestellung abzuschlieen."); + + builder.AddOrUpdate("Order.InitialOrderDoesNotExistForRecurringPayment", + "No initial order exists for the recurring payment.", + "Fr die wiederkehrende Zahlung existiert kein Ausgangsauftrag."); + + builder.AddOrUpdate("Order.CannotCalculateShippingTotal", + "The shipping total could not be calculated.", + "Die Versandkosten konnten nicht berechnet werden."); + + builder.AddOrUpdate("Order.CannotCalculateOrderTotal", + "The order total could not be calculated.", + "Die Auftragssumme konnte nicht berechnet werden."); + + builder.AddOrUpdate("Order.BillingAddressMissing", + "The billing address is missing.", + "Die Rechnungsanschrift fehlt."); + + builder.AddOrUpdate("Order.ShippingAddressMissing", + "The shipping address is missing.", + "Die Lieferanschrift fehlt."); + + builder.AddOrUpdate("Order.CountryNotAllowedForBilling", + "The country '{0}' is not allowed for billing.", + "Eine Rechnungslegung ist fr das Land '{0}' unzulssig."); + + builder.AddOrUpdate("Order.CountryNotAllowedForShipping", + "The country '{0}' is not allowed for shipping.", + "Ein Versand ist fr das Land '{0}' unzulssig."); + + builder.AddOrUpdate("Order.NoRecurringProducts", + "There are no recurring products.", + "Keine wiederkehrenden Produkte."); + + builder.AddOrUpdate("Order.NotFound", + "The order {0} was not found.", + "Der Auftrag {0} wurde nicht gefunden."); + + builder.AddOrUpdate("Order.CannotCancel", + "Cannot cancel order.", + "Der Auftrag kann nicht storniert werden."); + + builder.AddOrUpdate("Order.CannotMarkCompleted", + "Cannot mark order as completed.", + "Der Auftrag kann nicht als abgeschlossen markiert werden."); + + builder.AddOrUpdate("Order.CannotCapture", + "Cannot capture order.", + "Der Auftrag kann nicht gebucht werden."); + + builder.AddOrUpdate("Order.CannotMarkPaid", + "Cannot mark order as paid.", + "Der Auftrag kann nicht als bezahlt markiert werden."); + + builder.AddOrUpdate("Order.CannotRefund", + "Cannot do refund for order.", + "Eine Rckerstattung ist fr diesen Auftrag nicht mglich."); + + builder.AddOrUpdate("Order.CannotPartialRefund", + "Cannot do partial refund for order.", + "Eine Teilrckerstattung ist fr diesen Auftrag nicht mglich."); + + builder.AddOrUpdate("Order.CannotVoid", + "Cannot do void for order.", + "Eine Stornierung dieses Auftrages ist nicht mglich."); + + builder.AddOrUpdate("Shipment.AlreadyShipped", + "This shipment is already shipped.", + "Diese Sendung wird bereits ausgeliefert."); + + builder.AddOrUpdate("Shipment.AlreadyDelivered", + "This shipment is already delivered.", + "Diese Sendung wird bereits zugestellt."); + + builder.AddOrUpdate("Customer.DoesNotExist", + "The customer does not exist.", + "Der Kunde existiert nicht."); + + builder.AddOrUpdate("Checkout.AnonymousNotAllowed", + "An anonymous checkout is not allowed.", + "Ein anonymer Checkout ist nicht zulssig."); + + builder.AddOrUpdate("Common.Error.InvalidEmail", + "The email address is not valid.", + "Die E-Mail-Adresse ist ungltig."); + + builder.AddOrUpdate("Common.Error.NoActiveLanguage", + "No active language could be loaded.", + "Es wurde keine aktive Sprache gefunden."); + + builder.AddOrUpdate("Common.Error.NoEmailAccount", + "No email account could be loaded.", + "Es wurde kein E-Mail-Konto gefunden."); + + builder.AddOrUpdate("Admin.OrderNotice.RecurringPaymentCancellationError", + "Unable to cancel recurring payment for order {0}.", + "Es ist ein Fehler bei der Stornierung einer wiederkehrenden Zahlung fr Auftrag {0} aufgetreten."); + + builder.AddOrUpdate("Admin.OrderNotice.OrderRefundError", + "Unable to refund order {0}.", + "Es ist ein Fehler bei einer Rckerstattung zu Auftrag {0} aufgetreten."); + + builder.AddOrUpdate("Admin.OrderNotice.OrderPartiallyRefundError", + "Unable to partially refund order {0}.", + "Es ist ein Fehler bei einer Teilerstattung zu Auftrag {0} aufgetreten."); + + builder.AddOrUpdate("Admin.OrderNotice.OrderVoidError", + "Unable to void payment transaction of order {0}.", + "Es ist ein Fehler bei der Stornierung einer Zahlungstransaktion zu Auftrag {0} aufgetreten."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.SortFilterResultsByMatches", + "Sort filter results by number of matches", + "Filterergebnisse nach Trefferanzahl sortieren", + "Specifies to sort filter results by number of matches in descending order. If this option is deactivated then the result is sorted by the display order of the values.", + "Legt fest, das Filterergebnisse absteigend nach der Anzahl an bereinstimmungen sortiert werden. Ist diese Option deaktiviert, so wird in der fr die Werte festgelegten Reihenfolge sortiert."); + + builder.AddOrUpdate("Wishlist.IsDisabled", + "The wishlist is disabled.", + "Die Wunschliste ist deaktiviert."); + + builder.AddOrUpdate("ShoppingCart.IsDisabled", + "The shoping cart is disabled.", + "Der Warenkorb ist deaktiviert."); + + builder.AddOrUpdate("Products.NotFound", + "The product {0} was not found.", + "Das Produkt {0} wurde nicht gefunden."); + + builder.AddOrUpdate("Products.Variants.NotFound", + "The product variant {0} was not found.", + "Die Produktvariante {0} wurde nicht gefunden."); + + builder.AddOrUpdate("Reviews.NotFound", + "The product review {0} was not found.", + "Die Produktbewertung {0} wurde nicht gefunden."); + + builder.AddOrUpdate("Polls.AnswerNotFound", + "The poll answer {0} was not found.", + "Eine Umfrageantwort {0} wurde nicht gefunden."); + + builder.AddOrUpdate("Polls.NotAvailable", + "The poll is not available.", + "Die Umfrage ist nicht verfgbar."); + + builder.AddOrUpdate("Install.LanguageNotRegistered", + "The install language '{0}' is not registered.", + "Die Installationssprache '{0}' ist nicht registriert."); + + builder.AddOrUpdate("Admin.Catalog.Categories.DescriptionToggle", + "Show other description", + "Andere Beschreibung anzeigen"); + + builder.AddOrUpdate("Admin.Catalog.Categories.Fields.Description", + "Top description", + "Obere Beschreibung", + "Description of the category that is displayed above products on the category page.", + "Beschreibung der Warengruppe, die auf der Warengruppenseite oberhalb der Produkte angezeigt wird."); + + builder.AddOrUpdate("Common.CaptchaUnableToVerify", + "The API call to verify a CAPTCHA has failed.", + "Der API-Aufruf zur Prfung eines CAPTCHAs ist fehlgeschlagen."); + + builder.AddOrUpdate("Common.WrongCaptcha", + "Please confirm that you are not a \"robot\".", + "Bitte besttigen Sie, dass Sie kein \"Roboter\" sind."); + + builder.AddOrUpdate("DownloadableProducts.UserAgreementConfirmation", + "Yes, I agree to the user agreement for this product.", + "Ja, ich stimme der Nutzungsvereinbarung fr dieses Produkt zu."); + + builder.AddOrUpdate("DownloadableProducts.HasNoUserAgreement", + "The product has no user agreement.", + "Das Produkt besitzt keine Nutzungsvereinbarung."); + + builder.AddOrUpdate("Checkout.DownloadUserAgreement.PleaseAgree", + "Please agree to the user agreement for downloadable products.", + "Bitte stimmen Sie der Nutzungsvereinbarung fr herunterladbare Produkte zu."); + + + builder.AddOrUpdate("Admin.Configuration.Settings.ShoppingCart.OrderConfirmationPage", + "Order confirmation page", + "Bestellabschlussseite"); + + builder.AddOrUpdate("Admin.Configuration.Settings.ShoppingCart.ShowEsdRevocationWaiverBox", + "Show revocation waiver box for electronic services", + "Widerrufsverzichtbox fr elektronische Leistungen anzeigen", + "Specifies whether the customer must agree a revocation waiver for electronic services on the order confirmation page.", + "Legt fest, ob der Kunde auf der Bestellabschlussseite einem Widerrufsverzicht fr elektronische Leistungen zustimmen muss."); + + builder.AddOrUpdate("Admin.Configuration.Settings.ShoppingCart.ShowCommentBox", + "Show comment box", + "Kommentarbox anzeigen", + "Specifies whether comment box is displayed on the order confirmation page.", + "Legt fest, ob der Kunde auf der Bestellabschlussseite einen Kommentar zu seiner Bestellung hinterlegen kann."); + + builder.AddOrUpdate("Admin.Configuration.Settings.ShoppingCart.ShowConfirmOrderLegalHint", + "Show legal hints in order summary", + "Rechtliche Hinweise in der Warenkorbbersicht anzeigen", + "Specifies whether to show hints in order summary on the confirm order page. This text can be altered in the language resources.", + "Legt fest, ob rechtliche Hinweise in der Warenkorbbersicht auf der Bestellabschluseite angezeigt werden. Dieser Text kann in den Sprachresourcen gendert werden."); + + + builder.AddOrUpdate("Checkout.EsdRevocationWaiverConfirmation", + "Yes, I want access to the digital content immediately and know that my right of revocation expires with the access.", + "Ja, ich mchte sofort Zugang zu dem digitalen Inhalt und wei, dass mein Widerrufsrecht mit dem Zugang erlischt."); + + builder.AddOrUpdate("Checkout.EsdRevocationWaiverConfirmation.PleaseAgree", + "Please confirm that you would like access to the digital content immediately.", + "Bitte besttigen Sie, dass Sie sofort Zugang zu dem digitalen Inhalt wnschen."); + + + builder.AddOrUpdate("Admin.Configuration.Settings.DataExchange.MaxFileNameLength", + "Maximum length of file and folder names", + "Maximale Lnge von Datei- und Ordnernamen", + "Specifies the maximum length of file and folder names created during an import or export.", + "Legt die maximale Lnge von Datei- und Ordnernamen fest, die im Rahmen eines Imports\\Exports erzeugt wurden."); + + builder.AddOrUpdate("Admin.Configuration.Settings.DataExchange.ImageImportFolder", + "Image folder (relative path)", + "Bilderordner (relativer Pfad)", + "Specifies a relative path to a folder with images to be imported (e.g. Content\\Images).", + "Legt einen relativen Pfad zu einem Ordner mit zu importierenden Bildern fest (z.B. Inhalt\\Bilder)."); + + builder.AddOrUpdate("Admin.Configuration.Settings.DataExchange.ImageDownloadTimeout", + "Timeout for image download (minutes)", + "Zeitlimit fr Bilder-Download (Minuten)", + "Specifies the timeout for the image download in minutes.", + "Legt das Zeitlimit fr den Bilder-Download in Minuten fest."); + + builder.AddOrUpdate("Admin.System.Maintenance.SqlQuery.Succeeded", + "The SQL command was executed successfully.", + "Die SQL Anweisung wurde erfolgreich ausgefhrt."); + + builder.AddOrUpdate("Admin.DataExchange.Import.KeyFieldNames.Note", + "Please only use the ID field as a key field, if the data sourced from the same database to which it will be imported. Otherwise it is possible that the wrong records are updated.", + "Benutzen Sie das ID-Feld bitte nur dann als Schlsselfeld, wenn die Daten aus der derselben Datenbank stammen, in der sie importiert werden sollen. Ansonsten werden u.U. die falschen Datenstze aktualisiert."); + + builder.AddOrUpdate("Admin.Configuration.Settings.RewardPoints.RoundDownRewardPoints", + "Round down points", + "Punkte abrunden", + "Specifies whether to round down calculated points. Otherwise the bonus points will be rounded up.", + "Legt fest, ob bei der Punkteberechnung abgerundet werden soll. Ansonsten werden Bonuspunkte aufgerundet."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Order.GiftCards_Deactivated", + "Gift card deactivation order status", + "Geschenkgutschein wird deaktiviert, wenn Auftragsstatus..."); + + builder.AddOrUpdate("Admin.Configuration.Settings.ShoppingCart.NewsLetterSubscription", + "Subscribe to newsletters", + "Abonnieren von Newslettern", + "Specifies if customers can subscribe to newsletters when ordering and if the checkbox is enabled by default.", + "Legt fest, ob Kunden bei einer Bestellung Newsletter abonnieren knnen und ob die Checkbox standardmig aktiviert ist."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Orders.CheckoutNewsLetterSubscription.None", "Do not show", "Nicht anzeigen"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Orders.CheckoutNewsLetterSubscription.Deactivated", "Show deactivated", "Deaktiviert anzeigen"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Orders.CheckoutNewsLetterSubscription.Activated", "Show activated", "Aktiviert anzeigen"); + + builder.AddOrUpdate("Common.Options", "Options", "Optionen"); + + builder.AddOrUpdate("Checkout.SubscribeToNewsLetter", + "Subscribed to newsletter", + "Newsletter abonnieren"); + + builder.AddOrUpdate("Admin.OrderNotice.NewsLetterSubscriptionAdded", + "Subscribed to newsletter", + "Newsletter wurde abonniert"); + + builder.AddOrUpdate("Admin.OrderNotice.NewsLetterSubscriptionRemoved", + "Newsletter subscriber has been removed", + "Newsletter-Abonnent wurde entfernt"); + + builder.AddOrUpdate("Admin.Orders.Fields.CaptureTransactionID", + "Capture transaction ID", + "Transaktions-ID fr Buchung", + "Capture transaction identifier received from your payment gateway.", + "Vom Zahlungsanbieter erhaltene Transaktions-ID fr die Buchung."); + + builder.AddOrUpdate("Admin.Orders.Fields.AuthorizationTransactionID", + "Authorization transaction ID", + "Transaktions-ID fr Autorisierung", + "Authorization transaction identifier received from your payment gateway.", + "Vom Zahlungsanbieter erhaltene Transaktions-ID fr die Autorisierung."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.SearchDescriptions", + "Search product description", + "Produktbeschreibung durchsuchen", + "Specifies whether the product description should be included in the search.", + "Legt fest, ob die Produktbeschreibung in der Suche einbezogen werden soll."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.NumberOfPictures", + "Number of pictures", + "Anzahl der Bilder", + "Specifies the number of images per object to be exported.", + "Legt die Anzahl der zu exportierenden Bilder pro Objekt fest."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201601262000441_ImportFramework1.resx b/src/Libraries/SmartStore.Data/Migrations/201601262000441_ImportFramework1.resx new file mode 100644 index 0000000000..4e7843d120 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201601262000441_ImportFramework1.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/201603121451066_ThirdPartyEmailHandOver.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201603121451066_ThirdPartyEmailHandOver.Designer.cs new file mode 100644 index 0000000000..f7e948a531 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201603121451066_ThirdPartyEmailHandOver.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class ThirdPartyEmailHandOver : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(ThirdPartyEmailHandOver)); + + string IMigrationMetadata.Id + { + get { return "201603121451066_ThirdPartyEmailHandOver"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201603121451066_ThirdPartyEmailHandOver.cs b/src/Libraries/SmartStore.Data/Migrations/201603121451066_ThirdPartyEmailHandOver.cs new file mode 100644 index 0000000000..9b5970f35a --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201603121451066_ThirdPartyEmailHandOver.cs @@ -0,0 +1,235 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using SmartStore.Data.Setup; + using SmartStore.Core.Domain.Media; + using System.Linq; + + public partial class ThirdPartyEmailHandOver : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.Order", "AcceptThirdPartyEmailHandOver", c => c.Boolean(nullable: false)); + } + + public override void Down() + { + DropColumn("dbo.Order", "AcceptThirdPartyEmailHandOver"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + + // Some users have disabled the "TransientMediaClearTask" due to a bug. + // When the task is enabled again, it would delete media files, which are marked as transient + // but are permanent actually. To avoid this, we have to set IsTransient to false. + var transientPictures = context.Set().Where(x => x.IsTransient == true).ToList(); + transientPictures.Each(x => x.IsTransient = false); + + var transientDownloads = context.Set().Where(x => x.IsTransient == true).ToList(); + transientDownloads.Each(x => x.IsTransient = false); + + context.SaveChanges(); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.Common.Ignore", "Ignore", "Ignorieren"); + builder.AddOrUpdate("Common.My", "My", "Mein"); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Orders.CheckoutThirdPartyEmailHandOver.None", "Do not show", "Nicht anzeigen"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Orders.CheckoutThirdPartyEmailHandOver.Deactivated", "Show deactivated", "Deaktiviert anzeigen"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Orders.CheckoutThirdPartyEmailHandOver.Activated", "Show activated", "Aktiviert anzeigen"); + + builder.AddOrUpdate("Admin.Configuration.Settings.ShoppingCart.ThirdPartyEmailHandOver", + "Consent for email transfer to third parties", + "Zustimmung zur E-Mail Weitergabe an Dritte", + "Specifies whether customers can agree to a transferring of their email address to third parties when ordering, and whether the checkbox is enabled by default during checkout.", + "Legt fest, ob Kunden bei einer Bestellung der Weitergabe ihrer E-Mail Adresse an Dritte zustimmen knnen und ob die Checkbox dafr standardmig aktiviert ist."); + + builder.AddOrUpdate("Admin.Configuration.Settings.ShoppingCart.ThirdPartyEmailHandOverLabel", + "Text for email transfer consent", + "Text fr E-Mail Weitergabe", + "Specifies the text to be displayed to the customer. Please choose a specific reason, e.g. 'I agree to the transfer and storage of my email address for TrustedShops buyer protection.'", + "Legt den Text fr die Zustimmung zur Weitergabe der E-Mail Adresse an Dritte fest. Whlen Sie bitte einen konkreten Grund fr die Weitergabe, z.B. 'Mit der bermittlung und Speicherung meiner E-Mail-Adresse zur Abwicklung des Kuferschutzes durch Trusted Shops bin ich einverstanden.'"); + + builder.AddOrUpdate("Admin.Configuration.Settings.ShoppingCart.ThirdPartyEmailHandOverLabel.Default", + "I agree to the transfer and storage of my email address by third parties.", + "Mit der bermittlung und Speicherung meiner E-Mail-Adresse durch dritte Parteien bin ich einverstanden."); + + builder.AddOrUpdate("Admin.Orders.Fields.AcceptThirdPartyEmailHandOver", + "Accepts transfer of email", + "Akzeptiert Weitergabe der E-Mail", + "Indicates whether the customer has agreed to a transfer of his email address to third parties.", + "Gibt an, ob der Kunde bei der Bestellung einer Weitergabe seiner E-Mail Adresse an Dritte zugestimmt hat oder nicht."); + + builder.AddOrUpdate("Admin.OrderNotice.OrderCaptureError", + "Unable to capture payment for order {0}.", + "Es ist ein Fehler bei der Zahlungsbuchung zu Auftrag {0} aufgetreten."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportDescriptionMerging.None", + "None", "Keine"); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.ShowManufacturerPicturesInProductDetail", + "Show manufacturer pictures", + "Bilder von Herstellern anzeigen", + "Specifies whether to show manufacturer pictures on product detail page.", + "Legt fest, ob Herstellerbilder auf der Produktdetailseite angezeigt werden sollen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.HideManufacturerDefaultPictures", + "Hide default picture for manufacturers", + "Standardbild bei Herstellern ausblenden", + "Specifies whether to hide the default image for manufacturers. The default image is shown when no image is assigned to a manufacturer.", + "Legt fest, ob das Standardbild bei Herstellern ausgeblendet werden soll. Das Standardbild wird angezeigt, wenn dem Hersteller kein Bild zugeordnet ist."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Partition.Note", + "With the following settings you can partition the data to be exported. This includes
    • Skipping the first n records
    • The maximum number of records to be exported
    • The number of records per export file
    • Export data for each shop in a separate file
    By default, all data of a store will be exported into one file.", + "Mit den folgenden Einstellungen lassen sich die zu exportierenden Daten aufteilen. Dazu zhlt
    • Das berspringen der ersten n Datenstze
    • Die maximale Anzahl zu exportierender Datenstze
    • Die Anzahl der Datenstze pro Exportdatei
    • Daten von jedem Shop in eine separate Datei exportieren
    Standardmig werden alle Daten eines Shops in eine Datei exportiert."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Partition.Validate", + "Partitioning setting values must be greater than or equal to 0.", + "Einstellungen zur Aufteilung mssen grer oder gleich 0 sein."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.IsActiveSubscriber", + "Only active subscribers", + "Nur aktive Abonnenten", + "Filter by active or inactive newsletter subscribers.", + "Nach aktiven bzw. inaktiven Newsletter Abonnenten filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.IsActiveCustomer", + "Only active customers", + "Nur aktive Kunden", + "Filter by active or inactive customers.", + "Nach aktiven bzw. inaktiven Kunden filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.IsTaxExempt", + "Only tax exempt customers", + "Nur MwSt. befreite Kunden", + "Filter by tax exempt customers.", + "Nach MwSt. befreiten Kunden filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.BillingCountryIds", + "Billing countries", + "Rechnungslnder", + "Filter by billing countries.", + "Nach Rechnungslndern filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.ShippingCountryIds", + "Shipping countries", + "Versandlnder", + "Filter by shipping countries.", + "Nach Versandlndern filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.LastActivityFrom", + "Last activity from", + "Zuletzt aktiv von", + "Filter by date of last store activity.", + "Nach dem Datum der letzten Shop Aktivitt filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.LastActivityTo", + "Last active until", + "Zuletzt aktiv bis", + "Filter by date of last store activity.", + "Nach dem Datum der letzten Shop-Aktivitt filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.HasSpentAtLeastAmount", + "Has spent amount x", + "Hat Betrag x ausgegeben", + "Filter by spent amount.", + "Nach dem insgesamt ausgegebenen Betrag filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.HasPlacedAtLeastOrders", + "Has placed x orders", + "Hat x Bestellungen", + "Filter by number of placed orders.", + "Nach der Anzahl der gettigten Bestellungen filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.SystemProfileNote", + "The following list contains system profiles, which are provided by plugins such as the Data Export Plugin. You can customize system profiles as desired, but cannot create new ones. These profiles also add additional action buttons. You will find these above data lists, such as the product or order list.", + "Die folgende Liste enthlt Systemprofile, die von Plugins wie bspw. dem Datenexporte Plugin bereitgestellt werden. Sie knnen Systemprofile nach Belieben anpassen, aber keine Neuen erstellen. Fr diese Profile stehen auerdem zustzliche Aktions-Buttons zur Verfgung. Sie finden diese ber den entsprechenden Listen, wie z.B. der Produkt- oder Auftragsliste."); + + builder.AddOrUpdate("Admin.DataExchange.AddNewProfile", + "New profile", + "Neues Profil"); + + builder.AddOrUpdate("Admin.DataExchange.Import.ProfileCreationNote", + "Please select the import object and upload an import file.", + "Whlen Sie bitte das zu importierende Objekt und laden Sie eine Importdatei hoch."); + + builder.Delete("Admin.DataExchange.Import.ProfileEntitySelectNote"); + + builder.AddOrUpdate("Admin.Configuration.Restriction.SaveBeforeEdit", + "You need to save before you can specify restrictions.", + "Sie mssen zunchst speichern, bevor Sie Einschrnkungen festlegen knnen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CustomerNumberMethod", + "Customer numbers", + "Kundennummern", + "Specifies whether to assign customer numbers and whether they should be created automatically.", + "Legt fest, ob Kundennummern vergeben werden und ob diese automatisch vergeben werden sollen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CustomerNumberVisibility", + "Customer number presentation", + "Darstellung der Kundennummer", + "Specifies the presentation and handling of the customer number to the customer.", + "Legt die Darstellung und Handhabung der Kundennummer gegenber dem Kunden fest."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerNumberMethod.Disabled", "Disabled", "Deaktiviert"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerNumberMethod.Enabled", "Enabled", "Aktiviert"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerNumberMethod.AutomaticallySet", "Automatically assigned", "Automatisch vergeben"); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerNumberVisibility.None", "Do not display", "Nicht anzeigen"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerNumberVisibility.Display", "Display", "Anzeigen"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerNumberVisibility.EditableIfEmpty", "Editable if empty", "Editierbar falls leer"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Customers.CustomerNumberVisibility.Editable", "Always editable", "Immer editierbar"); + + builder.AddOrUpdate("Admin.Common.FileInUse", + "The file is in use and cannot be opened.", + "Die Datei ist in Benutzung und kann daher nicht geffnet werden."); + + + builder.AddOrUpdate("Admin.DataExchange.Export.UserProfilesTitle", + "User profiles", + "Benutzerprofile"); + builder.AddOrUpdate("Admin.DataExchange.Export.SystemProfilesTitle", + "System profiles", + "Systemprofile"); + + builder.AddOrUpdate("Admin.DataExchange.Export.CompletedEmailAddresses", + "Email addresses to", + "E-Mail Adressen an", + "Specifies the email addresses where to send the notification message.", + "Legt die E-Mail Adressen fest, an die die Benachrichtigung geschickt werden soll."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.EmailAddresses", + "Email addresses to", + "E-Mail Adressen an", + "Specifies the email addresses where to send the data.", + "Legt die E-Mail Adressen fest, an die die Daten verschickt werden soll."); + + builder.AddOrUpdate("Admin.Common.FileNotFound", "File not found", "Datei nicht gefunden"); + + builder.AddOrUpdate("Admin.Common.EnterEmailAdress", + "Please enter an email address.", + "Bitte geben Sie eine E-Mail-Adresse ein."); + + builder.AddOrUpdate("Admin.Configuration.EmailAccounts.TestingEmail", + "Testing email functionality.", + "Test der E-Mail-Funktion."); + + builder.AddOrUpdate("Admin.Common.EmailSuccessfullySent", + "The email has been successfully sent.", + "Die E-Mail wurde erfolgreich versendet."); + + builder.AddOrUpdate("Admin.Common.SkipAndTakeGreaterThanOrEqualZero", + "Values for skip and limit must be greater than or equal to 0.", + "Werte fr berspringen und Begrenzen mssen grer oder gleich 0 sein."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201603121451066_ThirdPartyEmailHandOver.resx b/src/Libraries/SmartStore.Data/Migrations/201603121451066_ThirdPartyEmailHandOver.resx new file mode 100644 index 0000000000..43d0084f04 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201603121451066_ThirdPartyEmailHandOver.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/201605020640016_GtinMpnIndex.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.Designer.cs new file mode 100644 index 0000000000..90e9ba6504 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class GtinMpnIndex : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(GtinMpnIndex)); + + string IMigrationMetadata.Id + { + get { return "201605020640016_GtinMpnIndex"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.cs b/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.cs new file mode 100644 index 0000000000..98f1936ecc --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.cs @@ -0,0 +1,19 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + + public partial class GtinMpnIndex : DbMigration + { + public override void Up() + { + CreateIndex("dbo.Product", "ManufacturerPartNumber"); + CreateIndex("dbo.Product", "Gtin"); + } + + public override void Down() + { + DropIndex("dbo.Product", new[] { "Gtin" }); + DropIndex("dbo.Product", new[] { "ManufacturerPartNumber" }); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.resx b/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.resx new file mode 100644 index 0000000000..15bcc90554 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605020640016_GtinMpnIndex.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/201605061916117_SwapColumnMappingValues.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.Designer.cs new file mode 100644 index 0000000000..1fa094db33 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class SwapColumnMappingValues : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(SwapColumnMappingValues)); + + string IMigrationMetadata.Id + { + get { return "201605061916117_SwapColumnMappingValues"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.cs b/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.cs new file mode 100644 index 0000000000..f16431d45b --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.cs @@ -0,0 +1,105 @@ +namespace SmartStore.Data.Migrations +{ + using System.Collections.Generic; + using System.Data.Entity.Migrations; + using System.Linq; + using Core.Domain; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + using Setup; + + public partial class SwapColumnMappingValues : 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); + + var importProfiles = context.Set().Where(x => x.ColumnMapping != null).ToList(); + + foreach (var profile in importProfiles) + { + var dic = new Dictionary>(); + var storeMapping = true; + + try + { + var json = JObject.Parse(profile.ColumnMapping); + + foreach (var kvp in json) + { + dynamic value = kvp.Value; + + var mappedName = (string)value.MappedName; + var property = (string)value.Property; + var defaultValue = (string)value.Default; + + if (mappedName.HasValue()) + { + // break migration because data is already migrated + storeMapping = false; + break; + } + else if (property.HasValue()) + { + if (!kvp.Key.IsCaseInsensitiveEqual(property) || defaultValue.HasValue()) + { + // swap value + dic.Add(property, new Dictionary + { + { "MappedName", kvp.Key }, + { "Default", defaultValue } + }); + } + else + { + // ignore because persisting not required anymore + } + } + else + { + // explicitly ignored property + dic.Add(property, new Dictionary + { + { "MappedName", property }, + { "Default", "[IGNOREPROPERTY]" } + }); + } + } + } + catch + { + storeMapping = true; + dic.Clear(); + } + + if (storeMapping) + { + if (dic.Any()) + profile.ColumnMapping = JsonConvert.SerializeObject(dic); + else + profile.ColumnMapping = null; + } + } + + context.SaveChanges(); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.Delete("Admin.DataExchange.ColumnMapping.Validate.MultipleMappedIgnored"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.resx b/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.resx new file mode 100644 index 0000000000..15bcc90554 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605061916117_SwapColumnMappingValues.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/201605111140288_ImportExtraData.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.Designer.cs new file mode 100644 index 0000000000..6cac518be6 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class ImportExtraData : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(ImportExtraData)); + + string IMigrationMetadata.Id + { + get { return "201605111140288_ImportExtraData"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.cs b/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.cs new file mode 100644 index 0000000000..191ace97ba --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.cs @@ -0,0 +1,72 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using Setup; + + public partial class ImportExtraData : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.ImportProfile", "ExtraData", c => c.String()); + } + + public override void Down() + { + DropColumn("dbo.ImportProfile", "ExtraData"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + + context.MigrateSettings(x => + { + x.Add("MediaSettings.VariantValueThumbPictureSize", "20"); + x.Delete("MediaSettings.AutoCompleteSearchThumbPictureSize"); + }); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.DataExchange.Import.NumberOfPictures", + "Number of pictures", + "Anzahl der Bilder", + "Specifies the number of images per object to be imported.", + "Legt die Anzahl der zu importierenden Bilder pro Objekt fest."); + + builder.Update("Admin.Configuration.Settings.Catalog.DefaultPageSizeOptions") + .Value("en", "Number of displayed products per page"); + + builder.AddOrUpdate("Admin.Validation.ValueGreaterZero", + "The value must be greater than 0.", + "Der Wert muss grer 0 sein."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Order.OrderListPageSize", + "Number of displayed orders per page", + "Anzahl der Auftrge pro Seite", + "Specifies the number of displayed orders per page.", + "Legt die Anzahl der dargestellten Auftrge pro Seite fest."); + + builder.AddOrUpdate("Admin.System.ScheduleTasks.RunningError", + "Error while running scheduled task \"{0}\"", + "Fehler beim Ausfhren der Aufgabe \"{0}\""); + + builder.AddOrUpdate("Admin.System.ScheduleTasks.Cancellation", + "The scheduled task \"{0}\" has been canceled", + "Die geplante Aufgabe \"{0}\" wurde abgebrochen"); + + builder.AddOrUpdate("Admin.Common.HttpStatus", + "HTTP status {0} ({1}).", + "HTTP-Status {0} ({1})."); + + builder.AddOrUpdate("Admin.System.Warnings.SitemapReachable.MethodNotAllowed", + "The reachability of the sitemap could not be validated.", + "Die Erreichbarkeit der Sitemap konnte nicht berprft werden."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.resx b/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.resx new file mode 100644 index 0000000000..738509bbb4 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605111140288_ImportExtraData.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/201605191216116_CheckoutAttributeMultiStore.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.Designer.cs new file mode 100644 index 0000000000..d4625babee --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class CheckoutAttributeMultiStore : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(CheckoutAttributeMultiStore)); + + string IMigrationMetadata.Id + { + get { return "201605191216116_CheckoutAttributeMultiStore"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.cs b/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.cs new file mode 100644 index 0000000000..1b6ba5a31f --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.cs @@ -0,0 +1,17 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + + public partial class CheckoutAttributeMultiStore : DbMigration + { + public override void Up() + { + AddColumn("dbo.CheckoutAttribute", "LimitedToStores", c => c.Boolean(nullable: false)); + } + + public override void Down() + { + DropColumn("dbo.CheckoutAttribute", "LimitedToStores"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.resx b/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.resx new file mode 100644 index 0000000000..d743254b0e --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605191216116_CheckoutAttributeMultiStore.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/201605201911421_ExportRevision.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.Designer.cs new file mode 100644 index 0000000000..e3a2d0014f --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class ExportRevision : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(ExportRevision)); + + string IMigrationMetadata.Id + { + get { return "201605201911421_ExportRevision"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.cs b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.cs new file mode 100644 index 0000000000..d168932260 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.cs @@ -0,0 +1,204 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using System.Linq; + using Core.Domain; + using Core.Domain.DataExchange; + using Setup; + using Utilities; + + public partial class ExportRevision : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.ExportDeployment", "ResultInfo", c => c.String()); + AddColumn("dbo.ExportDeployment", "SubFolder", c => c.String(maxLength: 400)); + AlterColumn("dbo.ExportProfile", "FolderName", c => c.String(nullable: false, maxLength: 400)); + DropColumn("dbo.ExportDeployment", "CreateZip"); + } + + public override void Down() + { + AddColumn("dbo.ExportDeployment", "CreateZip", c => c.Boolean(nullable: false)); + AlterColumn("dbo.ExportProfile", "FolderName", c => c.String(nullable: false, maxLength: 100)); + DropColumn("dbo.ExportDeployment", "SubFolder"); + DropColumn("dbo.ExportDeployment", "ResultInfo"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + + // migrate folder name to folder path + var rootPath = "~/App_Data/ExportProfiles/"; + var exportProfiles = context.Set().ToList(); + + foreach (var profile in exportProfiles) + { + if (!profile.FolderName.EmptyNull().StartsWith(rootPath)) + { + profile.FolderName = rootPath + profile.FolderName; + } + } + + context.SaveChanges(); + + // migrate public file system deployment to new public deployment + if (context.ColumnExists("ExportDeployment", "IsPublic")) + { + var fileSystemDeploymentTypeId = (int)ExportDeploymentType.FileSystem; + var publicFolderDeploymentTypeId = (int)ExportDeploymentType.PublicFolder; + + context.ExecuteSqlCommand("Update [ExportDeployment] Set DeploymentTypeId = {0} Where DeploymentTypeId = {1} And IsPublic = 1", + true, null, publicFolderDeploymentTypeId, fileSystemDeploymentTypeId); + + context.ColumnDelete("ExportDeployment", "IsPublic"); + } + + var oldFileManagerPath = CommonHelper.MapPath("~/Content/filemanager"); + FileSystemHelper.ClearDirectory(oldFileManagerPath, true); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.DataExchange.Export.FolderName", + "Folder path", + "Ordnerpfad", + "Specifies the relative path of the folder where to export the data.", + "Legt den relativen Pfad des Ordners fest, in den die Daten exportiert werden."); + + builder.AddOrUpdate("Admin.DataExchange.Export.FileNamePattern.Validate", + "Please enter a valid pattern for file names. Example for file names: %Store.Id%-%Profile.Id%-%File.Index%-%Profile.SeoName%", + "Bitte ein gltiges Muster fr Dateinamen eingeben. Beispiel: %Store.Id%-%Profile.Id%-%File.Index%-%Profile.SeoName%"); + + builder.AddOrUpdate("Admin.DataExchange.Export.FolderName.Validate", + "Please enter a valid, relative folder path for the export data.", + "Bitte einen gltigen, relativen Ordnerpfad fr die zu exportierenden Daten eingeben."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportDeploymentType.Http", "HTTP POST", "HTTP POST"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportDeploymentType.PublicFolder", "Public folder", "ffentlicher Ordner"); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.SubFolder", + "Name of subfolder", + "Name des Unterordners", + "Specifies the name of a subfolder where to publish the data.", + "Legt den Namen eines Unterordners fest, in den die Daten verffentlicht werden sollen."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.ZipUsageNote", + "If there are a large number of export files, it is recommended to use the option Create ZIP archive. This saves time and avoids problems, such as a full email mailbox.", + "Bei einer groen Anzahl an Exportdateien wird empfohlen die Option ZIP-Archiv erstellen zu benutzen. Das spart Zeit und vermeidet Probleme, wie z.B. ein volles E-Mail Postfach."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Cleanup", + "Clean up after successful deployment", + "Nach erfolgreicher Verffentlichung aufrumen", + "Specifies whether to delete unneeded files after all deployments succeeded.", + "Legt fest, ob nicht mehr bentigte Dateien gelscht werden sollen, nachdem alle Verffentlichungen erfolgreich waren."); + + builder.AddOrUpdate("Admin.Common.FtpStatus", + "FTP status {0} ({1}).", + "FTP-Status {0} ({1})."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.CopyFileFailed", + "At least one file could not be copied.", + "Mindestens eine Datei konnte nicht kopiert werden."); + + builder.AddOrUpdate("Admin.Common.LastRun", "Last run", "Letzte Ausfhrung"); + builder.AddOrUpdate("Admin.Common.SuccessfulOn", "Successful on", "Erfolgreich am"); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.Name", + "Name of profile", + "Name des Profils", + "Specifies the name of the publishing profile.", + "Legt den Namen des Verffentlichungsprofils fest."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.ProfilesTitle", + "Publishing profiles", + "Verffentlichungsprofile"); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.NoProfiles", + "There are no publishing profiles.", + "Es liegen keine Verffentlichungsprofile vor."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.Note", + "Click New profile to add one or multiple publishing profiles to specify how to further proceed with the export files.", + "Legen Sie ber Neues Profil ein oder mehrere Verffentlichungsprofile an, um festzulegen wie mit den Exportdateien weiter zu verfahren ist."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.PublishingTarget", + "Publishing target", + "Verffentlichungsziel"); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.DeploymentType", + "Publishing type", + "Art der Verffentlichung", + "Specifies the type of publishing.", + "Legt die Art Verffentlichung fest."); + + builder.AddOrUpdate("Common.Publishing", + "Publishing", + "Verffentlichung"); + + builder.AddOrUpdate("Common.NoFilesAvailable", + "There are no files available.", + "Es sind keine Dateien vorhanden."); + + builder.AddOrUpdate("Common.CopyToClipboard", "Copy to clipboard", "In die Zwischenablage kopieren"); + builder.AddOrUpdate("Common.CopyToClipboard.Succeeded", "Copied!", "Kopiert!"); + builder.AddOrUpdate("Common.CopyToClipboard.Failded", "Failed to copy.", "Kopieren ist fehlgeschlagen."); + + builder.AddOrUpdate("Products.NoBundledItems", + "No bundle items available", + "Keine Bundle-Bestandteile vorhanden"); + + builder.AddOrUpdate("Common.NoFileUploaded", + "There was no file uploaded.", + "Es wurde keine Datei hochgeladen."); + + builder.AddOrUpdate("Products.BasePriceInfo", + "Content: {0} {1} ({2} / {3} {1})", + "Inhalt: {0} {1} ({2} / {3} {1})"); + + builder.AddOrUpdate("Products.BasePriceInfo.LanguageInsensitive", + "{0} {1} ({2} / {3} {1})", + "{0} {1} ({2} / {3} {1})"); + + builder.AddOrUpdate("PrivateMessages.Disabled", + "Private messages are disabled.", + "Private Nachrichten sind deaktiviert."); + + builder.AddOrUpdate("Common.MethodNotSupportedForGuests", + "This function is not available for guests.", + "Diese Funktion steht fr Gste nicht zur Verfgung."); + + builder.AddOrUpdate("ContactUs.PrivacyAgreement", + "Privacy consent", + "Einwilligungserklrung Datenschutz"); + + builder.AddOrUpdate("ContactUs.PrivacyAgreement.MustBeAccepted", + "Please agree to the storage of your data.", + "Bitte stimmen Sie der Speicherung Ihrer Daten zu."); + + builder.AddOrUpdate("ContactUs.PrivacyAgreement.DetailText", + "Yes I've read the privacy terms and agree that my data given by me can be stored electronically. My data will thereby only be used to process my inquiry.", + "Ja, ich habe die Datenschutzerklrung zur Kenntnis genommen und bin damit einverstanden, dass die von mir angegebenen Daten elektronisch erhoben und gespeichert werden. Meine Daten werden dabei nur zur Bearbeitung meiner Anfrage genutzt."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.DisplayPrivacyAgreementOnContactUs", + "Get privacy consent for contact requests", + "Einwilligungserklrung im Kontaktformular fordern", + "Specifies whether a checkbox will be displayed on the contact page which requests the user to agree on storage of his data.", + "Bestimmt ob im Kontaktformular eine Checkbox angezeigt wird, die den Benutzer auffordert der Speicherung seiner Daten zuzustimmen."); + + + builder.Delete( + "Admin.DataExchange.Export.FolderAndFileName.Validate", + "Admin.DataExchange.Export.Deployment.IsPublic", + "Admin.DataExchange.Export.Deployment.CreateZip", + "Admin.Common.TemporaryFiles", + "Admin.Common.NoTempFilesFound"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.resx b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.resx new file mode 100644 index 0000000000..a653f94065 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201605201911421_ExportRevision.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/201607111548571_FixExportDeploymentIsPublic.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.Designer.cs new file mode 100644 index 0000000000..16b788408f --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class FixExportDeploymentIsPublic : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(FixExportDeploymentIsPublic)); + + string IMigrationMetadata.Id + { + get { return "201607111548571_FixExportDeploymentIsPublic"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.cs b/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.cs new file mode 100644 index 0000000000..f48207c9fc --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.cs @@ -0,0 +1,21 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using System.Web.Hosting; + using Core.Data; + + public partial class FixExportDeploymentIsPublic : DbMigration + { + public override void Up() + { + if (HostingEnvironment.IsHosted && DataSettings.Current.IsSqlServer) + { + Sql("IF EXISTS(SELECT TOP 1 1 FROM sys.objects o INNER JOIN sys.columns c ON o.object_id = c.object_id WHERE o.name = 'ExportDeployment' AND c.name = 'IsPublic') ALTER TABLE [dbo].[ExportDeployment] DROP COLUMN [IsPublic];"); + } + } + + public override void Down() + { + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.resx b/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.resx new file mode 100644 index 0000000000..55e598ca59 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201607111548571_FixExportDeploymentIsPublic.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/ObjectContextBase.cs b/src/Libraries/SmartStore.Data/ObjectContextBase.cs index 5fff7f7588..579994d654 100644 --- a/src/Libraries/SmartStore.Data/ObjectContextBase.cs +++ b/src/Libraries/SmartStore.Data/ObjectContextBase.cs @@ -2,27 +2,26 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; -using System.Data.SqlClient; using System.Data.Entity; using System.Data.Entity.Infrastructure; using System.Data.Entity.Validation; +using System.Data.SqlClient; using System.Linq; -using System.Linq.Expressions; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Smo; using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Data.Hooks; -using Microsoft.SqlServer.Management.Common; -using Microsoft.SqlServer.Management.Smo; using SmartStore.Core.Events; namespace SmartStore.Data { - /// - /// Object context - /// + /// + /// Object context + /// [DbConfigurationType(typeof(SmartDbConfiguration))] public abstract class ObjectContextBase : DbContext, IDbContext { @@ -192,7 +191,7 @@ private IEnumerable ToParameters(params object[] parameters) { for (int i = 0; i < result.Count; i++) { - result[i] = AttachEntity(result[i]); + result[i] = Attach(result[i]); } } } @@ -225,7 +224,7 @@ private IEnumerable ToParameters(params object[] parameters) { for (int i = 0; i < result.Count; i++) { - result[i] = AttachEntity(result[i]); + result[i] = Attach(result[i]); } } // close up the reader, we're done saving results @@ -333,12 +332,19 @@ public IDictionary GetModifiedProperties(BaseEntity entity) var props = new Dictionary(); var entry = this.Entry(entity); - var modifiedPropertyNames = from p in entry.CurrentValues.PropertyNames - where entry.Property(p).IsModified - select p; - foreach (var name in modifiedPropertyNames) + + // be aware of the entity state. you cannot get modified properties for detached entities. + if (entry.State != System.Data.Entity.EntityState.Detached) { - props.Add(name, entry.Property(name).OriginalValue); + var modifiedProperties = from p in entry.CurrentValues.PropertyNames + let prop = entry.Property(p) + where prop.IsModified + select prop; + + foreach (var prop in modifiedProperties) + { + props.Add(prop.Name, prop.OriginalValue); + } } return props; @@ -381,7 +387,7 @@ public override Task SaveChangesAsync() return result; } - // codehint: sm-add (required for UoW implementation) + // required for UoW implementation public string Alias { get; internal set; } // performance on bulk inserts @@ -490,32 +496,21 @@ protected internal bool IsSqlServer2012OrHigher() return s_isSqlServer2012OrHigher.Value; } - /// - /// Attach an entity to the context or return an already attached entity (if it was already attached) - /// - /// TEntity - /// Entity - /// Attached entity - protected virtual TEntity AttachEntity(TEntity entity) where TEntity : BaseEntity, new() + public TEntity Attach(TEntity entity) where TEntity : BaseEntity { - // little hack here until Entity Framework really supports stored procedures - // otherwise, navigation properties of loaded entities are not loaded until an entity is attached to the context var dbSet = Set(); - var alreadyAttached = dbSet.Local.Where(x => x.Id == entity.Id).FirstOrDefault(); + var alreadyAttached = dbSet.Local.FirstOrDefault(x => x.Id == entity.Id); + if (alreadyAttached == null) { - // attach new entity dbSet.Attach(entity); return entity; } - else - { - // entity is already loaded. - return alreadyAttached; - } - } - public bool IsAttached(TEntity entity) where TEntity : BaseEntity + return alreadyAttached; + } + + public bool IsAttached(TEntity entity) where TEntity : BaseEntity { if (entity != null) { @@ -555,6 +550,7 @@ public int DetachEntities(bool unchangedEntitiesOnly = true) where TEnt public void ChangeState(TEntity entity, System.Data.Entity.EntityState newState) where TEntity : BaseEntity { + Console.WriteLine("ChangeState ORIGINAL"); this.Entry(entity).State = newState; } diff --git a/src/Libraries/SmartStore.Data/Setup/Builder/ActivityLogTypeMigrator.cs b/src/Libraries/SmartStore.Data/Setup/Builder/ActivityLogTypeMigrator.cs new file mode 100644 index 0000000000..b397af928a --- /dev/null +++ b/src/Libraries/SmartStore.Data/Setup/Builder/ActivityLogTypeMigrator.cs @@ -0,0 +1,68 @@ +using System.Data.Entity; +using System.Linq; +using SmartStore.Core.Domain.Configuration; +using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Logging; + +namespace SmartStore.Data.Setup +{ + internal class ActivityLogTypeMigrator + { + private readonly SmartObjectContext _ctx; + private readonly DbSet _activityLogTypeRecords; + + public ActivityLogTypeMigrator(SmartObjectContext ctx) + { + Guard.ArgumentNotNull(() => ctx); + + _ctx = ctx; + _activityLogTypeRecords = _ctx.Set(); + } + + private Language GetDefaultAdminLanguage(DbSet settingRecords, DbSet languageRecords) + { + const string settingKey = "LocalizationSettings.DefaultAdminLanguageId"; + + var defaultAdminLanguageSetting = settingRecords.FirstOrDefault(x => x.Name == settingKey && x.StoreId == 0); + + if (defaultAdminLanguageSetting == null) + defaultAdminLanguageSetting = settingRecords.FirstOrDefault(x => x.Name == settingKey); + + if (defaultAdminLanguageSetting != null) + { + var defaultAdminLanguageId = defaultAdminLanguageSetting.Value.ToInt(); + if (defaultAdminLanguageId != 0) + { + var language = languageRecords.FirstOrDefault(x => x.Id == defaultAdminLanguageId); + if (language != null) + return language; + } + } + + return languageRecords.First(); + } + + public void AddActivityLogType(string systemKeyword, string enName, string deName) + { + Guard.ArgumentNotEmpty(() => systemKeyword); + Guard.ArgumentNotEmpty(() => enName); + Guard.ArgumentNotEmpty(() => deName); + + var record = _activityLogTypeRecords.FirstOrDefault(x => x.SystemKeyword == systemKeyword); + + if (record == null) + { + var language = GetDefaultAdminLanguage(_ctx.Set(), _ctx.Set()); + + _activityLogTypeRecords.Add(new ActivityLogType + { + Enabled = true, + SystemKeyword = systemKeyword, + Name = (language.UniqueSeoCode.IsCaseInsensitiveEqual("de") ? deName : enName) + }); + + _ctx.SaveChanges(); + } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesMigrator.cs b/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesMigrator.cs index 9a1d698396..92d059bec8 100644 --- a/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesMigrator.cs +++ b/src/Libraries/SmartStore.Data/Setup/Builder/LocaleResourcesMigrator.cs @@ -35,7 +35,7 @@ public void Migrate(IEnumerable entries, bool updateTouched using (var scope = new DbContextScope(_ctx, autoDetectChanges: false)) { - var langMap = _languages.ToDictionary(x => x.UniqueSeoCode.EmptyNull().ToLower()); + var langMap = _languages.ToDictionarySafe(x => x.UniqueSeoCode.EmptyNull().ToLower()); var toDelete = new List(); var toUpdate = new List(); diff --git a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs index baf11a8ca0..c5c2745302 100644 --- a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs +++ b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs @@ -3983,7 +3983,7 @@ public IList MessageTemplates() { Name = "OrderCancelled.CustomerNotification", Subject = "%Store.Name%. Your order cancelled", - Body = templateHeader + "

    %Store.Name%



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

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



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



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

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

    %Order.Product(s)%

    " + templateFooter, + Body = templateHeader + "

    %Store.Name%



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

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



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



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

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

    %Order.Product(s)%

    " + templateFooter, IsActive = true, EmailAccountId = eaGeneral.Id, }, @@ -3991,7 +3991,7 @@ public IList MessageTemplates() { Name = "OrderCompleted.CustomerNotification", Subject = "%Store.Name%. Your order completed", - Body = templateHeader + "

    %Store.Name%



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

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



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



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

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

    %Order.Product(s)%

    " + templateFooter, + Body = templateHeader + "

    %Store.Name%



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

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



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



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

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

    %Order.Product(s)%

    " + templateFooter, IsActive = true, EmailAccountId = eaGeneral.Id, }, @@ -3999,7 +3999,7 @@ public IList MessageTemplates() { Name = "ShipmentDelivered.CustomerNotification", Subject = "Your order from %Store.Name% has been delivered.", - Body = templateHeader + "

    %Store.Name%



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



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



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

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

    Delivered Products:

    %Shipment.Product(s)%

    " + templateFooter, + Body = templateHeader + "

    %Store.Name%



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



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



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

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

    Delivered Products:

    %Shipment.Product(s)%

    " + templateFooter, IsActive = true, EmailAccountId = eaGeneral.Id, }, @@ -4008,7 +4008,7 @@ public IList MessageTemplates() { Name = "OrderPlaced.CustomerNotification", Subject = "Order receipt from %Store.Name%.", - Body = templateHeader + "

    %Store.Name%



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

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



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



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

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

    %Order.Product(s)%

    " + templateFooter, + Body = templateHeader + "

    %Store.Name%



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

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



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



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

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

    %Order.Product(s)%

    " + templateFooter, IsActive = true, EmailAccountId = eaGeneral.Id, }, @@ -4016,7 +4016,7 @@ public IList MessageTemplates() { Name = "OrderPlaced.StoreOwnerNotification", Subject = "%Store.Name%. Purchase Receipt for Order #%Order.OrderNumber%", - Body = templateHeader + "

    %Store.Name%



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

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



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



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

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

    %Order.Product(s)%

    " + templateFooter, + Body = templateHeader + "

    %Store.Name%



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

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



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



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

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

    %Order.Product(s)%

    " + templateFooter, IsActive = true, EmailAccountId = eaGeneral.Id, }, @@ -4024,7 +4024,7 @@ public IList MessageTemplates() { Name = "ShipmentSent.CustomerNotification", Subject = "Your order from %Store.Name% has been shipped.", - Body = templateHeader + "

    %Store.Name%



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



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



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

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

    Shipped Products:

    %Shipment.Product(s)%

    " + templateFooter, + Body = templateHeader + "

    %Store.Name%



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



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



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

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

    Shipped Products:

    %Shipment.Product(s)%

    " + templateFooter, IsActive = true, EmailAccountId = eaGeneral.Id, }, @@ -4048,7 +4048,7 @@ public IList MessageTemplates() { Name = "ReturnRequestStatusChanged.CustomerNotification", Subject = "%Store.Name%. Return request status was changed.", - Body = templateHeader + "

    %Store.Name%



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

    " + templateFooter, + Body = templateHeader + "

    %Store.Name%



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

    " + templateFooter, IsActive = true, EmailAccountId = eaGeneral.Id, }, diff --git a/src/Libraries/SmartStore.Data/SmartObjectContext.cs b/src/Libraries/SmartStore.Data/SmartObjectContext.cs index 0d5356688c..bbe46338db 100644 --- a/src/Libraries/SmartStore.Data/SmartObjectContext.cs +++ b/src/Libraries/SmartStore.Data/SmartObjectContext.cs @@ -12,14 +12,12 @@ using SmartStore.Data.Setup; namespace SmartStore.Data -{ - +{ /// /// Object context /// public class SmartObjectContext : ObjectContextBase { - static SmartObjectContext() { var initializer = new MigrateDatabaseInitializer diff --git a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj index 8764a6a9ed..ff7fd39e2b 100644 --- a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj +++ b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj @@ -62,8 +62,9 @@ MinimumRecommendedRules.ruleset - - ..\..\packages\Autofac.3.4.1\lib\net40\Autofac.dll + + ..\..\packages\Autofac.3.5.2\lib\net40\Autofac.dll + True ..\..\packages\EntityFramework.Cache.1.0.0\lib\net45\EFCache.dll @@ -92,9 +93,9 @@ True ..\..\packages\Microsoft.SqlServer.Scripting.11.0.2100.61\lib\Microsoft.SqlServer.Smo.dll - - False - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True @@ -120,6 +121,7 @@ + @@ -366,6 +368,47 @@ 201509150931528_ExportFramework2.cs + + + 201511271019577_ExportFramework3.cs + + + + 201512151526290_ImportFramework.cs + + + + 201601262000441_ImportFramework1.cs + + + + 201603121451066_ThirdPartyEmailHandOver.cs + + + + 201605020640016_GtinMpnIndex.cs + + + + 201605061916117_SwapColumnMappingValues.cs + + + + 201605111140288_ImportExtraData.cs + + + + 201605191216116_CheckoutAttributeMultiStore.cs + + + + 201605201911421_ExportRevision.cs + + + + 201607111548571_FixExportDeploymentIsPublic.cs + + @@ -375,7 +418,6 @@ - @@ -687,6 +729,36 @@ 201509150931528_ExportFramework2.cs + + 201511271019577_ExportFramework3.cs + + + 201512151526290_ImportFramework.cs + + + 201601262000441_ImportFramework1.cs + + + 201603121451066_ThirdPartyEmailHandOver.cs + + + 201605020640016_GtinMpnIndex.cs + + + 201605061916117_SwapColumnMappingValues.cs + + + 201605111140288_ImportExtraData.cs + + + 201605191216116_CheckoutAttributeMultiStore.cs + + + 201605201911421_ExportRevision.cs + + + 201607111548571_FixExportDeploymentIsPublic.cs + diff --git a/src/Libraries/SmartStore.Data/app.config b/src/Libraries/SmartStore.Data/app.config index 4f592fbcd8..284b7ee573 100644 --- a/src/Libraries/SmartStore.Data/app.config +++ b/src/Libraries/SmartStore.Data/app.config @@ -1,15 +1,15 @@ - + - - + + - - + + - + diff --git a/src/Libraries/SmartStore.Data/packages.config b/src/Libraries/SmartStore.Data/packages.config index 1101cadf32..b1c169951c 100644 --- a/src/Libraries/SmartStore.Data/packages.config +++ b/src/Libraries/SmartStore.Data/packages.config @@ -1,10 +1,10 @@  - + - + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Authentication/External/AuthorizeState.cs b/src/Libraries/SmartStore.Services/Authentication/External/AuthorizeState.cs index dcc6c9762c..35e853bbfc 100644 --- a/src/Libraries/SmartStore.Services/Authentication/External/AuthorizeState.cs +++ b/src/Libraries/SmartStore.Services/Authentication/External/AuthorizeState.cs @@ -17,7 +17,7 @@ public AuthorizeState(string returnUrl, OpenAuthenticationStatus openAuthenticat _returnUrl = returnUrl; AuthenticationStatus = openAuthenticationStatus; - //in way SEO friendly language URLs will be persisted + // in a way SEO friendly language URLs will be persisted if (AuthenticationStatus == OpenAuthenticationStatus.Authenticated) Result = new RedirectResult(!string.IsNullOrEmpty(_returnUrl) ? _returnUrl : "~/"); } diff --git a/src/Libraries/SmartStore.Services/Authentication/FormsAuthenticationService.cs b/src/Libraries/SmartStore.Services/Authentication/FormsAuthenticationService.cs index b948930425..05cddc7f7b 100644 --- a/src/Libraries/SmartStore.Services/Authentication/FormsAuthenticationService.cs +++ b/src/Libraries/SmartStore.Services/Authentication/FormsAuthenticationService.cs @@ -12,7 +12,7 @@ namespace SmartStore.Services.Authentication ///
    public partial class FormsAuthenticationService : IAuthenticationService { - private readonly HttpContextBase _httpContext; + private readonly HttpContextBase _httpContext; private readonly ICustomerService _customerService; private readonly CustomerSettings _customerSettings; private readonly TimeSpan _expirationTimeSpan; @@ -33,7 +33,6 @@ public FormsAuthenticationService(HttpContextBase httpContext, ICustomerService this._expirationTimeSpan = FormsAuthentication.Timeout; } - public virtual void SignIn(Customer customer, bool createPersistentCookie) { var now = DateTime.UtcNow.ToLocalTime(); @@ -95,6 +94,21 @@ public virtual Customer GetAuthenticatedCustomer() if (customer != null && customer.Active && !customer.Deleted && customer.IsRegistered()) { + if (customer.LastLoginDateUtc == null) + { + try + { + // This is most probably the very first "login" after registering. Delete the + // ASP.NET anonymous id cookie so that a new guest account can be created + // upon signing out. + System.Web.Security.AnonymousIdentificationModule.ClearAnonymousIdentifier(); + } + finally + { + customer.LastLoginDateUtc = DateTime.UtcNow; + _customerService.UpdateCustomer(customer); + } + } _cachedCustomer = customer; } @@ -117,5 +131,5 @@ public virtual Customer GetAuthenticatedCustomerFromTicket(FormsAuthenticationTi return customer; } - } + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Blogs/BlogService.cs b/src/Libraries/SmartStore.Services/Blogs/BlogService.cs index c616abefbe..f3bb8103bb 100644 --- a/src/Libraries/SmartStore.Services/Blogs/BlogService.cs +++ b/src/Libraries/SmartStore.Services/Blogs/BlogService.cs @@ -1,16 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; -using System.ServiceModel.Syndication; -using System.Web.Mvc; using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Domain.Blogs; using SmartStore.Core.Domain.Stores; using SmartStore.Core.Events; using SmartStore.Services.Localization; -using SmartStore.Services.Seo; -using SmartStore.Utilities; namespace SmartStore.Services.Blogs { @@ -260,53 +256,6 @@ public virtual void UpdateCommentTotals(BlogPost blogPost) UpdateBlogPost(blogPost); } - /// - /// Creates a RSS feed with blog posts - /// - /// UrlHelper to generate URLs - /// Language identifier - /// SmartSyndicationFeed object - public virtual SmartSyndicationFeed CreateRssFeed(UrlHelper urlHelper, int languageId) - { - if (urlHelper == null) - throw new ArgumentNullException("urlHelper"); - - DateTime? maxAge = null; - var protocol = _services.WebHelper.IsCurrentConnectionSecured() ? "https" : "http"; - var selfLink = urlHelper.RouteUrl("BlogRSS", new { languageId = languageId }, protocol); - var blogLink = urlHelper.RouteUrl("Blog", null, protocol); - - var title = "{0} - Blog".FormatInvariant(_services.StoreContext.CurrentStore.Name); - - if (_blogSettings.MaxAgeInDays > 0) - maxAge = DateTime.UtcNow.Subtract(new TimeSpan(_blogSettings.MaxAgeInDays, 0, 0, 0)); - - var language = _languageService.GetLanguageById(languageId); - var feed = new SmartSyndicationFeed(new Uri(blogLink), title); - - feed.AddNamespaces(false); - feed.Init(selfLink, language); - - if (!_blogSettings.Enabled) - return feed; - - var items = new List(); - var blogPosts = GetAllBlogPosts(_services.StoreContext.CurrentStore.Id, languageId, null, null, 0, int.MaxValue, false, maxAge); - - foreach (var blogPost in blogPosts) - { - var blogPostUrl = urlHelper.RouteUrl("BlogPost", new { SeName = blogPost.GetSeName(blogPost.LanguageId, ensureTwoPublishedLanguages: false) }, "http"); - - var item = feed.CreateItem(blogPost.Title, blogPost.Body, blogPostUrl, blogPost.CreatedOnUtc); - - items.Add(item); - } - - feed.Items = items; - - return feed; - } - #endregion } } diff --git a/src/Libraries/SmartStore.Services/Blogs/IBlogService.cs b/src/Libraries/SmartStore.Services/Blogs/IBlogService.cs index 983932b0bf..682ddbe191 100644 --- a/src/Libraries/SmartStore.Services/Blogs/IBlogService.cs +++ b/src/Libraries/SmartStore.Services/Blogs/IBlogService.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.Web.Mvc; using SmartStore.Core; using SmartStore.Core.Domain.Blogs; -using SmartStore.Utilities; namespace SmartStore.Services.Blogs { @@ -79,13 +77,5 @@ IPagedList GetAllBlogPostsByTag(int storeId, int languageId, string ta ///
    /// Blog post void UpdateCommentTotals(BlogPost blogPost); - - /// - /// Creates a RSS feed with blog posts - /// - /// UrlHelper to generate URLs - /// Language identifier - /// SmartSyndicationFeed object - SmartSyndicationFeed CreateRssFeed(UrlHelper urlHelper, int languageId); } } diff --git a/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs b/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs index a525580283..86d8e52b2a 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs @@ -154,7 +154,7 @@ public virtual void InheritAclIntoChildren(int categoryId, _categoryRepository.Update(subcategory); } - var existingAclRecords = _aclService.GetAclRecords(subcategory).ToDictionary(x => x.CustomerRoleId); + var existingAclRecords = _aclService.GetAclRecords(subcategory).ToDictionarySafe(x => x.CustomerRoleId); foreach (var customerRole in allCustomerRoles) { @@ -186,7 +186,7 @@ public virtual void InheritAclIntoChildren(int categoryId, _productRepository.Update(product); } - var existingAclRecords = _aclService.GetAclRecords(product).ToDictionary(x => x.CustomerRoleId); + var existingAclRecords = _aclService.GetAclRecords(product).ToDictionarySafe(x => x.CustomerRoleId); foreach (var customerRole in allCustomerRoles) { diff --git a/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs b/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs index 71792bc582..f786bf9c78 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs @@ -84,8 +84,8 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli var utcNow = DateTime.UtcNow; // product download & sample download - int downloadId = product.DownloadId; - int? sampleDownloadId = product.SampleDownloadId; + int downloadId = 0; + int? sampleDownloadId = null; if (product.IsDownload) { @@ -518,6 +518,7 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli { var searchContext = new ProductSearchContext { + OrderBy = ProductSortingEnum.Position, ParentGroupedProductId = product.Id, PageSize = int.MaxValue, ShowHidden = true diff --git a/src/Libraries/SmartStore.Services/Catalog/IManufacturerService.cs b/src/Libraries/SmartStore.Services/Catalog/IManufacturerService.cs index 18f9103883..24917da095 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IManufacturerService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IManufacturerService.cs @@ -32,24 +32,26 @@ public partial interface IManufacturerService /// Manufacturer collection IList GetAllManufacturers(bool showHidden = false); - /// - /// Gets all manufacturers - /// - /// Manufacturer name - /// A value indicating whether to show hidden records - /// Manufacturer collection - IList GetAllManufacturers(string manufacturerName, bool showHidden = false); - - /// - /// Gets all manufacturers - /// - /// Manufacturer name - /// Page index - /// Page size - /// A value indicating whether to show hidden records - /// Manufacturers - IPagedList GetAllManufacturers(string manufacturerName, - int pageIndex, int pageSize, bool showHidden = false); + /// + /// Gets all manufacturers + /// + /// Manufacturer name + /// Whether to filter result by store identifier + /// A value indicating whether to show hidden records + /// Manufacturer collection + IList GetAllManufacturers(string manufacturerName, int storeId = 0, bool showHidden = false); + + /// + /// Gets all manufacturers + /// + /// Manufacturer name + /// Page index + /// Page size + /// Whether to filter result by store identifier + /// A value indicating whether to show hidden records + /// Manufacturers + IPagedList GetAllManufacturers(string manufacturerName, + int pageIndex, int pageSize, int storeId = 0, bool showHidden = false); /// /// Gets a manufacturer diff --git a/src/Libraries/SmartStore.Services/Catalog/IProductAttributeParser.cs b/src/Libraries/SmartStore.Services/Catalog/IProductAttributeParser.cs index 64a412d339..f8565562e5 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IProductAttributeParser.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IProductAttributeParser.cs @@ -33,21 +33,20 @@ public partial interface IProductAttributeParser IEnumerable ParseProductVariantAttributeValues(string attributesXml); /// - /// Get list of localized product variant attribute values + /// Get list of product variant attribute values /// - /// XML formatted attributes + /// Map of combined attributes /// Product variant attributes - /// Language identifier - /// List of localized product variant attribute values - IList ParseProductVariantAttributeValues(string attributesXml, IEnumerable attributes, int languageId = 0); + /// List of product variant attribute values + IList ParseProductVariantAttributeValues(Multimap attributeCombination, IEnumerable attributes); - /// - /// Gets selected product variant attribute value - /// + /// + /// Gets selected product variant attribute value + /// /// XML formatted attributes - /// Product variant attribute identifier - /// Product variant attribute value - IList ParseValues(string attributesXml, int productVariantAttributeId); + /// Product variant attribute identifier + /// Product variant attribute value + IList ParseValues(string attributesXml, int productVariantAttributeId); /// /// Adds an attribute diff --git a/src/Libraries/SmartStore.Services/Catalog/IProductService.cs b/src/Libraries/SmartStore.Services/Catalog/IProductService.cs index 69a8c2254b..0b1f7649d5 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IProductService.cs @@ -2,14 +2,12 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Web.Mvc; using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Discounts; using SmartStore.Core.Domain.Orders; -using SmartStore.Utilities; namespace SmartStore.Services.Catalog { @@ -110,7 +108,21 @@ public partial interface IProductService /// GTIN /// Product Product GetProductByGtin(string gtin); - + + /// + /// Gets a product by manufacturer part number (MPN) + /// + /// Manufacturer part number + /// Product + Product GetProductByManufacturerPartNumber(string manufacturerPartNumber); + + /// + /// Gets a product by name + /// + /// Product name + /// Product + Product GetProductByName(string name); + /// /// Adjusts inventory /// @@ -156,13 +168,6 @@ public partial interface IProductService /// Product void UpdateHasDiscountsApplied(Product product); - /// - /// Creates a RSS feed with recently added products - /// - /// UrlHelper to generate URLs - /// SmartSyndicationFeed object - SmartSyndicationFeed CreateRecentlyAddedProductsRssFeed(UrlHelper urlHelper); - /// /// Get product tags by product identifiers /// @@ -339,8 +344,9 @@ public partial interface IProductService /// Get product pictures by product identifiers /// /// Product identifiers + /// Whether to only load the first picture for each product /// Product pictures - Multimap GetProductPicturesByProductIds(int[] productIds); + Multimap GetProductPicturesByProductIds(int[] productIds, bool onlyFirstPicture = false); /// /// Gets a product picture diff --git a/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs b/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs new file mode 100644 index 0000000000..3f7315905b --- /dev/null +++ b/src/Libraries/SmartStore.Services/Catalog/Importer/CategoryImporter.cs @@ -0,0 +1,489 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using SmartStore.Core.Async; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.Domain.Seo; +using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Events; +using SmartStore.Services.DataExchange.Import; +using SmartStore.Services.Localization; +using SmartStore.Services.Media; +using SmartStore.Services.Seo; +using SmartStore.Services.Stores; +using SmartStore.Utilities; + +namespace SmartStore.Services.Catalog.Importer +{ + 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; + private readonly FileDownloadManager _fileDownloadManager; + + private static readonly Dictionary>> _localizableProperties = new Dictionary>> + { + { "Name", x => x.Name }, + { "FullName", x => x.FullName }, + { "Description", x => x.Description }, + { "BottomDescription", x => x.BottomDescription }, + { "MetaKeywords", x => x.MetaKeywords }, + { "MetaDescription", x => x.MetaDescription }, + { "MetaTitle", x => x.MetaTitle } + }; + + public CategoryImporter( + IRepository categoryRepository, + IRepository pictureRepository, + ICommonServices services, + ICategoryTemplateService categoryTemplateService, + IPictureService pictureService, + ILocalizedEntityService localizedEntityService, + FileDownloadManager fileDownloadManager) + { + _categoryRepository = categoryRepository; + _pictureRepository = pictureRepository; + _services = services; + _categoryTemplateService = categoryTemplateService; + _pictureService = pictureService; + _localizedEntityService = localizedEntityService; + _fileDownloadManager = fileDownloadManager; + } + + protected override void Import(ImportExecuteContext context) + { + var srcToDestId = new Dictionary(); + + var templateViewPaths = _categoryTemplateService.GetAllCategoryTemplates().ToDictionarySafe(x => x.ViewPath, x => x.Id); + + using (var scope = new DbContextScope(ctx: context.Services.DbContext, autoDetectChanges: false, proxyCreation: false, validateOnSave: false)) + { + var segmenter = context.DataSegmenter; + + Initialize(context); + + while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) + { + var batch = segmenter.GetCurrentBatch(); + + // Perf: detach all entities + _categoryRepository.Context.DetachAll(false); + + context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows); + + try + { + ProcessCategories(context, batch, templateViewPaths, srcToDestId); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessCategories"); + } + + // 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 + context.Result.NewRecords += batch.Count(x => x.IsNew && !x.IsTransient); + context.Result.ModifiedRecords += batch.Count(x => !x.IsNew && !x.IsTransient); + + // process slugs + if (segmenter.HasColumn("SeName", true) || batch.Any(x => x.IsNew || x.NameChanged)) + { + try + { + _categoryRepository.Context.AutoDetectChangesEnabled = true; + ProcessSlugs(context, batch, typeof(Category).Name); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessSlugs"); + } + finally + { + _categoryRepository.Context.AutoDetectChangesEnabled = false; + } + } + + // process store mappings + if (segmenter.HasColumn("StoreIds")) + { + try + { + ProcessStoreMappings(context, batch); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessStoreMappings"); + } + } + + // localizations + try + { + ProcessLocalizations(context, batch, _localizableProperties); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessLocalizedProperties"); + } + + // process pictures + if (srcToDestId.Any() && segmenter.HasColumn("ImageUrl") && !segmenter.IsIgnored("PictureId")) + { + try + { + _categoryRepository.Context.AutoDetectChangesEnabled = true; + ProcessPictures(context, batch, srcToDestId); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessPictures"); + } + finally + { + _categoryRepository.Context.AutoDetectChangesEnabled = false; + } + } + } + + // map parent id of inserted categories + if (srcToDestId.Any() && segmenter.HasColumn("Id") && segmenter.HasColumn("ParentCategoryId") && !segmenter.IsIgnored("ParentCategoryId")) + { + segmenter.Reset(); + + while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) + { + var batch = segmenter.GetCurrentBatch(); + _categoryRepository.Context.DetachAll(false); + + try + { + ProcessParentMappings(context, batch, srcToDestId); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessParentMappings"); + } + } + } + } + } + + protected virtual int ProcessPictures( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary srcToDestId) + { + Picture picture = null; + 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) + { + if (category.PictureId.HasValue && (picture = _pictureRepository.GetById(category.PictureId.Value)) != null) + currentPictures.Add(picture); + + pictureBinary = _pictureService.ValidatePicture(pictureBinary); + pictureBinary = _pictureService.FindEqualPicture(pictureBinary, currentPictures, out equalPictureId); + + if (pictureBinary != null && pictureBinary.Length > 0) + { + if ((picture = _pictureService.InsertPicture(pictureBinary, image.MimeType, seoName, true, false, false)) != 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"); + } + } + } + } + catch (Exception exception) + { + context.Result.AddWarning(exception.ToAllMessages(), row.GetRowInfo(), "ImageUrls"); + } + } + + var num = _categoryRepository.Context.SaveChanges(); + + return num; + } + + protected virtual int ProcessLocalizations( + ImportExecuteContext context, + IEnumerable> batch, + string[] localizedProperties) + { + if (localizedProperties.Length == 0) + { + return 0; + } + + bool shouldSave = false; + + foreach (var row in batch) + { + foreach (var prop in localizedProperties) + { + var lambda = _localizableProperties[prop]; + foreach (var lang in context.Languages) + { + var code = lang.UniqueSeoCode; + string value; + + if (row.TryGetDataValue(prop /* ColumnName */, code, out value)) + { + _localizedEntityService.SaveLocalizedValue(row.Entity, lambda, value, lang.Id); + shouldSave = true; + } + } + } + } + + if (shouldSave) + { + // commit whole batch at once + return context.Services.DbContext.SaveChanges(); + } + + return 0; + } + + protected virtual int ProcessParentMappings( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary srcToDestId) + { + foreach (var row in batch) + { + var id = row.GetDataValue("Id"); + var rawParentId = row.GetDataValue("ParentCategoryId"); + var parentId = rawParentId.ToInt(-1); + + if (id != 0 && parentId != -1 && srcToDestId.ContainsKey(id) && srcToDestId.ContainsKey(parentId)) + { + // only touch hierarchical data if child and parent were inserted + if (srcToDestId[id].Inserted && srcToDestId[parentId].Inserted && srcToDestId[id].DestinationId != 0) + { + var category = _categoryRepository.GetById(srcToDestId[id].DestinationId); + if (category != null) + { + category.ParentCategoryId = srcToDestId[parentId].DestinationId; + + _categoryRepository.Update(category); + } + } + } + } + + var num = _categoryRepository.Context.SaveChanges(); + + return num; + } + + protected virtual int ProcessCategories( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary templateViewPaths, + Dictionary srcToDestId) + { + _categoryRepository.AutoCommitEnabled = true; + + Category lastInserted = null; + Category lastUpdated = null; + var defaultTemplateId = templateViewPaths["CategoryTemplate.ProductsInGridOrLines"]; + + foreach (var row in batch) + { + Category category = null; + var id = row.GetDataValue("Id"); + var name = row.GetDataValue("Name"); + + foreach (var keyName in context.KeyFieldNames) + { + switch (keyName) + { + case "Id": + if (id != 0) + category = _categoryRepository.GetById(id); + break; + case "Name": + if (name.HasValue()) + category = _categoryRepository.Table.FirstOrDefault(x => x.Name == name); + break; + } + + if (category != null) + break; + } + + if (category == null) + { + if (context.UpdateOnly) + { + ++context.Result.SkippedRecords; + continue; + } + + // a Name is required with new categories + if (!row.Segmenter.HasColumn("Name")) + { + ++context.Result.SkippedRecords; + context.Result.AddError("The 'Name' field is required for new categories. Skipping row.", row.GetRowInfo(), "Name"); + continue; + } + + category = new Category(); + } + + row.Initialize(category, name ?? category.Name); + + if (!row.IsNew && !category.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) + { + // Perf: use this later for SeName updates. + row.NameChanged = true; + } + + row.SetProperty(context.Result, (x) => x.Name); + row.SetProperty(context.Result, (x) => x.FullName); + row.SetProperty(context.Result, (x) => x.Description); + row.SetProperty(context.Result, (x) => x.BottomDescription); + row.SetProperty(context.Result, (x) => x.MetaKeywords); + row.SetProperty(context.Result, (x) => x.MetaDescription); + row.SetProperty(context.Result, (x) => x.MetaTitle); + row.SetProperty(context.Result, (x) => x.PageSize, 12); + row.SetProperty(context.Result, (x) => x.AllowCustomersToSelectPageSize, true); + row.SetProperty(context.Result, (x) => x.PageSizeOptions); + row.SetProperty(context.Result, (x) => x.PriceRanges); + row.SetProperty(context.Result, (x) => x.ShowOnHomePage); + row.SetProperty(context.Result, (x) => x.HasDiscountsApplied); + row.SetProperty(context.Result, (x) => x.Published, true); + row.SetProperty(context.Result, (x) => x.DisplayOrder); + row.SetProperty(context.Result, (x) => x.Alias); + row.SetProperty(context.Result, (x) => x.DefaultViewMode); + // With new entities, "LimitedToStores" is an implicit field, meaning + // it has to be set to true by code if it's absent but "StoreIds" exists. + row.SetProperty(context.Result, (x) => x.LimitedToStores, !row.GetDataValue>("StoreIds").IsNullOrEmpty()); + + string tvp; + if (row.TryGetDataValue("CategoryTemplateViewPath", out tvp, row.IsTransient)) + { + category.CategoryTemplateId = (tvp.HasValue() && templateViewPaths.ContainsKey(tvp) ? templateViewPaths[tvp] : defaultTemplateId); + } + + row.SetProperty(context.Result, (x) => x.CreatedOnUtc, UtcNow); + category.UpdatedOnUtc = UtcNow; + + if (id != 0 && !srcToDestId.ContainsKey(id)) + { + srcToDestId.Add(id, new ImportCategoryMapping { Inserted = row.IsTransient }); + } + + if (row.IsTransient) + { + _categoryRepository.Insert(category); + lastInserted = category; + } + else + { + _categoryRepository.Update(category); + lastUpdated = category; + } + } + + // commit whole batch at once + var num = _categoryRepository.Context.SaveChanges(); + + // 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; + } + + // Perf: notify only about LAST insertion and update + if (lastInserted != null) + { + _services.EventPublisher.EntityInserted(lastInserted); + } + + if (lastUpdated != null) + { + _services.EventPublisher.EntityUpdated(lastUpdated); + } + + return num; + } + + public static string[] SupportedKeyFields + { + get + { + return new string[] { "Id", "Name" }; + } + } + + public static string[] DefaultKeyFields + { + get + { + return new string[] { "Name", "Id" }; + } + } + + public class ImportCategoryMapping + { + public int DestinationId { get; set; } + public bool Inserted { get; set; } + } + } + +} diff --git a/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs b/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs new file mode 100644 index 0000000000..6228f5b443 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Catalog/Importer/ProductImporter.cs @@ -0,0 +1,738 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using SmartStore.Core.Async; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Events; +using SmartStore.Services.DataExchange.Import; +using SmartStore.Services.Localization; +using SmartStore.Services.Media; +using SmartStore.Utilities; + +namespace SmartStore.Services.Catalog.Importer +{ + public class ProductImporter : EntityImporterBase + { + private readonly IRepository _productPictureRepository; + private readonly IRepository _productManufacturerRepository; + private readonly IRepository _productCategoryRepository; + private readonly IRepository _productRepository; + private readonly ICommonServices _services; + private readonly ILocalizedEntityService _localizedEntityService; + private readonly IPictureService _pictureService; + private readonly IManufacturerService _manufacturerService; + private readonly ICategoryService _categoryService; + private readonly IProductService _productService; + private readonly IProductTemplateService _productTemplateService; + private readonly FileDownloadManager _fileDownloadManager; + + private static readonly Dictionary>> _localizableProperties = new Dictionary>> + { + { "Name", x => x.Name }, + { "ShortDescription", x => x.ShortDescription }, + { "FullDescription", x => x.FullDescription }, + { "MetaKeywords", x => x.MetaKeywords }, + { "MetaDescription", x => x.MetaDescription }, + { "MetaTitle", x => x.MetaTitle }, + { "BundleTitleText", x => x.BundleTitleText } + }; + + public ProductImporter( + IRepository productPictureRepository, + IRepository productManufacturerRepository, + IRepository productCategoryRepository, + IRepository productRepository, + ICommonServices services, + ILocalizedEntityService localizedEntityService, + IPictureService pictureService, + IManufacturerService manufacturerService, + ICategoryService categoryService, + IProductService productService, + IProductTemplateService productTemplateService, + FileDownloadManager fileDownloadManager) + { + _productPictureRepository = productPictureRepository; + _productManufacturerRepository = productManufacturerRepository; + _productCategoryRepository = productCategoryRepository; + _productRepository = productRepository; + _services = services; + _localizedEntityService = localizedEntityService; + _pictureService = pictureService; + _manufacturerService = manufacturerService; + _categoryService = categoryService; + _productService = productService; + _productTemplateService = productTemplateService; + _fileDownloadManager = fileDownloadManager; + } + + protected override void Import(ImportExecuteContext context) + { + var srcToDestId = new Dictionary(); + + var templateViewPaths = _productTemplateService.GetAllProductTemplates().ToDictionarySafe(x => x.ViewPath, x => x.Id); + + using (var scope = new DbContextScope(ctx: _productRepository.Context, autoDetectChanges: false, proxyCreation: false, validateOnSave: false)) + { + var segmenter = context.DataSegmenter; + + Initialize(context); + + while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) + { + var batch = segmenter.GetCurrentBatch(); + + // Perf: detach all entities + _productRepository.Context.DetachAll(false); + + context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows); + + // =========================================================================== + // 1.) Import products + // =========================================================================== + try + { + ProcessProducts(context, batch, templateViewPaths, srcToDestId); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessProducts"); + } + + // 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 + context.Result.NewRecords += batch.Count(x => x.IsNew && !x.IsTransient); + context.Result.ModifiedRecords += batch.Count(x => !x.IsNew && !x.IsTransient); + + // =========================================================================== + // 2.) Import SEO Slugs + // IMPORTANT: Unlike with Products AutoCommitEnabled must be TRUE, + // as Slugs are going to be validated against existing ones in DB. + // =========================================================================== + if (segmenter.HasColumn("SeName", true) || batch.Any(x => x.IsNew || x.NameChanged)) + { + try + { + _productRepository.Context.AutoDetectChangesEnabled = true; + ProcessSlugs(context, batch, typeof(Product).Name); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessSlugs"); + } + finally + { + _productRepository.Context.AutoDetectChangesEnabled = false; + } + } + + // =========================================================================== + // 3.) Import StoreMappings + // =========================================================================== + if (segmenter.HasColumn("StoreIds")) + { + try + { + ProcessStoreMappings(context, batch); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessStoreMappings"); + } + } + + // =========================================================================== + // 4.) Import Localizations + // =========================================================================== + try + { + ProcessLocalizations(context, batch, _localizableProperties); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessLocalizations"); + } + + // =========================================================================== + // 5.) Import product category mappings + // =========================================================================== + if (segmenter.HasColumn("CategoryIds")) + { + try + { + ProcessProductCategories(context, batch); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessProductCategories"); + } + } + + // =========================================================================== + // 6.) Import product manufacturer mappings + // =========================================================================== + if (segmenter.HasColumn("ManufacturerIds")) + { + try + { + ProcessProductManufacturers(context, batch); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessProductManufacturers"); + } + } + + // =========================================================================== + // 7.) Import product picture mappings + // =========================================================================== + if (segmenter.HasColumn("ImageUrls")) + { + try + { + ProcessProductPictures(context, batch); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessProductPictures"); + } + } + } + + // =========================================================================== + // 8.) Map parent id of inserted products + // =========================================================================== + if (srcToDestId.Any() && segmenter.HasColumn("Id") && segmenter.HasColumn("ParentGroupedProductId") && !segmenter.IsIgnored("ParentGroupedProductId")) + { + segmenter.Reset(); + + while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) + { + var batch = segmenter.GetCurrentBatch(); + + _productRepository.Context.DetachAll(false); + + try + { + ProcessProductMappings(context, batch, srcToDestId); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessParentMappings"); + } + } + } + } + } + + protected virtual int ProcessProducts( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary templateViewPaths, + Dictionary srcToDestId) + { + _productRepository.AutoCommitEnabled = false; + + Product lastInserted = null; + Product lastUpdated = null; + var defaultTemplateId = templateViewPaths["ProductTemplate.Simple"]; + + foreach (var row in batch) + { + Product product = null; + var id = row.GetDataValue("Id"); + + foreach (var keyName in context.KeyFieldNames) + { + var keyValue = row.GetDataValue(keyName); + + if (keyValue.HasValue() || id > 0) + { + switch (keyName) + { + case "Id": + product = _productService.GetProductById(id); + break; + case "Sku": + product = _productService.GetProductBySku(keyValue); + break; + case "Gtin": + product = _productService.GetProductByGtin(keyValue); + break; + case "ManufacturerPartNumber": + product = _productService.GetProductByManufacturerPartNumber(keyValue); + break; + case "Name": + product = _productService.GetProductByName(keyValue); + break; + } + } + + if (product != null) + break; + } + + if (product == null) + { + if (context.UpdateOnly) + { + ++context.Result.SkippedRecords; + continue; + } + + // a Name is required for new products. + if (!row.HasDataValue("Name")) + { + ++context.Result.SkippedRecords; + context.Result.AddError("The 'Name' field is required for new products. Skipping row.", row.GetRowInfo(), "Name"); + continue; + } + + product = new Product(); + } + + var name = row.GetDataValue("Name"); + + row.Initialize(product, name ?? product.Name); + + if (!row.IsNew) + { + if (!product.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) + { + // Perf: use this later for SeName updates. + row.NameChanged = true; + } + } + + row.SetProperty(context.Result, (x) => x.ProductTypeId, (int)ProductType.SimpleProduct); + row.SetProperty(context.Result, (x) => x.VisibleIndividually, true); + row.SetProperty(context.Result, (x) => x.Name); + row.SetProperty(context.Result, (x) => x.ShortDescription); + row.SetProperty(context.Result, (x) => x.FullDescription); + row.SetProperty(context.Result, (x) => x.AdminComment); + row.SetProperty(context.Result, (x) => x.ShowOnHomePage); + row.SetProperty(context.Result, (x) => x.HomePageDisplayOrder); + row.SetProperty(context.Result, (x) => x.MetaKeywords); + row.SetProperty(context.Result, (x) => x.MetaDescription); + row.SetProperty(context.Result, (x) => x.MetaTitle); + row.SetProperty(context.Result, (x) => x.AllowCustomerReviews, true); + row.SetProperty(context.Result, (x) => x.ApprovedRatingSum); + row.SetProperty(context.Result, (x) => x.NotApprovedRatingSum); + row.SetProperty(context.Result, (x) => x.ApprovedTotalReviews); + row.SetProperty(context.Result, (x) => x.NotApprovedTotalReviews); + row.SetProperty(context.Result, (x) => x.Published, true); + row.SetProperty(context.Result, (x) => x.Sku); + row.SetProperty(context.Result, (x) => x.ManufacturerPartNumber); + row.SetProperty(context.Result, (x) => x.Gtin); + row.SetProperty(context.Result, (x) => x.IsGiftCard); + row.SetProperty(context.Result, (x) => x.GiftCardTypeId); + row.SetProperty(context.Result, (x) => x.RequireOtherProducts); + row.SetProperty(context.Result, (x) => x.RequiredProductIds); // TODO: global scope + row.SetProperty(context.Result, (x) => x.AutomaticallyAddRequiredProducts); + row.SetProperty(context.Result, (x) => x.IsDownload); + row.SetProperty(context.Result, (x) => x.DownloadId); + row.SetProperty(context.Result, (x) => x.UnlimitedDownloads, true); + row.SetProperty(context.Result, (x) => x.MaxNumberOfDownloads, 10); + row.SetProperty(context.Result, (x) => x.DownloadExpirationDays); + row.SetProperty(context.Result, (x) => x.DownloadActivationTypeId, 1); + row.SetProperty(context.Result, (x) => x.HasSampleDownload); + row.SetProperty(context.Result, (x) => x.SampleDownloadId, (int?)null, ZeroToNull); // TODO: global scope + row.SetProperty(context.Result, (x) => x.HasUserAgreement); + row.SetProperty(context.Result, (x) => x.UserAgreementText); + row.SetProperty(context.Result, (x) => x.IsRecurring); + row.SetProperty(context.Result, (x) => x.RecurringCycleLength, 100); + row.SetProperty(context.Result, (x) => x.RecurringCyclePeriodId); + row.SetProperty(context.Result, (x) => x.RecurringTotalCycles, 10); + row.SetProperty(context.Result, (x) => x.IsShipEnabled, true); + row.SetProperty(context.Result, (x) => x.IsFreeShipping); + row.SetProperty(context.Result, (x) => x.AdditionalShippingCharge); + row.SetProperty(context.Result, (x) => x.IsEsd); + row.SetProperty(context.Result, (x) => x.IsTaxExempt); + row.SetProperty(context.Result, (x) => x.TaxCategoryId, 1); // TODO: global scope + row.SetProperty(context.Result, (x) => x.ManageInventoryMethodId); + row.SetProperty(context.Result, (x) => x.StockQuantity, 10000); + row.SetProperty(context.Result, (x) => x.DisplayStockAvailability); + row.SetProperty(context.Result, (x) => x.DisplayStockQuantity); + row.SetProperty(context.Result, (x) => x.MinStockQuantity); + row.SetProperty(context.Result, (x) => x.LowStockActivityId); + row.SetProperty(context.Result, (x) => x.NotifyAdminForQuantityBelow, 1); + row.SetProperty(context.Result, (x) => x.BackorderModeId); + row.SetProperty(context.Result, (x) => x.AllowBackInStockSubscriptions); + row.SetProperty(context.Result, (x) => x.OrderMinimumQuantity, 1); + row.SetProperty(context.Result, (x) => x.OrderMaximumQuantity, 10000); + row.SetProperty(context.Result, (x) => x.AllowedQuantities); + row.SetProperty(context.Result, (x) => x.DisableBuyButton); + row.SetProperty(context.Result, (x) => x.DisableWishlistButton); + row.SetProperty(context.Result, (x) => x.AvailableForPreOrder); + row.SetProperty(context.Result, (x) => x.CallForPrice); + row.SetProperty(context.Result, (x) => x.Price); + row.SetProperty(context.Result, (x) => x.OldPrice); + row.SetProperty(context.Result, (x) => x.ProductCost); + row.SetProperty(context.Result, (x) => x.SpecialPrice); + row.SetProperty(context.Result, (x) => x.SpecialPriceStartDateTimeUtc); + row.SetProperty(context.Result, (x) => x.SpecialPriceEndDateTimeUtc); + row.SetProperty(context.Result, (x) => x.CustomerEntersPrice); + row.SetProperty(context.Result, (x) => x.MinimumCustomerEnteredPrice); + row.SetProperty(context.Result, (x) => x.MaximumCustomerEnteredPrice, 1000); + // HasTierPrices... ignore as long as no tier prices are imported + // LowestAttributeCombinationPrice... ignore as long as no combinations are imported + row.SetProperty(context.Result, (x) => x.Weight); + row.SetProperty(context.Result, (x) => x.Length); + row.SetProperty(context.Result, (x) => x.Width); + row.SetProperty(context.Result, (x) => x.Height); + row.SetProperty(context.Result, (x) => x.DisplayOrder); + row.SetProperty(context.Result, (x) => x.DeliveryTimeId); // TODO: global scope + row.SetProperty(context.Result, (x) => x.QuantityUnitId); // TODO: global scope + row.SetProperty(context.Result, (x) => x.BasePriceEnabled); + row.SetProperty(context.Result, (x) => x.BasePriceMeasureUnit); + row.SetProperty(context.Result, (x) => x.BasePriceAmount); + row.SetProperty(context.Result, (x) => x.BasePriceBaseAmount); + row.SetProperty(context.Result, (x) => x.BundleTitleText); + row.SetProperty(context.Result, (x) => x.BundlePerItemShipping); + row.SetProperty(context.Result, (x) => x.BundlePerItemPricing); + row.SetProperty(context.Result, (x) => x.BundlePerItemShoppingCart); + row.SetProperty(context.Result, (x) => x.AvailableStartDateTimeUtc); + row.SetProperty(context.Result, (x) => x.AvailableEndDateTimeUtc); + // With new entities, "LimitedToStores" is an implicit field, meaning + // it has to be set to true by code if it's absent but "StoreIds" exists. + row.SetProperty(context.Result, (x) => x.LimitedToStores, !row.GetDataValue>("StoreIds").IsNullOrEmpty()); + + string tvp; + if (row.TryGetDataValue("ProductTemplateViewPath", out tvp, row.IsTransient)) + { + product.ProductTemplateId = (tvp.HasValue() && templateViewPaths.ContainsKey(tvp) ? templateViewPaths[tvp] : defaultTemplateId); + } + + row.SetProperty(context.Result, (x) => x.CreatedOnUtc, UtcNow); + product.UpdatedOnUtc = UtcNow; + + if (id != 0 && !srcToDestId.ContainsKey(id)) + { + srcToDestId.Add(id, new ImportProductMapping { Inserted = row.IsTransient }); + } + + if (row.IsTransient) + { + _productRepository.Insert(product); + lastInserted = product; + } + else + { + _productRepository.Update(product); + lastUpdated = product; + } + } + + // commit whole batch at once + var num = _productRepository.Context.SaveChanges(); + + // get new product ids + foreach (var row in batch) + { + var id = row.GetDataValue("Id"); + + if (id != 0 && srcToDestId.ContainsKey(id)) + srcToDestId[id].DestinationId = row.Entity.Id; + } + + // Perf: notify only about LAST insertion and update + if (lastInserted != null) + { + _services.EventPublisher.EntityInserted(lastInserted); + } + + if (lastUpdated != null) + { + _services.EventPublisher.EntityUpdated(lastUpdated); + } + + return num; + } + + protected virtual int ProcessProductMappings( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary srcToDestId) + { + _productRepository.AutoCommitEnabled = false; + + foreach (var row in batch) + { + var id = row.GetDataValue("Id"); + var parentGroupedProductId = row.GetDataValue("ParentGroupedProductId"); + + if (id != 0 && parentGroupedProductId != 0 && srcToDestId.ContainsKey(id) && srcToDestId.ContainsKey(parentGroupedProductId)) + { + // only touch relationship if child and parent were inserted + if (srcToDestId[id].Inserted && srcToDestId[parentGroupedProductId].Inserted && srcToDestId[id].DestinationId != 0) + { + var product = _productRepository.GetById(srcToDestId[id].DestinationId); + if (product != null) + { + product.ParentGroupedProductId = srcToDestId[parentGroupedProductId].DestinationId; + _productRepository.Update(product); + } + } + } + } + + var num = _productRepository.Context.SaveChanges(); + + return num; + } + + protected virtual void ProcessProductPictures(ImportExecuteContext context, IEnumerable> batch) + { + // true, cause pictures must be saved and assigned an id prior adding a mapping. + _productPictureRepository.AutoCommitEnabled = true; + + ProductPicture lastInserted = null; + var equalPictureId = 0; + var numberOfPictures = (context.ExtraData.NumberOfPictures ?? int.MaxValue); + + foreach (var row in batch) + { + var imageUrls = row.GetDataValue>("ImageUrls"); + if (imageUrls.IsNullOrEmpty()) + continue; + + var imageNumber = 0; + var displayOrder = -1; + var seoName = _pictureService.GetPictureSeName(row.EntityDisplayName); + var imageFiles = new List(); + + // collect required image file infos + foreach (var urlOrPath in imageUrls) + { + var image = CreateDownloadImage(urlOrPath, seoName, ++imageNumber); + + if (image != null) + imageFiles.Add(image); + + if (imageFiles.Count >= numberOfPictures) + break; + } + + // download images + if (imageFiles.Any(x => x.Url.HasValue())) + { + // async downloading in batch processing is inefficient cause only the image processing benefits from async, + // not the record processing itself. a per record processing may speed up the import. + + AsyncRunner.RunSync(() => _fileDownloadManager.DownloadAsync(DownloaderContext, imageFiles.Where(x => x.Url.HasValue() && !x.Success.HasValue))); + } + + // import images + foreach (var image in imageFiles.OrderBy(x => x.DisplayOrder)) + { + try + { + if ((image.Success ?? false) && File.Exists(image.Path)) + { + Succeeded(image); + var pictureBinary = File.ReadAllBytes(image.Path); + + if (pictureBinary != null && pictureBinary.Length > 0) + { + var currentProductPictures = _productPictureRepository.TableUntracked.Expand(x => x.Picture) + .Where(x => x.ProductId == row.Entity.Id) + .ToList(); + + var currentPictures = currentProductPictures + .Select(x => x.Picture) + .ToList(); + + if (displayOrder == -1) + { + displayOrder = (currentProductPictures.Any() ? currentProductPictures.Select(x => x.DisplayOrder).Max() : 0); + } + + pictureBinary = _pictureService.ValidatePicture(pictureBinary); + pictureBinary = _pictureService.FindEqualPicture(pictureBinary, currentPictures, out equalPictureId); + + if (pictureBinary != null && pictureBinary.Length > 0) + { + // no equal picture found in sequence + var newPicture = _pictureService.InsertPicture(pictureBinary, image.MimeType, seoName, true, false, false); + if (newPicture != null) + { + var mapping = new ProductPicture + { + ProductId = row.Entity.Id, + PictureId = newPicture.Id, + DisplayOrder = ++displayOrder + }; + + _productPictureRepository.Insert(mapping); + lastInserted = mapping; + } + } + else + { + context.Result.AddInfo("Found equal picture in data store. Skipping field.", row.GetRowInfo(), "ImageUrls" + image.DisplayOrder.ToString()); + } + } + } + else if (image.Url.HasValue()) + { + context.Result.AddInfo("Download of an image failed.", row.GetRowInfo(), "ImageUrls" + image.DisplayOrder.ToString()); + } + } + catch (Exception exception) + { + context.Result.AddWarning(exception.ToAllMessages(), row.GetRowInfo(), "ImageUrls" + image.DisplayOrder.ToString()); + } + } + } + + // Perf: notify only about LAST insertion and update + if (lastInserted != null) + { + _services.EventPublisher.EntityInserted(lastInserted); + } + } + + protected virtual int ProcessProductManufacturers(ImportExecuteContext context, IEnumerable> batch) + { + _productManufacturerRepository.AutoCommitEnabled = false; + + ProductManufacturer lastInserted = null; + + foreach (var row in batch) + { + var manufacturerIds = row.GetDataValue>("ManufacturerIds"); + if (!manufacturerIds.IsNullOrEmpty()) + { + try + { + foreach (var id in manufacturerIds) + { + if (_productManufacturerRepository.TableUntracked.Where(x => x.ProductId == row.Entity.Id && x.ManufacturerId == id).FirstOrDefault() == null) + { + // ensure that manufacturer exists + var manufacturer = _manufacturerService.GetManufacturerById(id); + if (manufacturer != null) + { + var productManufacturer = new ProductManufacturer + { + ProductId = row.Entity.Id, + ManufacturerId = manufacturer.Id, + IsFeaturedProduct = false, + DisplayOrder = 1 + }; + _productManufacturerRepository.Insert(productManufacturer); + lastInserted = productManufacturer; + } + } + } + } + catch (Exception exception) + { + context.Result.AddWarning(exception.Message, row.GetRowInfo(), "ManufacturerIds"); + } + } + } + + // commit whole batch at once + var num = _productManufacturerRepository.Context.SaveChanges(); + + // Perf: notify only about LAST insertion and update + if (lastInserted != null) + _services.EventPublisher.EntityInserted(lastInserted); + + return num; + } + + protected virtual int ProcessProductCategories(ImportExecuteContext context, IEnumerable> batch) + { + _productCategoryRepository.AutoCommitEnabled = false; + + ProductCategory lastInserted = null; + + foreach (var row in batch) + { + var categoryIds = row.GetDataValue>("CategoryIds"); + if (!categoryIds.IsNullOrEmpty()) + { + try + { + foreach (var id in categoryIds) + { + if (_productCategoryRepository.TableUntracked.Where(x => x.ProductId == row.Entity.Id && x.CategoryId == id).FirstOrDefault() == null) + { + // ensure that category exists + var category = _categoryService.GetCategoryById(id); + if (category != null) + { + var productCategory = new ProductCategory + { + ProductId = row.Entity.Id, + CategoryId = category.Id, + IsFeaturedProduct = false, + DisplayOrder = 1 + }; + _productCategoryRepository.Insert(productCategory); + lastInserted = productCategory; + } + } + } + } + catch (Exception exception) + { + context.Result.AddWarning(exception.Message, row.GetRowInfo(), "CategoryIds"); + } + } + } + + // commit whole batch at once + var num = _productCategoryRepository.Context.SaveChanges(); + + // Perf: notify only about LAST insertion and update + if (lastInserted != null) + _services.EventPublisher.EntityInserted(lastInserted); + + return num; + } + + + private int? ZeroToNull(object value, CultureInfo culture) + { + int result; + if (CommonHelper.TryConvert(value, culture, out result) && result > 0) + { + return result; + } + + return (int?)null; + } + + public static string[] SupportedKeyFields + { + get + { + return new string[] { "Id", "Sku", "Gtin", "ManufacturerPartNumber", "Name" }; + } + } + + public static string[] DefaultKeyFields + { + get + { + return new string[] { "Sku", "Gtin", "ManufacturerPartNumber" }; + } + } + + public class ImportProductMapping + { + public int DestinationId { get; set; } + public bool Inserted { get; set; } + } + } +} diff --git a/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs b/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs index e616f8cb2b..9128610551 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs @@ -98,22 +98,19 @@ public virtual IQueryable GetManufacturers(bool showHidden = false if (!showHidden) query = query.Where(m => m.Published); - if (!showHidden) + if (!QuerySettings.IgnoreMultiStore && storeId > 0) { - if (!QuerySettings.IgnoreMultiStore && storeId > 0) - { - query = from m in query - join sm in _storeMappingRepository.Table - on new { c1 = m.Id, c2 = "Manufacturer" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into m_sm - from sm in m_sm.DefaultIfEmpty() - where !m.LimitedToStores || storeId == sm.StoreId - select m; - - query = from m in query - group m by m.Id into mGroup - orderby mGroup.Key - select mGroup.FirstOrDefault(); - } + query = from m in query + join sm in _storeMappingRepository.Table + on new { c1 = m.Id, c2 = "Manufacturer" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into m_sm + from sm in m_sm.DefaultIfEmpty() + where !m.LimitedToStores || storeId == sm.StoreId + select m; + + query = from m in query + group m by m.Id into mGroup + orderby mGroup.Key + select mGroup.FirstOrDefault(); } return query; @@ -126,18 +123,19 @@ orderby mGroup.Key /// Manufacturer collection public virtual IList GetAllManufacturers(bool showHidden = false) { - return GetAllManufacturers(null, showHidden); + return GetAllManufacturers(null, 0, showHidden); } /// /// Gets all manufacturers /// /// Manufacturer name + /// Whether to filter result by store identifier /// A value indicating whether to show hidden records /// Manufacturer collection - public virtual IList GetAllManufacturers(string manufacturerName, bool showHidden = false) + public virtual IList GetAllManufacturers(string manufacturerName, int storeId = 0, bool showHidden = false) { - var query = GetManufacturers(showHidden, _storeContext.CurrentStore.Id); + var query = GetManufacturers(showHidden, storeId); if (manufacturerName.HasValue()) query = query.Where(m => m.Name.Contains(manufacturerName)); @@ -148,19 +146,20 @@ public virtual IList GetAllManufacturers(string manufacturerName, var manufacturers = query.ToList(); return manufacturers; } - - /// - /// Gets all manufacturers - /// - /// Manufacturer name - /// Page index - /// Page size - /// A value indicating whether to show hidden records - /// Manufacturers - public virtual IPagedList GetAllManufacturers(string manufacturerName, - int pageIndex, int pageSize, bool showHidden = false) + + /// + /// Gets all manufacturers + /// + /// Manufacturer name + /// Page index + /// Page size + /// Whether to filter result by store identifier + /// A value indicating whether to show hidden records + /// Manufacturers + public virtual IPagedList GetAllManufacturers(string manufacturerName, + int pageIndex, int pageSize, int storeId = 0, bool showHidden = false) { - var manufacturers = GetAllManufacturers(manufacturerName, showHidden); + var manufacturers = GetAllManufacturers(manufacturerName, storeId, showHidden); return new PagedList(manufacturers, pageIndex, pageSize); } diff --git a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs index dcb4b35d5f..ca45f495d7 100644 --- a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs @@ -251,6 +251,12 @@ protected virtual decimal GetPreselectedPrice(Product product, PriceCalculationC if (selectedCombination != null && selectedCombination.IsActive && selectedCombination.Price.HasValue) { product.MergedDataValues = new Dictionary { { "Price", selectedCombination.Price.Value } }; + + if (selectedCombination.BasePriceAmount.HasValue) + product.MergedDataValues.Add("BasePriceAmount", selectedCombination.BasePriceAmount.Value); + + if (selectedCombination.BasePriceBaseAmount.HasValue) + product.MergedDataValues.Add("BasePriceBaseAmount", selectedCombination.BasePriceBaseAmount.Value); } } @@ -756,13 +762,18 @@ public virtual decimal GetUnitPrice(OrganizedShoppingCartItem shoppingCartItem, { product.MergeWithCombination(shoppingCartItem.Item.AttributesXml, _productAttributeParser); - decimal attributesTotalPrice = decimal.Zero; - var pvaValues = _productAttributeParser.ParseProductVariantAttributeValues(shoppingCartItem.Item.AttributesXml).ToList(); + var attributesTotalPrice = decimal.Zero; + + var pvaValuesEnum = _productAttributeParser.ParseProductVariantAttributeValues(shoppingCartItem.Item.AttributesXml); - if (pvaValues != null) + if (pvaValuesEnum != null) { + var pvaValues = pvaValuesEnum.ToList(); + foreach (var pvaValue in pvaValues) + { attributesTotalPrice += GetProductVariantAttributeValuePriceAdjustment(pvaValue); + } } finalPrice = GetFinalPrice(product, customer, attributesTotalPrice, includeDiscounts, shoppingCartItem.Item.Quantity, shoppingCartItem.BundleItemData); diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs index 23ee794f0b..cf4531f322 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs @@ -13,14 +13,13 @@ using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; -using SmartStore.Services.Localization; namespace SmartStore.Services.Catalog { - /// - /// Product attribute parser - /// - public partial class ProductAttributeParser : IProductAttributeParser + /// + /// Product attribute parser + /// + public partial class ProductAttributeParser : IProductAttributeParser { // 0 = ProductId, 1 = AttributeXml Hash private const string ATTRIBUTECOMBINATION_BY_ID_HASH = "SmartStore.parsedattributecombination.id-{0}-{1}"; @@ -194,21 +193,20 @@ where id.HasValue() return values; } - public IList ParseProductVariantAttributeValues(string attributesXml, IEnumerable attributes, int languageId = 0) + public virtual IList ParseProductVariantAttributeValues(Multimap attributeCombination, IEnumerable attributes) { - var values = new List(); + var result = new List(); - if (attributesXml.IsEmpty()) - return values; + if (attributeCombination == null || !attributeCombination.Any()) + return result; var allValueIds = new List(); - var combinedAttributes = DeserializeProductVariantAttributes(attributesXml); foreach (var pva in attributes.Where(x => x.ShouldHaveValues()).OrderBy(x => x.DisplayOrder)) { - if (combinedAttributes.ContainsKey(pva.Id)) + if (attributeCombination.ContainsKey(pva.Id)) { - var pvaValuesStr = combinedAttributes[pva.Id]; + var pvaValuesStr = attributeCombination[pva.Id]; var ids = pvaValuesStr.Where(x => x.HasValue()).Select(x => x.ToInt()); allValueIds.AddRange(ids); @@ -220,28 +218,24 @@ public IList ParseProductVariantAttributeValues(string attributesXml, IE foreach (var attribute in attributes) { var attributeValue = attribute.ProductVariantAttributeValues.FirstOrDefault(x => x.Id == id); - if (attributeValue != null) + if (attributeValue != null && !result.Any(x => x.Id == attributeValue.Id)) { - var value = attributeValue.GetLocalized(x => x.Name, languageId, true, false); - - if (!values.Any(x => x.IsCaseInsensitiveEqual(value))) - values.Add(value); + result.Add(attributeValue); break; } } } - return values; + return result; } - - /// - /// Gets selected product variant attribute value - /// - /// Attributes - /// Product variant attribute identifier - /// Product variant attribute value - public virtual IList ParseValues(string attributesXml, int productVariantAttributeId) + /// + /// Gets selected product variant attribute value + /// + /// Attributes + /// Product variant attribute identifier + /// Product variant attribute value + public virtual IList ParseValues(string attributesXml, int productVariantAttributeId) { var selectedProductVariantAttributeValues = new List(); try @@ -374,15 +368,20 @@ public virtual ProductVariantAttributeCombination FindProductVariantAttributeCom public virtual List> DeserializeQueryData(string jsonData) { - if (jsonData.HasValue()) + try { - if (jsonData.StartsWith("[")) + if (jsonData.HasValue()) { - return JsonConvert.DeserializeObject>>(jsonData); - } + if (jsonData.StartsWith("[")) + { + return JsonConvert.DeserializeObject>>(jsonData); + } - return new List> { JsonConvert.DeserializeObject>(jsonData) }; + return new List> { JsonConvert.DeserializeObject>(jsonData) }; + } } + catch { } + return new List>(); } diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs b/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs index 85b55cfb4f..051fc841f2 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs @@ -14,7 +14,7 @@ namespace SmartStore.Services.Catalog { - public static class ProductExtensions + public static class ProductExtensions { public static ProductVariantAttributeCombination MergeWithCombination(this Product product, string selectedAttributes) { @@ -299,7 +299,7 @@ public static int[] ParseRequiredProductIds(this Product product) } /// - /// Gets the base price + /// Gets the base price info /// /// Product /// Localization service @@ -309,17 +309,23 @@ public static int[] ParseRequiredProductIds(this Product product) /// Price calculation service /// Target currency /// Price adjustment - /// Whether the result string should be language independent - /// The base price - public static string GetBasePriceInfo(this Product product, ILocalizationService localizationService, IPriceFormatter priceFormatter, - ICurrencyService currencyService, ITaxService taxService, IPriceCalculationService priceCalculationService, - Currency currency, decimal priceAdjustment = decimal.Zero, bool languageIndependent = false) + /// Whether the result string should be language insensitive + /// The base price info + public static string GetBasePriceInfo(this Product product, + ILocalizationService localizationService, + IPriceFormatter priceFormatter, + ICurrencyService currencyService, + ITaxService taxService, + IPriceCalculationService priceCalculationService, + Currency currency, + decimal priceAdjustment = decimal.Zero, + bool languageInsensitive = false) { - if (product == null) - throw new ArgumentNullException("product"); - - if (localizationService == null && !languageIndependent) - throw new ArgumentNullException("localizationService"); + Guard.ArgumentNotNull(() => product); + Guard.ArgumentNotNull(() => currencyService); + Guard.ArgumentNotNull(() => taxService); + Guard.ArgumentNotNull(() => priceCalculationService); + Guard.ArgumentNotNull(() => currency); if (product.BasePriceHasValue && product.BasePriceAmount != Decimal.Zero) { @@ -327,24 +333,58 @@ public static string GetBasePriceInfo(this Product product, ILocalizationService var taxrate = decimal.Zero; var currentPrice = priceCalculationService.GetFinalPrice(product, workContext.CurrentCustomer, true); - decimal price = taxService.GetProductPrice(product, decimal.Add(currentPrice, priceAdjustment), out taxrate); + var price = taxService.GetProductPrice(product, decimal.Add(currentPrice, priceAdjustment), out taxrate); price = currencyService.ConvertFromPrimaryStoreCurrency(price, currency); - decimal basePriceValue = Convert.ToDecimal((price / product.BasePriceAmount) * product.BasePriceBaseAmount); + return product.GetBasePriceInfo(price, localizationService, priceFormatter, currency, languageInsensitive); + } - string basePrice = priceFormatter.FormatPrice(basePriceValue, true, currency); - string unit = "{0} {1}".FormatWith(product.BasePriceBaseAmount, product.BasePriceMeasureUnit); + return ""; + } - if (languageIndependent) - { - return "{0} / {1}".FormatWith(basePrice, unit); - } + /// + /// Gets the base price info + /// + /// Product + /// The calculated product price + /// Localization service + /// Price formatter + /// Target currency + /// Whether the result string should be language insensitive + /// The base price info + public static string GetBasePriceInfo(this Product product, + decimal productPrice, + ILocalizationService localizationService, + IPriceFormatter priceFormatter, + Currency currency, + bool languageInsensitive = false) + { + Guard.ArgumentNotNull(() => product); + Guard.ArgumentNotNull(() => localizationService); + Guard.ArgumentNotNull(() => priceFormatter); + Guard.ArgumentNotNull(() => currency); + + if (product.BasePriceHasValue && product.BasePriceAmount != Decimal.Zero) + { + var value = Convert.ToDecimal((productPrice / product.BasePriceAmount) * product.BasePriceBaseAmount); + var valueFormatted = priceFormatter.FormatPrice(value, true, currency); + var amountFormatted = Math.Round(product.BasePriceAmount.Value, 2).ToString("G29"); + + var infoTemplate = localizationService.GetResource(languageInsensitive ? "Products.BasePriceInfo.LanguageInsensitive" : "Products.BasePriceInfo"); + + var result = infoTemplate.FormatInvariant( + amountFormatted, + product.BasePriceMeasureUnit, + valueFormatted, + product.BasePriceBaseAmount + ); + + return result; + } - return localizationService.GetResource("Products.BasePriceInfo").FormatWith(basePrice, unit); - } return ""; - } + } public static string GetProductTypeLabel(this Product product, ILocalizationService localizationService) { diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductService.cs b/src/Libraries/SmartStore.Services/Catalog/ProductService.cs index c18f230bf5..6dc93b582d 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductService.cs @@ -4,8 +4,6 @@ using System.Globalization; using System.Linq; using System.Linq.Expressions; -using System.ServiceModel.Syndication; -using System.Web.Mvc; using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Caching; @@ -15,17 +13,13 @@ using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Discounts; using SmartStore.Core.Domain.Localization; -using SmartStore.Core.Domain.Media; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Security; using SmartStore.Core.Domain.Stores; using SmartStore.Core.Events; using SmartStore.Services.Localization; -using SmartStore.Services.Media; using SmartStore.Services.Messages; using SmartStore.Services.Orders; -using SmartStore.Services.Seo; -using SmartStore.Utilities; namespace SmartStore.Services.Catalog { @@ -64,9 +58,6 @@ public partial class ProductService : IProductService private readonly LocalizationSettings _localizationSettings; private readonly CommonSettings _commonSettings; private readonly ICommonServices _services; - private readonly CatalogSettings _catalogSettings; - private readonly MediaSettings _mediaSettings; - private readonly IPictureService _pictureService; #endregion @@ -117,10 +108,7 @@ public ProductService( ICacheManager cacheManager, LocalizationSettings localizationSettings, CommonSettings commonSettings, - ICommonServices services, - CatalogSettings catalogSettings, - MediaSettings mediaSettings, - IPictureService pictureService) + ICommonServices services) { this._productRepository = productRepository; this._relatedProductRepository = relatedProductRepository; @@ -143,9 +131,6 @@ public ProductService( this._localizationSettings = localizationSettings; this._commonSettings = commonSettings; this._services = services; - this._catalogSettings = catalogSettings; - this._mediaSettings = mediaSettings; - this._pictureService = pictureService; this.QuerySettings = DbQuerySettings.Default; } @@ -1164,6 +1149,36 @@ orderby p.Id return product; } + public virtual Product GetProductByManufacturerPartNumber(string manufacturerPartNumber) + { + if (manufacturerPartNumber.IsEmpty()) + return null; + + manufacturerPartNumber = manufacturerPartNumber.Trim(); + + var product = _productRepository.Table + .Where(x => !x.Deleted && x.ManufacturerPartNumber == manufacturerPartNumber) + .OrderBy(x => x.Id) + .FirstOrDefault(); + + return product; + } + + public virtual Product GetProductByName(string name) + { + if (name.IsEmpty()) + return null; + + name = name.Trim(); + + var product = _productRepository.Table + .Where(x => !x.Deleted && x.Name == name) + .OrderBy(x => x.Id) + .FirstOrDefault(); + + return product; + } + /// /// Adjusts inventory /// @@ -1369,76 +1384,6 @@ public virtual void UpdateHasDiscountsApplied(Product product) UpdateProduct(product); } - /// - /// Creates a RSS feed with recently added products - /// - /// UrlHelper to generate URLs - /// SmartSyndicationFeed object - public virtual SmartSyndicationFeed CreateRecentlyAddedProductsRssFeed(UrlHelper urlHelper) - { - if (urlHelper == null) - throw new ArgumentNullException("urlHelper"); - - var protocol = _services.WebHelper.IsCurrentConnectionSecured() ? "https" : "http"; - var selfLink = urlHelper.RouteUrl("RecentlyAddedProductsRSS", null, protocol); - var recentProductsLink = urlHelper.RouteUrl("RecentlyAddedProducts", null, protocol); - - var title = "{0} - {1}".FormatInvariant(_services.StoreContext.CurrentStore.Name, _services.Localization.GetResource("RSS.RecentlyAddedProducts")); - - var feed = new SmartSyndicationFeed(new Uri(recentProductsLink), title, _services.Localization.GetResource("RSS.InformationAboutProducts")); - - feed.AddNamespaces(true); - feed.Init(selfLink, _services.WorkContext.WorkingLanguage); - - if (!_catalogSettings.RecentlyAddedProductsEnabled) - return feed; - - var items = new List(); - var searchContext = new ProductSearchContext - { - LanguageId = _services.WorkContext.WorkingLanguage.Id, - OrderBy = ProductSortingEnum.CreatedOn, - PageSize = _catalogSettings.RecentlyAddedProductsNumber, - StoreId = _services.StoreContext.CurrentStoreIdIfMultiStoreMode, - VisibleIndividuallyOnly = true - }; - - var products = SearchProducts(searchContext); - var storeUrl = _services.StoreContext.CurrentStore.Url; - - foreach (var product in products) - { - string productUrl = urlHelper.RouteUrl("Product", new { SeName = product.GetSeName() }, "http"); - if (productUrl.HasValue()) - { - var item = feed.CreateItem( - product.GetLocalized(x => x.Name), - product.GetLocalized(x => x.ShortDescription), - productUrl, - product.CreatedOnUtc, - product.FullDescription); - - try - { - // we add only the first picture - var picture = product.ProductPictures.OrderBy(x => x.DisplayOrder).Select(x => x.Picture).FirstOrDefault(); - - if (picture != null) - { - feed.AddEnclosue(item, picture, _pictureService.GetPictureUrl(picture, _mediaSettings.ProductDetailsPictureSize, false, storeUrl)); - } - } - catch { } - - items.Add(item); - } - } - - feed.Items = items; - - return feed; - } - public virtual Multimap GetProductTagsByProductIds(int[] productIds) { Guard.ArgumentNotNull(() => productIds); @@ -1910,20 +1855,31 @@ orderby pp.DisplayOrder return productPictures; } - public virtual Multimap GetProductPicturesByProductIds(int[] productIds) + public virtual Multimap GetProductPicturesByProductIds(int[] productIds, bool onlyFirstPicture = false) { var query = - from pp in _productPictureRepository.TableUntracked + from pp in _productPictureRepository.TableUntracked.Expand(x => x.Picture) where productIds.Contains(pp.ProductId) + orderby pp.ProductId, pp.DisplayOrder select pp; - var map = query - .OrderBy(x => x.ProductId) - .ThenBy(x => x.DisplayOrder) - .ToList() - .ToMultimap(x => x.ProductId, x => x); + if (onlyFirstPicture) + { + var map = query.GroupBy(x => x.ProductId, x => x) + .Select(x => x.FirstOrDefault()) + .ToList() + .ToMultimap(x => x.ProductId, x => x); - return map; + return map; + } + else + { + var map = query + .ToList() + .ToMultimap(x => x.ProductId, x => x); + + return map; + } } /// diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductTagService.cs b/src/Libraries/SmartStore.Services/Catalog/ProductTagService.cs index 53cad4b1a5..a80d073138 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductTagService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductTagService.cs @@ -11,10 +11,10 @@ namespace SmartStore.Services.Catalog { - /// - /// Product tag service - /// - public partial class ProductTagService : IProductTagService + /// + /// Product tag service + /// + public partial class ProductTagService : IProductTagService { #region Constants @@ -36,6 +36,7 @@ public partial class ProductTagService : IProductTagService #region Fields private readonly IRepository _productTagRepository; + private readonly IRepository _storeMappingRepository; private readonly IDataProvider _dataProvider; private readonly IDbContext _dbContext; private readonly CommonSettings _commonSettings; @@ -55,7 +56,9 @@ public partial class ProductTagService : IProductTagService /// Common settings /// Cache manager /// Event published - public ProductTagService(IRepository productTagRepository, + public ProductTagService( + IRepository productTagRepository, + IRepository storeMappingRepository, IDataProvider dataProvider, IDbContext dbContext, CommonSettings commonSettings, @@ -63,14 +66,19 @@ public ProductTagService(IRepository productTagRepository, IEventPublisher eventPublisher) { _productTagRepository = productTagRepository; + _storeMappingRepository = storeMappingRepository; _dataProvider = dataProvider; _dbContext = dbContext; _commonSettings = commonSettings; _cacheManager = cacheManager; _eventPublisher = eventPublisher; - } - #endregion + QuerySettings = DbQuerySettings.Default; + } + + public DbQuerySettings QuerySettings { get; set; } + + #endregion #region Nested classes @@ -94,55 +102,38 @@ private Dictionary GetProductCount(int storeId) string key = string.Format(PRODUCTTAG_COUNT_KEY, storeId); return _cacheManager.Get(key, () => { + IEnumerable tagCount = null; if (_commonSettings.UseStoredProceduresIfSupported && _dataProvider.StoredProceduresSupported) { - //stored procedures are enabled and supported by the database. - //It's much faster than the LINQ implementation below + //stored procedures are enabled and supported by the database. It's much faster than the LINQ implementation below - #region Use stored procedure - - //prepare parameters var pStoreId = _dataProvider.GetParameter(); pStoreId.ParameterName = "StoreId"; pStoreId.Value = storeId; pStoreId.DbType = DbType.Int32; - - //invoke stored procedure - var result = _dbContext.SqlQuery( - "Exec ProductTagCountLoadAll @StoreId", - pStoreId); - - var dictionary = new Dictionary(); - foreach (var item in result) - dictionary.Add(item.ProductTagId, item.ProductCount); - return dictionary; - - #endregion + tagCount = _dbContext.SqlQuery("Exec ProductTagCountLoadAll @StoreId", pStoreId); } else { //stored procedures aren't supported. Use LINQ - #region Search products - var query = from pt in _productTagRepository.Table - select new - { - Id = pt.Id, - ProductCount = pt.Products - //published and not deleted product/variants - .Count(p => !p.Deleted && p.Published) - //UNDOEN filter by store identifier if specified ( > 0 ) - }; - - var dictionary = new Dictionary(); - foreach (var item in query) - dictionary.Add(item.Id, item.ProductCount); - return dictionary; - - #endregion + tagCount = _productTagRepository.Table + .Select(pt => new ProductTagWithCount + { + ProductTagId = pt.Id, + ProductCount = (storeId > 0 && !QuerySettings.IgnoreMultiStore) ? + (from p in pt.Products + join sm in _storeMappingRepository.Table on new { pid = p.Id, pname = "Product" } equals new { pid = sm.EntityId, pname = sm.EntityName } into psm + from sm in psm.DefaultIfEmpty() + where (!p.LimitedToStores || storeId == sm.StoreId) && !p.Deleted && p.Published + select p).Count() : + pt.Products.Count(p => !p.Deleted && p.Published) + }); } + + return tagCount.ToDictionary(x => x.ProductTagId, x => x.ProductCount); }); } @@ -265,10 +256,11 @@ public virtual void UpdateProductTag(ProductTag productTag) public virtual int GetProductCount(int productTagId, int storeId) { var dictionary = GetProductCount(storeId); + if (dictionary.ContainsKey(productTagId)) return dictionary[productTagId]; - else - return 0; + + return 0; } #endregion diff --git a/src/Libraries/SmartStore.Services/Common/AddressExtentions.cs b/src/Libraries/SmartStore.Services/Common/AddressExtentions.cs index 9f108f69ed..1055aaa340 100644 --- a/src/Libraries/SmartStore.Services/Common/AddressExtentions.cs +++ b/src/Libraries/SmartStore.Services/Common/AddressExtentions.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Collections.Generic; using System.Text; using SmartStore.Core.Domain.Common; @@ -7,41 +8,79 @@ namespace SmartStore.Services.Common { public static class AddressExtentions { - /// - /// Find an address - /// - /// Source - /// First name - /// Last name - /// Phone number - /// Email - /// Fax number - /// Company - /// Address 1 - /// Address 2 - /// City - /// State/province identifier - /// Zip postal code - /// Country identifier - /// Address - public static Address FindAddress(this List
    source, - string firstName, string lastName, string phoneNumber, - string email, string faxNumber, string company, string address1, - string address2, string city, int? stateProvinceId, - string zipPostalCode, int? countryId) + /// + /// Find first occurrence of an address + /// + /// Addresses to be searched + /// Address to find + /// First occurrence of address + public static Address FindAddress(this ICollection
    source, Address address) + { + return source.FindAddress( + address.FirstName, + address.LastName, + address.PhoneNumber, + address.Email, + address.FaxNumber, + address.Company, + address.Address1, + address.Address2, + address.City, + address.StateProvinceId, + address.ZipPostalCode, + address.CountryId + ); + } + + /// + /// Find an address + /// + /// Source + /// First name + /// Last name + /// Phone number + /// Email + /// Fax number + /// Company + /// Address 1 + /// Address 2 + /// City + /// State/province identifier + /// Zip postal code + /// Country identifier + /// Address + public static Address FindAddress( + this ICollection
    source, + string firstName, + string lastName, + string phoneNumber, + string email, + string faxNumber, + string company, + string address1, + string address2, + string city, + int? stateProvinceId, + string zipPostalCode, + int? countryId) { - return source.Find((a) => ((String.IsNullOrEmpty(a.FirstName) && String.IsNullOrEmpty(firstName)) || a.FirstName == firstName) && - ((String.IsNullOrEmpty(a.LastName) && String.IsNullOrEmpty(lastName)) || a.LastName == lastName) && - ((String.IsNullOrEmpty(a.PhoneNumber) && String.IsNullOrEmpty(phoneNumber)) || a.PhoneNumber == phoneNumber) && - ((String.IsNullOrEmpty(a.Email) && String.IsNullOrEmpty(email)) || a.Email == email) && - ((String.IsNullOrEmpty(a.FaxNumber) && String.IsNullOrEmpty(faxNumber)) || a.FaxNumber == faxNumber) && - ((String.IsNullOrEmpty(a.Company) && String.IsNullOrEmpty(company)) || a.Company == company) && - ((String.IsNullOrEmpty(a.Address1) && String.IsNullOrEmpty(address1)) || a.Address1 == address1) && - ((String.IsNullOrEmpty(a.Address2) && String.IsNullOrEmpty(address2)) || a.Address2 == address2) && - ((String.IsNullOrEmpty(a.City) && String.IsNullOrEmpty(city)) || a.City == city) && - ((a.StateProvinceId.IsNullOrDefault() && stateProvinceId.IsNullOrDefault()) || a.StateProvinceId == stateProvinceId) && - ((String.IsNullOrEmpty(a.ZipPostalCode) && String.IsNullOrEmpty(zipPostalCode)) || a.ZipPostalCode == zipPostalCode) && - ((a.CountryId.IsNullOrDefault() && countryId.IsNullOrDefault()) || a.CountryId == countryId)); + Func addressMatcher = (x) => + { + return x.Email.IsCaseInsensitiveEqual(email) + && x.LastName.IsCaseInsensitiveEqual(lastName) + && x.FirstName.IsCaseInsensitiveEqual(firstName) + && x.Address1.IsCaseInsensitiveEqual(address1) + && x.Address2.IsCaseInsensitiveEqual(address2) + && x.Company.IsCaseInsensitiveEqual(company) + && x.ZipPostalCode.IsCaseInsensitiveEqual(zipPostalCode) + && x.City.IsCaseInsensitiveEqual(city) + && x.PhoneNumber.IsCaseInsensitiveEqual(phoneNumber) + && x.FaxNumber.IsCaseInsensitiveEqual(faxNumber) + && x.StateProvinceId == stateProvinceId + && x.CountryId == countryId; + }; + + return source.FirstOrDefault(addressMatcher); } /// Returns the full name of the address. diff --git a/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs b/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs index 4c73f2e7ed..8f23424525 100644 --- a/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs @@ -209,49 +209,54 @@ public virtual void SaveAttribute(BaseEntity entity, string key, TPro if (entity == null) throw new ArgumentNullException("entity"); - string keyGroup = entity.GetUnproxiedEntityType().Name; + SaveAttribute(entity.Id, key, entity.GetUnproxiedEntityType().Name, value, storeId); + } + + public virtual void SaveAttribute(int entityId, string key, string keyGroup, TPropType value, int storeId = 0) + { + Guard.ArgumentNotZero(entityId, "entityId"); - var props = GetAttributesForEntity(entity.Id, keyGroup) + var props = GetAttributesForEntity(entityId, keyGroup) .Where(x => x.StoreId == storeId) .ToList(); - var prop = props.FirstOrDefault(ga => - ga.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)); // should be culture invariant + var prop = props.FirstOrDefault(ga => + ga.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)); // should be culture invariant - string valueStr = value.Convert(); + string valueStr = value.Convert(); - if (prop != null) - { - if (string.IsNullOrWhiteSpace(valueStr)) - { - //delete - DeleteAttribute(prop); - } - else - { - //update - prop.Value = valueStr; - UpdateAttribute(prop); - } - } - else - { - if (!string.IsNullOrWhiteSpace(valueStr)) - { - //insert - prop = new GenericAttribute() - { - EntityId = entity.Id, - Key = key, - KeyGroup = keyGroup, - Value = valueStr, + if (prop != null) + { + if (string.IsNullOrWhiteSpace(valueStr)) + { + // delete + DeleteAttribute(prop); + } + else + { + // update + prop.Value = valueStr; + UpdateAttribute(prop); + } + } + else + { + if (!string.IsNullOrWhiteSpace(valueStr)) + { + // insert + prop = new GenericAttribute + { + EntityId = entityId, + Key = key, + KeyGroup = keyGroup, + Value = valueStr, StoreId = storeId - }; - InsertAttribute(prop); - } - } - } + }; + InsertAttribute(prop); + } + } + } - #endregion - } + #endregion + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Common/IGenericAttributeService.cs b/src/Libraries/SmartStore.Services/Common/IGenericAttributeService.cs index cb837a6f74..c584c4ad4c 100644 --- a/src/Libraries/SmartStore.Services/Common/IGenericAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Common/IGenericAttributeService.cs @@ -69,5 +69,16 @@ public partial interface IGenericAttributeService /// Value /// Store identifier; pass 0 if this attribute will be available for all stores void SaveAttribute(BaseEntity entity, string key, TPropType value, int storeId = 0); - } + + /// + /// Save attribute value + /// + /// Property type + /// Entity identifier + /// The key + /// The key group + /// Property type + /// Store identifier; pass 0 if this attribute will be available for all stores + void SaveAttribute(int entityId, string key, string keyGroup, TPropType value, int storeId = 0); + } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Common/IUserAgent.cs b/src/Libraries/SmartStore.Services/Common/IUserAgent.cs index c6530bda07..a232323b07 100644 --- a/src/Libraries/SmartStore.Services/Common/IUserAgent.cs +++ b/src/Libraries/SmartStore.Services/Common/IUserAgent.cs @@ -67,6 +67,13 @@ private static string FormatVersionString(params string[] parts) public sealed class UserAgentInfo { + private static readonly HashSet s_Bots = new HashSet(StringComparer.InvariantCultureIgnoreCase) + { + "BingPreview" + }; + + private bool? _isBot; + public UserAgentInfo(string family, string major, string minor, string patch) { this.Family = family; @@ -83,6 +90,17 @@ public override string ToString() public string Major { get; private set; } public string Minor { get; private set; } public string Patch { get; private set; } + public bool IsBot + { + get + { + if (!_isBot.HasValue) + { + _isBot = s_Bots.Contains(Family); + } + return _isBot.Value; + } + } } internal static class VersionString diff --git a/src/Libraries/SmartStore.Services/Common/UAParserUserAgent.cs b/src/Libraries/SmartStore.Services/Common/UAParserUserAgent.cs index f0726c906c..a6b8e24305 100644 --- a/src/Libraries/SmartStore.Services/Common/UAParserUserAgent.cs +++ b/src/Libraries/SmartStore.Services/Common/UAParserUserAgent.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Text.RegularExpressions; using System.Web; +using SmartStore.Utilities; using uap = UAParser; namespace SmartStore.Services.Common @@ -14,7 +15,7 @@ public class UAParserUserAgent : IUserAgent # region Mobile UAs, OS & Devices - private static readonly HashSet s_MobileOS = new HashSet + private static readonly HashSet s_MobileOS = new HashSet(StringComparer.InvariantCultureIgnoreCase) { "Android", "iOS", @@ -32,8 +33,10 @@ public class UAParserUserAgent : IUserAgent "Maemo" }; - private static readonly HashSet s_MobileBrowsers = new HashSet + private static readonly HashSet s_MobileBrowsers = new HashSet(StringComparer.InvariantCultureIgnoreCase) { + "Googlebot-Mobile", + "Baiduspider-mobile", "Android", "Firefox Mobile", "Opera Mobile", @@ -80,7 +83,7 @@ public class UAParserUserAgent : IUserAgent "Skyfire" }; - private static readonly HashSet s_MobileDevices = new HashSet + private static readonly HashSet s_MobileDevices = new HashSet(StringComparer.InvariantCultureIgnoreCase) { "BlackBerry", "MI PAD", @@ -116,7 +119,8 @@ public class UAParserUserAgent : IUserAgent static UAParserUserAgent() { - s_uap = uap.Parser.GetDefault(); + //s_uap = uap.Parser.GetDefault(); + s_uap = uap.Parser.FromYamlFile(CommonHelper.MapPath("~/App_Data/ua-parser.regexes.yaml")); } public UAParserUserAgent(HttpContextBase httpContext) @@ -176,7 +180,7 @@ public virtual DeviceInfo Device if (_device == null) { var tmp = s_uap.ParseDevice(this.RawValue); - _device = new DeviceInfo(tmp.Family, tmp.IsSpider); + _device = new DeviceInfo(tmp.Family, tmp.IsSpider()); } return _device; } @@ -201,7 +205,8 @@ public virtual bool IsBot { if (!_isBot.HasValue) { - _isBot = _httpContext.Request.Browser.Crawler || this.Device.IsBot; + // empty useragent > bad bot! + _isBot = this.RawValue.IsEmpty() || _httpContext.Request.Browser.Crawler || this.Device.IsBot || this.UserAgent.IsBot; } return _isBot.Value; } @@ -247,7 +252,14 @@ public virtual bool IsPdfConverter return _isPdfConverter.Value; } } + } + internal static class DeviceExtensions + { + internal static bool IsSpider(this UAParser.Device device) + { + return device.Family.Equals("Spider", StringComparison.InvariantCultureIgnoreCase); + } } } diff --git a/src/Libraries/SmartStore.Services/CommonServices.cs b/src/Libraries/SmartStore.Services/CommonServices.cs index 86742ade49..0d6b989990 100644 --- a/src/Libraries/SmartStore.Services/CommonServices.cs +++ b/src/Libraries/SmartStore.Services/CommonServices.cs @@ -10,11 +10,14 @@ using SmartStore.Services.Security; using SmartStore.Services.Configuration; using SmartStore.Services.Stores; +using Autofac; +using SmartStore.Services.Helpers; namespace SmartStore.Services { public class CommonServices : ICommonServices { + private readonly IComponentContext _container; private readonly Lazy _cache; private readonly Lazy _dbContext; private readonly Lazy _storeContext; @@ -27,9 +30,11 @@ public class CommonServices : ICommonServices private readonly Lazy _permissions; private readonly Lazy _settings; private readonly Lazy _storeService; - + private readonly Lazy _dateTimeHelper; + public CommonServices( - Func> cache, + IComponentContext container, + Func> cache, Lazy dbContext, Lazy storeContext, Lazy webHelper, @@ -40,8 +45,10 @@ public CommonServices( Lazy notifier, Lazy permissions, Lazy settings, - Lazy storeService) + Lazy storeService, + Lazy dateTimeHelper) { + this._container = container; this._cache = cache("static"); this._dbContext = dbContext; this._storeContext = storeContext; @@ -54,8 +61,17 @@ public CommonServices( this._permissions = permissions; this._settings = settings; this._storeService = storeService; + this._dateTimeHelper = dateTimeHelper; + } + + public IComponentContext Container + { + get + { + return _container; + } } - + public ICacheManager Cache { get @@ -152,5 +168,13 @@ public IStoreService StoreService return _storeService.Value; } } + + public IDateTimeHelper DateTimeHelper + { + get + { + return _dateTimeHelper.Value; + } + } } } diff --git a/src/Libraries/SmartStore.Services/Configuration/SettingService.cs b/src/Libraries/SmartStore.Services/Configuration/SettingService.cs index 9bdcecbfcb..aa688a4e96 100644 --- a/src/Libraries/SmartStore.Services/Configuration/SettingService.cs +++ b/src/Libraries/SmartStore.Services/Configuration/SettingService.cs @@ -2,19 +2,15 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; -using SmartStore.Core; using SmartStore.Core.Caching; using SmartStore.Core.Configuration; using SmartStore.Core.Data; using SmartStore.Core.Domain.Configuration; -using SmartStore.Core.Infrastructure; using SmartStore.Core.Events; -using Fasterflect; using System.Linq.Expressions; using System.Reflection; -using SmartStore.Core.Plugins; -using System.ComponentModel; -using SmartStore.Utilities; +using SmartStore.ComponentModel; +using System.Collections; namespace SmartStore.Services.Configuration { @@ -163,6 +159,7 @@ private T LoadSettingsJson(int storeId = 0) { JsonConvert.PopulateObject(rawSetting, settings); } + return settings; } @@ -239,6 +236,7 @@ public virtual Setting GetSettingById(int settingId) if (setting != null) return setting.Value.Convert(); } + return defaultValue; } @@ -303,52 +301,66 @@ public virtual bool SettingExists(T settings, var settings = Activator.CreateInstance(); - foreach (var prop in typeof(T).GetProperties()) + foreach (var fastProp in FastProperty.GetProperties(typeof(T)).Values) { + var prop = fastProp.Property; + // get properties we can read and write to - if (!prop.CanRead || !prop.CanWrite) + if (!prop.CanWrite) continue; var key = typeof(T).Name + "." + prop.Name; - //load by store + // load by store string setting = GetSettingByKey(key, storeId: storeId, loadSharedValueIfNotFound: true); - if (setting == null) - { - if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>)) - { - // convenience: don't return null for simple list types - var listArg = prop.PropertyType.GetGenericArguments()[0]; - object list = null; - - if (listArg == typeof(int)) - list = new List(); - else if (listArg == typeof(decimal)) - list = new List(); - else if (listArg == typeof(string)) - list = new List(); - - if (list != null) + if (setting == null) + { + if (fastProp.IsSequenceType) + { + if ((fastProp.GetValue(settings) as IEnumerable) != null) { - prop.SetValue(settings, list, null); + // Instance of IEnumerable<> was already created, most likely in the constructor of the settings concrete class. + // In this case we shouldn't let the EnumerableConverter create a new instance but keep this one. + continue; } } + else + { + #region Obsolete ('EnumerableConverter' can handle this case now) + //if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>)) + //{ + // // convenience: don't return null for simple list types + // var listArg = prop.PropertyType.GetGenericArguments()[0]; + // object list = null; + + // if (listArg == typeof(int)) + // list = new List(); + // else if (listArg == typeof(decimal)) + // list = new List(); + // else if (listArg == typeof(string)) + // list = new List(); + + // if (list != null) + // { + // fastProp.SetValue(settings, list); + // } + //} + #endregion + + continue; + } - continue; - } + } - var converter = CommonHelper.GetTypeConverter(prop.PropertyType); + var converter = TypeConverterFactory.GetConverter(prop.PropertyType); if (converter == null || !converter.CanConvertFrom(typeof(string))) continue; - if (!converter.IsValid(setting)) - continue; - - object value = converter.ConvertFromInvariantString(setting); + object value = converter.ConvertFrom(setting); //set property - prop.SetValue(settings, value, null); + fastProp.SetValue(settings, value); } return settings; @@ -367,7 +379,7 @@ public virtual void SetSetting(string key, T value, int storeId = 0, bool cle Guard.ArgumentNotEmpty(() => key); key = key.Trim().ToLowerInvariant(); - string valueStr = CommonHelper.GetTypeConverter(typeof(T)).ConvertToInvariantString(value); + var str = value.Convert(); var allSettings = GetAllSettingsCached(); var settingForCaching = allSettings.ContainsKey(key) ? @@ -377,7 +389,7 @@ public virtual void SetSetting(string key, T value, int storeId = 0, bool cle { //update var setting = GetSettingById(settingForCaching.Id); - setting.Value = valueStr; + setting.Value = str; UpdateSetting(setting, clearCache); } else @@ -386,7 +398,7 @@ public virtual void SetSetting(string key, T value, int storeId = 0, bool cle var setting = new Setting { Name = key, - Value = valueStr, + Value = str, StoreId = storeId }; InsertSetting(setting, clearCache); @@ -410,18 +422,19 @@ public virtual void SetSetting(string key, T value, int storeId = 0, bool cle /* We do not clear cache after each setting update. * This behavior can increase performance because cached settings will not be cleared * and loaded from database after each update */ - foreach (var prop in typeof(T).GetProperties()) + foreach (var prop in FastProperty.GetProperties(typeof(T)).Values) { // get properties we can read and write to - if (!prop.CanRead || !prop.CanWrite) + if (!prop.IsPublicSettable) continue; - if (!CommonHelper.GetTypeConverter(prop.PropertyType).CanConvertFrom(typeof(string))) + var converter = TypeConverterFactory.GetConverter(prop.Property.PropertyType); + if (converter == null || !converter.CanConvertFrom(typeof(string))) continue; string key = typeof(T).Name + "." + prop.Name; - //Duck typing is not supported in C#. That's why we're using dynamic type - dynamic value = settings.TryGetPropertyValue(prop.Name); + // Duck typing is not supported in C#. That's why we're using dynamic type + dynamic value = prop.GetValue(settings); SetSetting(key, value ?? "", storeId, false); } @@ -459,8 +472,9 @@ public virtual void SaveSetting(T settings, } string key = typeof(T).Name + "." + propInfo.Name; - //Duck typing is not supported in C#. That's why we're using dynamic type - dynamic value = settings.TryGetPropertyValue(propInfo.Name); + // Duck typing is not supported in C#. That's why we're using dynamic type + var fastProp = FastProperty.GetProperty(propInfo, PropertyCachingStrategy.EagerCached); + dynamic value = fastProp.GetValue(settings); SetSetting(key, value ?? "", storeId, false); } diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerExtentions.cs b/src/Libraries/SmartStore.Services/Customers/CustomerExtentions.cs index 719f144ed5..afc1acdbe0 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerExtentions.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerExtentions.cs @@ -53,7 +53,7 @@ public static bool IsBackgroundTaskAccount(this Customer customer) } /// - /// Gets a value indicating whether customer a search engine + /// Gets a value indicating whether customer is a search engine /// /// Customer /// Result @@ -84,13 +84,13 @@ public static bool IsPdfConverter(this Customer customer) return result; } - /// - /// Gets a value indicating whether customer is administrator - /// - /// Customer - /// A value indicating whether we should look only in active customer roles - /// Result - public static bool IsAdmin(this Customer customer, bool onlyActiveCustomerRoles = true) + /// + /// Gets a value indicating whether customer is administrator + /// + /// Customer + /// A value indicating whether we should look only in active customer roles + /// Result + public static bool IsAdmin(this Customer customer, bool onlyActiveCustomerRoles = true) { return IsInCustomerRole(customer, SystemCustomerRoleNames.Administrators, onlyActiveCustomerRoles); } @@ -287,14 +287,15 @@ public static int CountProductsInCart(this Customer customer, ShoppingCartType c return count; } - public static List GetCartItems(this Customer customer, ShoppingCartType cartType, int? storeId = null, bool orderById = false) + + public static List GetCartItems(this Customer customer, ShoppingCartType cartType, int? storeId = null) { var rawItems = customer.ShoppingCartItems.Filter(cartType, storeId); - if (orderById) - rawItems = rawItems.OrderByDescending(x => x.Id); - - var organizedItems = rawItems.ToList().Organize(); + var organizedItems = rawItems + .OrderByDescending(x => x.Id) + .ToList() + .Organize(); return organizedItems.ToList(); } diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationService.cs b/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationService.cs index 1615a2ce96..49fb4587aa 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationService.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationService.cs @@ -3,23 +3,22 @@ using SmartStore.Core; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Events; -using SmartStore.Services.Localization; +using SmartStore.Core.Localization; using SmartStore.Services.Messages; using SmartStore.Services.Security; namespace SmartStore.Services.Customers { - /// - /// Customer registration service - /// - public partial class CustomerRegistrationService : ICustomerRegistrationService + /// + /// Customer registration service + /// + public partial class CustomerRegistrationService : ICustomerRegistrationService { #region Fields private readonly ICustomerService _customerService; private readonly IEncryptionService _encryptionService; private readonly INewsLetterSubscriptionService _newsLetterSubscriptionService; - private readonly ILocalizationService _localizationService; private readonly RewardPointsSettings _rewardPointsSettings; private readonly CustomerSettings _customerSettings; private readonly IStoreContext _storeContext; @@ -35,31 +34,33 @@ public partial class CustomerRegistrationService : ICustomerRegistrationService public CustomerRegistrationService(ICustomerService customerService, IEncryptionService encryptionService, INewsLetterSubscriptionService newsLetterSubscriptionService, - ILocalizationService localizationService, RewardPointsSettings rewardPointsSettings, CustomerSettings customerSettings, IStoreContext storeContext, IEventPublisher eventPublisher) { this._customerService = customerService; this._encryptionService = encryptionService; this._newsLetterSubscriptionService = newsLetterSubscriptionService; - this._localizationService = localizationService; this._rewardPointsSettings = rewardPointsSettings; this._customerSettings = customerSettings; this._storeContext = storeContext; this._eventPublisher = eventPublisher; - } - #endregion + T = NullLocalizer.Instance; + } - #region Methods + #endregion - /// - /// Validate customer - /// - /// Username or email - /// Password - /// Result - public virtual bool ValidateCustomer(string usernameOrEmail, string password) + public Localizer T { get; set; } + + #region Methods + + /// + /// Validate customer + /// + /// Username or email + /// Password + /// Result + public virtual bool ValidateCustomer(string usernameOrEmail, string password) { Customer customer = null; if (_customerSettings.UsernamesEnabled) @@ -112,48 +113,46 @@ public virtual bool ValidateCustomer(string usernameOrEmail, string password) /// Result public virtual CustomerRegistrationResult RegisterCustomer(CustomerRegistrationRequest request) { - if (request == null) - throw new ArgumentNullException("request"); - - if (request.Customer == null) - throw new ArgumentException("Can't load current customer"); + Guard.ArgumentNotNull(() => request); + Guard.ArgumentNotNull(() => request.Customer); var result = new CustomerRegistrationResult(); + if (request.Customer.IsSearchEngineAccount()) { - result.AddError("Search engine can't be registered"); + result.AddError(T("Account.Register.Errors.CannotRegisterSearchEngine")); return result; } if (request.Customer.IsBackgroundTaskAccount()) { - result.AddError("Background task account can't be registered"); + result.AddError(T("Account.Register.Errors.CannotRegisterTaskAccount")); return result; } if (request.Customer.IsRegistered()) { - result.AddError("Current customer is already registered"); + result.AddError(T("Account.Register.Errors.AlreadyRegistered")); return result; } if (String.IsNullOrEmpty(request.Email)) { - result.AddError(_localizationService.GetResource("Account.Register.Errors.EmailIsNotProvided")); + result.AddError(T("Account.Register.Errors.EmailIsNotProvided")); return result; } if (!request.Email.IsEmail()) { - result.AddError(_localizationService.GetResource("Common.WrongEmail")); + result.AddError(T("Common.WrongEmail")); return result; } if (String.IsNullOrWhiteSpace(request.Password)) { - result.AddError(_localizationService.GetResource("Account.Register.Errors.PasswordIsNotProvided")); + result.AddError(T("Account.Register.Errors.PasswordIsNotProvided")); return result; } if (_customerSettings.UsernamesEnabled) { if (String.IsNullOrEmpty(request.Username)) { - result.AddError(_localizationService.GetResource("Account.Register.Errors.UsernameIsNotProvided")); + result.AddError(T("Account.Register.Errors.UsernameIsNotProvided")); return result; } } @@ -161,14 +160,14 @@ public virtual CustomerRegistrationResult RegisterCustomer(CustomerRegistrationR //validate unique user if (_customerService.GetCustomerByEmail(request.Email) != null) { - result.AddError(_localizationService.GetResource("Account.Register.Errors.EmailAlreadyExists")); + result.AddError(T("Account.Register.Errors.EmailAlreadyExists")); return result; } if (_customerSettings.UsernamesEnabled) { if (_customerService.GetCustomerByUsername(request.Username) != null) { - result.AddError(_localizationService.GetResource("Account.Register.Errors.UsernameAlreadyExists")); + result.AddError(T("Account.Register.Errors.UsernameAlreadyExists")); return result; } } @@ -180,45 +179,52 @@ public virtual CustomerRegistrationResult RegisterCustomer(CustomerRegistrationR switch (request.PasswordFormat) { - case PasswordFormat.Clear: - { - request.Customer.Password = request.Password; - } - break; - case PasswordFormat.Encrypted: - { - request.Customer.Password = _encryptionService.EncryptText(request.Password); - } - break; - case PasswordFormat.Hashed: - { - string saltKey = _encryptionService.CreateSaltKey(5); - request.Customer.PasswordSalt = saltKey; - request.Customer.Password = _encryptionService.CreatePasswordHash(request.Password, saltKey, _customerSettings.HashedPasswordFormat); - } - break; - default: - break; + case PasswordFormat.Clear: + request.Customer.Password = request.Password; + break; + case PasswordFormat.Encrypted: + request.Customer.Password = _encryptionService.EncryptText(request.Password); + break; + case PasswordFormat.Hashed: + string saltKey = _encryptionService.CreateSaltKey(5); + request.Customer.PasswordSalt = saltKey; + request.Customer.Password = _encryptionService.CreatePasswordHash(request.Password, saltKey, _customerSettings.HashedPasswordFormat); + break; } request.Customer.Active = request.IsApproved; - - //add to 'Registered' role - var registeredRole = _customerService.GetCustomerRoleBySystemName(SystemCustomerRoleNames.Registered); - if (registeredRole == null) - throw new SmartException("'Registered' role could not be loaded"); + + if (_customerSettings.RegisterCustomerRoleId != 0) + { + var customerRole = _customerService.GetCustomerRoleById(_customerSettings.RegisterCustomerRoleId); + request.Customer.CustomerRoles.Add(customerRole); + } + + //add to 'Registered' role + var registeredRole = _customerService.GetCustomerRoleBySystemName(SystemCustomerRoleNames.Registered); + if (registeredRole == null) + { + throw new SmartException(T("Admin.Customers.CustomerRoles.CannotFoundRole", "Registered")); + } + request.Customer.CustomerRoles.Add(registeredRole); + //remove from 'Guests' role var guestRole = request.Customer.CustomerRoles.FirstOrDefault(cr => cr.SystemName == SystemCustomerRoleNames.Guests); - if (guestRole != null) - request.Customer.CustomerRoles.Remove(guestRole); + if (guestRole != null) + { + request.Customer.CustomerRoles.Remove(guestRole); + } - //Add reward points for customer registration (if enabled) - if (_rewardPointsSettings.Enabled && _rewardPointsSettings.PointsForRegistration > 0) - request.Customer.AddRewardPointsHistoryEntry(_rewardPointsSettings.PointsForRegistration, _localizationService.GetResource("RewardPoints.Message.RegisteredAsCustomer")); + //Add reward points for customer registration (if enabled) + if (_rewardPointsSettings.Enabled && _rewardPointsSettings.PointsForRegistration > 0) + { + request.Customer.AddRewardPointsHistoryEntry(_rewardPointsSettings.PointsForRegistration, T("RewardPoints.Message.RegisteredAsCustomer")); + } _customerService.UpdateCustomer(request.Customer); - _eventPublisher.Publish(new CustomerRegisteredEvent{ Customer = request.Customer }); + _eventPublisher.Publish(new CustomerRegisteredEvent { Customer = request.Customer }); + return result; } @@ -235,19 +241,19 @@ public virtual PasswordChangeResult ChangePassword(ChangePasswordRequest request var result = new PasswordChangeResult(); if (String.IsNullOrWhiteSpace(request.Email)) { - result.AddError(_localizationService.GetResource("Account.ChangePassword.Errors.EmailIsNotProvided")); + result.AddError(T("Account.ChangePassword.Errors.EmailIsNotProvided")); return result; } if (String.IsNullOrWhiteSpace(request.NewPassword)) { - result.AddError(_localizationService.GetResource("Account.ChangePassword.Errors.PasswordIsNotProvided")); + result.AddError(T("Account.ChangePassword.Errors.PasswordIsNotProvided")); return result; } var customer = _customerService.GetCustomerByEmail(request.Email); if (customer == null) { - result.AddError(_localizationService.GetResource("Account.ChangePassword.Errors.EmailNotFound")); + result.AddError(T("Account.ChangePassword.Errors.EmailNotFound")); return result; } @@ -272,7 +278,7 @@ public virtual PasswordChangeResult ChangePassword(ChangePasswordRequest request bool oldPasswordIsValid = oldPwd == customer.Password; if (!oldPasswordIsValid) - result.AddError(_localizationService.GetResource("Account.ChangePassword.Errors.OldPasswordDoesntMatch")); + result.AddError(T("Account.ChangePassword.Errors.OldPasswordDoesntMatch")); if (oldPasswordIsValid) requestIsValid = true; @@ -327,14 +333,14 @@ public virtual void SetEmail(Customer customer, string newEmail) string oldEmail = customer.Email; if (!newEmail.IsEmail()) - throw new SmartException(_localizationService.GetResource("Account.EmailUsernameErrors.NewEmailIsNotValid")); + throw new SmartException(T("Account.EmailUsernameErrors.NewEmailIsNotValid")); if (newEmail.Length > 100) - throw new SmartException(_localizationService.GetResource("Account.EmailUsernameErrors.EmailTooLong")); + throw new SmartException(T("Account.EmailUsernameErrors.EmailTooLong")); var customer2 = _customerService.GetCustomerByEmail(newEmail); if (customer2 != null && customer.Id != customer2.Id) - throw new SmartException(_localizationService.GetResource("Account.EmailUsernameErrors.EmailAlreadyExists")); + throw new SmartException(T("Account.EmailUsernameErrors.EmailAlreadyExists")); customer.Email = newEmail; _customerService.UpdateCustomer(customer); @@ -370,11 +376,11 @@ public virtual void SetUsername(Customer customer, string newUsername) newUsername = newUsername.Trim(); if (newUsername.Length > 100) - throw new SmartException(_localizationService.GetResource("Account.EmailUsernameErrors.UsernameTooLong")); + throw new SmartException(T("Account.EmailUsernameErrors.UsernameTooLong")); var user2 = _customerService.GetCustomerByUsername(newUsername); if (user2 != null && customer.Id != user2.Id) - throw new SmartException(_localizationService.GetResource("Account.EmailUsernameErrors.UsernameAlreadyExists")); + throw new SmartException(T("Account.EmailUsernameErrors.UsernameAlreadyExists")); customer.Username = newUsername; _customerService.UpdateCustomer(customer); diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerService.cs b/src/Libraries/SmartStore.Services/Customers/CustomerService.cs index e27636dc9d..d48fe10d6c 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerService.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerService.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using System.Linq.Expressions; +using System.Web; using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Caching; @@ -16,19 +17,20 @@ using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Events; using SmartStore.Core.Localization; +using SmartStore.Core.Fakes; using SmartStore.Services.Common; using SmartStore.Services.Localization; namespace SmartStore.Services.Customers { - /// - /// Customer service - /// - public partial class CustomerService : ICustomerService + /// + /// Customer service + /// + public partial class CustomerService : ICustomerService { - #region Constants + #region Constants - private const string CUSTOMERROLES_ALL_KEY = "SmartStore.customerrole.all-{0}"; + private const string CUSTOMERROLES_ALL_KEY = "SmartStore.customerrole.all-{0}"; private const string CUSTOMERROLES_BY_SYSTEMNAME_KEY = "SmartStore.customerrole.systemname-{0}"; private const string CUSTOMERROLES_PATTERN_KEY = "SmartStore.customerrole."; @@ -41,34 +43,38 @@ public partial class CustomerService : ICustomerService private readonly IRepository _gaRepository; private readonly IRepository _rewardPointsHistoryRepository; private readonly IGenericAttributeService _genericAttributeService; - private readonly ICacheManager _cacheManager; - private readonly IEventPublisher _eventPublisher; + private readonly ICommonServices _services; + private readonly ICacheManager _cache; private readonly RewardPointsSettings _rewardPointsSettings; - private readonly IWebHelper _webHelper; + private readonly HttpContextBase _httpContext; + private readonly IUserAgent _userAgent; - #endregion + #endregion - #region Ctor + #region Ctor - public CustomerService(ICacheManager cacheManager, + public CustomerService( IRepository customerRepository, IRepository customerRoleRepository, IRepository gaRepository, IRepository rewardPointsHistoryRepository, IGenericAttributeService genericAttributeService, - IEventPublisher eventPublisher, + ICommonServices services, + ICacheManager cache, RewardPointsSettings rewardPointsSettings, - IWebHelper webHelper) + HttpContextBase httpContext, + IUserAgent userAgent) { - this._cacheManager = cacheManager; this._customerRepository = customerRepository; this._customerRoleRepository = customerRoleRepository; this._gaRepository = gaRepository; this._rewardPointsHistoryRepository = rewardPointsHistoryRepository; this._genericAttributeService = genericAttributeService; - this._eventPublisher = eventPublisher; + this._services = services; + this._cache = cache; this._rewardPointsSettings = rewardPointsSettings; - this._webHelper = webHelper; + this._httpContext = httpContext; + this._userAgent = userAgent; T = NullLocalizer.Instance; } @@ -294,6 +300,11 @@ where customerIds.Contains(c.Id) return sortedCustomers; } + public virtual IList GetSystemAccountCustomers() + { + return _customerRepository.Table.Where(x => x.IsSystemAccount).ToList(); + } + public virtual Customer GetCustomerByGuid(Guid customerGuid) { if (customerGuid == Guid.Empty) @@ -330,6 +341,7 @@ orderby c.Id where c.SystemName == systemName select c; var customer = query.FirstOrDefault(); + return customer; } @@ -347,41 +359,62 @@ orderby c.Id return customer; } - public virtual Customer InsertGuestCustomer() + public virtual Customer InsertGuestCustomer(Guid? customerGuid = null) { - var ip = _webHelper.GetCurrentIpAddress(); - - if (ip.HasValue()) - { - var existingCustomer = _customerRepository.Table - .Where(x => !x.Deleted && x.Active && !x.IsSystemAccount && x.LastIpAddress == ip && x.CustomerRoles.Any(y => y.Active && y.SystemName == SystemCustomerRoleNames.Guests)) - .FirstOrDefault(); - - if (existingCustomer != null) - { - return existingCustomer; - } - } - - var customer = new Customer + var customer = new Customer { - CustomerGuid = Guid.NewGuid(), + CustomerGuid = customerGuid ?? Guid.NewGuid(), Active = true, CreatedOnUtc = DateTime.UtcNow, LastActivityDateUtc = DateTime.UtcNow, }; - //add to 'Guests' role + // add to 'Guests' role var guestRole = GetCustomerRoleBySystemName(SystemCustomerRoleNames.Guests); if (guestRole == null) throw new SmartException("'Guests' role could not be loaded"); customer.CustomerRoles.Add(guestRole); - _customerRepository.Insert(customer); + _customerRepository.Insert(customer); + + var clientIdent = _services.WebHelper.GetClientIdent(); + if (clientIdent.HasValue()) + { + //_genericAttributeService.SaveAttribute(customer, "ClientIdent", clientIdent); + } return customer; } + + public virtual Customer FindGuestCustomerByClientIdent(string clientIdent = null, int maxAgeSeconds = 60) + { + if (_httpContext.IsFakeContext() || _userAgent.IsBot || _userAgent.IsPdfConverter) + { + return null; + } + + clientIdent = clientIdent.NullEmpty() ?? _services.WebHelper.GetClientIdent(); + if (clientIdent.IsEmpty()) + { + return null; + } + + var dateFrom = DateTime.UtcNow.AddSeconds(maxAgeSeconds * -1); + + var query = from a in _gaRepository.TableUntracked + join c in _customerRepository.Table on a.EntityId equals c.Id into Customers + from c in Customers.DefaultIfEmpty() + where c.LastActivityDateUtc >= dateFrom + && c.Username == null + && c.Email == null + && a.KeyGroup == "Customer" + && a.Key == "ClientIdent" + && a.Value == clientIdent + select c; + + return query.FirstOrDefault(); + } public virtual void InsertCustomer(Customer customer) { @@ -390,8 +423,7 @@ public virtual void InsertCustomer(Customer customer) _customerRepository.Insert(customer); - //event notification - _eventPublisher.EntityInserted(customer); + _services.EventPublisher.EntityInserted(customer); } public virtual void UpdateCustomer(Customer customer) @@ -401,8 +433,7 @@ public virtual void UpdateCustomer(Customer customer) _customerRepository.Update(customer); - //event notification - _eventPublisher.EntityUpdated(customer); + _services.EventPublisher.EntityUpdated(customer); } public virtual void ResetCheckoutData(Customer customer, int storeId, @@ -581,10 +612,9 @@ public virtual void DeleteCustomerRole(CustomerRole customerRole) _customerRoleRepository.Delete(customerRole); - _cacheManager.RemoveByPattern(CUSTOMERROLES_PATTERN_KEY); + _cache.RemoveByPattern(CUSTOMERROLES_PATTERN_KEY); - //event notification - _eventPublisher.EntityDeleted(customerRole); + _services.EventPublisher.EntityDeleted(customerRole); } public virtual CustomerRole GetCustomerRoleById(int customerRoleId) @@ -601,7 +631,7 @@ public virtual CustomerRole GetCustomerRoleBySystemName(string systemName) return null; string key = string.Format(CUSTOMERROLES_BY_SYSTEMNAME_KEY, systemName); - return _cacheManager.Get(key, () => + return _cache.Get(key, () => { var query = from cr in _customerRoleRepository.Table orderby cr.Id @@ -615,7 +645,7 @@ orderby cr.Id public virtual IList GetAllCustomerRoles(bool showHidden = false) { string key = string.Format(CUSTOMERROLES_ALL_KEY, showHidden); - return _cacheManager.Get(key, () => + return _cache.Get(key, () => { var query = from cr in _customerRoleRepository.Table orderby cr.Name @@ -633,10 +663,9 @@ public virtual void InsertCustomerRole(CustomerRole customerRole) _customerRoleRepository.Insert(customerRole); - _cacheManager.RemoveByPattern(CUSTOMERROLES_PATTERN_KEY); + _cache.RemoveByPattern(CUSTOMERROLES_PATTERN_KEY); - //event notification - _eventPublisher.EntityInserted(customerRole); + _services.EventPublisher.EntityInserted(customerRole); } public virtual void UpdateCustomerRole(CustomerRole customerRole) @@ -646,10 +675,9 @@ public virtual void UpdateCustomerRole(CustomerRole customerRole) _customerRoleRepository.Update(customerRole); - _cacheManager.RemoveByPattern(CUSTOMERROLES_PATTERN_KEY); + _cache.RemoveByPattern(CUSTOMERROLES_PATTERN_KEY); - //event notification - _eventPublisher.EntityUpdated(customerRole); + _services.EventPublisher.EntityUpdated(customerRole); } #endregion diff --git a/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs b/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs index ec4b470074..57a2c6f8c9 100644 --- a/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs +++ b/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs @@ -88,12 +88,18 @@ IPagedList GetOnlineCustomers(DateTime lastActivityFromUtc, /// Customers IList GetCustomersByIds(int[] customerIds); - /// - /// Gets a customer by GUID - /// - /// Customer GUID - /// A customer - Customer GetCustomerByGuid(Guid customerGuid); + /// + /// Get system account customers + /// + /// System account customers + IList GetSystemAccountCustomers(); + + /// + /// Gets a customer by GUID + /// + /// Customer GUID + /// A customer + Customer GetCustomerByGuid(Guid customerGuid); /// /// Get customer by email @@ -119,14 +125,26 @@ IPagedList GetOnlineCustomers(DateTime lastActivityFromUtc, /// /// Insert a guest customer /// + /// The customer GUID. Pass null to create a random one. /// Customer - Customer InsertGuestCustomer(); + Customer InsertGuestCustomer(Guid? customerGuid = null); - /// - /// Insert a customer - /// - /// Customer - void InsertCustomer(Customer customer); + /// + /// Tries to find a guest/anonymous customer record by client ident. This method should be called when an + /// anonymous visitor rejects cookies and therefore cannot be identified automatically. + /// + /// + /// The client ident string, which is a hashed combination of client IP address and user agent. + /// Call to obtain an ident string, or pass null to let this method obtain it automatically. + /// The max age of the newly created guest customer record. The shorter, the better (default is 1 min.) + /// The identified customer or null + Customer FindGuestCustomerByClientIdent(string clientIdent = null, int maxAgeSeconds = 60); + + /// + /// Insert a customer + /// + /// Customer + void InsertCustomer(Customer customer); /// /// Updates the customer diff --git a/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs b/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs new file mode 100644 index 0000000000..452f281fb8 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Customers/Importer/CustomerImporter.cs @@ -0,0 +1,653 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using SmartStore.Core.Async; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Domain.Forums; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.Events; +using SmartStore.Services.Affiliates; +using SmartStore.Services.Common; +using SmartStore.Services.DataExchange.Import; +using SmartStore.Services.Directory; +using SmartStore.Services.Helpers; +using SmartStore.Services.Media; +using SmartStore.Services.Security; +using SmartStore.Utilities; + +namespace SmartStore.Services.Customers.Importer +{ + public class CustomerImporter : EntityImporterBase + { + private const string _attributeKeyGroup = "Customer"; + + private readonly IRepository _customerRepository; + private readonly IRepository _pictureRepository; + private readonly ICommonServices _services; + private readonly IGenericAttributeService _genericAttributeService; + private readonly ICustomerService _customerService; + private readonly IPictureService _pictureService; + private readonly IAffiliateService _affiliateService; + private readonly ICountryService _countryService; + private readonly IStateProvinceService _stateProvinceService; + private readonly FileDownloadManager _fileDownloadManager; + private readonly CustomerSettings _customerSettings; + private readonly DateTimeSettings _dateTimeSettings; + private readonly ForumSettings _forumSettings; + + public CustomerImporter( + IRepository customerRepository, + IRepository pictureRepository, + ICommonServices services, + IGenericAttributeService genericAttributeService, + ICustomerService customerService, + IPictureService pictureService, + IAffiliateService affiliateService, + ICountryService countryService, + IStateProvinceService stateProvinceService, + FileDownloadManager fileDownloadManager, + CustomerSettings customerSettings, + DateTimeSettings dateTimeSettings, + ForumSettings forumSettings) + { + _customerRepository = customerRepository; + _pictureRepository = pictureRepository; + _services = services; + _genericAttributeService = genericAttributeService; + _customerService = customerService; + _pictureService = pictureService; + _affiliateService = affiliateService; + _countryService = countryService; + _stateProvinceService = stateProvinceService; + _fileDownloadManager = fileDownloadManager; + _customerSettings = customerSettings; + _dateTimeSettings = dateTimeSettings; + _forumSettings = forumSettings; + } + + + protected override void Import(ImportExecuteContext context) + { + var customer = _services.WorkContext.CurrentCustomer; + var allowManagingCustomerRoles = _services.Permissions.Authorize(StandardPermissionProvider.ManageCustomerRoles, customer); + + var allAffiliateIds = _affiliateService.GetAllAffiliates(true) + .Select(x => x.Id) + .ToList(); + + var allCountries = new Dictionary(); + foreach (var country in _countryService.GetAllCountries(true)) + { + if (!allCountries.ContainsKey(country.TwoLetterIsoCode)) + allCountries.Add(country.TwoLetterIsoCode, country.Id); + + if (!allCountries.ContainsKey(country.ThreeLetterIsoCode)) + allCountries.Add(country.ThreeLetterIsoCode, country.Id); + } + + var allStateProvinces = _stateProvinceService.GetAllStateProvinces(true) + .ToDictionarySafe(x => new Tuple(x.CountryId, x.Abbreviation), x => x.Id); + + var allCustomerNumbers = new HashSet( + _genericAttributeService.GetAttributes(SystemCustomerAttributeNames.CustomerNumber, _attributeKeyGroup).Select(x => x.Value), + StringComparer.OrdinalIgnoreCase); + + using (var scope = new DbContextScope(ctx: _services.DbContext, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) + { + var segmenter = context.DataSegmenter; + + Initialize(context); + + while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) + { + var batch = segmenter.GetCurrentBatch(); + + _customerRepository.Context.DetachAll(false); + + context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows); + + // =========================================================================== + // Process customers + // =========================================================================== + try + { + ProcessCustomers(context, batch, allAffiliateIds); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessCustomers"); + } + + // reduce batch to saved (valid) records. + // No need to perform import operations on errored records. + batch = batch.Where(x => x.Entity != null && !x.IsTransient).ToArray(); + + // update result object + context.Result.NewRecords += batch.Count(x => x.IsNew && !x.IsTransient); + context.Result.ModifiedRecords += batch.Count(x => !x.IsNew && !x.IsTransient); + + // =========================================================================== + // Process generic attributes + // =========================================================================== + try + { + ProcessGenericAttributes(context, batch, allCountries, allStateProvinces, allCustomerNumbers); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessGenericAttributes"); + } + + // =========================================================================== + // Process avatars + // =========================================================================== + if (_customerSettings.AllowCustomersToUploadAvatars) + { + try + { + ProcessAvatars(context, batch); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessAvatars"); + } + } + + // =========================================================================== + // Process addresses + // =========================================================================== + try + { + _services.DbContext.AutoDetectChangesEnabled = true; + ProcessAddresses(context, batch, allCountries, allStateProvinces); + } + catch (Exception exception) + { + context.Result.AddError(exception, segmenter.CurrentSegment, "ProcessAddresses"); + } + finally + { + _services.DbContext.AutoDetectChangesEnabled = false; + } + } + } + } + + protected virtual int ProcessCustomers( + ImportExecuteContext context, + IEnumerable> batch, + List allAffiliateIds) + { + _customerRepository.AutoCommitEnabled = true; + + Customer lastInserted = null; + Customer lastUpdated = null; + var currentCustomer = _services.WorkContext.CurrentCustomer; + + var guestRole = _customerService.GetCustomerRoleBySystemName(SystemCustomerRoleNames.Guests); + var registeredRole = _customerService.GetCustomerRoleBySystemName(SystemCustomerRoleNames.Registered); + var forumModeratorRole = _customerService.GetCustomerRoleBySystemName(SystemCustomerRoleNames.ForumModerators); + + var customerQuery = _customerRepository.Table.Expand(x => x.Addresses); + + foreach (var row in batch) + { + Customer customer = null; + var id = row.GetDataValue("Id"); + var email = row.GetDataValue("Email"); + + foreach (var keyName in context.KeyFieldNames) + { + switch (keyName) + { + case "Id": + if (id != 0) + { + customer = customerQuery.FirstOrDefault(x => x.Id == id); + } + break; + case "CustomerGuid": + var customerGuid = row.GetDataValue("CustomerGuid"); + if (customerGuid.HasValue()) + { + var guid = new Guid(customerGuid); + customer = customerQuery.FirstOrDefault(x => x.CustomerGuid == guid); + } + break; + case "Email": + if (email.HasValue()) + { + customer = customerQuery.FirstOrDefault(x => x.Email == email); + } + break; + case "Username": + var userName = row.GetDataValue("Username"); + if (userName.HasValue()) + { + customer = customerQuery.FirstOrDefault(x => x.Username == userName); + } + break; + } + + if (customer != null) + break; + } + + if (customer == null) + { + if (context.UpdateOnly) + { + ++context.Result.SkippedRecords; + continue; + } + + customer = new Customer + { + CustomerGuid = new Guid(), + AffiliateId = 0, + Active = true + }; + } + else + { + _customerRepository.Context.LoadCollection(customer, (Customer x) => x.CustomerRoles); + } + + var isGuest = row.GetDataValue("IsGuest"); + var isRegistered = row.GetDataValue("IsRegistered"); + var isAdmin = row.GetDataValue("IsAdministrator"); + var isForumModerator = row.GetDataValue("IsForumModerator"); + var affiliateId = row.GetDataValue("AffiliateId"); + + row.Initialize(customer, email ?? id.ToString()); + + row.SetProperty(context.Result, (x) => x.CustomerGuid); + row.SetProperty(context.Result, (x) => x.Username); + row.SetProperty(context.Result, (x) => x.Email); + + if (email.HasValue() && currentCustomer.Email.IsCaseInsensitiveEqual(email)) + { + context.Result.AddInfo("Security. Ignored password of current customer (who started this import).", row.GetRowInfo(), "Password"); + } + else + { + row.SetProperty(context.Result, (x) => x.Password); + row.SetProperty(context.Result, (x) => x.PasswordFormatId); + row.SetProperty(context.Result, (x) => x.PasswordSalt); + } + + row.SetProperty(context.Result, (x) => x.AdminComment); + row.SetProperty(context.Result, (x) => x.IsTaxExempt); + row.SetProperty(context.Result, (x) => x.Active); + + row.SetProperty(context.Result, (x) => x.CreatedOnUtc, UtcNow); + row.SetProperty(context.Result, (x) => x.LastActivityDateUtc, UtcNow); + + if (affiliateId > 0 && allAffiliateIds.Contains(affiliateId)) + { + customer.AffiliateId = affiliateId; + } + + if (isAdmin) + { + context.Result.AddInfo("Security. Ignored administrator role.", row.GetRowInfo(), "IsAdministrator"); + } + + UpsertRole(row, guestRole, isGuest); + UpsertRole(row, registeredRole, isRegistered); + UpsertRole(row, forumModeratorRole, isForumModerator); + + if (row.IsTransient) + { + _customerRepository.Insert(customer); + lastInserted = customer; + } + else + { + _customerRepository.Update(customer); + lastUpdated = customer; + } + } + + var num = _customerRepository.Context.SaveChanges(); + + if (lastInserted != null) + { + _services.EventPublisher.EntityInserted(lastInserted); + } + + if (lastUpdated != null) + { + _services.EventPublisher.EntityUpdated(lastUpdated); + } + + return num; + } + + protected virtual int ProcessAddresses( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary allCountries, + Dictionary, int> allStateProvinces) + { + foreach (var row in batch) + { + ImportAddress("BillingAddress.", row, context, allCountries, allStateProvinces); + ImportAddress("ShippingAddress.", row, context, allCountries, allStateProvinces); + } + + return _services.DbContext.SaveChanges(); + } + + private void ImportAddress( + string fieldPrefix, + ImportRow row, + ImportExecuteContext context, + Dictionary allCountries, + Dictionary, int> allStateProvinces) + { + // last name is mandatory for an address to be imported or updated + if (!row.HasDataValue(fieldPrefix + "LastName")) + return; + + var address = new Address + { + CreatedOnUtc = UtcNow + }; + + var childRow = new ImportRow
    (row.Segmenter, row.DataRow, row.Position); + childRow.Initialize(address, row.EntityDisplayName); + + childRow.SetProperty(context.Result, fieldPrefix + "LastName", x => x.LastName); + childRow.SetProperty(context.Result, fieldPrefix + "FirstName", x => x.FirstName); + childRow.SetProperty(context.Result, fieldPrefix + "Email", x => x.Email); + childRow.SetProperty(context.Result, fieldPrefix + "Company", x => x.Company); + childRow.SetProperty(context.Result, fieldPrefix + "City", x => x.City); + childRow.SetProperty(context.Result, fieldPrefix + "Address1", x => x.Address1); + childRow.SetProperty(context.Result, fieldPrefix + "Address2", x => x.Address2); + childRow.SetProperty(context.Result, fieldPrefix + "ZipPostalCode", x => x.ZipPostalCode); + childRow.SetProperty(context.Result, fieldPrefix + "PhoneNumber", x => x.PhoneNumber); + childRow.SetProperty(context.Result, fieldPrefix + "FaxNumber", x => x.FaxNumber); + + childRow.SetProperty(context.Result, fieldPrefix + "CountryId", x => x.CountryId); + if (childRow.Entity.CountryId == null) + { + // try with country code + childRow.SetProperty(context.Result, fieldPrefix + "CountryCode", x => x.CountryId, converter: (val, ci) => CountryCodeToId(allCountries, val.ToString())); + } + + var countryId = childRow.Entity.CountryId; + + if (countryId.HasValue) + { + childRow.SetProperty(context.Result, fieldPrefix + "StateProvinceId", x => x.StateProvinceId); + if (childRow.Entity.StateProvinceId == null) + { + // try with state abbreviation + childRow.SetProperty(context.Result, fieldPrefix + "StateAbbreviation", x => x.StateProvinceId, converter: (val, ci) => StateAbbreviationToId(allStateProvinces, countryId, val.ToString())); + } + } + + if (!childRow.IsDirty) + { + // Not one single property could be set. Get out! + return; + } + + var appliedAddress = row.Entity.Addresses.FindAddress(address); + + if (appliedAddress == null) + { + appliedAddress = address; + row.Entity.Addresses.Add(appliedAddress); + } + + if (fieldPrefix == "BillingAddress.") + { + row.Entity.BillingAddress = appliedAddress; + } + else if (fieldPrefix == "ShippingAddress.") + { + row.Entity.ShippingAddress = appliedAddress; + } + + _customerRepository.Update(row.Entity); + } + + protected virtual int ProcessGenericAttributes( + ImportExecuteContext context, + IEnumerable> batch, + Dictionary allCountries, + Dictionary, int> allStateProvinces, + HashSet allCustomerNumbers) + { + foreach (var row in batch) + { + SaveAttribute(row, SystemCustomerAttributeNames.FirstName); + SaveAttribute(row, SystemCustomerAttributeNames.LastName); + + if (_dateTimeSettings.AllowCustomersToSetTimeZone) + SaveAttribute(row, SystemCustomerAttributeNames.TimeZoneId); + + if (_customerSettings.GenderEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.Gender); + + if (_customerSettings.DateOfBirthEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.DateOfBirth); + + if (_customerSettings.CompanyEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.Company); + + if (_customerSettings.StreetAddressEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.StreetAddress); + + if (_customerSettings.StreetAddress2Enabled) + SaveAttribute(row, SystemCustomerAttributeNames.StreetAddress2); + + if (_customerSettings.ZipPostalCodeEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.ZipPostalCode); + + if (_customerSettings.CityEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.City); + + if (_customerSettings.CountryEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.CountryId); + + if (_customerSettings.CountryEnabled && _customerSettings.StateProvinceEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.StateProvinceId); + + if (_customerSettings.PhoneEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.Phone); + + if (_customerSettings.FaxEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.Fax); + + if (_forumSettings.ForumsEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.ForumPostCount); + + if (_forumSettings.SignaturesEnabled) + SaveAttribute(row, SystemCustomerAttributeNames.Signature); + + var countryId = CountryCodeToId(allCountries, row.GetDataValue("CountryCode")); + var stateId = StateAbbreviationToId(allStateProvinces, countryId, row.GetDataValue("StateAbbreviation")); + + if (countryId.HasValue) + { + SaveAttribute(row, SystemCustomerAttributeNames.CountryId, countryId.Value); + } + + if (stateId.HasValue) + { + SaveAttribute(row, SystemCustomerAttributeNames.StateProvinceId, stateId.Value); + } + + string customerNumber = null; + + if (_customerSettings.CustomerNumberMethod == CustomerNumberMethod.AutomaticallySet) + customerNumber = row.Entity.Id.ToString(); + else + customerNumber = row.GetDataValue("CustomerNumber"); + + if (customerNumber.IsEmpty() || !allCustomerNumbers.Contains(customerNumber)) + { + SaveAttribute(row, SystemCustomerAttributeNames.CustomerNumber, customerNumber); + + if (!customerNumber.IsEmpty()) + allCustomerNumbers.Add(customerNumber); + } + } + + return _services.DbContext.SaveChanges(); + } + + protected virtual int ProcessAvatars( + ImportExecuteContext context, + IEnumerable> batch) + { + foreach (var row in batch) + { + var urlOrPath = row.GetDataValue("AvatarPictureUrl"); + if (urlOrPath.IsEmpty()) + continue; + + Picture picture = null; + var equalPictureId = 0; + var currentPictures = new List(); + var seoName = _pictureService.GetPictureSeName(row.EntityDisplayName); + + var image = CreateDownloadImage(urlOrPath, seoName, 1); + if (image == null) + continue; + + 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 currentPictureId = row.Entity.GetAttribute(SystemCustomerAttributeNames.AvatarPictureId); + if (currentPictureId != 0 && (picture = _pictureRepository.GetById(currentPictureId)) != null) + { + currentPictures.Add(picture); + } + + pictureBinary = _pictureService.ValidatePicture(pictureBinary); + pictureBinary = _pictureService.FindEqualPicture(pictureBinary, currentPictures, out equalPictureId); + + if (pictureBinary != null && pictureBinary.Length > 0) + { + if ((picture = _pictureService.InsertPicture(pictureBinary, image.MimeType, seoName, true, false, false)) != null) + { + _pictureRepository.Context.SaveChanges(); + SaveAttribute(row, SystemCustomerAttributeNames.AvatarPictureId, picture.Id); + } + } + else + { + context.Result.AddInfo("Found equal picture in data store. Skipping field.", row.GetRowInfo(), "AvatarPictureUrl"); + } + } + } + else + { + context.Result.AddInfo("Download of an image failed.", row.GetRowInfo(), "AvatarPictureUrl"); + } + } + + return _services.DbContext.SaveChanges(); + } + + public static string[] SupportedKeyFields + { + get + { + return new string[] { "Id", "CustomerGuid", "Email", "Username" }; + } + } + + public static string[] DefaultKeyFields + { + get + { + return new string[] { "Email" }; + } + } + + private int? CountryCodeToId(Dictionary allCountries, string code) + { + int countryId; + if (code.HasValue() && allCountries.TryGetValue(code, out countryId) && countryId != 0) + { + return countryId; + } + + return null; + } + + private int? StateAbbreviationToId(Dictionary, int> allStateProvinces, int? countryId, string abbreviation) + { + if (countryId.HasValue && abbreviation.HasValue()) + { + var key = Tuple.Create(countryId.Value, abbreviation); + + int stateId; + if (allStateProvinces.TryGetValue(key, out stateId) && stateId != 0) + { + return stateId; + } + } + + return null; + } + + private void SaveAttribute(ImportRow row, string key) + { + SaveAttribute(row, key, row.GetDataValue(key)); + } + + private void SaveAttribute(ImportRow row, string key) + { + + SaveAttribute(row, key, row.GetDataValue(key)); + } + + private void SaveAttribute(ImportRow row, string key, TPropType value) + { + if (row.IsTransient) + return; + + if (row.IsNew || value != null) + { + _genericAttributeService.SaveAttribute(row.Entity.Id, key, _attributeKeyGroup, value); + } + } + + private void UpsertRole(ImportRow row, CustomerRole role, bool value) + { + if (role == null) + return; + + var hasRole = row.Entity.CustomerRoles.Any(x => x.SystemName == role.SystemName); + + if (value && !hasRole) + { + row.Entity.CustomerRoles.Add(role); + } + else if (!value && hasRole) + { + row.Entity.CustomerRoles.Remove(role); + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfiguration.cs b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfiguration.cs new file mode 100644 index 0000000000..1680cf2d55 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfiguration.cs @@ -0,0 +1,296 @@ +using System; +using System.Linq; +using Newtonsoft.Json; + +namespace SmartStore.Services.DataExchange.Csv +{ + /// + /// Specifies the action to take when a parsing error has occured. + /// + public enum ParseErrorAction + { + /// + /// Raises the event. + /// + RaiseEvent = 0, + + /// + /// Tries to advance to next line. + /// + AdvanceToNextLine = 1, + + /// + /// Throws an exception. + /// + ThrowException = 2, + } + + /// + /// Specifies the action to take when a field is missing. + /// + public enum MissingFieldAction + { + /// + /// Treat as a parsing error. + /// + ParseError = 0, + + /// + /// Replaces by an empty value. + /// + ReplaceByEmpty = 1, + + /// + /// Replaces by a null value (). + /// + ReplaceByNull = 2, + } + + public class CsvConfiguration + { + private char _delimiter; + private char _escape; + private char _quote; + private string _quoteString; + private char[] _quotableChars; + + public CsvConfiguration() + { + _escape = '"'; + _delimiter = ';'; + _quote = '"'; + _quoteString = new String(new char[] { _escape, _quote }); + + Comment = '#'; + HasHeaders = true; + SkipEmptyLines = true; + SupportsMultiline = true; + DefaultHeaderName = "Column"; + + BuildQuotableChars(); + } + + private void BuildQuotableChars() + { + _quotableChars = new char[] { '\r', '\n', _delimiter, _quote }; + } + + internal char[] QuotableChars + { + get { return _quotableChars; } + } + + /// + /// Gets an Excel friendly configuration where the result can be directly edited by Excel + /// + public static CsvConfiguration ExcelFriendlyConfiguration + { + get + { + return new CsvConfiguration + { + Delimiter = ';', + Quote = '"', + Escape = '"', + SupportsMultiline = false + }; + } + } + + /// + /// Gets an array with preset characters + /// + public static char[] PresetCharacters + { + get { return new char[] { '\n', '\r', '\0' }; } + } + + /// + /// Gets the comment character indicating that a line is commented out (default: #). + /// + /// The comment character indicating that a line is commented out. + public char Comment + { + get; + set; + } + + /// + /// Gets the escape character letting insert quotation characters inside a quoted field (default: "). + /// + /// The escape character letting insert quotation characters inside a quoted field. + public char Escape + { + get { return _escape; } + set + { + if (value == _escape) + return; + + if (PresetCharacters.Contains(value)) + { + throw new SmartException("'{0}' is not a valid escape char.".FormatInvariant(value)); + } + if (value == _delimiter) + { + throw new SmartException("Escape and delimiter chars cannot be equal in CSV files."); + } + + _escape = value; + _quoteString = new String(new char[] { _escape, _quote }); + } + } + + /// + /// Gets the delimiter character separating each field (default: ;). + /// + /// The delimiter character separating each field. + public char Delimiter + { + get { return _delimiter; } + set + { + if (value == _delimiter) + return; + + if (PresetCharacters.Contains(value)) + { + throw new SmartException("'{0}' is not a valid delimiter char.".FormatInvariant(value)); + } + if (value == _quote) + { + throw new SmartException("Quote and delimiter chars cannot be equal in CSV files."); + } + + _delimiter = value; + BuildQuotableChars(); + + } + } + + /// + /// Gets the quotation character wrapping every field (default: "). + /// + /// The quotation character wrapping every field. + public char Quote + { + get { return _quote; } + set + { + if (value == _quote) + return; + + if (PresetCharacters.Contains(value)) + { + throw new SmartException("'{0}' is not a valid quote char.".FormatInvariant(value)); + } + if (value == _delimiter) + { + throw new SmartException("Quote and delimiter chars cannot be equal in CSV files."); + } + + _quote = value; + _quoteString = new String(new char[] { _escape, _quote }); + BuildQuotableChars(); + } + } + + /// + /// Gets the concatenation of escape and quote char + /// + [JsonIgnore] + public string QuoteString + { + get + { + return _quoteString; + } + } + + public bool QuoteAllFields + { + get; + set; + } + + /// + /// Indicates if field names are located on the first non commented line (default: true). + /// + /// if field names are located on the first non commented line, otherwise, . + public bool HasHeaders + { + get; + set; + } + + /// + /// Indicates if spaces at the start and end of a field are trimmed (default: false). + /// + /// if spaces at the start and end of a field are trimmed, otherwise, . + public bool TrimValues + { + get; + set; + } + + /// + /// Contains the value which denotes a DbNull-value. + /// + public string NullValue + { + get; + set; + } + + /// + /// Gets or sets the default action to take when a parsing error has occured. + /// + /// The default action to take when a parsing error has occured. + public ParseErrorAction DefaultParseErrorAction + { + get; + set; + } + + /// + /// Gets or sets the action to take when a field is missing. + /// + /// The action to take when a field is missing. + public MissingFieldAction MissingFieldAction + { + get; + set; + } + + /// + /// Gets or sets a value indicating if the reader supports multiline fields (default: true). + /// + /// A value indicating if the reader supports multiline field. + public bool SupportsMultiline + { + get; + set; + } + + /// + /// Gets or sets a value indicating if the reader will skip empty lines (default: true). + /// + /// A value indicating if the reader will skip empty lines. + public bool SkipEmptyLines + { + get; + set; + } + + /// + /// Gets or sets the default header name when it is an empty string or only whitespaces (default: Column). + /// The header index will be appended to the specified name. + /// + /// The default header name when it is an empty string or only whitespaces. + public string DefaultHeaderName + { + get; + set; + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfigurationConverter.cs b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfigurationConverter.cs new file mode 100644 index 0000000000..4a81792a19 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvConfigurationConverter.cs @@ -0,0 +1,65 @@ +using System; +using System.Globalization; +using Newtonsoft.Json; +using SmartStore.ComponentModel; + +namespace SmartStore.Services.DataExchange.Csv +{ + public class CsvConfigurationConverter : TypeConverterBase + { + public CsvConfigurationConverter() + : base(typeof(object)) + { + } + + public override bool CanConvertFrom(Type type) + { + return type == typeof(string); + } + + public override bool CanConvertTo(Type type) + { + return type == typeof(string); + } + + public override object ConvertFrom(CultureInfo culture, object value) + { + if (value is string) + { + return JsonConvert.DeserializeObject((string)value); + } + + return base.ConvertFrom(culture, value); + } + + public T ConvertFrom(string value) + { + if (value.HasValue()) + return (T)ConvertFrom(CultureInfo.InvariantCulture, value); + + return default(T); + } + + public override object ConvertTo(CultureInfo culture, string format, object value, Type to) + { + if (to == typeof(string)) + { + if (value is CsvConfiguration) + { + return JsonConvert.SerializeObject(value); + } + else + { + return string.Empty; + } + } + + return base.ConvertTo(culture, format, value, to); + } + + public string ConvertTo(object value) + { + return (string)ConvertTo(CultureInfo.InvariantCulture, null, value, typeof(string)); + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvDataReader.cs b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvDataReader.cs new file mode 100644 index 0000000000..9fbe870a2b --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvDataReader.cs @@ -0,0 +1,389 @@ +// Wrapper for LumenWorks CsvReader (fork by phatcher) +// ---------------------------------------------------- +// LumenWorks.Framework.IO.CsvReader +// Copyright (c) 2006 Sébastien Lorion +// https://github.com/phatcher/CsvReader/ +// +// MIT license (http://en.wikipedia.org/wiki/MIT_License) + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using LumenWorks.Framework.IO.Csv; + +namespace SmartStore.Services.DataExchange.Csv +{ + /// + /// Represents a reader that provides fast, non-cached, forward-only access to CSV data. + /// + public class CsvDataReader : DisposableObject, IDataReader, IEnumerable + { + private readonly CsvReader _csv; + private readonly IDataReader _reader; + + /// + /// Initializes a new instance of the CsvDataReader class. + /// + /// A pointing to the CSV file. + public CsvDataReader(TextReader reader) + : this(reader, new CsvConfiguration()) + { + } + + /// + /// Initializes a new instance of the CsvDataReader class. + /// + /// A pointing to the CSV file. + public CsvDataReader(TextReader reader, CsvConfiguration configuration) + { + Guard.ArgumentNotNull(() => reader); + Guard.ArgumentNotNull(() => configuration); + + this.Configuration = configuration; + + _csv = new CsvReader( + reader, + configuration.HasHeaders, + configuration.Delimiter, + configuration.Quote, + configuration.Escape, + configuration.Comment, + configuration.TrimValues ? ValueTrimmingOptions.All : ValueTrimmingOptions.None, + configuration.NullValue); + + _csv.SupportsMultiline = configuration.SupportsMultiline; + _csv.SkipEmptyLines = configuration.SkipEmptyLines; + _csv.DefaultHeaderName = configuration.DefaultHeaderName; + _csv.DefaultParseErrorAction = (LumenWorks.Framework.IO.Csv.ParseErrorAction)((int)configuration.DefaultParseErrorAction); + _csv.MissingFieldAction = (LumenWorks.Framework.IO.Csv.MissingFieldAction)((int)configuration.MissingFieldAction); + + _reader = _csv; + } + + public CsvConfiguration Configuration + { + get; + private set; + } + + #region Public wrapper members + + /// + /// Gets the field headers. + /// + /// The field headers or an empty array if headers are not supported. + /// + /// The instance has been disposed of. + /// + public string[] GetFieldHeaders() + { + return _csv.GetFieldHeaders(); + } + + public bool EndOfStream + { + get + { + return _csv.EndOfStream; + } + } + + /// + /// Gets the current row index in the CSV file (0-based). + /// + /// The current row index in the CSV file. + public long CurrentRowIndex + { + get + { + return _csv.CurrentRecordIndex; + } + } + + public void CopyCurrentRowTo(string[] array) + { + _csv.CopyCurrentRecordTo(array); + } + + /// + /// Gets the current row's raw CSV data. + /// + /// The current raw CSV data. + public string GetRawData() + { + return _csv.GetCurrentRawData(); + } + + /// + /// Gets the field with the specified name. must be . + /// + /// + /// The field with the specified name. + /// + /// + /// is or an empty string. + /// + /// + /// The CSV does not have headers ( property is ). + /// + /// + /// not found. + /// + /// + /// The CSV appears to be corrupt at the current position. + /// + /// + /// The instance has been disposed of. + /// + public string this[string field] + { + get + { + return _csv[field]; + } + } + + /// + /// Gets the field at the specified index. + /// + /// The field at the specified index. + /// + /// must be included in [0, [. + /// + /// + /// No record read yet. Call ReadLine() first. + /// + /// + /// The CSV appears to be corrupt at the current position. + /// + /// + /// The instance has been disposed of. + /// + public virtual string this[int index] + { + get + { + return _csv[index]; + } + } + + #endregion + + #region IDataReader (implicit) + + public int FieldCount + { + get + { + return _reader.FieldCount; + } + } + + public bool Read() + { + return _csv.ReadNextRecord(); + } + + public int GetOrdinal(string name) + { + return _reader.GetOrdinal(name); + } + + #endregion + + #region IDataReader (explicit) + + object IDataRecord.this[string name] + { + get + { + return _reader[name]; + } + } + + object IDataRecord.this[int i] + { + get + { + return _reader[i]; + } + } + + int IDataReader.Depth + { + get + { + return _reader.Depth; + } + } + + bool IDataReader.IsClosed + { + get + { + return _reader.IsClosed; + } + } + + int IDataReader.RecordsAffected + { + get + { + return _reader.RecordsAffected; + } + } + + void IDataReader.Close() + { + _reader.Close(); + } + + bool IDataRecord.GetBoolean(int i) + { + return _reader.GetBoolean(i); + } + + byte IDataRecord.GetByte(int i) + { + return _reader.GetByte(i); + } + + long IDataRecord.GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length) + { + return _reader.GetBytes(i, fieldOffset, buffer, bufferoffset, length); + } + + char IDataRecord.GetChar(int i) + { + return _reader.GetChar(i); + } + + long IDataRecord.GetChars(int i, long fieldOffset, char[] buffer, int bufferoffset, int length) + { + return _reader.GetChars(i, fieldOffset, buffer, bufferoffset, length); + } + + IDataReader IDataRecord.GetData(int i) + { + return _reader.GetData(i); + } + + string IDataRecord.GetDataTypeName(int i) + { + return _reader.GetDataTypeName(i); + } + + DateTime IDataRecord.GetDateTime(int i) + { + return _reader.GetDateTime(i); + } + + decimal IDataRecord.GetDecimal(int i) + { + return _reader.GetDecimal(i); + } + + double IDataRecord.GetDouble(int i) + { + return _reader.GetDouble(i); + } + + Type IDataRecord.GetFieldType(int i) + { + return _reader.GetFieldType(i); + } + + float IDataRecord.GetFloat(int i) + { + return _reader.GetFloat(i); + } + + Guid IDataRecord.GetGuid(int i) + { + return _reader.GetGuid(i); + } + + short IDataRecord.GetInt16(int i) + { + return _reader.GetInt16(i); + } + + int IDataRecord.GetInt32(int i) + { + return _reader.GetInt32(i); + } + + long IDataRecord.GetInt64(int i) + { + return _reader.GetInt64(i); + } + + string IDataRecord.GetName(int i) + { + return _reader.GetName(i); + } + + DataTable IDataReader.GetSchemaTable() + { + return _reader.GetSchemaTable(); + } + + string IDataRecord.GetString(int i) + { + return _reader.GetString(i); + } + + object IDataRecord.GetValue(int i) + { + return _reader.GetGuid(i); + } + + int IDataRecord.GetValues(object[] values) + { + return _reader.GetValues(values); + } + + bool IDataRecord.IsDBNull(int i) + { + return _reader.IsDBNull(i); + } + + bool IDataReader.NextResult() + { + return _reader.NextResult(); + } + + #endregion + + #region IEnumerable + + IEnumerator IEnumerable.GetEnumerator() + { + return _csv.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _csv.GetEnumerator(); + } + + #endregion + + #region Disposable + + protected override void OnDispose(bool disposing) + { + if (disposing) + { + _csv.Dispose(); + } + } + + #endregion + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvWriter.cs b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvWriter.cs new file mode 100644 index 0000000000..8bb9942052 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Csv/CsvWriter.cs @@ -0,0 +1,194 @@ +// Customized version of CsvHelper from Josh Close: +// ------------------------------------------------ +// Copyright 2009-2015 Josh Close and Contributors +// This file is a part of CsvHelper and is dual licensed under MS-PL and Apache 2.0. +// See LICENSE.txt for details or visit http://www.opensource.org/licenses/ms-pl.html for MS-PL and http://opensource.org/licenses/Apache-2.0 for Apache 2.0. +// http://csvhelper.com + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmartStore.Services.DataExchange.Csv +{ + /// + /// SImple utility class used to write CSV files. + /// + public class CsvWriter : DisposableObject + { + private TextWriter _writer; + private readonly IList _currentRow = new List(); + private int? _fieldCount; + + public CsvWriter(TextWriter writer) + : this(writer, new CsvConfiguration()) + { + } + + public CsvWriter(TextWriter writer, CsvConfiguration configuration) + { + Guard.ArgumentNotNull(() => writer); + Guard.ArgumentNotNull(() => configuration); + + _writer = writer; + this.Configuration = configuration; + } + + public CsvConfiguration Configuration + { + get; + private set; + } + + /// + /// Writes a sequence of fields to the CSV file. The fields + /// may get quotes added to it. + /// When all fields are written for a row, + /// must be called + /// to complete writing of the current row. + /// + /// The fields to write. + public virtual void WriteFields(IEnumerable fields) + { + Guard.ArgumentNotNull(() => fields); + fields.Each(x => WriteField(x)); + } + + /// + /// Writes a sequence of fields to the CSV file. This will + /// ignore any need to quote and ignore the + /// + /// and just quote based on the shouldQuote + /// parameter. + /// When all fields are written for a row, + /// must be called + /// to complete writing of the current row. + /// + /// The fields to write. + /// True to quote the fields, otherwise false. + public virtual void WriteFields(IEnumerable fields, bool shouldQuote) + { + Guard.ArgumentNotNull(() => fields); + fields.Each(x => WriteField(x, shouldQuote)); + } + + /// + /// Writes the field to the CSV file. The field + /// may get quotes added to it. + /// When all fields are written for a row, + /// must be called + /// to complete writing of the current row. + /// + /// The field to write. + public virtual void WriteField(string field) + { + var shouldQuote = Configuration.QuoteAllFields; + + if (!string.IsNullOrEmpty(field)) + { + if (Configuration.TrimValues) + { + field = field.Trim(); + } + + if (!Configuration.SupportsMultiline) + { + field = field.Replace('\r', ' ').Replace('\n', ' '); + } + + if (shouldQuote + || field[0] == ' ' + || field[field.Length - 1] == ' ' + || field.IndexOfAny(Configuration.QuotableChars) > -1) + { + shouldQuote = true; + } + } + + WriteField(field, shouldQuote); + } + + /// + /// Writes the field to the CSV file. This will + /// ignore any need to quote and ignore the + /// + /// and just quote based on the shouldQuote + /// parameter. + /// When all fields are written for a row, + /// must be called + /// to complete writing of the current row. + /// + /// The field to write. + /// True to quote the field, otherwise false. + public virtual void WriteField(string field, bool shouldQuote) + { + // All quotes must be escaped. + if (shouldQuote && !string.IsNullOrEmpty(field)) + { + field = field.Replace(Configuration.Quote.ToString(), Configuration.QuoteString); + } + + if (shouldQuote) + { + field = Configuration.Quote + (field ?? string.Empty) + Configuration.Quote; + } + + _currentRow.Add(field ?? string.Empty); + } + + /// + /// Ends writing of the current row + /// and starts a new row. + /// + public virtual void NextRow() + { + WriteRow(_currentRow.ToArray()); + _currentRow.Clear(); + } + + public string CurrentRawValue() + { + var row = string.Join(Configuration.Delimiter.ToString(), _currentRow); + return row; + } + + private void WriteRow(string[] fields) + { + CheckDisposed(); + + if (fields.Length == 0) + { + throw new SmartException("Cannot write an empty row to the CSV file."); + } + + if (!_fieldCount.HasValue) + { + _fieldCount = fields.Length; + } + + if (_fieldCount.Value != fields.Length) + { + throw new SmartException("The field count of the current row does not match the previous row's field count."); + } + + var row = string.Join(Configuration.Delimiter.ToString(), fields); + _writer.WriteLine(row); + } + + protected override void OnDispose(bool disposing) + { + if (disposing) + { + if (_writer != null) + { + _writer.Dispose(); + _writer = null; + } + } + } + + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Excel/ExcelDataReader.cs b/src/Libraries/SmartStore.Services/DataExchange/Excel/ExcelDataReader.cs new file mode 100644 index 0000000000..d46879193c --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Excel/ExcelDataReader.cs @@ -0,0 +1,612 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using OfficeOpenXml; + +namespace SmartStore.Services.DataExchange.Excel +{ + public class ExcelDataReader : DisposableObject, IDataReader + { + private ExcelPackage _package; + private ExcelWorksheet _sheet; + private int _totalRows; + private int _totalColumns; + + private bool _initialized; + private string[] _columns; + private Dictionary _columnIndexes; + private int _currentRowIndex; + private bool _eof; + + private readonly object _lock = new object(); + + public ExcelDataReader(Stream source, bool hasHeaders) + { + Guard.ArgumentNotNull(() => source); + + _package = new ExcelPackage(source); + + // get the first worksheet in the workbook + _sheet = _package.Workbook.Worksheets.FirstOrDefault(); + if (_sheet == null) + { + throw Error.InvalidOperation("The excel package does not contain any worksheet."); + } + + if (_sheet.Dimension == null) + { + throw Error.InvalidOperation("The excel worksheet does not contain any data."); + } + + HasHeaders = hasHeaders; + DefaultHeaderName = "Column"; + + _totalColumns = _sheet.Dimension.End.Column; + _totalRows = _sheet.Dimension.End.Row - (hasHeaders ? 1 : 0); + + _currentRowIndex = -1; + } + + #region Configuration + + public bool HasHeaders + { + get; + private set; + } + + public string DefaultHeaderName + { + get; + set; + } + + #endregion + + #region public members + + public bool MoveToStart() + { + return MoveTo(0); + } + + public bool MoveToEnd() + { + return MoveTo(_totalRows - 1); + } + + public bool MoveTo(int row) + { + EnsureInitialize(); + ValidateDataReader(validateInitialized: false); + + if (row < 0 || row >= _totalRows) + return false; + + _currentRowIndex = row; + _eof = false; + + return true; + } + + public bool EndOfStream + { + get { return _eof; } + } + + public int TotalRows + { + get + { + EnsureInitialize(); + return _totalRows; + } + } + + public IReadOnlyCollection GetColumnHeaders() + { + EnsureInitialize(); + return _columns.AsReadOnly(); + } + + public int CurrentRowIndex + { + get + { + return _currentRowIndex; + } + } + + public string GetFormatted(int i) + { + ValidateDataReader(); + return _sheet.Cells[ExcelRowIndex(_currentRowIndex), i + 1].Text; + } + + public int GetColumnIndex(string name) + { + Guard.ArgumentNotEmpty(name, "name"); + + EnsureInitialize(); + + int index; + + if (_columnIndexes != null && _columnIndexes.TryGetValue(name, out index)) + return index; + else + return -1; + } + + #endregion + + #region IDataReader + + bool IDataReader.NextResult() + { + ValidateDataReader(validateInitialized: false); + return false; + } + + public bool Read() + { + ValidateDataReader(validateInitialized: false); + return ReadNextRow(false); + } + + int IDataReader.Depth + { + get + { + ValidateDataReader(validateInitialized: false); + return 0; + } + } + + bool IDataReader.IsClosed + { + get + { + return _eof; + } + } + + int IDataReader.RecordsAffected + { + get + { + return -1; + } + } + + void IDataReader.Close() + { + Dispose(); + } + + + public int FieldCount + { + get + { + EnsureInitialize(); + return _totalColumns; + } + } + + public object this[string name] + { + get + { + int index = GetColumnIndex(name); + + if (index < 0) + Error.Argument("name", "'{0}' column header not found.".FormatInvariant(name)); + + return this[index]; + } + } + + public object this[int i] + { + get + { + ValidateDataReader(); + // Excel indexes start from 1 + return _sheet.GetValue(ExcelRowIndex(_currentRowIndex), i + 1); + } + } + + public bool GetBoolean(int i) + { + object value = this[i]; + + int result; + if (Int32.TryParse(value.ToString(), out result)) + return (result != 0); + else + return Boolean.Parse(value.ToString()); + } + + public byte GetByte(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public long GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length) + { + ValidateDataReader(); + return CopyFieldToArray(i, fieldOffset, buffer, bufferoffset, length); + } + + public char GetChar(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public long GetChars(int i, long fieldOffset, char[] buffer, int bufferoffset, int length) + { + ValidateDataReader(); + return CopyFieldToArray(i, fieldOffset, buffer, bufferoffset, length); + } + + IDataReader IDataRecord.GetData(int i) + { + ValidateDataReader(); + + if (i == 0) + return this; + else + return null; + } + + string IDataRecord.GetDataTypeName(int i) + { + return this[i].GetType().FullName; + } + + public DateTime GetDateTime(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public decimal GetDecimal(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public double GetDouble(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public Type GetFieldType(int i) + { + return this[i].GetType(); + } + + public float GetFloat(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public Guid GetGuid(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public short GetInt16(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public int GetInt32(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public long GetInt64(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public string GetName(int i) + { + EnsureInitialize(); + ValidateDataReader(validateInitialized: false); + + if (i < 0 || i >= _columns.Length) + { + throw new ArgumentOutOfRangeException("i", i, + "Column index must be included within [0, {0}], but specified column index was: '{1}'.".FormatInvariant(_columns.Length, i)); + } + + return _columns[i]; + } + + public int GetOrdinal(string name) + { + return GetColumnIndex(name); + } + + DataTable IDataReader.GetSchemaTable() + { + EnsureInitialize(); + ValidateDataReader(validateInitialized: false); + + var schema = new DataTable("SchemaTable") + { + Locale = CultureInfo.InvariantCulture, + MinimumCapacity = _columns.Length + }; + + schema.Columns.Add(SchemaTableColumn.AllowDBNull, typeof(bool)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.BaseColumnName, typeof(string)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.BaseSchemaName, typeof(string)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.BaseTableName, typeof(string)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.ColumnName, typeof(string)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.ColumnOrdinal, typeof(int)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.ColumnSize, typeof(int)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.DataType, typeof(object)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.IsAliased, typeof(bool)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.IsExpression, typeof(bool)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.IsKey, typeof(bool)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.IsLong, typeof(bool)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.IsUnique, typeof(bool)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.NumericPrecision, typeof(short)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.NumericScale, typeof(short)).ReadOnly = true; + schema.Columns.Add(SchemaTableColumn.ProviderType, typeof(int)).ReadOnly = true; + + schema.Columns.Add(SchemaTableOptionalColumn.BaseCatalogName, typeof(string)).ReadOnly = true; + schema.Columns.Add(SchemaTableOptionalColumn.BaseServerName, typeof(string)).ReadOnly = true; + schema.Columns.Add(SchemaTableOptionalColumn.IsAutoIncrement, typeof(bool)).ReadOnly = true; + schema.Columns.Add(SchemaTableOptionalColumn.IsHidden, typeof(bool)).ReadOnly = true; + schema.Columns.Add(SchemaTableOptionalColumn.IsReadOnly, typeof(bool)).ReadOnly = true; + schema.Columns.Add(SchemaTableOptionalColumn.IsRowVersion, typeof(bool)).ReadOnly = true; + + // null marks columns that will change for each row + object[] schemaRow = + { + true, // 00- AllowDBNull + null, // 01- BaseColumnName + string.Empty, // 02- BaseSchemaName + string.Empty, // 03- BaseTableName + null, // 04- ColumnName + null, // 05- ColumnOrdinal + int.MaxValue, // 06- ColumnSize + typeof(string), // 07- DataType + false, // 08- IsAliased + false, // 09- IsExpression + false, // 10- IsKey + false, // 11- IsLong + false, // 12- IsUnique + DBNull.Value, // 13- NumericPrecision + DBNull.Value, // 14- NumericScale + (int) DbType.String, // 15- ProviderType + string.Empty, // 16- BaseCatalogName + string.Empty, // 17- BaseServerName + false, // 18- IsAutoIncrement + false, // 19- IsHidden + true, // 20- IsReadOnly + false // 21- IsRowVersion + }; + + int r = ExcelRowIndex(0); + + for (int i = 0; i < _columns.Length; i++) + { + schemaRow[1] = _columns[i]; // Base column name + schemaRow[4] = _columns[i]; // Column name + schemaRow[5] = i; // Column ordinal + + // get data type from 1st row only + var firstValue = _sheet.Cells[r, i + 1].Value; + schemaRow[7] = firstValue != null ? firstValue.GetType() : typeof(string); + + schema.Rows.Add(schemaRow); + } + + return schema; + } + + public string GetString(int i) + { + return this[i].Convert(CultureInfo.CurrentCulture); + } + + public object GetValue(int i) + { + ValidateDataReader(); + return ((IDataRecord)this).IsDBNull(i) ? DBNull.Value : this[i]; + } + + int IDataRecord.GetValues(object[] values) + { + var record = (IDataRecord)this; + + for (int i = 0; i < _totalColumns; i++) + { + values[i] = record.GetValue(i); + } + + return _totalColumns; + } + + bool IDataRecord.IsDBNull(int i) + { + return this[i] == null; + } + + #endregion + + #region Helper + + /// + /// Reads the next record. + /// + /// + /// Indicates if the reader will proceed to the next record after having read headers. + /// if it stops after having read headers; otherwise, . + /// + /// if a record has been successfully reads; otherwise, . + /// + /// The instance has been disposed of. + /// + protected virtual bool ReadNextRow(bool onlyReadHeaders) + { + if (_eof) + return false; + + CheckDisposed(); + + if (!_initialized) + { + _columns = new string[_totalColumns]; + _columnIndexes = new Dictionary(_totalColumns, StringComparer.OrdinalIgnoreCase); + + for (int i = 1; i <= _totalColumns; i++) + { + // Excel indexes start from 1 + string name = null; + if (HasHeaders) + { + name = _sheet.GetValue(1, i).NullEmpty(); + } + + name = name ?? (DefaultHeaderName ?? "Column") + i; + + _columns[i - 1] = name; + _columnIndexes[name] = i - 1; + } + + if (_columns.Select(x => x.ToLower()).Distinct().ToArray().Length != _columns.Length) + { + _columns = null; + _columnIndexes = null; + throw Error.InvalidOperation("The first row cannot contain duplicate column names."); + } + + _initialized = true; + + if (!onlyReadHeaders) + { + return ReadNextRow(false); + } + } + else + { + _currentRowIndex++; + if (_currentRowIndex >= _totalRows) + { + _eof = true; + return false; + } + } + + return true; + } + + private int ExcelRowIndex(int i) + { + // Excel indexes start from 1 + return i + (HasHeaders ? 2 : 1); + } + + /// + /// Ensures that the reader is initialized. + /// + private void EnsureInitialize() + { + if (!_initialized) + { + ReadNextRow(true); + } + + Debug.Assert(_columns != null); + Debug.Assert(_columns.Length > 0 || (_columns.Length == 0 && _columnIndexes == null)); + } + + private void ValidateDataReader(bool validateInitialized = true, bool validateNotClosed = true) + { + if (validateInitialized && (!_initialized || _currentRowIndex < 0)) + throw new InvalidOperationException("No current record. Call Read() to initialize the reader."); + + if (validateNotClosed && IsDisposed) + throw new InvalidOperationException("This operation is invalid when the reader is closed."); + } + + private long CopyFieldToArray(int column, long columnOffset, Array destinationArray, int destinationOffset, int length) + { + EnsureInitialize(); + + if (column < 0 || column >= _totalColumns) + { + throw new ArgumentOutOfRangeException("column", column, + "Column index must be included within [0, {0}], but specified column index was: '{1}'.".FormatInvariant(_totalColumns, column)); + } + + if (columnOffset < 0 || columnOffset >= int.MaxValue) + throw new ArgumentOutOfRangeException("fieldOffset"); + + // Array.Copy(...) will do the remaining argument checks + + if (length == 0) + return 0; + + string value = this[column].ToString(); + + if (value == null) + value = string.Empty; + + if (destinationArray.GetType() == typeof(char[])) + Array.Copy(value.ToCharArray((int)columnOffset, length), 0, destinationArray, destinationOffset, length); + else + { + char[] chars = value.ToCharArray((int)columnOffset, length); + byte[] source = new byte[chars.Length]; + ; + + for (int i = 0; i < chars.Length; i++) + source[i] = Convert.ToByte(chars[i]); + + Array.Copy(source, 0, destinationArray, destinationOffset, length); + } + + return length; + } + + #endregion + + #region IDisposable Support + + protected override void OnDispose(bool disposing) + { + if (disposing) + { + try + { + _sheet = null; + if (_package != null) + { + lock (_lock) + { + if (_package != null) + { + _package.Dispose(); + _package = null; + _eof = true; + } + } + } + } + catch { } + } + } + + #endregion + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportExecuteResult.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportResult.cs similarity index 72% rename from src/Libraries/SmartStore.Services/DataExchange/ExportExecuteResult.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/DataExportResult.cs index 6bb8a87429..c9d3fa26f2 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportExecuteResult.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportResult.cs @@ -2,14 +2,14 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace SmartStore.Services.DataExchange +namespace SmartStore.Services.DataExchange.Export { [Serializable] - public class ExportExecuteResult + public class DataExportResult { - public ExportExecuteResult() + public DataExportResult() { - Files = new List(); + Files = new List(); } /// @@ -37,17 +37,11 @@ public bool Succeeded [XmlIgnore] public string FileFolder { get; set; } - /// - /// Suggested download file name - /// - [XmlIgnore] - public string DownloadFileName { get; set; } - [Serializable] public class ExportFileInfo { /// - /// Store identifier + /// Store identifier, can be 0. /// public int StoreId { get; set; } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportTask.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportTask.cs new file mode 100644 index 0000000000..f32f06c625 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportTask.cs @@ -0,0 +1,53 @@ +using System.Linq; +using SmartStore.Core.Localization; +using SmartStore.Services.Tasks; + +namespace SmartStore.Services.DataExchange.Export +{ + // note: namespace persisted in ScheduleTask.Type + public partial class DataExportTask : ITask + { + private readonly IDataExporter _exporter; + private readonly IExportProfileService _exportProfileService; + + public DataExportTask( + IDataExporter exporter, + IExportProfileService exportProfileService) + { + _exporter = exporter; + _exportProfileService = exportProfileService; + } + + public Localizer T { get; set; } + + public void Execute(TaskExecutionContext ctx) + { + var profileId = ctx.ScheduleTask.Alias.ToInt(); + var profile = _exportProfileService.GetExportProfileById(profileId); + + // load provider + var provider = _exportProfileService.LoadProvider(profile.ProviderSystemName); + if (provider == null) + throw new SmartException(T("Admin.Common.ProviderNotLoaded", profile.ProviderSystemName.NaIfEmpty())); + + // build export request + var request = new DataExportRequest(profile, provider); + + request.ProgressValueSetter = delegate (int val, int max, string msg) + { + ctx.SetProgress(val, max, msg, true); + }; + + if (ctx.Parameters.ContainsKey("SelectedIds")) + { + request.EntitiesToExport = ctx.Parameters["SelectedIds"] + .SplitSafe(",") + .Select(x => x.ToInt()) + .ToList(); + } + + // process! + _exporter.Export(request, ctx.CancellationToken); + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs new file mode 100644 index 0000000000..e6b182c270 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExporter.cs @@ -0,0 +1,1440 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Threading; +using System.Web; +using SmartStore.Core; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.Domain.Messages; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Email; +using SmartStore.Core.Localization; +using SmartStore.Core.Logging; +using SmartStore.Services.Catalog; +using SmartStore.Services.Common; +using SmartStore.Services.Customers; +using SmartStore.Services.DataExchange.Export.Deployment; +using SmartStore.Services.DataExchange.Export.Internal; +using SmartStore.Services.Directory; +using SmartStore.Services.Helpers; +using SmartStore.Services.Localization; +using SmartStore.Services.Media; +using SmartStore.Services.Messages; +using SmartStore.Services.Orders; +using SmartStore.Services.Security; +using SmartStore.Services.Seo; +using SmartStore.Services.Shipping; +using SmartStore.Services.Tax; +using SmartStore.Utilities; +using SmartStore.Utilities.Threading; + +namespace SmartStore.Services.DataExchange.Export +{ + public partial class DataExporter : IDataExporter + { + private static readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); + + #region Dependencies + + private readonly ICommonServices _services; + private readonly IDbContext _dbContext; + private readonly Lazy _priceFormatter; + private readonly Lazy _dateTimeHelper; + private readonly Lazy _exportProfileService; + private readonly Lazy _localizedEntityService; + private readonly Lazy _languageService; + private readonly Lazy _urlRecordService; + private readonly Lazy _pictureService; + private readonly Lazy _priceCalculationService; + private readonly Lazy _currencyService; + private readonly Lazy _taxService; + private readonly Lazy _categoryService; + private readonly Lazy _productAttributeParser; + private readonly Lazy _productAttributeService; + private readonly Lazy _productTemplateService; + private readonly Lazy _categoryTemplateService; + private readonly Lazy _productService; + private readonly Lazy _orderService; + private readonly Lazy _manufacturerService; + private readonly ICustomerService _customerService; + private readonly Lazy _addressService; + private readonly Lazy _countryService; + private readonly Lazy _shipmentService; + private readonly Lazy _genericAttributeService; + private readonly Lazy _emailAccountService; + private readonly Lazy _queuedEmailService; + private readonly Lazy _emailSender; + private readonly Lazy _deliveryTimeService; + private readonly Lazy _quantityUnitService; + + private readonly Lazy>_customerRepository; + private readonly Lazy> _subscriptionRepository; + private readonly Lazy> _orderRepository; + + private readonly Lazy _mediaSettings; + private readonly Lazy _contactDataSettings; + private readonly Lazy _customerSettings; + private readonly Lazy _catalogSettings; + + public DataExporter( + ICommonServices services, + IDbContext dbContext, + Lazy priceFormatter, + Lazy dateTimeHelper, + Lazy exportProfileService, + Lazy localizedEntityService, + Lazy languageService, + Lazy urlRecordService, + Lazy pictureService, + Lazy priceCalculationService, + Lazy currencyService, + Lazy taxService, + Lazy categoryService, + Lazy productAttributeParser, + Lazy productAttributeService, + Lazy productTemplateService, + Lazy categoryTemplateService, + Lazy productService, + Lazy orderService, + Lazy manufacturerService, + ICustomerService customerService, + Lazy addressService, + Lazy countryService, + Lazy shipmentService, + Lazy genericAttributeService, + Lazy emailAccountService, + Lazy queuedEmailService, + Lazy emailSender, + Lazy deliveryTimeService, + Lazy quantityUnitService, + Lazy> customerRepository, + Lazy> subscriptionRepository, + Lazy> orderRepository, + Lazy mediaSettings, + Lazy contactDataSettings, + Lazy customerSettings, + Lazy catalogSettings) + { + _services = services; + _dbContext = dbContext; + _priceFormatter = priceFormatter; + _dateTimeHelper = dateTimeHelper; + _exportProfileService = exportProfileService; + _localizedEntityService = localizedEntityService; + _languageService = languageService; + _urlRecordService = urlRecordService; + _pictureService = pictureService; + _priceCalculationService = priceCalculationService; + _currencyService = currencyService; + _taxService = taxService; + _categoryService = categoryService; + _productAttributeParser = productAttributeParser; + _productAttributeService = productAttributeService; + _productTemplateService = productTemplateService; + _categoryTemplateService = categoryTemplateService; + _productService = productService; + _orderService = orderService; + _manufacturerService = manufacturerService; + _customerService = customerService; + _addressService = addressService; + _countryService = countryService; + _shipmentService = shipmentService; + _genericAttributeService = genericAttributeService; + _emailAccountService = emailAccountService; + _queuedEmailService = queuedEmailService; + _emailSender = emailSender; + _deliveryTimeService = deliveryTimeService; + _quantityUnitService = quantityUnitService; + + _customerRepository = customerRepository; + _subscriptionRepository = subscriptionRepository; + _orderRepository = orderRepository; + + _mediaSettings = mediaSettings; + _contactDataSettings = contactDataSettings; + _customerSettings = customerSettings; + _catalogSettings = catalogSettings; + + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + #endregion + + #region Utilities + + private void SetProgress(DataExporterContext ctx, int loadedRecords) + { + try + { + if (!ctx.IsPreview && loadedRecords > 0) + { + int totalRecords = ctx.RecordsPerStore.Sum(x => x.Value); + + if (ctx.Request.Profile.Limit > 0 && totalRecords > ctx.Request.Profile.Limit) + totalRecords = ctx.Request.Profile.Limit; + + ctx.RecordCount = Math.Min(ctx.RecordCount + loadedRecords, totalRecords); + var msg = ctx.ProgressInfo.FormatInvariant(ctx.RecordCount, totalRecords); + ctx.Request.ProgressValueSetter.Invoke(ctx.RecordCount, totalRecords, msg); + } + } + catch { } + } + + private void SetProgress(DataExporterContext ctx, string message) + { + try + { + if (!ctx.IsPreview && message.HasValue()) + { + ctx.Request.ProgressValueSetter.Invoke(0, 0, message); + } + } + catch { } + } + + private bool HasPermission(DataExporterContext ctx) + { + if (ctx.Request.HasPermission) + return true; + + var customer = _services.WorkContext.CurrentCustomer; + + if (customer.SystemName == SystemCustomerNames.BackgroundTask) + return true; + + if (ctx.Request.Provider.Value.EntityType == ExportEntityType.Product || + ctx.Request.Provider.Value.EntityType == ExportEntityType.Category || + ctx.Request.Provider.Value.EntityType == ExportEntityType.Manufacturer) + return _services.Permissions.Authorize(StandardPermissionProvider.ManageCatalog, customer); + + if (ctx.Request.Provider.Value.EntityType == ExportEntityType.Customer) + return _services.Permissions.Authorize(StandardPermissionProvider.ManageCustomers, customer); + + if (ctx.Request.Provider.Value.EntityType == ExportEntityType.Order) + return _services.Permissions.Authorize(StandardPermissionProvider.ManageOrders, customer); + + if (ctx.Request.Provider.Value.EntityType == ExportEntityType.NewsLetterSubscription) + return _services.Permissions.Authorize(StandardPermissionProvider.ManageNewsletterSubscribers, customer); + + return true; + } + + private void DetachAllEntitiesAndClear(DataExporterContext ctx) + { + try + { + _dbContext.DetachAll(); + } + catch (Exception exception) + { + ctx.Log.Warning("Detaching all entities failed.", exception); + } + + try + { + // now again attach what is globally required + _dbContext.Attach(ctx.Request.Profile); + _dbContext.AttachRange(ctx.Stores.Values); + } + catch (Exception exception) + { + ctx.Log.Warning("Re-attaching entities failed.", exception); + } + + try + { + if (ctx.ProductExportContext != null) + ctx.ProductExportContext.Clear(); + + if (ctx.OrderExportContext != null) + ctx.OrderExportContext.Clear(); + + if (ctx.ManufacturerExportContext != null) + ctx.ManufacturerExportContext.Clear(); + + if (ctx.CategoryExportContext != null) + ctx.CategoryExportContext.Clear(); + + if (ctx.CustomerExportContext != null) + ctx.CustomerExportContext.Clear(); + } + catch { } + } + + private IExportDataSegmenterProvider CreateSegmenter(DataExporterContext ctx, int pageIndex = 0) + { + var offset = Math.Max(ctx.Request.Profile.Offset, 0) + (pageIndex * PageSize); + var limit = (ctx.IsPreview ? PageSize : Math.Max(ctx.Request.Profile.Limit, 0)); + var recordsPerSegment = (ctx.IsPreview ? 0 : Math.Max(ctx.Request.Profile.BatchSize, 0)); + var totalCount = Math.Max(ctx.Request.Profile.Offset, 0) + ctx.RecordsPerStore.First(x => x.Key == ctx.Store.Id).Value; + + switch (ctx.Request.Provider.Value.EntityType) + { + case ExportEntityType.Product: + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter + ( + skip => GetProducts(ctx, skip), + entities => + { + // load data behind navigation properties for current queue in one go + ctx.ProductExportContext = new ProductExportContext(entities, + x => _productAttributeService.Value.GetProductVariantAttributesByProductIds(x, null), + x => _productAttributeService.Value.GetProductVariantAttributeCombinations(x), + x => _productService.Value.GetTierPricesByProductIds(x, (ctx.Projection.CurrencyId ?? 0) != 0 ? ctx.ContextCustomer : null, ctx.Store.Id), + x => _categoryService.Value.GetProductCategoriesByProductIds(x, null, true), + x => _manufacturerService.Value.GetProductManufacturersByProductIds(x), + x => _productService.Value.GetProductPicturesByProductIds(x), + x => _productService.Value.GetProductTagsByProductIds(x), + x => _productService.Value.GetAppliedDiscountsByProductIds(x), + x => _productService.Value.GetProductSpecificationAttributesByProductIds(x), + x => _productService.Value.GetBundleItemsByProductIds(x, true) + ); + }, + entity => Convert(ctx, entity), + offset, PageSize, limit, recordsPerSegment, totalCount + ); + break; + + case ExportEntityType.Order: + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter + ( + skip => GetOrders(ctx, skip), + entities => + { + ctx.OrderExportContext = new OrderExportContext(entities, + x => _customerService.GetCustomersByIds(x), + x => _customerService.GetRewardPointsHistoriesByCustomerIds(x), + x => _addressService.Value.GetAddressByIds(x), + x => _orderService.Value.GetOrderItemsByOrderIds(x), + x => _shipmentService.Value.GetShipmentsByOrderIds(x) + ); + }, + entity => Convert(ctx, entity), + offset, PageSize, limit, recordsPerSegment, totalCount + ); + break; + + case ExportEntityType.Manufacturer: + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter + ( + skip => GetManufacturers(ctx, skip), + entities => + { + ctx.ManufacturerExportContext = new ManufacturerExportContext(entities, + x => _manufacturerService.Value.GetProductManufacturersByManufacturerIds(x), + x => _pictureService.Value.GetPicturesByIds(x) + ); + }, + entity => Convert(ctx, entity), + offset, PageSize, limit, recordsPerSegment, totalCount + ); + break; + + case ExportEntityType.Category: + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter + ( + skip => GetCategories(ctx, skip), + entities => + { + ctx.CategoryExportContext = new CategoryExportContext(entities, + x => _categoryService.Value.GetProductCategoriesByCategoryIds(x), + x => _pictureService.Value.GetPicturesByIds(x) + ); + }, + entity => Convert(ctx, entity), + offset, PageSize, limit, recordsPerSegment, totalCount + ); + break; + + case ExportEntityType.Customer: + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter + ( + skip => GetCustomers(ctx, skip), + entities => + { + ctx.CustomerExportContext = new CustomerExportContext(entities, + x => _genericAttributeService.Value.GetAttributesForEntity(x, "Customer") + ); + }, + entity => Convert(ctx, entity), + offset, PageSize, limit, recordsPerSegment, totalCount + ); + break; + + case ExportEntityType.NewsLetterSubscription: + ctx.ExecuteContext.DataSegmenter = new ExportDataSegmenter + ( + skip => GetNewsLetterSubscriptions(ctx, skip), + null, + entity => Convert(ctx, entity), + offset, PageSize, limit, recordsPerSegment, totalCount + ); + break; + + default: + ctx.ExecuteContext.DataSegmenter = null; + break; + } + + return ctx.ExecuteContext.DataSegmenter as IExportDataSegmenterProvider; + } + + private bool CallProvider(DataExporterContext ctx, string streamId, string method, string path) + { + if (method != "Execute" && method != "OnExecuted") + throw new SmartException("Unknown export method {0}.".FormatInvariant(method.NaIfEmpty())); + + try + { + ctx.ExecuteContext.DataStreamId = streamId; + + using (ctx.ExecuteContext.DataStream = new MemoryStream()) + { + if (method == "Execute") + { + ctx.Request.Provider.Value.Execute(ctx.ExecuteContext); + } + else if (method == "OnExecuted") + { + ctx.Request.Provider.Value.OnExecuted(ctx.ExecuteContext); + } + + if (ctx.IsFileBasedExport && path.HasValue() && ctx.ExecuteContext.DataStream.Length > 0) + { + if (!ctx.ExecuteContext.DataStream.CanSeek) + { + ctx.Log.Warning("Data stream seems to be closed!"); + } + + ctx.ExecuteContext.DataStream.Seek(0, SeekOrigin.Begin); + + using (_rwLock.GetWriteLock()) + using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite)) + { + ctx.Log.Information("Creating file {0}.".FormatInvariant(path)); + ctx.ExecuteContext.DataStream.CopyTo(fileStream); + } + } + } + } + catch (Exception exception) + { + ctx.ExecuteContext.Abort = DataExchangeAbortion.Hard; + ctx.Log.Error("The provider failed at the {0} method: {1}.".FormatInvariant(method, exception.ToAllMessages()), exception); + ctx.Result.LastError = exception.ToString(); + } + finally + { + if (ctx.ExecuteContext.DataStream != null) + { + ctx.ExecuteContext.DataStream.Dispose(); + ctx.ExecuteContext.DataStream = null; + } + + if (ctx.ExecuteContext.Abort == DataExchangeAbortion.Hard && ctx.IsFileBasedExport && path.HasValue()) + { + FileSystemHelper.Delete(path); + } + } + + return (ctx.ExecuteContext.Abort != DataExchangeAbortion.Hard); + } + + private bool Deploy(DataExporterContext ctx, string zipPath) + { + var allSucceeded = true; + var deployments = ctx.Request.Profile.Deployments.OrderBy(x => x.DeploymentTypeId).Where(x => x.Enabled); + + if (deployments.Count() == 0) + return false; + + var context = new ExportDeploymentContext + { + T = T, + Log = ctx.Log, + FolderContent = ctx.FolderContent, + ZipPath = zipPath, + CreateZipArchive = ctx.Request.Profile.CreateZipArchive + }; + + foreach (var deployment in deployments) + { + IFilePublisher publisher = null; + + context.Result = new DataDeploymentResult + { + LastExecutionUtc = DateTime.UtcNow + }; + + try + { + switch (deployment.DeploymentType) + { + case ExportDeploymentType.Email: + publisher = new EmailFilePublisher(_emailAccountService.Value, _queuedEmailService.Value); + break; + case ExportDeploymentType.FileSystem: + publisher = new FileSystemFilePublisher(); + break; + case ExportDeploymentType.Ftp: + publisher = new FtpFilePublisher(); + break; + case ExportDeploymentType.Http: + publisher = new HttpFilePublisher(); + break; + case ExportDeploymentType.PublicFolder: + publisher = new PublicFolderPublisher(); + break; + } + + if (publisher != null) + { + publisher.Publish(context, deployment); + + if (!context.Result.Succeeded) + allSucceeded = false; + } + } + catch (Exception exception) + { + allSucceeded = false; + + if (context.Result != null) + { + context.Result.LastError = exception.ToAllMessages(); + } + + ctx.Log.Error("Deployment \"{0}\" of type {1} failed: {2}".FormatInvariant( + deployment.Name, deployment.DeploymentType.ToString(), exception.Message), exception); + } + + deployment.ResultInfo = XmlHelper.Serialize(context.Result); + + _exportProfileService.Value.UpdateExportDeployment(deployment); + } + + return allSucceeded; + } + + private void SendCompletionEmail(DataExporterContext ctx, string zipPath) + { + var emailAccount = _emailAccountService.Value.GetEmailAccountById(ctx.Request.Profile.EmailAccountId); + + if (emailAccount == null) + emailAccount = _emailAccountService.Value.GetDefaultEmailAccount(); + + var downloadUrl = "{0}Admin/Export/DownloadExportFile/{1}?name=".FormatInvariant(_services.WebHelper.GetStoreLocation(ctx.Store.SslEnabled), ctx.Request.Profile.Id); + + var languageId = ctx.Projection.LanguageId ?? 0; + var smtpContext = new SmtpContext(emailAccount); + var message = new EmailMessage(); + + var storeInfo = "{0} ({1})".FormatInvariant(ctx.Store.Name, ctx.Store.Url); + var intro =_services.Localization.GetResource("Admin.DataExchange.Export.CompletedEmail.Body", languageId).FormatInvariant(storeInfo); + var body = new StringBuilder(intro); + + if (ctx.Result.LastError.HasValue()) + { + body.AppendFormat("

    {0}

    ", ctx.Result.LastError); + } + + if (ctx.IsFileBasedExport && File.Exists(zipPath)) + { + var fileName = Path.GetFileName(zipPath); + body.AppendFormat("

    {2}

    ", downloadUrl, HttpUtility.UrlEncode(fileName), fileName); + } + + if (ctx.IsFileBasedExport && ctx.Result.Files.Any()) + { + body.Append("

    "); + foreach (var file in ctx.Result.Files) + { + body.AppendFormat("

    ", downloadUrl, HttpUtility.UrlEncode(file.FileName), file.FileName); + } + body.Append("

    "); + } + + message.From = new EmailAddress(emailAccount.Email, emailAccount.DisplayName); + + if (ctx.Request.Profile.CompletedEmailAddresses.HasValue()) + message.To.AddRange(ctx.Request.Profile.CompletedEmailAddresses.SplitSafe(",").Where(x => x.IsEmail()).Select(x => new EmailAddress(x))); + + if (message.To.Count == 0 && _contactDataSettings.Value.CompanyEmailAddress.HasValue()) + message.To.Add(new EmailAddress(_contactDataSettings.Value.CompanyEmailAddress)); + + if (message.To.Count == 0) + message.To.Add(new EmailAddress(emailAccount.Email, emailAccount.DisplayName)); + + message.Subject = _services.Localization.GetResource("Admin.DataExchange.Export.CompletedEmail.Subject", languageId) + .FormatInvariant(ctx.Request.Profile.Name); + + message.Body = body.ToString(); + + _emailSender.Value.SendEmail(smtpContext, message); + + //_queuedEmailService.Value.InsertQueuedEmail(new QueuedEmail + //{ + // From = emailAccount.Email, + // FromName = emailAccount.DisplayName, + // To = message.To.First().Address, + // Subject = message.Subject, + // Body = message.Body, + // CreatedOnUtc = DateTime.UtcNow, + // EmailAccountId = emailAccount.Id, + // SendManually = true + //}); + //_dbContext.SaveChanges(); + } + + #endregion + + #region Getting data + + private IQueryable GetProductQuery(DataExporterContext ctx, int skip, int take) + { + IQueryable query = null; + + if (ctx.Request.ProductQuery == null) + { + var searchContext = new ProductSearchContext + { + OrderBy = ProductSortingEnum.CreatedOn, + ProductIds = ctx.Request.EntitiesToExport, + StoreId = (ctx.Request.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId), + VisibleIndividuallyOnly = true, + PriceMin = ctx.Filter.PriceMinimum, + PriceMax = ctx.Filter.PriceMaximum, + IsPublished = ctx.Filter.IsPublished, + WithoutCategories = ctx.Filter.WithoutCategories, + WithoutManufacturers = ctx.Filter.WithoutManufacturers, + ManufacturerId = ctx.Filter.ManufacturerId ?? 0, + FeaturedProducts = ctx.Filter.FeaturedProducts, + ProductType = ctx.Filter.ProductType, + ProductTagId = ctx.Filter.ProductTagId ?? 0, + IdMin = ctx.Filter.IdMinimum ?? 0, + IdMax = ctx.Filter.IdMaximum ?? 0, + AvailabilityMinimum = ctx.Filter.AvailabilityMinimum, + AvailabilityMaximum = ctx.Filter.AvailabilityMaximum + }; + + if (!ctx.Filter.IsPublished.HasValue) + searchContext.ShowHidden = true; + + if (ctx.Filter.CategoryIds != null && ctx.Filter.CategoryIds.Length > 0) + searchContext.CategoryIds = ctx.Filter.CategoryIds.ToList(); + + if (ctx.Filter.CreatedFrom.HasValue) + searchContext.CreatedFromUtc = _dateTimeHelper.Value.ConvertToUtcTime(ctx.Filter.CreatedFrom.Value, _dateTimeHelper.Value.CurrentTimeZone); + + if (ctx.Filter.CreatedTo.HasValue) + searchContext.CreatedToUtc = _dateTimeHelper.Value.ConvertToUtcTime(ctx.Filter.CreatedTo.Value, _dateTimeHelper.Value.CurrentTimeZone); + + query = _productService.Value.PrepareProductSearchQuery(searchContext); + + query = query.OrderByDescending(x => x.CreatedOnUtc); + } + else + { + query = ctx.Request.ProductQuery; + } + + if (skip > 0) + query = query.Skip(skip); + + if (take != int.MaxValue) + query = query.Take(take); + + return query; + } + + private List GetProducts(DataExporterContext ctx, int skip) + { + // we use ctx.EntityIdsPerSegment to avoid exporting products multiple times per segment\file (cause of associated products). + + var result = new List(); + + var products = GetProductQuery(ctx, skip, PageSize).ToList(); + + foreach (var product in products) + { + if (product.ProductType == ProductType.SimpleProduct || product.ProductType == ProductType.BundledProduct) + { + if (!ctx.EntityIdsPerSegment.Contains(product.Id)) + { + result.Add(product); + ctx.EntityIdsPerSegment.Add(product.Id); + } + } + else if (product.ProductType == ProductType.GroupedProduct) + { + if (ctx.Projection.NoGroupedProducts && !ctx.IsPreview) + { + var associatedSearchContext = new ProductSearchContext + { + OrderBy = ProductSortingEnum.Position, + PageSize = int.MaxValue, + StoreId = (ctx.Request.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId), + VisibleIndividuallyOnly = true, + ParentGroupedProductId = product.Id + }; + + foreach (var associatedProduct in _productService.Value.SearchProducts(associatedSearchContext)) + { + if (!ctx.EntityIdsPerSegment.Contains(associatedProduct.Id)) + { + result.Add(associatedProduct); + ctx.EntityIdsPerSegment.Add(associatedProduct.Id); + } + } + } + else + { + if (!ctx.EntityIdsPerSegment.Contains(product.Id)) + { + result.Add(product); + ctx.EntityIdsPerSegment.Add(product.Id); + } + } + } + } + + SetProgress(ctx, products.Count); + + return result; + } + + private IQueryable GetOrderQuery(DataExporterContext ctx, int skip, int take) + { + var query = _orderService.Value.GetOrders( + ctx.Request.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId, + ctx.Projection.CustomerId ?? 0, + ctx.Filter.CreatedFrom.HasValue ? (DateTime?)_dateTimeHelper.Value.ConvertToUtcTime(ctx.Filter.CreatedFrom.Value, _dateTimeHelper.Value.CurrentTimeZone) : null, + ctx.Filter.CreatedTo.HasValue ? (DateTime?)_dateTimeHelper.Value.ConvertToUtcTime(ctx.Filter.CreatedTo.Value, _dateTimeHelper.Value.CurrentTimeZone) : null, + ctx.Filter.OrderStatusIds, + ctx.Filter.PaymentStatusIds, + ctx.Filter.ShippingStatusIds, + null, + null, + null); + + if (ctx.Request.EntitiesToExport.Any()) + query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); + + query = query.OrderByDescending(x => x.CreatedOnUtc); + + if (skip > 0) + query = query.Skip(skip); + + if (take != int.MaxValue) + query = query.Take(take); + + return query; + } + + private List GetOrders(DataExporterContext ctx, int skip) + { + var orders = GetOrderQuery(ctx, skip, PageSize).ToList(); + + if (ctx.Projection.OrderStatusChange != ExportOrderStatusChange.None) + { + ctx.SetLoadedEntityIds(orders.Select(x => x.Id)); + } + + SetProgress(ctx, orders.Count); + + return orders; + } + + private IQueryable GetManufacturerQuery(DataExporterContext ctx, int skip, int take) + { + var showHidden = !ctx.Filter.IsPublished.HasValue; + var storeId = (ctx.Request.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId); + + var query = _manufacturerService.Value.GetManufacturers(showHidden, storeId); + + if (ctx.Request.EntitiesToExport.Any()) + query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); + + query = query.OrderBy(x => x.DisplayOrder); + + if (skip > 0) + query = query.Skip(skip); + + if (take != int.MaxValue) + query = query.Take(take); + + return query; + } + + private List GetManufacturers(DataExporterContext ctx, int skip) + { + var manus = GetManufacturerQuery(ctx, skip, PageSize).ToList(); + + SetProgress(ctx, manus.Count); + + return manus; + } + + private IQueryable GetCategoryQuery(DataExporterContext ctx, int skip, int take) + { + var showHidden = !ctx.Filter.IsPublished.HasValue; + var storeId = (ctx.Request.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId); + + var query = _categoryService.Value.GetCategories(null, showHidden, null, true, storeId); + + if (ctx.Request.EntitiesToExport.Any()) + query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); + + query = query + .OrderBy(x => x.ParentCategoryId) + .ThenBy(x => x.DisplayOrder); + + if (skip > 0) + query = query.Skip(skip); + + if (take != int.MaxValue) + query = query.Take(take); + + return query; + } + + private List GetCategories(DataExporterContext ctx, int skip) + { + var categories = GetCategoryQuery(ctx, skip, PageSize).ToList(); + + SetProgress(ctx, categories.Count); + + return categories; + } + + private IQueryable GetCustomerQuery(DataExporterContext ctx, int skip, int take) + { + var query = _customerRepository.Value.TableUntracked + .Expand(x => x.BillingAddress) + .Expand(x => x.ShippingAddress) + .Expand(x => x.Addresses.Select(y => y.Country)) + .Expand(x => x.Addresses.Select(y => y.StateProvince)) + .Expand(x => x.CustomerRoles) + .Where(x => !x.Deleted); + + if (ctx.Filter.IsActiveCustomer.HasValue) + query = query.Where(x => x.Active == ctx.Filter.IsActiveCustomer.Value); + + if (ctx.Filter.IsTaxExempt.HasValue) + query = query.Where(x => x.IsTaxExempt == ctx.Filter.IsTaxExempt.Value); + + if (ctx.Filter.CustomerRoleIds != null && ctx.Filter.CustomerRoleIds.Length > 0) + query = query.Where(x => x.CustomerRoles.Select(y => y.Id).Intersect(ctx.Filter.CustomerRoleIds).Any()); + + if (ctx.Filter.BillingCountryIds != null && ctx.Filter.BillingCountryIds.Length > 0) + query = query.Where(x => x.BillingAddress != null && ctx.Filter.BillingCountryIds.Contains(x.BillingAddress.Id)); + + if (ctx.Filter.ShippingCountryIds != null && ctx.Filter.ShippingCountryIds.Length > 0) + query = query.Where(x => x.ShippingAddress != null && ctx.Filter.ShippingCountryIds.Contains(x.ShippingAddress.Id)); + + if (ctx.Filter.LastActivityFrom.HasValue) + { + var activityFrom = _dateTimeHelper.Value.ConvertToUtcTime(ctx.Filter.LastActivityFrom.Value, _dateTimeHelper.Value.CurrentTimeZone); + query = query.Where(x => activityFrom <= x.LastActivityDateUtc); + } + + if (ctx.Filter.LastActivityTo.HasValue) + { + var activityTo = _dateTimeHelper.Value.ConvertToUtcTime(ctx.Filter.LastActivityTo.Value, _dateTimeHelper.Value.CurrentTimeZone); + query = query.Where(x => activityTo >= x.LastActivityDateUtc); + } + + if (ctx.Filter.HasSpentAtLeastAmount.HasValue) + { + query = query + .Join(_orderRepository.Value.Table, x => x.Id, y => y.CustomerId, (x, y) => new { Customer = x, Order = y }) + .GroupBy(x => x.Customer.Id) + .Select(x => new + { + Customer = x.FirstOrDefault().Customer, + OrderTotal = x.Sum(y => y.Order.OrderTotal) + }) + .Where(x => x.OrderTotal >= ctx.Filter.HasSpentAtLeastAmount.Value) + .Select(x => x.Customer); + } + + if (ctx.Filter.HasPlacedAtLeastOrders.HasValue) + { + query = query + .Join(_orderRepository.Value.Table, x => x.Id, y => y.CustomerId, (x, y) => new { Customer = x, Order = y }) + .GroupBy(x => x.Customer.Id) + .Select(x => new + { + Customer = x.FirstOrDefault().Customer, + OrderCount = x.Count() + }) + .Where(x => x.OrderCount >= ctx.Filter.HasPlacedAtLeastOrders.Value) + .Select(x => x.Customer); + } + + if (ctx.Request.EntitiesToExport.Any()) + query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); + + query = query.OrderByDescending(x => x.CreatedOnUtc); + + if (skip > 0) + query = query.Skip(skip); + + if (take != int.MaxValue) + query = query.Take(take); + + return query; + } + + private List GetCustomers(DataExporterContext ctx, int skip) + { + var customers = GetCustomerQuery(ctx, skip, PageSize).ToList(); + + SetProgress(ctx, customers.Count); + + return customers; + } + + private IQueryable GetNewsLetterSubscriptionQuery(DataExporterContext ctx, int skip, int take) + { + var storeId = (ctx.Request.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId); + + var query = _subscriptionRepository.Value.TableUntracked; + + if (storeId > 0) + query = query.Where(x => x.StoreId == storeId); + + if (ctx.Filter.IsActiveSubscriber.HasValue) + query = query.Where(x => x.Active == ctx.Filter.IsActiveSubscriber.Value); + + if (ctx.Filter.CreatedFrom.HasValue) + { + var createdFrom = _dateTimeHelper.Value.ConvertToUtcTime(ctx.Filter.CreatedFrom.Value, _dateTimeHelper.Value.CurrentTimeZone); + query = query.Where(x => createdFrom <= x.CreatedOnUtc); + } + + if (ctx.Filter.CreatedTo.HasValue) + { + var createdTo = _dateTimeHelper.Value.ConvertToUtcTime(ctx.Filter.CreatedTo.Value, _dateTimeHelper.Value.CurrentTimeZone); + query = query.Where(x => createdTo >= x.CreatedOnUtc); + } + + if (ctx.Request.EntitiesToExport.Any()) + query = query.Where(x => ctx.Request.EntitiesToExport.Contains(x.Id)); + + query = query + .OrderBy(x => x.StoreId) + .ThenBy(x => x.Email); + + if (skip > 0) + query = query.Skip(skip); + + if (take != int.MaxValue) + query = query.Take(take); + + return query; + } + + private List GetNewsLetterSubscriptions(DataExporterContext ctx, int skip) + { + var subscriptions = GetNewsLetterSubscriptionQuery(ctx, skip, PageSize).ToList(); + + SetProgress(ctx, subscriptions.Count); + + return subscriptions; + } + + #endregion + + private List Init(DataExporterContext ctx, int? totalRecords = null) + { + // Init base things that are even required for preview. Init all other things (regular export) in ExportCoreOuter. + List result = null; + + if (ctx.Projection.CurrencyId.HasValue) + ctx.ContextCurrency = _currencyService.Value.GetCurrencyById(ctx.Projection.CurrencyId.Value); + else + ctx.ContextCurrency = _services.WorkContext.WorkingCurrency; + + if (ctx.Projection.CustomerId.HasValue) + ctx.ContextCustomer = _customerService.GetCustomerById(ctx.Projection.CustomerId.Value); + else + ctx.ContextCustomer = _services.WorkContext.CurrentCustomer; + + if (ctx.Projection.LanguageId.HasValue) + ctx.ContextLanguage = _languageService.Value.GetLanguageById(ctx.Projection.LanguageId.Value); + else + ctx.ContextLanguage = _services.WorkContext.WorkingLanguage; + + ctx.Stores = _services.StoreService.GetAllStores().ToDictionary(x => x.Id, x => x); + ctx.Languages = _languageService.Value.GetAllLanguages(true).ToDictionary(x => x.Id, x => x); + + if (!ctx.IsPreview && ctx.Request.Profile.PerStore) + { + result = new List(ctx.Stores.Values.Where(x => x.Id == ctx.Filter.StoreId || ctx.Filter.StoreId == 0)); + } + else + { + int? storeId = (ctx.Filter.StoreId == 0 ? ctx.Projection.StoreId : ctx.Filter.StoreId); + + ctx.Store = ctx.Stores.Values.FirstOrDefault(x => x.Id == (storeId ?? _services.StoreContext.CurrentStore.Id)); + + result = new List { ctx.Store }; + } + + // get total records for progress + foreach (var store in result) + { + ctx.Store = store; + + int totalCount = 0; + + if (totalRecords.HasValue) + { + totalCount = totalRecords.Value; // speed up preview by not counting total at each page + } + else + { + switch (ctx.Request.Provider.Value.EntityType) + { + case ExportEntityType.Product: + totalCount = GetProductQuery(ctx, ctx.Request.Profile.Offset, int.MaxValue).Count(); + break; + case ExportEntityType.Order: + totalCount = GetOrderQuery(ctx, ctx.Request.Profile.Offset, int.MaxValue).Count(); + break; + case ExportEntityType.Manufacturer: + totalCount = GetManufacturerQuery(ctx, ctx.Request.Profile.Offset, int.MaxValue).Count(); + break; + case ExportEntityType.Category: + totalCount = GetCategoryQuery(ctx, ctx.Request.Profile.Offset, int.MaxValue).Count(); + break; + case ExportEntityType.Customer: + totalCount = GetCustomerQuery(ctx, ctx.Request.Profile.Offset, int.MaxValue).Count(); + break; + case ExportEntityType.NewsLetterSubscription: + totalCount = GetNewsLetterSubscriptionQuery(ctx, ctx.Request.Profile.Offset, int.MaxValue).Count(); + break; + } + } + + ctx.RecordsPerStore.Add(store.Id, totalCount); + } + + return result; + } + + private void ExportCoreInner(DataExporterContext ctx, Store store) + { + if (ctx.ExecuteContext.Abort != DataExchangeAbortion.None) + return; + + var fileIndex = 0; + var dataExchangeSettings = _services.Settings.LoadSetting(store.Id); + + ctx.Store = store; + + { + var logHead = new StringBuilder(); + logHead.AppendLine(); + logHead.AppendLine(new string('-', 40)); + logHead.AppendLine("SmartStore.NET:\t\tv." + SmartStoreVersion.CurrentFullVersion); + logHead.Append("Export profile:\t\t" + ctx.Request.Profile.Name); + logHead.AppendLine(ctx.Request.Profile.Id == 0 ? " (volatile)" : " (Id {0})".FormatInvariant(ctx.Request.Profile.Id)); + + if (ctx.Request.Provider.Metadata.FriendlyName.HasValue()) + logHead.AppendLine("Export provider:\t{0} ({1})".FormatInvariant(ctx.Request.Provider.Metadata.FriendlyName, ctx.Request.Profile.ProviderSystemName)); + else + logHead.AppendLine("Export provider:\t{0}".FormatInvariant(ctx.Request.Profile.ProviderSystemName)); + + var plugin = ctx.Request.Provider.Metadata.PluginDescriptor; + logHead.Append("Plugin:\t\t\t"); + logHead.AppendLine(plugin == null ? "".NaIfEmpty() : "{0} ({1}) v.{2}".FormatInvariant(plugin.FriendlyName, plugin.SystemName, plugin.Version.ToString())); + + logHead.AppendLine("Entity:\t\t\t" + ctx.Request.Provider.Value.EntityType.ToString()); + + try + { + var uri = new Uri(store.Url); + logHead.AppendLine("Store:\t\t\t{0} (Id {1})".FormatInvariant(uri.DnsSafeHost.NaIfEmpty(), ctx.Store.Id)); + } + catch { } + + var customer = _services.WorkContext.CurrentCustomer; + logHead.Append("Executed by:\t\t" + (customer.Email.HasValue() ? customer.Email : customer.SystemName)); + + ctx.Log.Information(logHead.ToString()); + } + + ctx.ExecuteContext.Store = ToDynamic(ctx, ctx.Store); + ctx.ExecuteContext.MaxFileNameLength = dataExchangeSettings.MaxFileNameLength; + + var publicDeployment = ctx.Request.Profile.Deployments.FirstOrDefault(x => x.DeploymentType == ExportDeploymentType.PublicFolder); + ctx.ExecuteContext.HasPublicDeployment = (publicDeployment != null); + ctx.ExecuteContext.PublicFolderPath = publicDeployment.GetDeploymentFolder(true); + + var fileExtension = (ctx.Request.Provider.Value.FileExtension.HasValue() ? ctx.Request.Provider.Value.FileExtension.ToLower().EnsureStartsWith(".") : ""); + + + using (var segmenter = CreateSegmenter(ctx)) + { + if (segmenter == null) + { + throw new SmartException("Unsupported entity type '{0}'.".FormatInvariant(ctx.Request.Provider.Value.EntityType.ToString())); + } + + if (segmenter.TotalRecords <= 0) + { + ctx.Log.Information("There are no records to export."); + } + + while (ctx.ExecuteContext.Abort == DataExchangeAbortion.None && segmenter.HasData) + { + segmenter.RecordPerSegmentCount = 0; + ctx.ExecuteContext.RecordsSucceeded = 0; + + string path = null; + + if (ctx.IsFileBasedExport) + { + var resolvedPattern = ctx.Request.Profile.ResolveFileNamePattern(ctx.Store, ++fileIndex, ctx.ExecuteContext.MaxFileNameLength); + + ctx.ExecuteContext.FileName = resolvedPattern + fileExtension; + path = Path.Combine(ctx.ExecuteContext.Folder, ctx.ExecuteContext.FileName); + } + + if (CallProvider(ctx, null, "Execute", path)) + { + ctx.Log.Information("Provider reports {0} successfully exported record(s).".FormatInvariant(ctx.ExecuteContext.RecordsSucceeded)); + + if (ctx.IsFileBasedExport && File.Exists(path)) + { + ctx.Result.Files.Add(new DataExportResult.ExportFileInfo + { + StoreId = ctx.Store.Id, + FileName = ctx.ExecuteContext.FileName + }); + } + } + + ctx.EntityIdsPerSegment.Clear(); + + if (ctx.ExecuteContext.IsMaxFailures) + ctx.Log.Warning("Export aborted. The maximum number of failures has been reached."); + + if (ctx.CancellationToken.IsCancellationRequested) + ctx.Log.Warning("Export aborted. A cancellation has been requested."); + + DetachAllEntitiesAndClear(ctx); + } + + if (ctx.ExecuteContext.Abort != DataExchangeAbortion.Hard) + { + // always call OnExecuted + if (ctx.ExecuteContext.ExtraDataUnits.Count == 0) + ctx.ExecuteContext.ExtraDataUnits.Add(new ExportDataUnit()); + + ctx.ExecuteContext.ExtraDataUnits.ForEach(x => + { + var path = (x.FileName.HasValue() ? Path.Combine(ctx.ExecuteContext.Folder, x.FileName) : null); + CallProvider(ctx, x.Id, "OnExecuted", path); + }); + + ctx.ExecuteContext.ExtraDataUnits.Clear(); + } + } + } + + private void ExportCoreOuter(DataExporterContext ctx) + { + if (ctx.Request.Profile == null || !ctx.Request.Profile.Enabled) + return; + + var logPath = ctx.Request.Profile.GetExportLogPath(); + var zipPath = ctx.Request.Profile.GetExportZipPath(); + + FileSystemHelper.Delete(logPath); + FileSystemHelper.Delete(zipPath); + FileSystemHelper.ClearDirectory(ctx.FolderContent, false); + + using (var logger = new TraceLogger(logPath)) + { + try + { + ctx.Log = logger; + ctx.ExecuteContext.Log = logger; + ctx.ProgressInfo = T("Admin.DataExchange.Export.ProgressInfo"); + + if (!ctx.Request.Provider.IsValid()) + throw new SmartException("Export aborted because the export provider is not valid."); + + if (!HasPermission(ctx)) + throw new SmartException("You do not have permission to perform the selected export."); + + foreach (var item in ctx.Request.CustomData) + { + ctx.ExecuteContext.CustomProperties.Add(item.Key, item.Value); + } + + if (ctx.Request.Profile.ProviderConfigData.HasValue()) + { + var configInfo = ctx.Request.Provider.Value.ConfigurationInfo; + if (configInfo != null) + { + ctx.ExecuteContext.ConfigurationData = XmlHelper.Deserialize(ctx.Request.Profile.ProviderConfigData, configInfo.ModelType); + } + } + + // lazyLoading: false, proxyCreation: false impossible. how to identify all properties of all data levels of all entities + // that require manual resolving for now and for future? fragile, susceptible to faults (e.g. price calculation)... + using (var scope = new DbContextScope(_dbContext, autoDetectChanges: false, proxyCreation: true, validateOnSave: false, forceNoTracking: true)) + { + ctx.DeliveryTimes = _deliveryTimeService.Value.GetAllDeliveryTimes().ToDictionary(x => x.Id); + ctx.QuantityUnits = _quantityUnitService.Value.GetAllQuantityUnits().ToDictionary(x => x.Id); + ctx.ProductTemplates = _productTemplateService.Value.GetAllProductTemplates().ToDictionary(x => x.Id, x => x.ViewPath); + ctx.CategoryTemplates = _categoryTemplateService.Value.GetAllCategoryTemplates().ToDictionary(x => x.Id, x => x.ViewPath); + + if (ctx.Request.Provider.Value.EntityType == ExportEntityType.Product) + { + var allCategories = _categoryService.Value.GetAllCategories(showHidden: true, applyNavigationFilters: false); + ctx.Categories = allCategories.ToDictionary(x => x.Id); + } + + if (ctx.Request.Provider.Value.EntityType == ExportEntityType.Order) + { + ctx.Countries = _countryService.Value.GetAllCountries(true).ToDictionary(x => x.Id, x => x); + } + + if (ctx.Request.Provider.Value.EntityType == ExportEntityType.Customer) + { + var subscriptionEmails = _subscriptionRepository.Value.TableUntracked + .Where(x => x.Active) + .Select(x => x.Email) + .Distinct() + .ToList(); + + ctx.NewsletterSubscriptions = new HashSet(subscriptionEmails, StringComparer.OrdinalIgnoreCase); + } + + var stores = Init(ctx); + + ctx.ExecuteContext.Language = ToDynamic(ctx, ctx.ContextLanguage); + ctx.ExecuteContext.Customer = ToDynamic(ctx, ctx.ContextCustomer); + ctx.ExecuteContext.Currency = ToDynamic(ctx, ctx.ContextCurrency); + + stores.ForEach(x => ExportCoreInner(ctx, x)); + } + + if (!ctx.IsPreview && ctx.ExecuteContext.Abort != DataExchangeAbortion.Hard) + { + if (ctx.IsFileBasedExport) + { + if (ctx.Request.Profile.CreateZipArchive) + { + ZipFile.CreateFromDirectory(ctx.FolderContent, zipPath, CompressionLevel.Fastest, false); + } + + if (ctx.Request.Profile.Deployments.Any(x => x.Enabled)) + { + SetProgress(ctx, T("Common.Publishing")); + + var allDeploymentsSucceeded = Deploy(ctx, zipPath); + + if (allDeploymentsSucceeded && ctx.Request.Profile.Cleanup) + { + logger.Information("Cleaning up export folder"); + + FileSystemHelper.ClearDirectory(ctx.FolderContent, false); + } + } + } + + if (ctx.Request.Profile.EmailAccountId != 0 && ctx.Request.Profile.CompletedEmailAddresses.HasValue()) + { + SendCompletionEmail(ctx, zipPath); + } + else if (ctx.Request.Profile.IsSystemProfile && !ctx.Supports(ExportFeatures.CanOmitCompletionMail)) + { + SendCompletionEmail(ctx, zipPath); + } + } + } + catch (Exception exception) + { + logger.ErrorsAll(exception); + ctx.Result.LastError = exception.ToString(); + } + finally + { + try + { + if (!ctx.IsPreview && ctx.Request.Profile.Id != 0) + { + ctx.Request.Profile.ResultInfo = XmlHelper.Serialize(ctx.Result); + + _exportProfileService.Value.UpdateExportProfile(ctx.Request.Profile); + } + } + catch (Exception exception) + { + logger.ErrorsAll(exception); + } + + DetachAllEntitiesAndClear(ctx); + + try + { + ctx.NewsletterSubscriptions.Clear(); + ctx.ProductTemplates.Clear(); + ctx.CategoryTemplates.Clear(); + ctx.Countries.Clear(); + ctx.Languages.Clear(); + ctx.QuantityUnits.Clear(); + ctx.DeliveryTimes.Clear(); + ctx.CategoryPathes.Clear(); + ctx.Categories.Clear(); + ctx.Stores.Clear(); + + ctx.Request.CustomData.Clear(); + + ctx.ExecuteContext.CustomProperties.Clear(); + ctx.ExecuteContext.Log = null; + ctx.Log = null; + } + catch (Exception exception) + { + logger.ErrorsAll(exception); + } + } + } + + if (ctx.IsPreview || ctx.ExecuteContext.Abort == DataExchangeAbortion.Hard) + return; + + // post process order entities + if (ctx.EntityIdsLoaded.Any() && ctx.Request.Provider.Value.EntityType == ExportEntityType.Order && ctx.Projection.OrderStatusChange != ExportOrderStatusChange.None) + { + using (var logger = new TraceLogger(logPath)) + { + try + { + int? orderStatusId = null; + + if (ctx.Projection.OrderStatusChange == ExportOrderStatusChange.Processing) + orderStatusId = (int)OrderStatus.Processing; + else if (ctx.Projection.OrderStatusChange == ExportOrderStatusChange.Complete) + orderStatusId = (int)OrderStatus.Complete; + + using (var scope = new DbContextScope(_dbContext, false, null, false, false, false, false)) + { + foreach (var chunk in ctx.EntityIdsLoaded.Chunk()) + { + var entities = _orderRepository.Value.Table.Where(x => chunk.Contains(x.Id)).ToList(); + + entities.ForEach(x => x.OrderStatusId = (orderStatusId ?? x.OrderStatusId)); + + _dbContext.SaveChanges(); + } + } + + logger.Information("Updated order status for {0} order(s).".FormatInvariant(ctx.EntityIdsLoaded.Count())); + } + catch (Exception exception) + { + logger.ErrorsAll(exception); + ctx.Result.LastError = exception.ToString(); + } + } + } + } + + /// + /// The name of the public export folder + /// + public static string PublicFolder + { + get { return "Exchange"; } + } + + public static int PageSize + { + get { return 100; } + } + + public DataExportResult Export(DataExportRequest request, CancellationToken cancellationToken) + { + var ctx = new DataExporterContext(request, cancellationToken); + + ExportCoreOuter(ctx); + + cancellationToken.ThrowIfCancellationRequested(); + + return ctx.Result; + } + + public IList Preview(DataExportRequest request, int pageIndex, int? totalRecords = null) + { + var resultData = new List(); + var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(5.0)); + + var ctx = new DataExporterContext(request, cancellation.Token, true); + + var unused = Init(ctx, totalRecords); + + if (!HasPermission(ctx)) + throw new SmartException(T("Admin.AccessDenied")); + + using (var segmenter = CreateSegmenter(ctx, pageIndex)) + { + if (segmenter == null) + { + throw new SmartException(T("Admin.Common.UnsupportedEntityType", ctx.Request.Provider.Value.EntityType.ToString())); + } + + while (segmenter.HasData) + { + segmenter.RecordPerSegmentCount = 0; + + while (segmenter.ReadNextSegment()) + { + resultData.AddRange(segmenter.CurrentSegment); + } + } + + DetachAllEntitiesAndClear(ctx); + } + + if (ctx.Result.LastError.HasValue()) + { + _services.Notifier.Error(ctx.Result.LastError); + } + + return resultData; + } + + public int GetDataCount(DataExportRequest request) + { + var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(5.0)); + + var ctx = new DataExporterContext(request, cancellation.Token, true); + + var unused = Init(ctx); + + var totalCount = ctx.RecordsPerStore.First().Value; + + return totalCount; + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/DataDeploymentResult.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/DataDeploymentResult.cs new file mode 100644 index 0000000000..7fff7a46e5 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/DataDeploymentResult.cs @@ -0,0 +1,23 @@ +using System; + +namespace SmartStore.Services.DataExchange.Export.Deployment +{ + [Serializable] + public class DataDeploymentResult + { + /// + /// Whether the deployment succeeded + /// + public bool Succeeded + { + get { return LastError.IsEmpty(); } + } + + public string LastError { get; set; } + + /// + /// Last execution date + /// + public DateTime LastExecutionUtc { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/EmailFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/EmailFilePublisher.cs new file mode 100644 index 0000000000..298d476c41 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/EmailFilePublisher.cs @@ -0,0 +1,65 @@ +using System; +using System.IO; +using System.Linq; +using SmartStore.Core.Domain; +using SmartStore.Core.Domain.Messages; +using SmartStore.Core.Email; +using SmartStore.Core.IO; +using SmartStore.Core.Logging; +using SmartStore.Services.Messages; + +namespace SmartStore.Services.DataExchange.Export.Deployment +{ + public class EmailFilePublisher : IFilePublisher + { + private IEmailAccountService _emailAccountService; + private IQueuedEmailService _queuedEmailService; + + public EmailFilePublisher( + IEmailAccountService emailAccountService, + IQueuedEmailService queuedEmailService) + { + _emailAccountService = emailAccountService; + _queuedEmailService = queuedEmailService; + } + + public virtual void Publish(ExportDeploymentContext context, ExportDeployment deployment) + { + var emailAccount = _emailAccountService.GetEmailAccountById(deployment.EmailAccountId); + var smtpContext = new SmtpContext(emailAccount); + var count = 0; + + foreach (var email in deployment.EmailAddresses.SplitSafe(",").Where(x => x.IsEmail())) + { + var queuedEmail = new QueuedEmail + { + From = emailAccount.Email, + FromName = emailAccount.DisplayName, + SendManually = false, + To = email, + Subject = deployment.EmailSubject.NaIfEmpty(), + CreatedOnUtc = DateTime.UtcNow, + EmailAccountId = deployment.EmailAccountId + }; + + foreach (var path in context.GetDeploymentFiles()) + { + var name = Path.GetFileName(path); + + queuedEmail.Attachments.Add(new QueuedEmailAttachment + { + StorageLocation = EmailAttachmentStorageLocation.Blob, + Data = File.ReadAllBytes(path), + Name = name, + MimeType = MimeTypes.MapNameToMimeType(name) + }); + } + + _queuedEmailService.InsertQueuedEmail(queuedEmail); + ++count; + } + + context.Log.Information("{0} email(s) created and queued for deployment.".FormatInvariant(count)); + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FileSystemFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FileSystemFilePublisher.cs new file mode 100644 index 0000000000..6bb56682e1 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FileSystemFilePublisher.cs @@ -0,0 +1,22 @@ +using System.IO; +using SmartStore.Core.Domain; +using SmartStore.Core.Logging; +using SmartStore.Utilities; + +namespace SmartStore.Services.DataExchange.Export.Deployment +{ + public class FileSystemFilePublisher : IFilePublisher + { + public virtual void Publish(ExportDeploymentContext context, ExportDeployment deployment) + { + var targetFolder = deployment.GetDeploymentFolder(true); + + if (!FileSystemHelper.CopyDirectory(new DirectoryInfo(context.FolderContent), new DirectoryInfo(targetFolder))) + { + context.Result.LastError = context.T("Admin.DataExchange.Export.Deployment.CopyFileFailed"); + } + + context.Log.Information("Copied export data files to " + targetFolder); + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FtpFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FtpFilePublisher.cs new file mode 100644 index 0000000000..7a0954a1c7 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/FtpFilePublisher.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using SmartStore.Core.Domain; +using SmartStore.Core.Logging; + +namespace SmartStore.Services.DataExchange.Export.Deployment +{ + public class FtpFilePublisher : IFilePublisher + { + public virtual void Publish(ExportDeploymentContext context, ExportDeployment deployment) + { + var bytesRead = 0; + var succeededFiles = 0; + var url = deployment.Url; + var buffLength = 32768; + byte[] buff = new byte[buffLength]; + var deploymentFiles = context.GetDeploymentFiles().ToList(); + var lastIndex = (deploymentFiles.Count - 1); + + if (!url.StartsWith("ftp://", StringComparison.InvariantCultureIgnoreCase)) + url = "ftp://" + url; + + foreach (var path in deploymentFiles) + { + var fileUrl = url.EnsureEndsWith("/") + Path.GetFileName(path); + + var request = (FtpWebRequest)WebRequest.Create(fileUrl); + request.Method = WebRequestMethods.Ftp.UploadFile; + request.KeepAlive = (deploymentFiles.IndexOf(path) != lastIndex); + request.UseBinary = true; + request.Proxy = null; + request.UsePassive = deployment.PassiveMode; + request.EnableSsl = deployment.UseSsl; + + if (deployment.Username.HasValue()) + request.Credentials = new NetworkCredential(deployment.Username, deployment.Password); + + request.ContentLength = (new FileInfo(path)).Length; + + var requestStream = request.GetRequestStream(); + + using (var stream = new FileStream(path, FileMode.Open)) + { + while ((bytesRead = stream.Read(buff, 0, buffLength)) != 0) + { + requestStream.Write(buff, 0, bytesRead); + } + } + + requestStream.Close(); + + using (var response = (FtpWebResponse)request.GetResponse()) + { + var statusCode = (int)response.StatusCode; + + if (statusCode >= 200 && statusCode <= 299) + { + ++succeededFiles; + } + else + { + context.Result.LastError = context.T("Admin.Common.FtpStatus", statusCode, response.StatusCode.ToString()); + + context.Log.Error("The FTP transfer failed. FTP status {0} ({1}). File {3}".FormatInvariant(statusCode, response.StatusCode.ToString(), path)); + } + } + } + + context.Log.Information("{0} file(s) successfully uploaded via FTP.".FormatInvariant(succeededFiles)); + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/HttpFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/HttpFilePublisher.cs new file mode 100644 index 0000000000..4f0d9df8d8 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/HttpFilePublisher.cs @@ -0,0 +1,77 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using SmartStore.Core.Domain; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Logging; + +namespace SmartStore.Services.DataExchange.Export.Deployment +{ + public class HttpFilePublisher : IFilePublisher + { + public virtual void Publish(ExportDeploymentContext context, ExportDeployment deployment) + { + var succeededFiles = 0; + var url = deployment.Url; + + if (!url.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) && !url.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase)) + url = "http://" + url; + + if (deployment.HttpTransmissionType == ExportHttpTransmissionType.MultipartFormDataPost) + { + var countFiles = 0; + ICredentials credentials = null; + + if (deployment.Username.HasValue()) + credentials = new NetworkCredential(deployment.Username, deployment.Password); + + using (var handler = new HttpClientHandler { Credentials = credentials }) + using (var client = new HttpClient(handler)) + using (var formData = new MultipartFormDataContent()) + { + foreach (var path in context.GetDeploymentFiles()) + { + byte[] fileData = File.ReadAllBytes(path); + formData.Add(new ByteArrayContent(fileData), "file {0}".FormatInvariant(++countFiles), Path.GetFileName(path)); + } + + var response = client.PostAsync(url, formData).Result; + + if (response.IsSuccessStatusCode) + { + succeededFiles = countFiles; + } + else if (response.Content != null) + { + context.Result.LastError = context.T("Admin.Common.HttpStatus", (int)response.StatusCode, response.StatusCode.ToString()); + + var content = response.Content.ReadAsStringAsync().Result; + + var msg = "Multipart form data upload failed. HTTP status {0} ({1}). Response: {2}".FormatInvariant( + (int)response.StatusCode, response.StatusCode.ToString(), content.NaIfEmpty().Truncate(2000, "...")); + + context.Log.Error(msg); + } + } + } + else + { + using (var webClient = new WebClient()) + { + if (deployment.Username.HasValue()) + webClient.Credentials = new NetworkCredential(deployment.Username, deployment.Password); + + foreach (var path in context.GetDeploymentFiles()) + { + webClient.UploadFile(url, path); + + ++succeededFiles; + } + } + } + + context.Log.Information("{0} file(s) successfully uploaded via HTTP ({1}).".FormatInvariant(succeededFiles, deployment.HttpTransmissionType.ToString())); + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/IFilePublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/IFilePublisher.cs new file mode 100644 index 0000000000..1ae3301ec4 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/IFilePublisher.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.IO; +using SmartStore.Core.Domain; +using SmartStore.Core.Localization; +using SmartStore.Core.Logging; + +namespace SmartStore.Services.DataExchange.Export.Deployment +{ + public interface IFilePublisher + { + void Publish(ExportDeploymentContext context, ExportDeployment deployment); + } + + + public class ExportDeploymentContext + { + public Localizer T { get; set; } + public ILogger Log { get; set; } + + public string FolderContent { get; set; } + + public string ZipPath { get; set; } + public bool CreateZipArchive { get; set; } + + public DataDeploymentResult Result { get; set; } + + public IEnumerable GetDeploymentFiles() + { + if (!CreateZipArchive) + { + return System.IO.Directory.EnumerateFiles(FolderContent, "*", SearchOption.AllDirectories); + } + + if (File.Exists(ZipPath)) + { + return new string[] { ZipPath }; + } + + return new string[0]; + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/PublicFolderPublisher.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/PublicFolderPublisher.cs new file mode 100644 index 0000000000..d438e808a2 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Deployment/PublicFolderPublisher.cs @@ -0,0 +1,44 @@ +using System.IO; +using SmartStore.Core.Domain; +using SmartStore.Core.Logging; +using SmartStore.Utilities; + +namespace SmartStore.Services.DataExchange.Export.Deployment +{ + public class PublicFolderPublisher : IFilePublisher + { + public virtual void Publish(ExportDeploymentContext context, ExportDeployment deployment) + { + var destinationFolder = deployment.GetDeploymentFolder(true); + + if (destinationFolder.IsEmpty()) + return; + + if (!System.IO.Directory.Exists(destinationFolder)) + { + System.IO.Directory.CreateDirectory(destinationFolder); + } + + if (context.CreateZipArchive) + { + if (File.Exists(context.ZipPath)) + { + var destinationFile = Path.Combine(destinationFolder, Path.GetFileName(context.ZipPath)); + + File.Copy(context.ZipPath, destinationFile, true); + + context.Log.Information("Copied zipped export data to " + destinationFile); + } + } + else + { + if (!FileSystemHelper.CopyDirectory(new DirectoryInfo(context.FolderContent), new DirectoryInfo(destinationFolder))) + { + context.Result.LastError = context.T("Admin.DataExchange.Export.Deployment.CopyFileFailed"); + } + + context.Log.Information("Copied export data files to " + destinationFolder); + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs new file mode 100644 index 0000000000..1917084af2 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DynamicEntityHelper.cs @@ -0,0 +1,1327 @@ +using System; +using System.Collections.Generic; +using System.Data.Entity; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Web; +using SmartStore.ComponentModel; +using SmartStore.Core; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Domain.Directory; +using SmartStore.Core.Domain.Discounts; +using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.Domain.Messages; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Seo; +using SmartStore.Core.Domain.Shipping; +using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Html; +using SmartStore.Services.Catalog; +using SmartStore.Services.DataExchange.Export.Events; +using SmartStore.Services.DataExchange.Export.Internal; +using SmartStore.Services.Localization; +using SmartStore.Services.Seo; + +namespace SmartStore.Services.DataExchange.Export +{ + public partial class DataExporter + { + private void PrepareProductDescription(DataExporterContext ctx, dynamic dynObject, Product product) + { + try + { + var languageId = (ctx.Projection.LanguageId ?? 0); + string description = ""; + + // description merging + if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.None) + { + // export empty description + } + else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ShortDescriptionOrNameIfEmpty) + { + description = dynObject.FullDescription; + + if (description.IsEmpty()) + description = dynObject.ShortDescription; + if (description.IsEmpty()) + description = dynObject.Name; + } + else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ShortDescription) + { + description = dynObject.ShortDescription; + } + else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.Description) + { + description = dynObject.FullDescription; + } + else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.NameAndShortDescription) + { + description = ((string)dynObject.Name).Grow((string)dynObject.ShortDescription, " "); + } + else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.NameAndDescription) + { + description = ((string)dynObject.Name).Grow((string)dynObject.FullDescription, " "); + } + else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ManufacturerAndNameAndShortDescription || + ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ManufacturerAndNameAndDescription) + { + var productManus = ctx.ProductExportContext.ProductManufacturers.Load(product.Id); + + if (productManus != null && productManus.Any()) + description = productManus.First().Manufacturer.GetLocalized(x => x.Name, languageId, true, false); + + description = description.Grow((string)dynObject.Name, " "); + + if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ManufacturerAndNameAndShortDescription) + description = description.Grow((string)dynObject.ShortDescription, " "); + else + description = description.Grow((string)dynObject.FullDescription, " "); + } + + // append text + if (ctx.Projection.AppendDescriptionText.HasValue() && ((string)dynObject.ShortDescription).IsEmpty() && ((string)dynObject.FullDescription).IsEmpty()) + { + string[] appendText = ctx.Projection.AppendDescriptionText.SplitSafe(","); + if (appendText.Length > 0) + { + var rnd = (new Random()).Next(0, appendText.Length - 1); + + description = description.Grow(appendText.SafeGet(rnd), " "); + } + } + + // remove critical characters + if (description.HasValue() && ctx.Projection.RemoveCriticalCharacters) + { + foreach (var str in ctx.Projection.CriticalCharacters.SplitSafe(",")) + description = description.Replace(str, ""); + } + + // convert to plain text + if (description.HasValue() && ctx.Projection.DescriptionToPlainText) + { + //Regex reg = new Regex("<[^>]+>", RegexOptions.IgnoreCase); + //description = HttpUtility.HtmlDecode(reg.Replace(description, "")); + + description = HtmlUtils.ConvertHtmlToPlainText(description); + description = HtmlUtils.StripTags(HttpUtility.HtmlDecode(description)); + } + + dynObject.FullDescription = description; + } + catch { } + } + + private decimal? ConvertPrice(DataExporterContext ctx, Product product, decimal? price) + { + if (price.HasValue) + { + if (ctx.Projection.ConvertNetToGrossPrices) + { + decimal taxRate; + price = _taxService.Value.GetProductPrice(product, price.Value, true, ctx.ContextCustomer, out taxRate); + } + + if (price != decimal.Zero) + { + price = _currencyService.Value.ConvertFromPrimaryStoreCurrency(price.Value, ctx.ContextCurrency, ctx.Store); + } + } + return price; + } + + private decimal CalculatePrice( + DataExporterContext ctx, + Product product, + ProductVariantAttributeCombination combination, + IList attributeValues) + { + var price = product.Price; + var priceCalculationContext = ctx.ProductExportContext as PriceCalculationContext; + + if (combination != null) + { + // price for attribute combination + var attributesTotalPriceBase = decimal.Zero; + + if (attributeValues != null) + { + attributeValues.Each(x => attributesTotalPriceBase += _priceCalculationService.Value.GetProductVariantAttributeValuePriceAdjustment(x)); + } + + price = _priceCalculationService.Value.GetFinalPrice(product, null, ctx.ContextCustomer, attributesTotalPriceBase, true, 1, null, priceCalculationContext); + } + else if (ctx.Projection.PriceType.HasValue) + { + // price for product + if (ctx.Projection.PriceType.Value == PriceDisplayType.LowestPrice) + { + bool displayFromMessage; + price = _priceCalculationService.Value.GetLowestPrice(product, priceCalculationContext, out displayFromMessage); + } + else if (ctx.Projection.PriceType.Value == PriceDisplayType.PreSelectedPrice) + { + price = _priceCalculationService.Value.GetPreselectedPrice(product, priceCalculationContext); + } + else if (ctx.Projection.PriceType.Value == PriceDisplayType.PriceWithoutDiscountsAndAttributes) + { + price = _priceCalculationService.Value.GetFinalPrice(product, null, ctx.ContextCustomer, decimal.Zero, false, 1, null, priceCalculationContext); + } + } + + return ConvertPrice(ctx, product, price) ?? price; + } + + private List GetLocalized(DataExporterContext ctx, T entity, params Expression>[] keySelectors) + where T : BaseEntity, ILocalizedEntity + { + if (ctx.Languages.Count <= 1) + return null; + + var localized = new List(); + + var localeKeyGroup = typeof(T).Name; + var isSlugSupported = typeof(ISlugSupported).IsAssignableFrom(typeof(T)); + + foreach (var language in ctx.Languages) + { + var languageCulture = language.Value.LanguageCulture.EmptyNull().ToLower(); + + // add SeName + if (isSlugSupported) + { + var value = _urlRecordService.Value.GetActiveSlug(entity.Id, localeKeyGroup, language.Value.Id); + if (value.HasValue()) + { + dynamic exp = new HybridExpando(); + exp.Culture = languageCulture; + exp.LocaleKey = "SeName"; + exp.LocaleValue = value; + + localized.Add(exp); + } + } + + foreach (var keySelector in keySelectors) + { + var member = keySelector.Body as MemberExpression; + var propInfo = member.Member as PropertyInfo; + string localeKey = propInfo.Name; + var value = _localizedEntityService.Value.GetLocalizedValue(language.Value.Id, entity.Id, localeKeyGroup, localeKey); + + // we better not export empty values. the risk is to high that they are imported and unnecessary fill databases. + if (value.HasValue()) + { + dynamic exp = new HybridExpando(); + exp.Culture = languageCulture; + exp.LocaleKey = localeKey; + exp.LocaleValue = value; + + localized.Add(exp); + } + } + } + + return (localized.Count == 0 ? null : localized); + } + + private dynamic ToDynamic(DataExporterContext ctx, Currency currency) + { + if (currency == null) + return null; + + dynamic result = new DynamicEntity(currency); + + result.Name = currency.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); + result._Localized = GetLocalized(ctx, currency, x => x.Name); + + return result; + } + + private dynamic ToDynamic(DataExporterContext ctx, Language language) + { + if (language == null) + return null; + + dynamic result = new DynamicEntity(language); + return result; + } + + private dynamic ToDynamic(DataExporterContext ctx, Country country) + { + if (country == null) + return null; + + dynamic result = new DynamicEntity(country); + + result.Name = country.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); + result._Localized = GetLocalized(ctx, country, x => x.Name); + + return result; + } + + private dynamic ToDynamic(DataExporterContext ctx, Address address) + { + if (address == null) + return null; + + dynamic result = new DynamicEntity(address); + + result.Country = ToDynamic(ctx, address.Country); + + if (address.StateProvinceId.GetValueOrDefault() > 0) + { + dynamic sp = new DynamicEntity(address.StateProvince); + + sp.Name = address.StateProvince.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); + sp._Localized = GetLocalized(ctx, address.StateProvince, x => x.Name); + + result.StateProvince = sp; + } + else + { + result.StateProvince = null; + } + + return result; + } + + private dynamic ToDynamic(DataExporterContext ctx, RewardPointsHistory points) + { + if (points == null) + return null; + + dynamic result = new DynamicEntity(points); + + return result; + } + + private dynamic ToDynamic(DataExporterContext ctx, Customer customer) + { + if (customer == null) + return null; + + dynamic result = new DynamicEntity(customer); + + result.BillingAddress = null; + result.ShippingAddress = null; + result.Addresses = null; + result.CustomerRoles = null; + + result.RewardPointsHistory = null; + result._RewardPointsBalance = 0; + + result._GenericAttributes = null; + result._HasNewsletterSubscription = false; + + result._FullName = null; + result._AvatarPictureUrl = null; + + return result; + } + + private dynamic ToDynamic(DataExporterContext ctx, Store store) + { + if (store == null) + return null; + + dynamic result = new DynamicEntity(store); + + result.PrimaryStoreCurrency = ToDynamic(ctx, store.PrimaryStoreCurrency); + result.PrimaryExchangeRateCurrency = ToDynamic(ctx, store.PrimaryExchangeRateCurrency); + + return result; + } + + private dynamic ToDynamic(DataExporterContext ctx, DeliveryTime deliveryTime) + { + if (deliveryTime == null) + return null; + + dynamic result = new DynamicEntity(deliveryTime); + + result.Name = deliveryTime.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); + result._Localized = GetLocalized(ctx, deliveryTime, x => x.Name); + + return result; + } + + private void ToDeliveryTime(DataExporterContext ctx, dynamic parent, int? deliveryTimeId) + { + if (ctx.DeliveryTimes != null) + { + if (deliveryTimeId.HasValue && ctx.DeliveryTimes.ContainsKey(deliveryTimeId.Value)) + parent.DeliveryTime = ToDynamic(ctx, ctx.DeliveryTimes[deliveryTimeId.Value]); + else + parent.DeliveryTime = null; + } + } + + private dynamic ToDynamic(DataExporterContext ctx, QuantityUnit quantityUnit) + { + if (quantityUnit == null) + return null; + + dynamic result = new DynamicEntity(quantityUnit); + + result.Name = quantityUnit.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); + result.Description = quantityUnit.GetLocalized(x => x.Description, ctx.Projection.LanguageId ?? 0, true, false); + + result._Localized = GetLocalized(ctx, quantityUnit, + x => x.Name, + x => x.Description); + + return result; + } + + private void ToQuantityUnit(DataExporterContext ctx, dynamic parent, int? quantityUnitId) + { + if (ctx.QuantityUnits != null) + { + if (quantityUnitId.HasValue && ctx.QuantityUnits.ContainsKey(quantityUnitId.Value)) + parent.QuantityUnit = ToDynamic(ctx, ctx.QuantityUnits[quantityUnitId.Value]); + else + parent.QuantityUnit = null; + } + } + + private dynamic ToDynamic(DataExporterContext ctx, Picture picture, int thumbPictureSize, int detailsPictureSize) + { + if (picture == null) + return null; + + dynamic result = new DynamicEntity(picture); + var relativeUrl = _pictureService.Value.GetPictureUrl(picture, 0, false); + + result._FileName = relativeUrl.Substring(relativeUrl.LastIndexOf("/") + 1); + result._RelativeUrl = relativeUrl; + result._ThumbImageUrl = _pictureService.Value.GetPictureUrl(picture, thumbPictureSize, false, ctx.Store.Url); + result._ImageUrl = _pictureService.Value.GetPictureUrl(picture, detailsPictureSize, false, ctx.Store.Url); + result._FullSizeImageUrl = _pictureService.Value.GetPictureUrl(picture, 0, false, ctx.Store.Url); + + result._ThumbLocalPath = _pictureService.Value.GetThumbLocalPath(picture); + + return result; + } + + private dynamic ToDynamic(DataExporterContext ctx, ProductVariantAttribute pva) + { + if (pva == null) + return null; + + dynamic result = new DynamicEntity(pva); + + dynamic attribute = new DynamicEntity(pva.ProductAttribute); + + attribute.Name = pva.ProductAttribute.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); + attribute.Description = pva.ProductAttribute.GetLocalized(x => x.Description, ctx.Projection.LanguageId ?? 0, true, false); + + attribute.Values = pva.ProductVariantAttributeValues + .OrderBy(x => x.DisplayOrder) + .Select(x => + { + dynamic dyn = new DynamicEntity(x); + + dyn.Name = x.GetLocalized(y => y.Name, ctx.Projection.LanguageId ?? 0, true, false); + dyn._Localized = GetLocalized(ctx, x, y => y.Name); + + return dyn; + }) + .ToList(); + + attribute._Localized = GetLocalized(ctx, pva.ProductAttribute, + x => x.Name, + x => x.Description); + + result.Attribute = attribute; + + return result; + } + + private dynamic ToDynamic(DataExporterContext ctx, ProductVariantAttributeCombination pvac) + { + if (pvac == null) + return null; + + dynamic result = new DynamicEntity(pvac); + + ToDeliveryTime(ctx, result, pvac.DeliveryTimeId); + ToQuantityUnit(ctx, result, pvac.QuantityUnitId); + + return result; + } + + private dynamic ToDynamic(DataExporterContext ctx, Manufacturer manufacturer) + { + if (manufacturer == null) + return null; + + dynamic result = new DynamicEntity(manufacturer); + + result.Name = manufacturer.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); + result.SeName = manufacturer.GetSeName(ctx.Projection.LanguageId ?? 0, true, false); + result.Description = manufacturer.GetLocalized(x => x.Description, ctx.Projection.LanguageId ?? 0, true, false); + result.MetaKeywords = manufacturer.GetLocalized(x => x.MetaKeywords, ctx.Projection.LanguageId ?? 0, true, false); + result.MetaDescription = manufacturer.GetLocalized(x => x.MetaDescription, ctx.Projection.LanguageId ?? 0, true, false); + result.MetaTitle = manufacturer.GetLocalized(x => x.MetaTitle, ctx.Projection.LanguageId ?? 0, true, false); + + result.Picture = null; + + result._Localized = GetLocalized(ctx, manufacturer, + x => x.Name, + x => x.Description, + x => x.MetaKeywords, + x => x.MetaDescription, + x => x.MetaTitle); + + return result; + } + + private dynamic ToDynamic(DataExporterContext ctx, Category category) + { + if (category == null) + return null; + + dynamic result = new DynamicEntity(category); + + result.Name = category.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); + result.SeName = category.GetSeName(ctx.Projection.LanguageId ?? 0, true, false); + result.FullName = category.GetLocalized(x => x.FullName, ctx.Projection.LanguageId ?? 0, true, false); + result.Description = category.GetLocalized(x => x.Description, ctx.Projection.LanguageId ?? 0, true, false); + result.BottomDescription = category.GetLocalized(x => x.BottomDescription, ctx.Projection.LanguageId ?? 0, true, false); + result.MetaKeywords = category.GetLocalized(x => x.MetaKeywords, ctx.Projection.LanguageId ?? 0, true, false); + result.MetaDescription = category.GetLocalized(x => x.MetaDescription, ctx.Projection.LanguageId ?? 0, true, false); + result.MetaTitle = category.GetLocalized(x => x.MetaTitle, ctx.Projection.LanguageId ?? 0, true, false); + + result.Picture = null; + + if (ctx.CategoryTemplates.ContainsKey(category.CategoryTemplateId)) + result._CategoryTemplateViewPath = ctx.CategoryTemplates[category.CategoryTemplateId]; + else + result._CategoryTemplateViewPath = ""; + + result._Localized = GetLocalized(ctx, category, + x => x.Name, + x => x.FullName, + x => x.Description, + x => x.BottomDescription, + x => x.MetaKeywords, + x => x.MetaDescription, + x => x.MetaTitle); + + return result; + } + + private dynamic ToDynamic(DataExporterContext ctx, Product product) + { + if (product == null) + return null; + + dynamic result = new DynamicEntity(product); + + result.Name = product.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); + result.SeName = product.GetSeName(ctx.Projection.LanguageId ?? 0, true, false); + result.ShortDescription = product.GetLocalized(x => x.ShortDescription, ctx.Projection.LanguageId ?? 0, true, false); + result.FullDescription = product.GetLocalized(x => x.FullDescription, ctx.Projection.LanguageId ?? 0, true, false); + result.MetaKeywords = product.GetLocalized(x => x.MetaKeywords, ctx.Projection.LanguageId ?? 0, true, false); + result.MetaDescription = product.GetLocalized(x => x.MetaDescription, ctx.Projection.LanguageId ?? 0, true, false); + result.MetaTitle = product.GetLocalized(x => x.MetaTitle, ctx.Projection.LanguageId ?? 0, true, false); + result.BundleTitleText = product.GetLocalized(x => x.BundleTitleText, ctx.Projection.LanguageId ?? 0, true, false); + + result.AppliedDiscounts = null; + result.TierPrices = null; + result.ProductAttributes = null; + result.ProductAttributeCombinations = null; + result.ProductPictures = null; + result.ProductCategories = null; + result.ProductManufacturers = null; + result.ProductTags = null; + result.ProductSpecificationAttributes = null; + result.ProductBundleItems = null; + + result._Localized = GetLocalized(ctx, product, + x => x.Name, + x => x.ShortDescription, + x => x.FullDescription, + x => x.MetaKeywords, + x => x.MetaDescription, + x => x.MetaTitle, + x => x.BundleTitleText); + + return result; + } + + private dynamic ToDynamic( + DataExporterContext ctx, + Product product, + ICollection combinations, + ProductVariantAttributeCombination combination) + { + product.MergeWithCombination(combination); + + var languageId = (ctx.Projection.LanguageId ?? 0); + var pictureSize = _mediaSettings.Value.ProductDetailsPictureSize; + var numberOfPictures = (ctx.Projection.NumberOfPictures ?? int.MaxValue); + int[] pictureIds = (combination == null ? new int[0] : combination.GetAssignedPictureIds()); + + if (ctx.Supports(ExportFeatures.CanIncludeMainPicture) && ctx.Projection.PictureSize > 0) + pictureSize = ctx.Projection.PictureSize; + + var perfLoadId = (ctx.IsPreview ? 0 : product.Id); // perf preview (it's a compromise) + IEnumerable productPictures = ctx.ProductExportContext.ProductPictures.Load(perfLoadId); + var productManufacturers = ctx.ProductExportContext.ProductManufacturers.Load(perfLoadId); + var productCategories = ctx.ProductExportContext.ProductCategories.Load(product.Id); + var productAttributes = ctx.ProductExportContext.Attributes.Load(product.Id); + var productTags = ctx.ProductExportContext.ProductTags.Load(product.Id); + var specificationAttributes = ctx.ProductExportContext.ProductSpecificationAttributes.Load(product.Id); + + var variantAttributes = (combination != null ? _productAttributeParser.Value.DeserializeProductVariantAttributes(combination.AttributesXml) : null); + var variantAttributeValues = (combination != null ? _productAttributeParser.Value.ParseProductVariantAttributeValues(variantAttributes, productAttributes) : null); + + if (pictureIds.Length > 0) + productPictures = productPictures.Where(x => pictureIds.Contains(x.PictureId)); + + productPictures = productPictures.Take(numberOfPictures); + + dynamic dynObject = ToDynamic(ctx, product); + + #region gerneral data + + dynObject._CategoryName = null; + dynObject._CategoryPath = null; + dynObject._AttributeCombination = null; + dynObject._AttributeCombinationValues = null; + dynObject._AttributeCombinationId = (combination == null ? 0 : combination.Id); + dynObject._DetailUrl = ctx.Store.Url.EnsureEndsWith("/") + (string)dynObject.SeName; + + if (combination == null) + dynObject._UniqueId = product.Id.ToString(); + else + dynObject._UniqueId = string.Concat(product.Id, "-", combination.Id); + + dynObject.Price = CalculatePrice(ctx, product, combination, variantAttributeValues); + + dynObject._BasePriceInfo = product.GetBasePriceInfo(_services.Localization, _priceFormatter.Value, _currencyService.Value, _taxService.Value, + _priceCalculationService.Value, ctx.ContextCurrency, decimal.Zero, true); + + if (ctx.ProductTemplates.ContainsKey(product.ProductTemplateId)) + dynObject._ProductTemplateViewPath = ctx.ProductTemplates[product.ProductTemplateId]; + else + dynObject._ProductTemplateViewPath = ""; + + if (combination != null) + { + if (ctx.Supports(ExportFeatures.UsesAttributeCombination)) + { + dynObject._AttributeCombination = variantAttributes; + dynObject._AttributeCombinationValues = variantAttributeValues; + } + + if (ctx.Projection.AttributeCombinationValueMerging == ExportAttributeValueMerging.AppendAllValuesToName) + { + var valueNames = variantAttributeValues + .Select(x => x.GetLocalized(y => y.Name, languageId, true, false)) + .ToList(); + + dynObject.Name = ((string)dynObject.Name).Grow(string.Join(", ", valueNames), " "); + } + + var attributeQueryString = _productAttributeParser.Value.SerializeQueryData(combination.AttributesXml, product.Id); + if (attributeQueryString.HasValue()) + { + var url = (string)dynObject._DetailUrl; + dynObject._DetailUrl = string.Concat(url, url.Contains("?") ? "&" : "?", "attributes=", attributeQueryString); + } + } + + if (ctx.Categories.Count > 0) + { + dynObject._CategoryPath = _categoryService.Value.GetCategoryPath( + product, + null, + x => ctx.CategoryPathes.ContainsKey(x) ? ctx.CategoryPathes[x] : null, + (id, value) => ctx.CategoryPathes[id] = value, + x => ctx.Categories.ContainsKey(x) ? ctx.Categories[x] : _categoryService.Value.GetCategoryById(x), + productCategories.OrderBy(x => x.DisplayOrder).FirstOrDefault() + ); + } + + ToDeliveryTime(ctx, dynObject, product.DeliveryTimeId); + ToQuantityUnit(ctx, dynObject, product.QuantityUnitId); + + dynObject.ProductPictures = productPictures + .OrderBy(x => x.DisplayOrder) + .Select(x => + { + dynamic dyn = new DynamicEntity(x); + + dyn.Picture = ToDynamic(ctx, x.Picture, _mediaSettings.Value.ProductThumbPictureSize, pictureSize); + + return dyn; + }) + .ToList(); + + dynObject.ProductManufacturers = productManufacturers + .OrderBy(x => x.DisplayOrder) + .Select(x => + { + dynamic dyn = new DynamicEntity(x); + + dyn.Manufacturer = ToDynamic(ctx, x.Manufacturer); + + if (x.Manufacturer != null && x.Manufacturer.PictureId.HasValue) + dyn.Manufacturer.Picture = ToDynamic(ctx, x.Manufacturer.Picture, _mediaSettings.Value.ManufacturerThumbPictureSize, _mediaSettings.Value.ManufacturerThumbPictureSize); + else + dyn.Manufacturer.Picture = null; + + return dyn; + }) + .ToList(); + + dynObject.ProductCategories = productCategories + .OrderBy(x => x.DisplayOrder) + .Select(x => + { + dynamic dyn = new DynamicEntity(x); + + dyn.Category = ToDynamic(ctx, x.Category); + + if (x.Category != null && x.Category.PictureId.HasValue) + dyn.Category.Picture = ToDynamic(ctx, x.Category.Picture, _mediaSettings.Value.CategoryThumbPictureSize, _mediaSettings.Value.CategoryThumbPictureSize); + + if (dynObject._CategoryName == null) + dynObject._CategoryName = (string)dyn.Category.Name; + + return dyn; + }) + .ToList(); + + dynObject.ProductAttributes = productAttributes + .OrderBy(x => x.DisplayOrder) + .Select(x => ToDynamic(ctx, x)) + .ToList(); + + dynObject.ProductAttributeCombinations = (combinations ?? Enumerable.Empty()) + .Select(x => + { + dynamic dyn = ToDynamic(ctx, x); + var assignedPictures = new List(); + + foreach (int pictureId in x.GetAssignedPictureIds().Take(numberOfPictures)) + { + var assignedPicture = productPictures.FirstOrDefault(y => y.PictureId == pictureId); + if (assignedPicture != null && assignedPicture.Picture != null) + { + assignedPictures.Add(ToDynamic(ctx, assignedPicture.Picture, _mediaSettings.Value.ProductThumbPictureSize, pictureSize)); + } + } + + dyn.Pictures = assignedPictures; + + return dyn; + }) + .ToList(); + + if (product.HasTierPrices) + { + var tierPrices = ctx.ProductExportContext.TierPrices.Load(product.Id) + .RemoveDuplicatedQuantities(); + + dynObject.TierPrices = tierPrices + .Select(x => + { + dynamic dyn = new DynamicEntity(x); + + return dyn; + }) + .ToList(); + } + + if (product.HasDiscountsApplied) + { + var appliedDiscounts = ctx.ProductExportContext.AppliedDiscounts.Load(product.Id); + + dynObject.AppliedDiscounts = appliedDiscounts + .Select(x => ToDynamic(ctx, x)) + .ToList(); + } + + dynObject.ProductTags = productTags + .Select(x => + { + dynamic dyn = new DynamicEntity(x); + + dyn.Name = x.GetLocalized(y => y.Name, languageId, true, false); + dyn.SeName = x.GetSeName(languageId); + dyn._Localized = GetLocalized(ctx, x, y => y.Name); + + return dyn; + }) + .ToList(); + + dynObject.ProductSpecificationAttributes = specificationAttributes + .Select(x => ToDynamic(ctx, x)) + .ToList(); + + if (product.ProductType == ProductType.BundledProduct) + { + var bundleItems = ctx.ProductExportContext.ProductBundleItems.Load(perfLoadId); + + dynObject.ProductBundleItems = bundleItems + .Select(x => + { + dynamic dyn = new DynamicEntity(x); + + dyn.Name = x.GetLocalized(y => y.Name, languageId, true, false); + dyn.ShortDescription = x.GetLocalized(y => y.ShortDescription, languageId, true, false); + dyn._Localized = GetLocalized(ctx, x, y => y.Name, y => y.ShortDescription); + + return dyn; + }) + .ToList(); + } + + #endregion + + #region more attribute controlled data + + if (ctx.Supports(ExportFeatures.CanProjectDescription)) + { + PrepareProductDescription(ctx, dynObject, product); + } + + if (ctx.Supports(ExportFeatures.OffersBrandFallback)) + { + string brand = null; + var productManus = ctx.ProductExportContext.ProductManufacturers.Load(perfLoadId); + + if (productManus != null && productManus.Any()) + brand = productManus.First().Manufacturer.GetLocalized(x => x.Name, languageId, true, false); + + if (brand.IsEmpty()) + brand = ctx.Projection.Brand; + + dynObject._Brand = brand; + } + + if (ctx.Supports(ExportFeatures.CanIncludeMainPicture)) + { + if (productPictures != null && productPictures.Any()) + { + var firstPicture = productPictures.First().Picture; + dynObject._MainPictureUrl = _pictureService.Value.GetPictureUrl(firstPicture, ctx.Projection.PictureSize, storeLocation: ctx.Store.Url); + dynObject._MainPictureRelativeUrl = _pictureService.Value.GetPictureUrl(firstPicture, ctx.Projection.PictureSize); + } + else if (!_catalogSettings.Value.HideProductDefaultPictures) + { + dynObject._MainPictureUrl = _pictureService.Value.GetDefaultPictureUrl(ctx.Projection.PictureSize, storeLocation: ctx.Store.Url); + dynObject._MainPictureRelativeUrl = _pictureService.Value.GetDefaultPictureUrl(ctx.Projection.PictureSize); + } + else + { + dynObject._MainPictureUrl = null; + dynObject._MainPictureRelativeUrl = null; + } + } + + if (ctx.Supports(ExportFeatures.UsesSkuAsMpnFallback) && product.ManufacturerPartNumber.IsEmpty()) + { + dynObject.ManufacturerPartNumber = product.Sku; + } + + if (ctx.Supports(ExportFeatures.OffersShippingTimeFallback)) + { + dynamic deliveryTime = dynObject.DeliveryTime; + dynObject._ShippingTime = (deliveryTime == null ? ctx.Projection.ShippingTime : deliveryTime.Name); + } + + if (ctx.Supports(ExportFeatures.OffersShippingCostsFallback)) + { + dynObject._FreeShippingThreshold = ctx.Projection.FreeShippingThreshold; + + if (product.IsFreeShipping || (ctx.Projection.FreeShippingThreshold.HasValue && (decimal)dynObject.Price >= ctx.Projection.FreeShippingThreshold.Value)) + dynObject._ShippingCosts = decimal.Zero; + else + dynObject._ShippingCosts = ctx.Projection.ShippingCosts; + } + + if (ctx.Supports(ExportFeatures.UsesOldPrice)) + { + if (product.OldPrice != decimal.Zero && product.OldPrice != (decimal)dynObject.Price && !(product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing)) + { + if (ctx.Projection.ConvertNetToGrossPrices) + { + decimal taxRate; + dynObject._OldPrice = _taxService.Value.GetProductPrice(product, product.OldPrice, true, ctx.ContextCustomer, out taxRate); + } + else + { + dynObject._OldPrice = product.OldPrice; + } + } + else + { + dynObject._OldPrice = null; + } + } + + if (ctx.Supports(ExportFeatures.UsesSpecialPrice)) + { + dynObject._SpecialPrice = null; // special price which is valid now + dynObject._FutureSpecialPrice = null; // special price which is valid now and in future + dynObject._RegularPrice = null; // price as if a special price would not exist + + if (!(product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing)) + { + if (product.SpecialPrice.HasValue && product.SpecialPriceEndDateTimeUtc.HasValue) + { + var endDate = DateTime.SpecifyKind(product.SpecialPriceEndDateTimeUtc.Value, DateTimeKind.Utc); + if (endDate > DateTime.UtcNow) + { + dynObject._FutureSpecialPrice = ConvertPrice(ctx, product, product.SpecialPrice.Value); + } + } + + var specialPrice = _priceCalculationService.Value.GetSpecialPrice(product); + + dynObject._SpecialPrice = ConvertPrice(ctx, product, specialPrice); + + if (specialPrice.HasValue || dynObject._FutureSpecialPrice != null) + { + decimal tmpSpecialPrice = product.SpecialPrice.Value; + product.SpecialPrice = null; + + dynObject._RegularPrice = CalculatePrice(ctx, product, combination, variantAttributeValues); + + product.SpecialPrice = tmpSpecialPrice; + } + } + } + + #endregion + + return dynObject; + } + + private dynamic ToDynamic(DataExporterContext ctx, Order order) + { + if (order == null) + return null; + + dynamic result = new DynamicEntity(order); + + result.OrderNumber = order.GetOrderNumber(); + result.OrderStatus = order.OrderStatus.GetLocalizedEnum(_services.Localization, ctx.Projection.LanguageId ?? 0); + result.PaymentStatus = order.PaymentStatus.GetLocalizedEnum(_services.Localization, ctx.Projection.LanguageId ?? 0); + result.ShippingStatus = order.ShippingStatus.GetLocalizedEnum(_services.Localization, ctx.Projection.LanguageId ?? 0); + + result.Customer = null; + result.BillingAddress = null; + result.ShippingAddress = null; + result.Store = null; + result.Shipments = null; + + result.RedeemedRewardPointsEntry = ToDynamic(ctx, order.RedeemedRewardPointsEntry); + + return result; + } + + private dynamic ToDynamic(DataExporterContext ctx, OrderItem orderItem) + { + if (orderItem == null) + return null; + + dynamic result = new DynamicEntity(orderItem); + + orderItem.Product.MergeWithCombination(orderItem.AttributesXml, _productAttributeParser.Value); + + result.Product = ToDynamic(ctx, orderItem.Product); + + return result; + } + + private dynamic ToDynamic(DataExporterContext ctx, Shipment shipment) + { + if (shipment == null) + return null; + + dynamic result = new DynamicEntity(shipment); + + result.ShipmentItems = shipment.ShipmentItems + .Select(x => + { + dynamic exp = new DynamicEntity(x); + + return exp; + }) + .ToList(); + + return result; + } + + private dynamic ToDynamic(DataExporterContext ctx, Discount discount) + { + if (discount == null) + return null; + + dynamic result = new DynamicEntity(discount); + + return result; + } + + private dynamic ToDynamic(DataExporterContext ctx, ProductSpecificationAttribute psa) + { + if (psa == null) + return null; + + var option = psa.SpecificationAttributeOption; + + dynamic result = new DynamicEntity(psa); + + dynamic dynAttribute = new DynamicEntity(option.SpecificationAttribute); + + dynAttribute.Name = option.SpecificationAttribute.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); + dynAttribute._Localized = GetLocalized(ctx, option.SpecificationAttribute, x => x.Name); + + dynamic dynOption = new DynamicEntity(option); + + dynOption.Name = option.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); + dynOption._Localized = GetLocalized(ctx, option, x => x.Name); + + dynOption.SpecificationAttribute = dynAttribute; + + result.SpecificationAttributeOption = dynOption; + + return result; + } + + private dynamic ToDynamic(DataExporterContext ctx, GenericAttribute genericAttribute) + { + if (genericAttribute == null) + return null; + + dynamic result = new DynamicEntity(genericAttribute); + + return result; + } + + private dynamic ToDynamic(DataExporterContext ctx, NewsLetterSubscription subscription) + { + if (subscription == null) + return null; + + dynamic result = new DynamicEntity(subscription); + + return result; + } + + + private List Convert(DataExporterContext ctx, Product product) + { + var result = new List(); + + var combinations = ctx.ProductExportContext.AttributeCombinations.Load(product.Id); + + if (!ctx.IsPreview && ctx.Projection.AttributeCombinationAsProduct && combinations.Where(x => x.IsActive).Count() > 0) + { + //var productType = typeof(Product); + //var productValues = new Dictionary(); + var dbContext = _dbContext as DbContext; + + foreach (var combination in combinations.Where(x => x.IsActive)) + { + product = _dbContext.Attach(product); + var entry = dbContext.Entry(product); + + // the returned object is not the entity and is not being tracked by the context. + // it also does not have any relationships set to other objects. + // CurrentValues only includes database (thus primitive) values. + var productClone = entry.CurrentValues.ToObject() as Product; + _dbContext.DetachEntity(product); + + var dynObject = ToDynamic(ctx, productClone, combinations, combination); + result.Add(dynObject); + } + } + else + { + var dynObject = ToDynamic(ctx, product, combinations, null); + result.Add(dynObject); + } + + if (result.Any()) + { + _services.EventPublisher.Publish(new RowExportingEvent + { + Row = result.First(), + EntityType = ExportEntityType.Product, + ExportRequest = ctx.Request, + ExecuteContext = ctx.ExecuteContext + }); + } + + return result; + } + + private List Convert(DataExporterContext ctx, Order order) + { + var result = new List(); + + if (!ctx.IsPreview) + { + ctx.OrderExportContext.Addresses.Collect(order.ShippingAddressId.HasValue ? order.ShippingAddressId.Value : 0); + ctx.OrderExportContext.Addresses.Load(order.BillingAddressId); + } + + var perfLoadId = (ctx.IsPreview ? 0 : order.Id); + var customers = ctx.OrderExportContext.Customers.Load(order.CustomerId); + var rewardPointsHistories = ctx.OrderExportContext.RewardPointsHistories.Load(ctx.IsPreview ? 0 : order.CustomerId); + var orderItems = ctx.OrderExportContext.OrderItems.Load(perfLoadId); + var shipments = ctx.OrderExportContext.Shipments.Load(perfLoadId); + + dynamic dynObject = ToDynamic(ctx, order); + + if (ctx.Stores.ContainsKey(order.StoreId)) + { + dynObject.Store = ToDynamic(ctx, ctx.Stores[order.StoreId]); + } + + dynObject.Customer = ToDynamic(ctx, customers.FirstOrDefault(x => x.Id == order.CustomerId)); + + dynObject.Customer.RewardPointsHistory = rewardPointsHistories + .Select(x => ToDynamic(ctx, x)) + .ToList(); + + if (rewardPointsHistories.Count > 0) + { + dynObject.Customer._RewardPointsBalance = rewardPointsHistories + .OrderByDescending(x => x.CreatedOnUtc) + .ThenByDescending(x => x.Id) + .FirstOrDefault() + .PointsBalance; + } + + if (ctx.OrderExportContext.Addresses.ContainsKey(order.BillingAddressId)) + { + dynObject.BillingAddress = ToDynamic(ctx, ctx.OrderExportContext.Addresses[order.BillingAddressId].FirstOrDefault()); + } + + if (order.ShippingAddressId.HasValue && ctx.OrderExportContext.Addresses.ContainsKey(order.ShippingAddressId.Value)) + { + dynObject.ShippingAddress = ToDynamic(ctx, ctx.OrderExportContext.Addresses[order.ShippingAddressId.Value].FirstOrDefault()); + } + + dynObject.OrderItems = orderItems + .Select(e => + { + dynamic dyn = ToDynamic(ctx, e); + + if (ctx.ProductTemplates.ContainsKey(e.Product.ProductTemplateId)) + dyn.Product._ProductTemplateViewPath = ctx.ProductTemplates[e.Product.ProductTemplateId]; + else + dyn.Product._ProductTemplateViewPath = ""; + + dyn.Product._BasePriceInfo = e.Product.GetBasePriceInfo(_services.Localization, _priceFormatter.Value, _currencyService.Value, _taxService.Value, + _priceCalculationService.Value, ctx.ContextCurrency, decimal.Zero, true); + + ToDeliveryTime(ctx, dyn.Product, e.Product.DeliveryTimeId); + ToQuantityUnit(ctx, dyn.Product, e.Product.QuantityUnitId); + + return dyn; + }) + .ToList(); + + dynObject.Shipments = shipments + .Select(x => ToDynamic(ctx, x)) + .ToList(); + + result.Add(dynObject); + + _services.EventPublisher.Publish(new RowExportingEvent + { + Row = dynObject, + EntityType = ExportEntityType.Order, + ExportRequest = ctx.Request, + ExecuteContext = ctx.ExecuteContext + }); + + return result; + } + + private List Convert(DataExporterContext ctx, Manufacturer manufacturer) + { + var result = new List(); + + var productManufacturers = ctx.ManufacturerExportContext.ProductManufacturers.Load(manufacturer.Id); + + dynamic dynObject = ToDynamic(ctx, manufacturer); + + if (!ctx.IsPreview && manufacturer.PictureId.HasValue) + { + var numberOfPictures = (ctx.Projection.NumberOfPictures ?? int.MaxValue); + var pictures = ctx.ManufacturerExportContext.Pictures.Load(manufacturer.PictureId.Value).Take(numberOfPictures); + + if (pictures.Any()) + dynObject.Picture = ToDynamic(ctx, pictures.First(), _mediaSettings.Value.ManufacturerThumbPictureSize, _mediaSettings.Value.ManufacturerThumbPictureSize); + } + + dynObject.ProductManufacturers = productManufacturers + .OrderBy(x => x.DisplayOrder) + .Select(x => + { + dynamic dyn = new DynamicEntity(x); + + return dyn; + }) + .ToList(); + + result.Add(dynObject); + + _services.EventPublisher.Publish(new RowExportingEvent + { + Row = dynObject, + EntityType = ExportEntityType.Manufacturer, + ExportRequest = ctx.Request, + ExecuteContext = ctx.ExecuteContext + }); + + return result; + } + + private List Convert(DataExporterContext ctx, Category category) + { + var result = new List(); + + var productCategories = ctx.CategoryExportContext.ProductCategories.Load(category.Id); + + dynamic dynObject = ToDynamic(ctx, category); + + if (!ctx.IsPreview && category.PictureId.HasValue) + { + var numberOfPictures = (ctx.Projection.NumberOfPictures ?? int.MaxValue); + var pictures = ctx.CategoryExportContext.Pictures.Load(category.PictureId.Value).Take(numberOfPictures); + + if (pictures.Any()) + dynObject.Picture = ToDynamic(ctx, pictures.First(), _mediaSettings.Value.CategoryThumbPictureSize, _mediaSettings.Value.CategoryThumbPictureSize); + } + + dynObject.ProductCategories = productCategories + .OrderBy(x => x.DisplayOrder) + .Select(x => + { + dynamic dyn = new DynamicEntity(x); + + return dyn; + }) + .ToList(); + + result.Add(dynObject); + + _services.EventPublisher.Publish(new RowExportingEvent + { + Row = dynObject, + EntityType = ExportEntityType.Category, + ExportRequest = ctx.Request, + ExecuteContext = ctx.ExecuteContext + }); + + return result; + } + + private List Convert(DataExporterContext ctx, Customer customer) + { + var result = new List(); + + var perfLoadId = (ctx.IsPreview ? 0 : customer.Id); + var genericAttributes = ctx.CustomerExportContext.GenericAttributes.Load(perfLoadId); + + dynamic dynObject = ToDynamic(ctx, customer); + + dynObject.BillingAddress = ToDynamic(ctx, customer.BillingAddress); + dynObject.ShippingAddress = ToDynamic(ctx, customer.ShippingAddress); + + dynObject.Addresses = customer.Addresses + .Select(x => ToDynamic(ctx, x)) + .ToList(); + + dynObject.CustomerRoles = customer.CustomerRoles + .Select(x => + { + dynamic dyn = new DynamicEntity(x); + + return dyn; + }) + .ToList(); + + dynObject._GenericAttributes = genericAttributes + .Select(x => ToDynamic(ctx, x)) + .ToList(); + + dynObject._HasNewsletterSubscription = ctx.NewsletterSubscriptions.Contains(customer.Email, StringComparer.CurrentCultureIgnoreCase); + + var attrFirstName = genericAttributes.FirstOrDefault(x => x.Key == SystemCustomerAttributeNames.FirstName); + var attrLastName = genericAttributes.FirstOrDefault(x => x.Key == SystemCustomerAttributeNames.LastName); + + string firstName = (attrFirstName == null ? "" : attrFirstName.Value); + string lastName = (attrLastName == null ? "" : attrLastName.Value); + + if (firstName.IsEmpty() && lastName.IsEmpty()) + { + var address = customer.Addresses.FirstOrDefault(); + if (address != null) + { + firstName = address.FirstName; + lastName = address.LastName; + } + } + + dynObject._FullName = firstName.Grow(lastName, " "); + + if (_customerSettings.Value.AllowCustomersToUploadAvatars) + { + var pictureId = genericAttributes.FirstOrDefault(x => x.Key == SystemCustomerAttributeNames.AvatarPictureId); + if (pictureId != null) + { + // reduce traffic and do not export default avatar + dynObject._AvatarPictureUrl = _pictureService.Value.GetPictureUrl(pictureId.Value.ToInt(), _mediaSettings.Value.AvatarPictureSize, false, ctx.Store.Url); + } + } + + result.Add(dynObject); + + _services.EventPublisher.Publish(new RowExportingEvent + { + Row = dynObject, + EntityType = ExportEntityType.Customer, + ExportRequest = ctx.Request, + ExecuteContext = ctx.ExecuteContext + }); + + return result; + } + + private List Convert(DataExporterContext ctx, NewsLetterSubscription subscription) + { + var result = new List(); + dynamic dynObject = ToDynamic(ctx, subscription); + result.Add(dynObject); + + _services.EventPublisher.Publish(new RowExportingEvent + { + Row = dynObject, + EntityType = ExportEntityType.NewsLetterSubscription, + ExportRequest = ctx.Request, + ExecuteContext = ctx.ExecuteContext + }); + + + return result; + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Events/RowExportingEvent.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Events/RowExportingEvent.cs new file mode 100644 index 0000000000..0b96526e00 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Events/RowExportingEvent.cs @@ -0,0 +1,35 @@ +using SmartStore.Core.Domain.DataExchange; + +namespace SmartStore.Services.DataExchange.Export.Events +{ + // TODO: Another event message must be implemented, say 'ColumnsBuildingEvent' + // The consumer of this event (most likely a plugin) could push a list of specific column headers + // into the global export definition. + + public class RowExportingEvent + { + public dynamic Row + { + get; + internal set; + } + + public ExportEntityType EntityType + { + get; + internal set; + } + + public DataExportRequest ExportRequest + { + get; + internal set; + } + + public ExportExecuteContext ExecuteContext + { + get; + internal set; + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportConfigurationInfo.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportConfigurationInfo.cs similarity index 91% rename from src/Libraries/SmartStore.Services/DataExchange/ExportConfigurationInfo.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/ExportConfigurationInfo.cs index 4816d9e2ab..2cfb17796d 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportConfigurationInfo.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportConfigurationInfo.cs @@ -1,6 +1,6 @@ using System; -namespace SmartStore.Services.DataExchange +namespace SmartStore.Services.DataExchange.Export { /// /// Serves information about export provider specific configuration diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportSegmenter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportDataSegmenter.cs similarity index 87% rename from src/Libraries/SmartStore.Services/DataExchange/ExportSegmenter.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/ExportDataSegmenter.cs index b86df2cb45..c84182c6e0 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportSegmenter.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportDataSegmenter.cs @@ -2,19 +2,19 @@ using System.Collections.Generic; using SmartStore.Core; -namespace SmartStore.Services.DataExchange +namespace SmartStore.Services.DataExchange.Export { - public interface IExportSegmenterConsumer + public interface IExportDataSegmenterConsumer { /// /// Total number of records /// - int RecordTotal { get; } + int TotalRecords { get; } /// /// Gets current data segment /// - List CurrentSegment { get; } + IReadOnlyCollection CurrentSegment { get; } /// /// Reads the next segment @@ -23,7 +23,7 @@ public interface IExportSegmenterConsumer bool ReadNextSegment(); } - internal interface IExportSegmenterProvider : IExportSegmenterConsumer, IDisposable + internal interface IExportDataSegmenterProvider : IExportDataSegmenterConsumer, IDisposable { /// /// Whether there is data available @@ -36,7 +36,7 @@ internal interface IExportSegmenterProvider : IExportSegmenterConsumer, IDisposa int RecordPerSegmentCount { get; set; } } - public class ExportSegmenter : IExportSegmenterProvider where T : BaseEntity + public class ExportDataSegmenter : IExportDataSegmenterProvider where T : BaseEntity { private Func> _load; private Action> _loaded; @@ -53,7 +53,7 @@ public class ExportSegmenter : IExportSegmenterProvider where T : BaseEntity private Queue _data; - public ExportSegmenter( + public ExportDataSegmenter( Func> load, Action> loaded, Func> convert, @@ -78,7 +78,7 @@ public ExportSegmenter( /// /// Total number of records /// - public int RecordTotal + public int TotalRecords { get { @@ -127,7 +127,7 @@ public bool HasData /// /// Gets current data segment /// - public List CurrentSegment + public IReadOnlyCollection CurrentSegment { get { @@ -136,7 +136,7 @@ public List CurrentSegment while (_data.Count > 0 && (entity = _data.Dequeue()) != null) { - _convert(entity).ForEach(x => records.Add(x)); + _convert(entity).Each(x => records.Add(x)); if (++_countRecords >= _limit && _limit > 0) return records; diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportExecuteContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExecuteContext.cs similarity index 62% rename from src/Libraries/SmartStore.Services/DataExchange/ExportExecuteContext.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/ExportExecuteContext.cs index 1459463bfb..d5f2860ff8 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportExecuteContext.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExecuteContext.cs @@ -1,190 +1,193 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading; using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Logging; -namespace SmartStore.Services.DataExchange +namespace SmartStore.Services.DataExchange.Export { - public interface IExportExecuteContext + public class ExportExecuteContext { + private DataExportResult _result; + private CancellationToken _cancellation; + private DataExchangeAbortion _providerAbort; + + internal ExportExecuteContext(DataExportResult result, CancellationToken cancellation, string folder) + { + _result = result; + _cancellation = cancellation; + Folder = folder; + ExtraDataUnits = new List(); + CustomProperties = new Dictionary(); + } + /// /// Provides the data to be exported /// - IExportSegmenterConsumer Segmenter { get; } + public IExportDataSegmenterConsumer DataSegmenter { get; set; } /// /// The store context to be used for the export /// - dynamic Store { get; } + public dynamic Store { get; internal set; } /// /// The customer context to be used for the export /// - dynamic Customer { get; } + public dynamic Customer { get; internal set; } /// /// The currency context to be used for the export /// - dynamic Currency { get; } + public dynamic Currency { get; internal set; } /// /// The language context to be used for the export /// - dynamic Language { get; } + public dynamic Language { get; internal set; } /// /// Projection data /// - ExportProjection Projection { get; } + public ExportProjection Projection { get; internal set; } /// /// To log information into the export log file /// - ILogger Log { get; } + public ILogger Log { get; internal set; } /// /// Indicates whether and how to abort the export /// - ExportAbortion Abort { get; set; } + public DataExchangeAbortion Abort + { + get + { + if (_cancellation.IsCancellationRequested || IsMaxFailures) + return DataExchangeAbortion.Hard; + + return _providerAbort; + } + set + { + _providerAbort = value; + } + } + public bool IsMaxFailures + { + get { return RecordsFailed > 11; } + } /// - /// The maximum allowed file name length + /// Identifier of current data stream. Can be null. /// - int MaxFileNameLength { get; } + public string DataStreamId { get; set; } /// - /// The path of the export content folder + /// Stream used to write data to /// - string Folder { get; } + public Stream DataStream { get; internal set; } /// - /// The name of the current export file + /// List with extra data units/streams required by provider /// - string FileName { get; } + public List ExtraDataUnits { get; private set; } /// - /// The path of the current export file + /// The maximum allowed file name length /// - string FilePath { get; } - + public int MaxFileNameLength { get; internal set; } /// - /// Whether the profile has a public deployment into "Exchange" folder + /// The name of the current export file /// - bool HasPublicDeployment { get; } + public string FileName { get; internal set; } /// - /// The local path to the public export folder "Exchange". null if the profile has no public deployment. + /// The path of the export content folder /// - string PublicFolderPath { get; } + public string Folder { get; private set; } /// - /// The public URL of the export file (accessible through the internet). null if the profile has no public deployment. + /// Whether the profile has a public deployment into "Exchange" folder /// - string PublicFileUrl { get; } + public bool HasPublicDeployment { get; internal set; } + /// + /// The local path to the public export folder "Exchange". null if the profile has no public deployment. + /// + public string PublicFolderPath { get; internal set; } /// /// Provider specific configuration data /// - object ConfigurationData { get; } + public object ConfigurationData { get; internal set; } /// /// Use this dictionary for any custom data required along the export /// - Dictionary CustomProperties { get; set; } + public Dictionary CustomProperties { get; set; } /// /// Number of successful processed records /// - int RecordsSucceeded { get; set; } + public int RecordsSucceeded { get; set; } /// /// Number of failed records /// - int RecordsFailed { get; set; } + public int RecordsFailed { get; set; } /// /// Processes an exception that occurred while exporting a record /// - /// Exception - void RecordException(Exception exc, int entityId); - } - - - public class ExportExecuteContext : IExportExecuteContext - { - private ExportExecuteResult _result; - private CancellationToken _cancellation; - private ExportAbortion _providerAbort; - - internal ExportExecuteContext(ExportExecuteResult result, CancellationToken cancellation, string folder) + /// Exception + public void RecordException(Exception exception, int entityId) { - _result = result; - _cancellation = cancellation; - Folder = folder; + ++RecordsFailed; - CustomProperties = new Dictionary(); - } + Log.Error("Error while processing record with id {0}: {1}".FormatInvariant(entityId, exception.ToAllMessages()), exception); - public IExportSegmenterConsumer Segmenter { get; set; } + if (IsMaxFailures) + _result.LastError = exception.ToString(); + } - public dynamic Store { get; internal set; } - public dynamic Customer { get; internal set; } - public dynamic Currency { get; internal set; } - public dynamic Language { get; internal set; } - public ExportProjection Projection { get; internal set; } + public ProgressValueSetter ProgressValueSetter { get; internal set; } - public ILogger Log { get; internal set; } - - public ExportAbortion Abort + /// + /// Allows to set a progress message + /// + /// Output message + public void SetProgress(string message) { - get - { - if (_cancellation.IsCancellationRequested || IsMaxFailures) - return ExportAbortion.Hard; - - return _providerAbort; - } - set + if (ProgressValueSetter != null && message.HasValue()) { - _providerAbort = value; + try + { + ProgressValueSetter.Invoke(0, 0, message); + } + catch { } } } + } - public bool IsMaxFailures - { - get { return RecordsFailed > 11; } - } - - public int MaxFileNameLength { get; internal set; } - - public string Folder { get; private set; } - public string FileName { get; internal set; } - public string FileExtension { get; internal set; } - public string FilePath { get; internal set; } - - public bool HasPublicDeployment { get; internal set; } - public string PublicFolderPath { get; internal set; } - public string PublicFileUrl { get; internal set; } - - public object ConfigurationData { get; internal set; } - - public Dictionary CustomProperties { get; set; } - - public int RecordsSucceeded { get; set; } - public int RecordsFailed { get; set; } - - public void RecordException(Exception exc, int entityId) - { - ++RecordsFailed; + public class ExportDataUnit + { + /// + /// Your Id to identify this stream within a list of streams + /// + public string Id { get; set; } - Log.Error("Error while processing record with id {0}: {1}".FormatInvariant(entityId, exc.ToAllMessages()), exc); + /// + /// Stream used to write data to + /// + public Stream DataStream { get; internal set; } - if (IsMaxFailures) - _result.LastError = exc.ToString(); - } + /// + /// The name of the file to be created + /// + public string FileName { get; set; } } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs new file mode 100644 index 0000000000..0371702eec --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExtensions.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Web; +using SmartStore.Core; +using SmartStore.Core.Domain; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Plugins; +using SmartStore.Services.Localization; +using SmartStore.Utilities; + +namespace SmartStore.Services.DataExchange.Export +{ + public static class ExportExtensions + { + /// + /// Returns a value indicating whether the export provider is valid + /// + /// Export provider + /// true provider is valid, false provider is invalid. + public static bool IsValid(this Provider provider) + { + return provider != null && provider.Value != null; + } + + /// + /// Gets the localized friendly name or the system name as fallback + /// + /// Export provider + /// Localization service + /// Provider name + public static string GetName(this Provider provider, ILocalizationService localizationService) + { + var systemName = provider.Metadata.SystemName; + var resourceName = provider.Metadata.ResourceKeyPattern.FormatInvariant(systemName, "FriendlyName"); + var name = localizationService.GetResource(resourceName, 0, false, systemName, true); + + return (name.IsEmpty() ? systemName : name); + } + + /// + /// Get temporary folder for an export profile + /// + /// Export profile + /// Folder path + public static string GetExportFolder(this ExportProfile profile, bool content = false, bool create = false) + { + var path = CommonHelper.MapPath(string.Concat(profile.FolderName, content ? "/Content" : "")); + + if (create && !System.IO.Directory.Exists(path)) + System.IO.Directory.CreateDirectory(path); + + return path; + } + + /// + /// Get log file path for an export profile + /// + /// Export profile + /// Log file path + public static string GetExportLogPath(this ExportProfile profile) + { + return Path.Combine(profile.GetExportFolder(), "log.txt"); + } + + /// + /// Gets the ZIP path for a profile + /// + /// Export profile + /// ZIP file path + public static string GetExportZipPath(this ExportProfile profile) + { + var name = (new DirectoryInfo(profile.FolderName)).Name; + if (name.IsEmpty()) + name = "ExportData"; + + return Path.Combine(profile.GetExportFolder(), name.ToValidFileName() + ".zip"); + } + + /// + /// Gets existing export files for an export profile + /// + /// Export profile + /// Export provider + /// List of file names + public static IEnumerable GetExportFiles(this ExportProfile profile, Provider provider) + { + var exportFolder = profile.GetExportFolder(true); + + if (System.IO.Directory.Exists(exportFolder) && provider.Value.FileExtension.HasValue()) + { + var filter = "*.{0}".FormatInvariant(provider.Value.FileExtension.ToLower()); + + return System.IO.Directory.EnumerateFiles(exportFolder, filter, SearchOption.AllDirectories).OrderBy(x => x); + } + + return Enumerable.Empty(); + } + + /// + /// Get number of existing export files + /// + /// Export profile + /// Export provider + /// Number of export files + public static int GetExportFileCount(this ExportProfile profile, Provider provider) + { + var result = profile.GetExportFiles(provider).Count(); + + if (File.Exists(profile.GetExportZipPath())) + ++result; + + return result; + } + + /// + /// Resolves the file name pattern for an export profile + /// + /// Export profile + /// Store + /// One based file index + /// The maximum length of the file name + /// Resolved file name pattern + public static string ResolveFileNamePattern(this ExportProfile profile, Store store, int fileIndex, int maxFileNameLength) + { + var sb = new StringBuilder(profile.FileNamePattern); + + sb.Replace("%Profile.Id%", profile.Id.ToString()); + sb.Replace("%Profile.FolderName%", profile.FolderName); + sb.Replace("%Store.Id%", store.Id.ToString()); + sb.Replace("%File.Index%", fileIndex.ToString("D4")); + + if (profile.FileNamePattern.Contains("%Profile.SeoName%")) + sb.Replace("%Profile.SeoName%", SeoHelper.GetSeName(profile.Name, true, false).Replace("/", "").Replace("-", "")); + + if (profile.FileNamePattern.Contains("%Store.SeoName%")) + sb.Replace("%Store.SeoName%", profile.PerStore ? SeoHelper.GetSeName(store.Name, true, false) : "allstores"); + + if (profile.FileNamePattern.Contains("%Random.Number%")) + sb.Replace("%Random.Number%", CommonHelper.GenerateRandomInteger().ToString()); + + if (profile.FileNamePattern.Contains("%Timestamp%")) + sb.Replace("%Timestamp%", DateTime.UtcNow.ToString("s", CultureInfo.InvariantCulture)); + + var result = sb.ToString() + .ToValidFileName("") + .Truncate(maxFileNameLength); + + return result; + } + + /// + /// Get path of the deployment folder + /// + /// Export deployment + /// Deployment folder path + public static string GetDeploymentFolder(this ExportDeployment deployment, bool create = false) + { + if (deployment == null) + return null; + + string path = null; + + if (deployment.DeploymentType == ExportDeploymentType.PublicFolder) + { + if (deployment.SubFolder.HasValue()) + path = Path.Combine(HttpRuntime.AppDomainAppPath, DataExporter.PublicFolder, deployment.SubFolder); + else + path = Path.Combine(HttpRuntime.AppDomainAppPath, DataExporter.PublicFolder); + } + else if (deployment.DeploymentType == ExportDeploymentType.FileSystem) + { + if (deployment.FileSystemPath.IsEmpty()) + return null; + + if (Path.IsPathRooted(deployment.FileSystemPath)) + { + path = deployment.FileSystemPath; + } + else + { + path = FileSystemHelper.ValidateRootPath(deployment.FileSystemPath); + path = CommonHelper.MapPath(path); + } + } + + if (create && !System.IO.Directory.Exists(path)) + System.IO.Directory.CreateDirectory(path); + + return path; + } + + /// + /// Get url of the public folder and take filtering and projection into consideration + /// + /// Export deployment + /// Common services + /// Store entity + /// Absolute URL of the public folder (always ends with /) or null + public static string GetPublicFolderUrl(this ExportDeployment deployment, ICommonServices services, Store store = null) + { + if (deployment != null && deployment.DeploymentType == ExportDeploymentType.PublicFolder) + { + if (store == null) + { + var filter = XmlHelper.Deserialize(deployment.Profile.Filtering); + var storeId = filter.StoreId; + + if (storeId == 0) + { + var projection = XmlHelper.Deserialize(deployment.Profile.Projection); + storeId = (projection.StoreId ?? 0); + } + + store = (storeId == 0 ? services.StoreContext.CurrentStore : services.StoreService.GetStoreById(storeId)); + } + + var url = string.Concat( + store.Url.EnsureEndsWith("/"), + DataExporter.PublicFolder.EnsureEndsWith("/"), + deployment.SubFolder.HasValue() ? deployment.SubFolder.EnsureEndsWith("/") : "" + ); + + return url; + } + + return null; + } + + /// + /// Get icon class for a deployment type + /// + /// Deployment type + /// Icon class + public static string GetIconClass(this ExportDeploymentType type) + { + switch (type) + { + case ExportDeploymentType.FileSystem: + return "fa-folder-open-o"; + case ExportDeploymentType.Email: + return "fa-envelope-o"; + case ExportDeploymentType.Http: + return "fa-globe"; + case ExportDeploymentType.Ftp: + return "fa-files-o"; + case ExportDeploymentType.PublicFolder: + return "fa-unlock"; + default: + return "fa-question"; + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportAttributes.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportFeaturesAttribute.cs similarity index 60% rename from src/Libraries/SmartStore.Services/DataExchange/ExportAttributes.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/ExportFeaturesAttribute.cs index ece4d81d38..eee6f1554e 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportAttributes.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportFeaturesAttribute.cs @@ -1,20 +1,15 @@ using System; using SmartStore.Core.Domain.DataExchange; -namespace SmartStore.Services.DataExchange +namespace SmartStore.Services.DataExchange.Export { /// /// Declares data processing types supported by an export provider. /// Projection type controls whether to display corresponding projection fields while editing an export profile. /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public sealed class ExportSupportingAttribute : Attribute + public sealed class ExportFeaturesAttribute : Attribute { - public ExportSupportingAttribute(params ExportSupport[] types) - { - Types = types; - } - - public ExportSupport[] Types { get; set; } + public ExportFeatures Features { get; set; } } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportService.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs similarity index 52% rename from src/Libraries/SmartStore.Services/DataExchange/ExportService.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs index 250e0eecff..1814ae56ae 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs @@ -9,16 +9,15 @@ using SmartStore.Core.Domain.Tasks; using SmartStore.Core.Events; using SmartStore.Core.Plugins; -using SmartStore.Services.DataExchange.ExportTask; using SmartStore.Services.Localization; using SmartStore.Services.Tasks; using SmartStore.Utilities; -namespace SmartStore.Services.DataExchange +namespace SmartStore.Services.DataExchange.Export { - public partial class ExportService : IExportService + public partial class ExportProfileService : IExportProfileService { - private const string _defaultFileNamePattern = "%Store.Id%-%ExportProfile.Id%-%Misc.FileNumber%-%ExportProfile.SeoName%"; + private const string _defaultFileNamePattern = "%Store.Id%-%Profile.Id%-%File.Index%-%Profile.SeoName%"; private readonly IRepository _exportProfileRepository; private readonly IRepository _exportDeploymentRepository; @@ -28,7 +27,7 @@ public partial class ExportService : IExportService private readonly DataExchangeSettings _dataExchangeSettings; private readonly ILocalizationService _localizationService; - public ExportService( + public ExportProfileService( IRepository exportProfileRepository, IRepository exportDeploymentRepository, IEventPublisher eventPublisher, @@ -46,52 +45,38 @@ public ExportService( _localizationService = localizationService; } - #region Export profiles - - public virtual ExportProfile CreateVolatileProfile(Provider provider) + public virtual ExportProfile InsertExportProfile( + string providerSystemName, + string name, + string fileExtension, + ExportFeatures features, + bool isSystemProfile = false, + string profileSystemName = null, + int cloneFromProfileId = 0) { - var name = provider.GetName(_localizationService); - var seoName = SeoHelper.GetSeName(name, true, false).Replace("/", "").Replace("-", ""); + Guard.ArgumentNotEmpty(() => providerSystemName); - var profile = new ExportProfile - { - Id = 0, - Name = name, - FolderName = seoName.ToValidPath().Truncate(_dataExchangeSettings.MaxFileNameLength), - FileNamePattern = _defaultFileNamePattern, - ProviderSystemName = provider.Metadata.SystemName, - Enabled = true, - SchedulingTaskId = 0, - PerStore = false, - CreateZipArchive = false, - Cleanup = false, - ScheduleTask = null, // volatile schedule task impossible cause of database accesses by core - Deployments = new List() - }; - - // profile.Projection and profile.Filtering should be null here + var profileCount = _exportProfileRepository.Table.Count(x => x.ProviderSystemName == providerSystemName); - return profile; - } + if (name.IsEmpty()) + name = providerSystemName; - public virtual ExportProfile InsertExportProfile(Provider provider, int cloneFromProfileId = 0) - { - if (provider == null) - throw new ArgumentNullException("provider"); + if (!isSystemProfile) + name = string.Concat(_localizationService.GetResource("Common.My"), " ", name); + + name = string.Concat(name, " ", profileCount + 1); var cloneProfile = GetExportProfileById(cloneFromProfileId); - + ScheduleTask task = null; ExportProfile profile = null; - var name = provider.GetName(_localizationService); - var seoName = SeoHelper.GetSeName(name, true, false).Replace("/", "").Replace("-", ""); - + if (cloneProfile == null) { task = new ScheduleTask { - CronExpression = "0 */6 * * *", // every six hours - Type = (new ExportProfileTask()).GetType().AssemblyQualifiedNameWithoutVersion(), + CronExpression = "0 */6 * * *", // every six hours + Type = typeof(DataExportTask).AssemblyQualifiedNameWithoutVersion(), Enabled = false, StopOnError = false, IsHidden = true @@ -103,62 +88,94 @@ public virtual ExportProfile InsertExportProfile(Provider provi task.LastEndUtc = task.LastStartUtc = task.LastSuccessUtc = null; } - task.Name = string.Concat(name, " export task"); - + task.Name = string.Concat(name, " Task"); + _scheduleTaskService.InsertTask(task); if (cloneProfile == null) { - // what we do here is to preset typical settings for feed creation - // but on the other hand they may be untypical for generic data export\exchange - var projection = new ExportProjection + profile = new ExportProfile { - RemoveCriticalCharacters = true, - CriticalCharacters = "¼,½,¾", - PriceType = PriceDisplayType.PreSelectedPrice, - NoGroupedProducts = (provider.Supports(ExportSupport.ProjectionNoGroupedProducts) ? true : false) + FileNamePattern = _defaultFileNamePattern }; - var filter = new ExportFilter + if (isSystemProfile) { - IsPublished = true - }; - - profile = new ExportProfile + profile.Enabled = true; + profile.PerStore = false; + profile.CreateZipArchive = false; + profile.Cleanup = false; + } + else { - FileNamePattern = _defaultFileNamePattern, - Filtering = XmlHelper.Serialize(filter), - Projection = XmlHelper.Serialize(projection) - }; + // what we do here is to preset typical settings for feed creation + // but on the other hand they may be untypical for generic data export\exchange + var projection = new ExportProjection + { + RemoveCriticalCharacters = true, + CriticalCharacters = "¼,½,¾", + PriceType = PriceDisplayType.PreSelectedPrice, + NoGroupedProducts = (features.HasFlag(ExportFeatures.CanOmitGroupedProducts) ? true : false), + DescriptionMerging = ExportDescriptionMerging.Description + }; + + var filter = new ExportFilter + { + IsPublished = true + }; + + profile.Projection = XmlHelper.Serialize(projection); + profile.Filtering = XmlHelper.Serialize(filter); + } } else { - profile = cloneProfile.Clone(); + profile = cloneProfile.Clone(); } + profile.IsSystemProfile = isSystemProfile; profile.Name = name; - profile.FolderName = seoName.ToValidPath().Truncate(_dataExchangeSettings.MaxFileNameLength); - profile.ProviderSystemName = provider.Metadata.SystemName; + profile.ProviderSystemName = providerSystemName; profile.SchedulingTaskId = task.Id; + var cleanedSystemName = providerSystemName + .Replace("Exports.", "") + .Replace("Feeds.", "") + .Replace("/", "") + .Replace("-", ""); + + var folderName = SeoHelper.GetSeName(cleanedSystemName, true, false) + .ToValidPath() + .Truncate(_dataExchangeSettings.MaxFileNameLength); + + profile.FolderName = "~/App_Data/ExportProfiles/" + FileSystemHelper.CreateNonExistingDirectoryName(CommonHelper.MapPath("~/App_Data/ExportProfiles"), folderName); + + if (profileSystemName.IsEmpty() && isSystemProfile) + profile.SystemName = cleanedSystemName; + else + profile.SystemName = profileSystemName; + _exportProfileRepository.Insert(profile); + task.Alias = profile.Id.ToString(); _scheduleTaskService.UpdateTask(task); - if (provider.Value.FileExtension.HasValue()) + if (fileExtension.HasValue() && !isSystemProfile) { if (cloneProfile == null) { - if (provider.Supports(ExportSupport.CreateInitialPublicDeployment)) + if (features.HasFlag(ExportFeatures.CreatesInitialPublicDeployment)) { + var subFolder = FileSystemHelper.CreateNonExistingDirectoryName(CommonHelper.MapPath("~/" + DataExporter.PublicFolder), folderName); + profile.Deployments.Add(new ExportDeployment { ProfileId = profile.Id, Enabled = true, - IsPublic = true, - DeploymentType = ExportDeploymentType.FileSystem, - Name = profile.Name + DeploymentType = ExportDeploymentType.PublicFolder, + Name = profile.Name, + SubFolder = subFolder }); UpdateExportProfile(profile); @@ -180,21 +197,46 @@ public virtual ExportProfile InsertExportProfile(Provider provi return profile; } + public virtual ExportProfile InsertExportProfile( + Provider provider, + bool isSystemProfile = false, + string profileSystemName = null, + int cloneFromProfileId = 0) + { + Guard.ArgumentNotNull(() => provider); + + var profile = InsertExportProfile( + provider.Metadata.SystemName, + provider.GetName(_localizationService), + provider.Value.FileExtension, + provider.Metadata.ExportFeatures, + isSystemProfile, + profileSystemName, + cloneFromProfileId); + + return profile; + } + public virtual void UpdateExportProfile(ExportProfile profile) { if (profile == null) throw new ArgumentNullException("profile"); + profile.FolderName = FileSystemHelper.ValidateRootPath(profile.FolderName); + _exportProfileRepository.Update(profile); _eventPublisher.EntityUpdated(profile); } - public virtual void DeleteExportProfile(ExportProfile profile) + public virtual void DeleteExportProfile(ExportProfile profile, bool force = false) { if (profile == null) throw new ArgumentNullException("profile"); + if (!force && profile.IsSystemProfile) + throw new SmartException(_localizationService.GetResource("Admin.DataExchange.Export.CannotDeleteSystemProfile")); + int scheduleTaskId = profile.SchedulingTaskId; var folder = profile.GetExportFolder(); @@ -213,9 +255,9 @@ public virtual void DeleteExportProfile(ExportProfile profile) public virtual IQueryable GetExportProfiles(bool? enabled = null) { - var query = - from x in _exportProfileRepository.Table.Expand(x => x.ScheduleTask) - select x; + var query = _exportProfileRepository.Table + .Expand(x => x.ScheduleTask) + .Expand(x => x.Deployments); if (enabled.HasValue) { @@ -223,7 +265,7 @@ from x in _exportProfileRepository.Table.Expand(x => x.ScheduleTask) } query = query - .OrderBy(x => x.ProviderSystemName) + .OrderBy(x => x.IsSystemProfile) .ThenBy(x => x.Name); return query; @@ -242,15 +284,29 @@ public virtual ExportProfile GetExportProfileById(int id) return profile; } - public virtual IList GetExportProfilesBySystemName(string systemName) + public virtual ExportProfile GetSystemExportProfile(string providerSystemName) + { + if (providerSystemName.IsEmpty()) + return null; + + var query = GetExportProfiles(true); + + var profile = query + .Where(x => x.IsSystemProfile && x.ProviderSystemName == providerSystemName) + .FirstOrDefault(); + + return profile; + } + + public virtual IList GetExportProfilesBySystemName(string providerSystemName) { - if (systemName.IsEmpty()) + if (providerSystemName.IsEmpty()) return new List(); var profiles = _exportProfileRepository.Table .Expand(x => x.ScheduleTask) .Expand(x => x.Deployments) - .Where(x => x.ProviderSystemName == systemName) + .Where(x => x.ProviderSystemName == providerSystemName) .ToList(); return profiles; @@ -261,8 +317,8 @@ public virtual IEnumerable> LoadAllExportProviders(int { var allProviders = _providerManager.GetAllProviders(storeId) .Where(x => x.IsValid() && (showHidden || !x.Metadata.IsHidden)) - .OrderBy(x => x.Metadata.SystemName) - .ThenBy(x => x.Metadata.FriendlyName); + //.OrderBy(x => x.Metadata.SystemName) + .OrderBy(x => x.Metadata.FriendlyName); return allProviders; } @@ -286,6 +342,16 @@ public virtual ExportDeployment GetExportDeploymentById(int id) return deployment; } + public virtual void UpdateExportDeployment(ExportDeployment deployment) + { + if (deployment == null) + throw new ArgumentNullException("deployment"); + + _exportDeploymentRepository.Update(deployment); + + _eventPublisher.EntityUpdated(deployment); + } + public virtual void DeleteExportDeployment(ExportDeployment deployment) { if (deployment == null) @@ -295,7 +361,5 @@ public virtual void DeleteExportDeployment(ExportDeployment deployment) _eventPublisher.EntityDeleted(deployment); } - - #endregion } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProviderBase.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProviderBase.cs new file mode 100644 index 0000000000..c77638f60b --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProviderBase.cs @@ -0,0 +1,50 @@ +using SmartStore.Core.Domain.DataExchange; + +namespace SmartStore.Services.DataExchange.Export +{ + public abstract class ExportProviderBase : IExportProvider + { + /// + /// The exported entity type + /// + public virtual ExportEntityType EntityType + { + get { return ExportEntityType.Product; } + } + + /// + /// File extension of the export files (without dot). Return null for a non file based, on-the-fly export. + /// + public virtual string FileExtension + { + get { return null; } + } + + /// + /// Get provider specific configuration information. Return null when no provider specific configuration is required. + /// + public virtual ExportConfigurationInfo ConfigurationInfo + { + get { return null; } + } + + public void Execute(ExportExecuteContext context) + { + Export(context); + } + + /// + /// Export data to a file + /// + /// Export execution context + protected abstract void Export(ExportExecuteContext context); + + /// + /// Called once per store when export execution ended + /// + /// Export execution context + public virtual void OnExecuted(ExportExecuteContext context) + { + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs new file mode 100644 index 0000000000..68c8b78d55 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportXmlHelper.cs @@ -0,0 +1,918 @@ +using System; +using System.Globalization; +using System.IO; +using System.Text; +using System.Xml; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Directory; +using SmartStore.Core.Domain.Discounts; +using SmartStore.Core.Domain.Media; + +namespace SmartStore.Services.DataExchange.Export +{ + public class ExportXmlHelper : IDisposable + { + private XmlWriter _writer; + private CultureInfo _culture; + private bool _doNotDispose; + + public ExportXmlHelper(XmlWriter writer, bool doNotDispose = false, CultureInfo culture = null) + { + _writer = writer; + _doNotDispose = doNotDispose; + _culture = (culture == null ? CultureInfo.InvariantCulture : culture); + } + public ExportXmlHelper(Stream stream, XmlWriterSettings settings = null, CultureInfo culture = null) + { + if (settings == null) + { + settings = DefaultSettings; + } + + _writer = XmlWriter.Create(stream, settings); + _culture = (culture == null ? CultureInfo.InvariantCulture : culture); + } + + public static XmlWriterSettings DefaultSettings + { + get + { + return new XmlWriterSettings + { + Encoding = Encoding.UTF8, + CheckCharacters = false, + Indent = true, + IndentChars = "\t" + }; + } + } + + public ExportXmlExclude Exclude { get; set; } + + public XmlWriter Writer + { + get { return _writer; } + } + + public void Dispose() + { + if (_writer != null && !_doNotDispose) + { + _writer.Dispose(); + } + } + + public void WriteLocalized(dynamic parentNode) + { + if (parentNode == null || parentNode._Localized == null) + return; + + _writer.WriteStartElement("Localized"); + foreach (dynamic item in parentNode._Localized) + { + _writer.WriteStartElement((string)item.LocaleKey); + _writer.WriteAttributeString("culture", (string)item.Culture); + _writer.WriteString(((string)item.LocaleValue).RemoveInvalidXmlChars()); + _writer.WriteEndElement(); // item.LocaleKey + } + _writer.WriteEndElement(); // Localized + } + + public void WriteGenericAttributes(dynamic parentNode) + { + if (parentNode == null || parentNode._GenericAttributes == null) + return; + + _writer.WriteStartElement("GenericAttributes"); + foreach (dynamic genericAttribute in parentNode._GenericAttributes) + { + GenericAttribute entity = genericAttribute.Entity; + + _writer.WriteStartElement("GenericAttribute"); + _writer.Write("Id", entity.ToString()); + _writer.Write("EntityId", entity.EntityId.ToString()); + _writer.Write("KeyGroup", entity.KeyGroup); + _writer.Write("Key", entity.Key); + _writer.Write("Value", (string)genericAttribute.Value); + _writer.Write("StoreId", entity.StoreId.ToString()); + _writer.WriteEndElement(); // GenericAttribute + } + _writer.WriteEndElement(); // GenericAttributes + } + + public void WriteAddress(dynamic address, string node) + { + if (address == null) + return; + + Address entity = address.Entity; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + _writer.Write("Id", entity.Id.ToString()); + _writer.Write("FirstName", entity.FirstName); + _writer.Write("LastName", entity.LastName); + _writer.Write("Email", entity.Email); + _writer.Write("Company", entity.Company); + _writer.Write("CountryId", entity.CountryId.HasValue ? entity.CountryId.Value.ToString() : ""); + _writer.Write("StateProvinceId", entity.StateProvinceId.HasValue ? entity.StateProvinceId.Value.ToString() : ""); + _writer.Write("City", entity.City); + _writer.Write("Address1", entity.Address1); + _writer.Write("Address2", entity.Address2); + _writer.Write("ZipPostalCode", entity.ZipPostalCode); + _writer.Write("PhoneNumber", entity.PhoneNumber); + _writer.Write("FaxNumber", entity.FaxNumber); + _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); + + if (address.Country != null) + { + dynamic country = address.Country; + Country entityCountry = address.Country.Entity; + + _writer.WriteStartElement("Country"); + _writer.Write("Id", entityCountry.Id.ToString()); + _writer.Write("Name", (string)country.Name); + _writer.Write("AllowsBilling", entityCountry.AllowsBilling.ToString()); + _writer.Write("AllowsShipping", entityCountry.AllowsShipping.ToString()); + _writer.Write("TwoLetterIsoCode", entityCountry.TwoLetterIsoCode); + _writer.Write("ThreeLetterIsoCode", entityCountry.ThreeLetterIsoCode); + _writer.Write("NumericIsoCode", entityCountry.NumericIsoCode.ToString()); + _writer.Write("SubjectToVat", entityCountry.SubjectToVat.ToString()); + _writer.Write("Published", entityCountry.Published.ToString()); + _writer.Write("DisplayOrder", entityCountry.DisplayOrder.ToString()); + _writer.Write("LimitedToStores", entityCountry.LimitedToStores.ToString()); + + WriteLocalized(country); + + _writer.WriteEndElement(); // Country + } + + if (address.StateProvince != null) + { + dynamic stateProvince = address.StateProvince; + StateProvince entityStateProvince = address.StateProvince.Entity; + + _writer.WriteStartElement("StateProvince"); + _writer.Write("Id", entityStateProvince.Id.ToString()); + _writer.Write("CountryId", entityStateProvince.CountryId.ToString()); + _writer.Write("Name", (string)stateProvince.Name); + _writer.Write("Abbreviation", (string)stateProvince.Abbreviation); + _writer.Write("Published", entityStateProvince.Published.ToString()); + _writer.Write("DisplayOrder", entityStateProvince.DisplayOrder.ToString()); + + WriteLocalized(stateProvince); + + _writer.WriteEndElement(); // StateProvince + } + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + + public void WriteCurrency(dynamic currency, string node) + { + if (currency == null) + return; + + Currency entity = currency.Entity; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + _writer.Write("Id", entity.Id.ToString()); + _writer.Write("Name", (string)currency.Name); + _writer.Write("CurrencyCode", entity.CurrencyCode); + _writer.Write("Rate", entity.Rate.ToString(_culture)); + _writer.Write("DisplayLocale", entity.DisplayLocale); + _writer.Write("CustomFormatting", entity.CustomFormatting); + _writer.Write("LimitedToStores", entity.LimitedToStores.ToString()); + _writer.Write("Published", entity.Published.ToString()); + _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); + _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); + _writer.Write("UpdatedOnUtc", entity.UpdatedOnUtc.ToString(_culture)); + _writer.Write("DomainEndings", entity.DomainEndings); + + WriteLocalized(currency); + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + + public void WriteRewardPointsHistory(dynamic rewardPoints, string node) + { + if (rewardPoints == null) + return; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + foreach (dynamic rewardPoint in rewardPoints) + { + RewardPointsHistory entity = rewardPoint.Entity; + + _writer.WriteStartElement("RewardPointsHistory"); + _writer.Write("Id", entity.ToString()); + _writer.Write("CustomerId", entity.ToString()); + _writer.Write("Points", entity.Points.ToString()); + _writer.Write("PointsBalance", entity.PointsBalance.ToString()); + _writer.Write("UsedAmount", entity.UsedAmount.ToString(_culture)); + _writer.Write("Message", (string)rewardPoint.Message); + _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); + _writer.WriteEndElement(); // RewardPointsHistory + } + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + + public void WriteDeliveryTime(dynamic deliveryTime, string node) + { + if (deliveryTime == null) + return; + + DeliveryTime entity = deliveryTime.Entity; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + _writer.Write("Id", entity.Id.ToString()); + _writer.Write("Name", (string)deliveryTime.Name); + _writer.Write("DisplayLocale", entity.DisplayLocale); + _writer.Write("ColorHexValue", entity.ColorHexValue); + _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); + + WriteLocalized(deliveryTime); + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + + public void WriteQuantityUnit(dynamic quantityUnit, string node) + { + if (quantityUnit == null) + return; + + QuantityUnit entity = quantityUnit.Entity; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + _writer.Write("Id", entity.Id.ToString()); + _writer.Write("Name", (string)quantityUnit.Name); + _writer.Write("Description", (string)quantityUnit.Description); + _writer.Write("DisplayLocale", entity.DisplayLocale); + _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); + _writer.Write("IsDefault", entity.IsDefault.ToString()); + + WriteLocalized(quantityUnit); + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + + public void WritePicture(dynamic picture, string node) + { + if (picture == null) + return; + + Picture entity = picture.Entity; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + _writer.Write("Id", entity.Id.ToString()); + _writer.Write("SeoFilename", (string)picture.SeoFilename); + _writer.Write("MimeType", (string)picture.MimeType); + _writer.Write("ThumbImageUrl", (string)picture._ThumbImageUrl); + _writer.Write("ImageUrl", (string)picture._ImageUrl); + _writer.Write("FullSizeImageUrl", (string)picture._FullSizeImageUrl); + _writer.Write("FileName", (string)picture._FileName); + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + + public void WriteCategory(dynamic category, string node) + { + if (category == null) + return; + + Category entity = category.Entity; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + _writer.Write("Id", entity.Id.ToString()); + + if (!Exclude.HasFlag(ExportXmlExclude.Category)) + { + _writer.Write("Name", (string)category.Name); + _writer.Write("FullName", (string)category.FullName); + _writer.Write("Description", (string)category.Description); + _writer.Write("BottomDescription", (string)category.BottomDescription); + _writer.Write("CategoryTemplateId", entity.CategoryTemplateId.ToString()); + _writer.Write("CategoryTemplateViewPath", (string)category._CategoryTemplateViewPath); + _writer.Write("MetaKeywords", (string)category.MetaKeywords); + _writer.Write("MetaDescription", (string)category.MetaDescription); + _writer.Write("MetaTitle", (string)category.MetaTitle); + _writer.Write("SeName", (string)category.SeName); + _writer.Write("ParentCategoryId", entity.ParentCategoryId.ToString()); + _writer.Write("PictureId", entity.PictureId.HasValue ? entity.PictureId.Value.ToString() : ""); + _writer.Write("PageSize", entity.PageSize.ToString()); + _writer.Write("AllowCustomersToSelectPageSize", entity.AllowCustomersToSelectPageSize.ToString()); + _writer.Write("PageSizeOptions", entity.PageSizeOptions); + _writer.Write("PriceRanges", entity.PriceRanges); + _writer.Write("ShowOnHomePage", entity.ShowOnHomePage.ToString()); + _writer.Write("HasDiscountsApplied", entity.HasDiscountsApplied.ToString()); + _writer.Write("Published", entity.Published.ToString()); + _writer.Write("Deleted", entity.Deleted.ToString()); + _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); + _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); + _writer.Write("UpdatedOnUtc", entity.UpdatedOnUtc.ToString(_culture)); + _writer.Write("SubjectToAcl", entity.SubjectToAcl.ToString()); + _writer.Write("LimitedToStores", entity.LimitedToStores.ToString()); + _writer.Write("Alias", (string)category.Alias); + _writer.Write("DefaultViewMode", entity.DefaultViewMode); + + WritePicture(category.Picture, "Picture"); + + WriteLocalized(category); + } + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + + public void WriteManufacturer(dynamic manufacturer, string node) + { + if (manufacturer == null) + return; + + Manufacturer entity = manufacturer.Entity; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + _writer.Write("Id", entity.Id.ToString()); + _writer.Write("Name", (string)manufacturer.Name); + _writer.Write("SeName", (string)manufacturer.SeName); + _writer.Write("Description", (string)manufacturer.Description); + _writer.Write("ManufacturerTemplateId", entity.ManufacturerTemplateId.ToString()); + _writer.Write("MetaKeywords", (string)manufacturer.MetaKeywords); + _writer.Write("MetaDescription", (string)manufacturer.MetaDescription); + _writer.Write("MetaTitle", (string)manufacturer.MetaTitle); + _writer.Write("PictureId", entity.PictureId.HasValue ? entity.PictureId.Value.ToString() : ""); + _writer.Write("PageSize", entity.PageSize.ToString()); + _writer.Write("AllowCustomersToSelectPageSize", entity.AllowCustomersToSelectPageSize.ToString()); + _writer.Write("PageSizeOptions", entity.PageSizeOptions); + _writer.Write("PriceRanges", entity.PriceRanges); + _writer.Write("Published", entity.Published.ToString()); + _writer.Write("Deleted", entity.Deleted.ToString()); + _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); + _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); + _writer.Write("UpdatedOnUtc", entity.UpdatedOnUtc.ToString(_culture)); + + WritePicture(manufacturer.Picture, "Picture"); + + WriteLocalized(manufacturer); + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + + public void WriteProduct(dynamic product, string node) + { + if (product == null) + return; + + Product entity = product.Entity; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + decimal? basePriceAmount = product.BasePriceAmount; + int? basePriceBaseAmount = product.BasePriceBaseAmount; + decimal? lowestAttributeCombinationPrice = product.LowestAttributeCombinationPrice; + + _writer.Write("Id", entity.Id.ToString()); + _writer.Write("Name", (string)product.Name); + _writer.Write("SeName", (string)product.SeName); + _writer.Write("ShortDescription", (string)product.ShortDescription); + _writer.Write("FullDescription", (string)product.FullDescription); + _writer.Write("AdminComment", (string)product.AdminComment); + _writer.Write("ProductTemplateId", entity.ProductTemplateId.ToString()); + _writer.Write("ProductTemplateViewPath", (string)product._ProductTemplateViewPath); + _writer.Write("ShowOnHomePage", entity.ShowOnHomePage.ToString()); + _writer.Write("HomePageDisplayOrder", entity.HomePageDisplayOrder.ToString()); + _writer.Write("MetaKeywords", (string)product.MetaKeywords); + _writer.Write("MetaDescription", (string)product.MetaDescription); + _writer.Write("MetaTitle", (string)product.MetaTitle); + _writer.Write("AllowCustomerReviews", entity.AllowCustomerReviews.ToString()); + _writer.Write("ApprovedRatingSum", entity.ApprovedRatingSum.ToString()); + _writer.Write("NotApprovedRatingSum", entity.NotApprovedRatingSum.ToString()); + _writer.Write("ApprovedTotalReviews", entity.ApprovedTotalReviews.ToString()); + _writer.Write("NotApprovedTotalReviews", entity.NotApprovedTotalReviews.ToString()); + _writer.Write("Published", entity.Published.ToString()); + _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); + _writer.Write("UpdatedOnUtc", entity.UpdatedOnUtc.ToString(_culture)); + _writer.Write("SubjectToAcl", entity.SubjectToAcl.ToString()); + _writer.Write("LimitedToStores", entity.LimitedToStores.ToString()); + _writer.Write("ProductTypeId", entity.ProductTypeId.ToString()); + _writer.Write("ParentGroupedProductId", entity.ParentGroupedProductId.ToString()); + _writer.Write("Sku", (string)product.Sku); + _writer.Write("ManufacturerPartNumber", (string)product.ManufacturerPartNumber); + _writer.Write("Gtin", (string)product.Gtin); + _writer.Write("IsGiftCard", entity.IsGiftCard.ToString()); + _writer.Write("GiftCardTypeId", entity.GiftCardTypeId.ToString()); + _writer.Write("RequireOtherProducts", entity.RequireOtherProducts.ToString()); + _writer.Write("RequiredProductIds", entity.RequiredProductIds); + _writer.Write("AutomaticallyAddRequiredProducts", entity.AutomaticallyAddRequiredProducts.ToString()); + _writer.Write("IsDownload", entity.IsDownload.ToString()); + _writer.Write("DownloadId", entity.DownloadId.ToString()); + _writer.Write("UnlimitedDownloads", entity.UnlimitedDownloads.ToString()); + _writer.Write("MaxNumberOfDownloads", entity.MaxNumberOfDownloads.ToString()); + _writer.Write("DownloadExpirationDays", entity.DownloadExpirationDays.HasValue ? entity.DownloadExpirationDays.Value.ToString() : ""); + _writer.Write("DownloadActivationTypeId", entity.DownloadActivationTypeId.ToString()); + _writer.Write("HasSampleDownload", entity.HasSampleDownload.ToString()); + _writer.Write("SampleDownloadId", entity.SampleDownloadId.HasValue ? entity.SampleDownloadId.Value.ToString() : ""); + _writer.Write("HasUserAgreement", entity.HasUserAgreement.ToString()); + _writer.Write("UserAgreementText", entity.UserAgreementText); + _writer.Write("IsRecurring", entity.IsRecurring.ToString()); + _writer.Write("RecurringCycleLength", entity.RecurringCycleLength.ToString()); + _writer.Write("RecurringCyclePeriodId", entity.RecurringCyclePeriodId.ToString()); + _writer.Write("RecurringTotalCycles", entity.RecurringTotalCycles.ToString()); + _writer.Write("IsShipEnabled", entity.IsShipEnabled.ToString()); + _writer.Write("IsFreeShipping", entity.IsFreeShipping.ToString()); + _writer.Write("AdditionalShippingCharge", entity.AdditionalShippingCharge.ToString(_culture)); + _writer.Write("IsTaxExempt", entity.IsTaxExempt.ToString()); + _writer.Write("TaxCategoryId", entity.TaxCategoryId.ToString()); + _writer.Write("ManageInventoryMethodId", entity.ManageInventoryMethodId.ToString()); + _writer.Write("StockQuantity", entity.StockQuantity.ToString()); + _writer.Write("DisplayStockAvailability", entity.DisplayStockAvailability.ToString()); + _writer.Write("DisplayStockQuantity", entity.DisplayStockQuantity.ToString()); + _writer.Write("MinStockQuantity", entity.MinStockQuantity.ToString()); + _writer.Write("LowStockActivityId", entity.LowStockActivityId.ToString()); + _writer.Write("NotifyAdminForQuantityBelow", entity.NotifyAdminForQuantityBelow.ToString()); + _writer.Write("BackorderModeId", entity.BackorderModeId.ToString()); + _writer.Write("AllowBackInStockSubscriptions", entity.AllowBackInStockSubscriptions.ToString()); + _writer.Write("OrderMinimumQuantity", entity.OrderMinimumQuantity.ToString()); + _writer.Write("OrderMaximumQuantity", entity.OrderMaximumQuantity.ToString()); + _writer.Write("AllowedQuantities", entity.AllowedQuantities); + _writer.Write("DisableBuyButton", entity.DisableBuyButton.ToString()); + _writer.Write("DisableWishlistButton", entity.DisableWishlistButton.ToString()); + _writer.Write("AvailableForPreOrder", entity.AvailableForPreOrder.ToString()); + _writer.Write("CallForPrice", entity.CallForPrice.ToString()); + _writer.Write("Price", entity.Price.ToString(_culture)); + _writer.Write("OldPrice", entity.OldPrice.ToString(_culture)); + _writer.Write("ProductCost", entity.ProductCost.ToString(_culture)); + _writer.Write("SpecialPrice", entity.SpecialPrice.HasValue ? entity.SpecialPrice.Value.ToString(_culture) : ""); + _writer.Write("SpecialPriceStartDateTimeUtc", entity.SpecialPriceStartDateTimeUtc.HasValue ? entity.SpecialPriceStartDateTimeUtc.Value.ToString(_culture) : ""); + _writer.Write("SpecialPriceEndDateTimeUtc", entity.SpecialPriceEndDateTimeUtc.HasValue ? entity.SpecialPriceEndDateTimeUtc.Value.ToString(_culture) : ""); + _writer.Write("CustomerEntersPrice", entity.CustomerEntersPrice.ToString()); + _writer.Write("MinimumCustomerEnteredPrice", entity.MinimumCustomerEnteredPrice.ToString(_culture)); + _writer.Write("MaximumCustomerEnteredPrice", entity.MaximumCustomerEnteredPrice.ToString(_culture)); + _writer.Write("HasTierPrices", entity.HasTierPrices.ToString()); + _writer.Write("HasDiscountsApplied", entity.HasDiscountsApplied.ToString()); + _writer.Write("Weight", ((decimal)product.Weight).ToString(_culture)); + _writer.Write("Length", ((decimal)product.Length).ToString(_culture)); + _writer.Write("Width", ((decimal)product.Width).ToString(_culture)); + _writer.Write("Height", ((decimal)product.Height).ToString(_culture)); + _writer.Write("AvailableStartDateTimeUtc", entity.AvailableStartDateTimeUtc.HasValue ? entity.AvailableStartDateTimeUtc.Value.ToString(_culture) : ""); + _writer.Write("AvailableEndDateTimeUtc", entity.AvailableEndDateTimeUtc.HasValue ? entity.AvailableEndDateTimeUtc.Value.ToString(_culture) : ""); + _writer.Write("BasePriceEnabled", ((bool)product.BasePriceEnabled).ToString()); + _writer.Write("BasePriceMeasureUnit", (string)product.BasePriceMeasureUnit); + _writer.Write("BasePriceAmount", basePriceAmount.HasValue ? basePriceAmount.Value.ToString(_culture) : ""); + _writer.Write("BasePriceBaseAmount", basePriceBaseAmount.HasValue ? basePriceBaseAmount.Value.ToString() : ""); + _writer.Write("BasePriceHasValue", ((bool)product.BasePriceHasValue).ToString()); + _writer.Write("BasePriceInfo", (string)product._BasePriceInfo); + _writer.Write("VisibleIndividually", entity.VisibleIndividually.ToString()); + _writer.Write("DisplayOrder", entity.DisplayOrder.ToString()); + _writer.Write("BundleTitleText", entity.BundleTitleText); + _writer.Write("BundlePerItemPricing", entity.BundlePerItemPricing.ToString()); + _writer.Write("BundlePerItemShipping", entity.BundlePerItemShipping.ToString()); + _writer.Write("BundlePerItemShoppingCart", entity.BundlePerItemShoppingCart.ToString()); + _writer.Write("LowestAttributeCombinationPrice", lowestAttributeCombinationPrice.HasValue ? lowestAttributeCombinationPrice.Value.ToString(_culture) : ""); + _writer.Write("IsEsd", entity.IsEsd.ToString()); + + WriteLocalized(product); + + WriteDeliveryTime(product.DeliveryTime, "DeliveryTime"); + + WriteQuantityUnit(product.QuantityUnit, "QuantityUnit"); + + if (product.AppliedDiscounts != null) + { + _writer.WriteStartElement("AppliedDiscounts"); + foreach (dynamic discount in product.AppliedDiscounts) + { + Discount entityDiscount = discount.Entity; + + _writer.WriteStartElement("AppliedDiscount"); + _writer.Write("Id", entityDiscount.Id.ToString()); + _writer.Write("Name", (string)discount.Name); + _writer.Write("DiscountTypeId", entityDiscount.DiscountTypeId.ToString()); + _writer.Write("UsePercentage", entityDiscount.UsePercentage.ToString()); + _writer.Write("DiscountPercentage", entityDiscount.DiscountPercentage.ToString(_culture)); + _writer.Write("DiscountAmount", entityDiscount.DiscountAmount.ToString(_culture)); + _writer.Write("StartDateUtc", entityDiscount.StartDateUtc.HasValue ? entityDiscount.StartDateUtc.Value.ToString(_culture) : ""); + _writer.Write("EndDateUtc", entityDiscount.EndDateUtc.HasValue ? entityDiscount.EndDateUtc.Value.ToString(_culture) : ""); + _writer.Write("RequiresCouponCode", entityDiscount.RequiresCouponCode.ToString()); + _writer.Write("CouponCode", entityDiscount.CouponCode); + _writer.Write("DiscountLimitationId", entityDiscount.DiscountLimitationId.ToString()); + _writer.Write("LimitationTimes", entityDiscount.LimitationTimes.ToString()); + _writer.WriteEndElement(); // AppliedDiscount + } + _writer.WriteEndElement(); // AppliedDiscounts + } + + if (product.TierPrices != null) + { + _writer.WriteStartElement("TierPrices"); + foreach (dynamic tierPrice in product.TierPrices) + { + TierPrice entityTierPrice = tierPrice.Entity; + + _writer.WriteStartElement("TierPrice"); + _writer.Write("Id", entityTierPrice.Id.ToString()); + _writer.Write("ProductId", entityTierPrice.ProductId.ToString()); + _writer.Write("StoreId", entityTierPrice.StoreId.ToString()); + _writer.Write("CustomerRoleId", entityTierPrice.CustomerRoleId.HasValue ? entityTierPrice.CustomerRoleId.Value.ToString() : ""); + _writer.Write("Quantity", entityTierPrice.Quantity.ToString()); + _writer.Write("Price", entityTierPrice.Price.ToString(_culture)); + _writer.WriteEndElement(); // TierPrice + } + _writer.WriteEndElement(); // TierPrices + } + + if (product.ProductTags != null) + { + _writer.WriteStartElement("ProductTags"); + foreach (dynamic tag in product.ProductTags) + { + _writer.WriteStartElement("ProductTag"); + _writer.Write("Id", ((int)tag.Id).ToString()); + _writer.Write("Name", (string)tag.Name); + _writer.Write("SeName", (string)tag.SeName); + + WriteLocalized(tag); + + _writer.WriteEndElement(); // ProductTag + } + _writer.WriteEndElement(); // ProductTags + } + + if (product.ProductAttributes != null) + { + _writer.WriteStartElement("ProductAttributes"); + foreach (dynamic pva in product.ProductAttributes) + { + ProductVariantAttribute entityPva = pva.Entity; + + _writer.WriteStartElement("ProductAttribute"); + _writer.Write("Id", entityPva.Id.ToString()); + _writer.Write("TextPrompt", (string)pva.TextPrompt); + _writer.Write("IsRequired", entityPva.IsRequired.ToString()); + _writer.Write("AttributeControlTypeId", entityPva.AttributeControlTypeId.ToString()); + _writer.Write("DisplayOrder", entityPva.DisplayOrder.ToString()); + + _writer.WriteStartElement("Attribute"); + _writer.Write("Id", ((int)pva.Attribute.Id).ToString()); + _writer.Write("Alias", (string)pva.Attribute.Alias); + _writer.Write("Name", (string)pva.Attribute.Name); + _writer.Write("Description", (string)pva.Attribute.Description); + + WriteLocalized(pva.Attribute); + + _writer.WriteEndElement(); // Attribute + + _writer.WriteStartElement("AttributeValues"); + foreach (dynamic value in pva.Attribute.Values) + { + ProductVariantAttributeValue entityPvav = value.Entity; + + _writer.WriteStartElement("AttributeValue"); + _writer.Write("Id", entityPvav.Id.ToString()); + _writer.Write("Alias", (string)value.Alias); + _writer.Write("Name", (string)value.Name); + _writer.Write("ColorSquaresRgb", (string)value.ColorSquaresRgb); + _writer.Write("PriceAdjustment", ((decimal)value.PriceAdjustment).ToString(_culture)); + _writer.Write("WeightAdjustment", ((decimal)value.WeightAdjustment).ToString(_culture)); + _writer.Write("IsPreSelected", entityPvav.IsPreSelected.ToString()); + _writer.Write("DisplayOrder", entityPvav.DisplayOrder.ToString()); + _writer.Write("ValueTypeId", entityPvav.ValueTypeId.ToString()); + _writer.Write("LinkedProductId", entityPvav.LinkedProductId.ToString()); + _writer.Write("Quantity", entityPvav.Quantity.ToString()); + + WriteLocalized(value); + + _writer.WriteEndElement(); // AttributeValue + } + _writer.WriteEndElement(); // AttributeValues + + _writer.WriteEndElement(); // ProductAttribute + } + _writer.WriteEndElement(); // ProductAttributes + } + + if (product.ProductAttributeCombinations != null) + { + _writer.WriteStartElement("ProductAttributeCombinations"); + foreach (dynamic combination in product.ProductAttributeCombinations) + { + ProductVariantAttributeCombination entityPvac = combination.Entity; + + _writer.WriteStartElement("ProductAttributeCombination"); + _writer.Write("Id", entityPvac.Id.ToString()); + _writer.Write("StockQuantity", entityPvac.StockQuantity.ToString()); + _writer.Write("AllowOutOfStockOrders", entityPvac.AllowOutOfStockOrders.ToString()); + _writer.Write("AttributesXml", entityPvac.AttributesXml); + _writer.Write("Sku", entityPvac.Sku); + _writer.Write("Gtin", entityPvac.Gtin); + _writer.Write("ManufacturerPartNumber", entityPvac.ManufacturerPartNumber); + _writer.Write("Price", entityPvac.Price.HasValue ? entityPvac.Price.Value.ToString(_culture) : ""); + _writer.Write("Length", entityPvac.Length.HasValue ? entityPvac.Length.Value.ToString(_culture) : ""); + _writer.Write("Width", entityPvac.Width.HasValue ? entityPvac.Width.Value.ToString(_culture) : ""); + _writer.Write("Height", entityPvac.Height.HasValue ? entityPvac.Height.Value.ToString(_culture) : ""); + _writer.Write("BasePriceAmount", entityPvac.BasePriceAmount.HasValue ? entityPvac.BasePriceAmount.Value.ToString(_culture) : ""); + _writer.Write("BasePriceBaseAmount", entityPvac.BasePriceBaseAmount.HasValue ? entityPvac.BasePriceBaseAmount.Value.ToString() : ""); + _writer.Write("AssignedPictureIds", entityPvac.AssignedPictureIds); + _writer.Write("DeliveryTimeId", entityPvac.DeliveryTimeId.HasValue ? entityPvac.DeliveryTimeId.Value.ToString() : ""); + _writer.Write("IsActive", entityPvac.IsActive.ToString()); + + WriteDeliveryTime(combination.DeliveryTime, "DeliveryTime"); + + WriteQuantityUnit(combination.QuantityUnit, "QuantityUnit"); + + _writer.WriteStartElement("Pictures"); + foreach (dynamic assignedPicture in combination.Pictures) + { + WritePicture(assignedPicture, "Picture"); + } + _writer.WriteEndElement(); // Pictures + + _writer.WriteEndElement(); // ProductAttributeCombination + } + _writer.WriteEndElement(); // ProductAttributeCombinations + } + + if (product.ProductPictures != null) + { + _writer.WriteStartElement("ProductPictures"); + foreach (dynamic productPicture in product.ProductPictures) + { + ProductPicture entityProductPicture = productPicture.Entity; + + _writer.WriteStartElement("ProductPicture"); + _writer.Write("Id", entityProductPicture.Id.ToString()); + _writer.Write("DisplayOrder", entityProductPicture.DisplayOrder.ToString()); + + WritePicture(productPicture.Picture, "Picture"); + + _writer.WriteEndElement(); // ProductPicture + } + _writer.WriteEndElement(); // ProductPictures + } + + if (product.ProductCategories != null) + { + _writer.WriteStartElement("ProductCategories"); + foreach (dynamic productCategory in product.ProductCategories) + { + ProductCategory entityProductCategory = productCategory.Entity; + + _writer.WriteStartElement("ProductCategory"); + _writer.Write("Id", entityProductCategory.Id.ToString()); + _writer.Write("DisplayOrder", entityProductCategory.DisplayOrder.ToString()); + _writer.Write("IsFeaturedProduct", entityProductCategory.IsFeaturedProduct.ToString()); + + WriteCategory(productCategory.Category, "Category"); + + _writer.WriteEndElement(); // ProductCategory + } + _writer.WriteEndElement(); // ProductCategories + } + + if (product.ProductManufacturers != null) + { + _writer.WriteStartElement("ProductManufacturers"); + foreach (dynamic productManu in product.ProductManufacturers) + { + ProductManufacturer entityProductManu = productManu.Entity; + + _writer.WriteStartElement("ProductManufacturer"); + + _writer.Write("Id", entityProductManu.Id.ToString()); + _writer.Write("DisplayOrder", entityProductManu.DisplayOrder.ToString()); + _writer.Write("IsFeaturedProduct", entityProductManu.IsFeaturedProduct.ToString()); + + WriteManufacturer(productManu.Manufacturer, "Manufacturer"); + + _writer.WriteEndElement(); // ProductManufacturer + } + _writer.WriteEndElement(); // ProductManufacturers + } + + if (product.ProductSpecificationAttributes != null) + { + _writer.WriteStartElement("ProductSpecificationAttributes"); + foreach (dynamic psa in product.ProductSpecificationAttributes) + { + ProductSpecificationAttribute entityPsa = psa.Entity; + + _writer.WriteStartElement("ProductSpecificationAttribute"); + + _writer.Write("Id", entityPsa.Id.ToString()); + _writer.Write("ProductId", entityPsa.ProductId.ToString()); + _writer.Write("SpecificationAttributeOptionId", entityPsa.SpecificationAttributeOptionId.ToString()); + _writer.Write("AllowFiltering", entityPsa.AllowFiltering.ToString()); + _writer.Write("ShowOnProductPage", entityPsa.ShowOnProductPage.ToString()); + _writer.Write("DisplayOrder", entityPsa.DisplayOrder.ToString()); + + dynamic option = psa.SpecificationAttributeOption; + SpecificationAttributeOption entitySao = option.Entity; + SpecificationAttribute entitySa = option.SpecificationAttribute.Entity; + + _writer.WriteStartElement("SpecificationAttributeOption"); + _writer.Write("Id", entitySao.Id.ToString()); + _writer.Write("SpecificationAttributeId", entitySao.SpecificationAttributeId.ToString()); + _writer.Write("DisplayOrder", entitySao.DisplayOrder.ToString()); + _writer.Write("Name", (string)option.Name); + + WriteLocalized(option); + + _writer.WriteStartElement("SpecificationAttribute"); + _writer.Write("Id", entitySa.Id.ToString()); + _writer.Write("Name", (string)option.SpecificationAttribute.Name); + _writer.Write("DisplayOrder", entitySa.DisplayOrder.ToString()); + + WriteLocalized(option.SpecificationAttribute); + + _writer.WriteEndElement(); // SpecificationAttribute + _writer.WriteEndElement(); // SpecificationAttributeOption + + _writer.WriteEndElement(); // ProductSpecificationAttribute + } + _writer.WriteEndElement(); // ProductSpecificationAttributes + } + + if (product.ProductBundleItems != null) + { + _writer.WriteStartElement("ProductBundleItems"); + foreach (dynamic bundleItem in product.ProductBundleItems) + { + ProductBundleItem entityPbi = bundleItem.Entity; + + _writer.WriteStartElement("ProductBundleItem"); + _writer.Write("Id", entityPbi.Id.ToString()); + _writer.Write("ProductId", entityPbi.ProductId.ToString()); + _writer.Write("BundleProductId", entityPbi.BundleProductId.ToString()); + _writer.Write("Quantity", entityPbi.Quantity.ToString()); + _writer.Write("Discount", entityPbi.Discount.HasValue ? entityPbi.Discount.Value.ToString(_culture) : ""); + _writer.Write("DiscountPercentage", entityPbi.DiscountPercentage.ToString()); + _writer.Write("Name", (string)bundleItem.Name); + _writer.Write("ShortDescription", (string)bundleItem.ShortDescription); + _writer.Write("FilterAttributes", entityPbi.FilterAttributes.ToString()); + _writer.Write("HideThumbnail", entityPbi.HideThumbnail.ToString()); + _writer.Write("Visible", entityPbi.Visible.ToString()); + _writer.Write("Published", entityPbi.Published.ToString()); + _writer.Write("DisplayOrder", ((int)bundleItem.DisplayOrder).ToString()); + _writer.Write("CreatedOnUtc", entityPbi.CreatedOnUtc.ToString(_culture)); + _writer.Write("UpdatedOnUtc", entityPbi.UpdatedOnUtc.ToString(_culture)); + + WriteLocalized(bundleItem); + + _writer.WriteEndElement(); // ProductBundleItem + } + _writer.WriteEndElement(); // ProductBundleItems + } + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + + public void WriteCustomer(dynamic customer, string node) + { + if (customer == null) + return; + + Customer entity = customer.Entity; + + if (node.HasValue()) + { + _writer.WriteStartElement(node); + } + + _writer.Write("Id", entity.Id.ToString()); + _writer.Write("CustomerGuid", entity.CustomerGuid.ToString()); + _writer.Write("Username", entity.Username); + _writer.Write("Email", entity.Email); + _writer.Write("Password", entity.Password); + _writer.Write("PasswordFormatId", entity.PasswordFormatId.ToString()); + _writer.Write("PasswordSalt", entity.PasswordSalt); + _writer.Write("AdminComment", entity.AdminComment); + _writer.Write("IsTaxExempt", entity.IsTaxExempt.ToString()); + _writer.Write("AffiliateId", entity.AffiliateId.ToString()); + _writer.Write("Active", entity.Active.ToString()); + _writer.Write("Deleted", entity.Deleted.ToString()); + _writer.Write("IsSystemAccount", entity.IsSystemAccount.ToString()); + _writer.Write("SystemName", entity.SystemName); + _writer.Write("LastIpAddress", entity.LastIpAddress); + _writer.Write("CreatedOnUtc", entity.CreatedOnUtc.ToString(_culture)); + _writer.Write("LastLoginDateUtc", entity.LastLoginDateUtc.HasValue ? entity.LastLoginDateUtc.Value.ToString(_culture) : ""); + _writer.Write("LastActivityDateUtc", entity.LastActivityDateUtc.ToString(_culture)); + _writer.Write("RewardPointsBalance", ((int)customer._RewardPointsBalance).ToString()); + + if (customer.CustomerRoles != null) + { + _writer.WriteStartElement("CustomerRoles"); + foreach (dynamic role in customer.CustomerRoles) + { + CustomerRole entityRole = role.Entity; + + _writer.WriteStartElement("CustomerRole"); + _writer.Write("Id", entityRole.Id.ToString()); + _writer.Write("Name", (string)role.Name); + _writer.Write("FreeShipping", entityRole.FreeShipping.ToString()); + _writer.Write("TaxExempt", entityRole.TaxExempt.ToString()); + _writer.Write("TaxDisplayType", entityRole.TaxDisplayType.HasValue ? entityRole.TaxDisplayType.Value.ToString() : ""); + _writer.Write("Active", entityRole.Active.ToString()); + _writer.Write("IsSystemRole", entityRole.IsSystemRole.ToString()); + _writer.Write("SystemName", entityRole.SystemName); + _writer.WriteEndElement(); // CustomerRole + } + _writer.WriteEndElement(); // CustomerRoles + } + + WriteRewardPointsHistory(customer.RewardPointsHistory, "RewardPointsHistories"); + WriteAddress(customer.BillingAddress, "BillingAddress"); + WriteAddress(customer.ShippingAddress, "ShippingAddress"); + + if (customer.Addresses != null) + { + _writer.WriteStartElement("Addresses"); + foreach (dynamic address in customer.Addresses) + { + WriteAddress(address, "Address"); + } + _writer.WriteEndElement(); // Addresses + } + + WriteGenericAttributes(customer); + + if (node.HasValue()) + { + _writer.WriteEndElement(); + } + } + } + + + /// + /// Allows to exclude XML nodes from export + /// + [Flags] + public enum ExportXmlExclude + { + None = 0, + Category = 1 + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/IDataExporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/IDataExporter.cs new file mode 100644 index 0000000000..34b96faf24 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/IDataExporter.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using SmartStore.Core.Domain; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Plugins; + +namespace SmartStore.Services.DataExchange.Export +{ + public interface IDataExporter + { + DataExportResult Export(DataExportRequest request, CancellationToken cancellationToken); + + IList Preview(DataExportRequest request, int pageIndex, int? totalRecords = null); + + int GetDataCount(DataExportRequest request); + } + + + public class DataExportRequest + { + private readonly static ProgressValueSetter _voidProgressValueSetter = DataExportRequest.SetProgress; + + public DataExportRequest(ExportProfile profile, Provider provider) + { + Guard.ArgumentNotNull(() => profile); + Guard.ArgumentNotNull(() => provider); + + Profile = profile; + Provider = provider; + ProgressValueSetter = _voidProgressValueSetter; + + EntitiesToExport = new List(); + CustomData = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + public ExportProfile Profile { get; private set; } + + public Provider Provider { get; private set; } + + public ProgressValueSetter ProgressValueSetter { get; set; } + + public bool HasPermission { get; set; } + + public IList EntitiesToExport { get; set; } + + public IDictionary CustomData { get; private set; } + + public IQueryable ProductQuery { get; set; } + + + private static void SetProgress(int val, int max, string msg) + { + // do nothing + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/IExportService.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProfileService.cs similarity index 54% rename from src/Libraries/SmartStore.Services/DataExchange/IExportService.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/IExportProfileService.cs index 734777325f..b0b0c8a6bd 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/IExportService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProfileService.cs @@ -1,26 +1,46 @@ using System.Collections.Generic; using System.Linq; using SmartStore.Core.Domain; +using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Plugins; -namespace SmartStore.Services.DataExchange +namespace SmartStore.Services.DataExchange.Export { - public interface IExportService + public interface IExportProfileService { /// - /// Creates a volatile export project + /// Inserts an export profile /// - /// Export provider + /// Provider system name + /// The name of the profile + /// File extension supported by provider + /// Features supportde by provider + /// Whether the new profile is a system profile + /// Profile system name + /// Identifier of a profile the settings should be copied from /// New export profile - ExportProfile CreateVolatileProfile(Provider provider); + ExportProfile InsertExportProfile( + string providerSystemName, + string name, + string fileExtension, + ExportFeatures features, + bool isSystemProfile = false, + string profileSystemName = null, + int cloneFromProfileId = 0); /// /// Inserts an export profile /// /// Export provider + /// Whether the new profile is a system profile + /// Profile system name /// Identifier of a profile the settings should be copied from /// New export profile - ExportProfile InsertExportProfile(Provider provider, int cloneFromProfileId = 0); + ExportProfile InsertExportProfile( + Provider provider, + bool isSystemProfile = false, + string profileSystemName = null, + int cloneFromProfileId = 0); /// /// Updates an export profile @@ -32,7 +52,8 @@ public interface IExportService /// Deletes an export profile /// /// Export profile - void DeleteExportProfile(ExportProfile profile); + /// Whether to delete system profiles + void DeleteExportProfile(ExportProfile profile, bool force = false); /// /// Get queryable export profiles @@ -48,12 +69,19 @@ public interface IExportService /// Export profile ExportProfile GetExportProfileById(int id); + /// + /// Gets system export profile by provider system name + /// + /// Provider system name + /// + ExportProfile GetSystemExportProfile(string providerSystemName); + /// /// Gets export profiles by provider system name /// - /// Provider system name + /// Provider system name /// List of export profiles - IList GetExportProfilesBySystemName(string systemName); + IList GetExportProfilesBySystemName(string providerSystemName); /// @@ -80,7 +108,13 @@ public interface IExportService ExportDeployment GetExportDeploymentById(int id); /// - /// Deleted a export deployment + /// Update an export deployment + /// + /// Export deployment + void UpdateExportDeployment(ExportDeployment deployment); + + /// + /// Deleted an export deployment /// /// Export deployment void DeleteExportDeployment(ExportDeployment deployment); diff --git a/src/Libraries/SmartStore.Services/DataExchange/IExportProvider.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProvider.cs similarity index 83% rename from src/Libraries/SmartStore.Services/DataExchange/IExportProvider.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/IExportProvider.cs index c0e683a0e8..60be2e11a9 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/IExportProvider.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProvider.cs @@ -1,12 +1,12 @@ using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Plugins; -namespace SmartStore.Services.DataExchange +namespace SmartStore.Services.DataExchange.Export { public partial interface IExportProvider : IProvider, IUserEditable { /// - /// Th exported entity type + /// The exported entity type /// ExportEntityType EntityType { get; } @@ -24,12 +24,12 @@ public partial interface IExportProvider : IProvider, IUserEditable /// Export data to a file /// /// Export execution context - void Execute(IExportExecuteContext context); + void Execute(ExportExecuteContext context); /// /// Called once per store when export execution ended /// /// Export execution context - void ExecuteEnded(IExportExecuteContext context); + void OnExecuted(ExportExecuteContext context); } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportTask/ExportDataContextCategory.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CategoryExportContext.cs similarity index 88% rename from src/Libraries/SmartStore.Services/DataExchange/ExportTask/ExportDataContextCategory.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CategoryExportContext.cs index f19fb56f47..ab6665b31e 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportTask/ExportDataContextCategory.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CategoryExportContext.cs @@ -5,9 +5,9 @@ using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Media; -namespace SmartStore.Services.DataExchange.ExportTask +namespace SmartStore.Services.DataExchange.Export.Internal { - internal class ExportDataContextCategory + internal class CategoryExportContext { protected List _categoryIds; protected List _pictureIds; @@ -18,7 +18,8 @@ internal class ExportDataContextCategory private LazyMultimap _productCategories; private LazyMultimap _pictures; - public ExportDataContextCategory(IEnumerable categories, + public CategoryExportContext( + IEnumerable categories, Func> productCategories, Func> pictures) { @@ -43,6 +44,9 @@ public void Clear() _productCategories.Clear(); if (_pictures != null) _pictures.Clear(); + + _categoryIds.Clear(); + _pictureIds.Clear(); } public LazyMultimap ProductCategories diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportTask/ExportDataContextCustomer.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CustomerExportContext.cs similarity index 84% rename from src/Libraries/SmartStore.Services/DataExchange/ExportTask/ExportDataContextCustomer.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CustomerExportContext.cs index ac85860cc9..918cb3e110 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportTask/ExportDataContextCustomer.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CustomerExportContext.cs @@ -5,9 +5,9 @@ using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; -namespace SmartStore.Services.DataExchange.ExportTask +namespace SmartStore.Services.DataExchange.Export.Internal { - public class ExportDataContextCustomer + public class CustomerExportContext { protected List _customerIds; @@ -15,7 +15,8 @@ public class ExportDataContextCustomer private LazyMultimap _genericAttributes; - public ExportDataContextCustomer(IEnumerable customers, + public CustomerExportContext( + IEnumerable customers, Func> genericAttributes) { if (customers == null) @@ -34,6 +35,8 @@ public void Clear() { if (_genericAttributes != null) _genericAttributes.Clear(); + + _customerIds.Clear(); } public LazyMultimap GenericAttributes diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs new file mode 100644 index 0000000000..5c6c1315dd --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DataExporterContext.cs @@ -0,0 +1,127 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using SmartStore.Core; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Domain.Directory; +using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Logging; + +namespace SmartStore.Services.DataExchange.Export.Internal +{ + internal class DataExporterContext + { + public DataExporterContext( + DataExportRequest request, + CancellationToken cancellationToken, + bool isPreview = false) + { + Request = request; + CancellationToken = cancellationToken; + Filter = XmlHelper.Deserialize(request.Profile.Filtering); + Projection = XmlHelper.Deserialize(request.Profile.Projection); + IsPreview = isPreview; + + FolderContent = request.Profile.GetExportFolder(true, true); + + Categories = new Dictionary(); + CategoryPathes = new Dictionary(); + DeliveryTimes = new Dictionary(); + QuantityUnits = new Dictionary(); + Stores = new Dictionary(); + Languages = new Dictionary(); + Countries = new Dictionary(); + ProductTemplates = new Dictionary(); + CategoryTemplates = new Dictionary(); + NewsletterSubscriptions = new HashSet(); + + RecordsPerStore = new Dictionary(); + EntityIdsLoaded = new List(); + EntityIdsPerSegment = new List(); + + Result = new DataExportResult + { + FileFolder = (IsFileBasedExport ? FolderContent : null) + }; + + ExecuteContext = new ExportExecuteContext(Result, CancellationToken, FolderContent); + ExecuteContext.Projection = XmlHelper.Deserialize(request.Profile.Projection); + + if (!IsPreview) + { + ExecuteContext.ProgressValueSetter = Request.ProgressValueSetter; + } + } + + /// + /// All entity identifiers per export + /// + public List EntityIdsLoaded { get; set; } + public void SetLoadedEntityIds(IEnumerable ids) + { + EntityIdsLoaded = EntityIdsLoaded + .Union(ids) + .Distinct() + .ToList(); + } + + /// + /// All entity identifiers per segment (to avoid exporting products multiple times) + /// + public List EntityIdsPerSegment { get; set; } + + public int RecordCount { get; set; } + public Dictionary RecordsPerStore { get; set; } + public string ProgressInfo { get; set; } + + public DataExportRequest Request { get; private set; } + public CancellationToken CancellationToken { get; private set; } + public bool IsPreview { get; private set; } + + public bool Supports(ExportFeatures feature) + { + return (!IsPreview && Request.Provider.Metadata.ExportFeatures.HasFlag(feature)); + } + + public ExportFilter Filter { get; private set; } + public ExportProjection Projection { get; private set; } + public Currency ContextCurrency { get; set; } + public Customer ContextCustomer { get; set; } + public Language ContextLanguage { get; set; } + + public TraceLogger Log { get; set; } + public Store Store { get; set; } + + public string FolderContent { get; private set; } + + public bool IsFileBasedExport + { + get { return Request.Provider == null || Request.Provider.Value == null || Request.Provider.Value.FileExtension.HasValue(); } + } + + // data loaded once per export + public Dictionary Categories { get; set; } + public Dictionary CategoryPathes { get; set; } + public Dictionary DeliveryTimes { get; set; } + public Dictionary QuantityUnits { get; set; } + public Dictionary Stores { get; set; } + public Dictionary Languages { get; set; } + public Dictionary Countries { get; set; } + public Dictionary ProductTemplates { get; set; } + public Dictionary CategoryTemplates { get; set; } + public HashSet NewsletterSubscriptions { get; set; } + + // data loaded once per page + public ProductExportContext ProductExportContext { get; set; } + public OrderExportContext OrderExportContext { get; set; } + public ManufacturerExportContext ManufacturerExportContext { get; set; } + public CategoryExportContext CategoryExportContext { get; set; } + public CustomerExportContext CustomerExportContext { get; set; } + + public ExportExecuteContext ExecuteContext { get; set; } + public DataExportResult Result { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DynamicEntity.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DynamicEntity.cs new file mode 100644 index 0000000000..874d43e7e3 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/DynamicEntity.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using SmartStore.ComponentModel; + +namespace SmartStore.Services.DataExchange.Export.Internal +{ + internal class DynamicEntity : HybridExpando + { + public DynamicEntity(DynamicEntity dynamicEntity) + : this(dynamicEntity.WrappedObject) + { + MergeRange(dynamicEntity); + } + + public DynamicEntity(object entity) + : base(entity) + { + base.Properties["Entity"] = entity; + } + + public void Merge(string name, object value) + { + Properties[name] = value; + } + + public void MergeRange(IDictionary other) + { + foreach (var kvp in other) + { + Properties[kvp.Key] = kvp.Value; + } + } + + protected override bool TrySetMemberCore(string name, object value) + { + Properties[name] = value; + return true; + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportTask/ExportDataContextManufacturer.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ManufacturerExportContext.cs similarity index 88% rename from src/Libraries/SmartStore.Services/DataExchange/ExportTask/ExportDataContextManufacturer.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ManufacturerExportContext.cs index 79bd1558ca..82c607f5ad 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportTask/ExportDataContextManufacturer.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ManufacturerExportContext.cs @@ -5,9 +5,9 @@ using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Media; -namespace SmartStore.Services.DataExchange.ExportTask +namespace SmartStore.Services.DataExchange.Export.Internal { - internal class ExportDataContextManufacturer + internal class ManufacturerExportContext { protected List _manufacturerIds; protected List _pictureIds; @@ -18,7 +18,8 @@ internal class ExportDataContextManufacturer private LazyMultimap _productManufacturers; private LazyMultimap _pictures; - public ExportDataContextManufacturer(IEnumerable manufacturers, + public ManufacturerExportContext( + IEnumerable manufacturers, Func> productManufacturers, Func> pictures) { @@ -43,6 +44,9 @@ public void Clear() _productManufacturers.Clear(); if (_pictures != null) _pictures.Clear(); + + _manufacturerIds.Clear(); + _pictureIds.Clear(); } public LazyMultimap ProductManufacturers diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportTask/ExportDataContextOrder.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/OrderExportContext.cs similarity index 93% rename from src/Libraries/SmartStore.Services/DataExchange/ExportTask/ExportDataContextOrder.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Internal/OrderExportContext.cs index 38fa4893ae..9ebe5c25a5 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportTask/ExportDataContextOrder.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/OrderExportContext.cs @@ -7,9 +7,9 @@ using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Shipping; -namespace SmartStore.Services.DataExchange.ExportTask +namespace SmartStore.Services.DataExchange.Export.Internal { - internal class ExportDataContextOrder + internal class OrderExportContext { protected List _orderIds; protected List _customerIds; @@ -27,7 +27,7 @@ internal class ExportDataContextOrder private LazyMultimap _orderItems; private LazyMultimap _shipments; - public ExportDataContextOrder(IEnumerable orders, + public OrderExportContext(IEnumerable orders, Func> customers, Func> rewardPointsHistory, Func> addresses, @@ -71,6 +71,10 @@ public void Clear() _orderItems.Clear(); if (_shipments != null) _shipments.Clear(); + + _orderIds.Clear(); + _customerIds.Clear(); + _addressIds.Clear(); } public LazyMultimap Customers diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportTask/ExportDataContextProduct.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ProductExportContext.cs similarity index 95% rename from src/Libraries/SmartStore.Services/DataExchange/ExportTask/ExportDataContextProduct.cs rename to src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ProductExportContext.cs index 14f9168be4..6a76e1ffef 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportTask/ExportDataContextProduct.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ProductExportContext.cs @@ -6,9 +6,9 @@ using SmartStore.Core.Domain.Discounts; using SmartStore.Services.Catalog; -namespace SmartStore.Services.DataExchange.ExportTask +namespace SmartStore.Services.DataExchange.Export.Internal { - internal class ExportDataContextProduct : PriceCalculationContext + internal class ProductExportContext : PriceCalculationContext { private List _productIdsBundleItems; @@ -24,7 +24,8 @@ internal class ExportDataContextProduct : PriceCalculationContext private LazyMultimap _productSpecificationAttributes; private LazyMultimap _productBundleItems; - public ExportDataContextProduct(IEnumerable products, + public ProductExportContext( + IEnumerable products, Func> attributes, Func> attributeCombinations, Func> tierPrices, @@ -71,6 +72,8 @@ public ExportDataContextProduct(IEnumerable products, if (_productBundleItems != null) _productBundleItems.Clear(); + _productIdsBundleItems.Clear(); + base.Clear(); } diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportExtensions.cs b/src/Libraries/SmartStore.Services/DataExchange/ExportExtensions.cs deleted file mode 100644 index c7f6827bb0..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportExtensions.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Web; -using System.Web.Caching; -using SmartStore.Core.Domain; -using SmartStore.Core.Domain.DataExchange; -using SmartStore.Core.Domain.Stores; -using SmartStore.Core.Plugins; -using SmartStore.Services.Localization; -using SmartStore.Utilities; - -namespace SmartStore.Services.DataExchange -{ - public static class ExportExtensions - { - /// - /// Returns a value indicating whether the export provider is valid - /// - /// Export provider - /// true provider is valid, false provider is invalid. - public static bool IsValid(this Provider provider) - { - return (provider != null); - } - - /// - /// Returns a value indicating whether the export provider supports a projection type - /// - /// Export provider - /// The type to check - /// true provider supports type, false provider does not support type. - public static bool Supports(this Provider provider, ExportSupport type) - { - if (provider != null) - return provider.Metadata.ExportSupport.Contains(type); - return false; - } - - /// - /// Gets the localized friendly name or the system name as fallback - /// - /// Export provider - /// Localization service - /// Provider name - public static string GetName(this Provider provider, ILocalizationService localizationService) - { - var systemName = provider.Metadata.SystemName; - var resourceName = provider.Metadata.ResourceKeyPattern.FormatInvariant(systemName, "FriendlyName"); - var name = localizationService.GetResource(resourceName, 0, false, systemName, true); - - return (name.IsEmpty() ? systemName : name); - } - - /// - /// Get temporary folder for an export profile - /// - /// Export profile - /// Folder path - public static string GetExportFolder(this ExportProfile profile, bool content = false) - { - var path = Path.Combine(FileSystemHelper.TempDir(), @"Profile\Export\{0}{1}".FormatInvariant(profile.FolderName, content ? @"\Content" : "")); - return path; - } - - /// - /// Get log file path for an export profile - /// - /// Export profile - /// Log file path - public static string GetExportLogFilePath(this ExportProfile profile) - { - var path = Path.Combine(profile.GetExportFolder(), "log.txt"); - return path; - } - - /// - /// Resolves the file name pattern for an export profile - /// - /// Export profile - /// Store - /// File number - /// The maximum length of the file name - /// Resolved file name pattern - public static string ResolveFileNamePattern(this ExportProfile profile, Store store, int fileNumber, int maxFileNameLength) - { - var result = profile.FileNamePattern - .Replace("%ExportProfile.Id%", profile.Id.ToString()) - .Replace("%ExportProfile.SeoName%", SeoHelper.GetSeName(profile.Name, true, false).Replace("/", "").Replace("-", "")) - .Replace("%ExportProfile.FolderName%", profile.FolderName) - .Replace("%Store.Id%", store.Id.ToString()) - .Replace("%Store.SeoName%", profile.PerStore ? SeoHelper.GetSeName(store.Name, true, false) : "allstores") - .Replace("%Misc.FileNumber%", fileNumber.ToString("D4")) - .ToValidFileName("") - .Truncate(maxFileNameLength); - - return result; - } - - /// - /// Gets the cache key for selected entity identifiers - /// - /// Export profile - /// Cache key for selected entity identifiers - public static string GetSelectedEntityIdsCacheKey(this ExportProfile profile) - { - // do not use profile.Id because it can be 0 - return "ExportProfile_SelectedEntityIds_" + profile.ProviderSystemName; - } - - /// - /// Caches selected entity identifiers to be considered during following export - /// - /// Export profile - /// Comma separated entity identifiers - public static void CacheSelectedEntityIds(this ExportProfile profile, string selectedIds) - { - var selectedIdsCacheKey = profile.GetSelectedEntityIdsCacheKey(); - - if (selectedIds.HasValue()) - HttpRuntime.Cache.Add(selectedIdsCacheKey, selectedIds, null, DateTime.UtcNow.AddMinutes(5), Cache.NoSlidingExpiration, CacheItemPriority.Normal, null); - else - HttpRuntime.Cache.Remove(selectedIdsCacheKey); - } - } -} diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportCategoryXmlProvider.cs b/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportCategoryXmlProvider.cs deleted file mode 100644 index 7e4e6f9fba..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportCategoryXmlProvider.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Text; -using System.Xml; -using SmartStore.Core; -using SmartStore.Core.Domain.DataExchange; -using SmartStore.Core.Logging; -using SmartStore.Core.Plugins; - -namespace SmartStore.Services.DataExchange.ExportProvider -{ - /// - /// Exports XML formatted category data to a file - /// - [SystemName("Exports.SmartStoreCategoryXml")] - [FriendlyName("SmartStore XML category export")] - [IsHidden(true)] - public class ExportCategoryXmlProvider : IExportProvider - { - public static string SystemName - { - get { return "Exports.SmartStoreCategoryXml"; } - } - - public ExportConfigurationInfo ConfigurationInfo - { - get { return null; } - } - - public ExportEntityType EntityType - { - get { return ExportEntityType.Category; } - } - - public string FileExtension - { - get { return "XML"; } - } - - public void Execute(IExportExecuteContext context) - { - var settings = new XmlWriterSettings - { - Encoding = Encoding.UTF8, - CheckCharacters = false, - Indent = true, - IndentChars = "\t" - }; - - var path = context.FilePath; - - context.Log.Information("Creating file " + path); - - using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite)) - using (var writer = XmlWriter.Create(stream, settings)) - { - var xmlHelper = new ExportXmlHelper(writer, CultureInfo.InvariantCulture); - - writer.WriteStartDocument(); - writer.WriteStartElement("Categories"); - writer.WriteAttributeString("Version", SmartStoreVersion.CurrentVersion); - - while (context.Abort == ExportAbortion.None && context.Segmenter.ReadNextSegment()) - { - var segment = context.Segmenter.CurrentSegment; - - foreach (dynamic category in segment) - { - if (context.Abort != ExportAbortion.None) - break; - - writer.WriteStartElement("Category"); - - try - { - xmlHelper.WriteCategory(category, null); - - writer.WriteStartElement("ProductCategories"); - foreach (dynamic productCategory in category.ProductCategories) - { - writer.WriteStartElement("ProductCategory"); - writer.Write("Id", ((int)productCategory.Id).ToString()); - writer.Write("ProductId", ((int)productCategory.ProductId).ToString()); - writer.Write("DisplayOrder", ((int)productCategory.DisplayOrder).ToString()); - writer.Write("IsFeaturedProduct", ((bool)productCategory.IsFeaturedProduct).ToString()); - writer.Write("CategoryId", ((int)productCategory.CategoryId).ToString()); - writer.WriteEndElement(); // ProductCategory - } - writer.WriteEndElement(); // ProductCategories - - ++context.RecordsSucceeded; - } - catch (Exception exc) - { - context.RecordException(exc, (int)category.Id); - } - - writer.WriteEndElement(); // Category - } - } - - writer.WriteEndElement(); // Categories - writer.WriteEndDocument(); - } - } - - public void ExecuteEnded(IExportExecuteContext context) - { - // nothing to do - } - } -} diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportCustomerXlsxProvider.cs b/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportCustomerXlsxProvider.cs deleted file mode 100644 index c2f4722f1a..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportCustomerXlsxProvider.cs +++ /dev/null @@ -1,223 +0,0 @@ -using System; -using System.Drawing; -using System.Globalization; -using System.IO; -using OfficeOpenXml; -using OfficeOpenXml.Style; -using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.DataExchange; -using SmartStore.Core.Logging; -using SmartStore.Core.Plugins; -using SmartStore.Services.Customers; - -namespace SmartStore.Services.DataExchange.ExportProvider -{ - /// - /// Exports Excel formatted customer data to a file - /// - [SystemName("Exports.SmartStoreCustomerXlsx")] - [FriendlyName("SmartStore Excel customer export")] - [IsHidden(true)] - public class ExportCustomerXlsxProvider : IExportProvider - { - private string[] Properties - { - get - { - return new string[] - { - "Id", - "CustomerNumber", - "CustomerGuid", - "Email", - "Username", - "PasswordStr", - "PasswordFormatId", - "PasswordSalt", - "AdminComment", - "IsTaxExempt", - "AffiliateId", - "Active", - "IsSystemAccount", - "SystemName", - "LastIpAddress", - "CreatedOnUtc", - "LastLoginDateUtc", - "LastActivityDateUtc", - "IsGuest", - "IsRegistered", - "IsAdministrator", - "IsForumModerator", - "FirstName", - "LastName", - "Gender", - "Company", - "StreetAddress", - "StreetAddress2", - "ZipPostalCode", - "City", - "CountryId", - "StateProvinceId", - "Phone", - "Fax", - "VatNumber", - "VatNumberStatusId", - "TimeZoneId", - "Newsletter", - "AvatarPictureId", - "ForumPostCount", - "Signature" - }; - } - } - - private void WriteCell(ExcelWorksheet worksheet, int row, ref int column, object value) - { - worksheet.Cells[row, column].Value = value; - ++column; - } - - private string GetGenericAttributeValue(dynamic customer, string attributeName) - { - if (customer._GenericAttributes != null) - { - foreach (var attribute in customer._GenericAttributes) - { - if (attributeName.IsCaseInsensitiveEqual((string)attribute.Key)) - { - return (string)attribute.Value; - } - } - } - return null; - } - - public static string SystemName - { - get { return "Exports.SmartStoreCustomerXlsx"; } - } - - public ExportConfigurationInfo ConfigurationInfo - { - get { return null; } - } - - public ExportEntityType EntityType - { - get { return ExportEntityType.Customer; } - } - - public string FileExtension - { - get { return "XLSX"; } - } - - public void Execute(IExportExecuteContext context) - { - var invariantCulture = CultureInfo.InvariantCulture; - var path = context.FilePath; - - context.Log.Information("Creating file " + path); - - using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite)) - using (var xlPackage = new ExcelPackage(stream)) - { - // uncomment this line if you want the XML written out to the outputDir - //xlPackage.DebugMode = true; - - // get handle to the existing worksheet - var worksheet = xlPackage.Workbook.Worksheets.Add("Customers"); - - // create headers and format them - string[] properties = Properties; - - for (int i = 0; i < properties.Length; ++i) - { - worksheet.Cells[1, i + 1].Value = properties[i]; - worksheet.Cells[1, i + 1].Style.Fill.PatternType = ExcelFillStyle.Solid; - worksheet.Cells[1, i + 1].Style.Fill.BackgroundColor.SetColor(Color.FromArgb(184, 204, 228)); - worksheet.Cells[1, i + 1].Style.Font.Bold = true; - } - - int row = 2; - - while (context.Abort == ExportAbortion.None && context.Segmenter.ReadNextSegment()) - { - var segment = context.Segmenter.CurrentSegment; - - foreach (dynamic customer in segment) - { - if (context.Abort != ExportAbortion.None) - break; - - try - { - int column = 1; - DateTime? lastLoginDateUtc = customer.LastLoginDateUtc; - Customer entity = customer._Entity; - - WriteCell(worksheet, row, ref column, (int)customer.Id); - WriteCell(worksheet, row, ref column, GetGenericAttributeValue(customer, SystemCustomerAttributeNames.CustomerNumber)); - WriteCell(worksheet, row, ref column, (Guid)customer.CustomerGuid); - WriteCell(worksheet, row, ref column, (string)customer.Email); - WriteCell(worksheet, row, ref column, (string)customer.Username); - WriteCell(worksheet, row, ref column, (string)customer.Password); - WriteCell(worksheet, row, ref column, (int)customer.PasswordFormatId); - WriteCell(worksheet, row, ref column, (string)customer.PasswordSalt); - WriteCell(worksheet, row, ref column, (string)customer.AdminComment); - WriteCell(worksheet, row, ref column, (bool)customer.IsTaxExempt); - WriteCell(worksheet, row, ref column, (int)customer.AffiliateId); - WriteCell(worksheet, row, ref column, (bool)customer.Active); - WriteCell(worksheet, row, ref column, (bool)customer.IsSystemAccount); - WriteCell(worksheet, row, ref column, (string)customer.SystemName); - WriteCell(worksheet, row, ref column, (string)customer.LastIpAddress); - WriteCell(worksheet, row, ref column, ((DateTime)customer.CreatedOnUtc).ToString(invariantCulture)); - WriteCell(worksheet, row, ref column, lastLoginDateUtc.HasValue ? lastLoginDateUtc.Value.ToString(invariantCulture) : null); - WriteCell(worksheet, row, ref column, ((DateTime)customer.LastActivityDateUtc).ToString(invariantCulture)); - WriteCell(worksheet, row, ref column, entity.IsGuest()); - WriteCell(worksheet, row, ref column, entity.IsRegistered()); - WriteCell(worksheet, row, ref column, entity.IsAdmin()); - WriteCell(worksheet, row, ref column, entity.IsForumModerator()); - - WriteCell(worksheet, row, ref column, GetGenericAttributeValue(customer, SystemCustomerAttributeNames.FirstName)); - WriteCell(worksheet, row, ref column, GetGenericAttributeValue(customer, SystemCustomerAttributeNames.LastName)); - WriteCell(worksheet, row, ref column, GetGenericAttributeValue(customer, SystemCustomerAttributeNames.Gender)); - WriteCell(worksheet, row, ref column, GetGenericAttributeValue(customer, SystemCustomerAttributeNames.Company)); - WriteCell(worksheet, row, ref column, GetGenericAttributeValue(customer, SystemCustomerAttributeNames.StreetAddress)); - WriteCell(worksheet, row, ref column, GetGenericAttributeValue(customer, SystemCustomerAttributeNames.StreetAddress2)); - WriteCell(worksheet, row, ref column, GetGenericAttributeValue(customer, SystemCustomerAttributeNames.ZipPostalCode)); - WriteCell(worksheet, row, ref column, GetGenericAttributeValue(customer, SystemCustomerAttributeNames.City)); - WriteCell(worksheet, row, ref column, GetGenericAttributeValue(customer, SystemCustomerAttributeNames.CountryId)); - WriteCell(worksheet, row, ref column, GetGenericAttributeValue(customer, SystemCustomerAttributeNames.StateProvinceId)); - WriteCell(worksheet, row, ref column, GetGenericAttributeValue(customer, SystemCustomerAttributeNames.Phone)); - WriteCell(worksheet, row, ref column, GetGenericAttributeValue(customer, SystemCustomerAttributeNames.Fax)); - WriteCell(worksheet, row, ref column, GetGenericAttributeValue(customer, SystemCustomerAttributeNames.VatNumber)); - WriteCell(worksheet, row, ref column, GetGenericAttributeValue(customer, SystemCustomerAttributeNames.VatNumberStatusId)); - WriteCell(worksheet, row, ref column, GetGenericAttributeValue(customer, SystemCustomerAttributeNames.TimeZoneId)); - WriteCell(worksheet, row, ref column, (bool)customer._HasNewsletterSubscription); - WriteCell(worksheet, row, ref column, GetGenericAttributeValue(customer, SystemCustomerAttributeNames.AvatarPictureId)); - WriteCell(worksheet, row, ref column, GetGenericAttributeValue(customer, SystemCustomerAttributeNames.ForumPostCount)); - WriteCell(worksheet, row, ref column, GetGenericAttributeValue(customer, SystemCustomerAttributeNames.Signature)); - - ++context.RecordsSucceeded; - } - catch (Exception exc) - { - context.RecordException(exc, (int)customer.Id); - } - - ++row; - } - } - - // save the new spreadsheet - xlPackage.Save(); - } - } - - public void ExecuteEnded(IExportExecuteContext context) - { - // nothing to do - } - } -} diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportCustomerXmlProvider.cs b/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportCustomerXmlProvider.cs deleted file mode 100644 index cbf1c80719..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportCustomerXmlProvider.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Text; -using System.Xml; -using SmartStore.Core; -using SmartStore.Core.Domain.DataExchange; -using SmartStore.Core.Logging; -using SmartStore.Core.Plugins; - -namespace SmartStore.Services.DataExchange.ExportProvider -{ - /// - /// Exports XML formatted customer data to a file - /// - [SystemName("Exports.SmartStoreCustomerXml")] - [FriendlyName("SmartStore XML customer export")] - [IsHidden(true)] - public class ExportCustomerXmlProvider : IExportProvider - { - public static string SystemName - { - get { return "Exports.SmartStoreCustomerXml"; } - } - - public ExportConfigurationInfo ConfigurationInfo - { - get { return null; } - } - - public ExportEntityType EntityType - { - get { return ExportEntityType.Customer; } - } - - public string FileExtension - { - get { return "XML"; } - } - - public void Execute(IExportExecuteContext context) - { - var settings = new XmlWriterSettings - { - Encoding = Encoding.UTF8, - CheckCharacters = false, - Indent = true, - IndentChars = "\t" - }; - - var path = context.FilePath; - - context.Log.Information("Creating file " + path); - - using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite)) - using (var writer = XmlWriter.Create(stream, settings)) - { - var xmlHelper = new ExportXmlHelper(writer, CultureInfo.InvariantCulture); - - writer.WriteStartDocument(); - writer.WriteStartElement("Customers"); - writer.WriteAttributeString("Version", SmartStoreVersion.CurrentVersion); - - while (context.Abort == ExportAbortion.None && context.Segmenter.ReadNextSegment()) - { - var segment = context.Segmenter.CurrentSegment; - - foreach (dynamic customer in segment) - { - if (context.Abort != ExportAbortion.None) - break; - - try - { - xmlHelper.WriteCustomer(customer, "Customer"); - - ++context.RecordsSucceeded; - } - catch (Exception exc) - { - context.RecordException(exc, (int)customer.Id); - } - } - } - - writer.WriteEndElement(); // Customers - writer.WriteEndDocument(); - } - } - - public void ExecuteEnded(IExportExecuteContext context) - { - // nothing to do - } - } -} diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportManufacturerXmlProvider.cs b/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportManufacturerXmlProvider.cs deleted file mode 100644 index ebe52eb5f9..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportManufacturerXmlProvider.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Text; -using System.Xml; -using SmartStore.Core; -using SmartStore.Core.Domain.DataExchange; -using SmartStore.Core.Logging; -using SmartStore.Core.Plugins; - -namespace SmartStore.Services.DataExchange.ExportProvider -{ - /// - /// Exports XML formatted manufacturer data to a file - /// - [SystemName("Exports.SmartStoreManufacturerXml")] - [FriendlyName("SmartStore XML manufacturer export")] - [IsHidden(true)] - public class ExportManufacturerXmlProvider : IExportProvider - { - public static string SystemName - { - get { return "Exports.SmartStoreManufacturerXml"; } - } - - public ExportConfigurationInfo ConfigurationInfo - { - get { return null; } - } - - public ExportEntityType EntityType - { - get { return ExportEntityType.Manufacturer; } - } - - public string FileExtension - { - get { return "XML"; } - } - - public void Execute(IExportExecuteContext context) - { - var settings = new XmlWriterSettings - { - Encoding = Encoding.UTF8, - CheckCharacters = false, - Indent = true, - IndentChars = "\t" - }; - - var path = context.FilePath; - - context.Log.Information("Creating file " + path); - - using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite)) - using (var writer = XmlWriter.Create(stream, settings)) - { - var xmlHelper = new ExportXmlHelper(writer, CultureInfo.InvariantCulture); - - writer.WriteStartDocument(); - writer.WriteStartElement("Manufacturers"); - writer.WriteAttributeString("Version", SmartStoreVersion.CurrentVersion); - - while (context.Abort == ExportAbortion.None && context.Segmenter.ReadNextSegment()) - { - var segment = context.Segmenter.CurrentSegment; - - foreach (dynamic manufacturer in segment) - { - if (context.Abort != ExportAbortion.None) - break; - - writer.WriteStartElement("Manufacturer"); - - try - { - xmlHelper.WriteManufacturer(manufacturer, null); - - writer.WriteStartElement("ProductManufacturers"); - foreach (dynamic productManu in manufacturer.ProductManufacturers) - { - writer.WriteStartElement("ProductManufacturer"); - writer.Write("Id", ((int)productManu.Id).ToString()); - writer.Write("ProductId", ((int)productManu.ProductId).ToString()); - writer.Write("DisplayOrder", ((int)productManu.DisplayOrder).ToString()); - writer.Write("IsFeaturedProduct", ((bool)productManu.IsFeaturedProduct).ToString()); - writer.Write("ManufacturerId", ((int)productManu.ManufacturerId).ToString()); - writer.WriteEndElement(); // ProductManufacturer - } - writer.WriteEndElement(); // ProductManufacturers - - ++context.RecordsSucceeded; - } - catch (Exception exc) - { - context.RecordException(exc, (int)manufacturer.Id); - } - - writer.WriteEndElement(); // Manufacturer - } - } - - writer.WriteEndElement(); // Manufacturers - writer.WriteEndDocument(); - } - } - - public void ExecuteEnded(IExportExecuteContext context) - { - // nothing to do - } - } -} diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportNewsSubscriptionCsvProvider.cs b/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportNewsSubscriptionCsvProvider.cs deleted file mode 100644 index 00400d64e9..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportNewsSubscriptionCsvProvider.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.IO; -using System.Text; -using SmartStore.Core.Domain.DataExchange; -using SmartStore.Core.Logging; -using SmartStore.Core.Plugins; - -namespace SmartStore.Services.DataExchange.ExportProvider -{ - /// - /// Exports CSV formatted newsletter subscription data to a file - /// - [SystemName("Exports.SmartStoreNewsSubscriptionCsv")] - [FriendlyName("SmartStore CSV newsletter subscription export")] - [IsHidden(true)] - public class ExportNewsSubscriptionCsvProvider : IExportProvider - { - public static string SystemName - { - get { return "Exports.SmartStoreNewsSubscriptionCsv"; } - } - - public ExportConfigurationInfo ConfigurationInfo - { - get { return null; } - } - - public ExportEntityType EntityType - { - get { return ExportEntityType.NewsLetterSubscription; } - } - - public string FileExtension - { - get { return "CSV"; } - } - - public void Execute(IExportExecuteContext context) - { - var path = context.FilePath; - - context.Log.Information("Creating file " + path); - - using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite)) - using (var writer = new StreamWriter(stream, Encoding.UTF8)) - { - while (context.Abort == ExportAbortion.None && context.Segmenter.ReadNextSegment()) - { - var segment = context.Segmenter.CurrentSegment; - - foreach (dynamic subscriber in segment) - { - if (context.Abort != ExportAbortion.None) - break; - - try - { - var row = "{0},{1},{2}".FormatInvariant( - ((string)subscriber.Email).ReplaceCsvChars(), - ((bool)subscriber.Active).ToString(), - (int)subscriber.StoreId - ); - - writer.WriteLine(row); - - ++context.RecordsSucceeded; - } - catch (Exception exc) - { - context.RecordException(exc, (int)subscriber.Id); - } - } - } - } - } - - public void ExecuteEnded(IExportExecuteContext context) - { - // nothing to do - } - } -} diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportOrderXlsxProvider.cs b/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportOrderXlsxProvider.cs deleted file mode 100644 index 4cd8a5c5e2..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportOrderXlsxProvider.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System; -using System.Drawing; -using System.IO; -using OfficeOpenXml; -using OfficeOpenXml.Style; -using SmartStore.Core.Domain.DataExchange; -using SmartStore.Core.Logging; -using SmartStore.Core.Plugins; - -namespace SmartStore.Services.DataExchange.ExportProvider -{ - /// - /// Exports Excel formatted order data to a file - /// - [SystemName("Exports.SmartStoreOrderXlsx")] - [FriendlyName("SmartStore Excel order export")] - [IsHidden(true)] - public class ExportOrderXlsxProvider : IExportProvider - { - private string[] Properties - { - get - { - return new string[] - { - "OrderId", - "OrderGuid", - "CustomerId", - "OrderSubtotalInclTax", - "OrderSubtotalExclTax", - "OrderSubTotalDiscountInclTax", - "OrderSubTotalDiscountExclTax", - "OrderShippingInclTax", - "OrderShippingExclTax", - "PaymentMethodAdditionalFeeInclTax", - "PaymentMethodAdditionalFeeExclTax", - "TaxRates", - "OrderTax", - "OrderTotal", - "RefundedAmount", - "OrderDiscount", - "CurrencyRate", - "CustomerCurrencyCode", - "AffiliateId", - "OrderStatusId", - "PaymentMethodSystemName", - "PurchaseOrderNumber", - "PaymentStatusId", - "ShippingStatusId", - "ShippingMethod", - "ShippingRateComputationMethodSystemName", - "VatNumber", - "CreatedOnUtc", - "UpdatedOnUtc", - "RewardPointsUsed", - "RewardPointsRemaining", - "HasNewPaymentNotification", - //billing address - "BillingFirstName", - "BillingLastName", - "BillingEmail", - "BillingCompany", - "BillingCountry", - "BillingStateProvince", - "BillingCity", - "BillingAddress1", - "BillingAddress2", - "BillingZipPostalCode", - "BillingPhoneNumber", - "BillingFaxNumber", - //shipping address - "ShippingFirstName", - "ShippingLastName", - "ShippingEmail", - "ShippingCompany", - "ShippingCountry", - "ShippingStateProvince", - "ShippingCity", - "ShippingAddress1", - "ShippingAddress2", - "ShippingZipPostalCode", - "ShippingPhoneNumber", - "ShippingFaxNumber" - }; - } - } - - private void WriteCell(ExcelWorksheet worksheet, int row, ref int column, object value) - { - worksheet.Cells[row, column].Value = value; - ++column; - } - - public static string SystemName - { - get { return "Exports.SmartStoreOrderXlsx"; } - } - - public ExportConfigurationInfo ConfigurationInfo - { - get { return null; } - } - - public ExportEntityType EntityType - { - get { return ExportEntityType.Order; } - } - - public string FileExtension - { - get { return "XLSX"; } - } - - public void Execute(IExportExecuteContext context) - { - var path = context.FilePath; - - context.Log.Information("Creating file " + path); - - using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite)) - using (var xlPackage = new ExcelPackage(stream)) - { - // uncomment this line if you want the XML written out to the outputDir - //xlPackage.DebugMode = true; - - // get handle to the existing worksheet - var worksheet = xlPackage.Workbook.Worksheets.Add("Orders"); - - // create headers and format them - string[] properties = Properties; - - for (int i = 0; i < properties.Length; ++i) - { - worksheet.Cells[1, i + 1].Value = properties[i]; - worksheet.Cells[1, i + 1].Style.Fill.PatternType = ExcelFillStyle.Solid; - worksheet.Cells[1, i + 1].Style.Fill.BackgroundColor.SetColor(Color.FromArgb(184, 204, 228)); - worksheet.Cells[1, i + 1].Style.Font.Bold = true; - } - - int row = 2; - - while (context.Abort == ExportAbortion.None && context.Segmenter.ReadNextSegment()) - { - var segment = context.Segmenter.CurrentSegment; - - foreach (dynamic order in segment) - { - if (context.Abort != ExportAbortion.None) - break; - - try - { - int column = 1; - dynamic customer = order.Customer; - dynamic billingAddress = order.BillingAddress; - dynamic shippingAddress = order.ShippingAddress; - - int rewardPointsUsed = 0; - int rewardPointsRemaining = customer._RewardPointsBalance; - - if (order.RedeemedRewardPointsEntry != null && (int)order.RedeemedRewardPointsEntry.Points != 0) - rewardPointsUsed = (-1 * (int)order.RedeemedRewardPointsEntry.Points); - - WriteCell(worksheet, row, ref column, (string)order.OrderNumber); - WriteCell(worksheet, row, ref column, (Guid)order.OrderGuid); - WriteCell(worksheet, row, ref column, (int)order.CustomerId); - WriteCell(worksheet, row, ref column, (decimal)order.OrderSubtotalInclTax); - WriteCell(worksheet, row, ref column, (decimal)order.OrderSubtotalExclTax); - WriteCell(worksheet, row, ref column, (decimal)order.OrderSubTotalDiscountInclTax); - WriteCell(worksheet, row, ref column, (decimal)order.OrderSubTotalDiscountExclTax); - WriteCell(worksheet, row, ref column, (decimal)order.OrderShippingInclTax); - WriteCell(worksheet, row, ref column, (decimal)order.OrderShippingExclTax); - WriteCell(worksheet, row, ref column, (decimal)order.PaymentMethodAdditionalFeeInclTax); - WriteCell(worksheet, row, ref column, (decimal)order.PaymentMethodAdditionalFeeExclTax); - WriteCell(worksheet, row, ref column, (string)order.TaxRates); - WriteCell(worksheet, row, ref column, (decimal)order.OrderTax); - WriteCell(worksheet, row, ref column, (decimal)order.OrderTotal); - WriteCell(worksheet, row, ref column, (decimal)order.RefundedAmount); - WriteCell(worksheet, row, ref column, (decimal)order.OrderDiscount); - WriteCell(worksheet, row, ref column, (decimal)order.CurrencyRate); - WriteCell(worksheet, row, ref column, (string)order.CustomerCurrencyCode); - WriteCell(worksheet, row, ref column, (int)order.AffiliateId); - WriteCell(worksheet, row, ref column, (int)order.OrderStatusId); - WriteCell(worksheet, row, ref column, (string)order.PaymentMethodSystemName); - WriteCell(worksheet, row, ref column, (string)order.PurchaseOrderNumber); - WriteCell(worksheet, row, ref column, (int)order.PaymentStatusId); - WriteCell(worksheet, row, ref column, (int)order.ShippingStatusId); - WriteCell(worksheet, row, ref column, (string)order.ShippingMethod); - WriteCell(worksheet, row, ref column, (string)order.ShippingRateComputationMethodSystemName); - WriteCell(worksheet, row, ref column, (string)order.VatNumber); - WriteCell(worksheet, row, ref column, ((DateTime)order.CreatedOnUtc).ToOADate()); - WriteCell(worksheet, row, ref column, ((DateTime)order.UpdatedOnUtc).ToOADate()); - WriteCell(worksheet, row, ref column, rewardPointsUsed > 0 ? rewardPointsUsed.ToString() : ""); - WriteCell(worksheet, row, ref column, rewardPointsRemaining > 0 ? rewardPointsRemaining.ToString() : ""); - WriteCell(worksheet, row, ref column, (bool)order.HasNewPaymentNotification); - - WriteCell(worksheet, row, ref column, billingAddress != null ? (string)billingAddress.FirstName : ""); - WriteCell(worksheet, row, ref column, billingAddress != null ? (string)billingAddress.LastName : ""); - WriteCell(worksheet, row, ref column, billingAddress != null ? (string)billingAddress.Email : ""); - WriteCell(worksheet, row, ref column, billingAddress != null ? (string)billingAddress.Company : ""); - WriteCell(worksheet, row, ref column, billingAddress != null && billingAddress.Country != null ? (string)billingAddress.Country.Name : ""); - WriteCell(worksheet, row, ref column, billingAddress != null && billingAddress.StateProvince != null ? (string)billingAddress.StateProvince.Name : ""); - WriteCell(worksheet, row, ref column, billingAddress != null ? (string)billingAddress.City : ""); - WriteCell(worksheet, row, ref column, billingAddress != null ? (string)billingAddress.Address1 : ""); - WriteCell(worksheet, row, ref column, billingAddress != null ? (string)billingAddress.Address2 : ""); - WriteCell(worksheet, row, ref column, billingAddress != null ? (string)billingAddress.ZipPostalCode : ""); - WriteCell(worksheet, row, ref column, billingAddress != null ? (string)billingAddress.PhoneNumber : ""); - WriteCell(worksheet, row, ref column, billingAddress != null ? (string)billingAddress.FaxNumber : ""); - - WriteCell(worksheet, row, ref column, shippingAddress != null ? (string)shippingAddress.FirstName : ""); - WriteCell(worksheet, row, ref column, shippingAddress != null ? (string)shippingAddress.LastName : ""); - WriteCell(worksheet, row, ref column, shippingAddress != null ? (string)shippingAddress.Email : ""); - WriteCell(worksheet, row, ref column, shippingAddress != null ? (string)shippingAddress.Company : ""); - WriteCell(worksheet, row, ref column, shippingAddress != null && shippingAddress.Country != null ? (string)shippingAddress.Country.Name : ""); - WriteCell(worksheet, row, ref column, shippingAddress != null && shippingAddress.StateProvince != null ? (string)shippingAddress.StateProvince.Name : ""); - WriteCell(worksheet, row, ref column, shippingAddress != null ? (string)shippingAddress.City : ""); - WriteCell(worksheet, row, ref column, shippingAddress != null ? (string)shippingAddress.Address1 : ""); - WriteCell(worksheet, row, ref column, shippingAddress != null ? (string)shippingAddress.Address2 : ""); - WriteCell(worksheet, row, ref column, shippingAddress != null ? (string)shippingAddress.ZipPostalCode : ""); - WriteCell(worksheet, row, ref column, shippingAddress != null ? (string)shippingAddress.PhoneNumber : ""); - WriteCell(worksheet, row, ref column, shippingAddress != null ? (string)shippingAddress.FaxNumber : ""); - - ++context.RecordsSucceeded; - } - catch (Exception exc) - { - context.RecordException(exc, (int)order.Id); - } - - ++row; - } - } - - // we had better add some document properties to the spreadsheet - // set some core property values - //var storeName = _storeInformationSettings.StoreName; - //var storeUrl = _storeInformationSettings.StoreUrl; - //xlPackage.Workbook.Properties.Title = string.Format("{0} orders", storeName); - //xlPackage.Workbook.Properties.Author = storeName; - //xlPackage.Workbook.Properties.Subject = string.Format("{0} orders", storeName); - //xlPackage.Workbook.Properties.Keywords = string.Format("{0} orders", storeName); - //xlPackage.Workbook.Properties.Category = "Orders"; - //xlPackage.Workbook.Properties.Comments = string.Format("{0} orders", storeName); - - // set some extended property values - //xlPackage.Workbook.Properties.Company = storeName; - //xlPackage.Workbook.Properties.HyperlinkBase = new Uri(storeUrl); - - // save the new spreadsheet - xlPackage.Save(); - } - } - - public void ExecuteEnded(IExportExecuteContext context) - { - // nothing to do - } - } -} diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportOrderXmlProvider.cs b/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportOrderXmlProvider.cs deleted file mode 100644 index e04e6829bf..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportOrderXmlProvider.cs +++ /dev/null @@ -1,269 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Text; -using System.Xml; -using SmartStore.Core; -using SmartStore.Core.Domain.DataExchange; -using SmartStore.Core.Logging; -using SmartStore.Core.Plugins; - -namespace SmartStore.Services.DataExchange.ExportProvider -{ - /// - /// Exports XML formatted order data to a file - /// - [SystemName("Exports.SmartStoreOrderXml")] - [FriendlyName("SmartStore XML order export")] - [IsHidden(true)] - public class ExportOrderXmlProvider : IExportProvider - { - public static string SystemName - { - get { return "Exports.SmartStoreOrderXml"; } - } - - public ExportConfigurationInfo ConfigurationInfo - { - get { return null; } - } - - public ExportEntityType EntityType - { - get { return ExportEntityType.Order; } - } - - public string FileExtension - { - get { return "XML"; } - } - - public void Execute(IExportExecuteContext context) - { - var settings = new XmlWriterSettings - { - Encoding = Encoding.UTF8, - CheckCharacters = false, - Indent = true, - IndentChars = "\t" - }; - - var path = context.FilePath; - var invariantCulture = CultureInfo.InvariantCulture; - - context.Log.Information("Creating file " + path); - - using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite)) - using (var writer = XmlWriter.Create(stream, settings)) - { - var xmlHelper = new ExportXmlHelper(writer, invariantCulture); - - writer.WriteStartDocument(); - writer.WriteStartElement("Orders"); - writer.WriteAttributeString("Version", SmartStoreVersion.CurrentVersion); - - while (context.Abort == ExportAbortion.None && context.Segmenter.ReadNextSegment()) - { - var segment = context.Segmenter.CurrentSegment; - - foreach (dynamic order in segment) - { - if (context.Abort != ExportAbortion.None) - break; - - writer.WriteStartElement("Order"); - - try - { - dynamic store = order.Store; - int? shippingAddressId = order.ShippingAddressId; - DateTime? paidDateUtc = order.PaidDateUtc; - int? rewardPointsRemaining = order.RewardPointsRemaining; - - writer.Write("Id", ((int)order.Id).ToString()); - writer.Write("OrderNumber", (string)order.OrderNumber); - writer.Write("OrderGuid", ((Guid)order.OrderGuid).ToString()); - writer.Write("StoreId", ((int)order.StoreId).ToString()); - writer.Write("CustomerId", ((int)order.CustomerId).ToString()); - writer.Write("BillingAddressId", ((int)order.BillingAddressId).ToString()); - writer.Write("ShippingAddressId", shippingAddressId.HasValue ? shippingAddressId.Value.ToString() : ""); - writer.Write("OrderStatusId", ((int)order.OrderStatusId).ToString()); - writer.Write("ShippingStatusId", ((int)order.ShippingStatusId).ToString()); - writer.Write("PaymentStatusId", ((int)order.PaymentStatusId).ToString()); - writer.Write("PaymentMethodSystemName", (string)order.PaymentMethodSystemName); - writer.Write("CustomerCurrencyCode", (string)order.CustomerCurrencyCode); - writer.Write("CurrencyRate", ((decimal)order.CurrencyRate).ToString(invariantCulture)); - writer.Write("CustomerTaxDisplayTypeId", ((int)order.CustomerTaxDisplayTypeId).ToString()); - writer.Write("VatNumber", (string)order.VatNumber); - writer.Write("OrderSubtotalInclTax", ((decimal)order.OrderSubtotalInclTax).ToString(invariantCulture)); - writer.Write("OrderSubtotalExclTax", ((decimal)order.OrderSubtotalExclTax).ToString(invariantCulture)); - writer.Write("OrderSubTotalDiscountInclTax", ((decimal)order.OrderSubTotalDiscountInclTax).ToString(invariantCulture)); - writer.Write("OrderSubTotalDiscountExclTax", ((decimal)order.OrderSubTotalDiscountExclTax).ToString(invariantCulture)); - writer.Write("OrderShippingInclTax", ((decimal)order.OrderShippingInclTax).ToString(invariantCulture)); - writer.Write("OrderShippingExclTax", ((decimal)order.OrderShippingExclTax).ToString(invariantCulture)); - writer.Write("OrderShippingTaxRate", ((decimal)order.OrderShippingTaxRate).ToString(invariantCulture)); - writer.Write("PaymentMethodAdditionalFeeInclTax", ((decimal)order.PaymentMethodAdditionalFeeInclTax).ToString(invariantCulture)); - writer.Write("PaymentMethodAdditionalFeeExclTax", ((decimal)order.PaymentMethodAdditionalFeeExclTax).ToString(invariantCulture)); - writer.Write("PaymentMethodAdditionalFeeTaxRate", ((decimal)order.PaymentMethodAdditionalFeeTaxRate).ToString(invariantCulture)); - writer.Write("TaxRates", (string)order.TaxRates); - writer.Write("OrderTax", ((decimal)order.OrderTax).ToString(invariantCulture)); - writer.Write("OrderDiscount", ((decimal)order.OrderDiscount).ToString(invariantCulture)); - writer.Write("OrderTotal", ((decimal)order.OrderTotal).ToString(invariantCulture)); - writer.Write("RefundedAmount", ((decimal)order.RefundedAmount).ToString(invariantCulture)); - writer.Write("RewardPointsWereAdded", ((bool)order.RewardPointsWereAdded).ToString()); - writer.Write("CheckoutAttributeDescription", (string)order.CheckoutAttributeDescription); - writer.Write("CheckoutAttributesXml", (string)order.CheckoutAttributesXml); - writer.Write("CustomerLanguageId", ((int)order.CustomerLanguageId).ToString()); - writer.Write("AffiliateId", ((int)order.AffiliateId).ToString()); - writer.Write("CustomerIp", (string)order.CustomerIp); - writer.Write("AllowStoringCreditCardNumber", ((bool)order.AllowStoringCreditCardNumber).ToString()); - writer.Write("CardType", (string)order.CardType); - writer.Write("CardName", (string)order.CardName); - writer.Write("CardNumber", (string)order.CardNumber); - writer.Write("MaskedCreditCardNumber", (string)order.MaskedCreditCardNumber); - writer.Write("CardCvv2", (string)order.CardCvv2); - writer.Write("CardExpirationMonth", (string)order.CardExpirationMonth); - writer.Write("CardExpirationYear", (string)order.CardExpirationYear); - writer.Write("AllowStoringDirectDebit", ((bool)order.AllowStoringDirectDebit).ToString()); - writer.Write("DirectDebitAccountHolder", (string)order.DirectDebitAccountHolder); - writer.Write("DirectDebitAccountNumber", (string)order.DirectDebitAccountNumber); - writer.Write("DirectDebitBankCode", (string)order.DirectDebitBankCode); - writer.Write("DirectDebitBankName", (string)order.DirectDebitBankName); - writer.Write("DirectDebitBIC", (string)order.DirectDebitBIC); - writer.Write("DirectDebitCountry", (string)order.DirectDebitCountry); - writer.Write("DirectDebitIban", (string)order.DirectDebitIban); - writer.Write("CustomerOrderComment", (string)order.CustomerOrderComment); - writer.Write("AuthorizationTransactionId", (string)order.AuthorizationTransactionId); - writer.Write("AuthorizationTransactionCode", (string)order.AuthorizationTransactionCode); - writer.Write("AuthorizationTransactionResult", (string)order.AuthorizationTransactionResult); - writer.Write("CaptureTransactionId", (string)order.CaptureTransactionId); - writer.Write("CaptureTransactionResult", (string)order.CaptureTransactionResult); - writer.Write("SubscriptionTransactionId", (string)order.SubscriptionTransactionId); - writer.Write("PurchaseOrderNumber", (string)order.PurchaseOrderNumber); - writer.Write("PaidDateUtc", paidDateUtc.HasValue ? paidDateUtc.Value.ToString(invariantCulture) : ""); - writer.Write("ShippingMethod", (string)order.ShippingMethod); - writer.Write("ShippingRateComputationMethodSystemName", (string)order.ShippingRateComputationMethodSystemName); - writer.Write("Deleted", ((bool)order.Deleted).ToString()); - writer.Write("CreatedOnUtc", ((DateTime)order.CreatedOnUtc).ToString(invariantCulture)); - writer.Write("UpdatedOnUtc", ((DateTime)order.UpdatedOnUtc).ToString(invariantCulture)); - writer.Write("RewardPointsRemaining", rewardPointsRemaining.HasValue ? rewardPointsRemaining.Value.ToString() : ""); - writer.Write("HasNewPaymentNotification", ((bool)order.HasNewPaymentNotification).ToString()); - writer.Write("OrderStatus", (string)order.OrderStatus); - writer.Write("PaymentStatus", (string)order.PaymentStatus); - writer.Write("ShippingStatus", (string)order.ShippingStatus); - - xmlHelper.WriteCustomer(order.Customer, "Customer"); - - xmlHelper.WriteAddress(order.BillingAddress, "BillingAddress"); - xmlHelper.WriteAddress(order.ShippingAddress, "ShippingAddress"); - - if (store != null) - { - writer.WriteStartElement("Store"); - writer.Write("Id", ((int)store.Id).ToString()); - writer.Write("Name", (string)store.Name); - writer.Write("Url", (string)store.Url); - writer.Write("SslEnabled", ((bool)store.SslEnabled).ToString()); - writer.Write("SecureUrl", (string)store.SecureUrl); - writer.Write("Hosts", (string)store.Hosts); - writer.Write("LogoPictureId", ((int)store.LogoPictureId).ToString()); - writer.Write("DisplayOrder", ((int)store.DisplayOrder).ToString()); - writer.Write("HtmlBodyId", (string)store.HtmlBodyId); - writer.Write("ContentDeliveryNetwork", (string)store.ContentDeliveryNetwork); - writer.Write("PrimaryStoreCurrencyId", ((int)store.PrimaryStoreCurrencyId).ToString()); - writer.Write("PrimaryExchangeRateCurrencyId", ((int)store.PrimaryExchangeRateCurrencyId).ToString()); - - xmlHelper.WriteCurrency(store.PrimaryStoreCurrency, "PrimaryStoreCurrency"); - xmlHelper.WriteCurrency(store.PrimaryExchangeRateCurrency, "PrimaryExchangeRateCurrency"); - - writer.WriteEndElement(); // Store - } - - writer.WriteStartElement("OrderItems"); - foreach (dynamic orderItem in order.OrderItems) - { - int? licenseDownloadId = orderItem.LicenseDownloadId; - decimal? itemWeight = orderItem.ItemWeight; - - writer.WriteStartElement("OrderItem"); - writer.Write("Id", ((int)orderItem.Id).ToString()); - writer.Write("OrderItemGuid", ((Guid)orderItem.OrderItemGuid).ToString()); - writer.Write("OrderId", ((int)orderItem.OrderId).ToString()); - writer.Write("ProductId", ((int)orderItem.ProductId).ToString()); - writer.Write("Quantity", ((int)orderItem.Quantity).ToString()); - writer.Write("UnitPriceInclTax", ((decimal)orderItem.UnitPriceInclTax).ToString(invariantCulture)); - writer.Write("UnitPriceExclTax", ((decimal)orderItem.UnitPriceExclTax).ToString(invariantCulture)); - writer.Write("PriceInclTax", ((decimal)orderItem.PriceInclTax).ToString(invariantCulture)); - writer.Write("PriceExclTax", ((decimal)orderItem.PriceExclTax).ToString(invariantCulture)); - writer.Write("TaxRate", ((decimal)orderItem.TaxRate).ToString(invariantCulture)); - writer.Write("DiscountAmountInclTax", ((decimal)orderItem.DiscountAmountInclTax).ToString(invariantCulture)); - writer.Write("DiscountAmountExclTax", ((decimal)orderItem.DiscountAmountExclTax).ToString(invariantCulture)); - writer.Write("AttributeDescription", (string)orderItem.AttributeDescription); - writer.Write("AttributesXml", (string)orderItem.AttributesXml); - writer.Write("DownloadCount", ((int)orderItem.DownloadCount).ToString()); - writer.Write("IsDownloadActivated", ((bool)orderItem.IsDownloadActivated).ToString()); - writer.Write("LicenseDownloadId", licenseDownloadId.HasValue ? licenseDownloadId.Value.ToString() : ""); - writer.Write("ItemWeight", itemWeight.HasValue ? itemWeight.Value.ToString(invariantCulture) : ""); - writer.Write("BundleData", (string)orderItem.BundleData); - writer.Write("ProductCost", ((decimal)orderItem.ProductCost).ToString(invariantCulture)); - - xmlHelper.WriteProduct(orderItem.Product, "Product"); - - writer.WriteEndElement(); // OrderItem - } - writer.WriteEndElement(); // OrderItems - - writer.WriteStartElement("Shipments"); - foreach (dynamic shipment in order.Shipments) - { - decimal? totalWeight = shipment.TotalWeight; - DateTime? shippedDateUtc = shipment.ShippedDateUtc; - DateTime? deliveryDateUtc = shipment.DeliveryDateUtc; - - writer.WriteStartElement("Shipment"); - writer.Write("Id", ((int)shipment.Id).ToString()); - writer.Write("OrderId", ((int)shipment.OrderId).ToString()); - writer.Write("TrackingNumber", (string)shipment.TrackingNumber); - writer.Write("TotalWeight", totalWeight.HasValue ? totalWeight.Value.ToString(invariantCulture) : ""); - writer.Write("ShippedDateUtc", shippedDateUtc.HasValue ? shippedDateUtc.Value.ToString(invariantCulture) : ""); - writer.Write("DeliveryDateUtc", deliveryDateUtc.HasValue ? deliveryDateUtc.Value.ToString(invariantCulture) : ""); - writer.Write("CreatedOnUtc", ((DateTime)shipment.CreatedOnUtc).ToString(invariantCulture)); - - writer.WriteStartElement("ShipmentItems"); - foreach (dynamic shipmentItem in shipment.ShipmentItems) - { - writer.WriteStartElement("ShipmentItem"); - writer.Write("Id", ((int)shipmentItem.Id).ToString()); - writer.Write("ShipmentId", ((int)shipmentItem.ShipmentId).ToString()); - writer.Write("OrderItemId", ((int)shipmentItem.OrderItemId).ToString()); - writer.Write("Quantity", ((int)shipmentItem.Quantity).ToString()); - writer.WriteEndElement(); // ShipmentItem - } - writer.WriteEndElement(); // ShipmentItems - - writer.WriteEndElement(); // Shipment - } - writer.WriteEndElement(); // Shipments - - ++context.RecordsSucceeded; - } - catch (Exception exc) - { - context.RecordException(exc, (int)order.Id); - } - - writer.WriteEndElement(); // Order - } - } - - writer.WriteEndElement(); // Orders - writer.WriteEndDocument(); - } - } - - public void ExecuteEnded(IExportExecuteContext context) - { - // nothing to do - } - } -} diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportProductXlsxProvider.cs b/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportProductXlsxProvider.cs deleted file mode 100644 index 516d05488a..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportProductXlsxProvider.cs +++ /dev/null @@ -1,423 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.IO; -using System.Linq; -using OfficeOpenXml; -using OfficeOpenXml.Style; -using SmartStore.Core.Domain.Catalog; -using SmartStore.Core.Domain.DataExchange; -using SmartStore.Core.Domain.Localization; -using SmartStore.Core.Logging; -using SmartStore.Core.Plugins; -using SmartStore.Services.Catalog; -using SmartStore.Services.Localization; -using SmartStore.Services.Stores; - -namespace SmartStore.Services.DataExchange.ExportProvider -{ - /// - /// Exports Excel formatted product data to a file - /// - [SystemName("Exports.SmartStoreProductXlsx")] - [FriendlyName("SmartStore Excel product export")] - [IsHidden(true)] - public class ExportProductXlsxProvider : IExportProvider - { - private readonly ILanguageService _languageService; - private readonly IProductService _productService; - private readonly IStoreMappingService _storeMappingService; - - private string[] Properties - { - get - { - return new string[] - { - "ProductTypeId", - "ParentGroupedProductId", - "VisibleIndividually", - "Name", - "ShortDescription", - "FullDescription", - "ProductTemplateId", - "ShowOnHomePage", - "HomePageDisplayOrder", - "MetaKeywords", - "MetaDescription", - "MetaTitle", - "SeName", - "AllowCustomerReviews", - "Published", - "SKU", - "ManufacturerPartNumber", - "Gtin", - "IsGiftCard", - "GiftCardTypeId", - "RequireOtherProducts", - "RequiredProductIds", - "AutomaticallyAddRequiredProducts", - "IsDownload", - "DownloadId", - "UnlimitedDownloads", - "MaxNumberOfDownloads", - "DownloadActivationTypeId", - "HasSampleDownload", - "SampleDownloadId", - "HasUserAgreement", - "UserAgreementText", - "IsRecurring", - "RecurringCycleLength", - "RecurringCyclePeriodId", - "RecurringTotalCycles", - "IsShipEnabled", - "IsFreeShipping", - "AdditionalShippingCharge", - "IsEsd", - "IsTaxExempt", - "TaxCategoryId", - "ManageInventoryMethodId", - "StockQuantity", - "DisplayStockAvailability", - "DisplayStockQuantity", - "MinStockQuantity", - "LowStockActivityId", - "NotifyAdminForQuantityBelow", - "BackorderModeId", - "AllowBackInStockSubscriptions", - "OrderMinimumQuantity", - "OrderMaximumQuantity", - "AllowedQuantities", - "DisableBuyButton", - "DisableWishlistButton", - "AvailableForPreOrder", - "CallForPrice", - "Price", - "OldPrice", - "ProductCost", - "SpecialPrice", - "SpecialPriceStartDateTimeUtc", - "SpecialPriceEndDateTimeUtc", - "CustomerEntersPrice", - "MinimumCustomerEnteredPrice", - "MaximumCustomerEnteredPrice", - "Weight", - "Length", - "Width", - "Height", - "CreatedOnUtc", - "CategoryIds", - "ManufacturerIds", - "Picture1", - "Picture2", - "Picture3", - "DeliveryTimeId", - "QuantityUnitId", - "BasePriceEnabled", - "BasePriceMeasureUnit", - "BasePriceAmount", - "BasePriceBaseAmount", - "BundleTitleText", - "BundlePerItemShipping", - "BundlePerItemPricing", - "BundlePerItemShoppingCart", - "BundleItemSkus", - "AvailableStartDateTimeUtc", - "AvailableEndDateTimeUtc", - "StoreIds", - "LimitedToStores" - }; - } - } - - public ExportProductXlsxProvider( - ILanguageService languageService, - IProductService productService, - IStoreMappingService storeMappingService) - { - _languageService = languageService; - _productService = productService; - _storeMappingService = storeMappingService; - } - - private void WriteCell(ExcelWorksheet worksheet, int row, ref int column, object value) - { - worksheet.Cells[row, column].Value = value; - ++column; - } - - private string GetLocalized(List allLocalized, Language language, string property) - { - if (allLocalized != null) - { - string localizeValue = allLocalized - .Where(x => property.IsCaseInsensitiveEqual((string)x.LocaleKey) && language.LanguageCulture.IsCaseInsensitiveEqual((string)x.Culture)) - .Select(x => (string)x.LocaleValue) - .FirstOrDefault(); - - return localizeValue; - } - return ""; - } - - public static string SystemName - { - get { return "Exports.SmartStoreProductXlsx"; } - } - - public ExportConfigurationInfo ConfigurationInfo - { - get { return null; } - } - - public ExportEntityType EntityType - { - get { return ExportEntityType.Product; } - } - - public string FileExtension - { - get { return "XLSX"; } - } - - public void Execute(IExportExecuteContext context) - { - var path = context.FilePath; - - context.Log.Information("Creating file " + path); - - using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite)) - using (var xlPackage = new ExcelPackage(stream)) - { - // uncomment this line if you want the XML written out to the outputDir - //xlPackage.DebugMode = true; - - // get handle to the existing worksheet - var worksheet = xlPackage.Workbook.Worksheets.Add("Products"); - - // get handle to the cells range of the worksheet - var cells = worksheet.Cells; - string[] properties = Properties; - var languages = _languageService.GetAllLanguages(true); - - var headlines = new string[properties.Length + languages.Count * 3]; - var languageFields = new string[languages.Count * 3]; - var row = 2; - var j = 0; - - foreach (var lang in languages) - { - languageFields.SetValue("Name[" + lang.UniqueSeoCode + "]", j++); - languageFields.SetValue("ShortDescription[" + lang.UniqueSeoCode + "]", j++); - languageFields.SetValue("FullDescription[" + lang.UniqueSeoCode + "]", j++); - } - - properties.CopyTo(headlines, 0); - languageFields.CopyTo(headlines, properties.Length); - - for (int i = 0; i < headlines.Length; i++) - { - cells[1, i + 1].Value = headlines[i]; - cells[1, i + 1].Style.Fill.PatternType = ExcelFillStyle.Solid; - cells[1, i + 1].Style.Fill.BackgroundColor.SetColor(Color.FromArgb(184, 204, 228)); - cells[1, i + 1].Style.Font.Bold = true; - } - - while (context.Abort == ExportAbortion.None && context.Segmenter.ReadNextSegment()) - { - var segment = context.Segmenter.CurrentSegment; - - foreach (dynamic product in segment) - { - if (context.Abort != ExportAbortion.None) - break; - - try - { - int column = 1; - DateTime? specialPriceStartDateTimeUtc = product.SpecialPriceStartDateTimeUtc; - DateTime? specialPriceEndDateTimeUtc = product.SpecialPriceEndDateTimeUtc; - - List productCategories = product.ProductCategories; - List productManus = product.ProductManufacturers; - List productPictures = product.ProductPictures; - List localized = product._Localized; - - WriteCell(worksheet, row, ref column, (int)product.ProductTypeId); - WriteCell(worksheet, row, ref column, (int)product.ParentGroupedProductId); - WriteCell(worksheet, row, ref column, (bool)product.VisibleIndividually); - WriteCell(worksheet, row, ref column, (string)product.Name); - WriteCell(worksheet, row, ref column, (string)product.ShortDescription); - WriteCell(worksheet, row, ref column, (string)product.FullDescription); - WriteCell(worksheet, row, ref column, (int)product.ProductTemplateId); - WriteCell(worksheet, row, ref column, (bool)product.ShowOnHomePage); - WriteCell(worksheet, row, ref column, (int)product.HomePageDisplayOrder); - WriteCell(worksheet, row, ref column, (string)product.MetaKeywords); - WriteCell(worksheet, row, ref column, (string)product.MetaDescription); - WriteCell(worksheet, row, ref column, (string)product.MetaTitle); - WriteCell(worksheet, row, ref column, (string)product.SeName); - WriteCell(worksheet, row, ref column, (bool)product.AllowCustomerReviews); - WriteCell(worksheet, row, ref column, (bool)product.Published); - WriteCell(worksheet, row, ref column, (string)product.Sku); - WriteCell(worksheet, row, ref column, (string)product.ManufacturerPartNumber); - WriteCell(worksheet, row, ref column, (string)product.Gtin); - WriteCell(worksheet, row, ref column, (bool)product.IsGiftCard); - WriteCell(worksheet, row, ref column, (int)product.GiftCardTypeId); - WriteCell(worksheet, row, ref column, (bool)product.RequireOtherProducts); - WriteCell(worksheet, row, ref column, (string)product.RequiredProductIds); - WriteCell(worksheet, row, ref column, (bool)product.AutomaticallyAddRequiredProducts); - WriteCell(worksheet, row, ref column, (bool)product.IsDownload); - WriteCell(worksheet, row, ref column, (int)product.DownloadId); - WriteCell(worksheet, row, ref column, (bool)product.UnlimitedDownloads); - WriteCell(worksheet, row, ref column, (int)product.MaxNumberOfDownloads); - WriteCell(worksheet, row, ref column, (int)product.DownloadActivationTypeId); - WriteCell(worksheet, row, ref column, (bool)product.HasSampleDownload); - WriteCell(worksheet, row, ref column, (int?)product.SampleDownloadId); - WriteCell(worksheet, row, ref column, (bool)product.HasUserAgreement); - WriteCell(worksheet, row, ref column, (string)product.UserAgreementText); - WriteCell(worksheet, row, ref column, (bool)product.IsRecurring); - WriteCell(worksheet, row, ref column, (int)product.RecurringCycleLength); - WriteCell(worksheet, row, ref column, (int)product.RecurringCyclePeriodId); - WriteCell(worksheet, row, ref column, (int)product.RecurringTotalCycles); - WriteCell(worksheet, row, ref column, (bool)product.IsShipEnabled); - WriteCell(worksheet, row, ref column, (bool)product.IsFreeShipping); - WriteCell(worksheet, row, ref column, (decimal)product.AdditionalShippingCharge); - WriteCell(worksheet, row, ref column, (bool)product.IsEsd); - WriteCell(worksheet, row, ref column, (bool)product.IsTaxExempt); - WriteCell(worksheet, row, ref column, (int)product.TaxCategoryId); - WriteCell(worksheet, row, ref column, (int)product.ManageInventoryMethodId); - WriteCell(worksheet, row, ref column, (int)product.StockQuantity); - WriteCell(worksheet, row, ref column, (bool)product.DisplayStockAvailability); - WriteCell(worksheet, row, ref column, (bool)product.DisplayStockQuantity); - WriteCell(worksheet, row, ref column, (int)product.MinStockQuantity); - WriteCell(worksheet, row, ref column, (int)product.LowStockActivityId); - WriteCell(worksheet, row, ref column, (int)product.NotifyAdminForQuantityBelow); - WriteCell(worksheet, row, ref column, (int)product.BackorderModeId); - WriteCell(worksheet, row, ref column, (bool)product.AllowBackInStockSubscriptions); - WriteCell(worksheet, row, ref column, (int)product.OrderMinimumQuantity); - WriteCell(worksheet, row, ref column, (int)product.OrderMaximumQuantity); - WriteCell(worksheet, row, ref column, (string)product.AllowedQuantities); - WriteCell(worksheet, row, ref column, (bool)product.DisableBuyButton); - WriteCell(worksheet, row, ref column, (bool)product.DisableWishlistButton); - WriteCell(worksheet, row, ref column, (bool)product.AvailableForPreOrder); - WriteCell(worksheet, row, ref column, (bool)product.CallForPrice); - WriteCell(worksheet, row, ref column, (decimal)product.Price); - WriteCell(worksheet, row, ref column, (decimal)product.OldPrice); - WriteCell(worksheet, row, ref column, (decimal)product.ProductCost); - WriteCell(worksheet, row, ref column, (decimal?)product.SpecialPrice); - WriteCell(worksheet, row, ref column, specialPriceStartDateTimeUtc.HasValue ? specialPriceStartDateTimeUtc.Value.ToOADate() : (double)0.0); - WriteCell(worksheet, row, ref column, specialPriceEndDateTimeUtc.HasValue ? specialPriceEndDateTimeUtc.Value.ToOADate() : (double)0.0); - WriteCell(worksheet, row, ref column, (bool)product.CustomerEntersPrice); - WriteCell(worksheet, row, ref column, (decimal)product.MinimumCustomerEnteredPrice); - WriteCell(worksheet, row, ref column, (decimal)product.MaximumCustomerEnteredPrice); - WriteCell(worksheet, row, ref column, (decimal)product.Weight); - WriteCell(worksheet, row, ref column, (decimal)product.Length); - WriteCell(worksheet, row, ref column, (decimal)product.Width); - WriteCell(worksheet, row, ref column, (decimal)product.Height); - WriteCell(worksheet, row, ref column, ((DateTime)product.CreatedOnUtc).ToOADate()); - - WriteCell(worksheet, row, ref column, string.Join(";", productCategories.Select(x => (int)x.CategoryId))); - WriteCell(worksheet, row, ref column, string.Join(";", productManus.Select(x => (int)x.ManufacturerId))); - - WriteCell(worksheet, row, ref column, productPictures.Count > 0 ? (string)productPictures[0].Picture._ThumbLocalPath : null); - WriteCell(worksheet, row, ref column, productPictures.Count > 1 ? (string)productPictures[1].Picture._ThumbLocalPath : null); - WriteCell(worksheet, row, ref column, productPictures.Count > 2 ? (string)productPictures[2].Picture._ThumbLocalPath : null); - - WriteCell(worksheet, row, ref column, (int?)product.DeliveryTimeId); - WriteCell(worksheet, row, ref column, (int?)product.QuantityUnitId); - WriteCell(worksheet, row, ref column, (bool)product.BasePriceEnabled); - WriteCell(worksheet, row, ref column, (string)product.BasePriceMeasureUnit); - WriteCell(worksheet, row, ref column, (decimal?)product.BasePriceAmount); - WriteCell(worksheet, row, ref column, (int?)product.BasePriceBaseAmount); - WriteCell(worksheet, row, ref column, (string)product.BundleTitleText); - WriteCell(worksheet, row, ref column, (bool)product.BundlePerItemShipping); - WriteCell(worksheet, row, ref column, (bool)product.BundlePerItemShipping); - WriteCell(worksheet, row, ref column, (bool)product.BundlePerItemShoppingCart); - - if ((int)product.ProductTypeId == (int)ProductType.BundledProduct) - { - List bundleItems = product.ProductBundleItems; - - var searchContext = new ProductSearchContext - { - ProductIds = bundleItems.Select(x => (int)x.ProductId).ToList(), - PageSize = int.MaxValue, - ShowHidden = true - }; - - var query = _productService.PrepareProductSearchQuery(searchContext, x => x.Sku); - var skus = query.ToList(); - - WriteCell(worksheet, row, ref column, string.Join(",", skus)); - } - else - { - WriteCell(worksheet, row, ref column, ""); - } - - WriteCell(worksheet, row, ref column, (DateTime?)product.AvailableStartDateTimeUtc); - WriteCell(worksheet, row, ref column, (DateTime?)product.AvailableEndDateTimeUtc); - - if ((bool)product.LimitedToStores) - { - var storeIds = _storeMappingService.GetStoreMappingsFor("Product", (int)product.Id) - .Select(x => x.StoreId) - .ToList(); - - WriteCell(worksheet, row, ref column, string.Join(";", storeIds)); - } - else - { - WriteCell(worksheet, row, ref column, ""); - } - - WriteCell(worksheet, row, ref column, (bool)product.LimitedToStores); - - foreach (var lang in languages) - { - WriteCell(worksheet, row, ref column, GetLocalized(localized, lang, "Name")); - WriteCell(worksheet, row, ref column, GetLocalized(localized, lang, "ShortDescription")); - WriteCell(worksheet, row, ref column, GetLocalized(localized, lang, "FullDescription")); - } - - ++context.RecordsSucceeded; - } - catch (Exception exc) - { - context.RecordException(exc, (int)product.Id); - } - - ++row; - } - } - - // we had better add some document properties to the spreadsheet - - // set some core property values - //var storeName = _storeInformationSettings.StoreName; - //var storeUrl = _storeInformationSettings.StoreUrl; - //xlPackage.Workbook.Properties.Title = string.Format("{0} products", storeName); - //xlPackage.Workbook.Properties.Author = storeName; - //xlPackage.Workbook.Properties.Subject = string.Format("{0} products", storeName); - //xlPackage.Workbook.Properties.Keywords = string.Format("{0} products", storeName); - //xlPackage.Workbook.Properties.Category = "Products"; - //xlPackage.Workbook.Properties.Comments = string.Format("{0} products", storeName); - - // set some extended property values - //xlPackage.Workbook.Properties.Company = storeName; - //xlPackage.Workbook.Properties.HyperlinkBase = new Uri(storeUrl); - - // save the new spreadsheet - xlPackage.Save(); - } - - // EPPLus had serious memory leak problems in V3. We enforce the garbage collector to release unused memory, it's not perfect, but better than nothing. - GC.Collect(); - } - - public void ExecuteEnded(IExportExecuteContext context) - { - // nothing to do - } - } -} diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportProductXmlProvider.cs b/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportProductXmlProvider.cs deleted file mode 100644 index a6894b4242..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportProvider/ExportProductXmlProvider.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Text; -using System.Xml; -using SmartStore.Core; -using SmartStore.Core.Domain.DataExchange; -using SmartStore.Core.Logging; -using SmartStore.Core.Plugins; - -namespace SmartStore.Services.DataExchange.ExportProvider -{ - /// - /// Exports XML formatted product data to a file - /// - [SystemName("Exports.SmartStoreProductXml")] - [FriendlyName("SmartStore XML product export")] - [IsHidden(true)] - public class ExportProductXmlProvider : IExportProvider - { - public static string SystemName - { - get { return "Exports.SmartStoreProductXml"; } - } - - public ExportConfigurationInfo ConfigurationInfo - { - get { return null; } - } - - public ExportEntityType EntityType - { - get { return ExportEntityType.Product; } - } - - public string FileExtension - { - get { return "XML"; } - } - - public void Execute(IExportExecuteContext context) - { - var settings = new XmlWriterSettings - { - Encoding = Encoding.UTF8, - CheckCharacters = false, - Indent = true, - IndentChars = "\t" - }; - - var path = context.FilePath; - - context.Log.Information("Creating file " + path); - - using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite)) - using (var writer = XmlWriter.Create(stream, settings)) - { - var xmlHelper = new ExportXmlHelper(writer, CultureInfo.InvariantCulture); - - writer.WriteStartDocument(); - writer.WriteStartElement("Products"); - writer.WriteAttributeString("Version", SmartStoreVersion.CurrentVersion); - - while (context.Abort == ExportAbortion.None && context.Segmenter.ReadNextSegment()) - { - var segment = context.Segmenter.CurrentSegment; - - foreach (dynamic product in segment) - { - if (context.Abort != ExportAbortion.None) - break; - - try - { - xmlHelper.WriteProduct(product, "Product"); - - ++context.RecordsSucceeded; - } - catch (Exception exc) - { - context.RecordException(exc, (int)product.Id); - } - } - } - - writer.WriteEndElement(); // Products - writer.WriteEndDocument(); - } - } - - public void ExecuteEnded(IExportExecuteContext context) - { - // nothing to do - } - } -} diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportTask/ExportProfileTask.cs b/src/Libraries/SmartStore.Services/DataExchange/ExportTask/ExportProfileTask.cs deleted file mode 100644 index 66698ab1a3..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportTask/ExportProfileTask.cs +++ /dev/null @@ -1,3055 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Dynamic; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Linq.Expressions; -using System.Net; -using System.Net.Http; -using System.Reflection; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Web; -using System.Web.Mvc; -using Autofac; -using SmartStore.Core; -using SmartStore.Core.Async; -using SmartStore.Core.Data; -using SmartStore.Core.Domain; -using SmartStore.Core.Domain.Catalog; -using SmartStore.Core.Domain.Common; -using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.DataExchange; -using SmartStore.Core.Domain.Directory; -using SmartStore.Core.Domain.Discounts; -using SmartStore.Core.Domain.Localization; -using SmartStore.Core.Domain.Media; -using SmartStore.Core.Domain.Messages; -using SmartStore.Core.Domain.Orders; -using SmartStore.Core.Domain.Seo; -using SmartStore.Core.Domain.Shipping; -using SmartStore.Core.Domain.Stores; -using SmartStore.Core.Email; -using SmartStore.Core.Html; -using SmartStore.Core.IO; -using SmartStore.Core.Logging; -using SmartStore.Core.Plugins; -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.Media; -using SmartStore.Services.Messages; -using SmartStore.Services.Orders; -using SmartStore.Services.Seo; -using SmartStore.Services.Shipping; -using SmartStore.Services.Tasks; -using SmartStore.Services.Tax; -using SmartStore.Utilities; -using SmartStore.Utilities.Threading; - -// note: namespace persisted in ScheduleTask.Type -namespace SmartStore.Services.DataExchange.ExportTask -{ - public class ExportProfileTask : ITask - { - private static readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); - - #region Dependencies - - private ICommonServices _services; - private Lazy _exportService; - private Lazy _dataExchangeSettings; - private Lazy _productService; - private Lazy _pictureService; - private Lazy _mediaSettings; - private Lazy _priceCalculationService; - private Lazy _taxService; - private Lazy _currencyService; - private Lazy _customerService; - private Lazy> _customerRepository; - private Lazy _categoryService; - private Lazy _priceFormatter; - private Lazy _dateTimeHelper; - private Lazy _emailAccountService; - private Lazy _emailSender; - private Lazy _queuedEmailService; - private Lazy _productAttributeService; - private Lazy _productAttributeParser; - private Lazy _deliveryTimeService; - private Lazy _quantityUnitService; - private Lazy _manufacturerService; - private Lazy _orderService; - private Lazy _addressesService; - private Lazy _countryService; - private Lazy> _orderRepository; - private Lazy _languageService; - private Lazy _shipmentService; - private Lazy _productTemplateService; - private Lazy _localizedEntityService; - private Lazy _urlRecordService; - private Lazy _genericAttributeService; - private Lazy> _subscriptionRepository; - - private void InitDependencies(TaskExecutionContext context) - { - _services = context.Resolve(); - _exportService = context.Resolve>(); - _dataExchangeSettings = context.Resolve>(); - _productService = context.Resolve>(); - _pictureService = context.Resolve>(); - _mediaSettings = context.Resolve>(); - _priceCalculationService = context.Resolve>(); - _taxService = context.Resolve>(); - _currencyService = context.Resolve>(); - _customerService = context.Resolve>(); - _customerRepository = context.Resolve>>(); - _categoryService = context.Resolve>(); - _priceFormatter = context.Resolve>(); - _dateTimeHelper = context.Resolve>(); - _emailAccountService = context.Resolve>(); - _emailSender = context.Resolve>(); - _queuedEmailService = context.Resolve>(); - _productAttributeService = context.Resolve>(); - _productAttributeParser = context.Resolve>(); - _deliveryTimeService = context.Resolve>(); - _quantityUnitService = context.Resolve>(); - _manufacturerService = context.Resolve>(); - _orderService = context.Resolve>(); - _addressesService = context.Resolve>(); - _countryService = context.Resolve>(); - _orderRepository = context.Resolve>>(); - _languageService = context.Resolve>(); - _shipmentService = context.Resolve>(); - _productTemplateService = context.Resolve>(); - _localizedEntityService = context.Resolve>(); - _urlRecordService = context.Resolve>(); - _genericAttributeService = context.Resolve>(); - _subscriptionRepository = context.Resolve>>(); - } - - #endregion - - #region Utilities - - private List GetLocalized(ExportProfileTaskContext ctx, T entity, params Expression>[] keySelectors) - where T : BaseEntity, ILocalizedEntity - { - if (ctx.Languages.Count <= 1) - return null; - - var localized = new List(); - - var localeKeyGroup = typeof(T).Name; - var isSlugSupported = typeof(ISlugSupported).IsAssignableFrom(typeof(T)); - - foreach (var language in ctx.Languages) - { - var languageCulture = language.Value.LanguageCulture.EmptyNull().ToLower(); - - // add SeName - if (isSlugSupported) - { - var value = _urlRecordService.Value.GetActiveSlug(entity.Id, localeKeyGroup, language.Value.Id); - if (value.HasValue()) - { - dynamic exp = new ExpandoObject(); - exp.Culture = languageCulture; - exp.LocaleKey = "SeName"; - exp.LocaleValue = value; - - localized.Add(exp); - } - } - - foreach (var keySelector in keySelectors) - { - var member = keySelector.Body as MemberExpression; - var propInfo = member.Member as PropertyInfo; - string localeKey = propInfo.Name; - var value = _localizedEntityService.Value.GetLocalizedValue(language.Value.Id, entity.Id, localeKeyGroup, localeKey); - - // we better not export empty values. the risk is to high that they are imported and unnecessary fill databases. - if (value.HasValue()) - { - dynamic exp = new ExpandoObject(); - exp.Culture = languageCulture; - exp.LocaleKey = localeKey; - exp.LocaleValue = value; - - localized.Add(exp); - } - } - } - - return (localized.Count == 0 ? null : localized); - } - - private IExportSegmenterProvider CreateSegmenter(ExportProfileTaskContext ctx, int pageIndex = 0) - { - var offset = ctx.Profile.Offset + (pageIndex * PageSize); - - var limit = (ctx.IsPreview ? PageSize : ctx.Profile.Limit); - - var recordsPerSegment = (ctx.IsPreview ? 0 : ctx.Profile.BatchSize); - - var totalCount = ctx.Profile.Offset + ctx.RecordsPerStore.First(x => x.Key == ctx.Store.Id).Value; - - switch (ctx.Provider.Value.EntityType) - { - case ExportEntityType.Product: - ctx.Export.Segmenter = new ExportSegmenter - ( - skip => GetProducts(ctx, skip), - entities => - { - // load data behind navigation properties for current queue in one go - ctx.DataContextProduct = new ExportDataContextProduct(entities, - x => _productAttributeService.Value.GetProductVariantAttributesByProductIds(x, null), - x => _productAttributeService.Value.GetProductVariantAttributeCombinations(x), - x => _productService.Value.GetTierPricesByProductIds(x, (ctx.Projection.CurrencyId ?? 0) != 0 ? ctx.ContextCustomer : null, ctx.Store.Id), - x => _categoryService.Value.GetProductCategoriesByProductIds(x), - x => _manufacturerService.Value.GetProductManufacturersByProductIds(x), - x => _productService.Value.GetProductPicturesByProductIds(x), - x => _productService.Value.GetProductTagsByProductIds(x), - x => _productService.Value.GetAppliedDiscountsByProductIds(x), - x => _productService.Value.GetProductSpecificationAttributesByProductIds(x), - x => _productService.Value.GetBundleItemsByProductIds(x, true) - ); - }, - entity => ConvertToExpando(ctx, entity), - offset, PageSize, limit, recordsPerSegment, totalCount - ); - break; - - case ExportEntityType.Order: - ctx.Export.Segmenter = new ExportSegmenter - ( - skip => GetOrders(ctx, skip), - entities => - { - ctx.DataContextOrder = new ExportDataContextOrder(entities, - x => _customerService.Value.GetCustomersByIds(x), - x => _customerService.Value.GetRewardPointsHistoriesByCustomerIds(x), - x => _addressesService.Value.GetAddressByIds(x), - x => _orderService.Value.GetOrderItemsByOrderIds(x), - x => _shipmentService.Value.GetShipmentsByOrderIds(x) - ); - }, - entity => ConvertToExpando(ctx, entity), - offset, PageSize, limit, recordsPerSegment, totalCount - ); - break; - - case ExportEntityType.Manufacturer: - ctx.Export.Segmenter = new ExportSegmenter - ( - skip => GetManufacturers(ctx, skip), - entities => - { - ctx.DataContextManufacturer = new ExportDataContextManufacturer(entities, - x => _manufacturerService.Value.GetProductManufacturersByManufacturerIds(x), - x => _pictureService.Value.GetPicturesByIds(x) - ); - }, - entity => ConvertToExpando(ctx, entity), - offset, PageSize, limit, recordsPerSegment, totalCount - ); - break; - - case ExportEntityType.Category: - ctx.Export.Segmenter = new ExportSegmenter - ( - skip => GetCategories(ctx, skip), - entities => - { - ctx.DataContextCategory = new ExportDataContextCategory(entities, - x => _categoryService.Value.GetProductCategoriesByCategoryIds(x), - x => _pictureService.Value.GetPicturesByIds(x) - ); - }, - entity => ConvertToExpando(ctx, entity), - offset, PageSize, limit, recordsPerSegment, totalCount - ); - break; - - case ExportEntityType.Customer: - ctx.Export.Segmenter = new ExportSegmenter - ( - skip => GetCustomers(ctx, skip), - entities => - { - ctx.DataContextCustomer = new ExportDataContextCustomer(entities, - x => _genericAttributeService.Value.GetAttributesForEntity(x, "Customer") - ); - }, - entity => ConvertToExpando(ctx, entity), - offset, PageSize, limit, recordsPerSegment, totalCount - ); - break; - - case ExportEntityType.NewsLetterSubscription: - ctx.Export.Segmenter = new ExportSegmenter - ( - skip => GetNewsLetterSubscriptions(ctx, skip), - null, - entity => ConvertToExpando(ctx, entity), - offset, PageSize, limit, recordsPerSegment, totalCount - ); - break; - - default: - ctx.Export.Segmenter = null; - break; - } - - return ctx.Export.Segmenter as IExportSegmenterProvider; - } - - private void PrepareProductDescription(ExportProfileTaskContext ctx, dynamic expando, Product product) - { - try - { - var languageId = (ctx.Projection.LanguageId ?? 0); - string description = ""; - - // description merging - if (ctx.Projection.DescriptionMerging != ExportDescriptionMerging.None) - { - if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ShortDescriptionOrNameIfEmpty) - { - description = expando.FullDescription; - - if (description.IsEmpty()) - description = expando.ShortDescription; - if (description.IsEmpty()) - description = expando.Name; - } - else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ShortDescription) - { - description = expando.ShortDescription; - } - else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.Description) - { - description = expando.FullDescription; - } - else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.NameAndShortDescription) - { - description = ((string)expando.Name).Grow((string)expando.ShortDescription, " "); - } - else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.NameAndDescription) - { - description = ((string)expando.Name).Grow((string)expando.FullDescription, " "); - } - else if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ManufacturerAndNameAndShortDescription || - ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ManufacturerAndNameAndDescription) - { - var productManus = ctx.DataContextProduct.ProductManufacturers.Load(product.Id); - - if (productManus != null && productManus.Any()) - description = productManus.First().Manufacturer.GetLocalized(x => x.Name, languageId, true, false); - - description = description.Grow((string)expando.Name, " "); - - if (ctx.Projection.DescriptionMerging == ExportDescriptionMerging.ManufacturerAndNameAndShortDescription) - description = description.Grow((string)expando.ShortDescription, " "); - else - description = description.Grow((string)expando.FullDescription, " "); - } - } - else - { - description = expando.FullDescription; - } - - // append text - if (ctx.Projection.AppendDescriptionText.HasValue() && ((string)expando.ShortDescription).IsEmpty() && ((string)expando.FullDescription).IsEmpty()) - { - string[] appendText = ctx.Projection.AppendDescriptionText.SplitSafe(","); - if (appendText.Length > 0) - { - var rnd = (new Random()).Next(0, appendText.Length - 1); - - description = description.Grow(appendText.SafeGet(rnd), " "); - } - } - - // remove critical characters - if (description.HasValue() && ctx.Projection.RemoveCriticalCharacters) - { - foreach (var str in ctx.Projection.CriticalCharacters.SplitSafe(",")) - description = description.Replace(str, ""); - } - - // convert to plain text - if (ctx.Projection.DescriptionToPlainText) - { - //Regex reg = new Regex("<[^>]+>", RegexOptions.IgnoreCase); - //description = HttpUtility.HtmlDecode(reg.Replace(description, "")); - - description = HtmlUtils.ConvertHtmlToPlainText(description); - description = HtmlUtils.StripTags(HttpUtility.HtmlDecode(description)); - } - - expando.FullDescription = description; - } - catch { } - } - - private decimal? ConvertPrice(ExportProfileTaskContext ctx, Product product, decimal? price) - { - if (price.HasValue) - { - if (ctx.Projection.ConvertNetToGrossPrices) - { - decimal taxRate; - price = _taxService.Value.GetProductPrice(product, price.Value, true, ctx.ContextCustomer, out taxRate); - } - - if (price != decimal.Zero) - { - price = _currencyService.Value.ConvertFromPrimaryStoreCurrency(price.Value, ctx.ContextCurrency, ctx.Store); - } - } - return price; - } - - private decimal CalculatePrice(ExportProfileTaskContext ctx, Product product, bool forAttributeCombination) - { - decimal price = product.Price; - - // price type - if (ctx.Projection.PriceType.HasValue && !forAttributeCombination) - { - var priceCalculationContext = ctx.DataContextProduct as PriceCalculationContext; - - if (ctx.Projection.PriceType.Value == PriceDisplayType.LowestPrice) - { - bool displayFromMessage; - price = _priceCalculationService.Value.GetLowestPrice(product, priceCalculationContext, out displayFromMessage); - } - else if (ctx.Projection.PriceType.Value == PriceDisplayType.PreSelectedPrice) - { - price = _priceCalculationService.Value.GetPreselectedPrice(product, priceCalculationContext); - } - else if (ctx.Projection.PriceType.Value == PriceDisplayType.PriceWithoutDiscountsAndAttributes) - { - price = _priceCalculationService.Value.GetFinalPrice(product, null, ctx.ContextCustomer, decimal.Zero, false, 1, null, priceCalculationContext); - } - } - - return ConvertPrice(ctx, product, price) ?? price; - } - - private void GetDeliveryTimeAndQuantityUnit(ExportProfileTaskContext ctx, dynamic expando, int? deliveryTimeId, int? quantityUnitId) - { - if (ctx.DeliveryTimes != null) - { - if (deliveryTimeId.HasValue && ctx.DeliveryTimes.ContainsKey(deliveryTimeId.Value)) - expando.DeliveryTime = ToExpando(ctx, ctx.DeliveryTimes[deliveryTimeId.Value]); - else - expando.DeliveryTime = null; - } - - if (ctx.QuantityUnits != null) - { - if (quantityUnitId.HasValue && ctx.QuantityUnits.ContainsKey(quantityUnitId.Value)) - expando.QuantityUnit = ToExpando(ctx, ctx.QuantityUnits[quantityUnitId.Value]); - else - expando.QuantityUnit = null; - } - } - - private void SetProgress(ExportProfileTaskContext ctx, int loadedRecords) - { - if (!ctx.IsPreview && ctx.TaskContext.ScheduleTask != null && loadedRecords > 0) - { - int totalRecords = ctx.RecordsPerStore.Sum(x => x.Value); - - if (ctx.Profile.Limit > 0 && totalRecords > ctx.Profile.Limit) - totalRecords = ctx.Profile.Limit; - - ctx.RecordCount = Math.Min(ctx.RecordCount + loadedRecords, totalRecords); - - var msg = ctx.ProgressInfo.FormatInvariant(ctx.RecordCount, totalRecords); - - ctx.TaskContext.SetProgress(ctx.RecordCount, totalRecords, msg, true); - } - } - - private void SetProgress(ExportProfileTaskContext ctx, string message) - { - if (!ctx.IsPreview && ctx.TaskContext.ScheduleTask != null && message.HasValue()) - { - ctx.TaskContext.SetProgress(null, message, true); - } - } - - private void SendCompletionEmail(ExportProfileTaskContext ctx) - { - var emailAccount = _emailAccountService.Value.GetEmailAccountById(ctx.Profile.EmailAccountId); - var smtpContext = new SmtpContext(emailAccount); - var message = new EmailMessage(); - - var storeInfo = "{0} ({1})".FormatInvariant(ctx.Store.Name, ctx.Store.Url); - - message.To.AddRange(ctx.Profile.CompletedEmailAddresses.SplitSafe(",").Where(x => x.IsEmail()).Select(x => new EmailAddress(x))); - message.From = new EmailAddress(emailAccount.Email, emailAccount.DisplayName); - - message.Subject = _services.Localization.GetResource("Admin.DataExchange.Export.CompletedEmail.Subject", ctx.Projection.LanguageId ?? 0) - .FormatInvariant(ctx.Profile.Name); - - message.Body = _services.Localization.GetResource("Admin.DataExchange.Export.CompletedEmail.Body", ctx.Projection.LanguageId ?? 0) - .FormatInvariant(storeInfo); - - _emailSender.Value.SendEmail(smtpContext, message); - } - - private void DeployFileSystem(ExportProfileTaskContext ctx, ExportDeployment deployment) - { - string folderDestination = null; - - if (deployment.IsPublic) - { - folderDestination = Path.Combine(HttpRuntime.AppDomainAppPath, PublicFolder); - } - else if (deployment.FileSystemPath.IsEmpty()) - { - return; - } - else if (deployment.FileSystemPath.StartsWith("/") || deployment.FileSystemPath.StartsWith("\\") || !Path.IsPathRooted(deployment.FileSystemPath)) - { - folderDestination = CommonHelper.MapPath(deployment.FileSystemPath); - } - else - { - folderDestination = deployment.FileSystemPath; - } - - if (!System.IO.Directory.Exists(folderDestination)) - { - System.IO.Directory.CreateDirectory(folderDestination); - } - - if (deployment.CreateZip) - { - var path = Path.Combine(folderDestination, ctx.Profile.FolderName + ".zip"); - - if (FileSystemHelper.Copy(ctx.ZipPath, path)) - ctx.Log.Information("Copied ZIP archive " + path); - } - else - { - FileSystemHelper.CopyDirectory(new DirectoryInfo(ctx.FolderContent), new DirectoryInfo(folderDestination)); - - ctx.Log.Information("Copied export data files to " + folderDestination); - } - } - - private void DeployEmail(ExportProfileTaskContext ctx, ExportDeployment deployment) - { - var emailAccount = _emailAccountService.Value.GetEmailAccountById(deployment.EmailAccountId); - var smtpContext = new SmtpContext(emailAccount); - var count = 0; - - foreach (var email in deployment.EmailAddresses.SplitSafe(",").Where(x => x.IsEmail())) - { - var queuedEmail = new QueuedEmail - { - From = emailAccount.Email, - FromName = emailAccount.DisplayName, - To = email, - Subject = deployment.EmailSubject.NaIfEmpty(), - CreatedOnUtc = DateTime.UtcNow, - EmailAccountId = deployment.EmailAccountId - }; - - foreach (string path in ctx.GetDeploymentFiles(deployment)) - { - string name = Path.GetFileName(path); - - queuedEmail.Attachments.Add(new QueuedEmailAttachment - { - StorageLocation = EmailAttachmentStorageLocation.Path, - Path = path, - Name = name, - MimeType = MimeTypes.MapNameToMimeType(name) - }); - } - - _queuedEmailService.Value.InsertQueuedEmail(queuedEmail); - ++count; - } - - ctx.Log.Information("{0} email(s) created and queued for deployment.".FormatInvariant(count)); - } - - private void DeployHttp(ExportProfileTaskContext ctx, ExportDeployment deployment) - { - var succeeded = 0; - var url = deployment.Url; - - if (!url.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) && !url.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase)) - url = "http://" + url; - - if (deployment.HttpTransmissionType == ExportHttpTransmissionType.MultipartFormDataPost) - { - var count = 0; - ICredentials credentials = null; - - if (deployment.Username.HasValue()) - credentials = new NetworkCredential(deployment.Username, deployment.Password); - - using (var handler = new HttpClientHandler { Credentials = credentials }) - using (var client = new HttpClient(handler)) - using (var formData = new MultipartFormDataContent()) - { - foreach (var path in ctx.GetDeploymentFiles(deployment)) - { - byte[] fileData = File.ReadAllBytes(path); - formData.Add(new ByteArrayContent(fileData), "file {0}".FormatInvariant(++count), Path.GetFileName(path)); - } - - var response = client.PostAsync(url, formData).Result; - - if (response.IsSuccessStatusCode) - { - succeeded = count; - } - else if (response.Content != null) - { - var content = response.Content.ReadAsStringAsync().Result; - - var msg = "Multipart form data upload failed. {0} ({1}). Response: {2}".FormatInvariant( - response.StatusCode.ToString(), (int)response.StatusCode, content.NaIfEmpty().Truncate(2000, "...")); - - ctx.Log.Error(msg); - } - } - } - else - { - using (var webClient = new WebClient()) - { - if (deployment.Username.HasValue()) - webClient.Credentials = new NetworkCredential(deployment.Username, deployment.Password); - - foreach (var path in ctx.GetDeploymentFiles(deployment)) - { - webClient.UploadFile(url, path); - - ++succeeded; - } - } - } - - ctx.Log.Information("{0} file(s) successfully uploaded via HTTP ({1}).".FormatInvariant(succeeded, deployment.HttpTransmissionType.ToString())); - } - - private void DeployFtp(ExportProfileTaskContext ctx, ExportDeployment deployment) - { - var bytesRead = 0; - var succeededFiles = 0; - var url = deployment.Url; - var buffLength = 32768; - byte[] buff = new byte[buffLength]; - var deploymentFiles = ctx.GetDeploymentFiles(deployment).ToList(); - var lastIndex = (deploymentFiles.Count - 1); - - if (!url.StartsWith("ftp://", StringComparison.InvariantCultureIgnoreCase)) - url = "ftp://" + url; - - foreach (var path in deploymentFiles) - { - var fileUrl = url.EnsureEndsWith("/") + Path.GetFileName(path); - - var request = (FtpWebRequest)WebRequest.Create(fileUrl); - request.Method = WebRequestMethods.Ftp.UploadFile; - request.KeepAlive = (deploymentFiles.IndexOf(path) != lastIndex); - request.UseBinary = true; - request.Proxy = null; - request.UsePassive = deployment.PassiveMode; - request.EnableSsl = deployment.UseSsl; - - if (deployment.Username.HasValue()) - request.Credentials = new NetworkCredential(deployment.Username, deployment.Password); - - request.ContentLength = (new FileInfo(path)).Length; - - var requestStream = request.GetRequestStream(); - - using (var stream = new FileStream(path, FileMode.Open)) - { - while ((bytesRead = stream.Read(buff, 0, buffLength)) != 0) - { - requestStream.Write(buff, 0, bytesRead); - } - } - - requestStream.Close(); - - using (var response = (FtpWebResponse)request.GetResponse()) - { - var statusCode = (int)response.StatusCode; - - if (statusCode >= 200 && statusCode <= 299) - { - ++succeededFiles; - } - else - { - var msg = "The FTP transfer might fail. {0} ({1}), {2}. File {3}".FormatInvariant( - response.StatusCode.ToString(), statusCode, response.StatusDescription.NaIfEmpty(), path); - - ctx.Log.Error(msg); - } - } - } - - ctx.Log.Information("{0} file(s) successfully uploaded via FTP.".FormatInvariant(succeededFiles)); - } - - #endregion - - #region Entity to expando - - private dynamic ToExpando(ExportProfileTaskContext ctx, Currency currency) - { - if (currency == null) - return null; - - dynamic expando = new ExpandoObject(); - expando._Entity = currency; - - expando.Id = currency.Id; - expando.Name = currency.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - expando.CurrencyCode = currency.CurrencyCode; - expando.Rate = currency.Rate; - expando.DisplayLocale = currency.DisplayLocale; - expando.CustomFormatting = currency.CustomFormatting; - expando.LimitedToStores = currency.LimitedToStores; - expando.Published = currency.Published; - expando.DisplayOrder = currency.DisplayOrder; - expando.CreatedOnUtc = currency.CreatedOnUtc; - expando.UpdatedOnUtc = currency.UpdatedOnUtc; - expando.DomainEndings = currency.DomainEndings; - - expando._Localized = GetLocalized(ctx, currency, x => x.Name); - - return expando; - } - - private dynamic ToExpando(ExportProfileTaskContext ctx, Language language) - { - if (language == null) - return null; - - dynamic expando = new ExpandoObject(); - expando._Entity = language; - - expando.Id = language.Id; - expando.Name = language.Name; - expando.LanguageCulture = language.LanguageCulture; - expando.UniqueSeoCode = language.UniqueSeoCode; - expando.FlagImageFileName = language.FlagImageFileName; - expando.Rtl = language.Rtl; - expando.LimitedToStores = language.LimitedToStores; - expando.Published = language.Published; - expando.DisplayOrder = language.DisplayOrder; - - return expando; - } - - private dynamic ToExpando(ExportProfileTaskContext ctx, Country country) - { - if (country == null) - return null; - - dynamic expando = new ExpandoObject(); - expando._Entity = country; - - expando.Id = country.Id; - expando.Name = country.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - expando.AllowsBilling = country.AllowsBilling; - expando.AllowsShipping = country.AllowsShipping; - expando.TwoLetterIsoCode = country.TwoLetterIsoCode; - expando.ThreeLetterIsoCode = country.ThreeLetterIsoCode; - expando.NumericIsoCode = country.NumericIsoCode; - expando.SubjectToVat = country.SubjectToVat; - expando.Published = country.Published; - expando.DisplayOrder = country.DisplayOrder; - expando.LimitedToStores = country.LimitedToStores; - - expando._Localized = GetLocalized(ctx, country, x => x.Name); - - return expando; - } - - private dynamic ToExpando(ExportProfileTaskContext ctx, Address address) - { - if (address == null) - return null; - - dynamic expando = new ExpandoObject(); - expando._Entity = address; - - expando.Id = address.Id; - expando.FirstName = address.FirstName; - expando.LastName = address.LastName; - expando.Email = address.Email; - expando.Company = address.Company; - expando.CountryId = address.CountryId; - expando.StateProvinceId = address.StateProvinceId; - expando.City = address.City; - expando.Address1 = address.Address1; - expando.Address2 = address.Address2; - expando.ZipPostalCode = address.ZipPostalCode; - expando.PhoneNumber = address.PhoneNumber; - expando.FaxNumber = address.FaxNumber; - expando.CreatedOnUtc = address.CreatedOnUtc; - - expando.Country = ToExpando(ctx, address.Country); - - if (address.StateProvince != null) - { - dynamic sp = new ExpandoObject(); - sp._Entity = address.StateProvince; - sp.Id = address.StateProvince.Id; - sp.CountryId = address.StateProvince.CountryId; - sp.Name = address.StateProvince.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - sp.Abbreviation = address.StateProvince.Abbreviation; - sp.Published = address.StateProvince.Published; - sp.DisplayOrder = address.StateProvince.DisplayOrder; - - sp._Localized = GetLocalized(ctx, address.StateProvince, x => x.Name); - - expando.StateProvince = sp; - } - else - { - expando.StateProvince = null; - } - - return expando; - } - - private dynamic ToExpando(ExportProfileTaskContext ctx, RewardPointsHistory points) - { - if (points == null) - return null; - - dynamic expando = new ExpandoObject(); - expando._Entity = points; - - expando.Id = points.Id; - expando.CustomerId = points.CustomerId; - expando.Points = points.Points; - expando.PointsBalance = points.PointsBalance; - expando.UsedAmount = points.UsedAmount; - expando.Message = points.Message; - expando.CreatedOnUtc = points.CreatedOnUtc; - - return expando; - } - - private dynamic ToExpando(ExportProfileTaskContext ctx, Customer customer) - { - if (customer == null) - return null; - - dynamic expando = new ExpandoObject(); - expando._Entity = customer; - - expando.Id = customer.Id; - expando.CustomerGuid = customer.CustomerGuid; - expando.Username = customer.Username; - expando.Email = customer.Email; - expando.Password = customer.Password; // not so good but referenced in Excel export - expando.PasswordFormatId = customer.PasswordFormatId; - expando.PasswordSalt = customer.PasswordSalt; - expando.AdminComment = customer.AdminComment; - expando.IsTaxExempt = customer.IsTaxExempt; - expando.AffiliateId = customer.AffiliateId; - expando.Active = customer.Active; - expando.Deleted = customer.Deleted; - expando.IsSystemAccount = customer.IsSystemAccount; - expando.SystemName = customer.SystemName; - expando.LastIpAddress = customer.LastIpAddress; - expando.CreatedOnUtc = customer.CreatedOnUtc; - expando.LastLoginDateUtc = customer.LastLoginDateUtc; - expando.LastActivityDateUtc = customer.LastActivityDateUtc; - - expando.BillingAddress = null; - expando.ShippingAddress = null; - expando.Addresses = null; - expando.CustomerRoles = null; - - expando.RewardPointsHistory = null; - expando._RewardPointsBalance = 0; - - expando._GenericAttributes = null; - expando._HasNewsletterSubscription = false; - - return expando; - } - - private dynamic ToExpando(ExportProfileTaskContext ctx, Store store) - { - if (store == null) - return null; - - dynamic expando = new ExpandoObject(); - expando._Entity = store; - - expando.Id = store.Id; - expando.Name = store.Name; - expando.Url = store.Url; - expando.SslEnabled = store.SslEnabled; - expando.SecureUrl = store.SecureUrl; - expando.Hosts = store.Hosts; - expando.LogoPictureId = store.LogoPictureId; - expando.DisplayOrder = store.DisplayOrder; - expando.HtmlBodyId = store.HtmlBodyId; - expando.ContentDeliveryNetwork = store.ContentDeliveryNetwork; - expando.PrimaryStoreCurrencyId = store.PrimaryStoreCurrencyId; - expando.PrimaryExchangeRateCurrencyId = store.PrimaryExchangeRateCurrencyId; - - expando.PrimaryStoreCurrency = ToExpando(ctx, store.PrimaryStoreCurrency); - expando.PrimaryExchangeRateCurrency = ToExpando(ctx, store.PrimaryExchangeRateCurrency); - - return expando; - } - - private dynamic ToExpando(ExportProfileTaskContext ctx, DeliveryTime deliveryTime) - { - if (deliveryTime == null) - return null; - - dynamic expando = new ExpandoObject(); - expando._Entity = deliveryTime; - - expando.Id = deliveryTime.Id; - expando.Name = deliveryTime.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - expando.DisplayLocale = deliveryTime.DisplayLocale; - expando.ColorHexValue = deliveryTime.ColorHexValue; - expando.DisplayOrder = deliveryTime.DisplayOrder; - - expando._Localized = GetLocalized(ctx, deliveryTime, x => x.Name); - - return expando; - } - - private dynamic ToExpando(ExportProfileTaskContext ctx, QuantityUnit quantityUnit) - { - if (quantityUnit == null) - return null; - - dynamic expando = new ExpandoObject(); - expando._Entity = quantityUnit; - - expando.Id = quantityUnit.Id; - expando.Name = quantityUnit.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - expando.Description = quantityUnit.GetLocalized(x => x.Description, ctx.Projection.LanguageId ?? 0, true, false); - expando.DisplayLocale = quantityUnit.DisplayLocale; - expando.DisplayOrder = quantityUnit.DisplayOrder; - expando.IsDefault = quantityUnit.IsDefault; - - expando._Localized = GetLocalized(ctx, quantityUnit, - x => x.Name, - x => x.Description); - - return expando; - } - - private dynamic ToExpando(ExportProfileTaskContext ctx, Picture picture, int thumbPictureSize, int detailsPictureSize) - { - if (picture == null) - return null; - - dynamic expando = new ExpandoObject(); - expando._Entity = picture; - - expando.Id = picture.Id; - expando.SeoFilename = picture.SeoFilename; - expando.MimeType = picture.MimeType; - - expando._ThumbImageUrl = _pictureService.Value.GetPictureUrl(picture, thumbPictureSize, false, ctx.Store.Url); - expando._ImageUrl = _pictureService.Value.GetPictureUrl(picture, detailsPictureSize, false, ctx.Store.Url); - expando._FullSizeImageUrl = _pictureService.Value.GetPictureUrl(picture, 0, false, ctx.Store.Url); - - var relativeUrl = _pictureService.Value.GetPictureUrl(picture); - expando._FileName = relativeUrl.Substring(relativeUrl.LastIndexOf("/") + 1); - - expando._ThumbLocalPath = _pictureService.Value.GetThumbLocalPath(picture); - - return expando; - } - - private dynamic ToExpando(ExportProfileTaskContext ctx, ProductVariantAttribute pva) - { - if (pva == null) - return null; - - dynamic expando = new ExpandoObject(); - expando._Entity = pva; - - expando.Id = pva.Id; - expando.TextPrompt = pva.TextPrompt; - expando.IsRequired = pva.IsRequired; - expando.AttributeControlTypeId = pva.AttributeControlTypeId; - expando.DisplayOrder = pva.DisplayOrder; - - dynamic attribute = new ExpandoObject(); - attribute._Entity = pva.ProductAttribute; - attribute.Id = pva.ProductAttribute.Id; - attribute.Alias = pva.ProductAttribute.Alias; - attribute.Name = pva.ProductAttribute.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - attribute.Description = pva.ProductAttribute.GetLocalized(x => x.Description, ctx.Projection.LanguageId ?? 0, true, false); - - attribute.Values = pva.ProductVariantAttributeValues - .OrderBy(x => x.DisplayOrder) - .Select(x => - { - dynamic value = new ExpandoObject(); - value._Entity = x; - value.Id = x.Id; - value.Alias = x.Alias; - value.Name = x.GetLocalized(y => y.Name, ctx.Projection.LanguageId ?? 0, true, false); - value.ColorSquaresRgb = x.ColorSquaresRgb; - value.PriceAdjustment = x.PriceAdjustment; - value.WeightAdjustment = x.WeightAdjustment; - value.IsPreSelected = x.IsPreSelected; - value.DisplayOrder = x.DisplayOrder; - value.ValueTypeId = x.ValueTypeId; - value.LinkedProductId = x.LinkedProductId; - value.Quantity = x.Quantity; - - value._Localized = GetLocalized(ctx, x, y => y.Name); - - return value; - }) - .ToList(); - - attribute._Localized = GetLocalized(ctx, pva.ProductAttribute, - x => x.Name, - x => x.Description); - - expando.Attribute = attribute; - - return expando; - } - - private dynamic ToExpando(ExportProfileTaskContext ctx, ProductVariantAttributeCombination pvac) - { - if (pvac == null) - return null; - - dynamic expando = new ExpandoObject(); - expando._Entity = pvac; - - expando.Id = pvac.Id; - expando.StockQuantity = pvac.StockQuantity; - expando.AllowOutOfStockOrders = pvac.AllowOutOfStockOrders; - expando.AttributesXml = pvac.AttributesXml; - expando.Sku = pvac.Sku; - expando.Gtin = pvac.Gtin; - expando.ManufacturerPartNumber = pvac.ManufacturerPartNumber; - expando.Price = pvac.Price; - expando.Length = pvac.Length; - expando.Width = pvac.Width; - expando.Height = pvac.Height; - expando.BasePriceAmount = pvac.BasePriceAmount; - expando.BasePriceBaseAmount = pvac.BasePriceBaseAmount; - expando.AssignedPictureIds = pvac.AssignedPictureIds; - expando.DeliveryTimeId = pvac.DeliveryTimeId; - expando.IsActive = pvac.IsActive; - - GetDeliveryTimeAndQuantityUnit(ctx, expando, pvac.DeliveryTimeId, pvac.QuantityUnitId); - - return expando; - } - - private dynamic ToExpando(ExportProfileTaskContext ctx, Manufacturer manufacturer) - { - if (manufacturer == null) - return null; - - dynamic expando = new ExpandoObject(); - expando._Entity = manufacturer; - - expando.Id = manufacturer.Id; - expando.Name = manufacturer.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - expando.SeName = manufacturer.GetSeName(ctx.Projection.LanguageId ?? 0, true, false); - expando.Description = manufacturer.GetLocalized(x => x.Description, ctx.Projection.LanguageId ?? 0, true, false); - expando.ManufacturerTemplateId = manufacturer.ManufacturerTemplateId; - expando.MetaKeywords = manufacturer.GetLocalized(x => x.MetaKeywords, ctx.Projection.LanguageId ?? 0, true, false); - expando.MetaDescription = manufacturer.GetLocalized(x => x.MetaDescription, ctx.Projection.LanguageId ?? 0, true, false); - expando.MetaTitle = manufacturer.GetLocalized(x => x.MetaTitle, ctx.Projection.LanguageId ?? 0, true, false); - expando.PictureId = manufacturer.PictureId; - expando.PageSize = manufacturer.PageSize; - expando.AllowCustomersToSelectPageSize = manufacturer.AllowCustomersToSelectPageSize; - expando.PageSizeOptions = manufacturer.PageSizeOptions; - expando.PriceRanges = manufacturer.PriceRanges; - expando.Published = manufacturer.Published; - expando.Deleted = manufacturer.Deleted; - expando.DisplayOrder = manufacturer.DisplayOrder; - expando.CreatedOnUtc = manufacturer.CreatedOnUtc; - expando.UpdatedOnUtc = manufacturer.UpdatedOnUtc; - - expando.Picture = null; - - expando._Localized = GetLocalized(ctx, manufacturer, - x => x.Name, - x => x.Description, - x => x.MetaKeywords, - x => x.MetaDescription, - x => x.MetaTitle); - - return expando; - } - - private dynamic ToExpando(ExportProfileTaskContext ctx, Category category) - { - if (category == null) - return null; - - dynamic expando = new ExpandoObject(); - expando._Entity = category; - - expando.Id = category.Id; - expando.Name = category.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - expando.SeName = category.GetSeName(ctx.Projection.LanguageId ?? 0, true, false); - expando.FullName = category.GetLocalized(x => x.FullName, ctx.Projection.LanguageId ?? 0, true, false); - expando.Description = category.GetLocalized(x => x.Description, ctx.Projection.LanguageId ?? 0, true, false); - expando.BottomDescription = category.GetLocalized(x => x.BottomDescription, ctx.Projection.LanguageId ?? 0, true, false); - expando.CategoryTemplateId = category.CategoryTemplateId; - expando.MetaKeywords = category.GetLocalized(x => x.MetaKeywords, ctx.Projection.LanguageId ?? 0, true, false); - expando.MetaDescription = category.GetLocalized(x => x.MetaDescription, ctx.Projection.LanguageId ?? 0, true, false); - expando.MetaTitle = category.GetLocalized(x => x.MetaTitle, ctx.Projection.LanguageId ?? 0, true, false); - expando.ParentCategoryId = category.ParentCategoryId; - expando.PictureId = category.PictureId; - expando.PageSize = category.PageSize; - expando.AllowCustomersToSelectPageSize = category.AllowCustomersToSelectPageSize; - expando.PageSizeOptions = category.PageSizeOptions; - expando.PriceRanges = category.PriceRanges; - expando.ShowOnHomePage = category.ShowOnHomePage; - expando.HasDiscountsApplied = category.HasDiscountsApplied; - expando.Published = category.Published; - expando.Deleted = category.Deleted; - expando.DisplayOrder = category.DisplayOrder; - expando.CreatedOnUtc = category.CreatedOnUtc; - expando.UpdatedOnUtc = category.UpdatedOnUtc; - expando.SubjectToAcl = category.SubjectToAcl; - expando.LimitedToStores = category.LimitedToStores; - expando.Alias = category.Alias; - expando.DefaultViewMode = category.DefaultViewMode; - - expando.Picture = null; - - expando._Localized = GetLocalized(ctx, category, - x => x.Name, - x => x.FullName, - x => x.Description, - x => x.BottomDescription, - x => x.MetaKeywords, - x => x.MetaDescription, - x => x.MetaTitle); - - return expando; - } - - private dynamic ToExpando(ExportProfileTaskContext ctx, Product product) - { - if (product == null) - return null; - - dynamic expando = new ExpandoObject(); - expando._Entity = product; - - expando.Id = product.Id; - expando.ProductTypeId = product.ProductTypeId; - expando.ParentGroupedProductId = product.ParentGroupedProductId; - expando.VisibleIndividually = product.VisibleIndividually; - expando.Name = product.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - expando.SeName = product.GetSeName(ctx.Projection.LanguageId ?? 0, true, false); - expando.ShortDescription = product.GetLocalized(x => x.ShortDescription, ctx.Projection.LanguageId ?? 0, true, false); - expando.FullDescription = product.GetLocalized(x => x.FullDescription, ctx.Projection.LanguageId ?? 0, true, false); - expando.AdminComment = product.AdminComment; - expando.ProductTemplateId = product.ProductTemplateId; - expando.ShowOnHomePage = product.ShowOnHomePage; - expando.HomePageDisplayOrder = product.HomePageDisplayOrder; - expando.MetaKeywords = product.GetLocalized(x => x.MetaKeywords, ctx.Projection.LanguageId ?? 0, true, false); - expando.MetaDescription = product.GetLocalized(x => x.MetaDescription, ctx.Projection.LanguageId ?? 0, true, false); - expando.MetaTitle = product.GetLocalized(x => x.MetaTitle, ctx.Projection.LanguageId ?? 0, true, false); - expando.AllowCustomerReviews = product.AllowCustomerReviews; - expando.ApprovedRatingSum = product.ApprovedRatingSum; - expando.NotApprovedRatingSum = product.NotApprovedRatingSum; - expando.ApprovedTotalReviews = product.ApprovedTotalReviews; - expando.NotApprovedTotalReviews = product.NotApprovedTotalReviews; - expando.SubjectToAcl = product.SubjectToAcl; - expando.LimitedToStores = product.LimitedToStores; - expando.Sku = product.Sku; - expando.ManufacturerPartNumber = product.ManufacturerPartNumber; - expando.Gtin = product.Gtin; - expando.IsGiftCard = product.IsGiftCard; - expando.GiftCardTypeId = product.GiftCardTypeId; - expando.RequireOtherProducts = product.RequireOtherProducts; - expando.RequiredProductIds = product.RequiredProductIds; - expando.AutomaticallyAddRequiredProducts = product.AutomaticallyAddRequiredProducts; - expando.IsDownload = product.IsDownload; - expando.DownloadId = product.DownloadId; - expando.UnlimitedDownloads = product.UnlimitedDownloads; - expando.MaxNumberOfDownloads = product.MaxNumberOfDownloads; - expando.DownloadExpirationDays = product.DownloadExpirationDays; - expando.DownloadActivationTypeId = product.DownloadActivationTypeId; - expando.HasSampleDownload = product.HasSampleDownload; - expando.SampleDownloadId = product.SampleDownloadId; - expando.HasUserAgreement = product.HasUserAgreement; - expando.UserAgreementText = product.UserAgreementText; - expando.IsRecurring = product.IsRecurring; - expando.RecurringCycleLength = product.RecurringCycleLength; - expando.RecurringCyclePeriodId = product.RecurringCyclePeriodId; - expando.RecurringTotalCycles = product.RecurringTotalCycles; - expando.IsShipEnabled = product.IsShipEnabled; - expando.IsFreeShipping = product.IsFreeShipping; - expando.AdditionalShippingCharge = product.AdditionalShippingCharge; - expando.IsTaxExempt = product.IsTaxExempt; - expando.IsEsd = product.IsEsd; - expando.TaxCategoryId = product.TaxCategoryId; - expando.ManageInventoryMethodId = product.ManageInventoryMethodId; - expando.StockQuantity = product.StockQuantity; - expando.DisplayStockAvailability = product.DisplayStockAvailability; - expando.DisplayStockQuantity = product.DisplayStockQuantity; - expando.MinStockQuantity = product.MinStockQuantity; - expando.LowStockActivityId = product.LowStockActivityId; - expando.NotifyAdminForQuantityBelow = product.NotifyAdminForQuantityBelow; - expando.BackorderModeId = product.BackorderModeId; - expando.AllowBackInStockSubscriptions = product.AllowBackInStockSubscriptions; - expando.OrderMinimumQuantity = product.OrderMinimumQuantity; - expando.OrderMaximumQuantity = product.OrderMaximumQuantity; - expando.AllowedQuantities = product.AllowedQuantities; - expando.DisableBuyButton = product.DisableBuyButton; - expando.DisableWishlistButton = product.DisableWishlistButton; - expando.AvailableForPreOrder = product.AvailableForPreOrder; - expando.CallForPrice = product.CallForPrice; - expando.Price = product.Price; - expando.OldPrice = product.OldPrice; - expando.ProductCost = product.ProductCost; - expando.SpecialPrice = product.SpecialPrice; - expando.SpecialPriceStartDateTimeUtc = product.SpecialPriceStartDateTimeUtc; - expando.SpecialPriceEndDateTimeUtc = product.SpecialPriceEndDateTimeUtc; - expando.CustomerEntersPrice = product.CustomerEntersPrice; - expando.MinimumCustomerEnteredPrice = product.MinimumCustomerEnteredPrice; - expando.MaximumCustomerEnteredPrice = product.MaximumCustomerEnteredPrice; - expando.HasTierPrices = product.HasTierPrices; - expando.LowestAttributeCombinationPrice = product.LowestAttributeCombinationPrice; - expando.HasDiscountsApplied = product.HasDiscountsApplied; - expando.Weight = product.Weight; - expando.Length = product.Length; - expando.Width = product.Width; - expando.Height = product.Height; - expando.AvailableStartDateTimeUtc = product.AvailableStartDateTimeUtc; - expando.AvailableEndDateTimeUtc = product.AvailableEndDateTimeUtc; - expando.DisplayOrder = product.DisplayOrder; - expando.Published = product.Published; - expando.Deleted = product.Deleted; - expando.CreatedOnUtc = product.CreatedOnUtc; - expando.UpdatedOnUtc = product.UpdatedOnUtc; - expando.DeliveryTimeId = product.DeliveryTimeId; - expando.QuantityUnitId = product.QuantityUnitId; - expando.BasePriceEnabled = product.BasePriceEnabled; - expando.BasePriceMeasureUnit = product.BasePriceMeasureUnit; - expando.BasePriceAmount = product.BasePriceAmount; - expando.BasePriceBaseAmount = product.BasePriceBaseAmount; - expando.BasePriceHasValue = product.BasePriceHasValue; - expando.BundleTitleText = product.GetLocalized(x => x.BundleTitleText, ctx.Projection.LanguageId ?? 0, true, false); - expando.BundlePerItemShipping = product.BundlePerItemShipping; - expando.BundlePerItemPricing = product.BundlePerItemPricing; - expando.BundlePerItemShoppingCart = product.BundlePerItemShoppingCart; - - expando.AppliedDiscounts = null; - expando.TierPrices = null; - expando.ProductAttributes = null; - expando.ProductAttributeCombinations = null; - expando.ProductPictures = null; - expando.ProductCategories = null; - expando.ProductManufacturers = null; - expando.ProductTags = null; - expando.ProductSpecificationAttributes = null; - expando.ProductBundleItems = null; - - expando._Localized = GetLocalized(ctx, product, - x => x.Name, - x => x.ShortDescription, - x => x.FullDescription, - x => x.MetaKeywords, - x => x.MetaDescription, - x => x.MetaTitle, - x => x.BundleTitleText); - - return expando; - } - - private dynamic ToExpando(ExportProfileTaskContext ctx, Order order) - { - if (order == null) - return null; - - dynamic expando = new ExpandoObject(); - expando._Entity = order; - - expando.Id = order.Id; - expando.OrderNumber = order.GetOrderNumber(); - expando.OrderGuid = order.OrderGuid; - expando.StoreId = order.StoreId; - expando.CustomerId = order.CustomerId; - expando.BillingAddressId = order.BillingAddressId; - expando.ShippingAddressId = order.ShippingAddressId; - expando.OrderStatusId = order.OrderStatusId; - expando.ShippingStatusId = order.ShippingStatusId; - expando.PaymentStatusId = order.PaymentStatusId; - expando.PaymentMethodSystemName = order.PaymentMethodSystemName; - expando.CustomerCurrencyCode = order.CustomerCurrencyCode; - expando.CurrencyRate = order.CurrencyRate; - expando.CustomerTaxDisplayTypeId = order.CustomerTaxDisplayTypeId; - expando.VatNumber = order.VatNumber; - expando.OrderSubtotalInclTax = order.OrderSubtotalInclTax; - expando.OrderSubtotalExclTax = order.OrderSubtotalExclTax; - expando.OrderSubTotalDiscountInclTax = order.OrderSubTotalDiscountInclTax; - expando.OrderSubTotalDiscountExclTax = order.OrderSubTotalDiscountExclTax; - expando.OrderShippingInclTax = order.OrderShippingInclTax; - expando.OrderShippingExclTax = order.OrderShippingExclTax; - expando.OrderShippingTaxRate = order.OrderShippingTaxRate; - expando.PaymentMethodAdditionalFeeInclTax = order.PaymentMethodAdditionalFeeInclTax; - expando.PaymentMethodAdditionalFeeExclTax = order.PaymentMethodAdditionalFeeExclTax; - expando.PaymentMethodAdditionalFeeTaxRate = order.PaymentMethodAdditionalFeeTaxRate; - expando.TaxRates = order.TaxRates; - expando.OrderTax = order.OrderTax; - expando.OrderDiscount = order.OrderDiscount; - expando.OrderTotal = order.OrderTotal; - expando.RefundedAmount = order.RefundedAmount; - expando.RewardPointsWereAdded = order.RewardPointsWereAdded; - expando.CheckoutAttributeDescription = order.CheckoutAttributeDescription; - expando.CheckoutAttributesXml = order.CheckoutAttributesXml; - expando.CustomerLanguageId = order.CustomerLanguageId; - expando.AffiliateId = order.AffiliateId; - expando.CustomerIp = order.CustomerIp; - expando.AllowStoringCreditCardNumber = order.AllowStoringCreditCardNumber; - expando.CardType = order.CardType; - expando.CardName = order.CardName; - expando.CardNumber = order.CardNumber; - expando.MaskedCreditCardNumber = order.MaskedCreditCardNumber; - expando.CardCvv2 = order.CardCvv2; - expando.CardExpirationMonth = order.CardExpirationMonth; - expando.CardExpirationYear = order.CardExpirationYear; - expando.AllowStoringDirectDebit = order.AllowStoringDirectDebit; - expando.DirectDebitAccountHolder = order.DirectDebitAccountHolder; - expando.DirectDebitAccountNumber = order.DirectDebitAccountNumber; - expando.DirectDebitBankCode = order.DirectDebitBankCode; - expando.DirectDebitBankName = order.DirectDebitBankName; - expando.DirectDebitBIC = order.DirectDebitBIC; - expando.DirectDebitCountry = order.DirectDebitCountry; - expando.DirectDebitIban = order.DirectDebitIban; - expando.CustomerOrderComment = order.CustomerOrderComment; - expando.AuthorizationTransactionId = order.AuthorizationTransactionId; - expando.AuthorizationTransactionCode = order.AuthorizationTransactionCode; - expando.AuthorizationTransactionResult = order.AuthorizationTransactionResult; - expando.CaptureTransactionId = order.CaptureTransactionId; - expando.CaptureTransactionResult = order.CaptureTransactionResult; - expando.SubscriptionTransactionId = order.SubscriptionTransactionId; - expando.PurchaseOrderNumber = order.PurchaseOrderNumber; - expando.PaidDateUtc = order.PaidDateUtc; - expando.ShippingMethod = order.ShippingMethod; - expando.ShippingRateComputationMethodSystemName = order.ShippingRateComputationMethodSystemName; - expando.Deleted = order.Deleted; - expando.CreatedOnUtc = order.CreatedOnUtc; - expando.UpdatedOnUtc = order.UpdatedOnUtc; - expando.RewardPointsRemaining = order.RewardPointsRemaining; - expando.HasNewPaymentNotification = order.HasNewPaymentNotification; - expando.OrderStatus = order.OrderStatus.GetLocalizedEnum(_services.Localization, ctx.Projection.LanguageId ?? 0); - expando.PaymentStatus = order.PaymentStatus.GetLocalizedEnum(_services.Localization, ctx.Projection.LanguageId ?? 0); - expando.ShippingStatus = order.ShippingStatus.GetLocalizedEnum(_services.Localization, ctx.Projection.LanguageId ?? 0); - - expando.Customer = null; - expando.BillingAddress = null; - expando.ShippingAddress = null; - expando.Store = null; - expando.Shipments = null; - expando.RedeemedRewardPointsEntry = ToExpando(ctx, order.RedeemedRewardPointsEntry); - - return expando; - } - - private dynamic ToExpando(ExportProfileTaskContext ctx, OrderItem orderItem) - { - if (orderItem == null) - return null; - - dynamic expando = new ExpandoObject(); - expando._Entity = orderItem; - - expando.Id = orderItem.Id; - expando.OrderItemGuid = orderItem.OrderItemGuid; - expando.OrderId = orderItem.OrderId; - expando.ProductId = orderItem.ProductId; - expando.Quantity = orderItem.Quantity; - expando.UnitPriceInclTax = orderItem.UnitPriceInclTax; - expando.UnitPriceExclTax = orderItem.UnitPriceExclTax; - expando.PriceInclTax = orderItem.PriceInclTax; - expando.PriceExclTax = orderItem.PriceExclTax; - expando.TaxRate = orderItem.TaxRate; - expando.DiscountAmountInclTax = orderItem.DiscountAmountInclTax; - expando.DiscountAmountExclTax = orderItem.DiscountAmountExclTax; - expando.AttributeDescription = orderItem.AttributeDescription; - expando.AttributesXml = orderItem.AttributesXml; - expando.DownloadCount = orderItem.DownloadCount; - expando.IsDownloadActivated = orderItem.IsDownloadActivated; - expando.LicenseDownloadId = orderItem.LicenseDownloadId; - expando.ItemWeight = orderItem.ItemWeight; - expando.BundleData = orderItem.BundleData; - expando.ProductCost = orderItem.ProductCost; - - expando.Product = ToExpando(ctx, orderItem.Product); - - return expando; - } - - private dynamic ToExpando(ExportProfileTaskContext ctx, Shipment shipment) - { - if (shipment == null) - return null; - - dynamic expando = new ExpandoObject(); - expando._Entity = shipment; - - expando.Id = shipment.Id; - expando.OrderId = shipment.OrderId; - expando.TrackingNumber = shipment.TrackingNumber; - expando.TotalWeight = shipment.TotalWeight; - expando.ShippedDateUtc = shipment.ShippedDateUtc; - expando.DeliveryDateUtc = shipment.DeliveryDateUtc; - expando.CreatedOnUtc = shipment.CreatedOnUtc; - - expando.ShipmentItems = shipment.ShipmentItems - .Select(x => - { - dynamic exp = new ExpandoObject(); - exp._Entity = x; - exp.Id = x.Id; - exp.ShipmentId = x.ShipmentId; - exp.OrderItemId = x.OrderItemId; - exp.Quantity = x.Quantity; - return exp; - }) - .ToList(); - - return expando; - } - - private dynamic ToExpando(ExportProfileTaskContext ctx, Discount discount) - { - if (discount == null) - return null; - - dynamic expando = new ExpandoObject(); - expando._Entity = discount; - - expando.Id = discount.Id; - expando.Name = discount.Name; - expando.DiscountTypeId = discount.DiscountTypeId; - expando.UsePercentage = discount.UsePercentage; - expando.DiscountPercentage = discount.DiscountPercentage; - expando.DiscountAmount = discount.DiscountAmount; - expando.StartDateUtc = discount.StartDateUtc; - expando.EndDateUtc = discount.EndDateUtc; - expando.RequiresCouponCode = discount.RequiresCouponCode; - expando.CouponCode = discount.CouponCode; - expando.DiscountLimitationId = discount.DiscountLimitationId; - expando.LimitationTimes = discount.LimitationTimes; - - return expando; - } - - private dynamic ToExpando(ExportProfileTaskContext ctx, ProductSpecificationAttribute psa) - { - if (psa == null) - return null; - - var option = psa.SpecificationAttributeOption; - - dynamic expando = new ExpandoObject(); - expando._Entity = psa; - - expando.Id = psa.Id; - expando.ProductId = psa.ProductId; - expando.SpecificationAttributeOptionId = psa.SpecificationAttributeOptionId; - expando.AllowFiltering = psa.AllowFiltering; - expando.ShowOnProductPage = psa.ShowOnProductPage; - expando.DisplayOrder = psa.DisplayOrder; - - dynamic expAttribute = new ExpandoObject(); - expAttribute._Entity = option.SpecificationAttribute; - expAttribute.Id = option.SpecificationAttribute.Id; - expAttribute.Name = option.SpecificationAttribute.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - expAttribute.DisplayOrder = option.SpecificationAttribute.DisplayOrder; - expAttribute._Localized = GetLocalized(ctx, option.SpecificationAttribute, x => x.Name); - - dynamic expOption = new ExpandoObject(); - expOption._Entity = option; - expOption.Id = option.Id; - expOption.SpecificationAttributeId = option.SpecificationAttributeId; - expOption.Name = option.GetLocalized(x => x.Name, ctx.Projection.LanguageId ?? 0, true, false); - expOption.DisplayOrder = option.DisplayOrder; - expOption._Localized = GetLocalized(ctx, option, x => x.Name); - expOption.SpecificationAttribute = expAttribute; - - expando.SpecificationAttributeOption = expOption; - - return expando; - } - - private dynamic ToExpando(ExportProfileTaskContext ctx, GenericAttribute genericAttribute) - { - if (genericAttribute == null) - return null; - - dynamic expando = new ExpandoObject(); - expando._Entity = genericAttribute; - - expando.Id = genericAttribute.Id; - expando.EntityId = genericAttribute.EntityId; - expando.KeyGroup = genericAttribute.KeyGroup; - expando.Key = genericAttribute.Key; - expando.Value = genericAttribute.Value; - expando.StoreId = genericAttribute.StoreId; - - return expando; - } - - private dynamic ToExpando(ExportProfileTaskContext ctx, NewsLetterSubscription subscription) - { - if (subscription == null) - return null; - - dynamic expando = new ExpandoObject(); - expando._Entity = subscription; - - expando.Id = subscription.Id; - expando.NewsLetterSubscriptionGuid = subscription.NewsLetterSubscriptionGuid; - expando.Email = subscription.Email; - expando.Active = subscription.Active; - expando.CreatedOnUtc = subscription.CreatedOnUtc; - expando.StoreId = subscription.StoreId; - - return expando; - } - - private List ConvertToExpando(ExportProfileTaskContext ctx, Product product) - { - var result = new List(); - - var languageId = (ctx.Projection.LanguageId ?? 0); - var productTemplate = ctx.ProductTemplates.FirstOrDefault(x => x.Key == product.ProductTemplateId); - var pictureSize = _mediaSettings.Value.ProductDetailsPictureSize; - - if (ctx.Supporting[ExportSupport.ProjectionMainPictureUrl] && ctx.Projection.PictureSize > 0) - pictureSize = ctx.Projection.PictureSize; - - var perfLoadId = (ctx.IsPreview ? 0 : product.Id); // perf preview (it's a compromise) - var productPictures = ctx.DataContextProduct.ProductPictures.Load(perfLoadId); - var productManufacturers = ctx.DataContextProduct.ProductManufacturers.Load(perfLoadId); - var productCategories = ctx.DataContextProduct.ProductCategories.Load(product.Id); - var productAttributes = ctx.DataContextProduct.Attributes.Load(product.Id); - var productAttributeCombinations = ctx.DataContextProduct.AttributeCombinations.Load(product.Id); - var productTags = ctx.DataContextProduct.ProductTags.Load(perfLoadId); - var specificationAttributes = ctx.DataContextProduct.ProductSpecificationAttributes.Load(perfLoadId); - - dynamic expando = ToExpando(ctx, product); - - #region gerneral data - - expando._ProductTemplateViewPath = (productTemplate.Value == null ? "" : productTemplate.Value.ViewPath); - - expando._DetailUrl = ctx.Store.Url.EnsureEndsWith("/") + (string)expando.SeName; - - expando._CategoryName = null; - - if (ctx.Categories.Count > 0 && ctx.CategoryPathes.Count > 0) - { - expando._CategoryPath = _categoryService.Value.GetCategoryPath( - product, - null, - x => ctx.CategoryPathes.ContainsKey(x) ? ctx.CategoryPathes[x] : null, - (id, value) => ctx.CategoryPathes[id] = value, - x => ctx.Categories.ContainsKey(x) ? ctx.Categories[x] : _categoryService.Value.GetCategoryById(x), - productCategories.OrderBy(x => x.DisplayOrder).FirstOrDefault() - ); - } - else - { - expando._CategoryPath = null; - } - - expando.ProductPictures = productPictures - .OrderBy(x => x.DisplayOrder) - .Select(x => - { - dynamic exp = new ExpandoObject(); - exp._Entity = x; - exp.Id = x.Id; - exp.ProductId = x.ProductId; - exp.DisplayOrder = x.DisplayOrder; - exp.PictureId = x.PictureId; - exp.Picture = ToExpando(ctx, x.Picture, _mediaSettings.Value.ProductThumbPictureSize, pictureSize); - - return exp; - }) - .ToList(); - - expando.ProductManufacturers = productManufacturers - .OrderBy(x => x.DisplayOrder) - .Select(x => - { - dynamic exp = new ExpandoObject(); - exp._Entity = x; - exp.Id = x.Id; - exp.ProductId = x.ProductId; - exp.DisplayOrder = x.DisplayOrder; - exp.IsFeaturedProduct = x.IsFeaturedProduct; - exp.ManufacturerId = x.ManufacturerId; - exp.Manufacturer = ToExpando(ctx, x.Manufacturer); - - if (x.Manufacturer != null && x.Manufacturer.PictureId.HasValue) - exp.Manufacturer.Picture = ToExpando(ctx, x.Manufacturer.Picture, _mediaSettings.Value.ManufacturerThumbPictureSize, _mediaSettings.Value.ManufacturerThumbPictureSize); - else - exp.Manufacturer.Picture = null; - - return exp; - }) - .ToList(); - - expando.ProductCategories = productCategories - .OrderBy(x => x.DisplayOrder) - .Select(x => - { - dynamic exp = new ExpandoObject(); - exp._Entity = x; - exp.Id = x.Id; - exp.ProductId = x.ProductId; - exp.DisplayOrder = x.DisplayOrder; - exp.IsFeaturedProduct = x.IsFeaturedProduct; - exp.CategoryId = x.CategoryId; - exp.Category = ToExpando(ctx, x.Category); - - if (x.Category != null && x.Category.PictureId.HasValue) - exp.Category.Picture = ToExpando(ctx, x.Category.Picture, _mediaSettings.Value.CategoryThumbPictureSize, _mediaSettings.Value.CategoryThumbPictureSize); - - if (expando._CategoryName == null) - expando._CategoryName = (string)exp.Category.Name; - - return exp; - }) - .ToList(); - - expando.ProductAttributes = productAttributes - .OrderBy(x => x.DisplayOrder) - .Select(x => ToExpando(ctx, x)) - .ToList(); - - expando.ProductAttributeCombinations = productAttributeCombinations - .Select(x => - { - dynamic exp = ToExpando(ctx, x); - var assignedPictures = new List(); - - foreach (int pictureId in x.GetAssignedPictureIds()) - { - var assignedPicture = productPictures.FirstOrDefault(y => y.PictureId == pictureId); - if (assignedPicture != null && assignedPicture.Picture != null) - { - assignedPictures.Add(ToExpando(ctx, assignedPicture.Picture, _mediaSettings.Value.ProductThumbPictureSize, pictureSize)); - } - } - - exp.Pictures = assignedPictures; - - return exp; - }) - .ToList(); - - if (product.HasTierPrices) - { - var tierPrices = ctx.DataContextProduct.TierPrices.Load(product.Id) - .RemoveDuplicatedQuantities(); - - expando.TierPrices = tierPrices - .Select(x => - { - dynamic exp = new ExpandoObject(); - exp._Entity = x; - exp.Id = x.Id; - exp.ProductId = x.ProductId; - exp.StoreId = x.StoreId; - exp.CustomerRoleId = x.CustomerRoleId; - exp.Quantity = x.Quantity; - exp.Price = x.Price; - return exp; - }) - .ToList(); - } - - if (product.HasDiscountsApplied) - { - var appliedDiscounts = ctx.DataContextProduct.AppliedDiscounts.Load(product.Id); - - expando.AppliedDiscounts = appliedDiscounts - .Select(x => ToExpando(ctx, x)) - .ToList(); - } - - expando.ProductTags = productTags - .Select(x => - { - dynamic exp = new ExpandoObject(); - exp._Entity = x; - exp.Id = x.Id; - exp.Name = x.GetLocalized(y => y.Name, languageId, true, false); - exp.SeName = x.GetSeName(languageId); - exp._Localized = GetLocalized(ctx, x, y => y.Name); - return exp; - }) - .ToList(); - - expando.ProductSpecificationAttributes = specificationAttributes - .Select(x => ToExpando(ctx, x)) - .ToList(); - - if (product.ProductType == ProductType.BundledProduct) - { - var bundleItems = ctx.DataContextProduct.ProductBundleItems.Load(perfLoadId); - - expando.ProductBundleItems = bundleItems - .Select(x => - { - dynamic exp = new ExpandoObject(); - exp._Entity = x; - exp.Id = x.Id; - exp.ProductId = x.ProductId; - exp.BundleProductId = x.BundleProductId; - exp.Quantity = x.Quantity; - exp.Discount = x.Discount; - exp.DiscountPercentage = x.DiscountPercentage; - exp.Name = x.GetLocalized(y => y.Name, languageId, true, false); - exp.ShortDescription = x.GetLocalized(y => y.ShortDescription, languageId, true, false); - exp.FilterAttributes = x.FilterAttributes; - exp.HideThumbnail = x.HideThumbnail; - exp.Visible = x.Visible; - exp.Published = x.Published; - exp.DisplayOrder = x.DisplayOrder; - exp.CreatedOnUtc = x.CreatedOnUtc; - exp.UpdatedOnUtc = x.UpdatedOnUtc; - exp._Localized = GetLocalized(ctx, x, y => y.Name, y => y.ShortDescription); - return exp; - }) - .ToList(); - } - - #endregion - - #region more attribute controlled data - - if (ctx.Supports(ExportSupport.ProjectionDescription)) - { - PrepareProductDescription(ctx, expando, product); - } - - if (ctx.Supports(ExportSupport.ProjectionBrand)) - { - string brand = null; - var productManus = ctx.DataContextProduct.ProductManufacturers.Load(perfLoadId); - - if (productManus != null && productManus.Any()) - brand = productManus.First().Manufacturer.GetLocalized(x => x.Name, languageId, true, false); - - if (brand.IsEmpty()) - brand = ctx.Projection.Brand; - - expando._Brand = brand; - } - - if (ctx.Supports(ExportSupport.ProjectionMainPictureUrl)) - { - if (productPictures != null && productPictures.Any()) - expando._MainPictureUrl = _pictureService.Value.GetPictureUrl(productPictures.First().Picture, ctx.Projection.PictureSize, storeLocation: ctx.Store.Url); - else - expando._MainPictureUrl = _pictureService.Value.GetDefaultPictureUrl(ctx.Projection.PictureSize, storeLocation: ctx.Store.Url); - } - - #endregion - - #region matter of data merging - - Action matterOfDataMerging = (exp, combination) => - { - product.MergeWithCombination(combination); - - exp.Price = CalculatePrice(ctx, product, combination != null); - exp.StockQuantity = product.StockQuantity; - exp.BackorderModeId = product.BackorderModeId; - exp.Sku = product.Sku; - exp.Gtin = product.Gtin; - exp.ManufacturerPartNumber = product.ManufacturerPartNumber; - exp.DeliveryTimeId = product.DeliveryTimeId; - exp.QuantityUnitId = product.QuantityUnitId; - exp.Length = product.Length; - exp.Width = product.Width; - exp.Height = product.Height; - exp.BasePriceAmount = product.BasePriceAmount; - exp.BasePriceBaseAmount = product.BasePriceBaseAmount; - - if (combination != null && ctx.Projection.AttributeCombinationValueMerging == ExportAttributeValueMerging.AppendAllValuesToName) - { - var values = _productAttributeParser.Value.ParseProductVariantAttributeValues(combination.AttributesXml, productAttributes, languageId); - exp.Name = ((string)exp.Name).Grow(string.Join(", ", values), " "); - } - - exp._BasePriceInfo = product.GetBasePriceInfo(_services.Localization, _priceFormatter.Value, _currencyService.Value, _taxService.Value, - _priceCalculationService.Value, ctx.ContextCurrency, decimal.Zero, true); - - // navigation properties - GetDeliveryTimeAndQuantityUnit(ctx, exp, product.DeliveryTimeId, product.QuantityUnitId); - - if (ctx.Supports(ExportSupport.ProjectionUseOwnProductNo) && product.ManufacturerPartNumber.IsEmpty()) - { - exp.ManufacturerPartNumber = product.Sku; - } - - if (ctx.Supports(ExportSupport.ProjectionShippingTime)) - { - dynamic deliveryTime = exp.DeliveryTime; - exp._ShippingTime = (deliveryTime == null ? ctx.Projection.ShippingTime : deliveryTime.Name); - } - - if (ctx.Supports(ExportSupport.ProjectionShippingCosts)) - { - exp._FreeShippingThreshold = ctx.Projection.FreeShippingThreshold; - - if (product.IsFreeShipping || (ctx.Projection.FreeShippingThreshold.HasValue && (decimal)exp.Price >= ctx.Projection.FreeShippingThreshold.Value)) - exp._ShippingCosts = decimal.Zero; - else - exp._ShippingCosts = ctx.Projection.ShippingCosts; - } - - if (ctx.Supports(ExportSupport.ProjectionOldPrice)) - { - if (product.OldPrice != decimal.Zero && product.OldPrice != (decimal)exp.Price && !(product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing)) - { - if (ctx.Projection.ConvertNetToGrossPrices) - { - decimal taxRate; - exp._OldPrice = _taxService.Value.GetProductPrice(product, product.OldPrice, true, ctx.ContextCustomer, out taxRate); - } - else - { - exp._OldPrice = product.OldPrice; - } - } - else - { - exp._OldPrice = null; - } - } - - if (ctx.Supports(ExportSupport.ProjectionSpecialPrice)) - { - exp._SpecialPrice = null; - exp._RegularPrice = null; // price if a special price would not exist - - if (!(product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing)) - { - var specialPrice = _priceCalculationService.Value.GetSpecialPrice(product); - - exp._SpecialPrice = ConvertPrice(ctx, product, specialPrice); - - if (specialPrice.HasValue) - { - decimal tmpSpecialPrice = product.SpecialPrice.Value; - product.SpecialPrice = null; - exp._RegularPrice = CalculatePrice(ctx, product, combination != null); - product.SpecialPrice = tmpSpecialPrice; - } - } - } - }; - - #endregion - - if (!ctx.IsPreview && ctx.Projection.AttributeCombinationAsProduct && productAttributeCombinations.Where(x => x.IsActive).Count() > 0) - { - // EF does not support entities to be constructed in a LINQ to entities query. - // So it's not possible to join-query attribute combinations and products without losing ProductSearchContext ability. - // We are reduced to somewhat compound data here. - - foreach (var combination in productAttributeCombinations.Where(x => x.IsActive)) - { - var expandoCombination = ((IDictionary)expando).ToExpandoObject(); // clone - - matterOfDataMerging(expandoCombination, combination); - result.Add(expandoCombination); - } - } - else - { - matterOfDataMerging(expando, null); - result.Add(expando); - } - - return result; - } - - private List ConvertToExpando(ExportProfileTaskContext ctx, Order order) - { - var result = new List(); - - ctx.DataContextOrder.Addresses.Collect(order.ShippingAddressId ?? 0); - - var perfLoadId = (ctx.IsPreview ? 0 : order.Id); - var addresses = ctx.DataContextOrder.Addresses.Load(ctx.IsPreview ? 0 : order.BillingAddressId); - var customers = ctx.DataContextOrder.Customers.Load(order.CustomerId); - var rewardPointsHistories = ctx.DataContextOrder.RewardPointsHistories.Load(ctx.IsPreview ? 0 : order.CustomerId); - var orderItems = ctx.DataContextOrder.OrderItems.Load(perfLoadId); - var shipments = ctx.DataContextOrder.Shipments.Load(perfLoadId); - - dynamic expando = ToExpando(ctx, order); - - if (ctx.Stores.ContainsKey(order.StoreId)) - { - expando.Store = ToExpando(ctx, ctx.Stores[order.StoreId]); - } - - expando.Customer = ToExpando(ctx, customers.FirstOrDefault(x => x.Id == order.CustomerId)); - - expando.Customer.RewardPointsHistory = rewardPointsHistories - .Select(x => ToExpando(ctx, x)) - .ToList(); - - if (rewardPointsHistories.Count > 0) - { - expando.Customer._RewardPointsBalance = rewardPointsHistories - .OrderByDescending(x => x.CreatedOnUtc) - .ThenByDescending(x => x.Id) - .FirstOrDefault() - .PointsBalance; - } - - expando.BillingAddress = ToExpando(ctx, addresses.FirstOrDefault(x => x.Id == order.BillingAddressId)); - - if (order.ShippingAddressId.HasValue) - { - expando.ShippingAddress = ToExpando(ctx, addresses.FirstOrDefault(x => x.Id == order.ShippingAddressId.Value)); - } - - expando.OrderItems = orderItems - .Select(e => - { - dynamic exp = ToExpando(ctx, e); - var productTemplate = ctx.ProductTemplates.FirstOrDefault(x => x.Key == e.Product.ProductTemplateId); - - exp.Product._ProductTemplateViewPath = (productTemplate.Value == null ? "" : productTemplate.Value.ViewPath); - - exp.Product._BasePriceInfo = e.Product.GetBasePriceInfo(_services.Localization, _priceFormatter.Value, _currencyService.Value, _taxService.Value, - _priceCalculationService.Value, ctx.ContextCurrency, decimal.Zero, true); - - GetDeliveryTimeAndQuantityUnit(ctx, exp.Product, e.Product.DeliveryTimeId, e.Product.QuantityUnitId); - - return exp; - }) - .ToList(); - - expando.Shipments = shipments - .Select(x => ToExpando(ctx, x)) - .ToList(); - - result.Add(expando); - return result; - } - - private List ConvertToExpando(ExportProfileTaskContext ctx, Manufacturer manu) - { - var result = new List(); - - var productManufacturers = ctx.DataContextManufacturer.ProductManufacturers.Load(manu.Id); - - dynamic expando = ToExpando(ctx, manu); - - if (!ctx.IsPreview && manu.PictureId.HasValue) - { - var pictures = ctx.DataContextManufacturer.Pictures.Load(manu.PictureId.Value); - - if (pictures.Count > 0) - expando.Picture = ToExpando(ctx, pictures.First(), _mediaSettings.Value.ManufacturerThumbPictureSize, _mediaSettings.Value.ManufacturerThumbPictureSize); - } - - expando.ProductManufacturers = productManufacturers - .OrderBy(x => x.DisplayOrder) - .Select(x => - { - dynamic exp = new ExpandoObject(); - exp._Entity = x; - exp.Id = x.Id; - exp.ProductId = x.ProductId; - exp.DisplayOrder = x.DisplayOrder; - exp.IsFeaturedProduct = x.IsFeaturedProduct; - exp.ManufacturerId = x.ManufacturerId; - - return exp; - }) - .ToList(); - - result.Add(expando); - return result; - } - - private List ConvertToExpando(ExportProfileTaskContext ctx, Category category) - { - var result = new List(); - - var productCategories = ctx.DataContextCategory.ProductCategories.Load(category.Id); - - dynamic expando = ToExpando(ctx, category); - - if (!ctx.IsPreview && category.PictureId.HasValue) - { - var pictures = ctx.DataContextCategory.Pictures.Load(category.PictureId.Value); - - if (pictures.Count > 0) - expando.Picture = ToExpando(ctx, pictures.First(), _mediaSettings.Value.CategoryThumbPictureSize, _mediaSettings.Value.CategoryThumbPictureSize); - } - - expando.ProductCategories = productCategories - .OrderBy(x => x.DisplayOrder) - .Select(x => - { - dynamic exp = new ExpandoObject(); - exp._Entity = x; - exp.Id = x.Id; - exp.ProductId = x.ProductId; - exp.DisplayOrder = x.DisplayOrder; - exp.IsFeaturedProduct = x.IsFeaturedProduct; - exp.CategoryId = x.CategoryId; - - return exp; - }) - .ToList(); - - result.Add(expando); - return result; - } - - private List ConvertToExpando(ExportProfileTaskContext ctx, Customer customer) - { - var result = new List(); - - var perfLoadId = (ctx.IsPreview ? 0 : customer.Id); - var genericAttributes = ctx.DataContextCustomer.GenericAttributes.Load(perfLoadId); - - dynamic expando = ToExpando(ctx, customer); - - expando.BillingAddress = ToExpando(ctx, customer.BillingAddress); - expando.ShippingAddress = ToExpando(ctx, customer.ShippingAddress); - - expando.Addresses = customer.Addresses - .Select(x => ToExpando(ctx, x)) - .ToList(); - - expando.CustomerRoles = customer.CustomerRoles - .Select(x => - { - dynamic exp = new ExpandoObject(); - exp._Entity = x; - exp.Id = x.Id; - exp.Name = x.Name; - exp.FreeShipping = x.FreeShipping; - exp.TaxExempt = x.TaxExempt; - exp.TaxDisplayType = x.TaxDisplayType; - exp.Active = x.Active; - exp.IsSystemRole = x.IsSystemRole; - exp.SystemName = x.SystemName; - - return exp; - }) - .ToList(); - - expando._GenericAttributes = genericAttributes - .Select(x => ToExpando(ctx, x)) - .ToList(); - - expando._HasNewsletterSubscription = ctx.NewsletterSubscriptions.Contains(customer.Email, StringComparer.CurrentCultureIgnoreCase); - - result.Add(expando); - return result; - } - - private List ConvertToExpando(ExportProfileTaskContext ctx, NewsLetterSubscription subscription) - { - var result = new List(); - - dynamic expando = ToExpando(ctx, subscription); - - result.Add(expando); - return result; - } - - #endregion - - #region Getting data - - private IQueryable GetProductQuery(ExportProfileTaskContext ctx, int skip, int take) - { - IQueryable query = null; - - if (ctx.QueryProducts == null) - { - var searchContext = new ProductSearchContext - { - OrderBy = ProductSortingEnum.CreatedOn, - ProductIds = ctx.EntityIdsSelected, - StoreId = (ctx.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId), - VisibleIndividuallyOnly = true, - PriceMin = ctx.Filter.PriceMinimum, - PriceMax = ctx.Filter.PriceMaximum, - IsPublished = ctx.Filter.IsPublished, - WithoutCategories = ctx.Filter.WithoutCategories, - WithoutManufacturers = ctx.Filter.WithoutManufacturers, - ManufacturerId = ctx.Filter.ManufacturerId ?? 0, - FeaturedProducts = ctx.Filter.FeaturedProducts, - ProductType = ctx.Filter.ProductType, - ProductTagId = ctx.Filter.ProductTagId ?? 0, - IdMin = ctx.Filter.IdMinimum ?? 0, - IdMax = ctx.Filter.IdMaximum ?? 0, - AvailabilityMinimum = ctx.Filter.AvailabilityMinimum, - AvailabilityMaximum = ctx.Filter.AvailabilityMaximum - }; - - if (!ctx.Filter.IsPublished.HasValue) - searchContext.ShowHidden = true; - - if (ctx.Filter.CategoryIds != null && ctx.Filter.CategoryIds.Length > 0) - searchContext.CategoryIds = ctx.Filter.CategoryIds.ToList(); - - if (ctx.Filter.CreatedFrom.HasValue) - searchContext.CreatedFromUtc = _dateTimeHelper.Value.ConvertToUtcTime(ctx.Filter.CreatedFrom.Value, _dateTimeHelper.Value.CurrentTimeZone); - - if (ctx.Filter.CreatedTo.HasValue) - searchContext.CreatedToUtc = _dateTimeHelper.Value.ConvertToUtcTime(ctx.Filter.CreatedTo.Value, _dateTimeHelper.Value.CurrentTimeZone); - - query = _productService.Value.PrepareProductSearchQuery(searchContext); - - query = query.OrderByDescending(x => x.CreatedOnUtc); - } - else - { - query = ctx.QueryProducts; - } - - if (skip > 0) - query = query.Skip(skip); - - if (take != int.MaxValue) - query = query.Take(take); - - return query; - } - - private List GetProducts(ExportProfileTaskContext ctx, int skip) - { - var result = new List(); - - var products = GetProductQuery(ctx, skip, PageSize).ToList(); - - foreach (var product in products) - { - if (product.ProductType == ProductType.SimpleProduct || product.ProductType == ProductType.BundledProduct) - { - result.Add(product); - } - else if (product.ProductType == ProductType.GroupedProduct) - { - if (ctx.Projection.NoGroupedProducts && !ctx.IsPreview) - { - var associatedSearchContext = new ProductSearchContext - { - OrderBy = ProductSortingEnum.CreatedOn, - PageSize = int.MaxValue, - StoreId = (ctx.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId), - VisibleIndividuallyOnly = false, - ParentGroupedProductId = product.Id - }; - - foreach (var associatedProduct in _productService.Value.SearchProducts(associatedSearchContext)) - { - result.Add(associatedProduct); - } - } - else - { - result.Add(product); - } - } - } - - try - { - SetProgress(ctx, products.Count); - - _services.DbContext.DetachEntities(result); - } - catch { } - - return result; - } - - private IQueryable GetOrderQuery(ExportProfileTaskContext ctx, int skip, int take) - { - var query = _orderService.Value.GetOrders( - ctx.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId, - ctx.Projection.CustomerId ?? 0, - ctx.Filter.CreatedFrom.HasValue ? (DateTime?)_dateTimeHelper.Value.ConvertToUtcTime(ctx.Filter.CreatedFrom.Value, _dateTimeHelper.Value.CurrentTimeZone) : null, - ctx.Filter.CreatedTo.HasValue ? (DateTime?)_dateTimeHelper.Value.ConvertToUtcTime(ctx.Filter.CreatedTo.Value, _dateTimeHelper.Value.CurrentTimeZone) : null, - ctx.Filter.OrderStatusIds, - ctx.Filter.PaymentStatusIds, - ctx.Filter.ShippingStatusIds, - null, - null, - null); - - if (ctx.EntityIdsSelected.Count > 0) - query = query.Where(x => ctx.EntityIdsSelected.Contains(x.Id)); - - query = query.OrderByDescending(x => x.CreatedOnUtc); - - if (skip > 0) - query = query.Skip(skip); - - if (take != int.MaxValue) - query = query.Take(take); - - return query; - } - - private List GetOrders(ExportProfileTaskContext ctx, int skip) - { - var orders = GetOrderQuery(ctx, skip, PageSize).ToList(); - - if (ctx.Projection.OrderStatusChange != ExportOrderStatusChange.None) - { - ctx.EntityIdsLoaded = ctx.EntityIdsLoaded - .Union(orders.Select(x => x.Id)) - .Distinct() - .ToList(); - } - - try - { - SetProgress(ctx, orders.Count); - - _services.DbContext.DetachEntities(orders); - } - catch { } - - return orders; - } - - private IQueryable GetManufacturerQuery(ExportProfileTaskContext ctx, int skip, int take) - { - var showHidden = !ctx.Filter.IsPublished.HasValue; - var storeId = (ctx.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId); - - var query = _manufacturerService.Value.GetManufacturers(showHidden, storeId); - - query = query.OrderBy(x => x.DisplayOrder); - - if (skip > 0) - query = query.Skip(skip); - - if (take != int.MaxValue) - query = query.Take(take); - - return query; - } - - private List GetManufacturers(ExportProfileTaskContext ctx, int skip) - { - var manus = GetManufacturerQuery(ctx, skip, PageSize).ToList(); - - try - { - SetProgress(ctx, manus.Count); - - _services.DbContext.DetachEntities(manus); - } - catch { } - - return manus; - } - - private IQueryable GetCategoryQuery(ExportProfileTaskContext ctx, int skip, int take) - { - var showHidden = !ctx.Filter.IsPublished.HasValue; - var storeId = (ctx.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId); - - var query = _categoryService.Value.GetCategories(null, showHidden, null, true, storeId); - - query = query - .OrderBy(x => x.ParentCategoryId) - .ThenBy(x => x.DisplayOrder); - - if (skip > 0) - query = query.Skip(skip); - - if (take != int.MaxValue) - query = query.Take(take); - - return query; - } - - private List GetCategories(ExportProfileTaskContext ctx, int skip) - { - var categories = GetCategoryQuery(ctx, skip, PageSize).ToList(); - - try - { - SetProgress(ctx, categories.Count); - - _services.DbContext.DetachEntities(categories); - } - catch { } - - return categories; - } - - private IQueryable GetCustomerQuery(ExportProfileTaskContext ctx, int skip, int take) - { - var query = _customerRepository.Value.TableUntracked - .Expand(x => x.BillingAddress) - .Expand(x => x.ShippingAddress) - .Expand(x => x.Addresses.Select(y => y.Country)) - .Expand(x => x.Addresses.Select(y => y.StateProvince)) - .Expand(x => x.CustomerRoles) - .Where(x => !x.Deleted); - - if (ctx.EntityIdsSelected.Count > 0) - { - query = query.Where(x => ctx.EntityIdsSelected.Contains(x.Id)); - } - - query = query.OrderByDescending(x => x.CreatedOnUtc); - - if (skip > 0) - query = query.Skip(skip); - - if (take != int.MaxValue) - query = query.Take(take); - - return query; - } - - private List GetCustomers(ExportProfileTaskContext ctx, int skip) - { - var customers = GetCustomerQuery(ctx, skip, PageSize).ToList(); - - try - { - SetProgress(ctx, customers.Count); - - _services.DbContext.DetachEntities(customers); - } - catch { } - - return customers; - } - - private IQueryable GetNewsLetterSubscriptionQuery(ExportProfileTaskContext ctx, int skip, int take) - { - var storeId = (ctx.Profile.PerStore ? ctx.Store.Id : ctx.Filter.StoreId); - - var query = _subscriptionRepository.Value.TableUntracked; - - if (storeId > 0) - { - query = query.Where(x => x.StoreId == storeId); - } - - query = query - .OrderBy(x => x.StoreId) - .ThenBy(x => x.Email); - - if (skip > 0) - query = query.Skip(skip); - - if (take != int.MaxValue) - query = query.Take(take); - - return query; - } - - private List GetNewsLetterSubscriptions(ExportProfileTaskContext ctx, int skip) - { - var subscriptions = GetNewsLetterSubscriptionQuery(ctx, skip, PageSize).ToList(); - - try - { - SetProgress(ctx, subscriptions.Count); - - _services.DbContext.DetachEntities(subscriptions); - } - catch { } - - return subscriptions; - } - - #endregion - - private List Init(ExportProfileTaskContext ctx, int? totalRecords = null) - { - // Init base things that are even required for preview. Init all other things (regular export) in ExportCoreOuter. - List result = null; - - if (ctx.Projection.CurrencyId.HasValue) - ctx.ContextCurrency = _currencyService.Value.GetCurrencyById(ctx.Projection.CurrencyId.Value); - else - ctx.ContextCurrency = _services.WorkContext.WorkingCurrency; - - if (ctx.Projection.CustomerId.HasValue) - ctx.ContextCustomer = _customerService.Value.GetCustomerById(ctx.Projection.CustomerId.Value); - else - ctx.ContextCustomer = _services.WorkContext.CurrentCustomer; - - if (ctx.Projection.LanguageId.HasValue) - ctx.ContextLanguage = _languageService.Value.GetLanguageById(ctx.Projection.LanguageId.Value); - else - ctx.ContextLanguage = _services.WorkContext.WorkingLanguage; - - ctx.Stores = _services.StoreService.GetAllStores().ToDictionary(x => x.Id, x => x); - ctx.Languages = _languageService.Value.GetAllLanguages(true).ToDictionary(x => x.Id, x => x); - - if (!ctx.IsPreview && ctx.Profile.PerStore) - { - result = new List(ctx.Stores.Values.Where(x => x.Id == ctx.Filter.StoreId || ctx.Filter.StoreId == 0)); - } - else - { - int? storeId = (ctx.Filter.StoreId == 0 ? ctx.Projection.StoreId : ctx.Filter.StoreId); - - ctx.Store = ctx.Stores.Values.FirstOrDefault(x => x.Id == (storeId ?? _services.StoreContext.CurrentStore.Id)); - - result = new List { ctx.Store }; - } - - // get total records for progress - foreach (var store in result) - { - ctx.Store = store; - - int totalCount = 0; - - if (totalRecords.HasValue) - { - totalCount = totalRecords.Value; // speed up preview by not counting total at each page - } - else - { - switch (ctx.Provider.Value.EntityType) - { - case ExportEntityType.Product: - totalCount = GetProductQuery(ctx, ctx.Profile.Offset, int.MaxValue).Count(); - break; - case ExportEntityType.Order: - totalCount = GetOrderQuery(ctx, ctx.Profile.Offset, int.MaxValue).Count(); - break; - case ExportEntityType.Manufacturer: - totalCount = GetManufacturerQuery(ctx, ctx.Profile.Offset, int.MaxValue).Count(); - break; - case ExportEntityType.Category: - totalCount = GetCategoryQuery(ctx, ctx.Profile.Offset, int.MaxValue).Count(); - break; - case ExportEntityType.Customer: - totalCount = GetCustomerQuery(ctx, ctx.Profile.Offset, int.MaxValue).Count(); - break; - case ExportEntityType.NewsLetterSubscription: - totalCount = GetNewsLetterSubscriptionQuery(ctx, ctx.Profile.Offset, int.MaxValue).Count(); - break; - } - } - - ctx.RecordsPerStore.Add(store.Id, totalCount); - } - - return result; - } - - private void ExportCoreInner(ExportProfileTaskContext ctx, Store store) - { - if (ctx.Export.Abort != ExportAbortion.None) - return; - - int fileIndex = 0; - - ctx.Store = store; - - { - var logHead = new StringBuilder(); - logHead.AppendLine(); - logHead.AppendLine(new string('-', 40)); - logHead.AppendLine("SmartStore.NET:\t\tv." + SmartStoreVersion.CurrentFullVersion); - logHead.Append("Export profile:\t\t" + ctx.Profile.Name); - logHead.AppendLine(ctx.Profile.Id == 0 ? " (volatile)" : " (Id {0})".FormatInvariant(ctx.Profile.Id)); - - logHead.AppendLine("Export provider:\t{0} ({1})".FormatInvariant(ctx.Provider.Metadata.FriendlyName, ctx.Profile.ProviderSystemName)); - - var plugin = ctx.Provider.Metadata.PluginDescriptor; - logHead.Append("Plugin:\t\t\t\t"); - logHead.AppendLine(plugin == null ? "".NaIfEmpty() : "{0} ({1}) v.{2}".FormatInvariant(plugin.FriendlyName, plugin.SystemName, plugin.Version.ToString())); - - logHead.AppendLine("Entity:\t\t\t\t" + ctx.Provider.Value.EntityType.ToString()); - - var storeInfo = (ctx.Profile.PerStore ? "{0} (Id {1})".FormatInvariant(ctx.Store.Name, ctx.Store.Id) : "All stores"); - logHead.Append("Store:\t\t\t\t" + storeInfo); - - ctx.Log.Information(logHead.ToString()); - } - - ctx.Export.Store = ToExpando(ctx, ctx.Store); - - ctx.Export.MaxFileNameLength = _dataExchangeSettings.Value.MaxFileNameLength; - - ctx.Export.FileExtension = (ctx.Provider.Value.FileExtension.HasValue() ? ctx.Provider.Value.FileExtension.ToLower().EnsureStartsWith(".") : ""); - - ctx.Export.HasPublicDeployment = ctx.Profile.Deployments.Any(x => x.IsPublic && x.DeploymentType == ExportDeploymentType.FileSystem); - - ctx.Export.PublicFolderPath = (ctx.Export.HasPublicDeployment ? Path.Combine(HttpRuntime.AppDomainAppPath, PublicFolder) : null); - - - using (var segmenter = CreateSegmenter(ctx)) - { - if (segmenter == null) - { - throw new SmartException("Unsupported entity type '{0}'".FormatInvariant(ctx.Provider.Value.EntityType.ToString())); - } - - if (segmenter.RecordTotal <= 0) - { - ctx.Log.Information("There are no records to export"); - } - - while (ctx.Export.Abort == ExportAbortion.None && segmenter.HasData) - { - segmenter.RecordPerSegmentCount = 0; - ctx.Export.RecordsSucceeded = 0; - - try - { - if (ctx.IsFileBasedExport) - { - var resolvedPattern = ctx.Profile.ResolveFileNamePattern(ctx.Store, ++fileIndex, ctx.Export.MaxFileNameLength); - - ctx.Export.FileName = resolvedPattern + ctx.Export.FileExtension; - ctx.Export.FilePath = Path.Combine(ctx.Export.Folder, ctx.Export.FileName); - - if (ctx.Export.HasPublicDeployment) - ctx.Export.PublicFileUrl = ctx.Store.Url.EnsureEndsWith("/") + PublicFolder.EnsureEndsWith("/") + ctx.Export.FileName; - - ctx.Provider.Value.Execute(ctx.Export); - - // create info for deployment list in profile edit - if (File.Exists(ctx.Export.FilePath)) - { - ctx.Result.Files.Add(new ExportExecuteResult.ExportFileInfo - { - StoreId = ctx.Store.Id, - FileName = ctx.Export.FileName - }); - } - } - else - { - ctx.Provider.Value.Execute(ctx.Export); - } - - ctx.Log.Information("Provider reports {0} successful exported record(s)".FormatInvariant(ctx.Export.RecordsSucceeded)); - } - catch (Exception exc) - { - ctx.Export.Abort = ExportAbortion.Hard; - ctx.Log.Error("The provider failed to execute the export: " + exc.ToAllMessages(), exc); - ctx.Result.LastError = exc.ToString(); - } - - if (ctx.Export.IsMaxFailures) - ctx.Log.Warning("Export aborted. The maximum number of failures has been reached"); - - if (ctx.TaskContext.CancellationToken.IsCancellationRequested) - ctx.Log.Warning("Export aborted. A cancellation has been requested"); - } - - if (ctx.Export.Abort != ExportAbortion.Hard) - { - ctx.Provider.Value.ExecuteEnded(ctx.Export); - } - } - } - - private void ExportCoreOuter(ExportProfileTaskContext ctx) - { - if (ctx.Profile == null || !ctx.Profile.Enabled) - return; - - FileSystemHelper.Delete(ctx.LogPath); - FileSystemHelper.ClearDirectory(ctx.FolderContent, false); - FileSystemHelper.Delete(ctx.ZipPath); - - using (_rwLock.GetWriteLock()) - using (var logger = new TraceLogger(ctx.LogPath)) - { - try - { - if (!ctx.Provider.IsValid()) - { - throw new SmartException("Export aborted because the export provider is not valid"); - } - - ctx.Log = logger; - ctx.Export.Log = logger; - ctx.ProgressInfo = _services.Localization.GetResource("Admin.DataExchange.Export.ProgressInfo"); - - if (ctx.Profile.ProviderConfigData.HasValue()) - { - var configInfo = ctx.Provider.Value.ConfigurationInfo; - if (configInfo != null) - { - ctx.Export.ConfigurationData = XmlHelper.Deserialize(ctx.Profile.ProviderConfigData, configInfo.ModelType); - } - } - - using (var scope = new DbContextScope(_services.DbContext, autoDetectChanges: false, proxyCreation: true, validateOnSave: false, forceNoTracking: true)) - { - ctx.DeliveryTimes = _deliveryTimeService.Value.GetAllDeliveryTimes().ToDictionary(x => x.Id, x => x); - ctx.QuantityUnits = _quantityUnitService.Value.GetAllQuantityUnits().ToDictionary(x => x.Id, x => x); - ctx.ProductTemplates = _productTemplateService.Value.GetAllProductTemplates().ToDictionary(x => x.Id, x => x); - - if (ctx.Provider.Value.EntityType == ExportEntityType.Product) - { - var allCategories = _categoryService.Value.GetAllCategories(showHidden: true, applyNavigationFilters: false); - ctx.Categories = allCategories.ToDictionary(x => x.Id); - } - - if (ctx.Provider.Value.EntityType == ExportEntityType.Order) - { - ctx.Countries = _countryService.Value.GetAllCountries(true).ToDictionary(x => x.Id, x => x); - } - - if (ctx.Provider.Value.EntityType == ExportEntityType.Customer) - { - var subscriptionEmails = _subscriptionRepository.Value.TableUntracked - .Where(x => x.Active) - .Select(x => x.Email) - .Distinct() - .ToList(); - - ctx.NewsletterSubscriptions = new HashSet(subscriptionEmails, StringComparer.OrdinalIgnoreCase); - } - - var stores = Init(ctx); - - ctx.Export.Language = ToExpando(ctx, ctx.ContextLanguage); - ctx.Export.Customer = ToExpando(ctx, ctx.ContextCustomer); - ctx.Export.Currency = ToExpando(ctx, ctx.ContextCurrency); - - stores.ForEach(x => ExportCoreInner(ctx, x)); - } - - if (!ctx.IsPreview && ctx.Export.Abort != ExportAbortion.Hard) - { - if (ctx.IsFileBasedExport) - { - if (ctx.Profile.CreateZipArchive || ctx.Profile.Deployments.Any(x => x.Enabled && x.CreateZip)) - { - ZipFile.CreateFromDirectory(ctx.FolderContent, ctx.ZipPath, CompressionLevel.Fastest, true); - } - - SetProgress(ctx, _services.Localization.GetResource("Common.Deployment")); - - foreach (var deployment in ctx.Profile.Deployments.OrderBy(x => x.DeploymentTypeId).Where(x => x.Enabled)) - { - try - { - switch (deployment.DeploymentType) - { - case ExportDeploymentType.FileSystem: - DeployFileSystem(ctx, deployment); - break; - case ExportDeploymentType.Email: - DeployEmail(ctx, deployment); - break; - case ExportDeploymentType.Http: - DeployHttp(ctx, deployment); - break; - case ExportDeploymentType.Ftp: - DeployFtp(ctx, deployment); - break; - } - } - catch (Exception exc) - { - logger.Error("Deployment \"{0}\" of type {1} failed: {2}".FormatInvariant( - deployment.Name, deployment.DeploymentType.ToString(), exc.Message), exc); - } - } - } - - if (ctx.Profile.EmailAccountId != 0 && ctx.Profile.CompletedEmailAddresses.HasValue()) - { - SendCompletionEmail(ctx); - } - } - } - catch (Exception exc) - { - logger.Error(exc); - ctx.Result.LastError = exc.ToString(); - } - finally - { - try - { - if (!ctx.IsPreview && ctx.Profile.Id != 0) - { - ctx.Profile.ResultInfo = XmlHelper.Serialize(ctx.Result); - - _exportService.Value.UpdateExportProfile(ctx.Profile); - } - } - catch { } - - try - { - if (ctx.IsFileBasedExport && ctx.Export.Abort != ExportAbortion.Hard && ctx.Profile.Cleanup) - { - FileSystemHelper.ClearDirectory(ctx.FolderContent, false); - } - } - catch { } - - try - { - ctx.NewsletterSubscriptions.Clear(); - ctx.ProductTemplates.Clear(); - ctx.Countries.Clear(); - ctx.Stores.Clear(); - ctx.QuantityUnits.Clear(); - ctx.DeliveryTimes.Clear(); - ctx.CategoryPathes.Clear(); - ctx.Categories.Clear(); - ctx.EntityIdsSelected.Clear(); - ctx.DataContextProduct = null; - ctx.DataContextOrder = null; - ctx.DataContextManufacturer = null; - ctx.DataContextCategory = null; - ctx.DataContextCustomer = null; - - ctx.Export.CustomProperties.Clear(); - ctx.Export.Log = null; - ctx.Log = null; - } - catch { } - } - } - - if (ctx.IsPreview || ctx.Export.Abort == ExportAbortion.Hard) - return; - - // post process order entities - if (ctx.EntityIdsLoaded.Count > 0 && ctx.Provider.Value.EntityType == ExportEntityType.Order && ctx.Projection.OrderStatusChange != ExportOrderStatusChange.None) - { - using (var logger = new TraceLogger(ctx.LogPath)) - { - try - { - int? orderStatusId = null; - - if (ctx.Projection.OrderStatusChange == ExportOrderStatusChange.Processing) - orderStatusId = (int)OrderStatus.Processing; - else if (ctx.Projection.OrderStatusChange == ExportOrderStatusChange.Complete) - orderStatusId = (int)OrderStatus.Complete; - - using (var scope = new DbContextScope(_services.DbContext, false, null, false, false, false, false)) - { - foreach (var chunk in ctx.EntityIdsLoaded.Chunk()) - { - var entities = _orderRepository.Value.Table.Where(x => chunk.Contains(x.Id)).ToList(); - - entities.ForEach(x => x.OrderStatusId = (orderStatusId ?? x.OrderStatusId)); - - _services.DbContext.SaveChanges(); - } - } - - logger.Information("Updated order status for {0} order(s).".FormatInvariant(ctx.EntityIdsLoaded.Count())); - } - catch (Exception exc) - { - logger.Error(exc); - ctx.Result.LastError = exc.ToString(); - } - } - } - } - - - /// - /// The name of the public export folder - /// - public static string PublicFolder - { - get { return "Exchange"; } - } - - public static int PageSize - { - get { return 100; } - } - - /// - /// Export using async runner and a volatile profile. Helper for internal exports to reduce duplicate code. - /// - /// Provider system name - /// Entity identifiers of entities to be exported. Can be null to export all entities. - /// Last error - /// File stream result of the first export data file - public static FileStreamResult Export(string providerSystemName, string selectedEntityIds, string downloadFileName, out string error) - { - Guard.ArgumentNotEmpty(() => providerSystemName); - - error = null; - var cancellation = new CancellationTokenSource(TimeSpan.FromHours(3.0)); - - var task = AsyncRunner.Run((container, ct) => - { - var exportTask = new ExportProfileTask(); - return exportTask.Execute(providerSystemName, container, ct, null, selectedEntityIds); - }, - cancellation.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); - - task.Wait(); - - if (task.Result != null && task.Result.Succeeded && task.Result.FileFolder.HasValue() && task.Result.Files.Count > 0) - { - var fileName = task.Result.Files.First().FileName; - var filePath = Path.Combine(task.Result.FileFolder, fileName); - - var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); - var result = new FileStreamResult(stream, MimeTypes.MapNameToMimeType(fileName)); - - if (downloadFileName.HasValue()) - result.FileDownloadName = downloadFileName.ToValidFileName() + Path.GetExtension(filePath); - else - result.FileDownloadName = task.Result.DownloadFileName; - - return result; - } - - if (task.Result != null) - error = task.Result.LastError; - - return null; - } - - /// - /// Export by executing schedule task of export profile - /// - /// Schedule task execution context - public void Execute(TaskExecutionContext taskContext) - { - InitDependencies(taskContext); - - var profileId = taskContext.ScheduleTask.Alias.ToInt(); - var profile = _exportService.Value.GetExportProfileById(profileId); - - var selectedIdsCacheKey = profile.GetSelectedEntityIdsCacheKey(); - var selectedEntityIds = HttpRuntime.Cache[selectedIdsCacheKey] as string; - - var provider = _exportService.Value.LoadProvider(profile.ProviderSystemName); - - if (provider == null) - throw new SmartException(_services.Localization.GetResource("Admin.Common.ProviderNotLoaded").FormatInvariant(profile.ProviderSystemName.NaIfEmpty())); - - var ctx = new ExportProfileTaskContext(taskContext, profile, provider, selectedEntityIds); - - HttpRuntime.Cache.Remove(selectedIdsCacheKey); - - ExportCoreOuter(ctx); - - taskContext.CancellationToken.ThrowIfCancellationRequested(); - } - - /// - /// Direct export using an export profile - /// - /// Provider system name - /// Component context - /// Export profile. Can be null to use a volatile profile. - /// Cancellation token - /// Entity identifiers of entities to be exported. Can be null to export all entities. - /// Any data passed on IExportExecuteContext.CustomProperties - /// Product query that supersede profile filtering - /// Export execute result - public ExportExecuteResult Execute(string providerSystemName, - IComponentContext context, - CancellationToken cancellationToken, - ExportProfile profile = null, - string selectedEntityIds = null, - Dictionary customProperties = null, - IQueryable queryProducts = null) - { - Guard.ArgumentNotEmpty(() => providerSystemName); - Guard.ArgumentNotNull(() => context); - - var taskContext = new TaskExecutionContext(context, null); - taskContext.CancellationToken = cancellationToken; - - InitDependencies(taskContext); - - var provider = _exportService.Value.LoadProvider(providerSystemName); - - if (provider == null) - throw new SmartException(_services.Localization.GetResource("Admin.Common.ProviderNotLoaded").FormatInvariant(providerSystemName.NaIfEmpty())); - - if (profile == null) - profile = _exportService.Value.CreateVolatileProfile(provider); - - var ctx = new ExportProfileTaskContext(taskContext, profile, provider, selectedEntityIds); - ctx.QueryProducts = queryProducts; - - if (customProperties != null) - { - foreach (var item in customProperties) - ctx.Export.CustomProperties.Add(item.Key, item.Value); - } - - ExportCoreOuter(ctx); - - if (ctx.Result != null && ctx.Result.Succeeded && ctx.Result.Files.Count > 0) - { - string prefix = null; - string suffix = null; - var extension = Path.GetExtension(ctx.Result.Files.First().FileName); - - if (provider.Value.EntityType == ExportEntityType.Product) - prefix = _services.Localization.GetResource("Admin.Catalog.Products"); - else if (provider.Value.EntityType == ExportEntityType.Order) - prefix = _services.Localization.GetResource("Admin.Orders"); - else if (provider.Value.EntityType == ExportEntityType.Category) - prefix = _services.Localization.GetResource("Admin.Catalog.Categories"); - else if (provider.Value.EntityType == ExportEntityType.Manufacturer) - prefix = _services.Localization.GetResource("Admin.Catalog.Manufacturers"); - else if (provider.Value.EntityType == ExportEntityType.Customer) - prefix = _services.Localization.GetResource("Admin.Customers"); - else if (provider.Value.EntityType == ExportEntityType.NewsLetterSubscription) - prefix = _services.Localization.GetResource("Admin.Promotions.NewsLetterSubscriptions"); - else - prefix = provider.Value.EntityType.ToString(); - - if (selectedEntityIds.HasValue()) - suffix = (selectedEntityIds.Contains(",") ? _services.Localization.GetResource("Admin.Common.Selected") : selectedEntityIds); - else - suffix = _services.Localization.GetResource("Common.All"); - - ctx.Result.DownloadFileName = string.Concat(prefix, "-", suffix).ToLower().ToValidFileName() + extension; - } - - return ctx.Result; - } - - /// - /// Get preview data of an export profile - /// - /// Export profile - /// Component context - /// Page index - /// Number of total records - /// Action to process preview data - public void Preview(ExportProfile profile, Provider provider, IComponentContext context, int pageIndex, int totalRecords, Action previewData) - { - Guard.ArgumentNotNull(() => profile); - Guard.ArgumentNotNull(() => provider); - Guard.ArgumentNotNull(() => context); - - var taskContext = new TaskExecutionContext(context, null); - var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(5.0)); - - taskContext.CancellationToken = cancellation.Token; - - InitDependencies(taskContext); - - var ctx = new ExportProfileTaskContext(taskContext, profile, provider, null, previewData); - - var unused = Init(ctx, totalRecords); - - using (var segmenter = CreateSegmenter(ctx, pageIndex)) - { - if (segmenter == null) - { - throw new SmartException("Unsupported entity type '{0}'".FormatInvariant(ctx.Provider.Value.EntityType.ToString())); - } - - while (segmenter.HasData) - { - segmenter.RecordPerSegmentCount = 0; - - while (segmenter.ReadNextSegment()) - { - segmenter.CurrentSegment.ForEach(x => ctx.PreviewData(x)); - } - } - } - - if (ctx.Result.LastError.HasValue()) - { - _services.Notifier.Error(ctx.Result.LastError); - } - } - - /// - /// Get the number of total export records - /// - /// Export profile - /// Export provider - /// Component context - /// Number of total records - public int GetRecordCount(ExportProfile profile, Provider provider, IComponentContext context) - { - Guard.ArgumentNotNull(() => profile); - Guard.ArgumentNotNull(() => provider); - Guard.ArgumentNotNull(() => context); - - var taskContext = new TaskExecutionContext(context, null); - var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(5.0)); - - taskContext.CancellationToken = cancellation.Token; - - InitDependencies(taskContext); - - var ctx = new ExportProfileTaskContext(taskContext, profile, provider, previewData: x => { }); - - var unused = Init(ctx); - - var totalCount = ctx.RecordsPerStore.First().Value; - - return totalCount; - } - } -} diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportTask/ExportProfileTaskContext.cs b/src/Libraries/SmartStore.Services/DataExchange/ExportTask/ExportProfileTaskContext.cs deleted file mode 100644 index 2beab3db1b..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportTask/ExportProfileTaskContext.cs +++ /dev/null @@ -1,217 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using SmartStore.Core; -using SmartStore.Core.Domain; -using SmartStore.Core.Domain.Catalog; -using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.DataExchange; -using SmartStore.Core.Domain.Directory; -using SmartStore.Core.Domain.Localization; -using SmartStore.Core.Domain.Stores; -using SmartStore.Core.Logging; -using SmartStore.Core.Plugins; -using SmartStore.Services.Tasks; -using SmartStore.Utilities; - -namespace SmartStore.Services.DataExchange.ExportTask -{ - internal class ExportProfileTaskContext - { - private ExportDataContextProduct _dataContextProduct; - private ExportDataContextOrder _dataContextOrder; - private ExportDataContextManufacturer _dataContextManufacturer; - private ExportDataContextCategory _dataContextCategory; - private ExportDataContextCustomer _dataContextCustomer; - - public ExportProfileTaskContext( - TaskExecutionContext taskContext, - ExportProfile profile, - Provider provider, - string selectedIds = null, - Action previewData = null) - { - TaskContext = taskContext; - Profile = profile; - Provider = provider; - Filter = XmlHelper.Deserialize(profile.Filtering); - Projection = XmlHelper.Deserialize(profile.Projection); - EntityIdsSelected = selectedIds.SplitSafe(",").Select(x => x.ToInt()).ToList(); - PreviewData = previewData; - - Supporting = Enum.GetValues(typeof(ExportSupport)) - .Cast() - .ToDictionary(x => x, x => Provider.Supports(x)); - - FolderContent = FileSystemHelper.TempDir(@"Profile\Export\{0}\Content".FormatInvariant(profile.FolderName)); - FolderRoot = System.IO.Directory.GetParent(FolderContent).FullName; - - Categories = new Dictionary(); - CategoryPathes = new Dictionary(); - Countries = new Dictionary(); - ProductTemplates = new Dictionary(); - NewsletterSubscriptions = new HashSet(); - - RecordsPerStore = new Dictionary(); - EntityIdsLoaded = new List(); - - Result = new ExportExecuteResult - { - FileFolder = (IsFileBasedExport ? FolderContent : null) - }; - - Export = new ExportExecuteContext(Result, TaskContext.CancellationToken, FolderContent); - Export.Projection = XmlHelper.Deserialize(profile.Projection); - } - - public List EntityIdsSelected { get; private set; } - public List EntityIdsLoaded { get; set; } - - public int RecordCount { get; set; } - public Dictionary RecordsPerStore { get; set; } - public string ProgressInfo { get; set; } - public IQueryable QueryProducts { get; set; } - - public Action PreviewData { get; private set; } - public bool IsPreview - { - get { return PreviewData != null; } - } - - public TaskExecutionContext TaskContext { get; private set; } - public ExportProfile Profile { get; private set; } - public Provider Provider { get; private set; } - - public Dictionary Supporting { get; private set; } - public bool Supports(ExportSupport type) - { - return (!IsPreview && Supporting[type]); - } - - public ExportFilter Filter { get; private set; } - public ExportProjection Projection { get; private set; } - public Currency ContextCurrency { get; set; } - public Customer ContextCustomer { get; set; } - public Language ContextLanguage { get; set; } - - public TraceLogger Log { get; set; } - public Store Store { get; set; } - - public string FolderRoot { get; private set; } - public string FolderContent { get; private set; } - public string ZipName - { - get { return Profile.FolderName + ".zip"; } - } - public string ZipPath - { - get { return Path.Combine(FolderRoot, ZipName); } - } - public string LogPath - { - get { return Path.Combine(FolderRoot, "log.txt"); } - } - - public bool IsFileBasedExport - { - get { return Provider == null || Provider.Value == null || Provider.Value.FileExtension.HasValue(); } - } - public string[] GetDeploymentFiles(ExportDeployment deployment) - { - if (!IsFileBasedExport) - return new string[0]; - - if (deployment.CreateZip) - return new string[] { ZipPath }; - - return System.IO.Directory.GetFiles(FolderContent, "*.*", SearchOption.AllDirectories); - } - - // data loaded once per export - public Dictionary Categories { get; set; } - public Dictionary CategoryPathes { get; set; } - public Dictionary DeliveryTimes { get; set; } - public Dictionary QuantityUnits { get; set; } - public Dictionary Stores { get; set; } - public Dictionary Languages { get; set; } - public Dictionary Countries { get; set; } - public Dictionary ProductTemplates { get; set; } - public HashSet NewsletterSubscriptions { get; set; } - - // data loaded once per page - public ExportDataContextProduct DataContextProduct - { - get - { - return _dataContextProduct; - } - set - { - if (_dataContextProduct != null) - _dataContextProduct.Clear(); - - _dataContextProduct = value; - } - } - public ExportDataContextOrder DataContextOrder - { - get - { - return _dataContextOrder; - } - set - { - if (_dataContextOrder != null) - _dataContextOrder.Clear(); - - _dataContextOrder = value; - } - } - public ExportDataContextManufacturer DataContextManufacturer - { - get - { - return _dataContextManufacturer; - } - set - { - if (_dataContextManufacturer != null) - _dataContextManufacturer.Clear(); - - _dataContextManufacturer = value; - } - } - public ExportDataContextCategory DataContextCategory - { - get - { - return _dataContextCategory; - } - set - { - if (_dataContextCategory != null) - _dataContextCategory.Clear(); - - _dataContextCategory = value; - } - } - public ExportDataContextCustomer DataContextCustomer - { - get - { - return _dataContextCustomer; - } - set - { - if (_dataContextCustomer != null) - _dataContextCustomer.Clear(); - - _dataContextCustomer = value; - } - } - - public ExportExecuteContext Export { get; set; } - public ExportExecuteResult Result { get; set; } - } -} diff --git a/src/Libraries/SmartStore.Services/DataExchange/ExportXmlHelper.cs b/src/Libraries/SmartStore.Services/DataExchange/ExportXmlHelper.cs deleted file mode 100644 index 8191f0d76e..0000000000 --- a/src/Libraries/SmartStore.Services/DataExchange/ExportXmlHelper.cs +++ /dev/null @@ -1,838 +0,0 @@ -using System; -using System.Globalization; -using System.Xml; -using SmartStore.Core.Domain.Catalog; -using SmartStore.Core.Domain.DataExchange; - -namespace SmartStore.Services.DataExchange -{ - public class ExportXmlHelper - { - private XmlWriter _writer; - private CultureInfo _culture; - - public ExportXmlHelper(XmlWriter writer, CultureInfo culture) - { - _writer = writer; - _culture = culture; - } - - public void WriteLocalized(dynamic parentNode) - { - if (parentNode == null || parentNode._Localized == null) - return; - - _writer.WriteStartElement("Localized"); - foreach (dynamic item in parentNode._Localized) - { - _writer.WriteStartElement((string)item.LocaleKey); - _writer.WriteAttributeString("culture", (string)item.Culture); - _writer.WriteString(((string)item.LocaleValue).RemoveInvalidXmlChars()); - _writer.WriteEndElement(); // item.LocaleKey - } - _writer.WriteEndElement(); // Localized - } - - public void WriteGenericAttributes(dynamic parentNode) - { - if (parentNode == null || parentNode._GenericAttributes == null) - return; - - _writer.WriteStartElement("GenericAttributes"); - foreach (dynamic genericAttribute in parentNode._GenericAttributes) - { - _writer.WriteStartElement("GenericAttribute"); - _writer.Write("Id", ((int)genericAttribute.Id).ToString()); - _writer.Write("EntityId", ((int)genericAttribute.EntityId).ToString()); - _writer.Write("KeyGroup", (string)genericAttribute.KeyGroup); - _writer.Write("Key", (string)genericAttribute.Key); - _writer.Write("Value", (string)genericAttribute.Value); - _writer.Write("StoreId", ((int)genericAttribute.StoreId).ToString()); - _writer.WriteEndElement(); // GenericAttribute - } - _writer.WriteEndElement(); // GenericAttributes - } - - public void WriteAddress(dynamic address, string node) - { - if (address == null) - return; - - int? countryId = address.CountryId; - int? stateProvinceId = address.StateProvinceId; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - _writer.Write("Id", ((int)address.Id).ToString()); - _writer.Write("FirstName", (string)address.FirstName); - _writer.Write("LastName", (string)address.LastName); - _writer.Write("Email", (string)address.Email); - _writer.Write("Company", (string)address.Company); - _writer.Write("CountryId", countryId.HasValue ? countryId.Value.ToString() : ""); - _writer.Write("StateProvinceId", stateProvinceId.HasValue ? stateProvinceId.Value.ToString() : ""); - _writer.Write("City", (string)address.City); - _writer.Write("Address1", (string)address.Address1); - _writer.Write("Address2", (string)address.Address2); - _writer.Write("ZipPostalCode", (string)address.ZipPostalCode); - _writer.Write("PhoneNumber", (string)address.PhoneNumber); - _writer.Write("FaxNumber", (string)address.FaxNumber); - _writer.Write("CreatedOnUtc", ((DateTime)address.CreatedOnUtc).ToString(_culture)); - - if (address.Country != null) - { - dynamic country = address.Country; - - _writer.WriteStartElement("Country"); - _writer.Write("Id", ((int)country.Id).ToString()); - _writer.Write("Name", (string)country.Name); - _writer.Write("AllowsBilling", ((bool)country.AllowsBilling).ToString()); - _writer.Write("AllowsShipping", ((bool)country.AllowsShipping).ToString()); - _writer.Write("TwoLetterIsoCode", (string)country.TwoLetterIsoCode); - _writer.Write("ThreeLetterIsoCode", (string)country.ThreeLetterIsoCode); - _writer.Write("NumericIsoCode", ((int)country.NumericIsoCode).ToString()); - _writer.Write("SubjectToVat", ((bool)country.SubjectToVat).ToString()); - _writer.Write("Published", ((bool)country.Published).ToString()); - _writer.Write("DisplayOrder", ((int)country.DisplayOrder).ToString()); - _writer.Write("LimitedToStores", ((bool)country.LimitedToStores).ToString()); - - WriteLocalized(country); - - _writer.WriteEndElement(); // Country - } - - if (address.StateProvince != null) - { - dynamic stateProvince = address.StateProvince; - - _writer.WriteStartElement("StateProvince"); - _writer.Write("Id", ((int)stateProvince.Id).ToString()); - _writer.Write("CountryId", ((int)stateProvince.CountryId).ToString()); - _writer.Write("Name", (string)stateProvince.Name); - _writer.Write("Abbreviation", (string)stateProvince.Abbreviation); - _writer.Write("Published", ((bool)stateProvince.Published).ToString()); - _writer.Write("DisplayOrder", ((int)stateProvince.DisplayOrder).ToString()); - - WriteLocalized(stateProvince); - - _writer.WriteEndElement(); // StateProvince - } - - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - - public void WriteCurrency(dynamic currency, string node) - { - if (currency == null) - return; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - _writer.Write("Id", ((int)currency.Id).ToString()); - _writer.Write("Name", (string)currency.Name); - _writer.Write("CurrencyCode", (string)currency.CurrencyCode); - _writer.Write("Rate", ((decimal)currency.Rate).ToString(_culture)); - _writer.Write("DisplayLocale", (string)currency.DisplayLocale); - _writer.Write("CustomFormatting", (string)currency.CustomFormatting); - _writer.Write("LimitedToStores", ((bool)currency.LimitedToStores).ToString()); - _writer.Write("Published", ((bool)currency.Published).ToString()); - _writer.Write("DisplayOrder", ((int)currency.DisplayOrder).ToString()); - _writer.Write("CreatedOnUtc", ((DateTime)currency.CreatedOnUtc).ToString(_culture)); - _writer.Write("UpdatedOnUtc", ((DateTime)currency.UpdatedOnUtc).ToString(_culture)); - _writer.Write("DomainEndings", (string)currency.DomainEndings); - - WriteLocalized(currency); - - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - - public void WriteRewardPointsHistory(dynamic rewardPoints, string node) - { - if (rewardPoints == null) - return; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - foreach (dynamic rewardPoint in rewardPoints) - { - _writer.WriteStartElement("RewardPointsHistory"); - _writer.Write("Id", ((int)rewardPoint.Id).ToString()); - _writer.Write("CustomerId", ((int)rewardPoint.CustomerId).ToString()); - _writer.Write("Points", ((int)rewardPoint.Points).ToString()); - _writer.Write("PointsBalance", ((int)rewardPoint.PointsBalance).ToString()); - _writer.Write("UsedAmount", ((decimal)rewardPoint.UsedAmount).ToString(_culture)); - _writer.Write("Message", (string)rewardPoint.Message); - _writer.Write("CreatedOnUtc", ((DateTime)rewardPoint.CreatedOnUtc).ToString(_culture)); - _writer.WriteEndElement(); // RewardPointsHistory - } - - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - - public void WriteDeliveryTime(dynamic deliveryTime, string node) - { - if (deliveryTime == null) - return; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - _writer.Write("Id", ((int)deliveryTime.Id).ToString()); - _writer.Write("Name", (string)deliveryTime.Name); - _writer.Write("DisplayLocale", (string)deliveryTime.DisplayLocale); - _writer.Write("ColorHexValue", (string)deliveryTime.ColorHexValue); - _writer.Write("DisplayOrder", ((int)deliveryTime.DisplayOrder).ToString()); - - WriteLocalized(deliveryTime); - - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - - public void WriteQuantityUnit(dynamic quantityUnit, string node) - { - if (quantityUnit == null) - return; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - _writer.Write("Id", ((int)quantityUnit.Id).ToString()); - _writer.Write("Name", (string)quantityUnit.Name); - _writer.Write("Description", (string)quantityUnit.Description); - _writer.Write("DisplayLocale", (string)quantityUnit.DisplayLocale); - _writer.Write("DisplayOrder", ((int)quantityUnit.DisplayOrder).ToString()); - _writer.Write("IsDefault", ((bool)quantityUnit.IsDefault).ToString()); - - WriteLocalized(quantityUnit); - - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - - public void WritePicture(dynamic picture, string node) - { - if (picture == null) - return; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - _writer.Write("Id", ((int)picture.Id).ToString()); - _writer.Write("SeoFilename", (string)picture.SeoFilename); - _writer.Write("MimeType", (string)picture.MimeType); - _writer.Write("ThumbImageUrl", (string)picture._ThumbImageUrl); - _writer.Write("ImageUrl", (string)picture._ImageUrl); - _writer.Write("FullSizeImageUrl", (string)picture._FullSizeImageUrl); - _writer.Write("FileName", (string)picture._FileName); - - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - - public void WriteCategory(dynamic category, string node) - { - if (category == null) - return; - - int? pictureId = category.PictureId; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - _writer.Write("Id", ((int)category.Id).ToString()); - _writer.Write("Name", (string)category.Name); - _writer.Write("FullName", (string)category.FullName); - _writer.Write("Description", (string)category.Description); - _writer.Write("BottomDescription", (string)category.BottomDescription); - _writer.Write("CategoryTemplateId", ((int)category.CategoryTemplateId).ToString()); - _writer.Write("MetaKeywords", (string)category.MetaKeywords); - _writer.Write("MetaDescription", (string)category.MetaDescription); - _writer.Write("MetaTitle", (string)category.MetaTitle); - _writer.Write("SeName", (string)category.SeName); - _writer.Write("ParentCategoryId", ((int)category.ParentCategoryId).ToString()); - _writer.Write("PictureId", pictureId.HasValue ? pictureId.Value.ToString() : ""); - _writer.Write("PageSize", ((int)category.PageSize).ToString()); - _writer.Write("AllowCustomersToSelectPageSize", ((bool)category.AllowCustomersToSelectPageSize).ToString()); - _writer.Write("PageSizeOptions", (string)category.PageSizeOptions); - _writer.Write("PriceRanges", (string)category.PriceRanges); - _writer.Write("ShowOnHomePage", ((bool)category.ShowOnHomePage).ToString()); - _writer.Write("HasDiscountsApplied", ((bool)category.HasDiscountsApplied).ToString()); - _writer.Write("Published", ((bool)category.Published).ToString()); - _writer.Write("Deleted", ((bool)category.Deleted).ToString()); - _writer.Write("DisplayOrder", ((int)category.DisplayOrder).ToString()); - _writer.Write("CreatedOnUtc", ((DateTime)category.CreatedOnUtc).ToString(_culture)); - _writer.Write("UpdatedOnUtc", ((DateTime)category.UpdatedOnUtc).ToString(_culture)); - _writer.Write("SubjectToAcl", ((bool)category.SubjectToAcl).ToString()); - _writer.Write("LimitedToStores", ((bool)category.LimitedToStores).ToString()); - _writer.Write("Alias", (string)category.Alias); - _writer.Write("DefaultViewMode", (string)category.DefaultViewMode); - - WritePicture(category.Picture, "Picture"); - - WriteLocalized(category); - - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - - public void WriteManufacturer(dynamic manufacturer, string node) - { - if (manufacturer == null) - return; - - int? pictureId = manufacturer.PictureId; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - _writer.Write("Id", ((int)manufacturer.Id).ToString()); - _writer.Write("Name", (string)manufacturer.Name); - _writer.Write("SeName", (string)manufacturer.SeName); - _writer.Write("Description", (string)manufacturer.Description); - _writer.Write("ManufacturerTemplateId", ((int)manufacturer.ManufacturerTemplateId).ToString()); - _writer.Write("MetaKeywords", (string)manufacturer.MetaKeywords); - _writer.Write("MetaDescription", (string)manufacturer.MetaDescription); - _writer.Write("MetaTitle", (string)manufacturer.MetaTitle); - _writer.Write("PictureId", pictureId.HasValue ? pictureId.Value.ToString() : ""); - _writer.Write("PageSize", ((int)manufacturer.PageSize).ToString()); - _writer.Write("AllowCustomersToSelectPageSize", ((bool)manufacturer.AllowCustomersToSelectPageSize).ToString()); - _writer.Write("PageSizeOptions", (string)manufacturer.PageSizeOptions); - _writer.Write("PriceRanges", (string)manufacturer.PriceRanges); - _writer.Write("Published", ((bool)manufacturer.Published).ToString()); - _writer.Write("Deleted", ((bool)manufacturer.Deleted).ToString()); - _writer.Write("DisplayOrder", ((int)manufacturer.DisplayOrder).ToString()); - _writer.Write("CreatedOnUtc", ((DateTime)manufacturer.CreatedOnUtc).ToString(_culture)); - _writer.Write("UpdatedOnUtc", ((DateTime)manufacturer.UpdatedOnUtc).ToString(_culture)); - - WritePicture(manufacturer.Picture, "Picture"); - - WriteLocalized(manufacturer); - - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - - public void WriteProduct(dynamic product, string node) - { - if (product == null) - return; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - int? downloadExpirationDays = product.DownloadExpirationDays; - int? sampleDownloadId = product.SampleDownloadId; - decimal? specialPrice = product.SpecialPrice; - DateTime? specialPriceStartDateTimeUtc = product.SpecialPriceStartDateTimeUtc; - DateTime? specialPriceEndDateTimeUtc = product.SpecialPriceEndDateTimeUtc; - DateTime? availableStartDateTimeUtc = product.AvailableStartDateTimeUtc; - DateTime? availableEndDateTimeUtc = product.AvailableEndDateTimeUtc; - decimal? basePriceAmount = product.BasePriceAmount; - int? basePriceBaseAmount = product.BasePriceBaseAmount; - decimal? lowestAttributeCombinationPrice = product.LowestAttributeCombinationPrice; - - _writer.Write("Id", ((int)product.Id).ToString()); - _writer.Write("Name", (string)product.Name); - _writer.Write("SeName", (string)product.SeName); - _writer.Write("ShortDescription", (string)product.ShortDescription); - _writer.Write("FullDescription", (string)product.FullDescription); - _writer.Write("AdminComment", (string)product.AdminComment); - _writer.Write("ProductTemplateId", ((int)product.ProductTemplateId).ToString()); - _writer.Write("ProductTemplateViewPath", (string)product._ProductTemplateViewPath); - _writer.Write("ShowOnHomePage", ((bool)product.ShowOnHomePage).ToString()); - _writer.Write("HomePageDisplayOrder", ((int)product.HomePageDisplayOrder).ToString()); - _writer.Write("MetaKeywords", (string)product.MetaKeywords); - _writer.Write("MetaDescription", (string)product.MetaDescription); - _writer.Write("MetaTitle", (string)product.MetaTitle); - _writer.Write("AllowCustomerReviews", ((bool)product.AllowCustomerReviews).ToString()); - _writer.Write("ApprovedRatingSum", ((int)product.ApprovedRatingSum).ToString()); - _writer.Write("NotApprovedRatingSum", ((int)product.NotApprovedRatingSum).ToString()); - _writer.Write("ApprovedTotalReviews", ((int)product.ApprovedTotalReviews).ToString()); - _writer.Write("NotApprovedTotalReviews", ((int)product.NotApprovedTotalReviews).ToString()); - _writer.Write("Published", ((bool)product.Published).ToString()); - _writer.Write("CreatedOnUtc", ((DateTime)product.CreatedOnUtc).ToString(_culture)); - _writer.Write("UpdatedOnUtc", ((DateTime)product.UpdatedOnUtc).ToString(_culture)); - _writer.Write("SubjectToAcl", ((bool)product.SubjectToAcl).ToString()); - _writer.Write("LimitedToStores", ((bool)product.LimitedToStores).ToString()); - _writer.Write("ProductTypeId", ((int)product.ProductTypeId).ToString()); - _writer.Write("ParentGroupedProductId", ((int)product.ParentGroupedProductId).ToString()); - _writer.Write("Sku", (string)product.Sku); - _writer.Write("ManufacturerPartNumber", (string)product.ManufacturerPartNumber); - _writer.Write("Gtin", (string)product.Gtin); - _writer.Write("IsGiftCard", ((bool)product.IsGiftCard).ToString()); - _writer.Write("GiftCardTypeId", ((int)product.GiftCardTypeId).ToString()); - _writer.Write("RequireOtherProducts", ((bool)product.RequireOtherProducts).ToString()); - _writer.Write("RequiredProductIds", (string)product.RequiredProductIds); - _writer.Write("AutomaticallyAddRequiredProducts", ((bool)product.AutomaticallyAddRequiredProducts).ToString()); - _writer.Write("IsDownload", ((bool)product.IsDownload).ToString()); - _writer.Write("DownloadId", ((int)product.DownloadId).ToString()); - _writer.Write("UnlimitedDownloads", ((bool)product.UnlimitedDownloads).ToString()); - _writer.Write("MaxNumberOfDownloads", ((int)product.MaxNumberOfDownloads).ToString()); - _writer.Write("DownloadExpirationDays", downloadExpirationDays.HasValue ? downloadExpirationDays.Value.ToString() : ""); - _writer.Write("DownloadActivationTypeId", ((int)product.DownloadActivationTypeId).ToString()); - _writer.Write("HasSampleDownload", ((bool)product.HasSampleDownload).ToString()); - _writer.Write("SampleDownloadId", sampleDownloadId.HasValue ? sampleDownloadId.Value.ToString() : ""); - _writer.Write("HasUserAgreement", ((bool)product.HasUserAgreement).ToString()); - _writer.Write("UserAgreementText", (string)product.UserAgreementText); - _writer.Write("IsRecurring", ((bool)product.IsRecurring).ToString()); - _writer.Write("RecurringCycleLength", ((int)product.RecurringCycleLength).ToString()); - _writer.Write("RecurringCyclePeriodId", ((int)product.RecurringCyclePeriodId).ToString()); - _writer.Write("RecurringTotalCycles", ((int)product.RecurringTotalCycles).ToString()); - _writer.Write("IsShipEnabled", ((bool)product.IsShipEnabled).ToString()); - _writer.Write("IsFreeShipping", ((bool)product.IsFreeShipping).ToString()); - _writer.Write("AdditionalShippingCharge", ((decimal)product.AdditionalShippingCharge).ToString(_culture)); - _writer.Write("IsTaxExempt", ((bool)product.IsTaxExempt).ToString()); - _writer.Write("TaxCategoryId", ((int)product.TaxCategoryId).ToString()); - _writer.Write("ManageInventoryMethodId", ((int)product.ManageInventoryMethodId).ToString()); - _writer.Write("StockQuantity", ((int)product.StockQuantity).ToString()); - _writer.Write("DisplayStockAvailability", ((bool)product.DisplayStockAvailability).ToString()); - _writer.Write("DisplayStockQuantity", ((bool)product.DisplayStockQuantity).ToString()); - _writer.Write("MinStockQuantity", ((int)product.MinStockQuantity).ToString()); - _writer.Write("LowStockActivityId", ((int)product.LowStockActivityId).ToString()); - _writer.Write("NotifyAdminForQuantityBelow", ((int)product.NotifyAdminForQuantityBelow).ToString()); - _writer.Write("BackorderModeId", ((int)product.BackorderModeId).ToString()); - _writer.Write("AllowBackInStockSubscriptions", ((bool)product.AllowBackInStockSubscriptions).ToString()); - _writer.Write("OrderMinimumQuantity", ((int)product.OrderMinimumQuantity).ToString()); - _writer.Write("OrderMaximumQuantity", ((int)product.OrderMaximumQuantity).ToString()); - _writer.Write("AllowedQuantities", (string)product.AllowedQuantities); - _writer.Write("DisableBuyButton", ((bool)product.DisableBuyButton).ToString()); - _writer.Write("DisableWishlistButton", ((bool)product.DisableWishlistButton).ToString()); - _writer.Write("AvailableForPreOrder", ((bool)product.AvailableForPreOrder).ToString()); - _writer.Write("CallForPrice", ((bool)product.CallForPrice).ToString()); - _writer.Write("Price", ((decimal)product.Price).ToString(_culture)); - _writer.Write("OldPrice", ((decimal)product.OldPrice).ToString(_culture)); - _writer.Write("ProductCost", ((decimal)product.ProductCost).ToString(_culture)); - _writer.Write("SpecialPrice", specialPrice.HasValue ? specialPrice.Value.ToString(_culture) : ""); - _writer.Write("SpecialPriceStartDateTimeUtc", specialPriceStartDateTimeUtc.HasValue ? specialPriceStartDateTimeUtc.Value.ToString(_culture) : ""); - _writer.Write("SpecialPriceEndDateTimeUtc", specialPriceEndDateTimeUtc.HasValue ? specialPriceEndDateTimeUtc.Value.ToString(_culture) : ""); - _writer.Write("CustomerEntersPrice", ((bool)product.CustomerEntersPrice).ToString()); - _writer.Write("MinimumCustomerEnteredPrice", ((decimal)product.MinimumCustomerEnteredPrice).ToString(_culture)); - _writer.Write("MaximumCustomerEnteredPrice", ((decimal)product.MaximumCustomerEnteredPrice).ToString(_culture)); - _writer.Write("HasTierPrices", ((bool)product.HasTierPrices).ToString()); - _writer.Write("HasDiscountsApplied", ((bool)product.HasDiscountsApplied).ToString()); - _writer.Write("Weight", ((decimal)product.Weight).ToString(_culture)); - _writer.Write("Length", ((decimal)product.Length).ToString(_culture)); - _writer.Write("Width", ((decimal)product.Width).ToString(_culture)); - _writer.Write("Height", ((decimal)product.Height).ToString(_culture)); - _writer.Write("AvailableStartDateTimeUtc", availableStartDateTimeUtc.HasValue ? availableStartDateTimeUtc.Value.ToString(_culture) : ""); - _writer.Write("AvailableEndDateTimeUtc", availableEndDateTimeUtc.HasValue ? availableEndDateTimeUtc.Value.ToString(_culture) : ""); - _writer.Write("BasePriceEnabled", ((bool)product.BasePriceEnabled).ToString()); - _writer.Write("BasePriceMeasureUnit", (string)product.BasePriceMeasureUnit); - _writer.Write("BasePriceAmount", basePriceAmount.HasValue ? basePriceAmount.Value.ToString(_culture) : ""); - _writer.Write("BasePriceBaseAmount", basePriceBaseAmount.HasValue ? basePriceBaseAmount.Value.ToString() : ""); - _writer.Write("BasePriceHasValue", ((bool)product.BasePriceHasValue).ToString()); - _writer.Write("BasePriceInfo", (string)product._BasePriceInfo); - _writer.Write("VisibleIndividually", ((bool)product.VisibleIndividually).ToString()); - _writer.Write("DisplayOrder", ((int)product.DisplayOrder).ToString()); - _writer.Write("BundleTitleText", (string)product.BundleTitleText); - _writer.Write("BundlePerItemPricing", ((bool)product.BundlePerItemPricing).ToString()); - _writer.Write("BundlePerItemShipping", ((bool)product.BundlePerItemShipping).ToString()); - _writer.Write("BundlePerItemShoppingCart", ((bool)product.BundlePerItemShoppingCart).ToString()); - _writer.Write("LowestAttributeCombinationPrice", lowestAttributeCombinationPrice.HasValue ? lowestAttributeCombinationPrice.Value.ToString(_culture) : ""); - _writer.Write("IsEsd", ((bool)product.IsEsd).ToString()); - - WriteLocalized(product); - - WriteDeliveryTime(product.DeliveryTime, "DeliveryTime"); - - WriteQuantityUnit(product.QuantityUnit, "QuantityUnit"); - - if (product.AppliedDiscounts != null) - { - _writer.WriteStartElement("AppliedDiscounts"); - foreach (dynamic discount in product.AppliedDiscounts) - { - DateTime? startDateUtc = discount.StartDateUtc; - DateTime? endDateUtc = discount.EndDateUtc; - - _writer.WriteStartElement("AppliedDiscount"); - _writer.Write("Id", ((int)discount.Id).ToString()); - _writer.Write("Name", (string)discount.Name); - _writer.Write("DiscountTypeId", ((int)discount.DiscountTypeId).ToString()); - _writer.Write("UsePercentage", ((bool)discount.UsePercentage).ToString()); - _writer.Write("DiscountPercentage", ((decimal)discount.DiscountPercentage).ToString(_culture)); - _writer.Write("DiscountAmount", ((decimal)discount.DiscountAmount).ToString(_culture)); - _writer.Write("StartDateUtc", startDateUtc.HasValue ? startDateUtc.Value.ToString(_culture) : ""); - _writer.Write("EndDateUtc", endDateUtc.HasValue ? endDateUtc.Value.ToString(_culture) : ""); - _writer.Write("RequiresCouponCode", ((bool)discount.RequiresCouponCode).ToString()); - _writer.Write("CouponCode", (string)discount.CouponCode); - _writer.Write("DiscountLimitationId", ((int)discount.DiscountLimitationId).ToString()); - _writer.Write("LimitationTimes", ((int)discount.LimitationTimes).ToString()); - _writer.WriteEndElement(); // AppliedDiscount - } - _writer.WriteEndElement(); // AppliedDiscounts - } - - if (product.TierPrices != null) - { - _writer.WriteStartElement("TierPrices"); - foreach (dynamic tierPrice in product.TierPrices) - { - int? customerRoleId = tierPrice.CustomerRoleId; - - _writer.WriteStartElement("TierPrice"); - _writer.Write("Id", ((int)tierPrice.Id).ToString()); - _writer.Write("ProductId", ((int)tierPrice.ProductId).ToString()); - _writer.Write("StoreId", ((int)tierPrice.StoreId).ToString()); - _writer.Write("CustomerRoleId", customerRoleId.HasValue ? customerRoleId.Value.ToString() : ""); - _writer.Write("Quantity", ((int)tierPrice.Quantity).ToString()); - _writer.Write("Price", ((decimal)tierPrice.Price).ToString(_culture)); - _writer.WriteEndElement(); // TierPrice - } - _writer.WriteEndElement(); // TierPrices - } - - if (product.ProductTags != null) - { - _writer.WriteStartElement("ProductTags"); - foreach (dynamic tag in product.ProductTags) - { - _writer.WriteStartElement("ProductTag"); - _writer.Write("Id", ((int)tag.Id).ToString()); - _writer.Write("Name", (string)tag.Name); - _writer.Write("SeName", (string)tag.SeName); - - WriteLocalized(tag); - - _writer.WriteEndElement(); // ProductTag - } - _writer.WriteEndElement(); // ProductTags - } - - if (product.ProductAttributes != null) - { - _writer.WriteStartElement("ProductAttributes"); - foreach (dynamic pa in product.ProductAttributes) - { - _writer.WriteStartElement("ProductAttribute"); - _writer.Write("Id", ((int)pa.Id).ToString()); - _writer.Write("TextPrompt", (string)pa.TextPrompt); - _writer.Write("IsRequired", ((bool)pa.IsRequired).ToString()); - _writer.Write("AttributeControlTypeId", ((int)pa.AttributeControlTypeId).ToString()); - _writer.Write("DisplayOrder", ((int)pa.DisplayOrder).ToString()); - - _writer.WriteStartElement("Attribute"); - _writer.Write("Id", ((int)pa.Attribute.Id).ToString()); - _writer.Write("Alias", (string)pa.Attribute.Alias); - _writer.Write("Name", (string)pa.Attribute.Name); - _writer.Write("Description", (string)pa.Attribute.Description); - - WriteLocalized(pa.Attribute); - - _writer.WriteEndElement(); // Attribute - - _writer.WriteStartElement("AttributeValues"); - foreach (dynamic value in pa.Attribute.Values) - { - _writer.WriteStartElement("AttributeValue"); - _writer.Write("Id", ((int)value.Id).ToString()); - _writer.Write("Alias", (string)value.Alias); - _writer.Write("Name", (string)value.Name); - _writer.Write("ColorSquaresRgb", (string)value.ColorSquaresRgb); - _writer.Write("PriceAdjustment", ((decimal)value.PriceAdjustment).ToString(_culture)); - _writer.Write("WeightAdjustment", ((decimal)value.WeightAdjustment).ToString(_culture)); - _writer.Write("IsPreSelected", ((bool)value.IsPreSelected).ToString()); - _writer.Write("DisplayOrder", ((int)value.DisplayOrder).ToString()); - _writer.Write("ValueTypeId", ((int)value.ValueTypeId).ToString()); - _writer.Write("LinkedProductId", ((int)value.LinkedProductId).ToString()); - _writer.Write("Quantity", ((int)value.Quantity).ToString()); - - WriteLocalized(value); - - _writer.WriteEndElement(); // AttributeValue - } - _writer.WriteEndElement(); // AttributeValues - - _writer.WriteEndElement(); // ProductAttribute - } - _writer.WriteEndElement(); // ProductAttributes - } - - if (product.ProductAttributeCombinations != null) - { - _writer.WriteStartElement("ProductAttributeCombinations"); - foreach (dynamic combination in product.ProductAttributeCombinations) - { - decimal? price = combination.Price; - decimal? length = combination.Length; - decimal? width = combination.Width; - decimal? height = combination.Height; - decimal? bpAmount = combination.BasePriceAmount; - int? bpbAmount = combination.BasePriceBaseAmount; - int? dlvTimeId = combination.DeliveryTimeId; - - _writer.WriteStartElement("ProductAttributeCombination"); - _writer.Write("Id", ((int)combination.Id).ToString()); - _writer.Write("StockQuantity", ((int)combination.StockQuantity).ToString()); - _writer.Write("AllowOutOfStockOrders", ((bool)combination.AllowOutOfStockOrders).ToString()); - _writer.Write("AttributesXml", (string)combination.AttributesXml); - _writer.Write("Sku", (string)combination.Sku); - _writer.Write("Gtin", (string)combination.Gtin); - _writer.Write("ManufacturerPartNumber", (string)combination.ManufacturerPartNumber); - _writer.Write("Price", price.HasValue ? price.Value.ToString(_culture) : ""); - _writer.Write("Length", length.HasValue ? length.Value.ToString(_culture) : ""); - _writer.Write("Width", width.HasValue ? width.Value.ToString(_culture) : ""); - _writer.Write("Height", height.HasValue ? height.Value.ToString(_culture) : ""); - _writer.Write("BasePriceAmount", bpAmount.HasValue ? bpAmount.Value.ToString(_culture) : ""); - _writer.Write("BasePriceBaseAmount", bpbAmount.HasValue ? bpbAmount.Value.ToString() : ""); - _writer.Write("AssignedPictureIds", (string)combination.AssignedPictureIds); - _writer.Write("DeliveryTimeId", dlvTimeId.HasValue ? dlvTimeId.Value.ToString() : ""); - _writer.Write("IsActive", ((bool)combination.IsActive).ToString()); - - WriteDeliveryTime(combination.DeliveryTime, "DeliveryTime"); - - WriteQuantityUnit(combination.QuantityUnit, "QuantityUnit"); - - _writer.WriteStartElement("Pictures"); - foreach (dynamic assignedPicture in combination.Pictures) - { - WritePicture(assignedPicture, "Picture"); - } - _writer.WriteEndElement(); // Pictures - - _writer.WriteEndElement(); // ProductAttributeCombination - } - _writer.WriteEndElement(); // ProductAttributeCombinations - } - - if (product.ProductPictures != null) - { - _writer.WriteStartElement("ProductPictures"); - foreach (dynamic productPicture in product.ProductPictures) - { - _writer.WriteStartElement("ProductPicture"); - _writer.Write("Id", ((int)productPicture.Id).ToString()); - _writer.Write("DisplayOrder", ((int)productPicture.DisplayOrder).ToString()); - - WritePicture(productPicture.Picture, "Picture"); - - _writer.WriteEndElement(); // ProductPicture - } - _writer.WriteEndElement(); // ProductPictures - } - - if (product.ProductCategories != null) - { - _writer.WriteStartElement("ProductCategories"); - foreach (dynamic productCategory in product.ProductCategories) - { - _writer.WriteStartElement("ProductCategory"); - _writer.Write("Id", ((int)productCategory.Id).ToString()); - _writer.Write("DisplayOrder", ((int)productCategory.DisplayOrder).ToString()); - _writer.Write("IsFeaturedProduct", ((bool)productCategory.IsFeaturedProduct).ToString()); - - WriteCategory(productCategory.Category, "Category"); - - _writer.WriteEndElement(); // ProductCategory - } - _writer.WriteEndElement(); // ProductCategories - } - - if (product.ProductManufacturers != null) - { - _writer.WriteStartElement("ProductManufacturers"); - foreach (dynamic productManu in product.ProductManufacturers) - { - _writer.WriteStartElement("ProductManufacturer"); - - _writer.Write("Id", ((int)productManu.Id).ToString()); - _writer.Write("DisplayOrder", ((int)productManu.DisplayOrder).ToString()); - _writer.Write("IsFeaturedProduct", ((bool)productManu.IsFeaturedProduct).ToString()); - - WriteManufacturer(productManu.Manufacturer, "Manufacturer"); - - _writer.WriteEndElement(); // ProductManufacturer - } - _writer.WriteEndElement(); // ProductManufacturers - } - - if (product.ProductSpecificationAttributes != null) - { - _writer.WriteStartElement("ProductSpecificationAttributes"); - foreach (dynamic psa in product.ProductSpecificationAttributes) - { - _writer.WriteStartElement("ProductSpecificationAttribute"); - - _writer.Write("Id", ((int)psa.Id).ToString()); - _writer.Write("ProductId", ((int)psa.ProductId).ToString()); - _writer.Write("SpecificationAttributeOptionId", ((int)psa.SpecificationAttributeOptionId).ToString()); - _writer.Write("AllowFiltering", ((bool)psa.AllowFiltering).ToString()); - _writer.Write("ShowOnProductPage", ((bool)psa.ShowOnProductPage).ToString()); - _writer.Write("DisplayOrder", ((int)psa.DisplayOrder).ToString()); - - dynamic option = psa.SpecificationAttributeOption; - - _writer.WriteStartElement("SpecificationAttributeOption"); - _writer.Write("Id", ((int)option.Id).ToString()); - _writer.Write("SpecificationAttributeId", ((int)option.SpecificationAttributeId).ToString()); - _writer.Write("DisplayOrder", ((int)option.DisplayOrder).ToString()); - _writer.Write("Name", (string)option.Name); - - WriteLocalized(option); - - _writer.WriteStartElement("SpecificationAttribute"); - _writer.Write("Id", ((int)option.SpecificationAttribute.Id).ToString()); - _writer.Write("Name", (string)option.SpecificationAttribute.Name); - _writer.Write("DisplayOrder", ((int)option.SpecificationAttribute.DisplayOrder).ToString()); - - WriteLocalized(option.SpecificationAttribute); - - _writer.WriteEndElement(); // SpecificationAttribute - _writer.WriteEndElement(); // SpecificationAttributeOption - - _writer.WriteEndElement(); // ProductSpecificationAttribute - } - _writer.WriteEndElement(); // ProductSpecificationAttributes - } - - if (product.ProductBundleItems != null) - { - _writer.WriteStartElement("ProductBundleItems"); - foreach (dynamic bundleItem in product.ProductBundleItems) - { - decimal? bundleItemDiscount = bundleItem.Discount; - - _writer.WriteStartElement("ProductBundleItem"); - _writer.Write("Id", ((int)bundleItem.Id).ToString()); - _writer.Write("ProductId", ((int)bundleItem.ProductId).ToString()); - _writer.Write("BundleProductId", ((int)bundleItem.BundleProductId).ToString()); - _writer.Write("Quantity", ((int)bundleItem.Quantity).ToString()); - _writer.Write("Discount", bundleItemDiscount.HasValue ? bundleItemDiscount.Value.ToString(_culture) : ""); - _writer.Write("DiscountPercentage", ((bool)bundleItem.DiscountPercentage).ToString()); - _writer.Write("Name", (string)bundleItem.Name); - _writer.Write("ShortDescription", (string)bundleItem.ShortDescription); - _writer.Write("FilterAttributes", ((bool)bundleItem.FilterAttributes).ToString()); - _writer.Write("HideThumbnail", ((bool)bundleItem.HideThumbnail).ToString()); - _writer.Write("Visible", ((bool)bundleItem.Visible).ToString()); - _writer.Write("Published", ((bool)bundleItem.Published).ToString()); - _writer.Write("DisplayOrder", ((int)bundleItem.DisplayOrder).ToString()); - _writer.Write("CreatedOnUtc", ((DateTime)bundleItem.CreatedOnUtc).ToString(_culture)); - _writer.Write("UpdatedOnUtc", ((DateTime)bundleItem.UpdatedOnUtc).ToString(_culture)); - - WriteLocalized(bundleItem); - - _writer.WriteEndElement(); // ProductBundleItem - } - _writer.WriteEndElement(); // ProductBundleItems - } - - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - - public void WriteCustomer(dynamic customer, string node) - { - if (customer == null) - return; - - if (node.HasValue()) - { - _writer.WriteStartElement(node); - } - - DateTime? lastLoginDateUtc = customer.LastLoginDateUtc; - - _writer.Write("Id", ((int)customer.Id).ToString()); - _writer.Write("CustomerGuid", ((Guid)customer.CustomerGuid).ToString()); - _writer.Write("Username", (string)customer.Username); - _writer.Write("Email", (string)customer.Email); - _writer.Write("PasswordFormatId", ((int)customer.PasswordFormatId).ToString()); - _writer.Write("AdminComment", (string)customer.AdminComment); - _writer.Write("IsTaxExempt", ((bool)customer.IsTaxExempt).ToString()); - _writer.Write("AffiliateId", ((int)customer.AffiliateId).ToString()); - _writer.Write("Active", ((bool)customer.Active).ToString()); - _writer.Write("Deleted", ((bool)customer.Deleted).ToString()); - _writer.Write("IsSystemAccount", ((bool)customer.IsSystemAccount).ToString()); - _writer.Write("SystemName", (string)customer.SystemName); - _writer.Write("LastIpAddress", (string)customer.LastIpAddress); - _writer.Write("CreatedOnUtc", ((DateTime)customer.CreatedOnUtc).ToString(_culture)); - _writer.Write("LastLoginDateUtc", lastLoginDateUtc.HasValue ? lastLoginDateUtc.Value.ToString(_culture) : ""); - _writer.Write("LastActivityDateUtc", ((DateTime)customer.LastActivityDateUtc).ToString(_culture)); - _writer.Write("RewardPointsBalance", ((int)customer._RewardPointsBalance).ToString()); - - if (customer.CustomerRoles != null) - { - _writer.WriteStartElement("CustomerRoles"); - foreach (dynamic role in customer.CustomerRoles) - { - int? taxDisplayType = role.TaxDisplayType; - - _writer.WriteStartElement("CustomerRole"); - _writer.Write("Id", ((int)role.Id).ToString()); - _writer.Write("Name", (string)role.Name); - _writer.Write("FreeShipping", ((bool)role.FreeShipping).ToString()); - _writer.Write("TaxExempt", ((bool)role.TaxExempt).ToString()); - _writer.Write("TaxDisplayType", taxDisplayType.HasValue ? taxDisplayType.Value.ToString() : ""); - _writer.Write("Active", ((bool)role.Active).ToString()); - _writer.Write("IsSystemRole", ((bool)role.IsSystemRole).ToString()); - _writer.Write("SystemName", (string)role.SystemName); - _writer.WriteEndElement(); // CustomerRole - } - _writer.WriteEndElement(); // CustomerRoles - } - - WriteRewardPointsHistory(customer.RewardPointsHistory, "RewardPointsHistories"); - WriteAddress(customer.BillingAddress, "BillingAddress"); - WriteAddress(customer.ShippingAddress, "ShippingAddress"); - - if (customer.Addresses != null) - { - _writer.WriteStartElement("Addresses"); - foreach (dynamic address in customer.Addresses) - { - WriteAddress(address, "Address"); - } - _writer.WriteEndElement(); // Addresses - } - - WriteGenericAttributes(customer); - - if (node.HasValue()) - { - _writer.WriteEndElement(); - } - } - } -} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMap.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMap.cs new file mode 100644 index 0000000000..14a0ac77e9 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMap.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace SmartStore.Services.DataExchange.Import +{ + public class ColumnMap + { + // maps source column to property + private readonly Dictionary _map = new Dictionary(StringComparer.OrdinalIgnoreCase); + + private static bool IsIndexed(string name) + { + return (name.EmptyNull().EndsWith("]") && name.EmptyNull().Contains("[")); + } + + private static string CreateSourceName(string name, string index) + { + if (index.HasValue()) + { + name += String.Concat("[", index, "]"); + } + + return name; + } + + public IReadOnlyDictionary Mappings + { + get { return _map; } + } + + public static bool ParseSourceName(string sourceName, out string nameWithoutIndex, out string index) + { + nameWithoutIndex = sourceName; + index = null; + + var result = true; + + if (sourceName.HasValue() && IsIndexed(sourceName)) + { + var x1 = sourceName.IndexOf('['); + var x2 = sourceName.IndexOf(']', x1); + + if (x1 != -1 && x2 != -1 && x2 > x1) + { + nameWithoutIndex = sourceName.Substring(0, x1); + index = sourceName.Substring(x1 + 1, x2 - x1 - 1); + } + else + { + result = false; + } + } + + return result; + } + + public void AddMapping(string sourceName, string mappedName, string defaultValue = null) + { + AddMapping(sourceName, null, mappedName, defaultValue); + } + + public void AddMapping(string sourceName, string index, string mappedName, string defaultValue = null) + { + Guard.ArgumentNotEmpty(() => sourceName); + Guard.ArgumentNotEmpty(() => mappedName); + + var key = CreateSourceName(sourceName, index); + + _map[key] = new ColumnMappingItem + { + SoureName = key, + MappedName = mappedName, + Default = defaultValue + }; + } + + /// + /// Gets a mapped column value + /// + /// The name of the column to get a mapped value for. + /// The column index, e.g. a language code (de, en etc.) + /// The mapped column value OR - if the name is unmapped - a value with the passed [] + public ColumnMappingItem GetMapping(string sourceName, string index) + { + return GetMapping(CreateSourceName(sourceName, index)); + } + + /// + /// Gets a mapped column value + /// + /// The name of the column to get a mapped value for. + /// The mapped column value OR - if the name is unmapped - the value of the passed + public ColumnMappingItem GetMapping(string sourceName) + { + ColumnMappingItem result; + + if (_map.TryGetValue(sourceName, out result)) + { + return result; + } + + return new ColumnMappingItem { SoureName = sourceName, MappedName = sourceName }; + } + } + + + [JsonObject(MemberSerialization.OptIn)] + public class ColumnMappingItem + { + private bool? _ignored; + + /// + /// The source name + /// + [JsonIgnore] + public string SoureName { get; set; } + + /// + /// The mapped name + /// + [JsonProperty] + public string MappedName { get; set; } + + /// + /// An optional default value + /// + [JsonProperty] + public string Default { get; set; } + + /// + /// Indicates whether to explicitly ignore this property + /// + public bool IgnoreProperty + { + get + { + if (_ignored == null) + { + _ignored = Default != null && Default == "[IGNOREPROPERTY]"; + } + + return _ignored.Value; + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMapConverter.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMapConverter.cs new file mode 100644 index 0000000000..c46b90b82a --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ColumnMapping/ColumnMapConverter.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Newtonsoft.Json; +using SmartStore.ComponentModel; + +namespace SmartStore.Services.DataExchange.Import +{ + public class ColumnMapConverter : TypeConverterBase + { + public ColumnMapConverter() + : base(typeof(object)) + { + } + + public override bool CanConvertFrom(Type type) + { + return type == typeof(string); + } + + public override bool CanConvertTo(Type type) + { + return type == typeof(string); + } + + public override object ConvertFrom(CultureInfo culture, object value) + { + if (value is string) + { + var dict = JsonConvert.DeserializeObject>((string)value); + var map = new ColumnMap(); + + foreach (var kvp in dict) + { + map.AddMapping(kvp.Key, null, kvp.Value.MappedName, kvp.Value.Default); + } + + return map; + } + + return base.ConvertFrom(culture, value); + } + + public T ConvertFrom(string value) + { + if (value.HasValue()) + return (T)ConvertFrom(CultureInfo.InvariantCulture, value); + + return default(T); + } + + public override object ConvertTo(CultureInfo culture, string format, object value, Type to) + { + if (to == typeof(string)) + { + if (value is ColumnMap) + { + return JsonConvert.SerializeObject(((ColumnMap)value).Mappings); + } + else + { + return string.Empty; + } + } + + return base.ConvertTo(culture, format, value, to); + } + + public string ConvertTo(object value) + { + return (string)ConvertTo(CultureInfo.InvariantCulture, null, value, typeof(string)); + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/DataImportTask.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/DataImportTask.cs new file mode 100644 index 0000000000..a25235fe00 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/DataImportTask.cs @@ -0,0 +1,34 @@ +using SmartStore.Services.Tasks; + +namespace SmartStore.Services.DataExchange.Import +{ + // note: namespace persisted in ScheduleTask.Type + public partial class DataImportTask : ITask + { + private readonly IDataImporter _importer; + private readonly IImportProfileService _importProfileService; + + public DataImportTask( + IDataImporter importer, + IImportProfileService importProfileService) + { + _importer = importer; + _importProfileService = importProfileService; + } + + public void Execute(TaskExecutionContext ctx) + { + var profileId = ctx.ScheduleTask.Alias.ToInt(); + var profile = _importProfileService.GetImportProfileById(profileId); + + var request = new DataImportRequest(profile); + + request.ProgressValueSetter = delegate (int val, int max, string msg) + { + ctx.SetProgress(val, max, msg, true); + }; + + _importer.Import(request, ctx.CancellationToken); + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/DataImporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/DataImporter.cs new file mode 100644 index 0000000000..aeb4b81897 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/DataImporter.cs @@ -0,0 +1,360 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Linq; +using SmartStore.Core; +using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Email; +using SmartStore.Core.Localization; +using SmartStore.Core.Logging; +using SmartStore.Services.DataExchange.Csv; +using SmartStore.Services.DataExchange.Import.Internal; +using SmartStore.Services.Localization; +using SmartStore.Services.Messages; +using SmartStore.Services.Security; +using SmartStore.Utilities; + +namespace SmartStore.Services.DataExchange.Import +{ + public partial class DataImporter : IDataImporter + { + private readonly ICommonServices _services; + private readonly IImportProfileService _importProfileService; + private readonly ILanguageService _languageService; + private readonly Func _importerFactory; + private readonly Lazy _emailAccountService; + private readonly Lazy _emailSender; + private readonly Lazy _contactDataSettings; + private readonly Lazy _dataExchangeSettings; + + public DataImporter( + ICommonServices services, + IImportProfileService importProfileService, + ILanguageService languageService, + Func importerFactory, + Lazy emailAccountService, + Lazy emailSender, + Lazy contactDataSettings, + Lazy dataExchangeSettings) + { + _services = services; + _importProfileService = importProfileService; + _languageService = languageService; + _importerFactory = importerFactory; + _emailAccountService = emailAccountService; + _emailSender = emailSender; + _contactDataSettings = contactDataSettings; + _dataExchangeSettings = dataExchangeSettings; + + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + private bool HasPermission(DataImporterContext ctx) + { + if (ctx.Request.HasPermission) + return true; + + var customer = _services.WorkContext.CurrentCustomer; + + if (customer.SystemName == SystemCustomerNames.BackgroundTask) + return true; + + if (ctx.Request.Profile.EntityType == ImportEntityType.Product || ctx.Request.Profile.EntityType == ImportEntityType.Category) + return _services.Permissions.Authorize(StandardPermissionProvider.ManageCatalog, customer); + + if (ctx.Request.Profile.EntityType == ImportEntityType.Customer) + return _services.Permissions.Authorize(StandardPermissionProvider.ManageCustomers, customer); + + if (ctx.Request.Profile.EntityType == ImportEntityType.NewsLetterSubscription) + return _services.Permissions.Authorize(StandardPermissionProvider.ManageNewsletterSubscribers, customer); + + return true; + } + + private void LogResult(DataImporterContext ctx) + { + var result = ctx.ExecuteContext.Result; + var sb = new StringBuilder(); + + sb.AppendLine(); + sb.AppendFormat("Started:\t\t{0}\r\n", result.StartDateUtc.ToLocalTime()); + sb.AppendFormat("Finished:\t\t{0}\r\n", result.EndDateUtc.ToLocalTime()); + sb.AppendFormat("Duration:\t\t{0}\r\n", (result.EndDateUtc - result.StartDateUtc).ToString("g")); + sb.AppendLine(); + sb.AppendFormat("Total rows:\t\t{0}\r\n", result.TotalRecords); + sb.AppendFormat("Rows processed:\t\t{0}\r\n", result.AffectedRecords); + sb.AppendFormat("Records imported:\t{0}\r\n", result.NewRecords); + sb.AppendFormat("Records updated:\t{0}\r\n", result.ModifiedRecords); + sb.AppendLine(); + sb.AppendFormat("Warnings:\t\t{0}\r\n", result.Warnings); + sb.AppendFormat("Errors:\t\t\t{0}", result.Errors); + + ctx.Log.Information(sb.ToString()); + + foreach (var message in result.Messages) + { + if (message.MessageType == ImportMessageType.Error) + ctx.Log.Error(message.ToString(), message.FullMessage); + else if (message.MessageType == ImportMessageType.Warning) + ctx.Log.Warning(message.ToString()); + else + ctx.Log.Information(message.ToString()); + } + } + + private void SendCompletionEmail(DataImporterContext ctx) + { + var emailAccount = _emailAccountService.Value.GetDefaultEmailAccount(); + var smtpContext = new SmtpContext(emailAccount); + var message = new EmailMessage(); + + var store = _services.StoreContext.CurrentStore; + var storeInfo = "{0} ({1})".FormatInvariant(store.Name, store.Url); + var intro = _services.Localization.GetResource("Admin.DataExchange.Import.CompletedEmail.Body").FormatInvariant(storeInfo); + var body = new StringBuilder(intro); + var result = ctx.ExecuteContext.Result; + + if (result.LastError.HasValue()) + { + body.AppendFormat("

    {0}

    ", result.LastError); + } + + body.Append("

    "); + + body.AppendFormat("

    {0}: {1} · {2}: {3}
    ", + T("Admin.Common.TotalRows"), result.TotalRecords, + T("Admin.Common.Skipped"), result.SkippedRecords); + + body.AppendFormat("
    {0}: {1} · {2}: {3}
    ", + T("Admin.Common.NewRecords"), result.NewRecords, + T("Admin.Common.Updated"), result.ModifiedRecords); + + body.AppendFormat("
    {0}: {1} · {2}: {3}
    ", + T("Admin.Common.Errors"), result.Errors, + T("Admin.Common.Warnings"), result.Warnings); + + body.Append("

    "); + + message.From = new EmailAddress(emailAccount.Email, emailAccount.DisplayName); + + if (_contactDataSettings.Value.WebmasterEmailAddress.HasValue()) + message.To.Add(new EmailAddress(_contactDataSettings.Value.WebmasterEmailAddress)); + + if (message.To.Count == 0 && _contactDataSettings.Value.CompanyEmailAddress.HasValue()) + message.To.Add(new EmailAddress(_contactDataSettings.Value.CompanyEmailAddress)); + + if (message.To.Count == 0) + message.To.Add(new EmailAddress(emailAccount.Email, emailAccount.DisplayName)); + + message.Subject = T("Admin.DataExchange.Import.CompletedEmail.Subject").Text.FormatInvariant(ctx.Request.Profile.Name); + + message.Body = body.ToString(); + + _emailSender.Value.SendEmail(smtpContext, message); + + //Core.Infrastructure.EngineContext.Current.Resolve().InsertQueuedEmail(new QueuedEmail + //{ + // From = emailAccount.Email, + // FromName = emailAccount.DisplayName, + // To = message.To.First().Address, + // Subject = message.Subject, + // Body = message.Body, + // CreatedOnUtc = DateTime.UtcNow, + // EmailAccountId = emailAccount.Id, + // SendManually = true + //}); + //_services.DbContext.SaveChanges(); + } + + private void ImportCoreInner(DataImporterContext ctx, string filePath) + { + if (ctx.ExecuteContext.Abort == DataExchangeAbortion.Hard) + return; + + { + var logHead = new StringBuilder(); + logHead.AppendLine(); + logHead.AppendLine(new string('-', 40)); + logHead.AppendLine("SmartStore.NET:\t\tv." + SmartStoreVersion.CurrentFullVersion); + logHead.Append("Import profile:\t\t" + ctx.Request.Profile.Name); + logHead.AppendLine(ctx.Request.Profile.Id == 0 ? " (volatile)" : " (Id {0})".FormatInvariant(ctx.Request.Profile.Id)); + + logHead.AppendLine("Entity:\t\t\t" + ctx.Request.Profile.EntityType.ToString()); + logHead.AppendLine("File:\t\t\t" + Path.GetFileName(filePath)); + + var customer = _services.WorkContext.CurrentCustomer; + logHead.Append("Executed by:\t\t" + (customer.Email.HasValue() ? customer.Email : customer.SystemName)); + + ctx.Log.Information(logHead.ToString()); + } + + if (!File.Exists(filePath)) + { + throw new SmartException("File does not exist {0}.".FormatInvariant(filePath)); + } + + CsvConfiguration csvConfiguration = null; + var extension = Path.GetExtension(filePath); + + if ((new string[] { ".csv", ".txt", ".tab" }).Contains(extension, StringComparer.OrdinalIgnoreCase)) + { + var converter = new CsvConfigurationConverter(); + csvConfiguration = converter.ConvertFrom(ctx.Request.Profile.FileTypeConfiguration); + } + + if (csvConfiguration == null) + { + csvConfiguration = CsvConfiguration.ExcelFriendlyConfiguration; + } + + using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + ctx.ExecuteContext.DataTable = LightweightDataTable.FromFile( + Path.GetFileName(filePath), + stream, + stream.Length, + csvConfiguration, + ctx.Request.Profile.Skip, + ctx.Request.Profile.Take > 0 ? ctx.Request.Profile.Take : int.MaxValue + ); + + try + { + ctx.Importer.Execute(ctx.ExecuteContext); + } + catch (Exception exception) + { + ctx.ExecuteContext.Abort = DataExchangeAbortion.Hard; + ctx.ExecuteContext.Result.AddError(exception, "The importer failed: {0}.".FormatInvariant(exception.ToAllMessages())); + } + + if (ctx.ExecuteContext.IsMaxFailures) + ctx.ExecuteContext.Result.AddWarning("Import aborted. The maximum number of failures has been reached."); + + if (ctx.CancellationToken.IsCancellationRequested) + ctx.ExecuteContext.Result.AddWarning("Import aborted. A cancellation has been requested."); + } + } + + private void ImportCoreOuter(DataImporterContext ctx) + { + if (ctx.Request.Profile == null || !ctx.Request.Profile.Enabled) + return; + + var logPath = ctx.Request.Profile.GetImportLogPath(); + + FileSystemHelper.Delete(logPath); + + using (var logger = new TraceLogger(logPath)) + { + try + { + ctx.Log = logger; + + ctx.ExecuteContext.DataExchangeSettings = _dataExchangeSettings.Value; + ctx.ExecuteContext.Services = _services; + ctx.ExecuteContext.Log = logger; + ctx.ExecuteContext.Languages = _languageService.GetAllLanguages(true); + ctx.ExecuteContext.UpdateOnly = ctx.Request.Profile.UpdateOnly; + ctx.ExecuteContext.KeyFieldNames = ctx.Request.Profile.KeyFieldNames.SplitSafe(","); + ctx.ExecuteContext.ImportFolder = ctx.Request.Profile.GetImportFolder(); + ctx.ExecuteContext.ExtraData = XmlHelper.Deserialize(ctx.Request.Profile.ExtraData); + + { + var mapConverter = new ColumnMapConverter(); + ctx.ExecuteContext.ColumnMap = mapConverter.ConvertFrom(ctx.Request.Profile.ColumnMapping) ?? new ColumnMap(); + } + + var files = ctx.Request.Profile.GetImportFiles(); + + if (files.Count == 0) + throw new SmartException("No files to import."); + + if (!HasPermission(ctx)) + throw new SmartException("You do not have permission to perform the selected import."); + + ctx.Importer = _importerFactory(ctx.Request.Profile.EntityType); + + files.ForEach(x => ImportCoreInner(ctx, x)); + } + catch (Exception exception) + { + ctx.ExecuteContext.Result.AddError(exception); + } + finally + { + try + { + // database context sharing problem: if there are entities in modified state left by the provider due to SaveChanges failure, + // then all subsequent SaveChanges would fail too (e.g. IImportProfileService.UpdateImportProfile, IScheduledTaskService.UpdateTask...). + // so whatever it is, detach\dispose all what the tracker still has tracked. + + _services.DbContext.DetachAll(false); + } + catch (Exception exception) + { + ctx.ExecuteContext.Result.AddError(exception); + } + + try + { + SendCompletionEmail(ctx); + } + catch (Exception exception) + { + ctx.ExecuteContext.Result.AddError(exception); + } + + try + { + ctx.ExecuteContext.Result.EndDateUtc = DateTime.UtcNow; + + LogResult(ctx); + } + catch (Exception exception) + { + logger.ErrorsAll(exception); + } + + try + { + ctx.Request.Profile.ResultInfo = XmlHelper.Serialize(ctx.ExecuteContext.Result.Clone()); + + _importProfileService.UpdateImportProfile(ctx.Request.Profile); + } + catch (Exception exception) + { + logger.ErrorsAll(exception); + } + + try + { + ctx.Request.CustomData.Clear(); + ctx.Log = null; + } + catch (Exception exception) + { + logger.ErrorsAll(exception); + } + } + } + } + + public void Import(DataImportRequest request, CancellationToken cancellationToken) + { + Guard.ArgumentNotNull(() => request); + Guard.ArgumentNotNull(() => cancellationToken); + + var ctx = new DataImporterContext(request, cancellationToken, T("Admin.DataExchange.Import.ProgressInfo")); + + ImportCoreOuter(ctx); + + cancellationToken.ThrowIfCancellationRequested(); + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/DataTable/IDataTable.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/DataTable/IDataTable.cs new file mode 100644 index 0000000000..f35c328fb6 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/DataTable/IDataTable.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; + +namespace SmartStore.Services.DataExchange.Import +{ + public interface IDataColumn + { + string Name { get; } + Type Type { get; } + } + + public interface IDataRow + { + object[] Values { get; } + object this[int index] { get; set; } + object this[string name] { get; set; } + + IDataTable Table { get; } + } + + public interface IDataTable + { + bool HasColumn(string name); + int GetColumnIndex(string name); + IList Columns { get; } + IList Rows { get; } + } + + public static class IDataRowExtensions + { + public static object GetValue(this IDataRow row, int index) + { + return row[index]; + } + + public static object GetValue(this IDataRow row, string name) + { + return row[name]; + } + + public static void SetValue(this IDataRow row, int index, object value) + { + row[index] = value; + } + + public static void SetValue(this IDataRow row, string name, object value) + { + row[name] = value; + } + + public static bool TryGetValue(this IDataRow row, string name, out object value) + { + value = null; + + var index = row.Table.GetColumnIndex(name); + if (index < 0) + return false; + + value = row[index]; + return true; + } + + public static bool TrySetValue(this IDataRow row, string name, object value) + { + var index = row.Table.GetColumnIndex(name); + if (index < 0) + return false; + + row[index] = value; + return true; + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/DataTable/LightweightDataTable.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/DataTable/LightweightDataTable.cs new file mode 100644 index 0000000000..52be0a8dfa --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/DataTable/LightweightDataTable.cs @@ -0,0 +1,406 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Data; +using System.Data.Common; +using System.Dynamic; +using System.IO; +using System.Linq; +using System.Web; +using SmartStore.Services.DataExchange.Csv; +using SmartStore.Services.DataExchange.Excel; + +namespace SmartStore.Services.DataExchange.Import +{ + public class LightweightDataTable : IDataTable + { + private readonly IList _columns; + private readonly IList _rows; + private readonly IDictionary _columnIndexes; + private readonly IDictionary _alternativeColumnIndexes; + + public LightweightDataTable(IList columns, IList data) + { + Guard.ArgumentNotNull(() => columns); + Guard.ArgumentNotNull(() => data); + + if (columns.Select(x => x.Name.ToLower()).Distinct().ToArray().Length != columns.Count) + { + throw Error.Argument("columns", "The columns collection cannot contain duplicate column names."); + } + + _columns = new ReadOnlyCollection(columns); + + TrimData(data); + + var rows = data.Select(x => new LightweightDataRow(this, x)).Cast().ToList(); + _rows = new ReadOnlyCollection(rows); + + _columnIndexes = new Dictionary(StringComparer.OrdinalIgnoreCase); + _alternativeColumnIndexes = new Dictionary(StringComparer.OrdinalIgnoreCase); + + for (var i = 0; i < columns.Count; i++) + { + var name = columns[i].Name; + var alternativeName = GetAlternativeColumnNameFor(name); + + _columnIndexes[name] = i; + + if (!alternativeName.IsCaseInsensitiveEqual(name)) + _alternativeColumnIndexes[alternativeName] = i; + } + } + + private static void TrimData(IList data) + { + // When a user deletes content instead of whole rows from an excel sheet, + // our data table contains completely empty rows at the end. + // Here we get rid of them as they are absolutely useless. + for (int i = data.Count - 1; i >= 0; i--) + { + var allColumnsEmpty = data[i].All(x => x == null || x == DBNull.Value); + if (allColumnsEmpty) + { + data.RemoveAt(i); + //i--; + } + else + { + // get out here on the first occurence of a NON-empty row + break; + } + } + } + + public bool HasColumn(string name) + { + if (name.HasValue()) + { + return (_columnIndexes.ContainsKey(name) || _alternativeColumnIndexes.ContainsKey(name)); + } + + return false; + } + + public int GetColumnIndex(string name) + { + int index; + + if (name.HasValue()) + { + if (_columnIndexes.TryGetValue(name, out index)) + return index; + + if (_alternativeColumnIndexes.TryGetValue(name, out index)) + return index; + } + + return -1; + } + + public IList Columns + { + get + { + return _columns; + } + } + + public IList Rows + { + get + { + return _rows; + } + } + + public static string GetAlternativeColumnNameFor(string name) + { + if (name.IsEmpty()) + return name; + + return name + .Replace(" ", "") + .Replace("-", "") + .Replace("_", ""); + } + + public static IDataTable FromPostedFile( + HttpPostedFileBase file, + int skip = 0, + int take = int.MaxValue) + { + Guard.ArgumentNotNull(() => file); + + return FromFile(file.FileName, file.InputStream, file.ContentLength, new CsvConfiguration(), skip, take); + } + + public static IDataTable FromPostedFile( + HttpPostedFileBase file, + CsvConfiguration configuration, + int skip = 0, + int take = int.MaxValue) + { + Guard.ArgumentNotNull(() => file); + + return FromFile(file.FileName, file.InputStream, file.ContentLength, configuration, skip, take); + } + + public static IDataTable FromFile( + string fileName, + Stream stream, + long contentLength, + CsvConfiguration configuration, + int skip = 0, + int take = int.MaxValue) + { + Guard.ArgumentNotEmpty(() => fileName); + Guard.ArgumentNotNull(() => stream); + Guard.ArgumentNotNull(() => configuration); + + if (contentLength == 0) + { + throw Error.Argument("fileName", "The posted file '{0}' does not contain any data.".FormatInvariant(fileName)); + } + + IDataReader dataReader = null; + + try + { + var fileExt = System.IO.Path.GetExtension(fileName).ToLowerInvariant(); + + switch (fileExt) + { + case ".xlsx": + dataReader = new ExcelDataReader(stream, true); // TODO: let the user specify if excel file has headers + break; + default: + dataReader = new CsvDataReader(new StreamReader(stream), configuration); + break; + } + + var table = LightweightDataTable.FromDataReader(dataReader, skip, take); + + if (table.Columns.Count == 0 || table.Rows.Count == 0) + { + throw Error.InvalidOperation("The posted file '{0}' does not contain any columns or data rows.".FormatInvariant(fileName)); + } + + return table; + } + catch (Exception ex) + { + throw ex; + } + finally + { + if (dataReader != null) + { + if (!dataReader.IsClosed) + { + dataReader.Dispose(); + } + dataReader = null; + } + } + } + + public static IDataTable FromDataReader( + IDataReader reader, + int skip = 0, + int take = int.MaxValue) + { + Guard.ArgumentNotNull(() => reader); + + if (reader.IsClosed) + throw new ArgumentException("This operation is invalid when the reader is closed.", "reader"); + + var columns = new List(reader.FieldCount); + var data = new List(); + + var schema = reader.GetSchemaTable(); + + var nameCol = schema.Columns[SchemaTableColumn.ColumnName]; + var typeCol = schema.Columns[SchemaTableColumn.DataType]; + + foreach (DataRow schemaRow in schema.Rows) + { + var column = new LightweightDataColumn((string)schemaRow[nameCol], (Type)schemaRow[typeCol]); + columns.Add(column); + } + + var fieldCount = reader.FieldCount; + + take = Math.Min(take, int.MaxValue - skip); + + int i = -1; + while (reader.Read()) + { + i++; + + if (skip > i) + continue; + + if (i >= skip + take) + break; + + var values = new object[fieldCount]; + reader.GetValues(values); + data.Add(values); + } + + var table = new LightweightDataTable(columns, data); + + return table; + } + } + + internal class LightweightDataRow : DynamicObject, IDataRow + { + private readonly IDataTable _table; + private readonly object[] _values; + + public LightweightDataRow(IDataTable table, object[] values) + { + Guard.ArgumentNotNull(() => values); + + if (table.Columns.Count != values.Length) + { + throw new ArgumentOutOfRangeException( + "values", + "The number of row values must match the number of columns. Expected: {0}, actual: {1}".FormatInvariant(table.Columns.Count, values.Length)); + } + + _table = table; + _values = values; + } + + public IDataTable Table + { + get { return _table; } + } + + public object[] Values + { + get { return _values; } + } + + public object this[string name] + { + get + { + var index = _table.GetColumnIndex(name); + if (index < 0) + throw new KeyNotFoundException(); + + return _values[index]; + } + set + { + var index = _table.GetColumnIndex(name); + if (index < 0) + throw new KeyNotFoundException(); + + _values[index] = value; + } + } + + public object this[int index] + { + get + { + ValidateColumnIndex(index); + return _values[index]; + } + set + { + ValidateColumnIndex(index); + _values[index] = value; + } + } + + private void ValidateColumnIndex(int index) + { + if (index < 0 || index >= _table.Columns.Count) + { + throw new ArgumentOutOfRangeException("index", index, + "Column index must be included within [0, {0}], but specified column index was: '{1}'.".FormatInvariant(_table.Columns.Count, index)); + } + } + + public override IEnumerable GetDynamicMemberNames() + { + return _table.Columns.Select(x => x.Name); + } + + + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + result = null; + + if (this.TryGetValue(binder.Name, out result)) + { + return true; + } + + return false; + } + + public override bool TrySetMember(SetMemberBinder binder, object value) + { + return this.TrySetValue(binder.Name, value); + } + + public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) + { + result = null; + + try + { + result = _values[(int)indexes[0]]; + return true; + } + catch + { + return false; + } + } + + public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value) + { + try + { + _values[(int)indexes[0]] = value; + return true; + } + catch + { + return false; + } + } + } + + internal class LightweightDataColumn : IDataColumn + { + public LightweightDataColumn(string name, Type type) + { + Guard.ArgumentNotEmpty(() => name); + Guard.ArgumentNotNull(() => type); + + this.Name = name; + this.Type = type; + } + + public string Name + { + get; + private set; + } + + public Type Type + { + get; + private set; + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs new file mode 100644 index 0000000000..eff63d8431 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/EntityImporterBase.cs @@ -0,0 +1,302 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Mime; +using System.Linq; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.IO; +using SmartStore.Utilities; +using System.Linq.Expressions; +using SmartStore.Core; +using Autofac; +using SmartStore.Services.Localization; +using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Seo; +using SmartStore.Services.Seo; +using SmartStore.Core.Data; +using SmartStore.Services.Stores; +using SmartStore.Core.Domain.Stores; + +namespace SmartStore.Services.DataExchange.Import +{ + public abstract class EntityImporterBase : IEntityImporter + { + private const string _imageDownloadFolder = @"Content\DownloadedImages"; + + public DateTime UtcNow + { + get; + private set; + } + + public Dictionary DownloadedItems + { + get; + private set; + } + + public string ImageDownloadFolder + { + get; + private set; + } + + public string ImageFolder + { + get; + private set; + } + + public FileDownloadManagerContext DownloaderContext + { + get; + private set; + } + + public void Execute(ImportExecuteContext context) + { + Import(context); + } + + protected abstract void Import(ImportExecuteContext context); + + protected void Initialize(ImportExecuteContext context) + { + UtcNow = DateTime.UtcNow; + DownloadedItems = new Dictionary(); + ImageDownloadFolder = Path.Combine(context.ImportFolder, _imageDownloadFolder); + + var settings = context.DataExchangeSettings; + + if (settings.ImageImportFolder.HasValue()) + ImageFolder = Path.Combine(context.ImportFolder, settings.ImageImportFolder); + else + ImageFolder = context.ImportFolder; + + if (!System.IO.Directory.Exists(ImageDownloadFolder)) + System.IO.Directory.CreateDirectory(ImageDownloadFolder); + + DownloaderContext = new FileDownloadManagerContext + { + Timeout = TimeSpan.FromMinutes(settings.ImageDownloadTimeout), + Logger = context.Log, + CancellationToken = context.CancellationToken + }; + + context.Result.TotalRecords = context.DataSegmenter.TotalRows; + } + + public FileDownloadManagerItem CreateDownloadImage(string urlOrPath, string seoName, int displayOrder) + { + var item = new FileDownloadManagerItem + { + Id = displayOrder, + DisplayOrder = displayOrder, + MimeType = MimeTypes.MapNameToMimeType(urlOrPath) + }; + + if (item.MimeType.IsEmpty()) + { + item.MimeType = MediaTypeNames.Image.Jpeg; + } + + var extension = MimeTypes.MapMimeTypeToExtension(item.MimeType); + + if (extension.HasValue()) + { + if (urlOrPath.IsWebUrl()) + { + item.Url = urlOrPath; + item.FileName = "{0}-{1}".FormatInvariant(seoName, item.Id).ToValidFileName(); + + if (DownloadedItems.ContainsKey(urlOrPath)) + { + item.Path = Path.Combine(ImageDownloadFolder, DownloadedItems[urlOrPath]); + item.Success = true; + } + else + { + item.Path = Path.Combine(ImageDownloadFolder, item.FileName + extension.EnsureStartsWith(".")); + } + } + else if (Path.IsPathRooted(urlOrPath)) + { + item.Path = urlOrPath; + item.Success = true; + } + else + { + item.Path = Path.Combine(ImageFolder, urlOrPath); + item.Success = true; + } + + return item; + } + + return null; + } + + public void Succeeded(FileDownloadManagerItem item) + { + if ((item.Success ?? false) && item.Url.HasValue() && !DownloadedItems.ContainsKey(item.Url)) + { + DownloadedItems.Add(item.Url, Path.GetFileName(item.Path)); + } + } + + protected virtual int ProcessLocalizations( + ImportExecuteContext context, + IEnumerable> batch, + IDictionary>> localizableProperties) where TEntity : BaseEntity, ILocalizedEntity + { + Guard.ArgumentNotNull(() => context); + Guard.ArgumentNotNull(() => batch); + Guard.ArgumentNotNull(() => localizableProperties); + + // Perf: determine whether our localizable properties actually have + // counterparts in the source BEFORE import batch begins. This way we spare ourself + // to query over and over for values. + var localizedProps = (from kvp in localizableProperties + where context.DataSegmenter.GetColumnIndexes(kvp.Key).Length > 0 + select kvp.Key).ToArray(); + + if (localizedProps.Length == 0) + { + return 0; + } + + var localizedEntityService = context.Services.Resolve(); + + bool shouldSave = false; + + foreach (var row in batch) + { + foreach (var prop in localizedProps) + { + var lambda = localizableProperties[prop]; + foreach (var lang in context.Languages) + { + var code = lang.UniqueSeoCode; + string value; + + if (row.TryGetDataValue(prop /* ColumnName */, code, out value)) + { + localizedEntityService.SaveLocalizedValue(row.Entity, lambda, value, lang.Id); + shouldSave = true; + } + } + } + } + + if (shouldSave) + { + // commit whole batch at once + return context.Services.DbContext.SaveChanges(); + } + + return 0; + } + + protected virtual int ProcessStoreMappings( + ImportExecuteContext context, + IEnumerable> batch) where TEntity : BaseEntity, IStoreMappingSupported + { + var storeMappingService = context.Services.Resolve(); + var storeMappingRepository = context.Services.Resolve>(); + + storeMappingRepository.AutoCommitEnabled = false; + + foreach (var row in batch) + { + var storeIds = row.GetDataValue>("StoreIds"); + if (!storeIds.IsNullOrEmpty()) + { + storeMappingService.SaveStoreMappings(row.Entity, storeIds.ToArray()); + } + } + + // commit whole batch at once + return context.Services.DbContext.SaveChanges(); + } + + protected virtual int ProcessSlugs( + ImportExecuteContext context, + IEnumerable> batch, + string entityName) where TEntity : BaseEntity, ISlugSupported + { + var slugMap = new Dictionary(); + UrlRecord urlRecord = null; + + var urlRecordService = context.Services.Resolve(); + var urlRecordRepository = context.Services.Resolve>(); + var seoSettings = context.Services.Resolve(); + + Func slugLookup = ((s) => + { + return (slugMap.ContainsKey(s) ? slugMap[s] : null); + }); + + foreach (var row in batch) + { + try + { + string seName = null; + string localizedName = null; + + if (row.TryGetDataValue("SeName", out seName) || row.IsNew || row.NameChanged) + { + seName = row.Entity.ValidateSeName(seName, row.EntityDisplayName, true, urlRecordService, seoSettings, extraSlugLookup: slugLookup); + + if (row.IsNew) + { + // dont't bother validating SeName for new entities. + urlRecord = new UrlRecord + { + EntityId = row.Entity.Id, + EntityName = entityName, + Slug = seName, + LanguageId = 0, + IsActive = true, + }; + urlRecordRepository.Insert(urlRecord); + } + else + { + urlRecord = urlRecordService.SaveSlug(row.Entity, seName, 0); + } + + if (urlRecord != null) + { + // a new record was inserted to the store: keep track of it for this batch. + slugMap[seName] = urlRecord; + } + } + + // process localized SeNames + foreach (var lang in context.Languages) + { + var hasSeName = row.TryGetDataValue("SeName", lang.UniqueSeoCode, out seName); + var hasLocalizedName = row.TryGetDataValue("Name", lang.UniqueSeoCode, out localizedName); + + if (hasSeName || hasLocalizedName) + { + seName = row.Entity.ValidateSeName(seName, localizedName, false, urlRecordService, seoSettings, lang.Id, slugLookup); + urlRecord = urlRecordService.SaveSlug(row.Entity, seName, lang.Id); + if (urlRecord != null) + { + slugMap[seName] = urlRecord; + } + } + } + } + catch (Exception exception) + { + context.Result.AddWarning(exception.Message, row.GetRowInfo(), "SeName"); + } + } + + // commit whole batch at once + return context.Services.DbContext.SaveChanges(); + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/IDataImporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/IDataImporter.cs new file mode 100644 index 0000000000..8c2fbebf73 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/IDataImporter.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using SmartStore.Core.Domain; +using SmartStore.Core.Domain.DataExchange; + +namespace SmartStore.Services.DataExchange.Import +{ + public interface IDataImporter + { + void Import(DataImportRequest request, CancellationToken cancellationToken); + } + + + public class DataImportRequest + { + private readonly static ProgressValueSetter _voidProgressValueSetter = DataImportRequest.SetProgress; + + public DataImportRequest(ImportProfile profile) + { + Guard.ArgumentNotNull(() => profile); + + Profile = profile; + ProgressValueSetter = _voidProgressValueSetter; + + EntitiesToImport = new List(); + CustomData = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + public ImportProfile Profile { get; private set; } + + public ProgressValueSetter ProgressValueSetter { get; set; } + + public bool HasPermission { get; set; } + + public IList EntitiesToImport { get; set; } + + public IDictionary CustomData { get; private set; } + + + private static void SetProgress(int val, int max, string msg) + { + // do nothing + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/IEntityImporter.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/IEntityImporter.cs new file mode 100644 index 0000000000..bc64b63531 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/IEntityImporter.cs @@ -0,0 +1,7 @@ +namespace SmartStore.Services.DataExchange.Import +{ + public partial interface IEntityImporter + { + void Execute(ImportExecuteContext context); + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/IImportProfileService.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/IImportProfileService.cs new file mode 100644 index 0000000000..6f7b7d4f8e --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/IImportProfileService.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Linq; +using SmartStore.Core.Domain; +using SmartStore.Core.Domain.DataExchange; + +namespace SmartStore.Services.DataExchange.Import +{ + public interface IImportProfileService + { + /// + /// Gets a new profile name + /// + /// Entity type + /// Suggestion for a new profile name + string GetNewProfileName(ImportEntityType entityType); + + /// + /// Inserts an import profile + /// + /// Name of the import file + /// Profile name + /// Entity type + /// Inserted import profile + ImportProfile InsertImportProfile(string fileName, string name, ImportEntityType entityType); + + /// + /// Updates an import profile + /// + /// Import profile + void UpdateImportProfile(ImportProfile profile); + + /// + /// Deletes an import profile + /// + /// Import profile + void DeleteImportProfile(ImportProfile profile); + + /// + /// Get queryable import profiles + /// + /// Whether to filter enabled or disabled profiles + /// Import profiles + IQueryable GetImportProfiles(bool? enabled = null); + + /// + /// Gets an import profile by identifier + /// + /// Import profile identifier + /// Import profile + ImportProfile GetImportProfileById(int id); + + /// + /// Gets an import profile by name + /// + /// Name of the import profile + /// Import profile + ImportProfile GetImportProfileByName(string name); + + /// + /// Get all importable entity properties and their localized values + /// + /// Import entity type + /// Importable entity properties + Dictionary GetImportableEntityProperties(ImportEntityType entityType); + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportDataSegmenter.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportDataSegmenter.cs new file mode 100644 index 0000000000..41129e24f8 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportDataSegmenter.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.Linq; +using SmartStore.Core; + +namespace SmartStore.Services.DataExchange.Import +{ + public class ImportDataSegmenter + { + private const int BATCHSIZE = 100; + + private readonly IDataTable _table; + private object[] _currentBatch; + private readonly IPageable _pageable; + private bool _bof; + private CultureInfo _culture; + private ColumnMap _columnMap; + + private readonly IDictionary _columnIndexes = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public ImportDataSegmenter(IDataTable table, ColumnMap map) + { + Guard.ArgumentNotNull(() => table); + Guard.ArgumentNotNull(() => map); + + _table = table; + _columnMap = map; + + _bof = true; + _pageable = new PagedList(0, BATCHSIZE, table.Rows.Count); + _culture = CultureInfo.InvariantCulture; + } + + public CultureInfo Culture + { + get + { + return _culture; + } + set + { + _culture = value ?? CultureInfo.InvariantCulture; + } + } + + public ColumnMap ColumnMap + { + get + { + return _columnMap; + } + set + { + _columnMap = value ?? new ColumnMap(); + } + } + + public int TotalRows + { + get { return _table.Rows.Count; } + } + + public int TotalColumns + { + get { return _table.Columns.Count; } + } + + public int CurrentSegment + { + get { return _bof ? 0 : _pageable.PageNumber; } + } + + public int CurrentSegmentFirstRowIndex + { + get { return _pageable.FirstItemIndex; } + } + + public int TotalSegments + { + get { return _pageable.TotalPages; } + } + + public int BatchSize + { + get { return BATCHSIZE; } + } + + /// + /// Determines whether a specific column exists in the underlying data table. + /// + /// The name of the column to find + /// + /// If true and a column with the passed does not exist, + /// this method tests for the existence of any indexed column with the same name. + /// + /// true if the column exists, false otherwise + /// + /// This method takes mapped column names into account. + /// + public bool HasColumn(string name, bool withAnyIndex = false) + { + var result = HasColumn(name, null); + + if (!result && withAnyIndex) + { + // Column does not exist, but withAnyIndex is true: + // Test for existence of any indexed column. + result = GetColumnIndexes(name).Length > 0; + } + + return result; + } + + /// + /// Determines whether the column name[index] exists in the underlying data table. + /// + /// The name of the column to find + /// The index of the column + /// true if the column exists, false otherwise + /// + /// This method takes mapped column names into account. + /// + public bool HasColumn(string name, string index) + { + return _table.HasColumn(_columnMap.GetMapping(name, index).MappedName); + } + + /// + /// Indicates whether to ignore the property that is mapped to columnName + /// + /// The name of the column + /// true ignore, false do not ignore + public bool IsIgnored(string columnName) + { + return IsIgnored(columnName, null); + } + + /// + /// Indicates whether to ignore the property that is mapped to columnName + /// + /// The name of the column + /// The index of the column + /// true ignore, false do not ignore + public bool IsIgnored(string columnName, string index) + { + var mapping = _columnMap.GetMapping(columnName, index); + + return mapping.IgnoreProperty; + } + + /// + /// Returns an array of exisiting index names for a column + /// + /// The name of the columns without index qualification + /// An array of index names + /// + /// If following columns exist in source: Attr[Color], Attr[Size] + /// This method returns: string[] { "Color", "Size" } + /// + public string[] GetColumnIndexes(string name) + { + string[] indexes; + + if (!_columnIndexes.TryGetValue(name, out indexes)) + { + var startsWith = name + "["; + + var columns1 = _columnMap.Mappings + .Where(x => x.Key.StartsWith(startsWith, StringComparison.OrdinalIgnoreCase)) + .Select(x => x.Key); + + var columns2 = _table.Columns + .Where(x => x.Name.StartsWith(startsWith, StringComparison.OrdinalIgnoreCase)) + .Select(x => x.Name); + + indexes = columns1.Concat(columns2) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Select(x => x.Substring(x.IndexOf("[", StringComparison.OrdinalIgnoreCase) + 1).TrimEnd(']')) + .ToArray(); + + _columnIndexes[name] = indexes; + } + + return indexes; + } + + public void Reset() + { + if (_pageable.PageIndex != 0 && _currentBatch != null) + { + _currentBatch = null; + } + _bof = true; + _pageable.PageIndex = 0; + } + + public bool ReadNextBatch() + { + if (_currentBatch != null) + { + _currentBatch = null; + } + + if (_bof) + { + _bof = false; + return _pageable.TotalCount > 0; + } + + if (_pageable.HasNextPage) + { + _pageable.PageIndex++; + return true; + } + + Reset(); + return false; + } + + public IEnumerable> GetCurrentBatch() where T : BaseEntity + { + if (_currentBatch == null) + { + int start = _pageable.FirstItemIndex - 1; + int end = _pageable.LastItemIndex - 1; + + _currentBatch = new ImportRow[(end - start) + 1]; + + // Determine values per row + int i = 0; + for (int r = start; r <= end; r++) + { + _currentBatch[i] = new ImportRow(this, _table.Rows[r], r); + i++; + } + } + + return _currentBatch.Cast>(); + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExecuteContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExecuteContext.cs new file mode 100644 index 0000000000..70170c0fd3 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExecuteContext.cs @@ -0,0 +1,227 @@ +using System.Collections.Generic; +using System.Threading; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Logging; + +namespace SmartStore.Services.DataExchange.Import +{ + public class ImportExecuteContext + { + private DataExchangeAbortion _abortion; + private ProgressValueSetter _progressValueSetter; + private string _progressInfo; + + private IDataTable _dataTable; + private ImportDataSegmenter _segmenter; + + public ImportExecuteContext( + CancellationToken cancellation, + ProgressValueSetter progressValueSetter, + string progressInfo) + { + _progressValueSetter = progressValueSetter; + _progressInfo = progressInfo; + + CancellationToken = cancellation; + CustomProperties = new Dictionary(); + Result = new ImportResult(); + } + + /// + /// Import settings + /// + public DataExchangeSettings DataExchangeSettings + { + get; + internal set; + } + + /// + /// The data source (CSV, Excel etc.) + /// + public IDataTable DataTable + { + get + { + return _dataTable; + } + internal set + { + _dataTable = value; + _segmenter = null; + } + } + + /// + /// Mapping information between database and data source + /// + public ColumnMap ColumnMap + { + get; + internal set; + } + + /// + /// Whether to only update existing records + /// + public bool UpdateOnly + { + get; + internal set; + } + + /// + /// Name of key fields to identify existing records for updating + /// + public string[] KeyFieldNames + { + get; + internal set; + } + + /// + /// All active languages + /// + public IList Languages + { + get; + internal set; + } + + /// + /// To log information into the import log file + /// + public ILogger Log + { + get; + internal set; + } + + /// + /// Common Services + /// + public ICommonServices Services + { + get; + internal set; + } + + /// + /// Cancellation token + /// + public CancellationToken CancellationToken + { + get; + private set; + } + + /// + /// The import folder + /// + public string ImportFolder + { + get; + internal set; + } + + /// + /// Use this dictionary for any custom data required along the import + /// + public Dictionary CustomProperties + { + get; + set; + } + + /// + /// Result of the import + /// + public ImportResult Result + { + get; + set; + } + + /// + /// Extra import configuration data + /// + public ImportExtraData ExtraData + { + get; + internal set; + } + + /// + /// Indicates whether and how to abort the import + /// + public DataExchangeAbortion Abort + { + get + { + if (CancellationToken.IsCancellationRequested || IsMaxFailures) + return DataExchangeAbortion.Hard; + + return _abortion; + } + set + { + _abortion = value; + } + } + + public bool IsMaxFailures + { + get + { + return Result.Errors > 11; + } + } + + public ImportDataSegmenter DataSegmenter + { + get + { + if (_segmenter == null) + { + if (this.DataTable == null || this.ColumnMap == null) + { + throw new SmartException("A DataTable and a ColumnMap must be specified before accessing the DataSegmenter property."); + } + _segmenter = new ImportDataSegmenter(DataTable, ColumnMap); + } + + return _segmenter; + } + } + + /// + /// Allows to set a progress message + /// + /// Progress value + /// /// Progress maximum + public void SetProgress(int value, int maximum) + { + try + { + if (_progressValueSetter != null) + _progressValueSetter.Invoke(value, maximum, _progressInfo.FormatInvariant(value, maximum)); + } + catch { } + } + + /// + /// Allows to set a message + /// + /// Message to display + public void SetProgress(string message) + { + try + { + if (_progressValueSetter != null && message.HasValue()) + _progressValueSetter.Invoke(0, 0, message); + } + catch { } + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExtensions.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExtensions.cs new file mode 100644 index 0000000000..89f576e47a --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExtensions.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using SmartStore.Core.Domain; +using SmartStore.Utilities; + +namespace SmartStore.Services.DataExchange.Import +{ + public static class ImportExtensions + { + /// + /// Get folder for import files + /// + /// Import profile + /// Folder path + public static string GetImportFolder(this ImportProfile profile, bool content = false, bool create = false) + { + var path = CommonHelper.MapPath(string.Concat("~/App_Data/ImportProfiles/", profile.FolderName, content ? "/Content" : "")); + + if (create && !System.IO.Directory.Exists(path)) + System.IO.Directory.CreateDirectory(path); + + return path; + } + + /// + /// Gets import files for an import profile + /// + /// Import profile + /// List of file paths + public static List GetImportFiles(this ImportProfile profile) + { + var importFolder = profile.GetImportFolder(true); + + if (System.IO.Directory.Exists(importFolder)) + { + return System.IO.Directory.EnumerateFiles(importFolder, "*", SearchOption.TopDirectoryOnly) + .OrderBy(x => x) + .ToList(); + } + + return new List(); + } + + /// + /// Get log file path for an import profile + /// + /// Import profile + /// Log file path + public static string GetImportLogPath(this ImportProfile profile) + { + return Path.Combine(profile.GetImportFolder(), "log.txt"); + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExtraData.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExtraData.cs new file mode 100644 index 0000000000..ecbe710677 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportExtraData.cs @@ -0,0 +1,13 @@ +using System; + +namespace SmartStore.Services.DataExchange.Import +{ + [Serializable] + public class ImportExtraData + { + /// + /// Number of images per object to be imported + /// + public int? NumberOfPictures { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Core/Data/Impex/ImportMessage.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportMessage.cs similarity index 64% rename from src/Libraries/SmartStore.Core/Data/Impex/ImportMessage.cs rename to src/Libraries/SmartStore.Services/DataExchange/Import/ImportMessage.cs index e1658cd057..1061e690e7 100644 --- a/src/Libraries/SmartStore.Core/Data/Impex/ImportMessage.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportMessage.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; -using System.Linq; -namespace SmartStore.Core.Data +namespace SmartStore.Services.DataExchange.Import { public class ImportMessage @@ -39,9 +37,28 @@ public string Message set; } + public string FullMessage + { + get; + set; + } + public override string ToString() { - return "{0} - {1}".FormatCurrent(MessageType.ToString().ToUpper(), Message.EmptyNull()); + var result = Message.NaIfEmpty(); + + string appendix = null; + + if (AffectedItem != null) + appendix = appendix.Grow("Pos: " + (AffectedItem.Position + 1).ToString(), ", "); + + if (AffectedField.HasValue()) + appendix = appendix.Grow("Field: " + AffectedField, ", "); + + if (appendix.HasValue()) + result = "{0} [{1}]".FormatInvariant(result, appendix); + + return result; } } diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs new file mode 100644 index 0000000000..c1e54633dc --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs @@ -0,0 +1,411 @@ +using System; +using System.Collections.Generic; +using System.Data.Entity.Core.Metadata.Edm; +using System.Data.Entity.Infrastructure; +using System.Diagnostics; +using System.IO; +using System.Linq; +using SmartStore.Core.Data; +using SmartStore.Core.Domain; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Domain.Tasks; +using SmartStore.Core.Events; +using SmartStore.Core.Localization; +using SmartStore.Services.Catalog.Importer; +using SmartStore.Services.Customers.Importer; +using SmartStore.Services.Localization; +using SmartStore.Services.Messages.Importer; +using SmartStore.Services.Tasks; +using SmartStore.Utilities; + +namespace SmartStore.Services.DataExchange.Import +{ + public partial class ImportProfileService : IImportProfileService + { + private static object _lock = new object(); + private static Dictionary> _entityProperties = null; + + private readonly IRepository _importProfileRepository; + private readonly IEventPublisher _eventPublisher; + private readonly IScheduleTaskService _scheduleTaskService; + private readonly ILocalizationService _localizationService; + private readonly ILanguageService _languageService; + private readonly DataExchangeSettings _dataExchangeSettings; + + public ImportProfileService( + IRepository importProfileRepository, + IEventPublisher eventPublisher, + IScheduleTaskService scheduleTaskService, + ILocalizationService localizationService, + ILanguageService languageService, + DataExchangeSettings dataExchangeSettings) + { + _importProfileRepository = importProfileRepository; + _eventPublisher = eventPublisher; + _scheduleTaskService = scheduleTaskService; + _localizationService = localizationService; + _languageService = languageService; + _dataExchangeSettings = dataExchangeSettings; + } + + private string GetLocalizedPropertyName(ImportEntityType type, string property) + { + if (property.IsEmpty()) + return ""; + + string key = null; + string prefixKey = null; + + if (property.StartsWith("BillingAddress.")) + prefixKey = "Admin.Orders.Fields.BillingAddress"; + else if (property.StartsWith("ShippingAddress.")) + prefixKey = "Admin.Orders.Fields.ShippingAddress"; + + #region Get resource key + + switch (property) + { + case "Id": + key = "Admin.Common.Entity.Fields.Id"; + break; + case "LimitedToStores": + key = "Admin.Common.Store.LimitedTo"; + break; + case "DisplayOrder": + key = "Common.DisplayOrder"; + break; + case "Deleted": + key = "Admin.Common.Deleted"; + break; + case "CreatedOnUtc": + case "BillingAddress.CreatedOnUtc": + case "ShippingAddress.CreatedOnUtc": + key = "Common.CreatedOn"; + break; + case "UpdatedOnUtc": + key = "Common.UpdatedOn"; + break; + case "HasDiscountsApplied": + key = "Admin.Catalog.Products.Fields.HasDiscountsApplied"; + break; + case "DefaultViewMode": + key = "Admin.Configuration.Settings.Catalog.DefaultViewMode"; + break; + case "StoreId": + key = "Admin.Common.Store"; + break; + case "ParentGroupedProductId": + key = "Admin.Catalog.Products.Fields.AssociatedToProductName"; + break; + case "PasswordFormatId": + key = "Admin.Configuration.Settings.CustomerUser.DefaultPasswordFormat"; + break; + case "LastIpAddress": + key = "Admin.Customers.Customers.Fields.IPAddress"; + break; + default: + switch (type) + { + case ImportEntityType.Product: + key = "Admin.Catalog.Products.Fields." + property; + break; + case ImportEntityType.Category: + key = "Admin.Catalog.Categories.Fields." + property; + break; + case ImportEntityType.Customer: + if (property.StartsWith("BillingAddress.") || property.StartsWith("ShippingAddress.")) + key = "Admin.Address.Fields." + property.Substring(property.IndexOf('.') + 1); + else + key = "Admin.Customers.Customers.Fields." + property; + break; + case ImportEntityType.NewsLetterSubscription: + key = "Admin.Promotions.NewsLetterSubscriptions.Fields." + property; + break; + } + break; + } + + #endregion + + if (key.IsEmpty()) + return ""; + + var result = _localizationService.GetResource(key, 0, false, "", true); + + if (result.IsEmpty()) + { + if (key.EndsWith("Id")) + result = _localizationService.GetResource(key.Substring(0, key.Length - 2), 0, false, "", true); + else if (key.EndsWith("Utc")) + result = _localizationService.GetResource(key.Substring(0, key.Length - 3), 0, false, "", true); + } + + if (result.IsEmpty()) + { + Debug.WriteLine("Missing string resource mapping for {0} - {1}".FormatInvariant(type.ToString(), property)); + result = property.SplitPascalCase(); + } + + if (prefixKey.HasValue()) + { + result = string.Concat(_localizationService.GetResource(prefixKey, 0, false, "", true), " - ", result); + } + + return result; + } + + public string GetNewProfileName(ImportEntityType entityType) + { + var defaultNames = _localizationService.GetResource("Admin.DataExchange.Import.DefaultProfileNames").SplitSafe(";"); + + var result = defaultNames.SafeGet((int)entityType); + + if (result.IsEmpty()) + result = entityType.ToString(); + + var profileCount = _importProfileRepository.Table.Count(x => x.EntityTypeId == (int)entityType); + + result = string.Concat(result, " ", profileCount + 1); + + return result; + } + + public virtual ImportProfile InsertImportProfile(string fileName, string name, ImportEntityType entityType) + { + Guard.ArgumentNotEmpty(() => fileName); + + if (name.IsEmpty()) + name = GetNewProfileName(entityType); + + var task = new ScheduleTask + { + CronExpression = "0 */24 * * *", + Type = typeof(DataImportTask).AssemblyQualifiedNameWithoutVersion(), + Enabled = false, + StopOnError = false, + IsHidden = true + }; + + task.Name = string.Concat(name, " Task"); + + _scheduleTaskService.InsertTask(task); + + var profile = new ImportProfile + { + Name = name, + EntityType = entityType, + Enabled = true, + SchedulingTaskId = task.Id + }; + + if (Path.GetExtension(fileName).IsCaseInsensitiveEqual(".xlsx")) + profile.FileType = ImportFileType.XLSX; + else + profile.FileType = ImportFileType.CSV; + + string[] keyFieldNames = null; + + switch (entityType) + { + case ImportEntityType.Product: + keyFieldNames = ProductImporter.DefaultKeyFields; + break; + case ImportEntityType.Category: + keyFieldNames = CategoryImporter.DefaultKeyFields; + break; + case ImportEntityType.Customer: + keyFieldNames = CustomerImporter.DefaultKeyFields; + break; + case ImportEntityType.NewsLetterSubscription: + keyFieldNames = NewsLetterSubscriptionImporter.DefaultKeyFields; + break; + } + + profile.KeyFieldNames = string.Join(",", keyFieldNames); + + profile.FolderName = SeoHelper.GetSeName(name, true, false) + .ToValidPath() + .Truncate(_dataExchangeSettings.MaxFileNameLength); + + profile.FolderName = FileSystemHelper.CreateNonExistingDirectoryName(CommonHelper.MapPath("~/App_Data/ImportProfiles"), profile.FolderName); + + _importProfileRepository.Insert(profile); + + task.Alias = profile.Id.ToString(); + _scheduleTaskService.UpdateTask(task); + + _eventPublisher.EntityInserted(profile); + + return profile; + } + + public virtual void UpdateImportProfile(ImportProfile profile) + { + if (profile == null) + throw new ArgumentNullException("profile"); + + _importProfileRepository.Update(profile); + + _eventPublisher.EntityUpdated(profile); + } + + public virtual void DeleteImportProfile(ImportProfile profile) + { + if (profile == null) + throw new ArgumentNullException("profile"); + + var scheduleTaskId = profile.SchedulingTaskId; + var folder = profile.GetImportFolder(); + + _importProfileRepository.Delete(profile); + + var scheduleTask = _scheduleTaskService.GetTaskById(scheduleTaskId); + _scheduleTaskService.DeleteTask(scheduleTask); + + _eventPublisher.EntityDeleted(profile); + + if (System.IO.Directory.Exists(folder)) + { + FileSystemHelper.ClearDirectory(folder, true); + } + } + + public virtual IQueryable GetImportProfiles(bool? enabled = null) + { + var query = _importProfileRepository.Table + .Expand(x => x.ScheduleTask); + + if (enabled.HasValue) + { + query = query.Where(x => x.Enabled == enabled.Value); + } + + query = query + .OrderBy(x => x.EntityTypeId) + .ThenBy(x => x.Name); + + return query; + } + + public virtual ImportProfile GetImportProfileById(int id) + { + if (id == 0) + return null; + + var profile = _importProfileRepository.Table + .Expand(x => x.ScheduleTask) + .FirstOrDefault(x => x.Id == id); + + return profile; + } + + public virtual ImportProfile GetImportProfileByName(string name) + { + if (name.IsEmpty()) + return null; + + var profile = _importProfileRepository.Table + .Expand(x => x.ScheduleTask) + .FirstOrDefault(x => x.Name == name); + + return profile; + } + + public virtual Dictionary GetImportableEntityProperties(ImportEntityType entityType) + { + if (_entityProperties == null) + { + lock (_lock) + { + if (_entityProperties == null) + { + _entityProperties = new Dictionary>(); + + var context = ((IObjectContextAdapter)_importProfileRepository.Context).ObjectContext; + var container = context.MetadataWorkspace.GetEntityContainer(context.DefaultContainerName, DataSpace.CSpace); + + var allLanguages = _languageService.GetAllLanguages(true); + var allLanguageNames = allLanguages.ToDictionarySafe(x => x.UniqueSeoCode, x => LocalizationHelper.GetLanguageNativeName(x.LanguageCulture) ?? x.Name); + + var localizableProperties = new Dictionary + { + { ImportEntityType.Product, new string[] { "Name", "ShortDescription", "FullDescription", "MetaKeywords", "MetaDescription", "MetaTitle", "SeName" } }, + { ImportEntityType.Category, new string[] { "Name", "FullName", "Description", "BottomDescription", "MetaKeywords", "MetaDescription", "MetaTitle", "SeName" } }, + { ImportEntityType.Customer, new string[] { } }, + { ImportEntityType.NewsLetterSubscription, new string[] { } } + }; + + var addressSet = container.GetEntitySetByName("Addresses", true); + + var addressProperties = addressSet.ElementType.Members + .Where(x => !x.Name.IsCaseInsensitiveEqual("Id") && x.BuiltInTypeKind.HasFlag(BuiltInTypeKind.EdmProperty)) + .Select(x => x.Name) + .ToList(); + + + foreach (ImportEntityType type in Enum.GetValues(typeof(ImportEntityType))) + { + EntitySet entitySet = null; + + try + { + if (type == ImportEntityType.Category) + entitySet = container.GetEntitySetByName("Categories", true); + else + entitySet = container.GetEntitySetByName(type.ToString() + "s", true); + } + catch (Exception) + { + throw new SmartException("There is no entity set for ImportEntityType {0}. Note, the enum value must equal the entity name.".FormatInvariant(type.ToString())); + } + + var dic = entitySet.ElementType.Members + .Where(x => !x.Name.IsCaseInsensitiveEqual("Id") && x.BuiltInTypeKind.HasFlag(BuiltInTypeKind.EdmProperty)) + .Select(x => x.Name) + .ToDictionary(x => x, x => "", StringComparer.OrdinalIgnoreCase); + + // lack of abstractness? + if ((type == ImportEntityType.Product || type == ImportEntityType.Category) && !dic.ContainsKey("SeName")) + { + dic.Add("SeName", ""); + } + + // shipping and billing address + if (type == ImportEntityType.Customer) + { + foreach (var property in addressProperties) + { + dic.Add("BillingAddress." + property, ""); + dic.Add("ShippingAddress." + property, ""); + } + } + + // add localized property names + foreach (var key in dic.Keys.ToList()) + { + var localizedValue = GetLocalizedPropertyName(type, key); + + dic[key] = localizedValue.NaIfEmpty(); + + if (localizableProperties[type].Contains(key)) + { + foreach (var language in allLanguages) + { + dic.Add( + "{0}[{1}]".FormatInvariant(key, language.UniqueSeoCode.EmptyNull().ToLower()), + "{0} {1}".FormatInvariant(localizedValue.NaIfEmpty(), allLanguageNames[language.UniqueSeoCode]) + ); + } + } + } + + _entityProperties.Add(type, dic); + } + } + } + } + + return (_entityProperties.ContainsKey(entityType) ? _entityProperties[entityType] : null); + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportResult.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportResult.cs new file mode 100644 index 0000000000..a8a29366f7 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportResult.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Serialization; + +namespace SmartStore.Services.DataExchange.Import +{ + public class ImportResult : ICloneable + { + public ImportResult() + { + this.Messages = new List(); + Clear(); + } + + public DateTime StartDateUtc + { + get; + set; + } + + public DateTime EndDateUtc + { + get; + set; + } + + public int TotalRecords + { + get; + set; + } + + public int SkippedRecords + { + get; + set; + } + + public int NewRecords + { + get; + set; + } + + public int ModifiedRecords + { + get; + set; + } + + public int AffectedRecords + { + get { return NewRecords + ModifiedRecords; } + } + + public bool Cancelled + { + get; + set; + } + + public void Clear() + { + Messages.Clear(); + StartDateUtc = EndDateUtc = DateTime.UtcNow; + TotalRecords = 0; + SkippedRecords = 0; + NewRecords = 0; + ModifiedRecords = 0; + Cancelled = false; + } + + public ImportMessage AddInfo(string message, ImportRowInfo affectedRow = null, string affectedField = null) + { + return this.AddMessage(message, ImportMessageType.Info, affectedRow, affectedField); + } + + public ImportMessage AddWarning(string message, ImportRowInfo affectedRow = null, string affectedField = null) + { + return this.AddMessage(message, ImportMessageType.Warning, affectedRow, affectedField); + } + + public ImportMessage AddError(string message, ImportRowInfo affectedRow = null, string affectedField = null) + { + return this.AddMessage(message, ImportMessageType.Error, affectedRow, affectedField); + } + + public ImportMessage AddError(Exception exception, int? affectedBatch = null, string stage = null) + { + var prefix = new List(); + if (affectedBatch.HasValue) + { + prefix.Add("Batch: " + affectedBatch.Value); + } + if (stage.HasValue()) + { + prefix.Add("Stage: " + stage); + } + + string msg = string.Empty; + if (prefix.Any()) + { + msg = "[{0}] ".FormatCurrent(String.Join(", ", prefix)); + } + + msg += exception.ToAllMessages(); + + return this.AddMessage(msg, ImportMessageType.Error, fullMessage: exception.StackTrace); + } + + public ImportMessage AddError(Exception exception, string message) + { + return AddMessage( + message ?? exception.ToAllMessages(), + ImportMessageType.Error, + null, + null, + exception.StackTrace); + } + + public ImportMessage AddMessage(string message, ImportMessageType severity, ImportRowInfo affectedRow = null, string affectedField = null, string fullMessage = null) + { + var msg = new ImportMessage(message, severity); + + msg.AffectedItem = affectedRow; + msg.AffectedField = affectedField; + msg.FullMessage = fullMessage; + + this.Messages.Add(msg); + return msg; + } + + public IList Messages + { + get; + private set; + } + + public bool HasWarnings + { + get { return this.Messages.Any(x => x.MessageType == ImportMessageType.Warning); } + } + + public int Warnings + { + get { return Messages.Count(x => x.MessageType == ImportMessageType.Warning); } + } + + public bool HasErrors + { + get { return this.Messages.Any(x => x.MessageType == ImportMessageType.Error); } + } + + public int Errors + { + get { return Messages.Count(x => x.MessageType == ImportMessageType.Error); } + } + + public string LastError + { + get + { + var lastError = Messages.LastOrDefault(x => x.MessageType == ImportMessageType.Error); + if (lastError != null) + return lastError.Message; + + return null; + } + } + + object ICloneable.Clone() + { + return this.Clone(); + } + + public SerializableImportResult Clone() + { + var result = new SerializableImportResult(); + result.StartDateUtc = StartDateUtc; + result.EndDateUtc = EndDateUtc; + result.TotalRecords = TotalRecords; + result.SkippedRecords = SkippedRecords; + result.NewRecords = NewRecords; + result.ModifiedRecords = ModifiedRecords; + result.AffectedRecords = AffectedRecords; + result.Cancelled = Cancelled; + result.Warnings = Warnings; + result.Errors = Errors; + result.LastError = LastError; + + return result; + } + } + + + [Serializable] + public partial class SerializableImportResult + { + public DateTime StartDateUtc { get; set; } + public DateTime EndDateUtc { get; set; } + public int TotalRecords { get; set; } + public int SkippedRecords { get; set; } + public int NewRecords { get; set; } + public int ModifiedRecords { get; set; } + public int AffectedRecords { get; set; } + public bool Cancelled { get; set; } + public int Warnings { get; set; } + public int Errors { get; set; } + public string LastError { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/ImportRow.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportRow.cs new file mode 100644 index 0000000000..1f213ee9e4 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/ImportRow.cs @@ -0,0 +1,331 @@ +using System; +using System.Globalization; +using System.Linq.Expressions; +using SmartStore.ComponentModel; +using SmartStore.Core; + +namespace SmartStore.Services.DataExchange.Import +{ + public class ImportRow where T : BaseEntity + { + private const string ExplicitNull = "[NULL]"; + + private bool _initialized = false; + private T _entity; + private string _entityDisplayName; + private readonly int _position; + private bool _isNew; + private bool _isDirty; + private ImportRowInfo _rowInfo; + + private readonly ImportDataSegmenter _segmenter; + private readonly IDataRow _row; + + public ImportRow(ImportDataSegmenter parent, IDataRow row, int position) + { + _segmenter = parent; + _row = row; + _position = position; + } + + public void Initialize(T entity, string entityDisplayName) + { + _entity = entity; + _entityDisplayName = entityDisplayName; + _isNew = _entity.Id == 0; + + _initialized = true; + } + + private void CheckInitialized() + { + if (_initialized) + { + throw Error.InvalidOperation("A row must be initialized before interacting with the entity or the data store"); + } + } + + private TProp GetDefaultValue(ColumnMappingItem mapping, TProp defaultValue, ImportResult result = null) + { + if (mapping != null && mapping.Default.HasValue()) + { + try + { + return mapping.Default.Convert(_segmenter.Culture); + } + catch (Exception exception) + { + if (result != null) + { + var msg = "Failed to convert default value '{0}'. Please specify a convertable default value. Column: {1}"; + result.AddWarning(msg.FormatInvariant(mapping.Default, exception.Message), this.GetRowInfo(), mapping.SoureName); + } + } + } + + return defaultValue; + } + + public bool IsTransient + { + get { return _entity.Id == 0; } + } + + public bool IsNew + { + get { return _isNew; } + } + + public bool IsDirty + { + get { return _isDirty; } + } + + public ImportDataSegmenter Segmenter + { + get { return _segmenter; } + } + + public T Entity + { + get { return _entity; } + } + + public IDataRow DataRow + { + get { return _row; } + } + + public string EntityDisplayName + { + get { return _entityDisplayName; } + } + + public bool NameChanged + { + get; + set; + } + + public int Position + { + get { return _position; } + } + + /// + /// Determines whether a specific column exists in the underlying data table + /// and contains a non-null, convertible value. + /// + /// The name of the column + /// + /// If true and a column with the passed does not exist, + /// this method seeks for any indexed column with the same name. + /// + /// true if the column exists and contains a value, false otherwise + /// + /// This method takes mapped column names into account. + /// + public bool HasDataValue(string columnName, bool withAnyIndex = false) + { + var result = HasDataValue(columnName, null); + + if (!result && withAnyIndex) + { + // Column does not have a value, but withAnyIndex is true: + // Test for values in any indexed column. + var indexes = _segmenter.GetColumnIndexes(columnName); + foreach (var idx in indexes) + { + result = HasDataValue(columnName, idx); + if (result) + break; + } + } + + return result; + } + + /// + /// Determines whether the column name[index] exists in the underlying data table + /// and contains a non-null, convertible value. + /// + /// The name of the column + /// The index of the column + /// true if the column exists and contains a value, false otherwise + /// + /// This method takes mapped column names into account. + /// + public bool HasDataValue(string columnName, string index) + { + var mapping = _segmenter.ColumnMap.GetMapping(columnName, index); + + object value; + return (_row.TryGetValue(mapping.MappedName, out value) && value != null && value != DBNull.Value); + } + + public TProp GetDataValue(string columnName, bool force = false) + { + TProp value; + TryGetDataValue(columnName, null, out value, force); + return value; + } + + public TProp GetDataValue(string columnName, string index, bool force = false) + { + TProp value; + TryGetDataValue(columnName, index, out value, force); + return value; + } + + public bool TryGetDataValue(string columnName, out TProp value, bool force = false) + { + return TryGetDataValue(columnName, null, out value, force); + } + + public bool TryGetDataValue(string columnName, string index, out TProp value, bool force = false) + { + var mapping = _segmenter.ColumnMap.GetMapping(columnName, index); + + if (!force && mapping.IgnoreProperty) + { + value = default(TProp); + return false; + } + + object rawValue; + if (_row.TryGetValue(mapping.MappedName, out rawValue) && rawValue != null && rawValue != DBNull.Value) + { + value = rawValue.ToString().IsCaseInsensitiveEqual(ExplicitNull) + ? default(TProp) + : rawValue.Convert(_segmenter.Culture); + return true; + } + + if (IsNew) + { + // only transient/new entities should fallback to possible defaults. + value = GetDefaultValue(mapping, default(TProp)); + return true; + } + + value = default(TProp); + return false; + } + + public bool SetProperty( + ImportResult result, + Expression> prop, + TProp defaultValue = default(TProp), + Func converter = null) + { + return SetProperty( + result, + null, // columnName + prop, + defaultValue, + converter); + } + + public bool SetProperty( + ImportResult result, + string columnName, + Expression> prop, + TProp defaultValue = default(TProp), + Func converter = null) + { + // TBD: (MC) do not check or validate for perf reason? + //CheckInitialized(); + + var isPropertySet = false; + var pi = prop.ExtractPropertyInfo(); + var propName = pi.Name; + var target = _entity; + + columnName = columnName ?? propName; + + try + { + object value; + var mapping = _segmenter.ColumnMap.GetMapping(columnName); + + if (mapping.IgnoreProperty) + { + // explicitly ignore this property + } + else if (_row.TryGetValue(mapping.MappedName, out value) && (value != null && value != DBNull.Value)) + { + // source contains field value. Set it. + TProp converted; + if (converter != null) + { + converted = converter(value, _segmenter.Culture); + } + else if (value.ToString().IsCaseInsensitiveEqual(ExplicitNull)) + { + // prop is "explicitly" set to null. Don't fallback to any default! + converted = default(TProp); + } + else + { + converted = value.Convert(_segmenter.Culture); + } + + var fastProp = FastProperty.GetProperty(target.GetUnproxiedType(), propName, PropertyCachingStrategy.EagerCached); + fastProp.SetValue(target, converted); + isPropertySet = true; + } + else + { + // source field value does not exist or is null/empty + if (IsNew) + { + // if entity is new and source field value is null, determine default value in this particular order: + // 2.) Default value in field mapping table + // 3.) passed default value argument + defaultValue = GetDefaultValue(mapping, defaultValue, result); + + // source does not contain field data or is empty... + if (defaultValue != null) + { + // ...but the entity is new. In this case set the default value if given. + var fastProp = FastProperty.GetProperty(target.GetUnproxiedType(), propName, PropertyCachingStrategy.EagerCached); + fastProp.SetValue(target, defaultValue); + isPropertySet = true; + } + } + } + } + catch (Exception exception) + { + result.AddWarning("Conversion failed: " + exception.Message, this.GetRowInfo(), propName); + } + + if (isPropertySet && !_isDirty) + { + _isDirty = true; + } + + return isPropertySet; + } + + public ImportRowInfo GetRowInfo() + { + if (_rowInfo == null) + { + _rowInfo = new ImportRowInfo(this.Position, this.EntityDisplayName); + } + + return _rowInfo; + } + + public override string ToString() + { + var str = "Pos: {0} - Name: {1}, IsNew: {2}, IsTransient: {3}".FormatCurrent( + Position, + EntityDisplayName.EmptyNull(), + _initialized ? IsNew.ToString() : "-", + _initialized ? IsTransient.ToString() : "-"); + return str; + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Import/Internal/DataImporterContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Import/Internal/DataImporterContext.cs new file mode 100644 index 0000000000..41efff1883 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Import/Internal/DataImporterContext.cs @@ -0,0 +1,28 @@ +using System.Threading; +using SmartStore.Core.Logging; + +namespace SmartStore.Services.DataExchange.Import.Internal +{ + internal class DataImporterContext + { + public DataImporterContext( + DataImportRequest request, + CancellationToken cancellationToken, + string progressInfo) + { + Request = request; + CancellationToken = cancellationToken; + + ExecuteContext = new ImportExecuteContext(CancellationToken, Request.ProgressValueSetter, progressInfo); + } + + public DataImportRequest Request { get; private set; } + public CancellationToken CancellationToken { get; private set; } + + public TraceLogger Log { get; set; } + + public ImportExecuteContext ExecuteContext { get; set; } + + public IEntityImporter Importer { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/SyncMappingService.cs b/src/Libraries/SmartStore.Services/DataExchange/SyncMappingService.cs index 76a72aad49..3c3a11551c 100644 --- a/src/Libraries/SmartStore.Services/DataExchange/SyncMappingService.cs +++ b/src/Libraries/SmartStore.Services/DataExchange/SyncMappingService.cs @@ -118,8 +118,6 @@ public void DeleteSyncMappings(string contextName, string entityName = null) { Guard.ArgumentNotEmpty(() => contextName); - Expression> expression = (x) => x.ContextName == contextName; - if (entityName.HasValue()) { _syncMappingsRepository.DeleteAll(x => x.ContextName == contextName && x.EntityName == entityName); diff --git a/src/Libraries/SmartStore.Services/Directory/IStateProvinceService.cs b/src/Libraries/SmartStore.Services/Directory/IStateProvinceService.cs index 95f0c91c2a..e62ff53527 100644 --- a/src/Libraries/SmartStore.Services/Directory/IStateProvinceService.cs +++ b/src/Libraries/SmartStore.Services/Directory/IStateProvinceService.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using SmartStore.Core.Domain.Directory; namespace SmartStore.Services.Directory @@ -14,12 +15,19 @@ public partial interface IStateProvinceService /// The state/province void DeleteStateProvince(StateProvince stateProvince); - /// - /// Gets a state/province - /// - /// The state/province identifier - /// State/province - StateProvince GetStateProvinceById(int stateProvinceId); + /// + /// Get all states/provinces + /// + /// A value indicating whether to show hidden records + /// + IQueryable GetAllStateProvinces(bool showHidden = false); + + /// + /// Gets a state/province + /// + /// The state/province identifier + /// State/province + StateProvince GetStateProvinceById(int stateProvinceId); /// /// Gets a state/province diff --git a/src/Libraries/SmartStore.Services/Directory/StateProvinceService.cs b/src/Libraries/SmartStore.Services/Directory/StateProvinceService.cs index 27ddf6f5a6..b631dca9e9 100644 --- a/src/Libraries/SmartStore.Services/Directory/StateProvinceService.cs +++ b/src/Libraries/SmartStore.Services/Directory/StateProvinceService.cs @@ -8,10 +8,10 @@ namespace SmartStore.Services.Directory { - /// - /// State province service - /// - public partial class StateProvinceService : IStateProvinceService + /// + /// State province service + /// + public partial class StateProvinceService : IStateProvinceService { #region Constants private const string STATEPROVINCES_ALL_KEY = "SmartStore.stateprovince.all-{0}"; @@ -46,6 +46,7 @@ public StateProvinceService(ICacheManager cacheManager, #endregion #region Methods + /// /// Deletes a state/province /// @@ -63,12 +64,22 @@ public virtual void DeleteStateProvince(StateProvince stateProvince) _eventPublisher.EntityDeleted(stateProvince); } - /// - /// Gets a state/province - /// - /// The state/province identifier - /// State/province - public virtual StateProvince GetStateProvinceById(int stateProvinceId) + public virtual IQueryable GetAllStateProvinces(bool showHidden = false) + { + var query = _stateProvinceRepository.Table; + + if (!showHidden) + query = query.Where(x => x.Published); + + return query; + } + + /// + /// Gets a state/province + /// + /// The state/province identifier + /// State/province + public virtual StateProvince GetStateProvinceById(int stateProvinceId) { if (stateProvinceId == 0) return null; diff --git a/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs b/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs index 9b75b86e8b..ffa0fa2615 100644 --- a/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs +++ b/src/Libraries/SmartStore.Services/Discounts/DiscountService.cs @@ -12,6 +12,7 @@ using SmartStore.Services.Customers; using SmartStore.Services.Common; using SmartStore.Services.Configuration; +using SmartStore.Services.Orders; namespace SmartStore.Services.Discounts { @@ -317,18 +318,18 @@ public virtual bool IsDiscountValid(Discount discount, Customer customer, string } //check date range - DateTime now = DateTime.UtcNow; - int storeId = _storeContext.CurrentStore.Id; + var now = DateTime.UtcNow; + var store = _storeContext.CurrentStore; if (discount.StartDateUtc.HasValue) { - DateTime startDate = DateTime.SpecifyKind(discount.StartDateUtc.Value, DateTimeKind.Utc); + var startDate = DateTime.SpecifyKind(discount.StartDateUtc.Value, DateTimeKind.Utc); if (startDate.CompareTo(now) > 0) return false; } if (discount.EndDateUtc.HasValue) { - DateTime endDate = DateTime.SpecifyKind(discount.EndDateUtc.Value, DateTimeKind.Utc); + var endDate = DateTime.SpecifyKind(discount.EndDateUtc.Value, DateTimeKind.Utc); if (endDate.CompareTo(now) < 0) return false; } @@ -336,33 +337,37 @@ public virtual bool IsDiscountValid(Discount discount, Customer customer, string if (!CheckDiscountLimitations(discount, customer)) return false; - // discount requirements - var requirements = discount.DiscountRequirements; + // better not to apply discounts if there are gift cards in the cart cause the customer could "earn" money through that. + if (discount.DiscountType == DiscountType.AssignedToOrderTotal || discount.DiscountType == DiscountType.AssignedToOrderSubTotal) + { + var cart = customer.ShoppingCartItems + .Filter(ShoppingCartType.ShoppingCart, store.Id) + .ToList(); + + if (cart.Any(x => x.Product.IsGiftCard)) + return false; + } + + // discount requirements + var requirements = discount.DiscountRequirements; foreach (var req in requirements) { - var requirementRule = LoadDiscountRequirementRuleBySystemName(req.DiscountRequirementRuleSystemName, storeId); + var requirementRule = LoadDiscountRequirementRuleBySystemName(req.DiscountRequirementRuleSystemName, store.Id); if (requirementRule == null) continue; - var request = new CheckDiscountRequirementRequest() + var request = new CheckDiscountRequirementRequest { DiscountRequirement = req, Customer = customer, - Store = _storeContext.CurrentStore + Store = store }; - if (!requirementRule.Value.CheckRequirement(request)) + + // TODO: cache result... CheckRequirement is very often called + if (!requirementRule.Value.CheckRequirement(request)) return false; } - // better not to apply discounts if there are gift cards in the cart cause the customer could "earn" money through that. - if (discount.DiscountType == DiscountType.AssignedToOrderTotal || discount.DiscountType == DiscountType.AssignedToOrderSubTotal) - { - var cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, storeId); - - if (cart.Any(x => x.Item.Product.IsGiftCard)) - return false; - } - return true; } diff --git a/src/Libraries/SmartStore.Services/Discounts/IDiscountRequirementRule.cs b/src/Libraries/SmartStore.Services/Discounts/IDiscountRequirementRule.cs index 527dcacab1..15325d3141 100644 --- a/src/Libraries/SmartStore.Services/Discounts/IDiscountRequirementRule.cs +++ b/src/Libraries/SmartStore.Services/Discounts/IDiscountRequirementRule.cs @@ -1,6 +1,4 @@ - -using SmartStore.Core.ComponentModel; -using SmartStore.Core.Plugins; +using SmartStore.Core.Plugins; namespace SmartStore.Services.Discounts { diff --git a/src/Libraries/SmartStore.Services/ExportImport/DataSegmenter.cs b/src/Libraries/SmartStore.Services/ExportImport/DataSegmenter.cs deleted file mode 100644 index 303f12a2c5..0000000000 --- a/src/Libraries/SmartStore.Services/ExportImport/DataSegmenter.cs +++ /dev/null @@ -1,370 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Text; -using Fasterflect; -using OfficeOpenXml; -using SmartStore.Core; -using SmartStore.Core.Data; - -namespace SmartStore.Services.ExportImport -{ - - internal class DataSegmenter : DisposableObject where T : BaseEntity - { - private const int BATCHSIZE = 100; - - private ExcelPackage _excelPackage; - private ExcelWorksheet _sheet; - private int _totalRows; - private int _totalColumns; - private readonly string[] _columns; - private readonly IDictionary _properties; - private IList> _currentBatch; - private IPageable _pageable; - private bool _bof; - - public DataSegmenter(Stream source) - { - Guard.ArgumentNotNull(() => source); - - _excelPackage = new ExcelPackage(source); - - // get the first worksheet in the workbook - _sheet = _excelPackage.Workbook.Worksheets.FirstOrDefault(); - if (_sheet == null) - { - throw Error.InvalidOperation("The excel package does not contain any worksheet."); - } - - if (_sheet.Dimension == null) - { - throw Error.InvalidOperation("The excel worksheet does not contain any data."); - } - - _totalColumns = _sheet.Dimension.End.Column; - _totalRows = _sheet.Dimension.End.Row - 1; // excluding 1st - - // Determine column names from 1st row (excel indexes start from 1) - var cols = new List(); - for (int i = 1; i <= _totalColumns; i++) - { - cols.Add(_sheet.Cells[1, i].Text); - } - - _columns = cols.ToArray(); - ValidateColumns(_columns); - _properties = new Dictionary(_columns.Length, StringComparer.InvariantCultureIgnoreCase); - - // determine corresponding Properties for given columns - var t = typeof(T); - foreach (var col in _columns) - { - var pi = t.GetProperty(col); - if (pi != null) - { - _properties[col] = new TargetProperty - { - IsSettable = pi.CanWrite && pi.GetSetMethod().IsPublic, - PropertyInfo = pi - }; - } - } - - _bof = true; - _pageable = new PagedList(0, BATCHSIZE, _totalRows); - } - - public int TotalRows - { - get { return _totalRows; } - } - - public int TotalColumns - { - get { return _totalColumns; } - } - - public int CurrentSegment - { - get { return _bof ? 0 : _pageable.PageNumber; } - } - - public int CurrentSegmentFirstRowIndex - { - get { return _pageable.FirstItemIndex; } - } - - public int TotalSegments - { - get { return _pageable.TotalPages; } - } - - public int BatchSize - { - get { return BATCHSIZE; } - } - - public void Reset() - { - if (_pageable.PageIndex != 0 && _currentBatch != null) - { - _currentBatch.Clear(); - _currentBatch = null; - } - _bof = true; - _pageable.PageIndex = 0; - } - - public bool ReadNextBatch() - { - if (_currentBatch != null) - { - _currentBatch.Clear(); - _currentBatch = null; - } - - if (_bof) - { - _bof = false; - return _pageable.TotalCount > 0; - } - - if (_pageable.HasNextPage) - { - _pageable.PageIndex++; - return true; - } - - Reset(); - return false; - } - - public ICollection> CurrentBatch - { - get - { - if (_currentBatch == null) - { - _currentBatch = new List>(); - - int start = _pageable.FirstItemIndex + 1; - int end = _pageable.LastItemIndex + 1; - - // Determine cell values per row - for (int r = start; r <= end; r++) - { - var values = new List(); - for (int c = 1; c <= _totalColumns; c++) - { - values.Add(_sheet.Cells[r, c].Value); - } - - _currentBatch.Add(new ImportRow(_columns, values.ToArray(), _properties, r - 1)); - } - } - - return _currentBatch.AsReadOnly(); - } - } - - protected override void OnDispose(bool disposing) - { - if (disposing) - { - _sheet = null; - if (_excelPackage != null) - { - _excelPackage.Dispose(); - _excelPackage = null; - } - } - } - - private void ValidateColumns(string[] columns) - { - if (columns.Any(x => x.IsEmpty())) - { - throw Error.InvalidOperation("The first row must contain the column names and therefore cannot have empty cells."); - } - - if (columns.Select(x => x.ToLower()).Distinct().ToArray().Length != columns.Length) - { - throw Error.InvalidOperation("The first row cannot contain duplicate column names."); - } - } - } - - internal class ImportRow : Dictionary where T : BaseEntity - { - private bool _initialized = false; - private T _entity; - private string _entityDisplayName; - private int _position; - private bool _isNew; - private ImportRowInfo _rowInfo; - - public ImportRow(string[] columns, object[] values, IDictionary properties, int position) - : base(columns.Length, StringComparer.InvariantCultureIgnoreCase) - { - _position = position; - - for (int i = 0; i < columns.Length; i++) - { - var col = columns[i]; - var val = values[i]; - - if (val != null && val.ToString().HasValue()) - { - if (!properties.ContainsKey(col) || properties[col].IsSettable) - { - // only add value when no correponding property exists (special field) - // or when property exists but it's publicly settable. - this[col] = val; - } - } - } - } - - public void Initialize(T entity, string entityDisplayName) - { - _entity = entity; - _entityDisplayName = entityDisplayName; - _isNew = _entity.Id == 0; - - _initialized = true; - } - - private void CheckInitialized() - { - if (_initialized) - { - throw Error.InvalidOperation("A row must be initialized before interacting with the entity or the data store"); - } - } - - public bool IsTransient - { - get { return _entity.Id == 0; } - } - - public bool IsNew - { - get { return _isNew; } - } - - public T Entity - { - get { return _entity; } - } - - public string EntityDisplayName - { - get { return _entityDisplayName; } - } - - public bool NameChanged - { - get; - set; - } - - public int Position - { - get { return _position; } - } - - public TProp GetValue(string columnName) - { - object value; - if (this.TryGetValue(columnName, out value)) - { - return value.Convert(); - } - - return default(TProp); - } - - public bool SetProperty(ImportResult result, T target, Expression> prop, TProp defaultValue = default(TProp), Func converter = null) - { - // TBD: (MC) do not check for perf reason? - //CheckInitialized(); - - var pi = prop.ExtractPropertyInfo(); - var propName = pi.Name; - - try - { - object value; - if (this.TryGetValue(propName, out value)) - { - // source contains field value. Set it. - TProp converted; - if (converter != null) - { - converted = converter(value); - } - else - { - if (value.ToString().ToUpper().Equals("NULL")) - { - // prop is set "explicitly" to null. - converted = default(TProp); - } - else - { - converted = value.Convert(); - } - } - return target.TrySetPropertyValue(propName, converted); - } - else - { - // source does not contain field data or it's empty... - if (IsTransient && defaultValue != null) - { - // ...but the entity is new. In this case - // set the default value if given. - return target.TrySetPropertyValue(propName, defaultValue); - } - } - } - catch (Exception ex) - { - result.AddWarning("Conversion failed: " + ex.Message, this.GetRowInfo(), propName); - } - - return false; - } - - public ImportRowInfo GetRowInfo() - { - if (_rowInfo == null) - { - _rowInfo = new ImportRowInfo(this.Position, this.EntityDisplayName); - } - - return _rowInfo; - } - - public override string ToString() - { - var str = "Pos: {0} - Name: {1}, IsNew: {2}, IsTransient: {3}".FormatCurrent( - Position, - EntityDisplayName.EmptyNull(), - _initialized ? IsNew.ToString() : "-", - _initialized ? IsTransient.ToString() : "-"); - return str; - } - } - - - internal class TargetProperty - { - public bool IsSettable { get; set; } - public PropertyInfo PropertyInfo { get; set; } - } -} diff --git a/src/Libraries/SmartStore.Services/ExportImport/IImportManager.cs b/src/Libraries/SmartStore.Services/ExportImport/IImportManager.cs deleted file mode 100644 index fdefe47639..0000000000 --- a/src/Libraries/SmartStore.Services/ExportImport/IImportManager.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using SmartStore.Core.Data; - -namespace SmartStore.Services.ExportImport -{ - /// - /// Import manager interface - /// - public interface IImportManager - { - ImportResult ImportProductsFromExcel( - Stream stream, - CancellationToken cancellationToken, - IProgress progress = null); - - /// - /// Dumps an instance to a string - /// - /// The result instance - /// The report - string CreateTextReport(ImportResult result); - } - - public static class IImportManagerExtensions - { - public static ImportResult ImportProductsFromExcel(this IImportManager importManager, Stream stream) - { - return importManager.ImportProductsFromExcel(stream, CancellationToken.None); - } - } -} diff --git a/src/Libraries/SmartStore.Services/ExportImport/ImportManager.cs b/src/Libraries/SmartStore.Services/ExportImport/ImportManager.cs deleted file mode 100644 index e592aa3b58..0000000000 --- a/src/Libraries/SmartStore.Services/ExportImport/ImportManager.cs +++ /dev/null @@ -1,783 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using SmartStore.Core.Data; -using SmartStore.Core.Domain.Catalog; -using SmartStore.Data; -using SmartStore.Services.Catalog; -using SmartStore.Core.Events; -using SmartStore.Services.Localization; -using SmartStore.Services.Media; -using SmartStore.Services.Seo; -using SmartStore.Utilities; -using System.Text; -using SmartStore.Core.Domain.Seo; -using SmartStore.Core.Domain.Media; -using SmartStore.Services.Stores; -using SmartStore.Core.Domain.Stores; -using SmartStore.Core; - -namespace SmartStore.Services.ExportImport -{ - /// - /// Import manager - /// - public partial class ImportManager : IImportManager - { - private readonly IProductService _productService; - private readonly ICategoryService _categoryService; - private readonly IManufacturerService _manufacturerService; - private readonly IPictureService _pictureService; - private readonly IUrlRecordService _urlRecordService; - private readonly SeoSettings _seoSettings; - private readonly IEventPublisher _eventPublisher; - private readonly IRepository _rsProduct; - private readonly IRepository _rsProductCategory; - private readonly IRepository _rsProductManufacturer; - private readonly IRepository _rsPicture; - private readonly IRepository _rsProductPicture; - private readonly IRepository _rsUrlRecord; - private readonly ILanguageService _languageService; - private readonly ILocalizedEntityService _localizedEntityService; - private readonly IStoreMappingService _storeMappingService; - - public ImportManager( - IProductService productService, - ICategoryService categoryService, - IManufacturerService manufacturerService, - IPictureService pictureService, - IUrlRecordService urlRecordService, - SeoSettings seoSettings, - IEventPublisher eventPublisher, - IRepository rsProduct, - IRepository rsProductCategory, - IRepository rsProductManufacturer, - IRepository rsPicture, - IRepository rsProductPicture, - IRepository rsUrlRecord, - ILanguageService languageService, - ILocalizedEntityService localizedEntityService, - IStoreMappingService storeMappingService) - { - this._productService = productService; - this._categoryService = categoryService; - this._manufacturerService = manufacturerService; - this._pictureService = pictureService; - this._urlRecordService = urlRecordService; - this._seoSettings = seoSettings; - this._eventPublisher = eventPublisher; - this._rsProduct = rsProduct; - this._rsProductCategory = rsProductCategory; - this._rsProductManufacturer = rsProductManufacturer; - this._rsProductPicture = rsProductPicture; - this._rsUrlRecord = rsUrlRecord; - this._rsPicture = rsPicture; - this._languageService = languageService; - this._localizedEntityService = localizedEntityService; - this._storeMappingService = storeMappingService; - } - - public virtual string CreateTextReport(ImportResult result) - { - var sb = new StringBuilder(); - - using (var writer = new StringWriter(sb)) - { - writer.WriteLine("SUMMARY"); - writer.WriteLine("=================================================================================="); - writer.WriteLine("Started: {0}".FormatCurrent(result.StartDateUtc.ToLocalTime())); - writer.WriteLine("Finished: {0}{1}".FormatCurrent(result.EndDateUtc.ToLocalTime(), result.Cancelled ? " (cancelled by user)" : "")); - writer.WriteLine("Duration: {0}".FormatCurrent((result.EndDateUtc - result.StartDateUtc).ToString("g"))); - - writer.WriteLine(""); - writer.WriteLine("Total rows in source: {0}".FormatCurrent(result.TotalRecords)); - writer.WriteLine("Rows processed: {0}".FormatCurrent(result.AffectedRecords)); - writer.WriteLine("Products imported: {0}".FormatCurrent(result.NewRecords)); - writer.WriteLine("Products updated: {0}".FormatCurrent(result.ModifiedRecords)); - - writer.WriteLine(""); - writer.WriteLine("Warnings: {0}".FormatCurrent(result.Messages.Count(x => x.MessageType == ImportMessageType.Warning))); - writer.WriteLine("Errors: {0}".FormatCurrent(result.Messages.Count(x => x.MessageType == ImportMessageType.Error))); - - writer.WriteLine(""); - writer.WriteLine(""); - writer.WriteLine("MESSAGES"); - writer.WriteLine("=================================================================================="); - - foreach (var message in result.Messages) - { - string msg = string.Empty; - var prefix = new List(); - if (message.AffectedItem != null) - { - prefix.Add("Pos: " + message.AffectedItem.Position + 1); - } - if (message.AffectedField.HasValue()) - { - prefix.Add("Field: " + message.AffectedField); - } - - if (prefix.Any()) - { - msg = "[{0}] ".FormatCurrent(String.Join(", ", prefix)); - } - - msg += message.Message; - - writer.WriteLine("{0}: {1}".FormatCurrent(message.MessageType.ToString().ToUpper(), msg)); - } - } - - return sb.ToString(); - } - - /// - /// Import products from XLSX file - /// - /// Stream - public virtual ImportResult ImportProductsFromExcel( - Stream stream, - CancellationToken cancellationToken, - IProgress progress = null) - { - Guard.ArgumentNotNull(() => stream); - - var result = new ImportResult(); - int saved = 0; - - if (progress != null) - progress.Report(new ImportProgressInfo { ElapsedTime = TimeSpan.Zero }); - - using (var scope = new DbContextScope(ctx: _rsProduct.Context, autoDetectChanges: false, proxyCreation: false, validateOnSave: false)) - { - try { - using (var segmenter = new DataSegmenter(stream)) - { - result.TotalRecords = segmenter.TotalRows; - - while (segmenter.ReadNextBatch() && !cancellationToken.IsCancellationRequested) - { - var batch = segmenter.CurrentBatch; - - // Perf: detach all entities - _rsProduct.Context.DetachAll(false); - - // Update progress for calling thread - if (progress != null) - { - progress.Report(new ImportProgressInfo - { - TotalRecords = result.TotalRecords, - TotalProcessed = segmenter.CurrentSegmentFirstRowIndex - 1, - NewRecords = result.NewRecords, - ModifiedRecords = result.ModifiedRecords, - ElapsedTime = DateTime.UtcNow - result.StartDateUtc, - TotalWarnings = result.Messages.Count(x => x.MessageType == ImportMessageType.Warning), - TotalErrors = result.Messages.Count(x => x.MessageType == ImportMessageType.Error), - }); - } - - // =========================================================================== - // 1.) Import products - // =========================================================================== - try - { - saved = ProcessProducts(batch, result); - } - catch (Exception ex) - { - result.AddError(ex, segmenter.CurrentSegment, "ProcessProducts"); - } - - // reduce batch to saved (valid) products. - // No need to perform import operations on errored products. - batch = batch.Where(x => x.Entity != null && !x.IsTransient).AsReadOnly(); - - // update result object - result.NewRecords += batch.Count(x => x.IsNew && !x.IsTransient); - result.ModifiedRecords += batch.Count(x => !x.IsNew && !x.IsTransient); - - // =========================================================================== - // 2.) Import SEO Slugs - // IMPORTANT: Unlike with Products AutoCommitEnabled must be TRUE, - // as Slugs are going to be validated against existing ones in DB. - // =========================================================================== - if (batch.Any(x => x.IsNew || (x.ContainsKey("SeName") || x.NameChanged))) - { - try - { - _rsProduct.Context.AutoDetectChangesEnabled = true; - ProcessSlugs(batch, result); - } - catch (Exception ex) - { - result.AddError(ex, segmenter.CurrentSegment, "ProcessSeoSlugs"); - } - finally - { - _rsProduct.Context.AutoDetectChangesEnabled = false; - } - } - - // =========================================================================== - // 3.) Import Localizations - // =========================================================================== - try - { - ProcessLocalizations(batch, result); - } - catch (Exception ex) - { - result.AddError(ex, segmenter.CurrentSegment, "ProcessLocalizations"); - } - - // =========================================================================== - // 4.) Import product category mappings - // =========================================================================== - if (batch.Any(x => x.ContainsKey("CategoryIds"))) - { - try - { - ProcessProductCategories(batch, result); - } - catch (Exception ex) - { - result.AddError(ex, segmenter.CurrentSegment, "ProcessProductCategories"); - } - } - - // =========================================================================== - // 5.) Import product manufacturer mappings - // =========================================================================== - if (batch.Any(x => x.ContainsKey("ManufacturerIds"))) - { - try - { - ProcessProductManufacturers(batch, result); - } - catch (Exception ex) - { - result.AddError(ex, segmenter.CurrentSegment, "ProcessProductManufacturers"); - } - } - - - // =========================================================================== - // 6.) Import product picture mappings - // =========================================================================== - if (batch.Any(x => x.ContainsKey("Picture1") || x.ContainsKey("Picture2") || x.ContainsKey("Picture3"))) - { - try - { - ProcessProductPictures(batch, result); - } - catch (Exception ex) - { - result.AddError(ex, segmenter.CurrentSegment, "ProcessProductPictures"); - } - } - - } - } - } - catch (Exception ex) - { - result.AddError(ex, null, "ReadFile"); - } - } - - result.EndDateUtc = DateTime.UtcNow; - - if (cancellationToken.IsCancellationRequested) - { - result.Cancelled = true; - result.AddInfo("Import task was cancelled by user"); - } - - return result; - } - - private int ProcessProducts(ICollection> batch, ImportResult result) - { - _rsProduct.AutoCommitEnabled = true; - - Product lastInserted = null; - Product lastUpdated = null; - - foreach (var row in batch) - { - if (row.Count == 0) - continue; - - Product product = null; - - object key; - - // try get by int ID - if (row.TryGetValue("Id", out key) && key.ToString().ToInt() > 0) - { - product = _productService.GetProductById(key.ToString().ToInt()); - } - - // try get by SKU - if (product == null && row.TryGetValue("SKU", out key)) - { - product = _productService.GetProductBySku(key.ToString()); - } - - // try get by GTIN - if (product == null && row.TryGetValue("Gtin", out key)) - { - product = _productService.GetProductByGtin(key.ToString()); - } - - if (product == null) - { - // a Name is required with new products. - if (!row.ContainsKey("Name")) - { - result.AddError("The 'Name' field is required for new products. Skipping row.", row.GetRowInfo(), "Name"); - continue; - } - product = new Product(); - } - - row.Initialize(product, row["Name"].ToString()); - - if (!row.IsNew) - { - if (!product.Name.Equals(row["Name"].ToString(), StringComparison.OrdinalIgnoreCase)) - { - // Perf: use this later for SeName updates. - row.NameChanged = true; - } - } - - row.SetProperty(result, product, (x) => x.Sku); - row.SetProperty(result, product, (x) => x.Gtin); - row.SetProperty(result, product, (x) => x.ManufacturerPartNumber); - row.SetProperty(result, product, (x) => x.ProductTypeId, (int)ProductType.SimpleProduct); - row.SetProperty(result, product, (x) => x.ParentGroupedProductId); - row.SetProperty(result, product, (x) => x.VisibleIndividually, true); - row.SetProperty(result, product, (x) => x.Name); - row.SetProperty(result, product, (x) => x.ShortDescription); - row.SetProperty(result, product, (x) => x.FullDescription); - row.SetProperty(result, product, (x) => x.ProductTemplateId); - row.SetProperty(result, product, (x) => x.ShowOnHomePage); - row.SetProperty(result, product, (x) => x.HomePageDisplayOrder); - row.SetProperty(result, product, (x) => x.MetaKeywords); - row.SetProperty(result, product, (x) => x.MetaDescription); - row.SetProperty(result, product, (x) => x.MetaTitle); - row.SetProperty(result, product, (x) => x.AllowCustomerReviews, true); - row.SetProperty(result, product, (x) => x.Published, true); - row.SetProperty(result, product, (x) => x.IsGiftCard); - row.SetProperty(result, product, (x) => x.GiftCardTypeId); - row.SetProperty(result, product, (x) => x.RequireOtherProducts); - row.SetProperty(result, product, (x) => x.RequiredProductIds); - row.SetProperty(result, product, (x) => x.AutomaticallyAddRequiredProducts); - row.SetProperty(result, product, (x) => x.IsDownload); - row.SetProperty(result, product, (x) => x.DownloadId); - row.SetProperty(result, product, (x) => x.UnlimitedDownloads, true); - row.SetProperty(result, product, (x) => x.MaxNumberOfDownloads, 10); - row.SetProperty(result, product, (x) => x.DownloadActivationTypeId, 1); - row.SetProperty(result, product, (x) => x.HasSampleDownload); - row.SetProperty(result, product, (x) => x.SampleDownloadId, (int?)null, ZeroToNull); - row.SetProperty(result, product, (x) => x.HasUserAgreement); - row.SetProperty(result, product, (x) => x.UserAgreementText); - row.SetProperty(result, product, (x) => x.IsRecurring); - row.SetProperty(result, product, (x) => x.RecurringCycleLength, 100); - row.SetProperty(result, product, (x) => x.RecurringCyclePeriodId); - row.SetProperty(result, product, (x) => x.RecurringTotalCycles, 10); - row.SetProperty(result, product, (x) => x.IsShipEnabled, true); - row.SetProperty(result, product, (x) => x.IsFreeShipping); - row.SetProperty(result, product, (x) => x.AdditionalShippingCharge); - row.SetProperty(result, product, (x) => x.IsEsd); - row.SetProperty(result, product, (x) => x.IsTaxExempt); - row.SetProperty(result, product, (x) => x.TaxCategoryId, 1); - row.SetProperty(result, product, (x) => x.ManageInventoryMethodId); - row.SetProperty(result, product, (x) => x.StockQuantity, 10000); - row.SetProperty(result, product, (x) => x.DisplayStockAvailability); - row.SetProperty(result, product, (x) => x.DisplayStockQuantity); - row.SetProperty(result, product, (x) => x.MinStockQuantity); - row.SetProperty(result, product, (x) => x.LowStockActivityId); - row.SetProperty(result, product, (x) => x.NotifyAdminForQuantityBelow, 1); - row.SetProperty(result, product, (x) => x.BackorderModeId); - row.SetProperty(result, product, (x) => x.AllowBackInStockSubscriptions); - row.SetProperty(result, product, (x) => x.OrderMinimumQuantity, 1); - row.SetProperty(result, product, (x) => x.OrderMaximumQuantity, 10000); - row.SetProperty(result, product, (x) => x.AllowedQuantities); - row.SetProperty(result, product, (x) => x.DisableBuyButton); - row.SetProperty(result, product, (x) => x.DisableWishlistButton); - row.SetProperty(result, product, (x) => x.AvailableForPreOrder); - row.SetProperty(result, product, (x) => x.CallForPrice); - row.SetProperty(result, product, (x) => x.Price); - row.SetProperty(result, product, (x) => x.OldPrice); - row.SetProperty(result, product, (x) => x.ProductCost); - row.SetProperty(result, product, (x) => x.SpecialPrice); - row.SetProperty(result, product, (x) => x.SpecialPriceStartDateTimeUtc, null, OADateToUtcDate); - row.SetProperty(result, product, (x) => x.SpecialPriceEndDateTimeUtc, null, OADateToUtcDate); - row.SetProperty(result, product, (x) => x.CustomerEntersPrice); - row.SetProperty(result, product, (x) => x.MinimumCustomerEnteredPrice); - row.SetProperty(result, product, (x) => x.MaximumCustomerEnteredPrice, 1000); - row.SetProperty(result, product, (x) => x.Weight); - row.SetProperty(result, product, (x) => x.Length); - row.SetProperty(result, product, (x) => x.Width); - row.SetProperty(result, product, (x) => x.Height); - row.SetProperty(result, product, (x) => x.DeliveryTimeId); - row.SetProperty(result, product, (x) => x.QuantityUnitId); - row.SetProperty(result, product, (x) => x.BasePriceEnabled); - row.SetProperty(result, product, (x) => x.BasePriceMeasureUnit); - row.SetProperty(result, product, (x) => x.BasePriceAmount); - row.SetProperty(result, product, (x) => x.BasePriceBaseAmount); - row.SetProperty(result, product, (x) => x.BundlePerItemPricing); - row.SetProperty(result, product, (x) => x.BundlePerItemShipping); - row.SetProperty(result, product, (x) => x.BundlePerItemShoppingCart); - row.SetProperty(result, product, (x) => x.BundleTitleText); - row.SetProperty(result, product, (x) => x.AvailableStartDateTimeUtc, null, OADateToUtcDate); - row.SetProperty(result, product, (x) => x.AvailableEndDateTimeUtc, null, OADateToUtcDate); - row.SetProperty(result, product, (x) => x.LimitedToStores); - - string storeIds = row.GetValue("StoreIds"); - if (storeIds.HasValue()) - { - _storeMappingService.SaveStoreMappings(product, - row["StoreIds"].ToString() - .Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries).Select(x => Convert.ToInt32(x.Trim())).ToArray()); - } - - row.SetProperty(result, product, (x) => x.CreatedOnUtc, DateTime.UtcNow, OADateToUtcDate); - - product.UpdatedOnUtc = DateTime.UtcNow; - - if (row.IsTransient) - { - _rsProduct.Insert(product); - lastInserted = product; - } - else - { - _rsProduct.Update(product); - lastUpdated = product; - } - } - - // commit whole batch at once - var num = _rsProduct.Context.SaveChanges(); - - // Perf: notify only about LAST insertion and update - if (lastInserted != null) - _eventPublisher.EntityInserted(lastInserted); - if (lastUpdated != null) - _eventPublisher.EntityUpdated(lastUpdated); - - return num; - } - - private int ProcessSlugs(ICollection> batch, ImportResult result) - { - var slugMap = new Dictionary(100); - Func slugLookup = ((s) => { - if (slugMap.ContainsKey(s)) - { - return slugMap[s]; - } - return (UrlRecord)null; - }); - - var entityName = typeof(Product).Name; - - foreach (var row in batch) - { - if (row.IsNew || row.NameChanged || row.ContainsKey("SeName")) - { - try - { - string seName = row.GetValue("SeName"); - seName = row.Entity.ValidateSeName(seName, row.Entity.Name, true, _urlRecordService, _seoSettings, extraSlugLookup: slugLookup); - - UrlRecord urlRecord = null; - - if (row.IsNew) - { - // dont't bother validating SeName for new entities. - urlRecord = new UrlRecord - { - EntityId = row.Entity.Id, - EntityName = entityName, - Slug = seName, - LanguageId = 0, - IsActive = true, - }; - _rsUrlRecord.Insert(urlRecord); - } - else - { - urlRecord = _urlRecordService.SaveSlug(row.Entity, seName, 0); - } - - if (urlRecord != null) - { - // a new record was inserted to the store: keep track of it for this batch. - slugMap[seName] = urlRecord; - } - } - catch (Exception ex) - { - result.AddWarning(ex.Message, row.GetRowInfo(), "SeName"); - } - } - } - - // commit whole batch at once - return _rsUrlRecord.Context.SaveChanges(); - } - - private int ProcessLocalizations(ICollection> batch, ImportResult result) - { - //_rsProductManufacturer.AutoCommitEnabled = false; - - //string lastInserted = null; - - var languages = _languageService.GetAllLanguages(true); - - foreach (var row in batch) - { - - Product product = null; - - //get product - try - { - product = _productService.GetProductById(row.Entity.Id); - } - catch (Exception ex) - { - result.AddWarning(ex.Message, row.GetRowInfo(), "ProcessLocalizations Product"); - } - - foreach (var lang in languages) - { - string localizedName = row.GetValue("Name[" + lang.UniqueSeoCode + "]"); - string localizedShortDescription = row.GetValue("ShortDescription[" + lang.UniqueSeoCode + "]"); - string localizedFullDescription = row.GetValue("FullDescription[" + lang.UniqueSeoCode + "]"); - - if (localizedName.HasValue()) - { - _localizedEntityService.SaveLocalizedValue(product, x => x.Name, localizedName, lang.Id); - } - if (localizedShortDescription.HasValue()) - { - _localizedEntityService.SaveLocalizedValue(product, x => x.ShortDescription, localizedShortDescription, lang.Id); - } - if (localizedFullDescription.HasValue()) - { - _localizedEntityService.SaveLocalizedValue(product, x => x.FullDescription, localizedFullDescription, lang.Id); - } - } - } - - // commit whole batch at once - var num = _rsProductManufacturer.Context.SaveChanges(); - - // Perf: notify only about LAST insertion and update - //if (lastInserted != null) - // _eventPublisher.EntityInserted(lastInserted); - - return num; - } - - - private int ProcessProductCategories(ICollection> batch, ImportResult result) - { - _rsProductCategory.AutoCommitEnabled = false; - - ProductCategory lastInserted = null; - - foreach (var row in batch) - { - string categoryIds = row.GetValue("CategoryIds"); - if (categoryIds.HasValue()) - { - try - { - foreach (var id in categoryIds.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries).Select(x => Convert.ToInt32(x.Trim()))) - { - if (_rsProductCategory.TableUntracked.Where(x => x.ProductId == row.Entity.Id && x.CategoryId == id).FirstOrDefault() == null) - { - // ensure that category exists - var category = _categoryService.GetCategoryById(id); - if (category != null) - { - var productCategory = new ProductCategory - { - ProductId = row.Entity.Id, - CategoryId = category.Id, - IsFeaturedProduct = false, - DisplayOrder = 1 - }; - _rsProductCategory.Insert(productCategory); - lastInserted = productCategory; - } - } - } - } - catch (Exception ex) - { - result.AddWarning(ex.Message, row.GetRowInfo(), "CategoryIds"); - } - } - } - - // commit whole batch at once - var num = _rsProductCategory.Context.SaveChanges(); - - // Perf: notify only about LAST insertion and update - if (lastInserted != null) - _eventPublisher.EntityInserted(lastInserted); - - return num; - } - - private int ProcessProductManufacturers(ICollection> batch, ImportResult result) - { - _rsProductManufacturer.AutoCommitEnabled = false; - - ProductManufacturer lastInserted = null; - - foreach (var row in batch) - { - string manufacturerIds = row.GetValue("ManufacturerIds"); - if (manufacturerIds.HasValue()) - { - try - { - foreach (var id in manufacturerIds.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries).Select(x => Convert.ToInt32(x.Trim()))) - { - if (_rsProductManufacturer.TableUntracked.Where(x => x.ProductId == row.Entity.Id && x.ManufacturerId == id).FirstOrDefault() == null) - { - // ensure that manufacturer exists - var manufacturer = _manufacturerService.GetManufacturerById(id); - if (manufacturer != null) - { - var productManufacturer = new ProductManufacturer - { - ProductId = row.Entity.Id, - ManufacturerId = manufacturer.Id, - IsFeaturedProduct = false, - DisplayOrder = 1 - }; - _rsProductManufacturer.Insert(productManufacturer); - lastInserted = productManufacturer; - } - } - } - } - catch (Exception ex) - { - result.AddWarning(ex.Message, row.GetRowInfo(), "ManufacturerIds"); - } - } - } - - // commit whole batch at once - var num = _rsProductManufacturer.Context.SaveChanges(); - - // Perf: notify only about LAST insertion and update - if (lastInserted != null) - _eventPublisher.EntityInserted(lastInserted); - - return num; - } - - private void ProcessProductPictures(ICollection> batch, ImportResult result) - { - // true, cause pictures must be saved and assigned an id - // prior adding a mapping. - _rsProductPicture.AutoCommitEnabled = true; - - ProductPicture lastInserted = null; - int equalPictureId = 0; - - foreach (var row in batch) - { - var pictures = new string[] - { - row.GetValue("Picture1"), - row.GetValue("Picture2"), - row.GetValue("Picture3") - }; - - int i = 0; - try - { - for (i = 0; i < pictures.Length; i++) - { - var picture = pictures[i]; - - if (picture.IsEmpty() || !File.Exists(picture)) - continue; - - var currentPictures = _rsProductPicture.TableUntracked.Expand(x => x.Picture).Where(x => x.ProductId == row.Entity.Id).Select(x => x.Picture).ToList(); - var pictureBinary = _pictureService.FindEqualPicture(picture, currentPictures, out equalPictureId); - - if (pictureBinary != null && pictureBinary.Length > 0) - { - // no equal picture found in sequence - var newPicture = _pictureService.InsertPicture(pictureBinary, "image/jpeg", _pictureService.GetPictureSeName(row.EntityDisplayName), true, false, false); - if (newPicture != null) - { - var mapping = new ProductPicture - { - ProductId = row.Entity.Id, - PictureId = newPicture.Id, - DisplayOrder = 1, - }; - _rsProductPicture.Insert(mapping); - lastInserted = mapping; - } - } - else - { - result.AddInfo("Found equal picture in data store. Skipping field.", row.GetRowInfo(), "Picture" + (i + 1).ToString()); - } - } - } - catch (Exception ex) - { - result.AddWarning(ex.Message, row.GetRowInfo(), "Picture" + (i + 1).ToString()); - } - - } - - // Perf: notify only about LAST insertion and update - if (lastInserted != null) - _eventPublisher.EntityInserted(lastInserted); - } - - private DateTime? OADateToUtcDate(object value) - { - double oaDate; - if (CommonHelper.TryConvert(value, out oaDate) && oaDate != 0) - { - return DateTime.FromOADate(Convert.ToDouble(oaDate)); - } - - return null; - } - - - private int? ZeroToNull(object value) - { - int result; - if (CommonHelper.TryConvert(value, out result) && result > 0) - { - return result; - } - - return (int?)null; - } - } -} diff --git a/src/Libraries/SmartStore.Services/Extensions/NameValueCollectionExtensions.cs b/src/Libraries/SmartStore.Services/Extensions/NameValueCollectionExtensions.cs index a2e53aa8da..216894b2d2 100644 --- a/src/Libraries/SmartStore.Services/Extensions/NameValueCollectionExtensions.cs +++ b/src/Libraries/SmartStore.Services/Extensions/NameValueCollectionExtensions.cs @@ -13,6 +13,7 @@ namespace SmartStore { public static class NameValueCollectionExtensions { + // TODO: find place to make it public static private static string AttributeFormatedName(int productAttributeId, int attributeId, int productId = 0, int bundleItemId = 0) { if (productId == 0) @@ -25,34 +26,57 @@ public static void AddProductAttribute(this NameValueCollection collection, int { if (productAttributeId != 0 && attributeId != 0 && valueId != 0) { - string name = AttributeFormatedName(productAttributeId, attributeId, productId, bundleItemId); + var name = AttributeFormatedName(productAttributeId, attributeId, productId, bundleItemId); collection.Add(name, valueId.ToString()); } } /// - /// Converts attribute query data + /// Get selected attributes from query string /// - /// Name value collection + /// Name value collection with selected attributes + /// Query string parameters /// Attribute query data items with following structure: /// Product.Id, ProductAttribute.Id, Product_ProductAttribute_Mapping.Id, ProductVariantAttributeValue.Id, [BundleItem.Id] /// Product identifier to filter - public static void ConvertAttributeQueryData(this NameValueCollection collection, List> queryData, int productId = 0) + public static void GetSelectedAttributes(this NameValueCollection collection, NameValueCollection queryString, List> attributes, int productId = 0) { - if (collection == null || queryData == null || queryData.Count <= 0) - return; + Guard.NotNull(() => collection); - var items = queryData.Where(i => i.Count > 3); + // ambiguous parameters: let other query string parameters win over the json formatted attributes parameter + if (queryString != null && queryString.Count > 0) + { + var items = queryString.AllKeys + .Where(x => x.EmptyNull().StartsWith("product_attribute_")) + .SelectMany(queryString.GetValues, (k, v) => new { key = k.EmptyNull(), value = v.TrimSafe() }); - if (productId != 0) - items = items.Where(i => i[0] == productId); + foreach (var item in items) + { + var ids = item.key.Replace("product_attribute_", "").SplitSafe("_"); + if (ids.Count() > 3) + { + if (productId == 0 || (productId != 0 && productId == ids[0].ToInt())) + { + collection.Add(item.key, item.value); + } + } + } + } - foreach (var item in items) + if (attributes != null && attributes.Count > 0) { - var name = AttributeFormatedName(item[1], item[2], item[0], item.Count > 4 ? item[4] : 0); + var items = attributes.Where(i => i.Count > 3); + + if (productId != 0) + items = items.Where(i => i[0] == productId); - collection.Add(name, item[3].ToString()); + foreach (var item in items) + { + var name = AttributeFormatedName(item[1], item[2], item[0], item.Count > 4 ? item[4] : 0); + + collection.Add(name, item[3].ToString()); + } } } @@ -88,7 +112,7 @@ public static string CreateSelectedAttributesXml(this NameValueCollection collec var ctrlAttributes = collection[controlId]; if (ctrlAttributes.HasValue()) { - int selectedAttributeId = int.Parse(ctrlAttributes); + var selectedAttributeId = ctrlAttributes.SplitSafe(",").SafeGet(0).ToInt(); if (selectedAttributeId > 0) selectedAttributes = productAttributeParser.AddProductAttribute(selectedAttributes, attribute, selectedAttributeId.ToString()); } @@ -102,7 +126,7 @@ public static string CreateSelectedAttributesXml(this NameValueCollection collec { foreach (var item in cblAttributes.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { - int selectedAttributeId = int.Parse(item); + var selectedAttributeId = item.SplitSafe(",").SafeGet(0).ToInt(); if (selectedAttributeId > 0) selectedAttributes = productAttributeParser.AddProductAttribute(selectedAttributes, attribute, selectedAttributeId.ToString()); } diff --git a/src/Libraries/SmartStore.Services/Filter/FilterCriteria.cs b/src/Libraries/SmartStore.Services/Filter/FilterCriteria.cs index 5470367d49..6bbd9756f9 100644 --- a/src/Libraries/SmartStore.Services/Filter/FilterCriteria.cs +++ b/src/Libraries/SmartStore.Services/Filter/FilterCriteria.cs @@ -38,6 +38,8 @@ public class FilterCriteria : IComparable // Metadata public int MatchCount { get; set; } + public int DisplayOrder { get; set; } + public int DisplayOrderValues { get; set; } public bool IsInactive { get; set; } public string NameLocalized { get; set; } public string ValueLocalized { get; set; } @@ -46,7 +48,7 @@ public string SqlName { get { - if (Entity == "Manufacturer" && !Name.Contains('.')) + if (Entity.IsCaseInsensitiveEqual("Manufacturer") && !Name.Contains('.')) return "{0}.{1}".FormatInvariant(Entity, Name); return Name; @@ -76,19 +78,6 @@ int IComparable.CompareTo(object obj) } return compare; - - - //int compare = 0; - - //if (this.Name.HasValue() && filter.Name.HasValue()) - // compare = string.Compare(this.Name, filter.Name, true); - //else - // compare = string.Compare(this.Entity, filter.Entity, true); - - //if (compare != 0) - // return compare; - - //return string.Compare(this.Value, filter.Value, true); } public override string ToString() diff --git a/src/Libraries/SmartStore.Services/Filter/FilterExtensions.cs b/src/Libraries/SmartStore.Services/Filter/FilterExtensions.cs index cd9996d010..56f0655f26 100644 --- a/src/Libraries/SmartStore.Services/Filter/FilterExtensions.cs +++ b/src/Libraries/SmartStore.Services/Filter/FilterExtensions.cs @@ -36,7 +36,7 @@ public static string ToDescription(this FilterCriteria criteria) string valueLeft, valueRight; criteria.Value.SplitToPair(out valueLeft, out valueRight, "~"); - if (criteria.Entity == FilterService.ShortcutPrice) + if (criteria.Entity.IsCaseInsensitiveEqual(FilterService.ShortcutPrice)) return "{0} - {1}".FormatInvariant(FormatPrice(valueLeft), FormatPrice(valueRight)); return "{0} - {1}".FormatInvariant(valueLeft, valueRight); @@ -44,7 +44,7 @@ public static string ToDescription(this FilterCriteria criteria) var value = (criteria.ValueLocalized.HasValue() ? criteria.ValueLocalized : criteria.Value); - if (criteria.Entity == FilterService.ShortcutPrice) + if (criteria.Entity.IsCaseInsensitiveEqual(FilterService.ShortcutPrice)) value = FormatPrice(criteria.Value); if (criteria.Operator == FilterOperator.Unequal) @@ -161,7 +161,7 @@ public static string GetUrl(this FilterProductContext context, FilterCriteria cr if (criteriaAdd != null) criterias.Add(criteriaAdd); else - criterias.RemoveAll(c => c.Entity == criteriaRemove.Entity && c.Name == criteriaRemove.Name && c.Value == criteriaRemove.Value); + criterias.RemoveAll(c => c.Entity.IsCaseInsensitiveEqual(criteriaRemove.Entity) && c.Name.IsCaseInsensitiveEqual(criteriaRemove.Name) && c.Value.IsCaseInsensitiveEqual(criteriaRemove.Value)); if (criterias.Count > 0) url = "{0}&filter={1}".FormatInvariant(url, HttpUtility.UrlEncode(JsonConvert.SerializeObject(criterias))); @@ -179,7 +179,10 @@ public static bool IsActive(this FilterProductContext context, FilterCriteria cr { if (criteria != null && context.Criteria != null) { - return (context.Criteria.FirstOrDefault(c => c.Entity == criteria.Entity && c.Name == criteria.Name && c.Value == criteria.Value && !c.IsInactive) != null); + var foundCriteria = context.Criteria.FirstOrDefault(c => !c.IsInactive && + c.Entity.IsCaseInsensitiveEqual(criteria.Entity) && c.Name.IsCaseInsensitiveEqual(criteria.Name) && c.Value.IsCaseInsensitiveEqual(criteria.Value)); + + return (foundCriteria != null); } return false; } diff --git a/src/Libraries/SmartStore.Services/Filter/FilterOperator.cs b/src/Libraries/SmartStore.Services/Filter/FilterOperator.cs index 7ae4fe9609..ed49bed2b5 100644 --- a/src/Libraries/SmartStore.Services/Filter/FilterOperator.cs +++ b/src/Libraries/SmartStore.Services/Filter/FilterOperator.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Newtonsoft.Json; namespace SmartStore.Services.Filter @@ -23,7 +19,8 @@ public sealed class FilterOperator private readonly string _name; - private FilterOperator(string name) { + private FilterOperator(string name) + { this._name = name; } @@ -39,11 +36,14 @@ private FilterOperator(string name) { public static readonly FilterOperator RangeGreaterEqualLessEqual = new FilterOperator(_rangeGreaterEqualLessEqual); public static readonly FilterOperator RangeGreaterEqualLess = new FilterOperator(_rangeGreaterEqualLess); - public override string ToString() { + public override string ToString() + { return _name ?? "="; } - public static FilterOperator Parse(string op) { - switch (op) { + public static FilterOperator Parse(string op) + { + switch (op) + { case _equal: return FilterOperator.Equal; case _unequal: @@ -56,38 +56,46 @@ public static FilterOperator Parse(string op) { return FilterOperator.Less; case _lessEqual: return FilterOperator.LessEqual; - case _contains: - return FilterOperator.Contains; - case _startWith: - return FilterOperator.StartsWith; - case _endsWith: - return FilterOperator.EndsWith; case _rangeGreaterEqualLessEqual: return FilterOperator.RangeGreaterEqualLessEqual; case _rangeGreaterEqualLess: return FilterOperator.RangeGreaterEqualLess; + default: + if (op.IsCaseInsensitiveEqual(_contains)) + return FilterOperator.Contains; + + if (op.IsCaseInsensitiveEqual(_startWith)) + return FilterOperator.StartsWith; + + if (op.IsCaseInsensitiveEqual(_endsWith)) + return FilterOperator.EndsWith; + return null; } } - } // class + } public class OperatorConverter : JsonConverter { - public override bool CanConvert(Type objectType) { + public override bool CanConvert(Type objectType) + { return objectType == typeof(FilterOperator); } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { writer.WriteValue(value.ToString()); } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - if (reader != null && reader.TokenType == JsonToken.String) { + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader != null && reader.TokenType == JsonToken.String) + { return FilterOperator.Parse(reader.Value as string); } return null; } - } // class + } } diff --git a/src/Libraries/SmartStore.Services/Filter/FilterService.cs b/src/Libraries/SmartStore.Services/Filter/FilterService.cs index 93673743aa..507b1f2fed 100644 --- a/src/Libraries/SmartStore.Services/Filter/FilterService.cs +++ b/src/Libraries/SmartStore.Services/Filter/FilterService.cs @@ -5,12 +5,12 @@ using System.Linq.Dynamic; using System.Text; using Newtonsoft.Json; +using SmartStore.ComponentModel; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Infrastructure; using SmartStore.Services.Catalog; using SmartStore.Services.Localization; -using SmartStore.Utilities; namespace SmartStore.Services.Filter { @@ -79,16 +79,16 @@ private object FilterValueToObject(string value, string type) //if (curlyBracketFormatting) // return value.FormatWith("\"{0}\"", value.Replace("\"", "\"\"")); - Type t = Type.GetType("System.{0}".FormatWith(ValidateValue(type, _defaultType))); + Type t = Type.GetType("System.{0}".FormatInvariant(ValidateValue(type, _defaultType))); - var result = CommonHelper.GetTypeConverter(t).ConvertFromString(null, CultureInfo.InvariantCulture, value); + var result = TypeConverterFactory.GetConverter(t).ConvertFrom(CultureInfo.InvariantCulture, value); return result; } private bool IsShortcut(FilterSql context, FilterCriteria itm, ref int index) { - if (itm.Entity == ShortcutPrice) + if (itm.Entity.IsCaseInsensitiveEqual(ShortcutPrice)) { // TODO: where clause of special price not correct. product can appear in price and in special price range. @@ -119,7 +119,7 @@ private bool IsShortcut(FilterSql context, FilterCriteria itm, ref int index) context.Values.Add(DateTime.UtcNow); } } - else if (itm.Entity == ShortcutSpecAttribute) + else if (itm.Entity.IsCaseInsensitiveEqual(ShortcutSpecAttribute)) { context.WhereClause.AppendFormat("SpecificationAttributeOptionId {0} {1}", itm.Operator == null ? "=" : itm.Operator.ToString(), FormatParameterIndex(ref index)); @@ -253,19 +253,28 @@ from pm in p.ProductManufacturers var grouped = from m in manus - orderby m.DisplayOrder group m by m.Id into grp orderby grp.Key select new FilterCriteria { MatchCount = grp.Count(), - Value = grp.FirstOrDefault().Name + Value = grp.FirstOrDefault().Name, + DisplayOrder = grp.FirstOrDefault().DisplayOrder }; - grouped = grouped.OrderByDescending(m => m.MatchCount); + if (_catalogSettings.SortFilterResultsByMatches) + { + grouped = grouped.OrderByDescending(m => m.MatchCount); + } + else + { + grouped = grouped.OrderBy(m => m.DisplayOrder); + } if (!getAll) + { grouped = grouped.Take(_catalogSettings.MaxFilterItemsToDisplay); + } var lst = grouped.ToList(); @@ -281,17 +290,20 @@ orderby grp.Key private List ProductFilterableSpecAttributes(FilterProductContext context, string attributeName = null) { + List criterias = null; + var languageId = _services.WorkContext.WorkingLanguage.Id; var query = ProductFilter(context); var attributes = from p in query from sa in p.ProductSpecificationAttributes where sa.AllowFiltering - orderby sa.DisplayOrder select sa.SpecificationAttributeOption; if (attributeName.HasValue()) + { attributes = attributes.Where(a => a.SpecificationAttribute.Name == attributeName); + } var grouped = from a in attributes @@ -302,14 +314,28 @@ from a in attributes Value = g.FirstOrDefault().Name, ID = g.Key.Id, PId = g.FirstOrDefault().SpecificationAttribute.Id, - MatchCount = g.Count() + MatchCount = g.Count(), + DisplayOrder = g.FirstOrDefault().SpecificationAttribute.DisplayOrder, + DisplayOrderValues = g.FirstOrDefault().DisplayOrder }; + if (_catalogSettings.SortFilterResultsByMatches) + { + criterias = grouped + .OrderBy(a => a.DisplayOrder) + .ThenByDescending(a => a.MatchCount) + .ThenBy(a => a.DisplayOrderValues) + .ToList(); + } + else + { + criterias = grouped + .OrderBy(a => a.DisplayOrder) + .ThenBy(a => a.DisplayOrderValues) + .ToList(); + } - var lst = grouped.OrderByDescending(a => a.MatchCount).ToList(); - int languageId = _services.WorkContext.WorkingLanguage.Id; - - lst.ForEach(c => + criterias.ForEach(c => { c.Entity = ShortcutSpecAttribute; c.IsInactive = true; @@ -321,7 +347,21 @@ from a in attributes c.ValueLocalized = _localizedEntityService.GetLocalizedValue(languageId, c.ID.Value, "SpecificationAttributeOption", "Name"); }); - return lst; + return criterias; + } + + private void AddChildCategoryIds(List result, int categoryId) + { + var ids = _categoryService.GetAllCategoriesByParentCategoryId(categoryId).Select(x => x.Id); + + foreach (var id in ids) + { + if (!result.Contains(id)) + { + result.Add(id); + AddChildCategoryIds(result, id); + } + } } public virtual List Deserialize(string jsonData) @@ -363,14 +403,12 @@ public virtual FilterProductContext CreateFilterProductContext(string filter, in if (_catalogSettings.ShowProductsFromSubcategories) { - context.CategoryIds.AddRange( - _categoryService.GetAllCategoriesByParentCategoryId(categoryID).Select(x => x.Id) - ); + AddChildCategoryIds(context.CategoryIds, categoryID); } int languageId = _services.WorkContext.WorkingLanguage.Id; - foreach (var criteria in context.Criteria.Where(x => x.Entity == ShortcutSpecAttribute)) + foreach (var criteria in context.Criteria.Where(x => x.Entity.IsCaseInsensitiveEqual(ShortcutSpecAttribute))) { if (criteria.PId.HasValue) criteria.NameLocalized = _localizedEntityService.GetLocalizedValue(languageId, criteria.PId.Value, "SpecificationAttribute", "Name"); @@ -471,13 +509,13 @@ public virtual IQueryable ProductFilter(FilterProductContext context) var query = AllProducts(context.CategoryIds); // prices - if (ToWhereClause(sql, context.Criteria, c => !c.IsInactive && c.Entity == ShortcutPrice)) + if (ToWhereClause(sql, context.Criteria, c => !c.IsInactive && c.Entity.IsCaseInsensitiveEqual(ShortcutPrice))) { query = query.Where(sql.WhereClause.ToString(), sql.Values.ToArray()); } // manufacturer - if (ToWhereClause(sql, context.Criteria, c => !c.IsInactive && c.Entity == "Manufacturer")) + if (ToWhereClause(sql, context.Criteria, c => !c.IsInactive && c.Entity.IsCaseInsensitiveEqual("Manufacturer"))) { var pmq = from p in query @@ -491,7 +529,7 @@ from pm in p.ProductManufacturers } // specification attribute - if (ToWhereClause(sql, context.Criteria, c => !c.IsInactive && (c.Entity == "SpecificationAttributeOption" || c.Entity == ShortcutSpecAttribute))) + if (ToWhereClause(sql, context.Criteria, c => !c.IsInactive && (c.Entity.IsCaseInsensitiveEqual("SpecificationAttributeOption") || c.Entity.IsCaseInsensitiveEqual(ShortcutSpecAttribute)))) { //var saq = ( // from p in query @@ -501,7 +539,7 @@ from pm in p.ProductManufacturers //query = saq.Select(sa => sa.Product); int countSameNameAttributes = sql.Criteria - .Where(c => c.Entity == ShortcutSpecAttribute) + .Where(c => c.Entity.IsCaseInsensitiveEqual(ShortcutSpecAttribute)) .GroupBy(c => c.Name) .Count(); @@ -556,10 +594,10 @@ join sa in saq on p.Id equals sa.Key public virtual void ProductFilterable(FilterProductContext context) { - if (context.Criteria.FirstOrDefault(c => c.Entity == FilterService.ShortcutPrice) == null) + if (context.Criteria.FirstOrDefault(c => c.Entity.IsCaseInsensitiveEqual(FilterService.ShortcutPrice)) == null) context.Criteria.AddRange(ProductFilterablePrices(context)); - if (context.Criteria.FirstOrDefault(c => c.Name == "Name" && c.Entity == "Manufacturer") == null) + if (context.Criteria.FirstOrDefault(c => c.Name.IsCaseInsensitiveEqual("Name") && c.Entity.IsCaseInsensitiveEqual("Manufacturer")) == null) context.Criteria.AddRange(ProductFilterableManufacturer(context)); context.Criteria.AddRange(ProductFilterableSpecAttributes(context)); @@ -572,13 +610,13 @@ public virtual void ProductFilterableMultiSelect(FilterProductContext context, s if (criteriaMultiSelect != null) { - context.Criteria.RemoveAll(c => c.Name == criteriaMultiSelect.Name && c.Entity == criteriaMultiSelect.Entity); + context.Criteria.RemoveAll(c => c.Name.IsCaseInsensitiveEqual(criteriaMultiSelect.Name) && c.Entity.IsCaseInsensitiveEqual(criteriaMultiSelect.Entity)); - if (criteriaMultiSelect.Name == "Name" && criteriaMultiSelect.Entity == "Manufacturer") + if (criteriaMultiSelect.Name.IsCaseInsensitiveEqual("Name") && criteriaMultiSelect.Entity.IsCaseInsensitiveEqual("Manufacturer")) inactive = ProductFilterableManufacturer(context, true); - else if (criteriaMultiSelect.Entity == FilterService.ShortcutPrice) + else if (criteriaMultiSelect.Entity.IsCaseInsensitiveEqual(FilterService.ShortcutPrice)) inactive = ProductFilterablePrices(context); - else if (criteriaMultiSelect.Entity == FilterService.ShortcutSpecAttribute) + else if (criteriaMultiSelect.Entity.IsCaseInsensitiveEqual(FilterService.ShortcutSpecAttribute)) inactive = ProductFilterableSpecAttributes(context, criteriaMultiSelect.Name); } diff --git a/src/Libraries/SmartStore.Services/Forums/ForumService.cs b/src/Libraries/SmartStore.Services/Forums/ForumService.cs index c9f9e63dbd..5871f34ab6 100644 --- a/src/Libraries/SmartStore.Services/Forums/ForumService.cs +++ b/src/Libraries/SmartStore.Services/Forums/ForumService.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.ServiceModel.Syndication; -using System.Web.Mvc; using SmartStore.Core; using SmartStore.Core.Caching; using SmartStore.Core.Data; @@ -12,10 +10,7 @@ using SmartStore.Core.Events; using SmartStore.Services.Common; using SmartStore.Services.Customers; -using SmartStore.Services.Localization; using SmartStore.Services.Messages; -using SmartStore.Services.Seo; -using SmartStore.Utilities; namespace SmartStore.Services.Forums { @@ -1477,104 +1472,6 @@ public virtual int CalculateTopicPageIndex(int forumTopicId, int pageSize, int p return pageIndex; } - /// - /// Creates a RSS feed with active discussions - /// - /// UrlHelper to generate URLs - /// SmartSyndicationFeed object - public virtual SmartSyndicationFeed CreateActiveDiscussionsRssFeed(UrlHelper urlHelper, int forumId) - { - if (urlHelper == null) - throw new ArgumentNullException("urlHelper"); - - var language = _services.WorkContext.WorkingLanguage; - var protocol = _services.WebHelper.IsCurrentConnectionSecured() ? "https" : "http"; - var selfLink = urlHelper.Action("ActiveDiscussionsRSS", "Boards", null, protocol); - var discussionLink = urlHelper.Action("ActiveDiscussions", "Boards", null, protocol); - - var title = "{0} - {1}".FormatInvariant(_services.StoreContext.CurrentStore.Name, _services.Localization.GetResource("Forum.ActiveDiscussionsFeedTitle")); - - var feed = new SmartSyndicationFeed(new Uri(discussionLink), title, _services.Localization.GetResource("Forum.ActiveDiscussionsFeedDescription")); - - feed.AddNamespaces(false); - feed.Init(selfLink, language); - - if (!_forumSettings.ActiveDiscussionsFeedEnabled) - return feed; - - var items = new List(); - var topics = GetActiveTopics(forumId, _forumSettings.ActiveDiscussionsFeedCount); - - var viewsText = _services.Localization.GetResource("Forum.Views"); - var repliesText = _services.Localization.GetResource("Forum.Replies"); - - foreach (var topic in topics) - { - string topicUrl = urlHelper.RouteUrl("TopicSlug", new { id = topic.Id, slug = topic.GetSeName() }, "http"); - var synopsis = "{0}: {1}, {2}: {3}".FormatInvariant(repliesText, topic.NumReplies, viewsText, topic.Views); - - var item = feed.CreateItem(topic.Subject, synopsis, topicUrl, topic.LastPostTime ?? topic.UpdatedOnUtc); - - items.Add(item); - } - - feed.Items = items; - - return feed; - } - - /// - /// Creates a RSS feed with forum topics - /// - /// UrlHelper to generate URLs - /// SmartSyndicationFeed object - public virtual SmartSyndicationFeed CreateForumRssFeed(UrlHelper urlHelper, int forumId) - { - if (urlHelper == null) - throw new ArgumentNullException("urlHelper"); - - var language = _services.WorkContext.WorkingLanguage; - var protocol = _services.WebHelper.IsCurrentConnectionSecured() ? "https" : "http"; - var selfLink = urlHelper.Action("ForumRSS", "Boards", null, protocol); - var forumLink = urlHelper.Action("Forum", "Boards", new { id = forumId }, protocol); - - var feed = new SmartSyndicationFeed(new Uri(forumLink), _services.StoreContext.CurrentStore.Name, _services.Localization.GetResource("Forum.ForumFeedDescription")); - - feed.AddNamespaces(false); - feed.Init(selfLink, language); - - if (!_forumSettings.ForumFeedsEnabled) - return feed; - - var forum = GetForumById(forumId); - - if (forum == null) - return feed; - - feed.Title = new TextSyndicationContent("{0} - {1}".FormatInvariant(_services.StoreContext.CurrentStore.Name, forum.GetLocalized(x => x.Name, language.Id))); - - var items = new List(); - var topics = GetAllTopics(forumId, 0, string.Empty, ForumSearchType.All, 0, 0, _forumSettings.ForumFeedCount); - - var viewsText = _services.Localization.GetResource("Forum.Views"); - var repliesText = _services.Localization.GetResource("Forum.Replies"); - - foreach (var topic in topics) - { - string topicUrl = urlHelper.RouteUrl("TopicSlug", new { id = topic.Id, slug = topic.GetSeName() }, "http"); - var synopsis = "{0}: {1}, {2}: {3}".FormatInvariant(repliesText, topic.NumReplies, viewsText, topic.Views); - - var item = feed.CreateItem(topic.Subject, synopsis, topicUrl, topic.LastPostTime ?? topic.UpdatedOnUtc); - - items.Add(item); - } - - feed.Items = items; - - return feed; - } - - #endregion } } diff --git a/src/Libraries/SmartStore.Services/Forums/IForumService.cs b/src/Libraries/SmartStore.Services/Forums/IForumService.cs index efcd21fa32..46e0f2f428 100644 --- a/src/Libraries/SmartStore.Services/Forums/IForumService.cs +++ b/src/Libraries/SmartStore.Services/Forums/IForumService.cs @@ -1,9 +1,7 @@ using System.Collections.Generic; -using System.Web.Mvc; using SmartStore.Core; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Forums; -using SmartStore.Utilities; namespace SmartStore.Services.Forums { @@ -346,19 +344,5 @@ IPagedList GetAllSubscriptions(int customerId, int forumId, /// Post identifier /// Page index int CalculateTopicPageIndex(int forumTopicId, int pageSize, int postId); - - /// - /// Creates a RSS feed with active discussions - /// - /// UrlHelper to generate URLs - /// SmartSyndicationFeed object - SmartSyndicationFeed CreateActiveDiscussionsRssFeed(UrlHelper urlHelper, int forumId); - - /// - /// Creates a RSS feed with forum topics - /// - /// UrlHelper to generate URLs - /// SmartSyndicationFeed object - SmartSyndicationFeed CreateForumRssFeed(UrlHelper urlHelper, int forumId); } } diff --git a/src/Libraries/SmartStore.Services/ICommonServices.cs b/src/Libraries/SmartStore.Services/ICommonServices.cs index cc9a048b54..f9b3043bf0 100644 --- a/src/Libraries/SmartStore.Services/ICommonServices.cs +++ b/src/Libraries/SmartStore.Services/ICommonServices.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using SmartStore.Core; using SmartStore.Core.Caching; using SmartStore.Core.Data; @@ -10,12 +9,18 @@ using SmartStore.Services.Security; using SmartStore.Services.Configuration; using SmartStore.Services.Stores; +using Autofac; +using SmartStore.Services.Helpers; namespace SmartStore.Services -{ - +{ public interface ICommonServices { + IComponentContext Container + { + get; + } + ICacheManager Cache { get; @@ -75,6 +80,43 @@ IStoreService StoreService { get; } + + IDateTimeHelper DateTimeHelper + { + get; + } } + public static class ICommonServicesExtensions + { + public static TService Resolve(this ICommonServices services) + { + return services.Container.Resolve(); + } + + public static TService Resolve(this ICommonServices services, object serviceKey) + { + return services.Container.ResolveKeyed(serviceKey); + } + + public static TService ResolveNamed(this ICommonServices services, string serviceName) + { + return services.Container.ResolveNamed(serviceName); + } + + public static object Resolve(this ICommonServices services, Type serviceType) + { + return services.Resolve(null, serviceType); + } + + public static object Resolve(this ICommonServices services, object serviceKey, Type serviceType) + { + return services.Container.ResolveKeyed(serviceKey, serviceType); + } + + public static object ResolveNamed(this ICommonServices services, string serviceName, Type serviceType) + { + return services.Container.ResolveNamed(serviceName, serviceType); + } + } } diff --git a/src/Libraries/SmartStore.Services/Localization/ILocalizationService.cs b/src/Libraries/SmartStore.Services/Localization/ILocalizationService.cs index ff2f1c872e..6195301c03 100644 --- a/src/Libraries/SmartStore.Services/Localization/ILocalizationService.cs +++ b/src/Libraries/SmartStore.Services/Localization/ILocalizationService.cs @@ -1,16 +1,16 @@ using System; using System.Collections.Generic; using System.Xml; -using SmartStore.Core.Data; +using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Plugins; namespace SmartStore.Services.Localization { - /// - /// Localization manager interface - /// - public partial interface ILocalizationService + /// + /// Localization manager interface + /// + public partial interface ILocalizationService { void ClearCache(); @@ -117,9 +117,10 @@ LocaleStringResource GetLocaleStringResourceByName(string resourceName, int lang /// Language /// XML document /// Prefix for resource key name - /// Specifies whether resource should be inserted or updated (or both) - /// Specifies whether user touched resources should also be updated - void ImportResourcesFromXml(Language language, + /// Specifies whether resource should be inserted or updated (or both) + /// Specifies whether user touched resources should also be updated + /// The number of processed (added or updated) resource entries + int ImportResourcesFromXml(Language language, XmlDocument xmlDocument, string rootKey = null, bool sourceIsPlugin = false, @@ -131,11 +132,14 @@ void ImportResourcesFromXml(Language language, /// /// codehint: sm-add /// Descriptor of the plugin - /// Load them into list rather than into database + /// Load them into the passed list rather than into database /// Specifies whether user touched resources should also be updated /// Import only files for particular languages - void ImportPluginResourcesFromXml(PluginDescriptor pluginDescriptor, - List forceToList = null, bool updateTouchedResources = true, IList filterLanguages = null); + void ImportPluginResourcesFromXml( + PluginDescriptor pluginDescriptor, + IList targetList = null, + bool updateTouchedResources = true, + IList filterLanguages = null); /// /// Flattens all nested LocaleResource child nodes into a new document diff --git a/src/Libraries/SmartStore.Services/Localization/ILocalizedEntityService.cs b/src/Libraries/SmartStore.Services/Localization/ILocalizedEntityService.cs index 871afe9887..070d1fbfe5 100644 --- a/src/Libraries/SmartStore.Services/Localization/ILocalizedEntityService.cs +++ b/src/Libraries/SmartStore.Services/Localization/ILocalizedEntityService.cs @@ -62,12 +62,14 @@ public partial interface ILocalizedEntityService /// Key selector /// Locale value /// Language ID - void SaveLocalizedValue(T entity, + void SaveLocalizedValue( + T entity, Expression> keySelector, string localeValue, int languageId) where T : BaseEntity, ILocalizedEntity; - void SaveLocalizedValue(T entity, + void SaveLocalizedValue( + T entity, Expression> keySelector, TPropType localeValue, int languageId) where T : BaseEntity, ILocalizedEntity; diff --git a/src/Libraries/SmartStore.Services/Localization/LanguageService.cs b/src/Libraries/SmartStore.Services/Localization/LanguageService.cs index bc8e7d0999..db75218b54 100644 --- a/src/Libraries/SmartStore.Services/Localization/LanguageService.cs +++ b/src/Libraries/SmartStore.Services/Localization/LanguageService.cs @@ -110,18 +110,19 @@ public virtual IList GetAllLanguages(bool showHidden = false, int stor { var query = _languageRepository.Table; if (!showHidden) - query = query.Where(l => l.Published); - query = query.OrderBy(l => l.DisplayOrder); + query = query.Where(x => x.Published); + query = query.OrderBy(x => x.DisplayOrder); return query.ToList(); }); - //store mapping + // store mapping if (storeId > 0) { languages = languages .Where(l => _storeMappingService.Authorize(l, storeId)) .ToList(); } + return languages; } diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizationExtentions.cs b/src/Libraries/SmartStore.Services/Localization/LocalizationExtentions.cs index b73e7660ed..a55b8ab84f 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizationExtentions.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizationExtentions.cs @@ -1,23 +1,19 @@ using System; using System.Linq.Expressions; using System.Reflection; +using System.Xml; +using SmartStore.ComponentModel; using SmartStore.Core; +using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Infrastructure; using SmartStore.Core.Plugins; -using Fasterflect; -using System.Xml; -using SmartStore.Core.Data; using SmartStore.Utilities; -using System.Collections.Concurrent; -using SmartStore.Core.ComponentModel; namespace SmartStore.Services.Localization { - public static class LocalizationExtentions + public static class LocalizationExtentions { - //private static readonly ConcurrentDictionary _compiledExpressions = new ConcurrentDictionary(); // --> MEM LEAK - /// /// Get localized property of an entity /// @@ -324,7 +320,13 @@ public static string GetLocalizedValue(this PluginDescriptor descriptor, ILocali string result = localizationService.GetResource(resourceName, languageId, false, "", true); if (String.IsNullOrEmpty(result) && returnDefaultValue) - result = descriptor.TryGetPropertyValue(propertyName) as string; + { + var fastProp = FastProperty.GetProperty(descriptor.GetType(), propertyName); + if (fastProp != null) + { + result = fastProp.GetValue(descriptor) as string; + } + } return result; } diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs b/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs index 07a55686b7..c6ac869c27 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizationService.cs @@ -1,31 +1,30 @@ using System; +using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Xml; -using System.Xml.Linq; using SmartStore.Core; using SmartStore.Core.Caching; using SmartStore.Core.Data; -using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Events; -using SmartStore.Data; using SmartStore.Core.Logging; using SmartStore.Core.Plugins; -using System.Text.RegularExpressions; -using System.Collections.Concurrent; -using System.Web.Mvc; -using System.Collections; +using SmartStore.Core.Localization; +using System.Globalization; namespace SmartStore.Services.Localization { - /// - /// Provides information about localization - /// - public partial class LocalizationService : ILocalizationService + /// + /// Provides information about localization + /// + public partial class LocalizationService : ILocalizationService { #region Constants private const string LOCALESTRINGRESOURCES_ALL_KEY = "SmartStore.lsr.all-{0}"; @@ -175,6 +174,7 @@ orderby lsr.ResourceName if (localeStringResource == null && logIfNotFound) _logger.Warning(string.Format("Resource string ({0}) not found. Language ID = {1}", resourceName, languageId)); + return localeStringResource; } @@ -273,6 +273,7 @@ orderby l.ResourceName string cacheKey = string.Format(LOCALESTRINGRESOURCES_ALL_KEY, languageId); var dict = _cacheManager.Get(cacheKey, () => { + // TODO: make result cacheable in distributed cache (IDictionary) var result = new ConcurrentDictionary>(8, 2000, StringComparer.CurrentCultureIgnoreCase); if (forceAll || _localizationSettings.LoadAllLocaleRecordsOnStartup) { @@ -418,13 +419,183 @@ public virtual string ExportResourcesToXml(Language language) return stringWriter.ToString(); } + public virtual void ImportPluginResourcesFromXml( + PluginDescriptor pluginDescriptor, + IList targetList = null, + bool updateTouchedResources = true, + IList filterLanguages = null) + { + var directory = new DirectoryInfo(Path.Combine(pluginDescriptor.OriginalAssemblyFile.Directory.FullName, "Localization")); + + if (!directory.Exists) + return; + + if (targetList == null && updateTouchedResources) + { + DeleteLocaleStringResources(pluginDescriptor.ResourceRootKey); + } + + var unprocessedLanguages = new List(); + + var defaultLanguageId = _languageService.GetDefaultLanguageId(); + var languages = filterLanguages ?? _languageService.GetAllLanguages(true); + + string code = null; + foreach (var language in languages) + { + code = ImportPluginResourcesForLanguage( + language, + null, + directory, + pluginDescriptor.ResourceRootKey, + targetList, + updateTouchedResources, + false); + + if (code == null) + { + unprocessedLanguages.Add(language); + } + } + + if (filterLanguages == null && unprocessedLanguages.Count > 0) + { + // There were unprocessed languages (no corresponding resource file could be found). + // In order for GetResource() to be able to gracefully fallback to the default language's resources, + // we need to import resources for the current default language.... + var processedLanguages = languages.Except(unprocessedLanguages).ToList(); + if (!processedLanguages.Any(x => x.Id == defaultLanguageId)) + { + // ...but only if no resource file could be mapped to the default language before, + // namely because in this case the following operation would be redundant. + var defaultLanguage = _languageService.GetLanguageById(_languageService.GetDefaultLanguageId()); + if (defaultLanguage != null) + { + ImportPluginResourcesForLanguage( + defaultLanguage, + "en-us", + directory, + pluginDescriptor.ResourceRootKey, + targetList, + updateTouchedResources, + true); + } + } + } + } + /// - /// Import language resources from XML file + /// Resolves a resource file for the specified language and processes the import /// /// Language - /// XML document - /// Prefix for resource key name - public virtual void ImportResourcesFromXml( + /// The culture code of the processed resource file + private string ImportPluginResourcesForLanguage( + Language language, + string fileCode, + DirectoryInfo directory, + string resourceRootKey, + IList targetList, + bool updateTouchedResources, + bool canFallBackToAnyResourceFile) + { + var fileNamePattern = "resources.{0}.xml"; + + var codeCandidates = GetResourceFileCodeCandidates( + fileCode ?? language.LanguageCulture, + directory, + canFallBackToAnyResourceFile); + + string path = null; + string code = null; + + foreach (var candidate in codeCandidates) + { + var pathCandidate = Path.Combine(directory.FullName, fileNamePattern.FormatInvariant(candidate)); + if (File.Exists(pathCandidate)) + { + code = candidate; + path = pathCandidate; + break; + } + } + + if (code != null) + { + var doc = new XmlDocument(); + + doc.Load(path); + doc = FlattenResourceFile(doc); + + if (targetList == null) + { + ImportResourcesFromXml(language, doc, resourceRootKey, true, updateTouchedResources: updateTouchedResources); + } + else + { + var nodes = doc.SelectNodes(@"//Language/LocaleResource"); + foreach (XmlNode node in nodes) + { + var valueNode = node.SelectSingleNode("Value"); + var res = new LocaleStringResource() + { + ResourceName = node.Attributes["Name"].InnerText.Trim(), + ResourceValue = (valueNode == null ? "" : valueNode.InnerText), + LanguageId = language.Id, + IsFromPlugin = true + }; + + if (res.ResourceName.HasValue()) + { + targetList.Add(res); + } + } + } + } + + return code; + } + + private IEnumerable GetResourceFileCodeCandidates(string code, DirectoryInfo directory, bool canFallBackToAnyResourceFile) + { + // exact match (de-DE) + yield return code; + + // neutral culture (de) + var ci = CultureInfo.GetCultureInfo(code); + if (ci.Parent != null && !ci.IsNeutralCulture) + { + code = ci.Parent.Name; + yield return code; + } + + var rgFileName = new Regex("^resources.(.+?).xml$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + // any other region with same language (de-*) + foreach (var fi in directory.EnumerateFiles("resources.{0}-*.xml".FormatInvariant(code), SearchOption.TopDirectoryOnly)) + { + code = rgFileName.Match(fi.Name).Groups[1].Value; + if (LocalizationHelper.IsValidCultureCode(code)) + { + yield return code; + yield break; + } + } + + if (canFallBackToAnyResourceFile) + { + foreach (var fi in directory.EnumerateFiles("resources.*.xml", SearchOption.TopDirectoryOnly)) + { + code = rgFileName.Match(fi.Name).Groups[1].Value; + if (LocalizationHelper.IsValidCultureCode(code)) + { + yield return code; + yield break; + } + } + } + } + + public virtual int ImportResourcesFromXml( Language language, XmlDocument xmlDocument, string rootKey = null, @@ -432,15 +603,18 @@ public virtual void ImportResourcesFromXml( ImportModeFlags mode = ImportModeFlags.Insert | ImportModeFlags.Update, bool updateTouchedResources = false) { - using (var scope = new DbContextScope(autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) + using (var scope = new DbContextScope(autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false, forceNoTracking: true, hooksEnabled: false)) { var toAdd = new List(); var toUpdate = new List(); var nodes = xmlDocument.SelectNodes(@"//Language/LocaleResource"); + var resources = language.LocaleStringResources.ToDictionarySafe(x => x.ResourceName, StringComparer.OrdinalIgnoreCase); + + LocaleStringResource resource; + foreach (var xel in nodes.Cast()) { - string name = xel.GetAttribute("Name").TrimSafe(); string value = ""; var valueNode = xel.SelectSingleNode("Value"); @@ -456,27 +630,32 @@ public virtual void ImportResourcesFromXml( name = "{0}.{1}".FormatWith(rootKey, name); } + resource = null; + // do not use "Insert"/"Update" methods because they clear cache // let's bulk insert - var resource = language.LocaleStringResources.Where(x => x.ResourceName.Equals(name, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); - if (resource != null) + //var resource = language.LocaleStringResources.Where(x => x.ResourceName.Equals(name, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); + if (resources.TryGetValue(name, out resource)) { - if (mode.IsSet(ImportModeFlags.Update)) + if (mode.HasFlag(ImportModeFlags.Update)) { if (updateTouchedResources || !resource.IsTouched.GetValueOrDefault()) { - resource.ResourceValue = value; - resource.IsTouched = null; - toUpdate.Add(resource); + if (value != resource.ResourceValue) + { + resource.ResourceValue = value; + resource.IsTouched = null; + toUpdate.Add(resource); + } } } } else { - if (mode.IsSet(ImportModeFlags.Insert)) + if (mode.HasFlag(ImportModeFlags.Insert)) { toAdd.Add( - new LocaleStringResource() + new LocaleStringResource { LanguageId = language.Id, ResourceName = name, @@ -487,90 +666,25 @@ public virtual void ImportResourcesFromXml( } } - _lsrRepository.AutoCommitEnabled = true; - _lsrRepository.InsertRange(toAdd, 500); - toAdd.Clear(); + //_lsrRepository.AutoCommitEnabled = true; - _lsrRepository.AutoCommitEnabled = null; - toUpdate.Each(x => + if (toAdd.Any() || toUpdate.Any()) { - _lsrRepository.Update(x); - }); - - _lsrRepository.Context.SaveChanges(); - toUpdate.Clear(); + _lsrRepository.InsertRange(toAdd); + toAdd.Clear(); - //clear cache - _cacheManager.RemoveByPattern(LOCALESTRINGRESOURCES_PATTERN_KEY); - } - } - - /// - /// Import plugin resources from xml files in plugin's localization directory. - /// - /// Descriptor of the plugin - /// Load them into list rather than into database - /// Specifies whether user touched resources should also be updated - /// Import only files for particular languages - public virtual void ImportPluginResourcesFromXml(PluginDescriptor pluginDescriptor, - List forceToList = null, bool updateTouchedResources = true, IList filterLanguages = null) - { - string pluginDir = pluginDescriptor.OriginalAssemblyFile.Directory.FullName; - string localizationDir = Path.Combine(pluginDir, "Localization"); - - if (!System.IO.Directory.Exists(localizationDir)) - return; - - if (forceToList == null && updateTouchedResources) - DeleteLocaleStringResources(pluginDescriptor.ResourceRootKey); + _lsrRepository.UpdateRange(toUpdate); + toUpdate.Clear(); - var languages = _languageService.GetAllLanguages(true); - var doc = new XmlDocument(); + int num = _lsrRepository.Context.SaveChanges(); - foreach (var filePath in System.IO.Directory.EnumerateFiles(localizationDir, "*.xml")) - { - Match match = Regex.Match(Path.GetFileName(filePath), Regex.Escape("resources.") + "(.*?)" + Regex.Escape(".xml")); - string languageCode = match.Groups[1].Value; + // clear cache + _cacheManager.RemoveByPattern(LOCALESTRINGRESOURCES_PATTERN_KEY); - Language language = languages.Where(l => l.LanguageCulture.IsCaseInsensitiveEqual(languageCode)).FirstOrDefault(); - if (language != null) - { - language = _languageService.GetLanguageById(language.Id); + return num; } - if (languageCode.HasValue() && language != null) - { - if (filterLanguages != null && !filterLanguages.Any(x => x.Id == language.Id)) - { - continue; - } - - doc.Load(filePath); - doc = FlattenResourceFile(doc); - - if (forceToList == null) - { - ImportResourcesFromXml(language, doc, pluginDescriptor.ResourceRootKey, true, updateTouchedResources: updateTouchedResources); - } - else - { - var nodes = doc.SelectNodes(@"//Language/LocaleResource"); - foreach (XmlNode node in nodes) - { - var valueNode = node.SelectSingleNode("Value"); - var res = new LocaleStringResource() - { - ResourceName = node.Attributes["Name"].InnerText.Trim(), - ResourceValue = (valueNode == null ? "" : valueNode.InnerText), - LanguageId = language.Id, - IsFromPlugin = true - }; - - if (res.ResourceName.HasValue()) - forceToList.Add(res); - } - } - } + return 0; } } diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs b/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs index a0b2c2989f..f765e0d580 100644 --- a/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs +++ b/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs @@ -7,6 +7,7 @@ using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Localization; +using System.Collections.Concurrent; namespace SmartStore.Services.Localization { @@ -15,126 +16,76 @@ namespace SmartStore.Services.Localization /// public partial class LocalizedEntityService : ILocalizedEntityService { - #region Constants - - private const string LOCALIZEDPROPERTY_KEY = "SmartStore.localizedproperty.value-{0}-{1}-{2}-{3}"; private const string LOCALIZEDPROPERTY_ALL_KEY = "SmartStore.localizedproperty.all"; - private const string LOCALIZEDPROPERTY_PATTERN_KEY = "SmartStore.localizedproperty."; - - #endregion - - #region Fields private readonly IRepository _localizedPropertyRepository; private readonly ICacheManager _cacheManager; private readonly LocalizationSettings _localizationSettings; - #endregion - - #region Ctor - - /// - /// Ctor - /// - /// Cache manager - /// Localized property repository - public LocalizedEntityService(ICacheManager cacheManager, IRepository localizedPropertyRepository, LocalizationSettings localizationSettings) + public LocalizedEntityService( + ICacheManager cacheManager, + IRepository localizedPropertyRepository, + LocalizationSettings localizationSettings) { this._cacheManager = cacheManager; this._localizedPropertyRepository = localizedPropertyRepository; this._localizationSettings = localizationSettings; } - #endregion - - #region Methods - - /// - /// Deletes a localized property - /// - /// Localized property - public virtual void DeleteLocalizedProperty(LocalizedProperty localizedProperty) - { - if (localizedProperty == null) - throw new ArgumentNullException("localizedProperty"); - - _localizedPropertyRepository.Delete(localizedProperty); - - //cache - _cacheManager.RemoveByPattern(LOCALIZEDPROPERTY_PATTERN_KEY); - } - - /// - /// Gets a localized property - /// - /// Localized property identifier - /// Localized property - public virtual LocalizedProperty GetLocalizedPropertyById(int localizedPropertyId) - { - if (localizedPropertyId == 0) - return null; - - var localizedProperty = _localizedPropertyRepository.GetById(localizedPropertyId); - return localizedProperty; - } - - /// - /// Find localized value - /// - /// Language identifier - /// Entity identifier - /// Locale key group - /// Locale key - /// Found localized value - public virtual string GetLocalizedValue(int languageId, int entityId, string localeKeyGroup, string localeKey) - { - string val = null; - - if (_localizationSettings.LoadAllLocalizedPropertiesOnStartup) + protected virtual ConcurrentDictionary GetAllProperties() + { + var result = _cacheManager.Get(LOCALIZEDPROPERTY_ALL_KEY, () => { - var allValues = _cacheManager.Get(LOCALIZEDPROPERTY_ALL_KEY, () => + if (_localizationSettings.LoadAllLocalizedPropertiesOnStartup) { - var result = _localizedPropertyRepository.TableUntracked.ToDictionary( + var props = _localizedPropertyRepository.TableUntracked.ToDictionarySafe( x => GenerateKey(x.LanguageId, x.LocaleKeyGroup, x.LocaleKey, x.EntityId), - x => x.LocaleValue); - return result; - }); + x => x.LocaleValue.EmptyNull()); + return new ConcurrentDictionary(props); + } + else + { + return new ConcurrentDictionary(); + } + }); - string key = GenerateKey(languageId, localeKeyGroup, localeKey, entityId); + return result; + } - if (!allValues.TryGetValue(key, out val)) + public virtual string GetLocalizedValue(int languageId, int entityId, string localeKeyGroup, string localeKey) + { + var props = GetAllProperties(); + string key = GenerateKey(languageId, localeKeyGroup, localeKey, entityId); + string val = null; + + if (_localizationSettings.LoadAllLocalizedPropertiesOnStartup) + { + if (!props.TryGetValue(key, out val)) { return string.Empty; } } else { - string cacheKey = string.Format(LOCALIZEDPROPERTY_KEY, languageId, entityId, localeKeyGroup, localeKey); - val = _cacheManager.Get(cacheKey, () => - { + val = props.GetOrAdd(key, k => { var query = from lp in _localizedPropertyRepository.TableUntracked - where lp.EntityId == entityId && - lp.LocaleKey == localeKey && - lp.LocaleKeyGroup == localeKeyGroup && - lp.LanguageId == languageId + where + lp.EntityId == entityId && + lp.LocaleKey == localeKey && + lp.LocaleKeyGroup == localeKeyGroup && + lp.LanguageId == languageId select lp.LocaleValue; - + return query.FirstOrDefault().EmptyNull(); }); } return val; - } + } - /// - /// Gets localized properties - /// - /// Entity identifier - /// Locale key group - /// Localized properties public virtual IList GetLocalizedProperties(int entityId, string localeKeyGroup) { - if (string.IsNullOrEmpty(localeKeyGroup)) + if (localeKeyGroup.IsEmpty()) return new List(); var query = from lp in _localizedPropertyRepository.Table @@ -142,49 +93,92 @@ orderby lp.Id where lp.EntityId == entityId && lp.LocaleKeyGroup == localeKeyGroup select lp; + var props = query.ToList(); return props; } - /// - /// Inserts a localized property - /// - /// Localized property - public virtual void InsertLocalizedProperty(LocalizedProperty localizedProperty) - { - if (localizedProperty == null) - throw new ArgumentNullException("localizedProperty"); + protected virtual LocalizedProperty GetLocalizedProperty(int languageId, int entityId, string localeKeyGroup, string localeKey) + { + var query = from lp in _localizedPropertyRepository.Table + where + lp.EntityId == entityId && + lp.LocaleKey == localeKey && + lp.LocaleKeyGroup == localeKeyGroup && + lp.LanguageId == languageId + select lp; + + return query.FirstOrDefault(); + } - _localizedPropertyRepository.Insert(localizedProperty); + public virtual void InsertLocalizedProperty(LocalizedProperty localizedProperty) + { + Guard.ArgumentNotNull(() => localizedProperty); - //cache - _cacheManager.RemoveByPattern(LOCALIZEDPROPERTY_PATTERN_KEY); + try + { + // db + _localizedPropertyRepository.Insert(localizedProperty); + + // cache + var key = GenerateKey(localizedProperty); + var val = localizedProperty.LocaleValue.EmptyNull(); + GetAllProperties().AddOrUpdate( + key, + val, + (k, v) => val); + } + catch { } } - /// - /// Updates the localized property - /// - /// Localized property public virtual void UpdateLocalizedProperty(LocalizedProperty localizedProperty) { - if (localizedProperty == null) - throw new ArgumentNullException("localizedProperty"); + Guard.ArgumentNotNull(() => localizedProperty); - _localizedPropertyRepository.Update(localizedProperty); + try + { + // db + _localizedPropertyRepository.Update(localizedProperty); + + // cache + var key = GenerateKey(localizedProperty); + var val = localizedProperty.LocaleValue.EmptyNull(); + GetAllProperties().AddOrUpdate( + key, + val, + (k, v) => val); + } + catch { } + } - //cache - _cacheManager.RemoveByPattern(LOCALIZEDPROPERTY_PATTERN_KEY); - } + public virtual void DeleteLocalizedProperty(LocalizedProperty localizedProperty) + { + Guard.ArgumentNotNull(() => localizedProperty); + + try + { + // cache + var key = GenerateKey(localizedProperty); + string val = null; + GetAllProperties().TryRemove(key, out val); + + // db + _localizedPropertyRepository.Delete(localizedProperty); + } + catch { } + } + + public virtual LocalizedProperty GetLocalizedPropertyById(int localizedPropertyId) + { + if (localizedPropertyId == 0) + return null; - /// - /// Save localized value - /// - /// Type - /// Entity - /// Key selector - /// Locale value - /// Language ID - public virtual void SaveLocalizedValue(T entity, + var localizedProperty = _localizedPropertyRepository.GetById(localizedPropertyId); + return localizedProperty; + } + + public virtual void SaveLocalizedValue( + T entity, Expression> keySelector, string localeValue, int languageId) where T : BaseEntity, ILocalizedEntity @@ -192,16 +186,14 @@ public virtual void SaveLocalizedValue(T entity, SaveLocalizedValue(entity, keySelector, localeValue, languageId); } - public virtual void SaveLocalizedValue(T entity, + public virtual void SaveLocalizedValue( + T entity, Expression> keySelector, TPropType localeValue, int languageId) where T : BaseEntity, ILocalizedEntity { - if (entity == null) - throw new ArgumentNullException("entity"); - - if (languageId == 0) - throw new ArgumentOutOfRangeException("languageId", "Language ID should not be 0"); + Guard.ArgumentNotNull(() => entity); + Guard.ArgumentNotZero(languageId, "languageId"); var member = keySelector.Body as MemberExpression; if (member == null) @@ -222,32 +214,33 @@ public virtual void SaveLocalizedValue(T entity, string localeKeyGroup = typeof(T).Name; string localeKey = propInfo.Name; - var props = GetLocalizedProperties(entity.Id, localeKeyGroup); - var prop = props.FirstOrDefault(lp => lp.LanguageId == languageId && - lp.LocaleKey.Equals(localeKey, StringComparison.InvariantCultureIgnoreCase)); //should be culture invariant + var prop = GetLocalizedProperty(languageId, entity.Id, localeKeyGroup, localeKey); string localeValueStr = localeValue.Convert(); if (prop != null) { - if (string.IsNullOrWhiteSpace(localeValueStr)) + if (localeValueStr.IsEmpty()) { - //delete + // delete DeleteLocalizedProperty(prop); } else { - //update - prop.LocaleValue = localeValueStr; - UpdateLocalizedProperty(prop); + // update + if (prop.LocaleValue != localeValueStr) + { + prop.LocaleValue = localeValueStr; + UpdateLocalizedProperty(prop); + } } } else { - if (!string.IsNullOrWhiteSpace(localeValueStr)) + if (localeValueStr.HasValue()) { - //insert - prop = new LocalizedProperty() + // insert + prop = new LocalizedProperty { EntityId = entity.Id, LanguageId = languageId, @@ -260,11 +253,14 @@ public virtual void SaveLocalizedValue(T entity, } } + private string GenerateKey(LocalizedProperty prop) + { + return GenerateKey(prop.LanguageId, prop.LocaleKeyGroup, prop.LocaleKey, prop.EntityId); + } + private string GenerateKey(int languageId, string localeKeyGroup, string localeKey, int entityId) { return "{0}.{1}.{2}.{3}".FormatInvariant(languageId, localeKeyGroup, localeKey, entityId); } - - #endregion } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Logging/CustomerActivityService.cs b/src/Libraries/SmartStore.Services/Logging/CustomerActivityService.cs index c84c5f7526..61ba8f21ad 100644 --- a/src/Libraries/SmartStore.Services/Logging/CustomerActivityService.cs +++ b/src/Libraries/SmartStore.Services/Logging/CustomerActivityService.cs @@ -1,64 +1,49 @@ using System; -using System.Collections.Generic; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using SmartStore.Core; -using SmartStore.Core.Caching; using SmartStore.Core.Data; -using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Logging; -using SmartStore.Data; using SmartStore.Core.Logging; namespace SmartStore.Services.Logging { - /// - /// Customer activity service - /// - public class CustomerActivityService : ICustomerActivityService + /// + /// Customer activity service + /// + public class CustomerActivityService : ICustomerActivityService { - #region Fields + #region Fields - /// - /// Cache manager - /// - private readonly ICacheManager _cacheManager; - private readonly IRepository _activityLogRepository; + private const int _deleteNumberOfEntries = 1000; + + private readonly IRepository _activityLogRepository; private readonly IRepository _activityLogTypeRepository; - private readonly IWorkContext _workContext; + private readonly IRepository _customerRepository; + private readonly IWorkContext _workContext; private readonly IDbContext _dbContext; - private readonly IDataProvider _dataProvider; - private readonly CommonSettings _commonSettings; - // codehint: sm-add + private readonly static object s_lock = new object(); private readonly static ConcurrentDictionary s_logTypes = new ConcurrentDictionary(); - #endregion - #region Ctor - /// - /// Ctor - /// - /// Cache manager - /// Activity log repository - /// Activity log type repository - /// Work context - /// DB context> - /// WeData provider - /// Common settings - public CustomerActivityService(ICacheManager cacheManager, + #endregion + + #region Ctor + + public CustomerActivityService( IRepository activityLogRepository, IRepository activityLogTypeRepository, - IWorkContext workContext, - IDbContext dbContext, IDataProvider dataProvider, CommonSettings commonSettings) + IRepository customerRepository, + IWorkContext workContext, + IDbContext dbContext) { - this._cacheManager = cacheManager; this._activityLogRepository = activityLogRepository; this._activityLogTypeRepository = activityLogTypeRepository; + this._customerRepository = customerRepository; this._workContext = workContext; this._dbContext = dbContext; - this._dataProvider = dataProvider; - this._commonSettings = commonSettings; } #endregion @@ -218,27 +203,55 @@ public virtual void DeleteActivity(ActivityLog activityLog) _activityLogRepository.Delete(activityLog); } - /// - /// Gets all activity log items - /// - /// Log item creation from; null to load all customers - /// Log item creation to; null to load all customers - /// Customer identifier; null to load all customers - /// Activity log type identifier - /// Page index - /// Page size - /// Activity log collection - public virtual IPagedList GetAllActivities(DateTime? createdOnFrom, - DateTime? createdOnTo, int? customerId, int activityLogTypeId, - int pageIndex, int pageSize) + /// + /// Gets all activity log items + /// + /// Log item creation from; null to load all customers + /// Log item creation to; null to load all customers + /// Customer identifier; null to load all customers + /// Activity log type identifier + /// Page index + /// Page size + /// Customer email + /// Customer system name + /// Activity log collection + public virtual IPagedList GetAllActivities( + DateTime? createdOnFrom, + DateTime? createdOnTo, + int? customerId, + int activityLogTypeId, + int pageIndex, + int pageSize, + string email = null, + bool? customerSystemAccount = null) { var query = _activityLogRepository.Table; + + if (email.HasValue() || customerSystemAccount.HasValue) + { + var queryCustomers = _customerRepository.Table; + + if (email.HasValue()) + queryCustomers = queryCustomers.Where(x => x.Email == email); + + if (customerSystemAccount.HasValue) + queryCustomers = queryCustomers.Where(x => x.IsSystemAccount == customerSystemAccount.Value); + + query = + from al in _activityLogRepository.Table + join c in queryCustomers on al.CustomerId equals c.Id + select al; + } + if (createdOnFrom.HasValue) query = query.Where(al => createdOnFrom.Value <= al.CreatedOnUtc); + if (createdOnTo.HasValue) query = query.Where(al => createdOnTo.Value >= al.CreatedOnUtc); + if (activityLogTypeId > 0) query = query.Where(al => activityLogTypeId == al.ActivityLogTypeId); + if (customerId.HasValue) query = query.Where(al => customerId.Value == al.CustomerId); @@ -266,29 +279,54 @@ public virtual ActivityLog GetActivityById(int activityLogId) return activityLog; } - /// - /// Clears activity log - /// - public virtual void ClearAllActivities() - { - if (_commonSettings.UseStoredProceduresIfSupported && _dataProvider.StoredProceduresSupported) - { - //although it's not a stored procedure we use it to ensure that a database supports them - //we cannot wait until EF team has it implemented - http://data.uservoice.com/forums/72025-entity-framework-feature-suggestions/suggestions/1015357-batch-cud-support + public virtual IList GetActivityByIds(int[] activityLogIds) + { + if (activityLogIds == null || activityLogIds.Length == 0) + return new List(); + var query = _activityLogRepository.Table + .Where(x => activityLogIds.Contains(x.Id)) + .OrderByDescending(x => x.CreatedOnUtc); - //do all databases support "Truncate command"? - //TODO: do not hard-code the table name - _dbContext.ExecuteSqlCommand("TRUNCATE TABLE [ActivityLog]"); - } - else - { - var activityLog = _activityLogRepository.Table.ToList(); - foreach (var activityLogItem in activityLog) - _activityLogRepository.Delete(activityLogItem); - } - } - #endregion + return query.ToList(); + } + /// + /// Clears activity log + /// + public virtual void ClearAllActivities() + { + try + { + _dbContext.ExecuteSqlCommand("TRUNCATE TABLE [ActivityLog]"); + } + catch + { + try + { + for (int i = 0; i < 100000; ++i) + { + if (_dbContext.ExecuteSqlCommand("Delete Top ({0}) From [ActivityLog]", false, null, _deleteNumberOfEntries) < _deleteNumberOfEntries) + break; + } + } + catch { } + + try + { + _dbContext.ExecuteSqlCommand("DBCC CHECKIDENT('ActivityLog', RESEED, 0)"); + } + catch + { + try + { + _dbContext.ExecuteSqlCommand("Alter Table [ActivityLog] Alter Column [Id] Identity(1,1)"); + } + catch { } + } + } + } + + #endregion } } diff --git a/src/Libraries/SmartStore.Services/Logging/DefaultLogger.cs b/src/Libraries/SmartStore.Services/Logging/DefaultLogger.cs index fb152d8a1f..8387de3969 100644 --- a/src/Libraries/SmartStore.Services/Logging/DefaultLogger.cs +++ b/src/Libraries/SmartStore.Services/Logging/DefaultLogger.cs @@ -119,9 +119,9 @@ public virtual IPagedList GetAllLogs(DateTime? fromUtc, DateTime? toUtc, st var query = _logRepository.Table; if (fromUtc.HasValue) - query = query.Where(l => fromUtc.Value <= l.CreatedOnUtc); + query = query.Where(l => fromUtc.Value <= l.CreatedOnUtc || fromUtc.Value <= l.UpdatedOnUtc); if (toUtc.HasValue) - query = query.Where(l => toUtc.Value >= l.CreatedOnUtc); + query = query.Where(l => toUtc.Value >= l.CreatedOnUtc || toUtc.Value >= l.UpdatedOnUtc); if (logLevel.HasValue) { int logLevelId = (int)logLevel.Value; @@ -129,7 +129,8 @@ public virtual IPagedList GetAllLogs(DateTime? fromUtc, DateTime? toUtc, st } if (!String.IsNullOrEmpty(message)) query = query.Where(l => l.ShortMessage.Contains(message) || l.FullMessage.Contains(message)); - query = query.OrderByDescending(l => l.CreatedOnUtc); + + query = query.OrderByDescending(l => l.UpdatedOnUtc).ThenByDescending(l => l.CreatedOnUtc); if (minFrequency > 0) query = query.Where(l => l.Frequency >= minFrequency); diff --git a/src/Libraries/SmartStore.Services/Media/IPictureService.cs b/src/Libraries/SmartStore.Services/Media/IPictureService.cs index 85bc1741b5..ce9f231349 100644 --- a/src/Libraries/SmartStore.Services/Media/IPictureService.cs +++ b/src/Libraries/SmartStore.Services/Media/IPictureService.cs @@ -5,10 +5,10 @@ namespace SmartStore.Services.Media { - /// - /// Picture service interface - /// - public partial interface IPictureService + /// + /// Picture service interface + /// + public partial interface IPictureService { /// /// Validates input picture dimensions and prevents that the image size exceeds global max size @@ -22,20 +22,19 @@ public partial interface IPictureService /// Finds an equal picture by comparing the binary buffer /// /// The picture to find a duplicate for - /// The sequence of product pictures to seek within for duplicates + /// The sequence of pictures to seek within for duplicates /// Id of equal picture if any /// The picture binary for path when no picture equals in the sequence, null otherwise. - byte[] FindEqualPicture(string path, IEnumerable productPictures, out int equalPictureId); + byte[] FindEqualPicture(string path, IEnumerable pictures, out int equalPictureId); /// /// Finds an equal picture by comparing the binary buffer /// /// Binary picture data - /// The sequence of product pictures to seek within for duplicates + /// The sequence of pictures to seek within for duplicates /// Id of equal picture if any /// The picture binary for path when no picture equals in the sequence, null otherwise. - byte[] FindEqualPicture(byte[] pictureBinary, IEnumerable productPictures, out int equalPictureId); - + byte[] FindEqualPicture(byte[] pictureBinary, IEnumerable pictures, out int equalPictureId); /// /// Gets the loaded picture binary depending on picture storage settings diff --git a/src/Libraries/SmartStore.Services/Media/PictureService.cs b/src/Libraries/SmartStore.Services/Media/PictureService.cs index 95c58fb845..563993234f 100644 --- a/src/Libraries/SmartStore.Services/Media/PictureService.cs +++ b/src/Libraries/SmartStore.Services/Media/PictureService.cs @@ -154,17 +154,21 @@ public virtual byte[] ValidatePicture(byte[] pictureBinary) } } - public byte[] FindEqualPicture(string path, IEnumerable productPictures, out int equalPictureId) + #endregion + + #region Methods + + public byte[] FindEqualPicture(string path, IEnumerable pictures, out int equalPictureId) { - return FindEqualPicture(File.ReadAllBytes(path), productPictures, out equalPictureId); + return FindEqualPicture(File.ReadAllBytes(path), pictures, out equalPictureId); } - public byte[] FindEqualPicture(byte[] pictureBinary, IEnumerable productPictures, out int equalPictureId) + public byte[] FindEqualPicture(byte[] pictureBinary, IEnumerable pictures, out int equalPictureId) { equalPictureId = 0; try { - foreach (var picture in productPictures) + foreach (var picture in pictures) { var otherPictureBinary = LoadPictureBinary(picture); @@ -187,11 +191,7 @@ public byte[] FindEqualPicture(byte[] pictureBinary, IEnumerable produc } } - #endregion - - #region Methods - - public virtual string GetPictureSeName(string name) + public virtual string GetPictureSeName(string name) { return SeoHelper.GetSeName(name, true, false); } diff --git a/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs b/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs index 4392ca231a..b67c04fbe5 100644 --- a/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs +++ b/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs @@ -43,7 +43,7 @@ public void Execute(TaskExecutionContext ctx) { using (var scope = new DbContextScope(autoDetectChanges: false, validateOnSave: false, hooksEnabled: false)) { - var pictures = _pictureRepository.Where(x => x.IsTransient && x.UpdatedOnUtc < olderThan).ToList(); + var pictures = _pictureRepository.Table.Where(x => x.IsTransient && x.UpdatedOnUtc < olderThan).ToList(); foreach (var picture in pictures) { _pictureService.DeletePicture(picture); diff --git a/src/Libraries/SmartStore.Services/Messages/IMessageTokenProvider.cs b/src/Libraries/SmartStore.Services/Messages/IMessageTokenProvider.cs index 7265b4faa4..3cf3bd7563 100644 --- a/src/Libraries/SmartStore.Services/Messages/IMessageTokenProvider.cs +++ b/src/Libraries/SmartStore.Services/Messages/IMessageTokenProvider.cs @@ -3,6 +3,7 @@ using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Forums; +using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Messages; using SmartStore.Core.Domain.News; using SmartStore.Core.Domain.Orders; @@ -11,13 +12,13 @@ namespace SmartStore.Services.Messages { - public partial interface IMessageTokenProvider + public partial interface IMessageTokenProvider { void AddStoreTokens(IList tokens, Store store); - void AddOrderTokens(IList tokens, Order order, int languageId); + void AddOrderTokens(IList tokens, Order order, Language language); - void AddShipmentTokens(IList tokens, Shipment shipment, int languageId); + void AddShipmentTokens(IList tokens, Shipment shipment, Language language); void AddOrderNoteTokens(IList tokens, OrderNote orderNote); @@ -37,9 +38,9 @@ public partial interface IMessageTokenProvider void AddNewsCommentTokens(IList tokens, NewsComment newsComment); - void AddProductTokens(IList tokens, Product product, int languageId); + void AddProductTokens(IList tokens, Product product, Language language); - void AddForumTokens(IList tokens, Forum forum, int languageId); + void AddForumTokens(IList tokens, Forum forum, Language language); void AddForumTopicTokens(IList tokens, ForumTopic forumTopic, int? friendlyForumTopicPageIndex = null, int? appendedPostIdentifierAnchor = null); @@ -54,13 +55,10 @@ void AddForumTopicTokens(IList tokens, ForumTopic forumTopic, string[] GetListOfAllowedTokens(); - //codehint: sm-add begin void AddBankConnectionTokens(IList tokens); void AddCompanyTokens(IList tokens); void AddContactDataTokens(IList tokens); - //codehint: sm-add end - } } diff --git a/src/Libraries/SmartStore.Services/Messages/INewsLetterSubscriptionService.cs b/src/Libraries/SmartStore.Services/Messages/INewsLetterSubscriptionService.cs index 851a4d952d..a579ff6859 100644 --- a/src/Libraries/SmartStore.Services/Messages/INewsLetterSubscriptionService.cs +++ b/src/Libraries/SmartStore.Services/Messages/INewsLetterSubscriptionService.cs @@ -1,21 +1,11 @@ using System; -using System.IO; using SmartStore.Core; using SmartStore.Core.Domain.Messages; -using SmartStore.Core.Data; namespace SmartStore.Services.Messages { - public partial interface INewsLetterSubscriptionService - { - - /// - /// Executes a bulk import of subscriber data from a CSV source - /// - /// The input file stream - /// The import result - ImportResult ImportSubscribers(Stream stream); - + public partial interface INewsLetterSubscriptionService + { /// /// Inserts a newsletter subscription /// @@ -37,12 +27,21 @@ public partial interface INewsLetterSubscriptionService /// if set to true [publish subscription events]. void DeleteNewsLetterSubscription(NewsLetterSubscription newsLetterSubscription, bool publishSubscriptionEvents = true); - /// - /// Gets a newsletter subscription by newsletter subscription identifier - /// - /// The newsletter subscription identifier - /// NewsLetter subscription - NewsLetterSubscription GetNewsLetterSubscriptionById(int newsLetterSubscriptionId); + /// + /// Adds or deletes a newsletter subscription + /// + /// true add subscription, false delete + /// Email address + /// Store identifier + /// true added subscription, false removed subscription, null did nothing + bool? AddNewsLetterSubscriptionFor(bool add, string email, int storeId); + + /// + /// Gets a newsletter subscription by newsletter subscription identifier + /// + /// The newsletter subscription identifier + /// NewsLetter subscription + NewsLetterSubscription GetNewsLetterSubscriptionById(int newsLetterSubscriptionId); /// /// Gets a newsletter subscription by newsletter subscription GUID @@ -70,4 +69,4 @@ public partial interface INewsLetterSubscriptionService /// NewsLetterSubscription entity list IPagedList GetAllNewsLetterSubscriptions(string email, int pageIndex, int pageSize, bool showHidden = false, int storeId = 0); } -} +} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs b/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs new file mode 100644 index 0000000000..979051d1d6 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Messages/Importer/NewsLetterSubscriptionImporter.cs @@ -0,0 +1,154 @@ +using System; +using System.Linq; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Domain.Messages; +using SmartStore.Services.DataExchange.Import; + +namespace SmartStore.Services.Messages.Importer +{ + public class NewsLetterSubscriptionImporter : IEntityImporter + { + private readonly ICommonServices _services; + private readonly IRepository _subscriptionRepository; + + public NewsLetterSubscriptionImporter( + ICommonServices services, + IRepository subscriptionRepository) + { + _services = services; + _subscriptionRepository = subscriptionRepository; + } + + public static string[] SupportedKeyFields + { + get + { + return new string[] { "Email" }; + } + } + + public static string[] DefaultKeyFields + { + get + { + return new string[] { "Email" }; + } + } + + public void Execute(ImportExecuteContext context) + { + var utcNow = DateTime.UtcNow; + var currentStoreId = _services.StoreContext.CurrentStore.Id; + + using (var scope = new DbContextScope(ctx: _services.DbContext, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) + { + var segmenter = context.DataSegmenter; + + context.Result.TotalRecords = segmenter.TotalRows; + + while (context.Abort == DataExchangeAbortion.None && segmenter.ReadNextBatch()) + { + var batch = segmenter.GetCurrentBatch(); + + // Perf: detach all entities + _subscriptionRepository.Context.DetachEntities(false); + + context.SetProgress(segmenter.CurrentSegmentFirstRowIndex - 1, segmenter.TotalRows); + + foreach (var row in batch) + { + try + { + var active = true; + var email = row.GetDataValue("Email"); + var storeId = row.GetDataValue("StoreId"); + + if (storeId == 0) + { + storeId = currentStoreId; + } + + if (row.HasDataValue("Active") && row.TryGetDataValue("Active", out active)) + { + } + else + { + active = true; // default + } + + if (email.IsEmpty()) + { + context.Result.AddWarning("Skipped empty email address", row.GetRowInfo(), "Email"); + continue; + } + + if (email.Length > 255) + { + context.Result.AddWarning("Skipped email address '{0}'. It exceeds the maximum allowed length of 255".FormatInvariant(email), row.GetRowInfo(), "Email"); + continue; + } + + if (!email.IsEmail()) + { + context.Result.AddWarning("Skipped invalid email address '{0}'".FormatInvariant(email), row.GetRowInfo(), "Email"); + continue; + } + + NewsLetterSubscription subscription = null; + + foreach (var keyName in context.KeyFieldNames) + { + switch (keyName) + { + case "Email": + subscription = _subscriptionRepository.Table + .OrderBy(x => x.Id) + .FirstOrDefault(x => x.Email == email && x.StoreId == storeId); + break; + } + + if (subscription != null) + break; + } + + if (subscription == null) + { + if (context.UpdateOnly) + { + ++context.Result.SkippedRecords; + continue; + } + + subscription = new NewsLetterSubscription + { + Active = active, + CreatedOnUtc = utcNow, + Email = email, + NewsLetterSubscriptionGuid = Guid.NewGuid(), + StoreId = storeId + }; + + _subscriptionRepository.Insert(subscription); + context.Result.NewRecords++; + } + else + { + subscription.Active = active; + + _subscriptionRepository.Update(subscription); + context.Result.ModifiedRecords++; + } + } + catch (Exception exception) + { + context.Result.AddError(exception.ToAllMessages(), row.GetRowInfo()); + } + } // for + + _subscriptionRepository.Context.SaveChanges(); + } // while + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/Messages/MessageTokenProvider.cs b/src/Libraries/SmartStore.Services/Messages/MessageTokenProvider.cs index b1e50412da..d0ff917259 100644 --- a/src/Libraries/SmartStore.Services/Messages/MessageTokenProvider.cs +++ b/src/Libraries/SmartStore.Services/Messages/MessageTokenProvider.cs @@ -13,6 +13,8 @@ using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Forums; +using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Media; using SmartStore.Core.Domain.Messages; using SmartStore.Core.Domain.News; using SmartStore.Core.Domain.Orders; @@ -71,12 +73,14 @@ public partial class MessageTokenProvider : IMessageTokenProvider private readonly IUrlRecordService _urlRecordService; private readonly IStoreService _storeService; private readonly IGenericAttributeService _attrService; + private readonly IPictureService _pictureService; + private readonly MediaSettings _mediaSettings; - #endregion + #endregion - #region Ctor + #region Ctor - public MessageTokenProvider(ILanguageService languageService, + public MessageTokenProvider(ILanguageService languageService, ILocalizationService localizationService, IDateTimeHelper dateTimeHelper, IEmailAccountService emailAccountService, IPriceFormatter priceFormatter, ICurrencyService currencyService, IWebHelper webHelper, @@ -91,7 +95,9 @@ public MessageTokenProvider(ILanguageService languageService, ContactDataSettings contactDataSettings, ITopicService topicService, IDeliveryTimeService deliveryTimeService, IQuantityUnitService quantityUnitService, IUrlRecordService urlRecordService, IStoreService storeService, - IGenericAttributeService attrService) + IGenericAttributeService attrService, + IPictureService pictureService, + MediaSettings mediaSettings) { this._languageService = languageService; this._localizationService = localizationService; @@ -122,33 +128,87 @@ public MessageTokenProvider(ILanguageService languageService, this._urlRecordService = urlRecordService; this._storeService = storeService; this._attrService = attrService; + this._pictureService = pictureService; + this._mediaSettings = mediaSettings; } - #endregion + #endregion - #region Utilities + #region Utilities - /// - /// Convert a collection to a HTML table - /// - /// Order - /// Language identifier - /// HTML table of products - protected virtual string ProductListToHtmlTable(Order order, int languageId) - { - var result = ""; + protected virtual Picture GetPictureFor(Product product, string attributesXml) + { + Picture picture = null; + + if (attributesXml.HasValue()) + { + var combination = _productAttributeParser.FindProductVariantAttributeCombination(product.Id, attributesXml); + + if (combination != null) + { + var picturesIds = combination.GetAssignedPictureIds(); + if (picturesIds != null && picturesIds.Length > 0) + picture = _pictureService.GetPictureById(picturesIds[0]); + } + } + + if (picture == null) + { + picture = _pictureService.GetPicturesByProductId(product.Id, 1).FirstOrDefault(); + } + + if (picture == null && !product.VisibleIndividually && product.ParentGroupedProductId > 0) + { + picture = _pictureService.GetPicturesByProductId(product.ParentGroupedProductId, 1).FirstOrDefault(); + } + + return picture; + } + + protected virtual string ProductPictureToHtml(Picture picture, Language language, string productName, string productUrl, string storeLocation) + { + if (picture != null && _mediaSettings.MessageProductThumbPictureSize > 0) + { + var imageUrl = _pictureService.GetPictureUrl(picture, _mediaSettings.MessageProductThumbPictureSize, false, storeLocation); + if (imageUrl.HasValue()) + { + var title = _localizationService.GetResource("Media.Product.ImageLinkTitleFormat", language.Id).FormatInvariant(productName); + var alternate = _localizationService.GetResource("Media.Product.ImageAlternateTextFormat", language.Id).FormatInvariant(productName); - var language = _languageService.GetLanguageById(languageId); + var polaroid = "padding: 3px; background-color: #fff; border: 1px solid #ccc; border: 1px solid rgba(0,0,0,.2);"; + var style = "max-width: {0}px; max-height: {0}px; {1}".FormatInvariant(_mediaSettings.MessageProductThumbPictureSize, polaroid); + var image = "\"{1}\"".FormatInvariant(imageUrl, alternate, title, style); + + if (productUrl.IsEmpty()) + return image; + + return "{1}".FormatInvariant(productUrl, image); + } + } + return ""; + } + + /// + /// Convert a collection to a HTML table + /// + /// Order + /// Language identifier + /// HTML table of products + protected virtual string ProductListToHtmlTable(Order order, Language language) + { var sb = new StringBuilder(); - sb.AppendLine(""); + var storeLocation = _webHelper.GetStoreLocation(false); + + sb.AppendLine("
    "); #region Products - sb.AppendLine(string.Format("", _templatesSettings.Color1)); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Name", languageId))); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Price", languageId))); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Quantity", languageId))); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Total", languageId))); + + sb.AppendLine(string.Format("", _templatesSettings.Color1)); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Name", language.Id))); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Price", language.Id))); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Quantity", language.Id))); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Total", language.Id))); sb.AppendLine(""); var table = order.OrderItems.ToList(); @@ -161,11 +221,7 @@ protected virtual string ProductListToHtmlTable(Order order, int languageId) DeliveryTime deliveryTime = null; - // merging attribute combination data required? - if (_catalogSettings.ShowProductSku || (_shoppingCartSettings.ShowDeliveryTimes && product.IsShipEnabled)) - { - product.MergeWithCombination(orderItem.AttributesXml, _productAttributeParser); - } + product.MergeWithCombination(orderItem.AttributesXml, _productAttributeParser); if (_shoppingCartSettings.ShowDeliveryTimes && product.IsShipEnabled) { @@ -173,16 +229,29 @@ protected virtual string ProductListToHtmlTable(Order order, int languageId) } sb.AppendLine(string.Format("", _templatesSettings.Color2)); - //product name - string productName = product.GetLocalized(x => x.Name, languageId); + + var productName = product.GetLocalized(x => x.Name, language.Id); + var productUrl = _productAttributeParser.GetProductUrlWithAttributes(orderItem.AttributesXml, product.Id, product.GetSeName()); + + sb.AppendLine(""); @@ -237,9 +306,9 @@ protected virtual string ProductListToHtmlTable(Order order, int languageId) sb.AppendLine(string.Format("", unitPriceStr)); var quantityUnit = _quantityUnitService.GetQuantityUnitById(product.QuantityUnitId); + sb.AppendLine(string.Format("", - orderItem.Quantity, - quantityUnit == null ? "" : quantityUnit.GetLocalized(x => x.Name))); + orderItem.Quantity, quantityUnit == null ? "" : quantityUnit.GetLocalized(x => x.Name))); string priceStr = string.Empty; switch (order.CustomerTaxDisplayType) @@ -261,6 +330,7 @@ protected virtual string ProductListToHtmlTable(Order order, int languageId) sb.AppendLine(""); } + #endregion #region Checkout Attributes @@ -285,6 +355,7 @@ protected virtual string ProductListToHtmlTable(Order order, int languageId) string cusTaxTotal = string.Empty; string cusDiscount = string.Empty; string cusTotal = string.Empty; + //subtotal, shipping, payment method fee switch (order.CustomerTaxDisplayType) { @@ -383,41 +454,34 @@ protected virtual string ProductListToHtmlTable(Order order, int languageId) var orderTotalInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderTotal, order.CurrencyRate); cusTotal = _priceFormatter.FormatPrice(orderTotalInCustomerCurrency, true, order.CustomerCurrencyCode, false, language); - - - //subtotal - //sb.AppendLine(string.Format("", _templatesSettings.Color3, _localizationService.GetResource("Messages.Order.SubTotal", languageId), cusSubTotal)); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.SubTotal", languageId), cusSubTotal)); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.SubTotal", language.Id), cusSubTotal)); //discount (applied to order subtotal) if (dislaySubTotalDiscount) { - //sb.AppendLine(string.Format("", _templatesSettings.Color3, _localizationService.GetResource("Messages.Order.SubTotalDiscount", languageId), cusSubTotalDiscount)); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.SubTotalDiscount", languageId), cusSubTotalDiscount)); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.SubTotalDiscount", language.Id), cusSubTotalDiscount)); } //shipping if (dislayShipping) { - //sb.AppendLine(string.Format("", _templatesSettings.Color3, _localizationService.GetResource("Messages.Order.Shipping", languageId), cusShipTotal)); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Shipping", languageId), cusShipTotal)); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Shipping", language.Id), cusShipTotal)); } //payment method fee if (displayPaymentMethodFee) { - string paymentMethodFeeTitle = _localizationService.GetResource("Messages.Order.PaymentMethodAdditionalFee", languageId); - //sb.AppendLine(string.Format("", _templatesSettings.Color3, paymentMethodFeeTitle, cusPaymentMethodAdditionalFee)); - sb.AppendLine(string.Format("", paymentMethodFeeTitle, cusPaymentMethodAdditionalFee)); + string paymentMethodFeeTitle = _localizationService.GetResource("Messages.Order.PaymentMethodAdditionalFee", language.Id); + + sb.AppendLine(string.Format("", paymentMethodFeeTitle, cusPaymentMethodAdditionalFee)); } //tax if (displayTax) { - //sb.AppendLine(string.Format("", _templatesSettings.Color3, _localizationService.GetResource("Messages.Order.Tax", languageId), cusTaxTotal)); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Tax", languageId), cusTaxTotal)); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Tax", language.Id), cusTaxTotal)); } if (displayTaxRates) { @@ -425,45 +489,50 @@ protected virtual string ProductListToHtmlTable(Order order, int languageId) { string taxRate = String.Format(_localizationService.GetResource("Messages.Order.TaxRateLine"), _priceFormatter.FormatTaxRate(item.Key)); string taxValue = _priceFormatter.FormatPrice(item.Value, true, order.CustomerCurrencyCode, false, language); - //sb.AppendLine(string.Format("", _templatesSettings.Color3, taxRate, taxValue)); - sb.AppendLine(string.Format("", taxRate, taxValue)); + + sb.AppendLine(string.Format("", taxRate, taxValue)); } } //discount if (dislayDiscount) { - //sb.AppendLine(string.Format("", _templatesSettings.Color3, _localizationService.GetResource("Messages.Order.TotalDiscount", languageId), cusDiscount)); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.TotalDiscount", languageId), cusDiscount)); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.TotalDiscount", language.Id), cusDiscount)); } //gift cards var gcuhC = order.GiftCardUsageHistory; foreach (var gcuh in gcuhC) { - string giftCardText = String.Format(_localizationService.GetResource("Messages.Order.GiftCardInfo", languageId), HttpUtility.HtmlEncode(gcuh.GiftCard.GiftCardCouponCode)); + string giftCardText = String.Format(_localizationService.GetResource("Messages.Order.GiftCardInfo", language.Id), HttpUtility.HtmlEncode(gcuh.GiftCard.GiftCardCouponCode)); string giftCardAmount = _priceFormatter.FormatPrice(-(_currencyService.ConvertCurrency(gcuh.UsedValue, order.CurrencyRate)), true, order.CustomerCurrencyCode, false, language); - //sb.AppendLine(string.Format("", _templatesSettings.Color3, giftCardText, giftCardAmount)); - sb.AppendLine(string.Format("", giftCardText, giftCardAmount)); + + var remaining = _currencyService.ConvertCurrency(gcuh.GiftCard.GetGiftCardRemainingAmount(), order.CurrencyRate); + var remainingFormatted = _priceFormatter.FormatPrice(remaining, true, false); + var remainingText = _localizationService.GetResource("ShoppingCart.Totals.GiftCardInfo.Remaining", language.Id).FormatInvariant(remainingFormatted); + + sb.AppendLine(string.Format("", + giftCardText, remainingText, giftCardAmount)); } //reward points if (order.RedeemedRewardPointsEntry != null) { - string rpTitle = string.Format(_localizationService.GetResource("Messages.Order.RewardPoints", languageId), -order.RedeemedRewardPointsEntry.Points); + string rpTitle = string.Format(_localizationService.GetResource("Messages.Order.RewardPoints", language.Id), -order.RedeemedRewardPointsEntry.Points); string rpAmount = _priceFormatter.FormatPrice(-(_currencyService.ConvertCurrency(order.RedeemedRewardPointsEntry.UsedAmount, order.CurrencyRate)), true, order.CustomerCurrencyCode, false, language); - //sb.AppendLine(string.Format("", _templatesSettings.Color3, rpTitle, rpAmount)); - sb.AppendLine(string.Format("", rpTitle, rpAmount)); + + sb.AppendLine(string.Format("", rpTitle, rpAmount)); } //total - sb.AppendLine(string.Format("", _templatesSettings.Color3, _localizationService.GetResource("Messages.Order.OrderTotal", languageId), cusTotal)); - #endregion + sb.AppendLine(string.Format("", _templatesSettings.Color3, _localizationService.GetResource("Messages.Order.OrderTotal", language.Id), cusTotal)); + + #endregion sb.AppendLine("
    {0}{0}{0}{0}
    {0}{0}{0}{0}
    "); + + if (_mediaSettings.MessageProductThumbPictureSize > 0) + { + var pictureHtml = ProductPictureToHtml(GetPictureFor(product, orderItem.AttributesXml), language, productName, productUrl, storeLocation); + if (pictureHtml.HasValue()) + { + sb.AppendLine("
    {0}
    ".FormatInvariant(pictureHtml)); + } + } + + sb.AppendLine("{1}".FormatInvariant(productUrl, HttpUtility.HtmlEncode(productName))); - sb.AppendLine("
    " + HttpUtility.HtmlEncode(productName)); - //add download link - if (_downloadService.IsDownloadAllowed(orderItem)) + //add download link + if (_downloadService.IsDownloadAllowed(orderItem)) { //TODO add a method for getting URL (use routing because it handles all SEO friendly URLs) - string downloadUrl = string.Format("{0}download/getdownload/{1}", _webHelper.GetStoreLocation(false), orderItem.OrderItemGuid); - string downloadLink = string.Format("{1}", downloadUrl, _localizationService.GetResource("Messages.Order.Product(s).Download", languageId)); + string downloadUrl = string.Format("{0}download/getdownload/{1}", storeLocation, orderItem.OrderItemGuid); + string downloadLink = string.Format("{1}", downloadUrl, _localizationService.GetResource("Messages.Order.Product(s).Download", language.Id)); sb.AppendLine("  ("); sb.AppendLine(downloadLink); sb.AppendLine(")"); @@ -195,7 +264,7 @@ protected virtual string ProductListToHtmlTable(Order order, int languageId) sb.AppendLine("
    "); sb.AppendLine("
    "); - sb.AppendLine("" + _localizationService.GetResource("Products.DeliveryTime") + ""); + sb.AppendLine("" + _localizationService.GetResource("Products.DeliveryTime", language.Id) + ""); sb.AppendLine(""); sb.AppendLine("" + deliveryTimeName + ""); sb.AppendLine("
    "); @@ -213,7 +282,7 @@ protected virtual string ProductListToHtmlTable(Order order, int languageId) if (!String.IsNullOrEmpty(product.Sku)) { sb.AppendLine("
    "); - sb.AppendLine(string.Format(_localizationService.GetResource("Messages.Order.Product(s).SKU", languageId), HttpUtility.HtmlEncode(product.Sku))); + sb.AppendLine(string.Format(_localizationService.GetResource("Messages.Order.Product(s).SKU", language.Id), HttpUtility.HtmlEncode(product.Sku))); } } sb.AppendLine("
    {0}{0} {1}
     {1} {2}
     {0} {1}
     {0} {1}
     {1} {2}
     {0} {1}
     {0} {1}
     {1} {2}
     {0} {1}
     {0} {1}
     {1} {2}
     {0} {1}
     {0} {1}
     {1} {2}
     {0} {1}
     {0} {1}
     {1} {2}
     {0} {1}
     {0} {1}
     {1} {2}
     {0} {1}
     {0} {1}
     {1} {2}
     {0} {1}
     {0}
    {1}
    {2}
     {1} {2}
     {0} {1}
     {0} {1}
     {1}{2}
     {1}{2}
    "); - result = sb.ToString(); - return result; - } + + return sb.ToString(); + } /// /// Convert a collection to a HTML table @@ -471,17 +540,17 @@ protected virtual string ProductListToHtmlTable(Order order, int languageId) /// Shipment /// Language identifier /// HTML table of products - protected virtual string ProductListToHtmlTable(Shipment shipment, int languageId) + protected virtual string ProductListToHtmlTable(Shipment shipment, Language language) { - var result = ""; - var sb = new StringBuilder(); + sb.AppendLine(""); #region Products + sb.AppendLine(string.Format("", _templatesSettings.Color1)); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Name", languageId))); - sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Quantity", languageId))); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Name", language.Id))); + sb.AppendLine(string.Format("", _localizationService.GetResource("Messages.Order.Product(s).Quantity", language.Id))); sb.AppendLine(""); var table = shipment.ShipmentItems.ToList(); @@ -497,12 +566,15 @@ protected virtual string ProductListToHtmlTable(Shipment shipment, int languageI continue; sb.AppendLine(string.Format("", _templatesSettings.Color2)); - //product name - string productName = product.GetLocalized(x => x.Name, languageId); - sb.AppendLine(""); @@ -524,12 +596,13 @@ protected virtual string ProductListToHtmlTable(Shipment shipment, int languageI sb.AppendLine(""); } + #endregion sb.AppendLine("
    {0}{0}{0}{0}
    " + HttpUtility.HtmlEncode(productName)); - //attributes - if (!String.IsNullOrEmpty(orderItem.AttributeDescription)) + var productName = product.GetLocalized(x => x.Name, language.Id); + var productUrl = _productAttributeParser.GetProductUrlWithAttributes(orderItem.AttributesXml, product.Id, product.GetSeName()); + + sb.AppendLine(""); + sb.AppendLine("{1}".FormatInvariant(productUrl, HttpUtility.HtmlEncode(productName))); + + //attributes + if (!String.IsNullOrEmpty(orderItem.AttributeDescription)) { sb.AppendLine("
    "); sb.AppendLine(orderItem.AttributeDescription); @@ -515,7 +587,7 @@ protected virtual string ProductListToHtmlTable(Shipment shipment, int languageI if (!String.IsNullOrEmpty(product.Sku)) { sb.AppendLine("
    "); - sb.AppendLine(string.Format(_localizationService.GetResource("Messages.Order.Product(s).SKU", languageId), HttpUtility.HtmlEncode(product.Sku))); + sb.AppendLine(string.Format(_localizationService.GetResource("Messages.Order.Product(s).SKU", language.Id), HttpUtility.HtmlEncode(product.Sku))); } } sb.AppendLine("
    "); - result = sb.ToString(); - return result; - } + + return sb.ToString(); + } protected virtual string TopicToHtml(string systemName, int languageId) { @@ -722,7 +795,7 @@ public virtual void AddContactDataTokens(IList tokens) tokens.Add(new Token("Contact.ContactEmailAddress", _contactDataSettings.ContactEmailAddress)); } - public virtual void AddOrderTokens(IList tokens, Order order, int languageId) + public virtual void AddOrderTokens(IList tokens, Order order, Language language) { tokens.Add(new Token("Order.ID", order.Id.ToString())); tokens.Add(new Token("Order.OrderNumber", order.GetOrderNumber())); @@ -757,15 +830,22 @@ public virtual void AddOrderTokens(IList tokens, Order order, int languag tokens.Add(new Token("Order.ShippingZipPostalCode", order.ShippingAddress != null ? order.ShippingAddress.ZipPostalCode : "")); tokens.Add(new Token("Order.ShippingCountry", order.ShippingAddress != null && order.ShippingAddress.Country != null ? order.ShippingAddress.Country.GetLocalized(x => x.Name) : "")); + string paymentMethodName = null; var paymentMethod = _providerManager.GetProvider(order.PaymentMethodSystemName); - var paymentMethodName = paymentMethod != null ? GetLocalizedValue(paymentMethod.Metadata, "FriendlyName", x => x.FriendlyName) : order.PaymentMethodSystemName; - - tokens.Add(new Token("Order.PaymentMethod", paymentMethodName)); + if (paymentMethod != null) + { + paymentMethodName = GetLocalizedValue(paymentMethod.Metadata, "FriendlyName", x => x.FriendlyName); + } + if (paymentMethodName.IsEmpty()) + { + paymentMethodName = order.PaymentMethodSystemName; + } + + tokens.Add(new Token("Order.PaymentMethod", paymentMethodName)); tokens.Add(new Token("Order.VatNumber", order.VatNumber)); - tokens.Add(new Token("Order.Product(s)", ProductListToHtmlTable(order, languageId), true)); + tokens.Add(new Token("Order.Product(s)", ProductListToHtmlTable(order, language), true)); tokens.Add(new Token("Order.CustomerComment", order.CustomerOrderComment, true)); - var language = _languageService.GetLanguageById(languageId); if (language != null && !String.IsNullOrEmpty(language.LanguageCulture)) { DateTime createdOn = _dateTimeHelper.ConvertToUserTime(order.CreatedOnUtc, TimeZoneInfo.Utc, _dateTimeHelper.GetCustomerTimeZone(order.Customer)); @@ -779,8 +859,8 @@ public virtual void AddOrderTokens(IList tokens, Order order, int languag // TODO add a method for getting URL (use routing because it handles all SEO friendly URLs) tokens.Add(new Token("Order.OrderURLForCustomer", string.Format("{0}order/details/{1}", _webHelper.GetStoreLocation(false), order.Id), true)); - tokens.Add(new Token("Order.Disclaimer", TopicToHtml("Disclaimer", languageId), true)); - tokens.Add(new Token("Order.ConditionsOfUse", TopicToHtml("ConditionsOfUse", languageId), true)); + tokens.Add(new Token("Order.Disclaimer", TopicToHtml("Disclaimer", language.Id), true)); + tokens.Add(new Token("Order.ConditionsOfUse", TopicToHtml("ConditionsOfUse", language.Id), true)); //event notification _eventPublisher.EntityTokensAdded(order, tokens); @@ -803,11 +883,11 @@ private string GetLocalizedValue(ProviderMetadata metadata, string propertyName, return result; } - public virtual void AddShipmentTokens(IList tokens, Shipment shipment, int languageId) + public virtual void AddShipmentTokens(IList tokens, Shipment shipment, Language language) { tokens.Add(new Token("Shipment.ShipmentNumber", shipment.Id.ToString())); tokens.Add(new Token("Shipment.TrackingNumber", shipment.TrackingNumber)); - tokens.Add(new Token("Shipment.Product(s)", ProductListToHtmlTable(shipment, languageId), true)); + tokens.Add(new Token("Shipment.Product(s)", ProductListToHtmlTable(shipment, language), true)); tokens.Add(new Token("Shipment.URLForCustomer", string.Format("{0}order/shipmentdetails/{1}", _webHelper.GetStoreLocation(false), shipment.Id), true)); //event notification @@ -847,14 +927,27 @@ public virtual void AddReturnRequestTokens(IList tokens, ReturnRequest re public virtual void AddGiftCardTokens(IList tokens, GiftCard giftCard) { - tokens.Add(new Token("GiftCard.SenderName", giftCard.SenderName)); + var order = (giftCard.PurchasedWithOrderItem != null ? giftCard.PurchasedWithOrderItem.Order : null); + + if (order != null) + { + var remainingAmount = _currencyService.ConvertCurrency(giftCard.GetGiftCardRemainingAmount(), order.CurrencyRate); + + tokens.Add(new Token("GiftCard.RemainingAmount", _priceFormatter.FormatPrice(remainingAmount, true, false))); + } + else + { + tokens.Add(new Token("GiftCard.RemainingAmount", "")); + } + + tokens.Add(new Token("GiftCard.SenderName", giftCard.SenderName)); tokens.Add(new Token("GiftCard.SenderEmail", giftCard.SenderEmail)); tokens.Add(new Token("GiftCard.RecipientName", giftCard.RecipientName)); tokens.Add(new Token("GiftCard.RecipientEmail", giftCard.RecipientEmail)); tokens.Add(new Token("GiftCard.Amount", _priceFormatter.FormatPrice(giftCard.Amount, true, false))); - tokens.Add(new Token("GiftCard.CouponCode", giftCard.GiftCardCouponCode)); + tokens.Add(new Token("GiftCard.CouponCode", giftCard.GiftCardCouponCode)); - var giftCardMesage = !String.IsNullOrWhiteSpace(giftCard.Message) ? + var giftCardMesage = !String.IsNullOrWhiteSpace(giftCard.Message) ? HtmlUtils.FormatText(giftCard.Message, false, true, false, false, false, false) : ""; tokens.Add(new Token("GiftCard.Message", giftCardMesage, true)); @@ -929,20 +1022,36 @@ public virtual void AddNewsCommentTokens(IList tokens, NewsComment newsCo _eventPublisher.EntityTokensAdded(newsComment, tokens); } - public virtual void AddProductTokens(IList tokens, Product product, int languageId) + public virtual void AddProductTokens(IList tokens, Product product, Language language) { + // TODO: add a method for getting URL (use routing because it handles all SEO friendly URLs) + var storeLocation = _webHelper.GetStoreLocation(false); + var productUrl = storeLocation + product.GetSeName(); + var productName = product.GetLocalized(x => x.Name, language.Id); + tokens.Add(new Token("Product.ID", product.Id.ToString())); tokens.Add(new Token("Product.Sku", product.Sku)); - tokens.Add(new Token("Product.Name", product.GetLocalized(x => x.Name, languageId))); - tokens.Add(new Token("Product.ShortDescription", product.GetLocalized(x => x.ShortDescription, languageId), true)); + tokens.Add(new Token("Product.Name", productName)); + tokens.Add(new Token("Product.ShortDescription", product.GetLocalized(x => x.ShortDescription, language.Id), true)); tokens.Add(new Token("Product.StockQuantity", product.StockQuantity.ToString())); - - // TODO: add a method for getting URL (use routing because it handles all SEO friendly URLs) - var productUrl = string.Format("{0}{1}", _webHelper.GetStoreLocation(false), product.GetSeName()); tokens.Add(new Token("Product.ProductURLForCustomer", productUrl, true)); - //event notification - _eventPublisher.EntityTokensAdded(product, tokens); + var currency = _workContext.WorkingCurrency; + + var additionalShippingCharge = _currencyService.ConvertFromPrimaryStoreCurrency(product.AdditionalShippingCharge, currency); + var additionalShippingChargeFormatted = _priceFormatter.FormatPrice(additionalShippingCharge, false, currency.CurrencyCode, false, language); + + tokens.Add(new Token("Product.AdditionalShippingCharge", additionalShippingChargeFormatted)); + + if (_mediaSettings.MessageProductThumbPictureSize > 0) + { + var pictureHtml = ProductPictureToHtml(GetPictureFor(product, null), language, productName, productUrl, storeLocation); + + tokens.Add(new Token("Product.Thumbnail", pictureHtml, true)); + } + + //event notification + _eventPublisher.EntityTokensAdded(product, tokens); } public virtual void AddForumTopicTokens(IList tokens, ForumTopic forumTopic, @@ -972,12 +1081,12 @@ public virtual void AddForumPostTokens(IList tokens, ForumPost forumPost) _eventPublisher.EntityTokensAdded(forumPost, tokens); } - public virtual void AddForumTokens(IList tokens, Forum forum, int languageId) + public virtual void AddForumTokens(IList tokens, Forum forum, Language language) { //TODO add a method for getting URL (use routing because it handles all SEO friendly URLs) - var forumUrl = string.Format("{0}boards/forum/{1}/{2}", _webHelper.GetStoreLocation(false), forum.Id, forum.GetSeName(languageId)); + var forumUrl = string.Format("{0}boards/forum/{1}/{2}", _webHelper.GetStoreLocation(false), forum.Id, forum.GetSeName(language.Id)); tokens.Add(new Token("Forums.ForumURL", forumUrl, true)); - tokens.Add(new Token("Forums.ForumName", forum.GetLocalized(x => x.Name, languageId))); + tokens.Add(new Token("Forums.ForumName", forum.GetLocalized(x => x.Name, language.Id))); //event notification _eventPublisher.EntityTokensAdded(forum, tokens); @@ -1075,7 +1184,9 @@ public virtual string[] GetListOfAllowedTokens() "%Product.ShortDescription%", "%Product.ProductURLForCustomer%", "%Product.StockQuantity%", - "%RecurringPayment.ID%", + "%Product.AdditionalShippingCharge%", + "%Product.Thumbnail%", + "%RecurringPayment.ID%", "%Shipment.ShipmentNumber%", "%Shipment.TrackingNumber%", "%Shipment.Product(s)%", @@ -1092,8 +1203,9 @@ public virtual string[] GetListOfAllowedTokens() "%GiftCard.SenderEmail%", "%GiftCard.RecipientName%", "%GiftCard.RecipientEmail%", - "%GiftCard.Amount%", - "%GiftCard.CouponCode%", + "%GiftCard.Amount%", + "%GiftCard.RemainingAmount%", + "%GiftCard.CouponCode%", "%GiftCard.Message%", "%Customer.Email%", "%Customer.Username%", diff --git a/src/Libraries/SmartStore.Services/Messages/NewsLetterSubscriptionService.cs b/src/Libraries/SmartStore.Services/Messages/NewsLetterSubscriptionService.cs index 4061193914..dde1d076c9 100644 --- a/src/Libraries/SmartStore.Services/Messages/NewsLetterSubscriptionService.cs +++ b/src/Libraries/SmartStore.Services/Messages/NewsLetterSubscriptionService.cs @@ -1,148 +1,25 @@ using System; -using System.Collections.Generic; -using System.IO; using System.Linq; using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Domain.Messages; -using SmartStore.Data; using SmartStore.Core.Events; -using SmartStore.Services.Stores; namespace SmartStore.Services.Messages { - - public class NewsLetterSubscriptionService : INewsLetterSubscriptionService + public class NewsLetterSubscriptionService : INewsLetterSubscriptionService { private readonly IEventPublisher _eventPublisher; private readonly IDbContext _context; private readonly IRepository _subscriptionRepository; - private readonly IStoreService _storeService; public NewsLetterSubscriptionService(IDbContext context, IRepository subscriptionRepository, - IEventPublisher eventPublisher, - IStoreService storeService) + IEventPublisher eventPublisher) { _context = context; _subscriptionRepository = subscriptionRepository; _eventPublisher = eventPublisher; - _storeService = storeService; - } - - public ImportResult ImportSubscribers(Stream stream) - { - Guard.ArgumentNotNull(() => stream); - - var result = new ImportResult(); - var toAdd = new List(); - var toUpdate = new List(); - - using (var scope = new DbContextScope(ctx: _context, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) - { - using (var reader = new StreamReader(stream)) - { - while (!reader.EndOfStream) - { - string line = reader.ReadLine(); - if (line.IsEmpty()) - { - continue; - } - string[] tmp = line.Split(','); - - var email = ""; - bool isActive = true; - int storeId = 0; - - // parse - if (tmp.Length == 1) - { - // "email" only - email = tmp[0].Trim(); - } - else if (tmp.Length == 2) - { - // "email" and "active" fields specified - email = tmp[0].Trim(); - isActive = Boolean.Parse(tmp[1].Trim()); - } - else if (tmp.Length == 3) - { - email = tmp[0].Trim(); - isActive = Boolean.Parse(tmp[1].Trim()); - storeId = int.Parse(tmp[2].Trim()); - } - else - { - throw new SmartException("Wrong file format (expected comma separated entries 'Email' and optionally 'IsActive')"); - } - - result.TotalRecords++; - - if (email.Length > 255) - { - result.AddWarning("The emal address '{0}' exceeds the maximun allowed length of 255.".FormatInvariant(email)); - continue; - } - - if (!email.IsEmail()) - { - result.AddWarning("'{0}' is not a valid email address.".FormatInvariant(email)); - continue; - } - - if (storeId == 0) - { - storeId = _storeService.GetAllStores().First().Id; - } - - // import - var subscription = (from nls in _subscriptionRepository.Table - where nls.Email == email && nls.StoreId == storeId - orderby nls.Id - select nls).FirstOrDefault(); - - if (subscription != null) - { - subscription.Active = isActive; - - toUpdate.Add(subscription); - result.ModifiedRecords++; - } - else - { - subscription = new NewsLetterSubscription - { - Active = isActive, - CreatedOnUtc = DateTime.UtcNow, - Email = email, - NewsLetterSubscriptionGuid = Guid.NewGuid(), - StoreId = storeId - }; - - toAdd.Add(subscription); - result.NewRecords++; - } - } - } - - // insert new subscribers - _subscriptionRepository.AutoCommitEnabled = true; - _subscriptionRepository.InsertRange(toAdd, 500); - toAdd.Clear(); - - // update modified subscribers - _subscriptionRepository.AutoCommitEnabled = null; - toUpdate.Each(x => - { - _subscriptionRepository.Update(x); - }); - _subscriptionRepository.Context.SaveChanges(); - toUpdate.Clear(); - } - - return result; } /// @@ -236,7 +113,8 @@ public void UpdateNewsLetterSubscription(NewsLetterSubscription newsLetterSubscr /// if set to true [publish subscription events]. public virtual void DeleteNewsLetterSubscription(NewsLetterSubscription newsLetterSubscription, bool publishSubscriptionEvents = true) { - if (newsLetterSubscription == null) throw new ArgumentNullException("newsLetterSubscription"); + if (newsLetterSubscription == null) + throw new ArgumentNullException("newsLetterSubscription"); _subscriptionRepository.Delete(newsLetterSubscription); @@ -247,12 +125,52 @@ public virtual void DeleteNewsLetterSubscription(NewsLetterSubscription newsLett _eventPublisher.EntityDeleted(newsLetterSubscription); } - /// - /// Gets a newsletter subscription by newsletter subscription identifier - /// - /// The newsletter subscription identifier - /// NewsLetter subscription - public virtual NewsLetterSubscription GetNewsLetterSubscriptionById(int newsLetterSubscriptionId) + public virtual bool? AddNewsLetterSubscriptionFor(bool add, string email, int storeId) + { + bool? result = null; + + if (email.IsEmail()) + { + var newsletter = GetNewsLetterSubscriptionByEmail(email, storeId); + if (newsletter != null) + { + if (add) + { + newsletter.Active = true; + UpdateNewsLetterSubscription(newsletter); + result = true; + } + else + { + DeleteNewsLetterSubscription(newsletter); + result = false; + } + } + else + { + if (add) + { + InsertNewsLetterSubscription(new NewsLetterSubscription + { + NewsLetterSubscriptionGuid = Guid.NewGuid(), + Email = email, + Active = true, + CreatedOnUtc = DateTime.UtcNow, + StoreId = storeId + }); + result = true; + } + } + } + return result; + } + + /// + /// Gets a newsletter subscription by newsletter subscription identifier + /// + /// The newsletter subscription identifier + /// NewsLetter subscription + public virtual NewsLetterSubscription GetNewsLetterSubscriptionById(int newsLetterSubscriptionId) { if (newsLetterSubscriptionId == 0) return null; diff --git a/src/Libraries/SmartStore.Services/Messages/WorkflowMessageService.cs b/src/Libraries/SmartStore.Services/Messages/WorkflowMessageService.cs index 64adb4aa26..1f2746245e 100644 --- a/src/Libraries/SmartStore.Services/Messages/WorkflowMessageService.cs +++ b/src/Libraries/SmartStore.Services/Messages/WorkflowMessageService.cs @@ -8,12 +8,14 @@ using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Forums; +using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Messages; using SmartStore.Core.Domain.News; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Domain.Stores; using SmartStore.Core.Events; +using SmartStore.Core.Localization; using SmartStore.Services.Customers; using SmartStore.Services.Localization; using SmartStore.Services.Media; @@ -71,13 +73,17 @@ public WorkflowMessageService( this._workContext = workContext; this._httpRequest = httpRequest; this._downloadServioce = downloadServioce; - } - #endregion + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + #endregion - #region Utilities + #region Utilities - protected int SendNotification( + protected int SendNotification( MessageTemplate messageTemplate, EmailAccount emailAccount, int languageId, @@ -232,7 +238,7 @@ private string GetDisplayNameForCustomer(Customer customer) return name ?? customer.Username.EmptyNull(); } - protected int EnsureLanguageIsActive(int languageId, int storeId) + protected Language EnsureLanguageIsActive(int languageId, int storeId) { //load language by specified ID var language = _languageService.GetLanguageById(languageId); @@ -249,8 +255,9 @@ protected int EnsureLanguageIsActive(int languageId, int storeId) } if (language == null) - throw new Exception("No active language could be loaded"); - return language.Id; + throw new SmartException(T("Common.Error.NoActiveLanguage")); + + return language; } #endregion @@ -271,7 +278,7 @@ public virtual int SendCustomerRegisteredNotificationMessage(Customer customer, throw new ArgumentNullException("customer"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("NewCustomer.Notification", store.Id); if (messageTemplate == null) @@ -285,17 +292,14 @@ public virtual int SendCustomerRegisteredNotificationMessage(Customer customer, //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = emailAccount.Email; var toName = emailAccount.DisplayName; // use customer email as reply address var replyTo = GetReplyToEmail(customer); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName, - replyTo.Item1, replyTo.Item2); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, replyTo.Item1, replyTo.Item2); } /// @@ -310,7 +314,7 @@ public virtual int SendCustomerWelcomeMessage(Customer customer, int languageId) throw new ArgumentNullException("customer"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Customer.WelcomeMessage", store.Id); if (messageTemplate == null) @@ -324,12 +328,11 @@ public virtual int SendCustomerWelcomeMessage(Customer customer, int languageId) //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = customer.Email; var toName = customer.GetFullName(); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -344,7 +347,7 @@ public virtual int SendCustomerEmailValidationMessage(Customer customer, int lan throw new ArgumentNullException("customer"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Customer.EmailValidationMessage", store.Id); if (messageTemplate == null) @@ -358,12 +361,11 @@ public virtual int SendCustomerEmailValidationMessage(Customer customer, int lan //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = customer.Email; var toName = customer.GetFullName(); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -378,7 +380,7 @@ public virtual int SendCustomerPasswordRecoveryMessage(Customer customer, int la throw new ArgumentNullException("customer"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Customer.PasswordRecovery", store.Id); if (messageTemplate == null) @@ -392,12 +394,11 @@ public virtual int SendCustomerPasswordRecoveryMessage(Customer customer, int la //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = customer.Email; var toName = customer.GetFullName(); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } #endregion @@ -416,7 +417,7 @@ public virtual int SendOrderPlacedStoreOwnerNotification(Order order, int langua throw new ArgumentNullException("order"); var store = _storeService.GetStoreById(order.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("OrderPlaced.StoreOwnerNotification", store.Id); if (messageTemplate == null) @@ -425,13 +426,13 @@ public virtual int SendOrderPlacedStoreOwnerNotification(Order order, int langua //tokens var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddOrderTokens(tokens, order, languageId); + _messageTokenProvider.AddOrderTokens(tokens, order, language); _messageTokenProvider.AddCustomerTokens(tokens, order.Customer); //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = emailAccount.Email; var toName = emailAccount.DisplayName; @@ -443,10 +444,7 @@ public virtual int SendOrderPlacedStoreOwnerNotification(Order order, int langua replyToName += ", " + order.BillingAddress.Company; } - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName, - replyToEmail, replyToName); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, replyToEmail, replyToName); } /// @@ -461,7 +459,7 @@ public virtual int SendOrderPlacedCustomerNotification(Order order, int language throw new ArgumentNullException("order"); var store = _storeService.GetStoreById(order.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("OrderPlaced.CustomerNotification", store.Id); if (messageTemplate == null) @@ -470,7 +468,7 @@ public virtual int SendOrderPlacedCustomerNotification(Order order, int language //tokens var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddOrderTokens(tokens, order, languageId); + _messageTokenProvider.AddOrderTokens(tokens, order, language); _messageTokenProvider.AddCustomerTokens(tokens, order.Customer); _messageTokenProvider.AddCompanyTokens(tokens); @@ -480,12 +478,11 @@ public virtual int SendOrderPlacedCustomerNotification(Order order, int language // event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = order.BillingAddress.Email; var toName = string.Format("{0} {1}", order.BillingAddress.FirstName, order.BillingAddress.LastName); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -501,10 +498,10 @@ public virtual int SendShipmentSentCustomerNotification(Shipment shipment, int l var order = shipment.Order; if (order == null) - throw new Exception("Order cannot be loaded"); + throw new SmartException(T("Order.NotFound", shipment.OrderId)); var store = _storeService.GetStoreById(order.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("ShipmentSent.CustomerNotification", store.Id); if (messageTemplate == null) @@ -513,19 +510,18 @@ public virtual int SendShipmentSentCustomerNotification(Shipment shipment, int l //tokens var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddShipmentTokens(tokens, shipment, languageId); - _messageTokenProvider.AddOrderTokens(tokens, shipment.Order, languageId); + _messageTokenProvider.AddShipmentTokens(tokens, shipment, language); + _messageTokenProvider.AddOrderTokens(tokens, shipment.Order, language); _messageTokenProvider.AddCustomerTokens(tokens, shipment.Order.Customer); //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = order.BillingAddress.Email; var toName = string.Format("{0} {1}", order.BillingAddress.FirstName, order.BillingAddress.LastName); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -541,10 +537,10 @@ public virtual int SendShipmentDeliveredCustomerNotification(Shipment shipment, var order = shipment.Order; if (order == null) - throw new Exception("Order cannot be loaded"); + throw new SmartException(T("Order.NotFound", shipment.OrderId)); var store = _storeService.GetStoreById(order.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("ShipmentDelivered.CustomerNotification", store.Id); if (messageTemplate == null) @@ -553,19 +549,18 @@ public virtual int SendShipmentDeliveredCustomerNotification(Shipment shipment, //tokens var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddShipmentTokens(tokens, shipment, languageId); - _messageTokenProvider.AddOrderTokens(tokens, shipment.Order, languageId); + _messageTokenProvider.AddShipmentTokens(tokens, shipment, language); + _messageTokenProvider.AddOrderTokens(tokens, shipment.Order, language); _messageTokenProvider.AddCustomerTokens(tokens, shipment.Order.Customer); //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = order.BillingAddress.Email; var toName = string.Format("{0} {1}", order.BillingAddress.FirstName, order.BillingAddress.LastName); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -580,7 +575,7 @@ public virtual int SendOrderCompletedCustomerNotification(Order order, int langu throw new ArgumentNullException("order"); var store = _storeService.GetStoreById(order.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("OrderCompleted.CustomerNotification", store.Id); if (messageTemplate == null) @@ -589,7 +584,7 @@ public virtual int SendOrderCompletedCustomerNotification(Order order, int langu //tokens var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddOrderTokens(tokens, order, languageId); + _messageTokenProvider.AddOrderTokens(tokens, order, language); _messageTokenProvider.AddCustomerTokens(tokens, order.Customer); _messageTokenProvider.AddCompanyTokens(tokens); @@ -599,12 +594,11 @@ public virtual int SendOrderCompletedCustomerNotification(Order order, int langu //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = order.BillingAddress.Email; var toName = string.Format("{0} {1}", order.BillingAddress.FirstName, order.BillingAddress.LastName); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -619,7 +613,7 @@ public virtual int SendOrderCancelledCustomerNotification(Order order, int langu throw new ArgumentNullException("order"); var store = _storeService.GetStoreById(order.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("OrderCancelled.CustomerNotification", store.Id); if (messageTemplate == null) @@ -628,18 +622,17 @@ public virtual int SendOrderCancelledCustomerNotification(Order order, int langu //tokens var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddOrderTokens(tokens, order, languageId); + _messageTokenProvider.AddOrderTokens(tokens, order, language); _messageTokenProvider.AddCustomerTokens(tokens, order.Customer); //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = order.BillingAddress.Email; var toName = string.Format("{0} {1}", order.BillingAddress.FirstName, order.BillingAddress.LastName); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -656,7 +649,7 @@ public virtual int SendNewOrderNoteAddedCustomerNotification(OrderNote orderNote var order = orderNote.Order; var store = _storeService.GetStoreById(order.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Customer.NewOrderNote", store.Id); if (messageTemplate == null) @@ -666,18 +659,17 @@ public virtual int SendNewOrderNoteAddedCustomerNotification(OrderNote orderNote var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); _messageTokenProvider.AddOrderNoteTokens(tokens, orderNote); - _messageTokenProvider.AddOrderTokens(tokens, orderNote.Order, languageId); + _messageTokenProvider.AddOrderTokens(tokens, orderNote.Order, language); _messageTokenProvider.AddCustomerTokens(tokens, orderNote.Order.Customer); //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = order.BillingAddress.Email; var toName = string.Format("{0} {1}", order.BillingAddress.FirstName, order.BillingAddress.LastName); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -692,7 +684,7 @@ public virtual int SendRecurringPaymentCancelledStoreOwnerNotification(Recurring throw new ArgumentNullException("recurringPayment"); var store = _storeService.GetStoreById(recurringPayment.InitialOrder.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("RecurringPaymentCancelled.StoreOwnerNotification", store.Id); if (messageTemplate == null) @@ -701,20 +693,18 @@ public virtual int SendRecurringPaymentCancelledStoreOwnerNotification(Recurring //tokens var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddOrderTokens(tokens, recurringPayment.InitialOrder, languageId); + _messageTokenProvider.AddOrderTokens(tokens, recurringPayment.InitialOrder, language); _messageTokenProvider.AddCustomerTokens(tokens, recurringPayment.InitialOrder.Customer); _messageTokenProvider.AddRecurringPaymentTokens(tokens, recurringPayment); //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = emailAccount.Email; var toName = emailAccount.DisplayName; - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } #endregion @@ -727,14 +717,13 @@ public virtual int SendRecurringPaymentCancelledStoreOwnerNotification(Recurring /// Newsletter subscription /// Language identifier /// Queued email identifier - public virtual int SendNewsLetterSubscriptionActivationMessage(NewsLetterSubscription subscription, - int languageId) + public virtual int SendNewsLetterSubscriptionActivationMessage(NewsLetterSubscription subscription, int languageId) { if (subscription == null) throw new ArgumentNullException("subscription"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("NewsLetterSubscription.ActivationMessage", store.Id); if (messageTemplate == null) @@ -748,12 +737,11 @@ public virtual int SendNewsLetterSubscriptionActivationMessage(NewsLetterSubscri //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = subscription.Email; var toName = ""; - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -762,14 +750,13 @@ public virtual int SendNewsLetterSubscriptionActivationMessage(NewsLetterSubscri /// Newsletter subscription /// Language identifier /// Queued email identifier - public virtual int SendNewsLetterSubscriptionDeactivationMessage(NewsLetterSubscription subscription, - int languageId) + public virtual int SendNewsLetterSubscriptionDeactivationMessage(NewsLetterSubscription subscription, int languageId) { if (subscription == null) throw new ArgumentNullException("subscription"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("NewsLetterSubscription.DeactivationMessage", store.Id); if (messageTemplate == null) @@ -782,12 +769,11 @@ public virtual int SendNewsLetterSubscriptionDeactivationMessage(NewsLetterSubsc //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = subscription.Email; var toName = ""; - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } #endregion @@ -814,7 +800,7 @@ public virtual int SendProductEmailAFriendMessage(Customer customer, int languag throw new ArgumentNullException("product"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Service.EmailAFriend", store.Id); if (messageTemplate == null) @@ -824,19 +810,19 @@ public virtual int SendProductEmailAFriendMessage(Customer customer, int languag var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); _messageTokenProvider.AddCustomerTokens(tokens, customer); - _messageTokenProvider.AddProductTokens(tokens, product, languageId); + _messageTokenProvider.AddProductTokens(tokens, product, language); + tokens.Add(new Token("EmailAFriend.PersonalMessage", personalMessage, true)); tokens.Add(new Token("EmailAFriend.Email", customerEmail)); //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = friendsEmail; var toName = ""; - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } public virtual int SendProductQuestionMessage(Customer customer, int languageId, Product product, @@ -852,7 +838,7 @@ public virtual int SendProductQuestionMessage(Customer customer, int languageId, throw new ArgumentNullException("product"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Product.AskQuestion", store.Id); if (messageTemplate == null) @@ -861,7 +847,7 @@ public virtual int SendProductQuestionMessage(Customer customer, int languageId, var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); _messageTokenProvider.AddCustomerTokens(tokens, customer); - _messageTokenProvider.AddProductTokens(tokens, product, languageId); + _messageTokenProvider.AddProductTokens(tokens, product, language); tokens.Add(new Token("ProductQuestion.Message", question, true)); tokens.Add(new Token("ProductQuestion.SenderEmail", senderEmail)); @@ -871,11 +857,11 @@ public virtual int SendProductQuestionMessage(Customer customer, int languageId, //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = emailAccount.Email; var toName = emailAccount.DisplayName; - return SendNotification(messageTemplate, emailAccount, languageId, tokens, toEmail, toName, senderEmail, senderName); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, senderEmail, senderName); } /// @@ -894,7 +880,7 @@ public virtual int SendWishlistEmailAFriendMessage(Customer customer, int langua throw new ArgumentNullException("customer"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Wishlist.EmailAFriend", store.Id); if (messageTemplate == null) @@ -910,12 +896,11 @@ public virtual int SendWishlistEmailAFriendMessage(Customer customer, int langua //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = friendsEmail; var toName = ""; - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } #endregion @@ -935,7 +920,7 @@ public virtual int SendNewReturnRequestStoreOwnerNotification(ReturnRequest retu throw new ArgumentNullException("returnRequest"); var store = _storeService.GetStoreById(orderItem.Order.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("NewReturnRequest.StoreOwnerNotification", store.Id); if (messageTemplate == null) @@ -950,17 +935,14 @@ public virtual int SendNewReturnRequestStoreOwnerNotification(ReturnRequest retu //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = emailAccount.Email; var toName = emailAccount.DisplayName; // use customer email as reply address var replyTo = GetReplyToEmail(returnRequest.Customer); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName, - replyTo.Item1, replyTo.Item2); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, replyTo.Item1, replyTo.Item2); } /// @@ -976,7 +958,7 @@ public virtual int SendReturnRequestStatusChangedCustomerNotification(ReturnRequ throw new ArgumentNullException("returnRequest"); var store = _storeService.GetStoreById(orderItem.Order.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("ReturnRequestStatusChanged.CustomerNotification", store.Id); if (messageTemplate == null) @@ -991,14 +973,14 @@ public virtual int SendReturnRequestStatusChangedCustomerNotification(ReturnRequ //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = returnRequest.Customer.FindEmail(); var toName = returnRequest.Customer.GetFullName(); if (toEmail.IsEmpty()) return 0; - return SendNotification(messageTemplate, emailAccount, languageId, tokens, toEmail, toName); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } #endregion @@ -1013,8 +995,7 @@ public virtual int SendReturnRequestStatusChangedCustomerNotification(ReturnRequ /// Forum /// Message language identifier /// Queued email identifier - public int SendNewForumTopicMessage(Customer customer, - ForumTopic forumTopic, Forum forum, int languageId) + public int SendNewForumTopicMessage(Customer customer, ForumTopic forumTopic, Forum forum, int languageId) { if (customer == null) { @@ -1022,6 +1003,7 @@ public int SendNewForumTopicMessage(Customer customer, } var store = _storeContext.CurrentStore; + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Forums.NewForumTopic", store.Id); if (messageTemplate == null) @@ -1034,16 +1016,16 @@ public int SendNewForumTopicMessage(Customer customer, _messageTokenProvider.AddStoreTokens(tokens, store); _messageTokenProvider.AddCustomerTokens(tokens, customer); _messageTokenProvider.AddForumTopicTokens(tokens, forumTopic); - _messageTokenProvider.AddForumTokens(tokens, forumTopic.Forum, languageId); + _messageTokenProvider.AddForumTokens(tokens, forumTopic.Forum, language); //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = customer.Email; var toName = customer.GetFullName(); - return SendNotification(messageTemplate, emailAccount, languageId, tokens, toEmail, toName); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -1056,9 +1038,7 @@ public int SendNewForumTopicMessage(Customer customer, /// Friendly (starts with 1) forum topic page to use for URL generation /// Message language identifier /// Queued email identifier - public int SendNewForumPostMessage(Customer customer, - ForumPost forumPost, ForumTopic forumTopic, - Forum forum, int friendlyForumTopicPageIndex, int languageId) + public int SendNewForumPostMessage(Customer customer, ForumPost forumPost, ForumTopic forumTopic, Forum forum, int friendlyForumTopicPageIndex, int languageId) { if (customer == null) { @@ -1066,6 +1046,7 @@ public int SendNewForumPostMessage(Customer customer, } var store = _storeContext.CurrentStore; + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Forums.NewForumPost", store.Id); if (messageTemplate == null) @@ -1079,16 +1060,16 @@ public int SendNewForumPostMessage(Customer customer, _messageTokenProvider.AddForumPostTokens(tokens, forumPost); _messageTokenProvider.AddCustomerTokens(tokens, customer); _messageTokenProvider.AddForumTopicTokens(tokens, forumPost.ForumTopic, friendlyForumTopicPageIndex, forumPost.Id); - _messageTokenProvider.AddForumTokens(tokens, forumPost.ForumTopic.Forum, languageId); + _messageTokenProvider.AddForumTokens(tokens, forumPost.ForumTopic.Forum, language); //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = customer.Email; var toName = customer.GetFullName(); - return SendNotification(messageTemplate, emailAccount, languageId, tokens, toEmail, toName); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -1163,7 +1144,8 @@ public virtual int SendGenericMessage(string messageTemplateName, Action @@ -1231,14 +1212,13 @@ public virtual int SendGiftCardNotification(GiftCard giftCard, int languageId) /// Product review /// Message language identifier /// Queued email identifier - public virtual int SendProductReviewNotificationMessage(ProductReview productReview, - int languageId) + public virtual int SendProductReviewNotificationMessage(ProductReview productReview, int languageId) { if (productReview == null) throw new ArgumentNullException("productReview"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Product.ProductReview", store.Id); if (messageTemplate == null) @@ -1252,17 +1232,14 @@ public virtual int SendProductReviewNotificationMessage(ProductReview productRev //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = emailAccount.Email; var toName = emailAccount.DisplayName; // use customer email as reply address var replyTo = GetReplyToEmail(productReview.Customer); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName, - replyTo.Item1, replyTo.Item2); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, replyTo.Item1, replyTo.Item2); } /// @@ -1277,7 +1254,7 @@ public virtual int SendQuantityBelowStoreOwnerNotification(Product product, int throw new ArgumentNullException("product"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("QuantityBelow.StoreOwnerNotification", store.Id); if (messageTemplate == null) @@ -1285,17 +1262,16 @@ public virtual int SendQuantityBelowStoreOwnerNotification(Product product, int var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddProductTokens(tokens, product, languageId); + _messageTokenProvider.AddProductTokens(tokens, product, language); //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = emailAccount.Email; var toName = emailAccount.DisplayName; - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } /// @@ -1306,14 +1282,13 @@ public virtual int SendQuantityBelowStoreOwnerNotification(Product product, int /// Received VAT address /// Message language identifier /// Queued email identifier - public virtual int SendNewVatSubmittedStoreOwnerNotification(Customer customer, - string vatName, string vatAddress, int languageId) + public virtual int SendNewVatSubmittedStoreOwnerNotification(Customer customer, string vatName, string vatAddress, int languageId) { if (customer == null) throw new ArgumentNullException("customer"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("NewVATSubmitted.StoreOwnerNotification", store.Id); if (messageTemplate == null) @@ -1329,17 +1304,14 @@ public virtual int SendNewVatSubmittedStoreOwnerNotification(Customer customer, //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = emailAccount.Email; var toName = emailAccount.DisplayName; // use customer email as reply address var replyTo = GetReplyToEmail(customer); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName, - replyTo.Item1, replyTo.Item2); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, replyTo.Item1, replyTo.Item2); } /// @@ -1354,7 +1326,7 @@ public virtual int SendBlogCommentNotificationMessage(BlogComment blogComment, i throw new ArgumentNullException("blogComment"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Blog.BlogComment", store.Id); if (messageTemplate == null) @@ -1368,17 +1340,14 @@ public virtual int SendBlogCommentNotificationMessage(BlogComment blogComment, i //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = emailAccount.Email; var toName = emailAccount.DisplayName; // use customer email as reply address var replyTo = GetReplyToEmail(blogComment.Customer); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName, - replyTo.Item1, replyTo.Item2); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, replyTo.Item1, replyTo.Item2); } /// @@ -1393,7 +1362,7 @@ public virtual int SendNewsCommentNotificationMessage(NewsComment newsComment, i throw new ArgumentNullException("newsComment"); var store = _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("News.NewsComment", store.Id); if (messageTemplate == null) @@ -1407,17 +1376,14 @@ public virtual int SendNewsCommentNotificationMessage(NewsComment newsComment, i //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var toEmail = emailAccount.Email; var toName = emailAccount.DisplayName; // use customer email as sender/reply address var replyTo = GetReplyToEmail(newsComment.Customer); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName, - replyTo.Item1, replyTo.Item2); + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName, replyTo.Item1, replyTo.Item2); } /// @@ -1432,7 +1398,7 @@ public virtual int SendBackInStockNotification(BackInStockSubscription subscript throw new ArgumentNullException("subscription"); var store = _storeService.GetStoreById(subscription.StoreId) ?? _storeContext.CurrentStore; - languageId = EnsureLanguageIsActive(languageId, store.Id); + var language = EnsureLanguageIsActive(languageId, store.Id); var messageTemplate = GetActiveMessageTemplate("Customer.BackInStock", store.Id); if (messageTemplate == null) @@ -1447,13 +1413,12 @@ public virtual int SendBackInStockNotification(BackInStockSubscription subscript //event notification _eventPublisher.MessageTokensAdded(messageTemplate, tokens); - var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, languageId); + var emailAccount = GetEmailAccountOfMessageTemplate(messageTemplate, language.Id); var customer = subscription.Customer; var toEmail = customer.Email; var toName = customer.GetFullName(); - return SendNotification(messageTemplate, emailAccount, - languageId, tokens, - toEmail, toName); + + return SendNotification(messageTemplate, emailAccount, language.Id, tokens, toEmail, toName); } #endregion diff --git a/src/Libraries/SmartStore.Services/News/INewsService.cs b/src/Libraries/SmartStore.Services/News/INewsService.cs index c07cceb570..d25a8d8afa 100644 --- a/src/Libraries/SmartStore.Services/News/INewsService.cs +++ b/src/Libraries/SmartStore.Services/News/INewsService.cs @@ -1,9 +1,7 @@ using System; using System.Linq; -using System.Web.Mvc; using SmartStore.Core; using SmartStore.Core.Domain.News; -using SmartStore.Utilities; namespace SmartStore.Services.News { @@ -61,13 +59,5 @@ public partial interface INewsService /// /// News item void UpdateCommentTotals(NewsItem newsItem); - - /// - /// Creates a RSS feed with news items - /// - /// UrlHelper to generate URLs - /// Language identifier - /// SmartSyndicationFeed object - SmartSyndicationFeed CreateRssFeed(UrlHelper urlHelper, int languageId); } } diff --git a/src/Libraries/SmartStore.Services/News/NewsService.cs b/src/Libraries/SmartStore.Services/News/NewsService.cs index 66adadf90e..743ab7f9f4 100644 --- a/src/Libraries/SmartStore.Services/News/NewsService.cs +++ b/src/Libraries/SmartStore.Services/News/NewsService.cs @@ -1,16 +1,10 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.ServiceModel.Syndication; -using System.Web.Mvc; using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Domain.News; using SmartStore.Core.Domain.Stores; using SmartStore.Core.Events; -using SmartStore.Services.Localization; -using SmartStore.Services.Seo; -using SmartStore.Utilities; namespace SmartStore.Services.News { @@ -24,7 +18,6 @@ public partial class NewsService : INewsService private readonly IRepository _newsItemRepository; private readonly IRepository _storeMappingRepository; private readonly ICommonServices _services; - private readonly ILanguageService _languageService; private readonly NewsSettings _newsSettings; @@ -35,13 +28,11 @@ public partial class NewsService : INewsService public NewsService(IRepository newsItemRepository, IRepository storeMappingRepository, ICommonServices services, - ILanguageService languageService, NewsSettings newsSettings) { _newsItemRepository = newsItemRepository; _storeMappingRepository = storeMappingRepository; _services = services; - _languageService = languageService; _newsSettings = newsSettings; this.QuerySettings = DbQuerySettings.Default; @@ -211,53 +202,6 @@ public virtual void UpdateCommentTotals(NewsItem newsItem) UpdateNews(newsItem); } - /// - /// Creates a RSS feed with news items - /// - /// UrlHelper to generate URLs - /// Language identifier - /// SmartSyndicationFeed object - public virtual SmartSyndicationFeed CreateRssFeed(UrlHelper urlHelper, int languageId) - { - if (urlHelper == null) - throw new ArgumentNullException("urlHelper"); - - DateTime? maxAge = null; - var protocol = _services.WebHelper.IsCurrentConnectionSecured() ? "https" : "http"; - var selfLink = urlHelper.Action("rss", "News", new { languageId = languageId }, protocol); - var newsLink = urlHelper.RouteUrl("NewsArchive", null, protocol); - - var title = "{0} - News".FormatInvariant(_services.StoreContext.CurrentStore.Name); - - if (_newsSettings.MaxAgeInDays > 0) - maxAge = DateTime.UtcNow.Subtract(new TimeSpan(_newsSettings.MaxAgeInDays, 0, 0, 0)); - - var language = _languageService.GetLanguageById(languageId); - var feed = new SmartSyndicationFeed(new Uri(newsLink), title); - - feed.AddNamespaces(true); - feed.Init(selfLink, language); - - if (!_newsSettings.Enabled) - return feed; - - var items = new List(); - var newsItems = GetAllNews(languageId, _services.StoreContext.CurrentStore.Id, 0, int.MaxValue, false, maxAge); - - foreach (var news in newsItems) - { - var newsUrl = urlHelper.RouteUrl("NewsItem", new { SeName = news.GetSeName(news.LanguageId, ensureTwoPublishedLanguages: false) }, "http"); - - var item = feed.CreateItem(news.Title, news.Short, newsUrl, news.CreatedOnUtc, news.Full); - - items.Add(item); - } - - feed.Items = items; - - return feed; - } - #endregion } } diff --git a/src/Libraries/SmartStore.Services/Orders/AppliedGiftCard.cs b/src/Libraries/SmartStore.Services/Orders/AppliedGiftCard.cs index ea8cdd8be8..4318b43cd6 100644 --- a/src/Libraries/SmartStore.Services/Orders/AppliedGiftCard.cs +++ b/src/Libraries/SmartStore.Services/Orders/AppliedGiftCard.cs @@ -1,6 +1,3 @@ - - - using SmartStore.Core.Domain.Orders; namespace SmartStore.Services.Orders diff --git a/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeService.cs b/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeService.cs index d9a340c451..7e266a98b1 100644 --- a/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Orders/CheckoutAttributeService.cs @@ -4,18 +4,19 @@ using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Stores; using SmartStore.Core.Events; namespace SmartStore.Services.Orders { - /// - /// Checkout attribute service - /// - public partial class CheckoutAttributeService : ICheckoutAttributeService + /// + /// Checkout attribute service + /// + public partial class CheckoutAttributeService : ICheckoutAttributeService { #region Constants - private const string CHECKOUTATTRIBUTES_ALL_KEY = "SmartStore.checkoutattribute.all-{0}"; + private const string CHECKOUTATTRIBUTES_ALL_KEY = "SmartStore.checkoutattribute.all-{0}-{1}"; private const string CHECKOUTATTRIBUTEVALUES_ALL_KEY = "SmartStore.checkoutattributevalue.all-{0}"; private const string CHECKOUTATTRIBUTES_PATTERN_KEY = "SmartStore.checkoutattribute."; private const string CHECKOUTATTRIBUTEVALUES_PATTERN_KEY = "SmartStore.checkoutattributevalue."; @@ -28,7 +29,8 @@ public partial class CheckoutAttributeService : ICheckoutAttributeService private readonly IRepository _checkoutAttributeRepository; private readonly IRepository _checkoutAttributeValueRepository; - private readonly IEventPublisher _eventPublisher; + private readonly IRepository _storeMappingRepository; + private readonly IEventPublisher _eventPublisher; private readonly ICacheManager _cacheManager; #endregion @@ -45,25 +47,31 @@ public partial class CheckoutAttributeService : ICheckoutAttributeService public CheckoutAttributeService(ICacheManager cacheManager, IRepository checkoutAttributeRepository, IRepository checkoutAttributeValueRepository, - IEventPublisher eventPublisher) + IRepository storeMappingRepository, + IEventPublisher eventPublisher) { _cacheManager = cacheManager; _checkoutAttributeRepository = checkoutAttributeRepository; _checkoutAttributeValueRepository = checkoutAttributeValueRepository; + _storeMappingRepository = storeMappingRepository; _eventPublisher = eventPublisher; - } - #endregion + this.QuerySettings = DbQuerySettings.Default; + } - #region Methods + #endregion - #region Checkout attributes + public DbQuerySettings QuerySettings { get; set; } - /// - /// Deletes a checkout attribute - /// - /// Checkout attribute - public virtual void DeleteCheckoutAttribute(CheckoutAttribute checkoutAttribute) + #region Methods + + #region Checkout attributes + + /// + /// Deletes a checkout attribute + /// + /// Checkout attribute + public virtual void DeleteCheckoutAttribute(CheckoutAttribute checkoutAttribute) { if (checkoutAttribute == null) throw new ArgumentNullException("checkoutAttribute"); @@ -77,26 +85,56 @@ public virtual void DeleteCheckoutAttribute(CheckoutAttribute checkoutAttribute) _eventPublisher.EntityDeleted(checkoutAttribute); } - /// - /// Gets all checkout attributes - /// - /// Checkout attribute collection - public virtual IList GetAllCheckoutAttributes(bool showHidden = false) + /// + /// Gets checkout attributes + /// + /// Whether to filter result by store identifier + /// A value indicating whether to show hidden records + /// Checkout attributes query + public virtual IQueryable GetCheckoutAttributes(int storeId = 0, bool showHidden = false) + { + var query = _checkoutAttributeRepository.Table; + + if (!showHidden) + query = query.Where(x => x.IsActive); + + if (storeId > 0 && !QuerySettings.IgnoreMultiStore) + { + query = + from x in query + join sm in _storeMappingRepository.Table on new { c1 = x.Id, c2 = "CheckoutAttribute" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into x_sm + from sm in x_sm.DefaultIfEmpty() + where !x.LimitedToStores || storeId == sm.StoreId + select x; + + query = + from x in query + group x by x.Id into grp + orderby grp.Key + select grp.FirstOrDefault(); + } + + query = query.OrderBy(x => x.DisplayOrder); + + return query; + } + + /// + /// Gets all checkout attributes + /// + /// Whether to filter result by store identifier + /// A value indicating whether to show hidden records + /// Checkout attribute collection + public virtual IList GetAllCheckoutAttributes(int storeId = 0, bool showHidden = false) { - string key = CHECKOUTATTRIBUTES_ALL_KEY.FormatInvariant(showHidden); + string key = CHECKOUTATTRIBUTES_ALL_KEY.FormatInvariant(storeId, showHidden); return _cacheManager.Get(key, () => { - var query = _checkoutAttributeRepository.Table; + var query = GetCheckoutAttributes(storeId, showHidden); - if (!showHidden) - query = query.Where(x => x.IsActive); - - query = query.OrderBy(x => x.DisplayOrder); - - var checkoutAttributes = query.ToList(); - return checkoutAttributes; - }); + return query.ToList(); + }); } /// diff --git a/src/Libraries/SmartStore.Services/Orders/ICheckoutAttributeService.cs b/src/Libraries/SmartStore.Services/Orders/ICheckoutAttributeService.cs index 2ceb9c3396..9a520ca28b 100644 --- a/src/Libraries/SmartStore.Services/Orders/ICheckoutAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Orders/ICheckoutAttributeService.cs @@ -1,12 +1,13 @@ using System.Collections.Generic; +using System.Linq; using SmartStore.Core.Domain.Orders; namespace SmartStore.Services.Orders { - /// - /// Checkout attribute service - /// - public partial interface ICheckoutAttributeService + /// + /// Checkout attribute service + /// + public partial interface ICheckoutAttributeService { #region Checkout attributes @@ -16,11 +17,21 @@ public partial interface ICheckoutAttributeService /// Checkout attribute void DeleteCheckoutAttribute(CheckoutAttribute checkoutAttribute); - /// - /// Gets all checkout attributes - /// - /// Checkout attribute collection - IList GetAllCheckoutAttributes(bool showHidden = false); + /// + /// Gets checkout attributes + /// + /// Whether to filter result by store identifier + /// A value indicating whether to show hidden records + /// Checkout attributes query + IQueryable GetCheckoutAttributes(int storeId = 0, bool showHidden = false); + + /// + /// Gets all checkout attributes + /// + /// Whether to filter result by store identifier + /// A value indicating whether to show hidden records + /// Checkout attribute collection + IList GetAllCheckoutAttributes(int storeId = 0, bool showHidden = false); /// /// Gets a checkout attribute diff --git a/src/Libraries/SmartStore.Services/Orders/IOrderProcessingService.cs b/src/Libraries/SmartStore.Services/Orders/IOrderProcessingService.cs index 9452f42b7b..9c3f825b03 100644 --- a/src/Libraries/SmartStore.Services/Orders/IOrderProcessingService.cs +++ b/src/Libraries/SmartStore.Services/Orders/IOrderProcessingService.cs @@ -266,5 +266,14 @@ public partial interface IOrderProcessingService /// Shopping cart /// true - OK; false - minimum order total amount is not reached bool ValidateMinOrderTotalAmount(IList cart); + + /// + /// Adds a shipment to an order + /// + /// Order + /// Tracking number + /// Quantities by order item identifiers. null to use the remaining total number of products for each order item. + /// New shipment, null if no shipment was added + Shipment AddShipment(Order order, string trackingNumber, Dictionary quantities); } } diff --git a/src/Libraries/SmartStore.Services/Orders/IOrderService.cs b/src/Libraries/SmartStore.Services/Orders/IOrderService.cs index 039beeef57..9fca02b17f 100644 --- a/src/Libraries/SmartStore.Services/Orders/IOrderService.cs +++ b/src/Libraries/SmartStore.Services/Orders/IOrderService.cs @@ -164,17 +164,25 @@ IPagedList SearchOrders(int storeId, int customerId, DateTime? startTime, /// Payment method system name /// Order Order GetOrderByAuthorizationTransactionIdAndPaymentMethod(string authorizationTransactionId, string paymentMethodSystemName); - - #endregion - #region Orders items - - /// - /// Gets an order item - /// - /// Order item identifier - /// Order item - OrderItem GetOrderItemById(int orderItemId); + /// + /// Shortcut to add an order + /// + /// Order + /// Order note + /// Whether to display the note to the customer + void AddOrderNote(Order order, string note, bool displayToCustomer = false); + + #endregion + + #region Orders items + + /// + /// Gets an order item + /// + /// Order item identifier + /// Order item + OrderItem GetOrderItemById(int orderItemId); /// /// Gets an order item diff --git a/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs b/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs index 4995d1bd38..0993597da4 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Text; using SmartStore.Core; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Common; @@ -16,6 +15,7 @@ using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Domain.Tax; using SmartStore.Core.Events; +using SmartStore.Core.Localization; using SmartStore.Core.Logging; using SmartStore.Core.Plugins; using SmartStore.Services.Affiliates; @@ -33,10 +33,10 @@ namespace SmartStore.Services.Orders { - /// - /// Order processing service - /// - public partial class OrderProcessingService : IOrderProcessingService + /// + /// Order processing service + /// + public partial class OrderProcessingService : IOrderProcessingService { #region Fields @@ -69,8 +69,9 @@ public partial class OrderProcessingService : IOrderProcessingService private readonly IAffiliateService _affiliateService; private readonly IEventPublisher _eventPublisher; private readonly IGenericAttributeService _genericAttributeService; + private readonly INewsLetterSubscriptionService _newsLetterSubscriptionService; - private readonly PaymentSettings _paymentSettings; + private readonly PaymentSettings _paymentSettings; private readonly RewardPointsSettings _rewardPointsSettings; private readonly OrderSettings _orderSettings; private readonly TaxSettings _taxSettings; @@ -78,48 +79,48 @@ public partial class OrderProcessingService : IOrderProcessingService private readonly CurrencySettings _currencySettings; private readonly ShoppingCartSettings _shoppingCartSettings; - #endregion + #endregion - #region Ctor + #region Ctor - /// - /// Ctor - /// - /// Order service - /// Web helper - /// Localization service - /// Language service - /// Product service - /// Payment service - /// Logger - /// Order total calculationservice - /// Price calculation service - /// Price formatter - /// Product attribute parser - /// Product attribute formatter - /// Gift card service - /// Shopping cart service - /// Checkout attribute service - /// Shipping service - /// Shipment service - /// Tax service - /// Customer service - /// Discount service - /// Encryption service - /// Work context + /// + /// Ctor + /// + /// Order service + /// Web helper + /// Localization service + /// Language service + /// Product service + /// Payment service + /// Logger + /// Order total calculationservice + /// Price calculation service + /// Price formatter + /// Product attribute parser + /// Product attribute formatter + /// Gift card service + /// Shopping cart service + /// Checkout attribute service + /// Shipping service + /// Shipment service + /// Tax service + /// Customer service + /// Discount service + /// Encryption service + /// Work context /// Store context - /// Workflow message service - /// Customer activity service - /// Currency service + /// Workflow message service + /// Customer activity service + /// Currency service /// Affiliate service - /// Event published - /// Payment settings - /// Reward points settings - /// Order settings - /// Tax settings - /// Localization settings - /// Currency settings - public OrderProcessingService(IOrderService orderService, + /// Event published + /// Payment settings + /// Reward points settings + /// Order settings + /// Tax settings + /// Localization settings + /// Currency settings + public OrderProcessingService(IOrderService orderService, IWebHelper webHelper, ILocalizationService localizationService, ILanguageService languageService, @@ -148,7 +149,8 @@ public OrderProcessingService(IOrderService orderService, IAffiliateService affiliateService, IEventPublisher eventPublisher, IGenericAttributeService genericAttributeService, - PaymentSettings paymentSettings, + INewsLetterSubscriptionService newsLetterSubscriptionService, + PaymentSettings paymentSettings, RewardPointsSettings rewardPointsSettings, OrderSettings orderSettings, TaxSettings taxSettings, @@ -185,6 +187,7 @@ public OrderProcessingService(IOrderService orderService, this._affiliateService = affiliateService; this._eventPublisher = eventPublisher; this._genericAttributeService = genericAttributeService; + this._newsLetterSubscriptionService = newsLetterSubscriptionService; this._paymentSettings = paymentSettings; this._rewardPointsSettings = rewardPointsSettings; this._orderSettings = orderSettings; @@ -192,21 +195,31 @@ public OrderProcessingService(IOrderService orderService, this._localizationSettings = localizationSettings; this._currencySettings = currencySettings; this._shoppingCartSettings = shoppingCartSettings; - } - #endregion + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } - #region Utilities + #endregion + + #region Utilities private decimal Round(decimal value) { return (_shoppingCartSettings.RoundPricesDuringCalculation ? Math.Round(value, 2) : value); } - private string TNote(string resKey) - { - return _localizationService.GetResource("Admin.OrderNotice." + resKey); - } + private void ProcessErrors(Order order, IList errors, string messageKey) + { + if (errors.Any()) + { + var msg = string.Concat(T(messageKey, order.GetOrderNumber()), " ", string.Join(" ", errors)); + + _orderService.AddOrderNote(order, msg); + _logger.InsertLog(LogLevel.Error, msg, msg); + } + } /// /// Award reward points @@ -229,18 +242,15 @@ protected void AwardRewardPoints(Order order, decimal? amount = null) if (order.RewardPointsWereAdded) return; - // Truncate increases the risk of inaccuracy of rounding - //int points = (int)Math.Truncate((amount ?? order.OrderTotal) / _rewardPointsSettings.PointsForPurchases_Amount * _rewardPointsSettings.PointsForPurchases_Points); - - // why are points awarded for OrderTotal? wouldn't be OrderSubtotalInclTax better? - - int points = (int)Math.Round((amount ?? order.OrderTotal) / _rewardPointsSettings.PointsForPurchases_Amount * _rewardPointsSettings.PointsForPurchases_Points); + // Trucate same as Floor for positive amounts + int points = (int)Math.Truncate((amount ?? order.OrderTotal) / _rewardPointsSettings.PointsForPurchases_Amount * _rewardPointsSettings.PointsForPurchases_Points); if (points == 0) return; //add reward points - order.Customer.AddRewardPointsHistoryEntry(points, string.Format(_localizationService.GetResource("RewardPoints.Message.EarnedForOrder"), order.GetOrderNumber())); + order.Customer.AddRewardPointsHistoryEntry(points, T("RewardPoints.Message.EarnedForOrder", order.GetOrderNumber())); order.RewardPointsWereAdded = true; + _orderService.UpdateOrder(order); } @@ -277,7 +287,7 @@ protected void ReduceRewardPoints(Order order, decimal? amount = null) return; //reduce reward points - order.Customer.AddRewardPointsHistoryEntry(-points, string.Format(_localizationService.GetResource("RewardPoints.Message.ReducedForOrder"), order.GetOrderNumber())); + order.Customer.AddRewardPointsHistoryEntry(-points, T("RewardPoints.Message.ReducedForOrder", order.GetOrderNumber())); if (!order.RewardPointsRemaining.HasValue) order.RewardPointsRemaining = (int)Math.Round(order.OrderTotal / _rewardPointsSettings.PointsForPurchases_Amount * _rewardPointsSettings.PointsForPurchases_Points); @@ -295,34 +305,42 @@ protected void ReduceRewardPoints(Order order, decimal? amount = null) protected void SetActivatedValueForPurchasedGiftCards(Order order, bool activate) { var giftCards = _giftCardService.GetAllGiftCards(order.Id, null, null, !activate); + foreach (var gc in giftCards) { if (activate) { //activate - bool isRecipientNotified = gc.IsRecipientNotified; + var isRecipientNotified = gc.IsRecipientNotified; + if (gc.GiftCardType == GiftCardType.Virtual) { //send email for virtual gift card - if (!String.IsNullOrEmpty(gc.RecipientEmail) && - !String.IsNullOrEmpty(gc.SenderEmail)) + if (!String.IsNullOrEmpty(gc.RecipientEmail) && !String.IsNullOrEmpty(gc.SenderEmail)) { var customerLang = _languageService.GetLanguageById(order.CustomerLanguageId); if (customerLang == null) customerLang = _languageService.GetAllLanguages().FirstOrDefault(); - int queuedEmailId = _workflowMessageService.SendGiftCardNotification(gc, customerLang.Id); - if (queuedEmailId > 0) - isRecipientNotified = true; + + var queuedEmailId = _workflowMessageService.SendGiftCardNotification(gc, customerLang.Id); + + if (queuedEmailId > 0) + { + isRecipientNotified = true; + } } } + gc.IsGiftCardActivated = true; gc.IsRecipientNotified = isRecipientNotified; + _giftCardService.UpdateGiftCard(gc); } else { //deactivate gc.IsGiftCardActivated = false; + _giftCardService.UpdateGiftCard(gc); } } @@ -347,15 +365,7 @@ protected void SetOrderStatus(Order order, OrderStatus os, bool notifyCustomer) order.OrderStatusId = (int)os; _orderService.UpdateOrder(order); - //order notes, notifications - order.OrderNotes.Add(new OrderNote - { - Note = string.Format(TNote("OrderStatusChanged"), os.GetLocalizedEnum(_localizationService)), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); - + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderStatusChanged", os.GetLocalizedEnum(_localizationService))); if (prevOrderStatus != OrderStatus.Complete && os == OrderStatus.Complete && notifyCustomer) { @@ -363,13 +373,7 @@ protected void SetOrderStatus(Order order, OrderStatus os, bool notifyCustomer) int orderCompletedCustomerNotificationQueuedEmailId = _workflowMessageService.SendOrderCompletedCustomerNotification(order, order.CustomerLanguageId); if (orderCompletedCustomerNotificationQueuedEmailId > 0) { - order.OrderNotes.Add(new OrderNote - { - Note = string.Format(TNote("CustomerCompletedEmailQueued"), orderCompletedCustomerNotificationQueuedEmailId), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerCompletedEmailQueued", orderCompletedCustomerNotificationQueuedEmailId)); } } @@ -379,13 +383,7 @@ protected void SetOrderStatus(Order order, OrderStatus os, bool notifyCustomer) int orderCancelledCustomerNotificationQueuedEmailId = _workflowMessageService.SendOrderCancelledCustomerNotification(order, order.CustomerLanguageId); if (orderCancelledCustomerNotificationQueuedEmailId > 0) { - order.OrderNotes.Add(new OrderNote - { - Note = string.Format(TNote("CustomerCancelledEmailQueued"), orderCancelledCustomerNotificationQueuedEmailId), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerCancelledEmailQueued", orderCancelledCustomerNotificationQueuedEmailId)); } } @@ -400,15 +398,13 @@ protected void SetOrderStatus(Order order, OrderStatus os, bool notifyCustomer) } //gift cards activation - if (_orderSettings.GiftCards_Activated_OrderStatusId > 0 && - _orderSettings.GiftCards_Activated_OrderStatusId == (int)order.OrderStatus) + if (_orderSettings.GiftCards_Activated_OrderStatusId > 0 && _orderSettings.GiftCards_Activated_OrderStatusId == (int)order.OrderStatus) { SetActivatedValueForPurchasedGiftCards(order, true); } //gift cards deactivation - if (_orderSettings.GiftCards_Deactivated_OrderStatusId > 0 && - _orderSettings.GiftCards_Deactivated_OrderStatusId == (int)order.OrderStatus) + if (_orderSettings.GiftCards_Deactivated_OrderStatusId > 0 && _orderSettings.GiftCards_Deactivated_OrderStatusId == (int)order.OrderStatus) { SetActivatedValueForPurchasedGiftCards(order, false); } @@ -468,9 +464,11 @@ public void CheckOrderStatus(Order order) /// /// Process payment request /// Place order result - public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentRequest, Dictionary extraData) + 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 + // think about moving functionality of processing recurring orders (after the initial order was placed) to ProcessNextRecurringPayment() method if (processPaymentRequest == null) throw new ArgumentNullException("processPaymentRequest"); @@ -484,28 +482,30 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR { #region Order details (customer, totals) - //Recurring orders. Load initial order - Order initialOrder = _orderService.GetOrderById(processPaymentRequest.InitialOrderId); + // Recurring orders. Load initial order + var initialOrder = _orderService.GetOrderById(processPaymentRequest.InitialOrderId); if (processPaymentRequest.IsRecurringPayment) { if (initialOrder == null) - throw new ArgumentException("Initial order is not set for recurring payment"); + throw new ArgumentException(T("Order.InitialOrderDoesNotExistForRecurringPayment")); processPaymentRequest.PaymentMethodSystemName = initialOrder.PaymentMethodSystemName; } - //customer + // customer var customer = _customerService.GetCustomerById(processPaymentRequest.CustomerId); if (customer == null) - throw new ArgumentException("Customer is not set"); + throw new ArgumentException(T("Customer.DoesNotExist")); - //affilites - int affiliateId = 0; + // affilites + var affiliateId = 0; var affiliate = _affiliateService.GetAffiliateById(customer.AffiliateId); if (affiliate != null && affiliate.Active && !affiliate.Deleted) + { affiliateId = affiliate.Id; + } - //customer currency + // customer currency string customerCurrencyCode = ""; decimal customerCurrencyRate = decimal.Zero; if (!processPaymentRequest.IsRecurringPayment) @@ -524,87 +524,77 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR customerCurrencyRate = initialOrder.CurrencyRate; } - //customer language + // customer language Language customerLanguage = null; if (!processPaymentRequest.IsRecurringPayment) { - customerLanguage = _languageService.GetLanguageById(customer.GetAttribute( - SystemCustomerAttributeNames.LanguageId, processPaymentRequest.StoreId)); + customerLanguage = _languageService.GetLanguageById(customer.GetAttribute(SystemCustomerAttributeNames.LanguageId, processPaymentRequest.StoreId)); } else { customerLanguage = _languageService.GetLanguageById(initialOrder.CustomerLanguageId); } - if (customerLanguage == null || !customerLanguage.Published) - customerLanguage = _workContext.WorkingLanguage; - //check whether customer is guest + if (customerLanguage == null || !customerLanguage.Published) + { + customerLanguage = _workContext.WorkingLanguage; + } + + // check whether customer is guest if (customer.IsGuest() && !_orderSettings.AnonymousCheckoutAllowed) - throw new SmartException("Anonymous checkout is not allowed"); + throw new SmartException(T("Checkout.AnonymousNotAllowed")); var storeId = _storeContext.CurrentStore.Id; - //load and validate customer shopping cart + // load and validate customer shopping cart IList cart = null; if (!processPaymentRequest.IsRecurringPayment) { //load shopping cart - cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, processPaymentRequest.StoreId); + if (processPaymentRequest.ShoppingCartItems.Count > 0) + cart = processPaymentRequest.ShoppingCartItems; + else + cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, processPaymentRequest.StoreId); if (cart.Count == 0) - throw new SmartException("Cart is empty"); + throw new SmartException(T("ShoppingCart.CartIsEmpty")); - //validate the entire shopping cart - var warnings = _shoppingCartService.GetShoppingCartWarnings(cart, - customer.GetAttribute(SystemCustomerAttributeNames.CheckoutAttributes), true); + // validate the entire shopping cart + var warnings = _shoppingCartService.GetShoppingCartWarnings(cart, customer.GetAttribute(SystemCustomerAttributeNames.CheckoutAttributes), true); if (warnings.Count > 0) - { - var warningsSb = new StringBuilder(); - foreach (string warning in warnings) - { - warningsSb.Append(warning); - warningsSb.Append(";"); - } - throw new SmartException(warningsSb.ToString()); - } + throw new SmartException(string.Join(" ", warnings)); - //validate individual cart items + // 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) - { - var warningsSb = new StringBuilder(); - foreach (string warning in sciWarnings) - { - warningsSb.Append(warning); - warningsSb.Append(";"); - } - throw new SmartException(warningsSb.ToString()); - } + throw new SmartException(string.Join(" ", sciWarnings)); } } - //min totals validation + // min totals validation if (!processPaymentRequest.IsRecurringPayment) { - bool minOrderSubtotalAmountOk = ValidateMinOrderSubtotalAmount(cart); + var minOrderSubtotalAmountOk = ValidateMinOrderSubtotalAmount(cart); if (!minOrderSubtotalAmountOk) { - decimal minOrderSubtotalAmount = _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderSubtotalAmount, _workContext.WorkingCurrency); - throw new SmartException(string.Format(_localizationService.GetResource("Checkout.MinOrderSubtotalAmount"), _priceFormatter.FormatPrice(minOrderSubtotalAmount, true, false))); + var minOrderSubtotalAmount = _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderSubtotalAmount, _workContext.WorkingCurrency); + throw new SmartException(T("Checkout.MinOrderSubtotalAmount", _priceFormatter.FormatPrice(minOrderSubtotalAmount, true, false))); } - bool minOrderTotalAmountOk = ValidateMinOrderTotalAmount(cart); + + var minOrderTotalAmountOk = ValidateMinOrderTotalAmount(cart); if (!minOrderTotalAmountOk) { - decimal minOrderTotalAmount = _currencyService.ConvertFromPrimaryStoreCurrency(_orderSettings.MinOrderTotalAmount, _workContext.WorkingCurrency); - throw new SmartException(string.Format(_localizationService.GetResource("Checkout.MinOrderTotalAmount"), _priceFormatter.FormatPrice(minOrderTotalAmount, true, false))); + 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) { @@ -615,7 +605,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR customerTaxDisplayType = initialOrder.CustomerTaxDisplayType; } - //checkout attributes + // checkout attributes string checkoutAttributeDescription, checkoutAttributesXml; if (!processPaymentRequest.IsRecurringPayment) { @@ -628,15 +618,15 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR 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 + // sub total decimal orderSubTotalInclTax, orderSubTotalExclTax; decimal orderSubTotalDiscountInclTax = 0, orderSubTotalDiscountExclTax = 0; if (!processPaymentRequest.IsRecurringPayment) { - //sub total (incl tax) + // sub total (incl tax) decimal orderSubTotalDiscountAmount1 = decimal.Zero; Discount orderSubTotalAppliedDiscount1 = null; decimal subTotalWithoutDiscountBase1 = decimal.Zero; @@ -648,11 +638,11 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR 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) + // sub total (excl tax) decimal orderSubTotalDiscountAmount2 = decimal.Zero; Discount orderSubTotalAppliedDiscount2 = null; decimal subTotalWithoutDiscountBase2 = decimal.Zero; @@ -671,7 +661,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR } - //shipping info + // shipping info bool shoppingCartRequiresShipping = false; if (!processPaymentRequest.IsRecurringPayment) { @@ -687,7 +677,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR { if (!processPaymentRequest.IsRecurringPayment) { - var shippingOption = customer.GetAttribute(SystemCustomerAttributeNames.SelectedShippingOption, processPaymentRequest.StoreId); + var shippingOption = customer.GetAttribute(SystemCustomerAttributeNames.SelectedShippingOption, processPaymentRequest.StoreId); if (shippingOption != null) { shippingMethodName = shippingOption.Name; @@ -701,7 +691,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR } } - //shipping total + // shipping total decimal? orderShippingTotalInclTax, orderShippingTotalExclTax = null; decimal orderShippingTaxRate = decimal.Zero; if (!processPaymentRequest.IsRecurringPayment) @@ -710,7 +700,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR orderShippingTotalInclTax = _orderTotalCalculationService.GetShoppingCartShippingTotal(cart, true, out orderShippingTaxRate, out shippingTotalDiscount); orderShippingTotalExclTax = _orderTotalCalculationService.GetShoppingCartShippingTotal(cart, false); if (!orderShippingTotalInclTax.HasValue || !orderShippingTotalExclTax.HasValue) - throw new SmartException("Shipping total couldn't be calculated"); + throw new SmartException(T("Order.CannotCalculateShippingTotal")); if (shippingTotalDiscount != null && !appliedDiscounts.Any(x => x.Id == shippingTotalDiscount.Id)) appliedDiscounts.Add(shippingTotalDiscount); @@ -749,7 +739,9 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR //VAT number var customerVatStatus = (VatNumberStatus)customer.GetAttribute(SystemCustomerAttributeNames.VatNumberStatusId); if (_taxSettings.EuVatEnabled && customerVatStatus == VatNumberStatus.Valid) + { vatNumber = customer.GetAttribute(SystemCustomerAttributeNames.VatNumber); + } //tax rates foreach (var kvp in taxRatesDictionary) @@ -777,14 +769,16 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR { Discount orderAppliedDiscount = null; orderTotal = _orderTotalCalculationService.GetShoppingCartTotal(cart, - out orderDiscountAmount, out orderAppliedDiscount, out appliedGiftCards, - out redeemedRewardPoints, out redeemedRewardPointsAmount); + out orderDiscountAmount, out orderAppliedDiscount, out appliedGiftCards, out redeemedRewardPoints, out redeemedRewardPointsAmount); + if (!orderTotal.HasValue) - throw new SmartException("Order total couldn't be calculated"); + throw new SmartException(T("Order.CannotCalculateOrderTotal")); - //discount history - if (orderAppliedDiscount != null && !appliedDiscounts.Any(x => x.Id == orderAppliedDiscount.Id)) - appliedDiscounts.Add(orderAppliedDiscount); + //discount history + if (orderAppliedDiscount != null && !appliedDiscounts.Any(x => x.Id == orderAppliedDiscount.Id)) + { + appliedDiscounts.Add(orderAppliedDiscount); + } } else { @@ -803,7 +797,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR if (!preProcessPaymentResult.Success) { result.Errors.AddRange(preProcessPaymentResult.Errors); - result.Errors.Add(_localizationService.GetResource("Common.Error.PreProcessPayment")); + result.Errors.Add(T("Common.Error.PreProcessPayment")); return result; } @@ -811,23 +805,23 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR if (!processPaymentRequest.IsRecurringPayment) { if (customer.BillingAddress == null) - throw new SmartException("Billing address is not provided"); + throw new SmartException(T("Order.BillingAddressMissing")); if (!customer.BillingAddress.Email.IsEmail()) - throw new SmartException("Email is not valid"); + throw new SmartException(T("Common.Error.InvalidEmail")); billingAddress = (Address)customer.BillingAddress.Clone(); } else { if (initialOrder.BillingAddress == null) - throw new SmartException("Billing address is not available"); + throw new SmartException(T("Order.BillingAddressMissing")); billingAddress = (Address)initialOrder.BillingAddress.Clone(); } if (billingAddress.Country != null && !billingAddress.Country.AllowsBilling) - throw new SmartException(string.Format("Country '{0}' is not allowed for billing", billingAddress.Country.Name)); + throw new SmartException(T("Order.CountryNotAllowedForBilling", billingAddress.Country.Name)); Address shippingAddress = null; if (shoppingCartRequiresShipping) @@ -835,64 +829,68 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR if (!processPaymentRequest.IsRecurringPayment) { if (customer.ShippingAddress == null) - throw new SmartException("Shipping address is not provided"); + throw new SmartException(T("Order.ShippingAddressMissing")); if (!customer.ShippingAddress.Email.IsEmail()) - throw new SmartException("Email is not valid"); + throw new SmartException(T("Common.Error.InvalidEmail")); shippingAddress = (Address)customer.ShippingAddress.Clone(); } else { if (initialOrder.ShippingAddress == null) - throw new SmartException("Shipping address is not available"); + throw new SmartException(T("Order.ShippingAddressMissing")); shippingAddress = (Address)initialOrder.ShippingAddress.Clone(); } if (shippingAddress.Country != null && !shippingAddress.Country.AllowsShipping) - throw new SmartException(string.Format("Country '{0}' is not allowed for shipping", shippingAddress.Country.Name)); + throw new SmartException(T("Order.CountryNotAllowedForShipping", shippingAddress.Country.Name)); } #endregion #region Payment workflow - //skip payment workflow if order total equals zero - bool skipPaymentWorkflow = false; - if (orderTotal.Value == decimal.Zero) - skipPaymentWorkflow = true; + // skip payment workflow if order total equals zero + var skipPaymentWorkflow = false; + if (orderTotal.Value == decimal.Zero) + { + skipPaymentWorkflow = true; + } - //payment workflow + // payment workflow Provider paymentMethod = null; if (!skipPaymentWorkflow) { paymentMethod = _paymentService.LoadPaymentMethodBySystemName(processPaymentRequest.PaymentMethodSystemName); if (paymentMethod == null) - throw new SmartException("Payment method couldn't be loaded"); + throw new SmartException(T("Payment.CouldNotLoadMethod")); - //ensure that payment method is active + // ensure that payment method is active if (!paymentMethod.IsPaymentMethodActive(_paymentSettings)) - throw new SmartException("Payment method is not active"); + throw new SmartException(T("Payment.MethodNotAvailable")); } else { processPaymentRequest.PaymentMethodSystemName = ""; } - //recurring or standard shopping cart? - bool isRecurringShoppingCart = false; + // recurring or standard shopping cart? + var isRecurringShoppingCart = false; if (!processPaymentRequest.IsRecurringPayment) { isRecurringShoppingCart = cart.IsRecurring(); if (isRecurringShoppingCart) { - int recurringCycleLength = 0; + var recurringCycleLength = 0; + var recurringTotalCycles = 0; RecurringProductCyclePeriod recurringCyclePeriod; - int recurringTotalCycles = 0; string recurringCyclesError = cart.GetRecurringCycleInfo(_localizationService, out recurringCycleLength, out recurringCyclePeriod, out recurringTotalCycles); + if (!string.IsNullOrEmpty(recurringCyclesError)) throw new SmartException(recurringCyclesError); + processPaymentRequest.RecurringCycleLength = recurringCycleLength; processPaymentRequest.RecurringCyclePeriod = recurringCyclePeriod; processPaymentRequest.RecurringTotalCycles = recurringTotalCycles; @@ -903,31 +901,31 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR isRecurringShoppingCart = true; } - //process payment + // process payment ProcessPaymentResult processPaymentResult = null; - if (!skipPaymentWorkflow) + if (!skipPaymentWorkflow && !processPaymentRequest.IsMultiOrder) { if (!processPaymentRequest.IsRecurringPayment) { if (isRecurringShoppingCart) { - //recurring cart + // recurring cart var recurringPaymentType = _paymentService.GetRecurringPaymentType(processPaymentRequest.PaymentMethodSystemName); switch (recurringPaymentType) { case RecurringPaymentType.NotSupported: - throw new SmartException("Recurring payments are not supported by selected payment method"); + throw new SmartException(T("Payment.RecurringPaymentNotSupported")); case RecurringPaymentType.Manual: case RecurringPaymentType.Automatic: processPaymentResult = _paymentService.ProcessRecurringPayment(processPaymentRequest); break; default: - throw new SmartException("Not supported recurring payment type"); + throw new SmartException(T("Payment.RecurringPaymentTypeUnknown")); } } else { - //standard cart + // standard cart processPaymentResult = _paymentService.ProcessPayment(processPaymentRequest); } } @@ -935,12 +933,13 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR { 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 { processPaymentRequest.CreditCardExpireMonth = initialOrder.AllowStoringCreditCardNumber ? Convert.ToInt32(_encryptionService.DecryptText(initialOrder.CardExpirationMonth)) : 0; @@ -952,7 +951,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR switch (recurringPaymentType) { case RecurringPaymentType.NotSupported: - throw new SmartException("Recurring payments are not supported by selected payment method"); + throw new SmartException(T("Payment.RecurringPaymentNotSupported")); case RecurringPaymentType.Manual: processPaymentResult = _paymentService.ProcessRecurringPayment(processPaymentRequest); break; @@ -961,26 +960,25 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR processPaymentResult = new ProcessPaymentResult(); break; default: - throw new SmartException("Not supported recurring payment type"); - } + throw new SmartException(T("Payment.RecurringPaymentTypeUnknown")); + } } else { - throw new SmartException("No recurring products"); + throw new SmartException(T("Order.NoRecurringProducts")); } } } else { - //payment is not required - if (processPaymentResult == null) - processPaymentResult = new ProcessPaymentResult(); + // payment is not required + if (processPaymentResult == null) + { + processPaymentResult = new ProcessPaymentResult(); + } processPaymentResult.NewPaymentStatus = PaymentStatus.Paid; } - if (processPaymentResult == null) - throw new SmartException("processPaymentResult is not available"); - #endregion if (processPaymentResult.Success) @@ -992,10 +990,12 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR #region Save order details var shippingStatus = ShippingStatus.NotYetShipped; - if (!shoppingCartRequiresShipping) - shippingStatus = ShippingStatus.ShippingNotRequired; + if (!shoppingCartRequiresShipping) + { + shippingStatus = ShippingStatus.ShippingNotRequired; + } - var order = new Order() + var order = new Order { StoreId = processPaymentRequest.StoreId, OrderGuid = processPaymentRequest.OrderGuid, @@ -1060,6 +1060,12 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR UpdatedOnUtc = utcNow, CustomerOrderComment = extraData.ContainsKey("CustomerComment") ? extraData["CustomerComment"] : "" }; + + if (extraData.ContainsKey("AcceptThirdPartyEmailHandOver") && _shoppingCartSettings.ThirdPartyEmailHandOver != CheckoutThirdPartyEmailHandOver.None) + { + order.AcceptThirdPartyEmailHandOver = extraData["AcceptThirdPartyEmailHandOver"].ToBool(); + } + _orderService.InsertOrder(order); result.PlacedOrder = order; @@ -1086,12 +1092,14 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR decimal discountAmount = _priceCalculationService.GetDiscountAmount(sc, out scDiscount); decimal discountAmountInclTax = _taxService.GetProductPrice(sc.Item.Product, discountAmount, true, customer, out taxRate); decimal discountAmountExclTax = _taxService.GetProductPrice(sc.Item.Product, discountAmount, false, customer, out taxRate); - + if (scDiscount != null && !appliedDiscounts.Any(x => x.Id == scDiscount.Id)) - appliedDiscounts.Add(scDiscount); + { + appliedDiscounts.Add(scDiscount); + } //attributes - string attributeDescription = _productAttributeFormatter.FormatAttributes(sc.Item.Product, sc.Item.AttributesXml, customer); + var attributeDescription = _productAttributeFormatter.FormatAttributes(sc.Item.Product, sc.Item.AttributesXml, customer); var itemWeight = _shippingService.GetShoppingCartItemWeight(sc); @@ -1124,10 +1132,10 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR foreach (var childItem in sc.ChildItems) { - decimal bundleItemSubTotal = _taxService.GetProductPrice(childItem.Item.Product, _priceCalculationService.GetSubTotal(childItem, true), out taxRate); + var bundleItemSubTotal = _taxService.GetProductPrice(childItem.Item.Product, _priceCalculationService.GetSubTotal(childItem, true), out taxRate); - string attributesInfo = _productAttributeFormatter.FormatAttributes(childItem.Item.Product, childItem.Item.AttributesXml, order.Customer, - renderPrices: false, allowHyperlinks: false); + var attributesInfo = _productAttributeFormatter.FormatAttributes(childItem.Item.Product, childItem.Item.AttributesXml, order.Customer, + renderPrices: false, allowHyperlinks: true); childItem.BundleItemData.ToOrderData(listBundleData, bundleItemSubTotal, childItem.Item.AttributesXml, attributesInfo); } @@ -1141,11 +1149,10 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR //gift cards if (sc.Item.Product.IsGiftCard) { - string giftCardRecipientName, giftCardRecipientEmail, - giftCardSenderName, giftCardSenderEmail, giftCardMessage; + string giftCardRecipientName, giftCardRecipientEmail, giftCardSenderName, giftCardSenderEmail, giftCardMessage; + _productAttributeParser.GetGiftCardAttribute(sc.Item.AttributesXml, - out giftCardRecipientName, out giftCardRecipientEmail, - out giftCardSenderName, out giftCardSenderEmail, out giftCardMessage); + out giftCardRecipientName, out giftCardRecipientEmail, out giftCardSenderName, out giftCardSenderEmail, out giftCardMessage); for (int i = 0; i < sc.Item.Quantity; i++) { @@ -1168,12 +1175,12 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR } } - //inventory _productService.AdjustInventory(sc, true); } //clear shopping cart - cart.ToList().ForEach(sci => _shoppingCartService.DeleteShoppingCartItem(sci.Item, false)); + if (!processPaymentRequest.IsMultiOrder) + cart.ToList().ForEach(sci => _shoppingCartService.DeleteShoppingCartItem(sci.Item, false)); } else { @@ -1182,7 +1189,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR foreach (var orderItem in initialOrderItems) { //save item - var newOrderItem = new OrderItem() + var newOrderItem = new OrderItem { OrderItemGuid = Guid.NewGuid(), Order = order, @@ -1213,12 +1220,11 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR string giftCardRecipientName, giftCardRecipientEmail, giftCardSenderName, giftCardSenderEmail, giftCardMessage; _productAttributeParser.GetGiftCardAttribute(orderItem.AttributesXml, - out giftCardRecipientName, out giftCardRecipientEmail, - out giftCardSenderName, out giftCardSenderEmail, out giftCardMessage); + out giftCardRecipientName, out giftCardRecipientEmail, out giftCardSenderName, out giftCardSenderEmail, out giftCardMessage); for (int i = 0; i < orderItem.Quantity; i++) { - var gc = new GiftCard() + var gc = new GiftCard { GiftCardType = orderItem.Product.GiftCardType, PurchasedWithOrderItem = newOrderItem, @@ -1237,7 +1243,6 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR } } - //inventory _productService.AdjustInventory(orderItem, true, orderItem.Quantity); } } @@ -1247,7 +1252,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR { foreach (var discount in appliedDiscounts) { - var duh = new DiscountUsageHistory() + var duh = new DiscountUsageHistory { Discount = discount, Order = order, @@ -1262,8 +1267,8 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR { foreach (var agc in appliedGiftCards) { - decimal amountUsed = agc.AmountCanBeUsed; - var gcuh = new GiftCardUsageHistory() + var amountUsed = agc.AmountCanBeUsed; + var gcuh = new GiftCardUsageHistory { GiftCard = agc.GiftCard, UsedWithOrder = order, @@ -1279,9 +1284,10 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR if (redeemedRewardPointsAmount > decimal.Zero) { customer.AddRewardPointsHistoryEntry(-redeemedRewardPoints, - string.Format(_localizationService.GetResource("RewardPoints.Message.RedeemedForOrder", order.CustomerLanguageId), order.GetOrderNumber()), + _localizationService.GetResource("RewardPoints.Message.RedeemedForOrder", order.CustomerLanguageId).FormatInvariant(order.GetOrderNumber()), order, - redeemedRewardPointsAmount); + redeemedRewardPointsAmount); + _customerService.UpdateCustomer(customer); } @@ -1289,7 +1295,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR if (!processPaymentRequest.IsRecurringPayment && isRecurringShoppingCart) { //create recurring payment (the first payment) - var rp = new RecurringPayment() + var rp = new RecurringPayment { CycleLength = processPaymentRequest.RecurringCycleLength, CyclePeriod = processPaymentRequest.RecurringCyclePeriod, @@ -1301,7 +1307,6 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR }; _orderService.InsertRecurringPayment(rp); - var recurringPaymentType = _paymentService.GetRecurringPaymentType(processPaymentRequest.PaymentMethodSystemName); switch (recurringPaymentType) { @@ -1333,50 +1338,34 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR } } - #endregion + #endregion - #region Notifications, notes and attributes - - //notes, messages - order.OrderNotes.Add(new OrderNote - { - Note = TNote("OrderPlaced"), - DisplayToCustomer = false, - CreatedOnUtc = utcNow - }); - _orderService.UpdateOrder(order); + #region Notifications, notes and attributes + + // notes, messages + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderPlaced")); //send email notifications int orderPlacedStoreOwnerNotificationQueuedEmailId = _workflowMessageService.SendOrderPlacedStoreOwnerNotification(order, _localizationSettings.DefaultAdminLanguageId); if (orderPlacedStoreOwnerNotificationQueuedEmailId > 0) { - order.OrderNotes.Add(new OrderNote - { - Note = string.Format(TNote("MerchantEmailQueued"), orderPlacedStoreOwnerNotificationQueuedEmailId), - DisplayToCustomer = false, - CreatedOnUtc = utcNow - }); - _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.MerchantEmailQueued", orderPlacedStoreOwnerNotificationQueuedEmailId)); } int orderPlacedCustomerNotificationQueuedEmailId = _workflowMessageService.SendOrderPlacedCustomerNotification(order, order.CustomerLanguageId); if (orderPlacedCustomerNotificationQueuedEmailId > 0) { - order.OrderNotes.Add(new OrderNote - { - Note = string.Format(TNote("CustomerEmailQueued"), orderPlacedCustomerNotificationQueuedEmailId), - DisplayToCustomer = false, - CreatedOnUtc = utcNow - }); - _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerEmailQueued", orderPlacedCustomerNotificationQueuedEmailId)); } - //check order status + // check order status CheckOrderStatus(order); - //reset checkout data - if (!processPaymentRequest.IsRecurringPayment) + //reset checkout data + if (!processPaymentRequest.IsRecurringPayment && !processPaymentRequest.IsMultiOrder) + { _customerService.ResetCheckoutData(customer, processPaymentRequest.StoreId, clearCouponCodes: true, clearCheckoutAttributes: true); + } // check for generic attributes to be inserted automatically foreach (var customProperty in processPaymentRequest.CustomProperties.Where(x => x.Key.HasValue() && x.Value.AutoCreateGenericAttribute)) @@ -1387,15 +1376,12 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR //uncomment this line to support transactions //scope.Complete(); - //raise event + // raise event _eventPublisher.PublishOrderPlaced(order); if (!processPaymentRequest.IsRecurringPayment) { - _customerActivityService.InsertActivity( - "PublicStore.PlaceOrder", - _localizationService.GetResource("ActivityLog.PublicStore.PlaceOrder"), - order.GetOrderNumber()); + _customerActivityService.InsertActivity("PublicStore.PlaceOrder", T("ActivityLog.PublicStore.PlaceOrder", order.GetOrderNumber())); } //raise event @@ -1403,13 +1389,37 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR { _eventPublisher.PublishOrderPaid(order); } - #endregion - } - } + + #endregion + + #region Newsletter subscription + + if (extraData.ContainsKey("SubscribeToNewsLetter") && _shoppingCartSettings.NewsLetterSubscription != CheckoutNewsLetterSubscription.None) + { + var addSubscription = extraData["SubscribeToNewsLetter"].ToBool(); + + bool? nsResult = _newsLetterSubscriptionService.AddNewsLetterSubscriptionFor(addSubscription, customer.Email, order.StoreId); + + if (nsResult.HasValue) + { + if (nsResult.Value) + _orderService.AddOrderNote(order, T("Admin.OrderNotice.NewsLetterSubscriptionAdded")); + else + _orderService.AddOrderNote(order, T("Admin.OrderNotice.NewsLetterSubscriptionRemoved")); + } + } + + #endregion + } + } else { - foreach (var paymentError in processPaymentResult.Errors) - result.AddError(paymentError); + result.AddError(T("Payment.PayingFailed")); + + foreach (var paymentError in processPaymentResult.Errors) + { + result.AddError(paymentError); + } } } catch (Exception exc) @@ -1452,14 +1462,8 @@ public virtual void DeleteOrder(Order order) _productService.AdjustInventory(orderItem, false, orderItem.Quantity); } - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = TNote("OrderDeleted"), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + //add a note + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderDeleted")); //now delete an order _orderService.DeleteOrder(order); @@ -1474,25 +1478,25 @@ public virtual void ProcessNextRecurringPayment(RecurringPayment recurringPaymen { if (recurringPayment == null) throw new ArgumentNullException("recurringPayment"); + try { if (!recurringPayment.IsActive) - throw new SmartException("Recurring payment is not active"); + throw new SmartException(T("Payment.RecurringPaymentNotActive")); var initialOrder = recurringPayment.InitialOrder; if (initialOrder == null) - throw new SmartException("Initial order could not be loaded"); + throw new SmartException(T("Order.InitialOrderDoesNotExistForRecurringPayment")); var customer = initialOrder.Customer; if (customer == null) - throw new SmartException("Customer could not be loaded"); + throw new SmartException(T("Customer.DoesNotExist")); var nextPaymentDate = recurringPayment.NextPaymentDate; if (!nextPaymentDate.HasValue) - throw new SmartException("Next payment date could not be calculated"); + throw new SmartException(T("Payment.CannotCalculateNextPaymentDate")); - //payment info - var paymentInfo = new ProcessPaymentRequest() + var paymentInfo = new ProcessPaymentRequest { StoreId = initialOrder.StoreId, CustomerId = customer.Id, @@ -1506,35 +1510,30 @@ public virtual void ProcessNextRecurringPayment(RecurringPayment recurringPaymen //place a new order var result = this.PlaceOrder(paymentInfo, new Dictionary()); + if (result.Success) { if (result.PlacedOrder == null) - throw new SmartException("Placed order could not be loaded"); + throw new SmartException(T("Order.NotFound", "".NaIfEmpty())); - var rph = new RecurringPaymentHistory() + var rph = new RecurringPaymentHistory { RecurringPayment = recurringPayment, CreatedOnUtc = DateTime.UtcNow, - OrderId = result.PlacedOrder.Id, + OrderId = result.PlacedOrder.Id }; + recurringPayment.RecurringPaymentHistory.Add(rph); _orderService.UpdateRecurringPayment(recurringPayment); } - else - { - string error = ""; - for (int i = 0; i < result.Errors.Count; i++) - { - error += string.Format("Error {0}: {1}", i, result.Errors[i]); - if (i != result.Errors.Count - 1) - error += ". "; - } - throw new SmartException(error); + else if (result.Errors.Count > 0) + { + throw new SmartException(string.Join(" ", result.Errors)); } } - catch (Exception exc) + catch (Exception exception) { - _logger.Error(string.Format("Error while processing recurring order. {0}", exc.Message), exc); + _logger.ErrorsAll(exception); throw; } } @@ -1550,69 +1549,42 @@ public virtual IList CancelRecurringPayment(RecurringPayment recurringPa var initialOrder = recurringPayment.InitialOrder; if (initialOrder == null) - return new List() { "Initial order could not be loaded" }; - + return new List { T("Order.InitialOrderDoesNotExistForRecurringPayment") }; var request = new CancelRecurringPaymentRequest(); CancelRecurringPaymentResult result = null; + try { request.Order = initialOrder; - result = _paymentService.CancelRecurringPayment(request); + + result = _paymentService.CancelRecurringPayment(request); + if (result.Success) { //update recurring payment recurringPayment.IsActive = false; _orderService.UpdateRecurringPayment(recurringPayment); - - //add a note - initialOrder.OrderNotes.Add(new OrderNote() - { - Note = TNote("RecurringPaymentCancelled"), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(initialOrder); + _orderService.AddOrderNote(initialOrder, T("Admin.OrderNotice.RecurringPaymentCancelled")); //notify a store owner - _workflowMessageService - .SendRecurringPaymentCancelledStoreOwnerNotification(recurringPayment, - _localizationSettings.DefaultAdminLanguageId); + _workflowMessageService.SendRecurringPaymentCancelledStoreOwnerNotification(recurringPayment, _localizationSettings.DefaultAdminLanguageId); } } - catch (Exception exc) + catch (Exception exception) { - if (result == null) - result = new CancelRecurringPaymentResult(); - result.AddError(string.Format("Error: {0}. Full exception: {1}", exc.Message, exc.ToString())); + if (result == null) + { + result = new CancelRecurringPaymentResult(); + } + + result.AddError(exception.ToAllMessages()); } + ProcessErrors(initialOrder, result.Errors, "Admin.OrderNotice.RecurringPaymentCancellationError"); - //process errors - string error = ""; - for (int i = 0; i < result.Errors.Count; i++) - { - error += string.Format("Error {0}: {1}", i, result.Errors[i]); - if (i != result.Errors.Count - 1) - error += ". "; - } - if (!String.IsNullOrEmpty(error)) - { - //add a note - initialOrder.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("RecurringPaymentCancellationError"), error), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(initialOrder); - - //log it - string logError = string.Format("Error cancelling recurring payment. Order #{0}. Error: {1}", initialOrder.Id, error); - _logger.InsertLog(LogLevel.Error, logError, logError); - } - return result.Errors; + return result.Errors; } /// @@ -1666,10 +1638,10 @@ public virtual void Ship(Shipment shipment, bool notifyCustomer) var order = _orderService.GetOrderById(shipment.OrderId); if (order == null) - throw new Exception("Order cannot be loaded"); + throw new SmartException(T("Order.NotFound", shipment.OrderId)); if (shipment.ShippedDateUtc.HasValue) - throw new Exception("This shipment is already shipped"); + throw new SmartException(T("Shipment.AlreadyShipped")); shipment.ShippedDateUtc = DateTime.UtcNow; _shipmentService.UpdateShipment(shipment); @@ -1679,30 +1651,18 @@ public virtual void Ship(Shipment shipment, bool notifyCustomer) order.ShippingStatusId = (int)ShippingStatus.PartiallyShipped; else order.ShippingStatusId = (int)ShippingStatus.Shipped; - _orderService.UpdateOrder(order); - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("ShipmentSent"), shipment.Id), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.ShipmentSent", shipment.Id)); + if (notifyCustomer) { //notify customer int queuedEmailId = _workflowMessageService.SendShipmentSentCustomerNotification(shipment, order.CustomerLanguageId); if (queuedEmailId > 0) { - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("CustomerShippedEmailQueued"), queuedEmailId), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerShippedEmailQueued", queuedEmailId)); } } @@ -1722,40 +1682,30 @@ public virtual void Deliver(Shipment shipment, bool notifyCustomer) var order = shipment.Order; if (order == null) - throw new Exception("Order cannot be loaded"); + throw new SmartException(T("Order.NotFound", shipment.OrderId)); - if (shipment.DeliveryDateUtc.HasValue) - throw new Exception("This shipment is already delivered"); + if (shipment.DeliveryDateUtc.HasValue) + throw new SmartException(T("Shipment.AlreadyDelivered")); shipment.DeliveryDateUtc = DateTime.UtcNow; _shipmentService.UpdateShipment(shipment); - if (!order.HasItemsToAddToShipment() && !order.HasItemsToShip() && !order.HasItemsToDeliver()) - order.ShippingStatusId = (int)ShippingStatus.Delivered; - _orderService.UpdateOrder(order); + if (!order.HasItemsToAddToShipment() && !order.HasItemsToShip() && !order.HasItemsToDeliver()) + { + order.ShippingStatusId = (int)ShippingStatus.Delivered; + } - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("ShipmentDelivered"), shipment.Id), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.ShipmentDelivered", shipment.Id)); + if (notifyCustomer) { //send email notification int queuedEmailId = _workflowMessageService.SendShipmentDeliveredCustomerNotification(shipment, order.CustomerLanguageId); if (queuedEmailId > 0) { - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("CustomerDeliveredEmailQueued"), queuedEmailId), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerDeliveredEmailQueued", queuedEmailId)); } } @@ -1792,19 +1742,12 @@ public virtual void CancelOrder(Order order, bool notifyCustomer) throw new ArgumentNullException("order"); if (!CanCancelOrder(order)) - throw new SmartException("Cannot do cancel for order."); + throw new SmartException(T("Order.CannotCancel")); //Cancel order SetOrderStatus(order, OrderStatus.Cancelled, notifyCustomer); - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = TNote("OrderCancelled"), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderCancelled")); //cancel recurring payments var recurringPayments = _orderService.SearchRecurringPayments(0, 0, order.Id, null); @@ -1915,14 +1858,7 @@ public virtual void MarkAsAuthorized(Order order) order.PaymentStatusId = (int)PaymentStatus.Authorized; _orderService.UpdateOrder(order); - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = TNote("OrderMarkedAsAuthorized"), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsAuthorized")); //check order status CheckOrderStatus(order); @@ -1949,7 +1885,7 @@ public virtual bool CanCompleteOrder(Order order) public virtual void CompleteOrder(Order order) { if (!CanCompleteOrder(order)) - throw new SmartException("You can't mark this order as completed"); + throw new SmartException(T("Order.CannotMarkCompleted")); if (CanMarkOrderAsPaid(order)) { @@ -1977,12 +1913,10 @@ public virtual bool CanCapture(Order order) if (order == null) throw new ArgumentNullException("order"); - if (order.OrderStatus == OrderStatus.Cancelled || - order.OrderStatus == OrderStatus.Pending) + if (order.OrderStatus == OrderStatus.Cancelled || order.OrderStatus == OrderStatus.Pending) return false; - if (order.PaymentStatus == PaymentStatus.Authorized && - _paymentService.SupportCapture(order.PaymentMethodSystemName)) + if (order.PaymentStatus == PaymentStatus.Authorized && _paymentService.SupportCapture(order.PaymentMethodSystemName)) return true; return false; @@ -1999,7 +1933,7 @@ public virtual IList Capture(Order order) throw new ArgumentNullException("order"); if (!CanCapture(order)) - throw new SmartException("Cannot do capture for order."); + throw new SmartException(T("Order.CannotCapture")); var request = new CapturePaymentRequest(); CapturePaymentResult result = null; @@ -2012,24 +1946,20 @@ public virtual IList Capture(Order order) if (result.Success) { var paidDate = order.PaidDateUtc; - if (result.NewPaymentStatus == PaymentStatus.Paid) - paidDate = DateTime.UtcNow; + if (result.NewPaymentStatus == PaymentStatus.Paid) + { + paidDate = DateTime.UtcNow; + } order.CaptureTransactionId = result.CaptureTransactionId; order.CaptureTransactionResult = result.CaptureTransactionResult; order.PaymentStatus = result.NewPaymentStatus; order.PaidDateUtc = paidDate; - _orderService.UpdateOrder(order); - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = TNote("OrderCaptured"), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderCaptured")); + CheckOrderStatus(order); //raise event @@ -2039,38 +1969,19 @@ public virtual IList Capture(Order order) } } } - catch (Exception exc) + catch (Exception exception) { - if (result == null) - result = new CapturePaymentResult(); - result.AddError(string.Format("Error: {0}. Full exception: {1}", exc.Message, exc.ToString())); - } + if (result == null) + { + result = new CapturePaymentResult(); + } + result.AddError(exception.ToAllMessages()); + } - //process errors - string error = ""; - for (int i = 0; i < result.Errors.Count; i++) - { - error += string.Format("Error {0}: {1}", i, result.Errors[i]); - if (i != result.Errors.Count - 1) - error += ". "; - } - if (!String.IsNullOrEmpty(error)) - { - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format("Unable to capture order. {0}", error), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + ProcessErrors(order, result.Errors, "Admin.OrderNotice.OrderCaptureError"); - //log it - string logError = string.Format(TNote("OrderCaptureError"), order.GetOrderNumber(), error); - _logger.InsertLog(LogLevel.Error, logError, logError); - } - return result.Errors; + return result.Errors; } /// @@ -2104,20 +2015,14 @@ public virtual void MarkOrderAsPaid(Order order) throw new ArgumentNullException("order"); if (!CanMarkOrderAsPaid(order)) - throw new SmartException("You can't mark this order as paid"); + throw new SmartException(T("Order.CannotMarkPaid")); order.PaymentStatusId = (int)PaymentStatus.Paid; order.PaidDateUtc = DateTime.UtcNow; - _orderService.UpdateOrder(order); - //add a note - order.OrderNotes.Add(new OrderNote - { - Note = TNote("OrderMarkedAsPaid"), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + _orderService.UpdateOrder(order); + + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsPaid")); CheckOrderStatus(order); @@ -2165,7 +2070,7 @@ public virtual IList Refund(Order order) throw new ArgumentNullException("order"); if (!CanRefund(order)) - throw new SmartException("Cannot do refund for order."); + throw new SmartException(T("Order.CannotRefund")); var request = new RefundPaymentRequest(); RefundPaymentResult result = null; @@ -2174,7 +2079,9 @@ public virtual IList Refund(Order order) request.Order = order; request.AmountToRefund = order.OrderTotal; request.IsPartialRefund = false; - result = _paymentService.Refund(request); + + result = _paymentService.Refund(request); + if (result.Success) { //total amount refunded @@ -2183,53 +2090,29 @@ public virtual IList Refund(Order order) //update order info order.RefundedAmount = totalAmountRefunded; order.PaymentStatus = result.NewPaymentStatus; - _orderService.UpdateOrder(order); - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("OrderRefunded"), _priceFormatter.FormatPrice(request.AmountToRefund, true, false)), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderRefunded", _priceFormatter.FormatPrice(request.AmountToRefund, true, false))); + //check order status CheckOrderStatus(order); } } - catch (Exception exc) + catch (Exception exception) { - if (result == null) - result = new RefundPaymentResult(); - result.AddError(string.Format("Error: {0}. Full exception: {1}", exc.Message, exc.ToString())); - } + if (result == null) + { + result = new RefundPaymentResult(); + } - //process errors - string error = ""; - for (int i = 0; i < result.Errors.Count; i++) - { - error += string.Format("Error {0}: {1}", i, result.Errors[i]); - if (i != result.Errors.Count - 1) - error += ". "; + result.AddError(exception.ToAllMessages()); } - if (!String.IsNullOrEmpty(error)) - { - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("OrderRefundError"), error), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); - //log it - string logError = string.Format("Error refunding order '{0}'. Error: {1}", order.GetOrderNumber(), error); - _logger.InsertLog(LogLevel.Error, logError, logError); - } - return result.Errors; + ProcessErrors(order, result.Errors, "Admin.OrderNotice.OrderRefundError"); + + return result.Errors; } /// @@ -2265,7 +2148,7 @@ public virtual void RefundOffline(Order order) throw new ArgumentNullException("order"); if (!CanRefundOffline(order)) - throw new SmartException("You can't refund this order"); + throw new SmartException(T("Order.CannotRefund")); //amout to refund decimal amountToRefund = order.OrderTotal; @@ -2276,17 +2159,11 @@ public virtual void RefundOffline(Order order) //update order info order.RefundedAmount = totalAmountRefunded; order.PaymentStatus = PaymentStatus.Refunded; - _orderService.UpdateOrder(order); - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("OrderMarkedAsRefunded"), _priceFormatter.FormatPrice(amountToRefund, true, false)), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsRefunded", _priceFormatter.FormatPrice(amountToRefund, true, false))); + //check order status CheckOrderStatus(order); } @@ -2336,10 +2213,11 @@ public virtual IList PartiallyRefund(Order order, decimal amountToRefund throw new ArgumentNullException("order"); if (!CanPartiallyRefund(order, amountToRefund)) - throw new SmartException("Cannot do partial refund for order."); + throw new SmartException(T("Order.CannotPartialRefund")); var request = new RefundPaymentRequest(); RefundPaymentResult result = null; + try { request.Order = order; @@ -2356,53 +2234,28 @@ public virtual IList PartiallyRefund(Order order, decimal amountToRefund //update order info order.RefundedAmount = totalAmountRefunded; order.PaymentStatus = result.NewPaymentStatus; - _orderService.UpdateOrder(order); - - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("OrderPartiallyRefunded"), _priceFormatter.FormatPrice(amountToRefund, true, false)), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderPartiallyRefunded", _priceFormatter.FormatPrice(amountToRefund, true, false))); + //check order status CheckOrderStatus(order); } } - catch (Exception exc) + catch (Exception exception) { - if (result == null) - result = new RefundPaymentResult(); - result.AddError(string.Format("Error: {0}. Full exception: {1}", exc.Message, exc.ToString())); - } + if (result == null) + { + result = new RefundPaymentResult(); + } - //process errors - string error = ""; - for (int i = 0; i < result.Errors.Count; i++) - { - error += string.Format("Error {0}: {1}", i, result.Errors[i]); - if (i != result.Errors.Count - 1) - error += ". "; - } - if (!String.IsNullOrEmpty(error)) - { - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("OrderPartiallyRefundError"), error), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + result.AddError(exception.ToAllMessages()); + } - //log it - string logError = string.Format("Error refunding order '{0}'. Error: {1}", order.GetOrderNumber(), error); - _logger.InsertLog(LogLevel.Error, logError, logError); - } - return result.Errors; + ProcessErrors(order, result.Errors, "Admin.OrderNotice.OrderPartiallyRefundError"); + + return result.Errors; } /// @@ -2448,7 +2301,7 @@ public virtual void PartiallyRefundOffline(Order order, decimal amountToRefund) throw new ArgumentNullException("order"); if (!CanPartiallyRefundOffline(order, amountToRefund)) - throw new SmartException("You can't partially refund (offline) this order"); + throw new SmartException(T("Order.CannotPartialRefund")); //total amount refunded decimal totalAmountRefunded = order.RefundedAmount + amountToRefund; @@ -2457,17 +2310,11 @@ public virtual void PartiallyRefundOffline(Order order, decimal amountToRefund) order.RefundedAmount = totalAmountRefunded; //if (order.OrderTotal == totalAmountRefunded), then set order.PaymentStatus = PaymentStatus.Refunded; order.PaymentStatus = PaymentStatus.PartiallyRefunded; - _orderService.UpdateOrder(order); - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("OrderMarkedAsPartiallyRefunded"), _priceFormatter.FormatPrice(amountToRefund, true, false)), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsPartiallyRefunded", _priceFormatter.FormatPrice(amountToRefund, true, false))); + //check order status CheckOrderStatus(order); } @@ -2509,10 +2356,11 @@ public virtual IList Void(Order order) throw new ArgumentNullException("order"); if (!CanVoid(order)) - throw new SmartException("Cannot do void for order."); + throw new SmartException(T("Order.CannotVoid")); var request = new VoidPaymentRequest(); VoidPaymentResult result = null; + try { request.Order = order; @@ -2522,51 +2370,27 @@ public virtual IList Void(Order order) { //update order info order.PaymentStatus = result.NewPaymentStatus; - _orderService.UpdateOrder(order); - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = TNote("OrderVoided"), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderVoided")); + //check order status CheckOrderStatus(order); } } - catch (Exception exc) + catch (Exception exception) { - if (result == null) - result = new VoidPaymentResult(); - result.AddError(string.Format("Error: {0}. Full exception: {1}", exc.Message, exc.ToString())); - } + if (result == null) + { + result = new VoidPaymentResult(); + } - //process errors - string error = ""; - for (int i = 0; i < result.Errors.Count; i++) - { - error += string.Format("Error {0}: {1}", i, result.Errors[i]); - if (i != result.Errors.Count - 1) - error += ". "; - } - if (!String.IsNullOrEmpty(error)) - { - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(TNote("OrderVoidError"), error), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + result.AddError(exception.ToAllMessages()); + } + + ProcessErrors(order, result.Errors, "Admin.OrderNotice.OrderVoidError"); - //log it - string logError = string.Format("Error voiding order '{0}'. Error: {1}", order.GetOrderNumber(), error); - _logger.InsertLog(LogLevel.Error, logError, logError); - } return result.Errors; } @@ -2603,20 +2427,14 @@ public virtual void VoidOffline(Order order) throw new ArgumentNullException("order"); if (!CanVoidOffline(order)) - throw new SmartException("You can't void this order"); + throw new SmartException(T("Order.CannotVoid")); - order.PaymentStatusId = (int)PaymentStatus.Voided; - _orderService.UpdateOrder(order); + order.PaymentStatusId = (int)PaymentStatus.Voided; - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = TNote("OrderMarkedAsVoided"), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsVoided")); + //check orer status CheckOrderStatus(order); } @@ -2634,7 +2452,7 @@ public virtual void ReOrder(Order order) foreach (var orderItem in order.OrderItems) { - bool isBundle = (orderItem.Product.ProductType == ProductType.BundledProduct); + var isBundle = (orderItem.Product.ProductType == ProductType.BundledProduct); var addToCartContext = new AddToCartContext(); @@ -2673,7 +2491,8 @@ public virtual bool IsReturnRequestAllowed(Order order) if (order.OrderStatus != OrderStatus.Complete) return false; - bool numberOfDaysReturnRequestAvailableValid = false; + var numberOfDaysReturnRequestAvailableValid = false; + if (_orderSettings.NumberOfDaysReturnRequestAvailable == 0) { numberOfDaysReturnRequestAvailableValid = true; @@ -2700,14 +2519,13 @@ public virtual bool ValidateMinOrderSubtotalAmount(IList 0 && _orderSettings.MinOrderSubtotalAmount > decimal.Zero) { - //subtotal decimal orderSubTotalDiscountAmountBase = decimal.Zero; Discount orderSubTotalAppliedDiscount = null; decimal subTotalWithoutDiscountBase = decimal.Zero; decimal subTotalWithDiscountBase = decimal.Zero; + _orderTotalCalculationService.GetShoppingCartSubTotal(cart, - out orderSubTotalDiscountAmountBase, out orderSubTotalAppliedDiscount, - out subTotalWithoutDiscountBase, out subTotalWithDiscountBase); + out orderSubTotalDiscountAmountBase, out orderSubTotalAppliedDiscount, out subTotalWithoutDiscountBase, out subTotalWithDiscountBase); if (subTotalWithoutDiscountBase < _orderSettings.MinOrderSubtotalAmount) return false; @@ -2729,6 +2547,7 @@ public virtual bool ValidateMinOrderTotalAmount(IList if (cart.Count > 0 && _orderSettings.MinOrderTotalAmount > decimal.Zero) { decimal? shoppingCartTotalBase = _orderTotalCalculationService.GetShoppingCartTotal(cart); + if (shoppingCartTotalBase.HasValue && shoppingCartTotalBase.Value < _orderSettings.MinOrderTotalAmount) return false; } @@ -2736,6 +2555,80 @@ public virtual bool ValidateMinOrderTotalAmount(IList return true; } + public virtual Shipment AddShipment(Order order, string trackingNumber, Dictionary quantities) + { + Guard.ArgumentNotNull(() => order); + + Shipment shipment = null; + decimal? totalWeight = null; + + foreach (var orderItem in order.OrderItems) + { + if (!orderItem.Product.IsShipEnabled) + continue; + + //ensure that this product can be shipped (have at least one item to ship) + var maxQtyToAdd = orderItem.GetTotalNumberOfItemsCanBeAddedToShipment(); + if (maxQtyToAdd <= 0) + continue; + + var qtyToAdd = 0; + + if (quantities != null && quantities.ContainsKey(orderItem.Id)) + qtyToAdd = quantities[orderItem.Id]; + else if (quantities == null) + qtyToAdd = maxQtyToAdd; + + if (qtyToAdd <= 0) + continue; + + if (qtyToAdd > maxQtyToAdd) + qtyToAdd = maxQtyToAdd; + + var orderItemTotalWeight = orderItem.ItemWeight.HasValue ? orderItem.ItemWeight * qtyToAdd : null; + if (orderItemTotalWeight.HasValue) + { + if (!totalWeight.HasValue) + totalWeight = 0; + + totalWeight += orderItemTotalWeight.Value; + } + + if (shipment == null) + { + shipment = new Shipment + { + OrderId = order.Id, + Order = order, // otherwise order updated event would not be fired during InsertShipment + TrackingNumber = trackingNumber, + TotalWeight = null, + ShippedDateUtc = null, + DeliveryDateUtc = null, + CreatedOnUtc = DateTime.UtcNow, + }; + } + + var shipmentItem = new ShipmentItem + { + OrderItemId = orderItem.Id, + Quantity = qtyToAdd + }; + + shipment.ShipmentItems.Add(shipmentItem); + } + + if (shipment != null && shipment.ShipmentItems.Count > 0) + { + shipment.TotalWeight = totalWeight; + + _shipmentService.InsertShipment(shipment); + + return shipment; + } + + return null; + } + #endregion } } diff --git a/src/Libraries/SmartStore.Services/Orders/OrderService.cs b/src/Libraries/SmartStore.Services/Orders/OrderService.cs index c54392d8c4..7ed45bb61a 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderService.cs @@ -13,10 +13,10 @@ namespace SmartStore.Services.Orders { - /// - /// Order service - /// - public partial class OrderService : IOrderService + /// + /// Order service + /// + public partial class OrderService : IOrderService { #region Fields @@ -392,8 +392,7 @@ public virtual void DeleteOrderNote(OrderNote orderNote) /// Authorization transaction ID /// Payment method system name /// Order - public virtual Order GetOrderByAuthorizationTransactionIdAndPaymentMethod(string authorizationTransactionId, - string paymentMethodSystemName) + public virtual Order GetOrderByAuthorizationTransactionIdAndPaymentMethod(string authorizationTransactionId, string paymentMethodSystemName) { var query = _orderRepository.Table; if (!String.IsNullOrWhiteSpace(authorizationTransactionId)) @@ -406,17 +405,32 @@ public virtual Order GetOrderByAuthorizationTransactionIdAndPaymentMethod(string var order = query.FirstOrDefault(); return order; } - - #endregion - - #region Order items - /// - /// Gets an Order item - /// - /// Order item identifier - /// Order item - public virtual OrderItem GetOrderItemById(int orderItemId) + public virtual void AddOrderNote(Order order, string note, bool displayToCustomer = false) + { + if (order != null && note.HasValue()) + { + order.OrderNotes.Add(new OrderNote + { + Note = note, + DisplayToCustomer = displayToCustomer, + CreatedOnUtc = DateTime.UtcNow + }); + + UpdateOrder(order); + } + } + + #endregion + + #region Order items + + /// + /// Gets an Order item + /// + /// Order item identifier + /// Order item + public virtual OrderItem GetOrderItemById(int orderItemId) { if (orderItemId == 0) return null; diff --git a/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs b/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs index 3e48061380..e154dd519d 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs @@ -9,6 +9,7 @@ using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Domain.Tax; +using SmartStore.Core.Localization; using SmartStore.Core.Plugins; using SmartStore.Services.Catalog; using SmartStore.Services.Common; @@ -19,10 +20,10 @@ namespace SmartStore.Services.Orders { - /// - /// Order service - /// - public partial class OrderTotalCalculationService : IOrderTotalCalculationService + /// + /// Order service + /// + public partial class OrderTotalCalculationService : IOrderTotalCalculationService { #region Fields @@ -98,20 +99,24 @@ public OrderTotalCalculationService(IWorkContext workContext, this._shippingSettings = shippingSettings; this._shoppingCartSettings = shoppingCartSettings; this._catalogSettings = catalogSettings; - } - #endregion + T = NullLocalizer.Instance; + } - #region Methods + public Localizer T { get; set; } - /// - /// Gets shopping cart subtotal - /// - /// Cart - /// Applied discount amount - /// Applied discount - /// Sub total (without discount) - /// Sub total (with discount) + #endregion + + #region Methods + + /// + /// Gets shopping cart subtotal + /// + /// Cart + /// Applied discount amount + /// Applied discount + /// Sub total (without discount) + /// Sub total (with discount) public virtual void GetShoppingCartSubTotal(IList cart, out decimal discountAmount, out Discount appliedDiscount, out decimal subTotalWithoutDiscount, out decimal subTotalWithDiscount) @@ -613,7 +618,7 @@ public virtual decimal AdjustShippingRate(decimal shippingRate, IList cart, out So decimal taxRate = decimal.Zero; var provider = _providerManager.GetProvider(paymentMethodSystemName); - decimal paymentMethodAdditionalFee = provider.GetAdditionalHandlingFee(cart, _shoppingCartSettings.RoundPricesDuringCalculation); + var paymentMethodAdditionalFee = (provider != null ? provider.Value.GetAdditionalHandlingFee(cart) : decimal.Zero); + + if (_shoppingCartSettings.RoundPricesDuringCalculation) + { + paymentMethodAdditionalFee = Math.Round(paymentMethodAdditionalFee, 2); + } decimal paymentMethodAdditionalFeeExclTax = _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, false, customer, out taxRate); decimal paymentMethodAdditionalFeeInclTax = _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, true, customer, out taxRate); @@ -859,9 +869,9 @@ public virtual decimal GetTaxTotal(IList cart, out So int redeemedRewardPoints = 0; decimal redeemedRewardPointsAmount = decimal.Zero; List appliedGiftCards = null; + return GetShoppingCartTotal(cart, out discountAmount, out appliedDiscount, - out appliedGiftCards, - out redeemedRewardPoints, out redeemedRewardPointsAmount, ignoreRewardPonts, usePaymentMethodAdditionalFee); + out appliedGiftCards, out redeemedRewardPoints, out redeemedRewardPointsAmount, ignoreRewardPonts, usePaymentMethodAdditionalFee); } /// @@ -899,9 +909,7 @@ public virtual decimal GetTaxTotal(IList cart, out So decimal subTotalWithoutDiscountBase = decimal.Zero; decimal subTotalWithDiscountBase = decimal.Zero; - GetShoppingCartSubTotal(cart, false, - out orderSubTotalDiscountAmount, out orderSubTotalAppliedDiscount, - out subTotalWithoutDiscountBase, out subTotalWithDiscountBase); + GetShoppingCartSubTotal(cart, false, out orderSubTotalDiscountAmount, out orderSubTotalAppliedDiscount, out subTotalWithoutDiscountBase, out subTotalWithDiscountBase); //subtotal with discount subtotalBase = subTotalWithDiscountBase; @@ -914,9 +922,14 @@ public virtual decimal GetTaxTotal(IList cart, out So if (usePaymentMethodAdditionalFee && !String.IsNullOrEmpty(paymentMethodSystemName)) { var provider = _providerManager.GetProvider(paymentMethodSystemName); - decimal paymentMethodAdditionalFee = provider.GetAdditionalHandlingFee(cart, _shoppingCartSettings.RoundPricesDuringCalculation); + var paymentMethodAdditionalFee = (provider != null ? provider.Value.GetAdditionalHandlingFee(cart) : decimal.Zero); - paymentMethodAdditionalFeeWithoutTax = _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, false, customer); + if (_shoppingCartSettings.RoundPricesDuringCalculation) + { + paymentMethodAdditionalFee = Math.Round(paymentMethodAdditionalFee, 2); + } + + paymentMethodAdditionalFeeWithoutTax = _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, false, customer); } //tax @@ -1124,8 +1137,14 @@ public virtual int ConvertAmountToRewardPoints(decimal amount) if (amount <= 0) return 0; - if (_rewardPointsSettings.ExchangeRate > 0) - result = (int)Math.Ceiling(amount / _rewardPointsSettings.ExchangeRate); + if (_rewardPointsSettings.ExchangeRate > 0) + { + if (_rewardPointsSettings.RoundDownRewardPoints) + result = (int)Math.Floor(amount / _rewardPointsSettings.ExchangeRate); + else + result = (int)Math.Ceiling(amount / _rewardPointsSettings.ExchangeRate); + } + return result; } diff --git a/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs b/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs index ceec6e4ee5..27f0331be4 100644 --- a/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs +++ b/src/Libraries/SmartStore.Services/Orders/ShoppingCartService.cs @@ -8,6 +8,7 @@ using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Events; +using SmartStore.Core.Localization; using SmartStore.Services.Catalog; using SmartStore.Services.Common; using SmartStore.Services.Customers; @@ -19,10 +20,10 @@ namespace SmartStore.Services.Orders { - /// - /// Shopping cart service - /// - public partial class ShoppingCartService : IShoppingCartService + /// + /// Shopping cart service + /// + public partial class ShoppingCartService : IShoppingCartService { #region Fields @@ -112,20 +113,24 @@ public ShoppingCartService( this._genericAttributeService = genericAttributeService; this._downloadService = downloadService; this._catalogSettings = catalogSettings; - } - #endregion + T = NullLocalizer.Instance; + } - #region Methods + public Localizer T { get; set; } - /// - /// Delete shopping cart item - /// - /// Shopping cart item - /// A value indicating whether to reset checkout data - /// A value indicating whether to ensure that only active checkout attributes are attached to the current customer + #endregion + + #region Methods + + /// + /// Delete shopping cart item + /// + /// Shopping cart item + /// A value indicating whether to reset checkout data + /// A value indicating whether to ensure that only active checkout attributes are attached to the current customer /// A value indicating whether to delete child cart items - public virtual void DeleteShoppingCartItem(ShoppingCartItem shoppingCartItem, bool resetCheckoutData = true, + public virtual void DeleteShoppingCartItem(ShoppingCartItem shoppingCartItem, bool resetCheckoutData = true, bool ensureOnlyActiveCheckoutAttributes = false, bool deleteChildCartItems = true) { if (shoppingCartItem == null) @@ -271,17 +276,17 @@ public virtual IList GetRequiredProductWarnings(Customer customer, //don't display specific errors from 'addToCartWarnings' variable //display only generic error - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.RequiredProductWarning"), rp.GetLocalized(x => x.Name))); + warnings.Add(T("ShoppingCart.RequiredProductWarning", rp.GetLocalized(x => x.Name))); } } else { - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.RequiredProductWarning"), rp.GetLocalized(x => x.Name))); + warnings.Add(T("ShoppingCart.RequiredProductWarning", rp.GetLocalized(x => x.Name))); } } else { - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.RequiredProductWarning"), rp.GetLocalized(x => x.Name))); + warnings.Add(T("ShoppingCart.RequiredProductWarning", rp.GetLocalized(x => x.Name))); } } } @@ -314,57 +319,57 @@ public virtual IList GetStandardWarnings(Customer customer, ShoppingCart //deleted? if (product.Deleted) { - warnings.Add(_localizationService.GetResource("ShoppingCart.ProductDeleted")); + warnings.Add(T("ShoppingCart.ProductDeleted")); return warnings; } // check if the product type is available for order if (product.ProductType == ProductType.GroupedProduct) { - warnings.Add(_localizationService.GetResource("ShoppingCart.ProductNotAvailableForOrder")); + warnings.Add(T("ShoppingCart.ProductNotAvailableForOrder")); } // validate bundle if (product.ProductType == ProductType.BundledProduct) { if (product.BundlePerItemPricing && customerEnteredPrice != decimal.Zero) - warnings.Add(_localizationService.GetResource("ShoppingCart.Bundle.NoCustomerEnteredPrice")); + warnings.Add(T("ShoppingCart.Bundle.NoCustomerEnteredPrice")); } //published? if (!product.Published) { - warnings.Add(_localizationService.GetResource("ShoppingCart.ProductUnpublished")); + warnings.Add(T("ShoppingCart.ProductUnpublished")); } //ACL if (!_aclService.Authorize(product, customer)) { - warnings.Add(_localizationService.GetResource("ShoppingCart.ProductUnpublished")); + warnings.Add(T("ShoppingCart.ProductUnpublished")); } //Store mapping if (!_storeMappingService.Authorize(product, _storeContext.CurrentStore.Id)) { - warnings.Add(_localizationService.GetResource("ShoppingCart.ProductUnpublished")); + warnings.Add(T("ShoppingCart.ProductUnpublished")); } //disabled "add to cart" button if (shoppingCartType == ShoppingCartType.ShoppingCart && product.DisableBuyButton) { - warnings.Add(_localizationService.GetResource("ShoppingCart.BuyingDisabled")); + warnings.Add(T("ShoppingCart.BuyingDisabled")); } //disabled "add to wishlist" button if (shoppingCartType == ShoppingCartType.Wishlist && product.DisableWishlistButton) { - warnings.Add(_localizationService.GetResource("ShoppingCart.WishlistDisabled")); + warnings.Add(T("ShoppingCart.WishlistDisabled")); } //call for price if (shoppingCartType == ShoppingCartType.ShoppingCart && product.CallForPrice) { - warnings.Add(_localizationService.GetResource("Products.CallForPrice")); + warnings.Add(T("Products.CallForPrice")); } //customer entered price @@ -373,11 +378,13 @@ public virtual IList GetStandardWarnings(Customer customer, ShoppingCart if (customerEnteredPrice < product.MinimumCustomerEnteredPrice || customerEnteredPrice > product.MaximumCustomerEnteredPrice) { - decimal minimumCustomerEnteredPrice = _currencyService.ConvertFromPrimaryStoreCurrency(product.MinimumCustomerEnteredPrice, _workContext.WorkingCurrency); - decimal maximumCustomerEnteredPrice = _currencyService.ConvertFromPrimaryStoreCurrency(product.MaximumCustomerEnteredPrice, _workContext.WorkingCurrency); - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.CustomerEnteredPrice.RangeError"), - _priceFormatter.FormatPrice(minimumCustomerEnteredPrice, true, false), - _priceFormatter.FormatPrice(maximumCustomerEnteredPrice, true, false))); + var minimumCustomerEnteredPrice = _currencyService.ConvertFromPrimaryStoreCurrency(product.MinimumCustomerEnteredPrice, _workContext.WorkingCurrency); + var maximumCustomerEnteredPrice = _currencyService.ConvertFromPrimaryStoreCurrency(product.MaximumCustomerEnteredPrice, _workContext.WorkingCurrency); + + warnings.Add(T("ShoppingCart.CustomerEnteredPrice.RangeError", + _priceFormatter.FormatPrice(minimumCustomerEnteredPrice, true, false), + _priceFormatter.FormatPrice(maximumCustomerEnteredPrice, true, false)) + ); } } @@ -385,18 +392,20 @@ public virtual IList GetStandardWarnings(Customer customer, ShoppingCart var hasQtyWarnings = false; if (quantity < product.OrderMinimumQuantity) { - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.MinimumQuantity"), product.OrderMinimumQuantity)); + warnings.Add(T("ShoppingCart.MinimumQuantity", product.OrderMinimumQuantity)); hasQtyWarnings = true; } + if (quantity > product.OrderMaximumQuantity) { - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.MaximumQuantity"), product.OrderMaximumQuantity)); + warnings.Add(T("ShoppingCart.MaximumQuantity", product.OrderMaximumQuantity)); hasQtyWarnings = true; } + var allowedQuantities = product.ParseAllowedQuatities(); if (allowedQuantities.Length > 0 && !allowedQuantities.Contains(quantity)) { - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.AllowedQuantities"), string.Join(", ", allowedQuantities))); + warnings.Add(T("ShoppingCart.AllowedQuantities", string.Join(", ", allowedQuantities))); } var validateOutOfStock = shoppingCartType == ShoppingCartType.ShoppingCart || !_shoppingCartSettings.AllowOutOfStockItemsToBeAddedToWishlist; @@ -414,11 +423,12 @@ public virtual IList GetStandardWarnings(Customer customer, ShoppingCart { if (product.StockQuantity < quantity) { - int maximumQuantityCanBeAdded = product.StockQuantity; + var maximumQuantityCanBeAdded = product.StockQuantity; + if (maximumQuantityCanBeAdded <= 0) - warnings.Add(_localizationService.GetResource("ShoppingCart.OutOfStock")); + warnings.Add(T("ShoppingCart.OutOfStock")); else - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.QuantityExceedsStock"), maximumQuantityCanBeAdded)); + warnings.Add(T("ShoppingCart.QuantityExceedsStock", maximumQuantityCanBeAdded)); } } } @@ -434,9 +444,9 @@ public virtual IList GetStandardWarnings(Customer customer, ShoppingCart int maximumQuantityCanBeAdded = combination.StockQuantity; if (maximumQuantityCanBeAdded <= 0) - warnings.Add(_localizationService.GetResource("ShoppingCart.OutOfStock")); + warnings.Add(T("ShoppingCart.OutOfStock")); else - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.QuantityExceedsStock"), maximumQuantityCanBeAdded)); + warnings.Add(T("ShoppingCart.QuantityExceedsStock", maximumQuantityCanBeAdded)); } } } @@ -447,14 +457,14 @@ public virtual IList GetStandardWarnings(Customer customer, ShoppingCart } //availability dates - bool availableStartDateError = false; + var availableStartDateError = false; if (product.AvailableStartDateTimeUtc.HasValue) { DateTime now = DateTime.UtcNow; DateTime availableStartDateTime = DateTime.SpecifyKind(product.AvailableStartDateTimeUtc.Value, DateTimeKind.Utc); if (availableStartDateTime.CompareTo(now) > 0) { - warnings.Add(_localizationService.GetResource("ShoppingCart.NotAvailable")); + warnings.Add(T("ShoppingCart.NotAvailable")); availableStartDateError = true; } } @@ -464,7 +474,7 @@ public virtual IList GetStandardWarnings(Customer customer, ShoppingCart DateTime availableEndDateTime = DateTime.SpecifyKind(product.AvailableEndDateTimeUtc.Value, DateTimeKind.Utc); if (availableEndDateTime.CompareTo(now) < 0) { - warnings.Add(_localizationService.GetResource("ShoppingCart.NotAvailable")); + warnings.Add(T("ShoppingCart.NotAvailable")); } } return warnings; @@ -497,7 +507,7 @@ public virtual IList GetShoppingCartItemAttributeWarnings( if (pv1 == null || pv1.Id != product.Id) { - warnings.Add(_localizationService.GetResource("ShoppingCart.AttributeError")); + warnings.Add(T("ShoppingCart.AttributeError")); return warnings; } } @@ -533,8 +543,7 @@ public virtual IList GetShoppingCartItemAttributeWarnings( if (!found) { - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.SelectAttribute"), - pva2.TextPrompt.HasValue() ? pva2.TextPrompt : pva2.ProductAttribute.GetLocalized(a => a.Name))); + warnings.Add(T("ShoppingCart.SelectAttribute", pva2.TextPrompt.HasValue() ? pva2.TextPrompt : pva2.ProductAttribute.GetLocalized(a => a.Name))); } } } @@ -549,7 +558,7 @@ public virtual IList GetShoppingCartItemAttributeWarnings( if (combination != null && !combination.IsActive) { - warnings.Add(_localizationService.GetResource("ShoppingCart.NotAvailable")); + warnings.Add(T("ShoppingCart.NotAvailable")); } } @@ -568,18 +577,16 @@ public virtual IList GetShoppingCartItemAttributeWarnings( foreach (var linkageWarning in linkageWarnings) { - string msg = _localizationService.GetResource("ShoppingCart.ProductLinkageAttributeWarning").FormatWith( + warnings.Add(T("ShoppingCart.ProductLinkageAttributeWarning", pvaValue.ProductVariantAttribute.ProductAttribute.GetLocalized(a => a.Name), pvaValue.GetLocalized(a => a.Name), - linkageWarning); - - warnings.Add(msg); + linkageWarning) + ); } } else { - string msg = _localizationService.GetResource("ShoppingCart.ProductLinkageProductNotLoading").FormatWith(pvaValue.LinkedProductId); - warnings.Add(msg); + warnings.Add(T("ShoppingCart.ProductLinkageProductNotLoading", pvaValue.LinkedProductId)); } } } @@ -668,27 +675,34 @@ public virtual IList GetShoppingCartItemGiftCardWarnings(ShoppingCartTyp string giftCardMessage = string.Empty; _productAttributeParser.GetGiftCardAttribute(selectedAttributes, - out giftCardRecipientName, out giftCardRecipientEmail, - out giftCardSenderName, out giftCardSenderEmail, out giftCardMessage); + out giftCardRecipientName, out giftCardRecipientEmail, out giftCardSenderName, out giftCardSenderEmail, out giftCardMessage); - if (String.IsNullOrEmpty(giftCardRecipientName)) - warnings.Add(_localizationService.GetResource("ShoppingCart.RecipientNameError")); + if (String.IsNullOrEmpty(giftCardRecipientName)) + { + warnings.Add(T("ShoppingCart.RecipientNameError")); + } if (product.GiftCardType == GiftCardType.Virtual) { - //validate for virtual gift cards only + //validate for virtual gift cards only if (String.IsNullOrEmpty(giftCardRecipientEmail) || !giftCardRecipientEmail.IsEmail()) - warnings.Add(_localizationService.GetResource("ShoppingCart.RecipientEmailError")); + { + warnings.Add(T("ShoppingCart.RecipientEmailError")); + } } - if (String.IsNullOrEmpty(giftCardSenderName)) - warnings.Add(_localizationService.GetResource("ShoppingCart.SenderNameError")); + if (String.IsNullOrEmpty(giftCardSenderName)) + { + warnings.Add(T("ShoppingCart.SenderNameError")); + } if (product.GiftCardType == GiftCardType.Virtual) { - //validate for virtual gift cards only + //validate for virtual gift cards only if (String.IsNullOrEmpty(giftCardSenderEmail) || !giftCardSenderEmail.IsEmail()) - warnings.Add(_localizationService.GetResource("ShoppingCart.SenderEmailError")); + { + warnings.Add(T("ShoppingCart.SenderEmailError")); + } } } @@ -707,19 +721,27 @@ public virtual IList GetBundleItemWarnings(ProductBundleItem bundleItem) if (bundleItem != null) { - string name = bundleItem.GetLocalizedName(); + var name = bundleItem.GetLocalizedName(); if (!bundleItem.Published) - warnings.Add(_localizationService.GetResource("ShoppingCart.Bundle.BundleItemUnpublished").FormatWith(name)); + { + warnings.Add(T("ShoppingCart.Bundle.BundleItemUnpublished", name)); + } if (bundleItem.ProductId == 0 || bundleItem.BundleProductId == 0 || bundleItem.Product == null || bundleItem.BundleProduct == null) - warnings.Add(_localizationService.GetResource("ShoppingCart.Bundle.MissingProduct").FormatWith(name)); + { + warnings.Add(T("ShoppingCart.Bundle.MissingProduct", name)); + } if (bundleItem.Quantity <= 0) - warnings.Add(_localizationService.GetResource("ShoppingCart.Bundle.Quantity").FormatWith(name)); + { + warnings.Add(T("ShoppingCart.Bundle.Quantity", name)); + } if (bundleItem.Product.IsDownload || bundleItem.Product.IsRecurring) - warnings.Add(_localizationService.GetResource("ShoppingCart.Bundle.ProductResrictions").FormatWith(name)); + { + warnings.Add(T("ShoppingCart.Bundle.ProductResrictions", name)); + } } return warnings; @@ -819,7 +841,7 @@ public virtual IList GetShoppingCartWarnings(IList GetShoppingCartWarnings(IList GetShoppingCartWarnings(IList GetShoppingCartWarnings(IList GetShoppingCartWarnings(IList a.TextPrompt))) warnings.Add(ca2.GetLocalized(a => a.TextPrompt)); else - warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.SelectAttribute"), ca2.GetLocalized(a => a.Name))); + warnings.Add(T("ShoppingCart.SelectAttribute", ca2.GetLocalized(a => a.Name))); } } } @@ -927,7 +955,7 @@ public virtual OrganizedShoppingCartItem FindShoppingCartItemInTheCart(IList AddToCart(Customer customer, Product product, Shoppi if (cartType == ShoppingCartType.ShoppingCart && !_permissionService.Authorize(StandardPermissionProvider.EnableShoppingCart, customer)) { - warnings.Add("Shopping cart is disabled"); + warnings.Add(T("ShoppingCart.IsDisabled")); return warnings; } if (cartType == ShoppingCartType.Wishlist && !_permissionService.Authorize(StandardPermissionProvider.EnableWishlist, customer)) { - warnings.Add("Wishlist is disabled"); + warnings.Add(T("Wishlist.IsDisabled")); return warnings; } if (quantity <= 0) { - warnings.Add(_localizationService.GetResource("ShoppingCart.QuantityShouldPositive")); + warnings.Add(T("ShoppingCart.QuantityShouldPositive")); return warnings; } //if (parentItemId.HasValue && (parentItemId.Value == 0 || bundleItem == null || bundleItem.Id == 0)) //{ - // warnings.Add(_localizationService.GetResource("ShoppingCart.Bundle.BundleItemNotFound").FormatWith(bundleItem.GetLocalizedName())); + // warnings.Add(T("ShoppingCart.Bundle.BundleItemNotFound", bundleItem.GetLocalizedName())); // return warnings; //} @@ -1066,17 +1098,17 @@ public virtual List AddToCart(Customer customer, Product product, Shoppi //maximum items validation if (cartType == ShoppingCartType.ShoppingCart && cart.Count >= _shoppingCartSettings.MaximumShoppingCartItems) { - warnings.Add(_localizationService.GetResource("ShoppingCart.MaximumShoppingCartItems")); + warnings.Add(T("ShoppingCart.MaximumShoppingCartItems")); return warnings; } else if (cartType == ShoppingCartType.Wishlist && cart.Count >= _shoppingCartSettings.MaximumWishlistItems) { - warnings.Add(_localizationService.GetResource("ShoppingCart.MaximumWishlistItems")); + warnings.Add(T("ShoppingCart.MaximumWishlistItems")); return warnings; } var now = DateTime.UtcNow; - var cartItem = new ShoppingCartItem() + var cartItem = new ShoppingCartItem { ShoppingCartType = cartType, StoreId = storeId, @@ -1090,8 +1122,9 @@ public virtual List AddToCart(Customer customer, Product product, Shoppi }; if (bundleItem != null) + { cartItem.BundleItemId = bundleItem.Id; - + } if (ctx == null) { @@ -1137,7 +1170,7 @@ public virtual void AddToCart(AddToCartContext ctx) _downloadService, _catalogSettings, null, ctx.Warnings, true, ctx.BundleItemId); if (ctx.Product.ProductType == ProductType.BundledProduct && ctx.Attributes.HasValue()) - ctx.Warnings.Add(_localizationService.GetResource("ShoppingCart.Bundle.NoAttributes")); + ctx.Warnings.Add(T("ShoppingCart.Bundle.NoAttributes")); if (ctx.Product.IsGiftCard) ctx.Attributes = ctx.AttributeForm.AddGiftCardAttribute(ctx.Attributes, ctx.Product.Id, _productAttributeParser, ctx.BundleItemId); diff --git a/src/Libraries/SmartStore.Services/Payments/IPaymentMethodFilter.cs b/src/Libraries/SmartStore.Services/Payments/IPaymentMethodFilter.cs index e9e557dcb3..4ecd12457d 100644 --- a/src/Libraries/SmartStore.Services/Payments/IPaymentMethodFilter.cs +++ b/src/Libraries/SmartStore.Services/Payments/IPaymentMethodFilter.cs @@ -12,7 +12,7 @@ public partial interface IPaymentMethodFilter /// /// Payment filter request /// true filter out method, false do not filter out method - bool FilterOutPaymentMethod(PaymentFilterRequest request); + bool IsExcluded(PaymentFilterRequest request); /// /// Get URL for filter configuration diff --git a/src/Libraries/SmartStore.Services/Payments/PaymentExtentions.cs b/src/Libraries/SmartStore.Services/Payments/PaymentExtentions.cs index 3ba3d4c3d3..d9cd258259 100644 --- a/src/Libraries/SmartStore.Services/Payments/PaymentExtentions.cs +++ b/src/Libraries/SmartStore.Services/Payments/PaymentExtentions.cs @@ -9,7 +9,7 @@ namespace SmartStore.Services.Payments { - public static class PaymentExtentions + public static class PaymentExtentions { /// /// Is payment method active? @@ -34,25 +34,6 @@ public static bool IsPaymentMethodActive(this Provider paymentMe return paymentSettings.ActivePaymentMethodSystemNames.Contains(paymentMethod.Metadata.SystemName, StringComparer.OrdinalIgnoreCase); } - /// - /// Gets an additional handling fee of a payment method - /// - /// Shoping cart - /// Whether to round the fee - /// Additional handling fee - public static decimal GetAdditionalHandlingFee(this Provider paymentMethod, IList cart, bool round) - { - var result = decimal.Zero; - if (paymentMethod != null) - { - result = paymentMethod.Value.GetAdditionalHandlingFee(cart); - - if (round) - result = Math.Round(result, 2); - } - return result; - } - /// /// Calculate payment method fee /// diff --git a/src/Libraries/SmartStore.Services/Payments/PaymentService.cs b/src/Libraries/SmartStore.Services/Payments/PaymentService.cs index d7bd0f6176..0bce57bb5e 100644 --- a/src/Libraries/SmartStore.Services/Payments/PaymentService.cs +++ b/src/Libraries/SmartStore.Services/Payments/PaymentService.cs @@ -2,42 +2,37 @@ using System.Collections.Generic; using System.Linq; using SmartStore.Core.Data; -using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Discounts; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; -using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Events; using SmartStore.Core.Infrastructure; +using SmartStore.Core.Localization; using SmartStore.Core.Plugins; -using SmartStore.Services.Common; -using SmartStore.Services.Directory; -using SmartStore.Services.Orders; namespace SmartStore.Services.Payments { - /// - /// Payment service - /// - public partial class PaymentService : IPaymentService + /// + /// Payment service + /// + public partial class PaymentService : IPaymentService { #region Constants private const string PAYMENTMETHOD_ALL_KEY = "SmartStore.paymentmethod.all"; - + #endregion - #region Fields + #region Fields + + private readonly static object _lock = new object(); + private static IList _paymentMethodFilterTypes = null; private readonly IRepository _paymentMethodRepository; private readonly PaymentSettings _paymentSettings; - private readonly IPluginFinder _pluginFinder; private readonly ShoppingCartSettings _shoppingCartSettings; private readonly IProviderManager _providerManager; - private readonly ICurrencyService _currencyService; private readonly ICommonServices _services; - private readonly IOrderTotalCalculationService _orderTotalCalculationService; private readonly ITypeFinder _typeFinder; #endregion @@ -54,38 +49,36 @@ public partial class PaymentService : IPaymentService public PaymentService( IRepository paymentMethodRepository, PaymentSettings paymentSettings, - IPluginFinder pluginFinder, ShoppingCartSettings shoppingCartSettings, IProviderManager providerManager, - ICurrencyService currencyService, ICommonServices services, - IOrderTotalCalculationService orderTotalCalculationService, ITypeFinder typeFinder) { - this._paymentMethodRepository = paymentMethodRepository; - this._paymentSettings = paymentSettings; - this._pluginFinder = pluginFinder; - this._shoppingCartSettings = shoppingCartSettings; - this._providerManager = providerManager; - this._currencyService = currencyService; - this._services = services; - this._orderTotalCalculationService = orderTotalCalculationService; - this._typeFinder = typeFinder; - } + _paymentMethodRepository = paymentMethodRepository; + _paymentSettings = paymentSettings; + _shoppingCartSettings = shoppingCartSettings; + _providerManager = providerManager; + _services = services; + _typeFinder = typeFinder; + + T = NullLocalizer.Instance; + } - #endregion + public Localizer T { get; set; } + + #endregion #region Methods /// - /// Load active payment methods - /// + /// Load active payment methods + /// /// Filter payment methods by customer and apply payment method restrictions; null to load all records /// Filter payment methods by cart amount; null to load all records /// Filter payment methods by store identifier; pass 0 to load all records /// Filter payment methods by payment method types /// Provide a fallback payment method if none is active - /// Payment methods + /// Payment methods public virtual IEnumerable> LoadActivePaymentMethods( Customer customer = null, IList cart = null, @@ -93,11 +86,6 @@ public virtual IEnumerable> LoadActivePaymentMethods( PaymentMethodType[] types = null, bool provideFallbackMethod = true) { - List customerRoleIds = null; - int? selectedShippingMethodId = null; - decimal? orderSubTotal = null; - decimal? orderTotal = null; - IList allMethods = null; IList allFilters = null; IEnumerable> allProviders = null; @@ -125,100 +113,9 @@ public virtual IEnumerable> LoadActivePaymentMethods( filterRequest.PaymentMethod = p; - if (allFilters.Any(x => x.FilterOutPaymentMethod(filterRequest))) + if (allFilters.Any(x => x.IsExcluded(filterRequest))) return false; - // payment method core restrictions - if (customer != null) - { - if (allMethods == null) - allMethods = GetAllPaymentMethods(); - - var method = allMethods.FirstOrDefault(x => x.PaymentMethodSystemName.IsCaseInsensitiveEqual(p.Metadata.SystemName)); - if (method != null) - { - // method restricted by customer role id? - var excludedRoleIds = method.ExcludedCustomerRoleIds.ToIntArray(); - if (excludedRoleIds.Any()) - { - if (customerRoleIds == null) - customerRoleIds = customer.CustomerRoles.Where(r => r.Active).Select(r => r.Id).ToList(); - - if (customerRoleIds != null && !customerRoleIds.Except(excludedRoleIds).Any()) - return false; - } - - // method restricted by selected shipping method? - var excludedShippingMethodIds = method.ExcludedShippingMethodIds.ToIntArray(); - if (excludedShippingMethodIds.Any()) - { - if (!selectedShippingMethodId.HasValue) - { - var selectedShipping = customer.GetAttribute(SystemCustomerAttributeNames.SelectedShippingOption, storeId); - selectedShippingMethodId = (selectedShipping == null ? 0 : selectedShipping.ShippingMethodId); - } - - if ((selectedShippingMethodId ?? 0) != 0 && excludedShippingMethodIds.Contains(selectedShippingMethodId.Value)) - return false; - } - - // method restricted by country of selected billing or shipping address? - var excludedCountryIds = method.ExcludedCountryIds.ToIntArray(); - if (excludedCountryIds.Any()) - { - int countryId = 0; - if (method.CountryExclusionContext == CountryRestrictionContextType.ShippingAddress) - countryId = (customer.ShippingAddress != null ? (customer.ShippingAddress.CountryId ?? 0) : 0); - else - countryId = (customer.BillingAddress != null ? (customer.BillingAddress.CountryId ?? 0) : 0); - - if (countryId != 0 && excludedCountryIds.Contains(countryId)) - return false; - } - - // method restricted by min\max order amount? - if ((method.MinimumOrderAmount.HasValue || method.MaximumOrderAmount.HasValue) && cart != null) - { - decimal compareAmount = decimal.Zero; - - if (method.AmountRestrictionContext == AmountRestrictionContextType.SubtotalAmount) - { - if (!orderSubTotal.HasValue) - { - decimal orderSubTotalDiscountAmountBase = decimal.Zero; - Discount orderSubTotalAppliedDiscount = null; - decimal subTotalWithoutDiscountBase = decimal.Zero; - decimal subTotalWithDiscountBase = decimal.Zero; - - _orderTotalCalculationService.GetShoppingCartSubTotal(cart, out orderSubTotalDiscountAmountBase, out orderSubTotalAppliedDiscount, - out subTotalWithoutDiscountBase, out subTotalWithDiscountBase); - - orderSubTotal = _currencyService.ConvertFromPrimaryStoreCurrency(subTotalWithoutDiscountBase, _services.WorkContext.WorkingCurrency); - } - - compareAmount = orderSubTotal.Value; - } - else if (method.AmountRestrictionContext == AmountRestrictionContextType.TotalAmount) - { - if (!orderTotal.HasValue) - { - orderTotal = _orderTotalCalculationService.GetShoppingCartTotal(cart) ?? decimal.Zero; - - orderTotal = _currencyService.ConvertFromPrimaryStoreCurrency(orderTotal.Value, _services.WorkContext.WorkingCurrency); - } - - compareAmount = orderTotal.Value; - } - - if (method.MinimumOrderAmount.HasValue && compareAmount < method.MinimumOrderAmount.Value) - return false; - - if (method.MaximumOrderAmount.HasValue && compareAmount > method.MaximumOrderAmount.Value) - return false; - } - } - } - return true; }); @@ -236,7 +133,7 @@ public virtual IEnumerable> LoadActivePaymentMethods( else { if (DataSettings.DatabaseIsInstalled()) - throw Error.Application("At least one payment method provider is required to be active."); + throw new SmartException(T("Payment.OneActiveMethodProviderRequired")); } } @@ -361,7 +258,7 @@ public virtual PreProcessPaymentResult PreProcessPayment(ProcessPaymentRequest p { var paymentMethod = LoadPaymentMethodBySystemName(processPaymentRequest.PaymentMethodSystemName); if (paymentMethod == null) - throw new SmartException("Payment method couldn't be loaded"); + throw new SmartException(T("Payment.CouldNotLoadMethod")); return paymentMethod.Value.PreProcessPayment(processPaymentRequest); } @@ -390,9 +287,12 @@ public virtual ProcessPaymentResult ProcessPayment(ProcessPaymentRequest process processPaymentRequest.CreditCardNumber = processPaymentRequest.CreditCardNumber.Replace(" ", ""); processPaymentRequest.CreditCardNumber = processPaymentRequest.CreditCardNumber.Replace("-", ""); } - var paymentMethod = LoadPaymentMethodBySystemName(processPaymentRequest.PaymentMethodSystemName); - if (paymentMethod == null) - throw new SmartException("Payment method couldn't be loaded"); + + var paymentMethod = LoadPaymentMethodBySystemName(processPaymentRequest.PaymentMethodSystemName); + + if (paymentMethod == null) + throw new SmartException(T("Payment.CouldNotLoadMethod")); + return paymentMethod.Value.ProcessPayment(processPaymentRequest); } } @@ -408,7 +308,7 @@ public virtual void PostProcessPayment(PostProcessPaymentRequest postProcessPaym { var paymentMethod = LoadPaymentMethodBySystemName(postProcessPaymentRequest.Order.PaymentMethodSystemName); if (paymentMethod == null) - throw new SmartException("Payment method couldn't be loaded"); + throw new SmartException(T("Payment.CouldNotLoadMethod")); paymentMethod.Value.PostProcessPayment(postProcessPaymentRequest); } @@ -457,8 +357,14 @@ public virtual bool CanRePostProcessPayment(Order order) public virtual decimal GetAdditionalHandlingFee(IList cart, string paymentMethodSystemName) { var paymentMethod = LoadPaymentMethodBySystemName(paymentMethodSystemName); + var paymentMethodAdditionalFee = (paymentMethod != null ? paymentMethod.Value.GetAdditionalHandlingFee(cart) : decimal.Zero); + + if (_shoppingCartSettings.RoundPricesDuringCalculation) + { + paymentMethodAdditionalFee = Math.Round(paymentMethodAdditionalFee, 2); + } - return paymentMethod.GetAdditionalHandlingFee(cart, _shoppingCartSettings.RoundPricesDuringCalculation); + return paymentMethodAdditionalFee; } @@ -485,7 +391,7 @@ public virtual CapturePaymentResult Capture(CapturePaymentRequest capturePayment { var paymentMethod = LoadPaymentMethodBySystemName(capturePaymentRequest.Order.PaymentMethodSystemName); if (paymentMethod == null) - throw new SmartException("Payment method couldn't be loaded"); + throw new SmartException(T("Payment.CouldNotLoadMethod")); try { @@ -494,7 +400,7 @@ public virtual CapturePaymentResult Capture(CapturePaymentRequest capturePayment catch (NotSupportedException) { var result = new CapturePaymentResult(); - result.AddError(_services.Localization.GetResource("Common.Payment.NoCaptureSupport")); + result.AddError(T("Common.Payment.NoCaptureSupport")); return result; } catch @@ -540,7 +446,7 @@ public virtual RefundPaymentResult Refund(RefundPaymentRequest refundPaymentRequ { var paymentMethod = LoadPaymentMethodBySystemName(refundPaymentRequest.Order.PaymentMethodSystemName); if (paymentMethod == null) - throw new SmartException("Payment method couldn't be loaded"); + throw new SmartException(T("Payment.CouldNotLoadMethod")); try { @@ -549,7 +455,7 @@ public virtual RefundPaymentResult Refund(RefundPaymentRequest refundPaymentRequ catch (NotSupportedException) { var result = new RefundPaymentResult(); - result.AddError(_services.Localization.GetResource("Common.Payment.NoRefundSupport")); + result.AddError(T("Common.Payment.NoRefundSupport")); return result; } catch @@ -582,7 +488,7 @@ public virtual VoidPaymentResult Void(VoidPaymentRequest voidPaymentRequest) { var paymentMethod = LoadPaymentMethodBySystemName(voidPaymentRequest.Order.PaymentMethodSystemName); if (paymentMethod == null) - throw new SmartException("Payment method couldn't be loaded"); + throw new SmartException(T("Payment.CouldNotLoadMethod")); try { @@ -591,7 +497,7 @@ public virtual VoidPaymentResult Void(VoidPaymentRequest voidPaymentRequest) catch (NotSupportedException) { var result = new VoidPaymentResult(); - result.AddError(_services.Localization.GetResource("Common.Payment.NoVoidSupport")); + result.AddError(T("Common.Payment.NoVoidSupport")); return result; } catch @@ -612,6 +518,7 @@ public virtual RecurringPaymentType GetRecurringPaymentType(string paymentMethod var paymentMethod = LoadPaymentMethodBySystemName(paymentMethodSystemName); if (paymentMethod == null) return RecurringPaymentType.NotSupported; + return paymentMethod.Value.RecurringPaymentType; } @@ -634,7 +541,7 @@ public virtual ProcessPaymentResult ProcessRecurringPayment(ProcessPaymentReques { var paymentMethod = LoadPaymentMethodBySystemName(processPaymentRequest.PaymentMethodSystemName); if (paymentMethod == null) - throw new SmartException("Payment method couldn't be loaded"); + throw new SmartException(T("Payment.CouldNotLoadMethod")); try { @@ -643,7 +550,7 @@ public virtual ProcessPaymentResult ProcessRecurringPayment(ProcessPaymentReques catch (NotSupportedException) { var result = new ProcessPaymentResult(); - result.AddError(_services.Localization.GetResource("Common.Payment.NoRecurringPaymentSupport")); + result.AddError(T("Common.Payment.NoRecurringPaymentSupport")); return result; } catch @@ -665,7 +572,7 @@ public virtual CancelRecurringPaymentResult CancelRecurringPayment(CancelRecurri var paymentMethod = LoadPaymentMethodBySystemName(cancelPaymentRequest.Order.PaymentMethodSystemName); if (paymentMethod == null) - throw new SmartException("Payment method couldn't be loaded"); + throw new SmartException(T("Payment.CouldNotLoadMethod")); try { @@ -674,7 +581,7 @@ public virtual CancelRecurringPaymentResult CancelRecurringPayment(CancelRecurri catch (NotSupportedException) { var result = new CancelRecurringPaymentResult(); - result.AddError(_services.Localization.GetResource("Common.Payment.NoRecurringPaymentSupport")); + result.AddError(T("Common.Payment.NoRecurringPaymentSupport")); return result; } catch @@ -695,6 +602,7 @@ public virtual PaymentMethodType GetPaymentMethodType(string paymentMethodSystem var paymentMethod = LoadPaymentMethodBySystemName(paymentMethodSystemName); if (paymentMethod == null) return PaymentMethodType.Unknown; + return paymentMethod.Value.PaymentMethodType; } @@ -722,11 +630,25 @@ public virtual string GetMaskedCreditCardNumber(string creditCardNumber) public virtual IList GetAllPaymentMethodFilters() { - return _typeFinder.FindClassesOfType(ignoreInactivePlugins: true) + if (_paymentMethodFilterTypes == null) + { + lock (_lock) + { + if (_paymentMethodFilterTypes == null) + { + _paymentMethodFilterTypes = _typeFinder.FindClassesOfType(ignoreInactivePlugins: true) + .ToList(); + } + } + } + + var paymentMethodFilters = _paymentMethodFilterTypes .Select(x => EngineContext.Current.ContainerManager.ResolveUnregistered(x) as IPaymentMethodFilter) .ToList(); + + return paymentMethodFilters; } - #endregion - } + #endregion + } } diff --git a/src/Libraries/SmartStore.Services/Payments/ProcessPaymentRequest.cs b/src/Libraries/SmartStore.Services/Payments/ProcessPaymentRequest.cs index 805af5ed70..a3ba77cb5b 100644 --- a/src/Libraries/SmartStore.Services/Payments/ProcessPaymentRequest.cs +++ b/src/Libraries/SmartStore.Services/Payments/ProcessPaymentRequest.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Orders; namespace SmartStore.Services.Payments { @@ -12,7 +13,9 @@ public partial class ProcessPaymentRequest { public ProcessPaymentRequest() { - this.CustomProperties = new Dictionary(); + CustomProperties = new Dictionary(); + IsMultiOrder = false; + ShoppingCartItems = new List(); } /// @@ -45,6 +48,11 @@ public ProcessPaymentRequest() /// public string PaymentMethodSystemName { get; set; } + /// + /// Gets or sets a payment method identifier + /// + public bool IsMultiOrder { get; set; } + /// /// Use that dictionary for any payment method or checkout flow specific data /// @@ -144,6 +152,8 @@ public ProcessPaymentRequest() /// public int RecurringTotalCycles { get; set; } + public IList ShoppingCartItems { get; set; } + #endregion } diff --git a/src/Libraries/SmartStore.Services/Pdf/WkHtmlToPdfConverter.cs b/src/Libraries/SmartStore.Services/Pdf/WkHtmlToPdfConverter.cs index 22dfb32589..4694512828 100644 --- a/src/Libraries/SmartStore.Services/Pdf/WkHtmlToPdfConverter.cs +++ b/src/Libraries/SmartStore.Services/Pdf/WkHtmlToPdfConverter.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Text; -using System.Web; using NReco.PdfGenerator; using SmartStore.Core.Logging; using SmartStore.Utilities; @@ -12,11 +9,8 @@ namespace SmartStore.Services.Pdf { public class WkHtmlToPdfConverter : IPdfConverter { - private readonly HttpContextBase _httpContext; - - public WkHtmlToPdfConverter(HttpContextBase httpContext) + public WkHtmlToPdfConverter() { - this._httpContext = httpContext; Logger = NullLogger.Instance; } diff --git a/src/Libraries/SmartStore.Services/Security/AclService.cs b/src/Libraries/SmartStore.Services/Security/AclService.cs index 8f0a52eed7..f1ea323ff2 100644 --- a/src/Libraries/SmartStore.Services/Security/AclService.cs +++ b/src/Libraries/SmartStore.Services/Security/AclService.cs @@ -60,8 +60,7 @@ public bool HasActiveAcl { if (!_hasActiveAcl.HasValue) { - var query = _aclRecordRepository.Where(x => !x.IsIdle); - _hasActiveAcl = query.Any(); + _hasActiveAcl = _aclRecordRepository.TableUntracked.Any(x => !x.IsIdle); } return _hasActiveAcl.Value; } diff --git a/src/Libraries/SmartStore.Services/Security/PermissionService.cs b/src/Libraries/SmartStore.Services/Security/PermissionService.cs index a4c8bd1803..a3ada9721a 100644 --- a/src/Libraries/SmartStore.Services/Security/PermissionService.cs +++ b/src/Libraries/SmartStore.Services/Security/PermissionService.cs @@ -10,10 +10,10 @@ namespace SmartStore.Services.Security { - /// - /// Permission service - /// - public partial class PermissionService : IPermissionService + /// + /// Permission service + /// + public partial class PermissionService : IPermissionService { #region Constants /// @@ -141,9 +141,11 @@ orderby pr.Id /// Permissions public virtual IList GetAllPermissionRecords() { - var query = from pr in _permissionRecordRepository.Table - orderby pr.Name - select pr; + var query = + from pr in _permissionRecordRepository.Table + orderby pr.Category, pr.Name + select pr; + var permissions = query.ToList(); return permissions; } @@ -207,7 +209,7 @@ public virtual void InstallPermissions(IPermissionProvider permissionProvider) if (customerRole == null) { //new role (save it) - customerRole = new CustomerRole() + customerRole = new CustomerRole { Name = defaultPermission.CustomerRoleSystemName, Active = true, diff --git a/src/Libraries/SmartStore.Services/Security/StandardPermissionProvider.cs b/src/Libraries/SmartStore.Services/Security/StandardPermissionProvider.cs index 7018c51146..4f1032077a 100644 --- a/src/Libraries/SmartStore.Services/Security/StandardPermissionProvider.cs +++ b/src/Libraries/SmartStore.Services/Security/StandardPermissionProvider.cs @@ -50,10 +50,11 @@ public partial class StandardPermissionProvider : IPermissionProvider public static readonly PermissionRecord UploadPictures = new PermissionRecord { Name = "Admin area. Upload Pictures", SystemName = "UploadPictures", Category = "Configuration" }; public static readonly PermissionRecord ManageScheduleTasks = new PermissionRecord { Name = "Admin area. Manage Schedule Tasks", SystemName = "ManageScheduleTasks", Category = "Configuration" }; public static readonly PermissionRecord ManageExports = new PermissionRecord { Name = "Admin area. Manage Exports", SystemName = "ManageExports", Category = "Configuration" }; + public static readonly PermissionRecord ManageImports = new PermissionRecord { Name = "Admin area. Manage Imports", SystemName = "ManageImports", Category = "Configuration" }; + public static readonly PermissionRecord ManageUrlRecords = new PermissionRecord { Name = "Admin area. Manage Url Records", SystemName = "ManageUrlRecords", Category = "Configuration" }; - - //public store permissions - public static readonly PermissionRecord DisplayPrices = new PermissionRecord { Name = "Public store. Display Prices", SystemName = "DisplayPrices", Category = "PublicStore" }; + //public store permissions + public static readonly PermissionRecord DisplayPrices = new PermissionRecord { Name = "Public store. Display Prices", SystemName = "DisplayPrices", Category = "PublicStore" }; public static readonly PermissionRecord EnableShoppingCart = new PermissionRecord { Name = "Public store. Enable shopping cart", SystemName = "EnableShoppingCart", Category = "PublicStore" }; public static readonly PermissionRecord EnableWishlist = new PermissionRecord { Name = "Public store. Enable wishlist", SystemName = "EnableWishlist", Category = "PublicStore" }; public static readonly PermissionRecord PublicStoreAllowNavigation = new PermissionRecord { Name = "Public store. Allow navigation", SystemName = "PublicStoreAllowNavigation", Category = "PublicStore" }; @@ -105,7 +106,9 @@ public virtual IEnumerable GetPermissions() UploadPictures, ManageScheduleTasks, ManageExports, - DisplayPrices, + ManageImports, + ManageUrlRecords, + DisplayPrices, EnableShoppingCart, EnableWishlist, PublicStoreAllowNavigation, @@ -163,6 +166,8 @@ public virtual IEnumerable GetDefaultPermissions() UploadPictures, ManageScheduleTasks, ManageExports, + ManageImports, + ManageUrlRecords, DisplayPrices, EnableShoppingCart, EnableWishlist, diff --git a/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs b/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs index c2e14366ff..d3bfa5a04d 100644 --- a/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs +++ b/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs @@ -59,8 +59,9 @@ public partial interface IUrlRecordService /// Entity name /// Entity identifier /// Whether to load only active records + /// Language identifier /// Customer collection - IPagedList GetAllUrlRecords(int pageIndex, int pageSize, string slug, string entityName, int? entityId, bool? isActive); + IPagedList GetAllUrlRecords(int pageIndex, int pageSize, string slug, string entityName, int? entityId, int? languageId, bool? isActive); /// /// Gets all URL records for the specified entity @@ -107,5 +108,13 @@ public partial interface IUrlRecordService /// URL record identifier /// Dictionary of slugs per entity count Dictionary CountSlugsPerEntity(int[] urlRecordIds); + + /// + /// Get number of slugs per entity + /// + /// Entity name + /// Entity identifier + /// Number of slugs per entity + int CountSlugsPerEntity(string entityName, int entityId); } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs b/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs index e6cc541ac4..29c29d13ef 100644 --- a/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs +++ b/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs @@ -243,7 +243,7 @@ public static string ValidateSeName(this T entity, } } - // validate and alter sename if it could be interpreted as SEO code + // validate and alter SeName if it could be interpreted as SEO code if (LocalizationHelper.IsValidCultureCode(seName)) { if (seName.Length == 2) @@ -304,7 +304,8 @@ public static string GetSeName(string name, SeoSettings seoSettings) return SeoHelper.GetSeName( name, seoSettings == null ? false : seoSettings.ConvertNonWesternChars, - seoSettings == null ? false : seoSettings.AllowUnicodeCharsInUrls); + seoSettings == null ? false : seoSettings.AllowUnicodeCharsInUrls, + seoSettings == null ? null : seoSettings.SeoNameCharConversion); } #endregion diff --git a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs index 78ec3ed5c4..38fe4a7bda 100644 --- a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs +++ b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs @@ -68,7 +68,7 @@ public virtual IList GetUrlRecordsByIds(int[] urlRecordIds) if (urlRecordIds == null || urlRecordIds.Length == 0) return new List(); - var urlRecords = _urlRecordRepository + var urlRecords = _urlRecordRepository.Table .Where(x => urlRecordIds.Contains(x.Id)) .ToList(); @@ -97,7 +97,7 @@ public virtual void UpdateUrlRecord(UrlRecord urlRecord) _cacheManager.RemoveByPattern(URLRECORD_PATTERN_KEY); } - public virtual IPagedList GetAllUrlRecords(int pageIndex, int pageSize, string slug, string entityName, int? entityId, bool? isActive) + public virtual IPagedList GetAllUrlRecords(int pageIndex, int pageSize, string slug, string entityName, int? entityId, int? languageId, bool? isActive) { var query = _urlRecordRepository.Table; @@ -113,6 +113,9 @@ public virtual IPagedList GetAllUrlRecords(int pageIndex, int pageSiz if (isActive.HasValue) query = query.Where(x => x.IsActive == isActive.Value); + if (languageId.HasValue) + query = query.Where(x => x.LanguageId == languageId); + query = query.OrderBy(x => x.Slug); var urlRecords = new PagedList(query, pageIndex, pageSize); @@ -162,9 +165,9 @@ where x.IsActive orderby x.Id descending select x; - var result = query.ToDictionary( - x => GenerateKey(x.EntityId, x.EntityName, x.LanguageId), // Key - x => x.Slug, // Value + var result = query.ToDictionarySafe( + x => GenerateKey(x.EntityId, x.EntityName, x.LanguageId), + x => x.Slug, StringComparer.OrdinalIgnoreCase); return result; @@ -348,6 +351,15 @@ where urlRecordIds.Contains(x.Id) return result; } + public virtual int CountSlugsPerEntity(string entityName, int entityId) + { + var count = _urlRecordRepository.Table + .Where(x => x.EntityName == entityName && x.EntityId == entityId) + .Count(); + + return count; + } + #endregion } } \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/Shipping/IShippingMethodFilter.cs b/src/Libraries/SmartStore.Services/Shipping/IShippingMethodFilter.cs index 5718dc56ff..2fbed519bc 100644 --- a/src/Libraries/SmartStore.Services/Shipping/IShippingMethodFilter.cs +++ b/src/Libraries/SmartStore.Services/Shipping/IShippingMethodFilter.cs @@ -9,7 +9,7 @@ public partial interface IShippingMethodFilter /// /// Shipping filter request /// true filter out method, false do not filter out method - bool FilterOutShippingMethod(ShippingFilterRequest request); + bool IsExcluded(ShippingFilterRequest request); /// /// Get URL for filter configuration @@ -30,6 +30,6 @@ public partial class ShippingFilterRequest /// /// Shipping method request /// - public GetShippingOptionRequest Request { get; set; } + public GetShippingOptionRequest Option { get; set; } } } diff --git a/src/Libraries/SmartStore.Services/Shipping/ShipmentService.cs b/src/Libraries/SmartStore.Services/Shipping/ShipmentService.cs index 301122a7fc..3a8c261164 100644 --- a/src/Libraries/SmartStore.Services/Shipping/ShipmentService.cs +++ b/src/Libraries/SmartStore.Services/Shipping/ShipmentService.cs @@ -7,7 +7,6 @@ using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Events; -using SmartStore.Core.Plugins; using SmartStore.Services.Orders; namespace SmartStore.Services.Shipping @@ -240,7 +239,20 @@ public virtual void InsertShipmentItem(ShipmentItem shipmentItem) //event notifications _eventPublisher.EntityInserted(shipmentItem); - _eventPublisher.PublishOrderUpdated(shipmentItem.Shipment.Order); + + if (shipmentItem.Shipment != null && shipmentItem.Shipment.Order != null) + { + _eventPublisher.PublishOrderUpdated(shipmentItem.Shipment.Order); + } + else + { + var shipment = _shipmentRepository.Table + .Expand(x => x.Order) + .FirstOrDefault(x => x.Id == shipmentItem.ShipmentId); + + if (shipment != null) + _eventPublisher.PublishOrderUpdated(shipment.Order); + } } /// @@ -256,7 +268,20 @@ public virtual void UpdateShipmentItem(ShipmentItem shipmentItem) //event notifications _eventPublisher.EntityUpdated(shipmentItem); - _eventPublisher.PublishOrderUpdated(shipmentItem.Shipment.Order); + + if (shipmentItem.Shipment != null && shipmentItem.Shipment.Order != null) + { + _eventPublisher.PublishOrderUpdated(shipmentItem.Shipment.Order); + } + else + { + var shipment = _shipmentRepository.Table + .Expand(x => x.Order) + .FirstOrDefault(x => x.Id == shipmentItem.ShipmentId); + + if (shipment != null) + _eventPublisher.PublishOrderUpdated(shipment.Order); + } } #endregion diff --git a/src/Libraries/SmartStore.Services/Shipping/ShippingService.cs b/src/Libraries/SmartStore.Services/Shipping/ShippingService.cs index ec8bd410ad..4f6db7fa9c 100644 --- a/src/Libraries/SmartStore.Services/Shipping/ShippingService.cs +++ b/src/Libraries/SmartStore.Services/Shipping/ShippingService.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Common; @@ -10,30 +9,30 @@ using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Events; using SmartStore.Core.Infrastructure; +using SmartStore.Core.Localization; using SmartStore.Core.Logging; using SmartStore.Core.Plugins; using SmartStore.Services.Catalog; using SmartStore.Services.Common; using SmartStore.Services.Configuration; -using SmartStore.Services.Localization; using SmartStore.Services.Orders; namespace SmartStore.Services.Shipping { - public partial class ShippingService : IShippingService + public partial class ShippingService : IShippingService { - #region Fields + #region Fields - private readonly IRepository _shippingMethodRepository; - private readonly ICacheManager _cacheManager; + private readonly static object _lock = new object(); + private static IList _shippingMethodFilterTypes = null; + + private readonly IRepository _shippingMethodRepository; private readonly ILogger _logger; private readonly IProductAttributeParser _productAttributeParser; private readonly IProductService _productService; private readonly ICheckoutAttributeParser _checkoutAttributeParser; private readonly IGenericAttributeService _genericAttributeService; - private readonly ILocalizationService _localizationService; private readonly ShippingSettings _shippingSettings; - private readonly IPluginFinder _pluginFinder; private readonly IEventPublisher _eventPublisher; private readonly ShoppingCartSettings _shoppingCartSettings; private readonly ISettingService _settingService; @@ -60,50 +59,49 @@ public partial class ShippingService : IShippingService /// Event published /// Shopping cart settings /// Setting service - public ShippingService(ICacheManager cacheManager, + public ShippingService( IRepository shippingMethodRepository, ILogger logger, IProductAttributeParser productAttributeParser, IProductService productService, ICheckoutAttributeParser checkoutAttributeParser, IGenericAttributeService genericAttributeService, - ILocalizationService localizationService, ShippingSettings shippingSettings, - IPluginFinder pluginFinder, IEventPublisher eventPublisher, ShoppingCartSettings shoppingCartSettings, ISettingService settingService, IProviderManager providerManager, ITypeFinder typeFinder) { - this._cacheManager = cacheManager; this._shippingMethodRepository = shippingMethodRepository; this._logger = logger; this._productAttributeParser = productAttributeParser; this._productService = productService; this._checkoutAttributeParser = checkoutAttributeParser; this._genericAttributeService = genericAttributeService; - this._localizationService = localizationService; this._shippingSettings = shippingSettings; - this._pluginFinder = pluginFinder; this._eventPublisher = eventPublisher; this._shoppingCartSettings = shoppingCartSettings; this._settingService = settingService; this._providerManager = providerManager; this._typeFinder = typeFinder; + + T = NullLocalizer.Instance; } - #endregion - - #region Methods + public Localizer T { get; set; } - #region Shipping rate computation methods + #endregion - /// - /// Load active shipping rate computation methods - /// + #region Methods + + #region Shipping rate computation methods + + /// + /// Load active shipping rate computation methods + /// /// Load records allows only in specified store; pass 0 to load all records - /// Shipping rate computation methods + /// Shipping rate computation methods public virtual IEnumerable> LoadActiveShippingRateComputationMethods(int storeId = 0) { var allMethods = LoadAllShippingRateComputationMethods(storeId); @@ -129,7 +127,7 @@ public virtual IEnumerable> LoadActiveS else { if (DataSettings.DatabaseIsInstalled()) - throw Error.Application("At least one shipping method provider is required to be active."); + throw new SmartException(T("Shipping.OneActiveMethodProviderRequired")); } } @@ -191,8 +189,6 @@ public virtual ShippingMethod GetShippingMethodById(int shippingMethodId) public virtual IList GetAllShippingMethods(GetShippingOptionRequest request = null) { - List customerRoleIds = null; - var query = from sm in _shippingMethodRepository.Table orderby sm.DisplayOrder @@ -203,42 +199,20 @@ orderby sm.DisplayOrder if (request == null) return allMethods; - var allFilters = GetAllShippingMethodFilters(); - var filterRequest = new ShippingFilterRequest { Request = request }; + IList allFilters = null; + var filterRequest = new ShippingFilterRequest { Option = request }; var activeShippingMethods = allMethods.Where(s => { // shipping method filtering + if (allFilters == null) + allFilters = GetAllShippingMethodFilters(); + filterRequest.ShippingMethod = s; - if (allFilters.Any(x => x.FilterOutShippingMethod(filterRequest))) + if (allFilters.Any(x => x.IsExcluded(filterRequest))) return false; - // shipping method core restrictions - if (request.Customer != null) - { - // method restricted by customer role id? - var excludedRoleIds = s.ExcludedCustomerRoleIds.ToIntArray(); - if (excludedRoleIds.Any()) - { - if (customerRoleIds == null) - customerRoleIds = request.Customer.CustomerRoles.Where(r => r.Active).Select(r => r.Id).ToList(); - - if (customerRoleIds != null && !customerRoleIds.Except(excludedRoleIds).Any()) - return false; - } - - // method restricted by country of selected billing or shipping address? - int countryId = 0; - if (s.CountryExclusionContext == CountryRestrictionContextType.ShippingAddress) - countryId = (request.Customer.ShippingAddress != null ? (request.Customer.ShippingAddress.CountryId ?? 0) : 0); - else - countryId = (request.Customer.BillingAddress != null ? (request.Customer.BillingAddress.CountryId ?? 0) : 0); - - if (countryId != 0 && s.CountryRestrictionExists(countryId)) - return false; - } - return true; }); @@ -343,11 +317,11 @@ public virtual decimal GetShoppingCartTotalWeight(IList(SystemCustomerAttributeNames.CheckoutAttributes, _genericAttributeService); @@ -358,6 +332,7 @@ public virtual decimal GetShoppingCartTotalWeight(IList 0 && result.Errors.Count > 0) result.Errors.Clear(); } - - //no shipping options loaded - if (result.ShippingOptions.Count == 0 && result.Errors.Count == 0) - result.Errors.Add(_localizationService.GetResource("Checkout.ShippingOptionCouldNotBeLoaded")); + + //no shipping options loaded + if (result.ShippingOptions.Count == 0 && result.Errors.Count == 0) + { + result.Errors.Add(T("Checkout.ShippingOptionCouldNotBeLoaded")); + } return result; } public virtual IList GetAllShippingMethodFilters() { - return _typeFinder.FindClassesOfType(ignoreInactivePlugins: true) + if (_shippingMethodFilterTypes == null) + { + lock (_lock) + { + if (_shippingMethodFilterTypes == null) + { + _shippingMethodFilterTypes = _typeFinder.FindClassesOfType(ignoreInactivePlugins: true) + .ToList(); + } + } + } + + var shippingMethodFilters = _shippingMethodFilterTypes .Select(x => EngineContext.Current.ContainerManager.ResolveUnregistered(x) as IShippingMethodFilter) .ToList(); - } + + return shippingMethodFilters; + } #endregion diff --git a/src/Libraries/SmartStore.Services/SmartStore.Services.csproj b/src/Libraries/SmartStore.Services/SmartStore.Services.csproj index 2de19dd74f..1edfe58738 100644 --- a/src/Libraries/SmartStore.Services/SmartStore.Services.csproj +++ b/src/Libraries/SmartStore.Services/SmartStore.Services.csproj @@ -62,8 +62,9 @@ MinimumRecommendedRules.ruleset - - ..\..\packages\Autofac.3.4.1\lib\net40\Autofac.dll + + ..\..\packages\Autofac.3.5.2\lib\net40\Autofac.dll + True ..\..\packages\CronExpressionDescriptor.1.17.0\lib\net45\CronExpressionDescriptor.dll @@ -80,17 +81,17 @@ False ..\..\packages\EPPlus.4.0.3\lib\net20\EPPlus.dll - - False - ..\..\packages\fasterflect.2.1.3\lib\net40\Fasterflect.dll + + ..\..\packages\ImageResizer.4.0.4\lib\net45\ImageResizer.dll + True - - False - ..\..\packages\ImageResizer.3.4.2\lib\ImageResizer.dll + + ..\..\packages\ImageResizer.Plugins.PrettyGifs.4.0.4\lib\net45\ImageResizer.Plugins.PrettyGifs.dll + True - - False - ..\..\packages\ImageResizer.Plugins.PrettyGifs.3.4.2\lib\ImageResizer.Plugins.PrettyGifs.dll + + ..\..\packages\LumenWorksCsvReader.3.9\lib\net20\LumenWorks.Framework.IO.dll + True ..\..\packages\MaxMind.GeoIP.2.1.17\lib\net35\MaxMind.GeoIP.dll @@ -103,9 +104,9 @@ ..\..\packages\ncrontab.2.0.0\lib\net20\NCrontab.dll - - False - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True False @@ -153,8 +154,9 @@ - - ..\..\packages\UAParser.1.2.0.0\lib\net35-Client\UAParser.dll + + ..\..\packages\UAParser.2.0.0.0\lib\net40-Client\UAParser.dll + True @@ -164,45 +166,77 @@ Properties\AssemblyVersionInfo.cs + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + - - - + + + + + + @@ -298,7 +332,6 @@ - @@ -332,7 +365,6 @@ - @@ -351,7 +383,6 @@ - @@ -572,6 +603,7 @@ Nop_Services_EuropaCheckVatService_checkVatService + diff --git a/src/Libraries/SmartStore.Services/Stores/IStoreMappingService.cs b/src/Libraries/SmartStore.Services/Stores/IStoreMappingService.cs index bd5c339ad3..ee938acb15 100644 --- a/src/Libraries/SmartStore.Services/Stores/IStoreMappingService.cs +++ b/src/Libraries/SmartStore.Services/Stores/IStoreMappingService.cs @@ -46,7 +46,7 @@ public partial interface IStoreMappingService IQueryable GetStoreMappingsFor(string entityName, int entityId); /// - /// Save the store napping for an entity + /// Save the store mapping for an entity /// /// Entity type /// The entity diff --git a/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs b/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs index da282801ef..c2ccb86f17 100644 --- a/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs +++ b/src/Libraries/SmartStore.Services/Stores/StoreMappingService.cs @@ -141,12 +141,12 @@ public virtual void SaveStoreMappings(T entity, int[] selectedStoreIds) where { if (selectedStoreIds != null && selectedStoreIds.Contains(store.Id)) { - if (existingStoreMappings.Where(sm => sm.StoreId == store.Id).Count() == 0) + if (!existingStoreMappings.Any(x => x.StoreId == store.Id)) InsertStoreMapping(entity, store.Id); } else { - var storeMappingToDelete = existingStoreMappings.Where(sm => sm.StoreId == store.Id).FirstOrDefault(); + var storeMappingToDelete = existingStoreMappings.FirstOrDefault(x => x.StoreId == store.Id); if (storeMappingToDelete != null) DeleteStoreMapping(storeMappingToDelete); } @@ -185,7 +185,7 @@ public virtual void InsertStoreMapping(T entity, int storeId) where T : BaseE int entityId = entity.Id; string entityName = typeof(T).Name; - var storeMapping = new StoreMapping() + var storeMapping = new StoreMapping { EntityId = entityId, EntityName = entityName, @@ -219,7 +219,7 @@ public virtual void UpdateStoreMapping(StoreMapping storeMapping) public virtual int[] GetStoresIdsWithAccess(T entity) where T : BaseEntity, IStoreMappingSupported { if (entity == null) - throw new ArgumentNullException("entity"); + return new int[0]; int entityId = entity.Id; string entityName = typeof(T).Name; diff --git a/src/Libraries/SmartStore.Services/Stores/StoreService.cs b/src/Libraries/SmartStore.Services/Stores/StoreService.cs index 16daad6591..a773079402 100644 --- a/src/Libraries/SmartStore.Services/Stores/StoreService.cs +++ b/src/Libraries/SmartStore.Services/Stores/StoreService.cs @@ -177,6 +177,8 @@ public virtual bool IsStoreDataValid(Store store) { case "www.yourstore.com": case "yourstore.com": + case "www.mystore.com": + case "mystore.com": case "www.mein-shop.de": case "mein-shop.de": return false; diff --git a/src/Libraries/SmartStore.Services/Tasks/CronExpression.cs b/src/Libraries/SmartStore.Services/Tasks/CronExpression.cs index 25243b0ad5..c35b4b7128 100644 --- a/src/Libraries/SmartStore.Services/Tasks/CronExpression.cs +++ b/src/Libraries/SmartStore.Services/Tasks/CronExpression.cs @@ -59,14 +59,16 @@ public static string GetFriendlyDescription(string expression) Use24HourTimeFormat = Thread.CurrentThread.CurrentUICulture.DateTimeFormat.AMDesignator.IsEmpty() }; - try - { - return ExpressionDescriptor.GetDescription(expression, options); - } - catch + if (expression.HasValue()) { - return null; + try + { + return ExpressionDescriptor.GetDescription(expression, options); + } + catch { } } + + return "?"; } } diff --git a/src/Libraries/SmartStore.Services/Tasks/ITaskExecutor.cs b/src/Libraries/SmartStore.Services/Tasks/ITaskExecutor.cs index b14c0e7c22..053cf45937 100644 --- a/src/Libraries/SmartStore.Services/Tasks/ITaskExecutor.cs +++ b/src/Libraries/SmartStore.Services/Tasks/ITaskExecutor.cs @@ -1,10 +1,14 @@ using System; +using System.Collections.Generic; using SmartStore.Core.Domain.Tasks; namespace SmartStore.Services.Tasks { public interface ITaskExecutor { - void Execute(ScheduleTask task, bool throwOnError = false); + void Execute( + ScheduleTask task, + IDictionary taskParameters = null, + bool throwOnError = false); } } diff --git a/src/Libraries/SmartStore.Services/Tasks/ITaskScheduler.cs b/src/Libraries/SmartStore.Services/Tasks/ITaskScheduler.cs index c1805dc7a8..f852d12952 100644 --- a/src/Libraries/SmartStore.Services/Tasks/ITaskScheduler.cs +++ b/src/Libraries/SmartStore.Services/Tasks/ITaskScheduler.cs @@ -53,7 +53,8 @@ public interface ITaskScheduler /// Executes a single task immediately /// /// - void RunSingleTask(int scheduleTaskId); + /// Optional task parameters + void RunSingleTask(int scheduleTaskId, IDictionary taskParameters = null); /// /// Verifies the authentication token which is generated right before the HTTP endpoint gets called. diff --git a/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs b/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs index e5b6db97de..3329fbf3a7 100644 --- a/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs +++ b/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs @@ -17,24 +17,35 @@ namespace SmartStore.Services.Tasks public class InitializeSchedulerFilter : IAuthorizationFilter { private readonly static object s_lock = new object(); + private static int s_errCount; private static bool s_initializing = false; public void OnAuthorization(AuthorizationContext filterContext) { - lock (s_lock) + if (filterContext == null || filterContext.HttpContext == null) + return; + + var request = filterContext.HttpContext.Request; + if (request == null) + return; + + if (filterContext.IsChildAction) + return; + + lock (s_lock) { if (!s_initializing) { s_initializing = true; - var logger = EngineContext.Current.Resolve(); + ILogger logger = EngineContext.Current.Resolve(); + ITaskScheduler taskScheduler = EngineContext.Current.Resolve(); try { var taskService = EngineContext.Current.Resolve(); var storeService = EngineContext.Current.Resolve(); var eventPublisher = EngineContext.Current.Resolve(); - var taskScheduler = EngineContext.Current.Resolve(); var tasks = taskService.GetAllTasks(true); taskService.CalculateFutureSchedules(tasks, true /* isAppStart */); @@ -59,11 +70,23 @@ public void OnAuthorization(AuthorizationContext filterContext) } catch (Exception ex) { + s_errCount++; + s_initializing = false; logger.Error("Error while initializing Task Scheduler", ex); } finally { - GlobalFilters.Filters.Remove(this); + var tooManyFailures = s_errCount >= 10; + + if (tooManyFailures || (taskScheduler != null && taskScheduler.IsActive)) + { + GlobalFilters.Filters.Remove(this); + } + + if (tooManyFailures && logger != null) + { + logger.Warning("Stopped trying to initialize the Task Scheduler: too many failed attempts in succession (10+). Maybe uncommenting the setting 'sm:TaskSchedulerBaseUrl' in web.config solves the problem?"); + } } } } diff --git a/src/Libraries/SmartStore.Services/Tasks/ScheduleTaskService.cs b/src/Libraries/SmartStore.Services/Tasks/ScheduleTaskService.cs index 9569515fc9..83a4a97b75 100644 --- a/src/Libraries/SmartStore.Services/Tasks/ScheduleTaskService.cs +++ b/src/Libraries/SmartStore.Services/Tasks/ScheduleTaskService.cs @@ -237,9 +237,9 @@ public void CalculateFutureSchedules(IEnumerable tasks, bool isApp { try { - var baseTime = _dateTimeHelper.ConvertToUserTime(DateTime.UtcNow); + var baseTime = DateTime.UtcNow; var next = CronExpression.GetNextSchedule(task.CronExpression, baseTime); - return _dateTimeHelper.ConvertToUtcTime(next); + return next; } catch { } } diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs b/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs index f18a23e6ca..9b0f1aaad5 100644 --- a/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs +++ b/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs @@ -21,6 +21,7 @@ internal TaskExecutionContext(IComponentContext componentContext, ScheduleTask o { this._componentContext = componentContext; this._originalTask = originalTask; + this.Parameters = new Dictionary(); } public T Resolve(object key = null) where T : class @@ -45,6 +46,30 @@ public T ResolveNamed(string name) where T : class public ScheduleTask ScheduleTask { get; set; } + public IDictionary Parameters { get; set; } + + /// + /// Persists a task's progress information to the database + /// + /// Progress value (numerator) + /// Progress maximum (denominator) + /// Progress message. Can be null. + /// if true, saves the updated task entity immediately, or lazily with the next database commit otherwise. + public void SetProgress(int value, int maximum, string message, bool immediately = false) + { + if (value == 0 && maximum == 0) + { + SetProgress(null, message, immediately); + } + else + { + float fraction = (float)value / (float)Math.Max(maximum, 1f); + int percentage = (int)Math.Round(fraction * 100f, 0); + + SetProgress(Math.Min(Math.Max(percentage, 0), 100), message, immediately); + } + } + /// /// Persists a task's progress information to the database /// @@ -75,20 +100,5 @@ public void SetProgress(int? progress, string message, bool immediately = false catch { } } } - - /// - /// Persists a task's progress information to the database - /// - /// Progress numerator - /// Progress denominator - /// Progress message. Can be null. - /// if true, saves the updated task entity immediately, or lazily with the next database commit otherwise. - public void SetProgress(int numerator, int denominator, string message, bool immediately = false) - { - float fraction = (float)numerator / (float)Math.Max(denominator, 1f); - int percentage = (int)Math.Round(fraction * 100f, 0); - - SetProgress(Math.Min(Math.Max(percentage, 0), 100), message, immediately); - } } } diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs b/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs index 92e7fa4668..8fd93c2956 100644 --- a/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs +++ b/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs @@ -1,20 +1,22 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Threading; using Autofac; +using SmartStore.Core; using SmartStore.Core.Async; using SmartStore.Core.Data; +using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Tasks; +using SmartStore.Core.Localization; using SmartStore.Core.Logging; using SmartStore.Core.Plugins; using SmartStore.Services.Customers; -using SmartStore.Core; -using SmartStore.Core.Domain.Customers; -using System.Diagnostics; namespace SmartStore.Services.Tasks { - public class TaskExecutor : ITaskExecutor + public class TaskExecutor : ITaskExecutor { private readonly IScheduleTaskService _scheduledTaskService; private readonly IDbContext _dbContext; @@ -23,6 +25,8 @@ public class TaskExecutor : ITaskExecutor private readonly Func _taskResolver; private readonly IComponentContext _componentContext; + public const string CurrentCustomerIdParamName = "CurrentCustomerId"; + public TaskExecutor( IScheduleTaskService scheduledTaskService, IDbContext dbContext, @@ -39,11 +43,16 @@ public TaskExecutor( this._taskResolver = taskResolver; Logger = NullLogger.Instance; - } + T = NullLocalizer.Instance; + } public ILogger Logger { get; set; } + public Localizer T { get; set; } - public void Execute(ScheduleTask task, bool throwOnError = false) + public void Execute( + ScheduleTask task, + IDictionary taskParameters = null, + bool throwOnError = false) { if (task.IsRunning) return; @@ -78,8 +87,20 @@ public void Execute(ScheduleTask task, bool throwOnError = false) try { - // set background task system customer as current customer - var customer = _customerService.GetCustomerBySystemName(SystemCustomerNames.BackgroundTask); + Customer customer = null; + + // try virtualize current customer (which is necessary when user manually executes a task) + if (taskParameters != null && taskParameters.ContainsKey(CurrentCustomerIdParamName)) + { + customer = _customerService.GetCustomerById(taskParameters[CurrentCustomerIdParamName].ToInt()); + } + + if (customer == null) + { + // no virtualization: set background task system customer as current customer + customer = _customerService.GetCustomerBySystemName(SystemCustomerNames.BackgroundTask); + } + _workContext.CurrentCustomer = customer; // create task instance @@ -102,17 +123,23 @@ public void Execute(ScheduleTask task, bool throwOnError = false) var ctx = new TaskExecutionContext(_componentContext, task) { ScheduleTask = task.Clone(), - CancellationToken = cts.Token + CancellationToken = cts.Token, + Parameters = taskParameters ?? new Dictionary() }; instance.Execute(ctx); } - catch (Exception ex) + catch (Exception exception) { faulted = true; - canceled = ex is OperationCanceledException; - Logger.Error(string.Format("Error while running scheduled task '{0}'. {1}", task.Name, ex.Message), ex); - lastError = ex.Message.Truncate(995, "..."); + canceled = exception is OperationCanceledException; + lastError = exception.Message.Truncate(995, "..."); + + if (canceled) + Logger.Warning(T("Admin.System.ScheduleTasks.Cancellation", task.Name), exception); + else + Logger.Error(string.Concat(T("Admin.System.ScheduleTasks.RunningError", task.Name), ": ", exception.Message), exception); + if (throwOnError) { throw; diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs b/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs index 30eb8b8288..0e055029a3 100644 --- a/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs +++ b/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs @@ -10,10 +10,13 @@ using SmartStore.Core.Async; using SmartStore.Core.Domain.Tasks; using SmartStore.Core.Logging; +using SmartStore.Collections; +using SmartStore.Core.Infrastructure; +using SmartStore.Core.Caching; +using SmartStore.Core; namespace SmartStore.Services.Tasks { - public class DefaultTaskScheduler : DisposableObject, ITaskScheduler, IRegisteredObject { private bool _intervalFixed; @@ -22,7 +25,6 @@ public class DefaultTaskScheduler : DisposableObject, ITaskScheduler, IRegistere private System.Timers.Timer _timer; private bool _shuttingDown; private int _errCount; - private readonly ConcurrentDictionary _authTokens = new ConcurrentDictionary(); public DefaultTaskScheduler() { @@ -47,7 +49,7 @@ public string BaseUrl { CheckUrl(value); _baseUrl = value.TrimEnd('/', '\\'); - } + } } public void Start() @@ -92,18 +94,49 @@ public CancellationTokenSource GetCancelTokenSourceFor(int scheduleTaskId) return cts; } + private string CreateAuthToken() + { + string authToken = Guid.NewGuid().ToString(); + + var cacheManager = EngineContext.Current.Resolve("static"); + cacheManager.Set(GenerateAuthTokenCacheKey(authToken), true, 1); + + return authToken; + } + + private string GenerateAuthTokenCacheKey(string authToken) + { + return "Scheduler.AuthToken." + authToken; + } + public bool VerifyAuthToken(string authToken) { if (authToken.IsEmpty()) return false; - bool val = false; - return _authTokens.TryRemove(authToken, out val); + var cacheManager = EngineContext.Current.Resolve("static"); + var cacheKey = GenerateAuthTokenCacheKey(authToken); + if (cacheManager.Contains(cacheKey)) + { + cacheManager.Remove(cacheKey); + return true; + } + + return false; } - public void RunSingleTask(int scheduleTaskId) + public void RunSingleTask(int scheduleTaskId, IDictionary taskParameters = null) { - CallEndpoint(_baseUrl + "/Execute/{0}".FormatInvariant(scheduleTaskId)); + string query = ""; + + if (taskParameters != null && taskParameters.Any()) + { + var qs = new QueryString(); + taskParameters.Each(x => qs.Add(x.Key, x.Value)); + query = qs.ToString(); + } + + CallEndpoint(new Uri("{0}/Execute/{1}{2}".FormatInvariant(_baseUrl, scheduleTaskId, query))); } private void Elapsed(object sender, System.Timers.ElapsedEventArgs e) @@ -121,7 +154,7 @@ private void Elapsed(object sender, System.Timers.ElapsedEventArgs e) _intervalFixed = true; } - CallEndpoint(_baseUrl + "/Sweep"); + CallEndpoint(new Uri(_baseUrl + "/Sweep")); } } finally @@ -130,26 +163,25 @@ private void Elapsed(object sender, System.Timers.ElapsedEventArgs e) } } - protected internal virtual void CallEndpoint(string url) + protected internal virtual void CallEndpoint(Uri uri) { if (_shuttingDown) return; - - var req = (HttpWebRequest)WebRequest.Create(url); - req.UserAgent = "SmartStore.NET"; - req.Method = "POST"; + + var req = WebHelper.CreateHttpRequestForSafeLocalCall(uri); + req.Method = "POST"; req.ContentType = "text/plain"; req.ContentLength = 0; + req.Timeout = 10000; // 10 sec. - string authToken = Guid.NewGuid().ToString(); - _authTokens.TryAdd(authToken, true); + string authToken = CreateAuthToken(); req.Headers.Add("X-AUTH-TOKEN", authToken); - req.GetResponseAsync().ContinueWith(t => - { + req.GetResponseAsync().ContinueWith(t => + { if (t.IsFaulted) { - HandleException(t.Exception, url); + HandleException(t.Exception, uri); _errCount++; if (_errCount >= 10) { @@ -164,34 +196,48 @@ protected internal virtual void CallEndpoint(string url) else { _errCount = 0; - t.Result.Dispose(); + var response = t.Result; + + //using (var logger = new TraceLogger()) + //{ + // logger.Debug("TaskScheduler Sweep called successfully: {0}".FormatCurrent(response.GetResponseStream().AsString())); + //} + + response.Dispose(); } - }); - } + }); + } - private void HandleException(AggregateException exception, string url) + private void HandleException(AggregateException exception, Uri uri) { using (var logger = new TraceLogger()) { - string msg = "Error while calling TaskScheduler endpoint '{0}'.".FormatInvariant(url); + string msg = "Error while calling TaskScheduler endpoint '{0}'.".FormatInvariant(uri.OriginalString); var wex = exception.InnerExceptions.OfType().FirstOrDefault(); if (wex == null) { - logger.Error(msg, exception); + logger.Error(msg, exception.InnerException); + } + else if (wex.Response == null) + { + logger.Error(msg, wex); } else { using (var response = wex.Response as HttpWebResponse) { - msg += " HTTP {0}, {1}".FormatCurrent((int)response.StatusCode, response.StatusDescription); + if (response != null) + { + msg += " HTTP {0}, {1}".FormatCurrent((int)response.StatusCode, response.StatusDescription); + } logger.Error(msg); } } } } - private void CheckUrl(string url) + private void CheckUrl(string url) { if (!url.IsWebUrl()) { diff --git a/src/Libraries/SmartStore.Services/Topics/TopicService.cs b/src/Libraries/SmartStore.Services/Topics/TopicService.cs index daaa424a3c..d9ffb1a6af 100644 --- a/src/Libraries/SmartStore.Services/Topics/TopicService.cs +++ b/src/Libraries/SmartStore.Services/Topics/TopicService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Stores; using SmartStore.Core.Domain.Topics; @@ -13,23 +14,29 @@ namespace SmartStore.Services.Topics /// public partial class TopicService : ITopicService { - #region Fields + private const string TOPICS_ALL_KEY = "SmartStore.topics.all-{0}"; + private const string TOPICS_PATTERN_KEY = "SmartStore.topics."; - private readonly IRepository _topicRepository; + #region Fields + + private readonly IRepository _topicRepository; private readonly IRepository _storeMappingRepository; private readonly IEventPublisher _eventPublisher; + private readonly ICacheManager _cacheManager; - #endregion + #endregion - #region Ctor + #region Ctor - public TopicService(IRepository topicRepository, + public TopicService(IRepository topicRepository, IRepository storeMappingRepository, - IEventPublisher eventPublisher) + IEventPublisher eventPublisher, + ICacheManager cacheManager) { _topicRepository = topicRepository; _storeMappingRepository = storeMappingRepository; _eventPublisher = eventPublisher; + _cacheManager = cacheManager; this.QuerySettings = DbQuerySettings.Default; } @@ -51,8 +58,10 @@ public virtual void DeleteTopic(Topic topic) _topicRepository.Delete(topic); - //event notification - _eventPublisher.EntityDeleted(topic); + _cacheManager.RemoveByPattern(TOPICS_PATTERN_KEY); + + //event notification + _eventPublisher.EntityDeleted(topic); } /// @@ -79,29 +88,13 @@ public virtual Topic GetTopicBySystemName(string systemName, int storeId) if (String.IsNullOrEmpty(systemName)) return null; - var query = _topicRepository.Table; - query = query.Where(t => t.SystemName == systemName); - query = query.OrderBy(t => t.Id); + var allTopics = GetAllTopics(storeId); - //Store mapping - if (storeId > 0 && !QuerySettings.IgnoreMultiStore) - { - query = from t in query - join sm in _storeMappingRepository.Table - on new { c1 = t.Id, c2 = "Topic" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into t_sm - from sm in t_sm.DefaultIfEmpty() - where !t.LimitedToStores || storeId == sm.StoreId - select t; - - //only distinct items (group by ID) - query = from t in query - group t by t.Id into tGroup - orderby tGroup.Key - select tGroup.FirstOrDefault(); - query = query.OrderBy(t => t.Id); - } - - return query.FirstOrDefault(); + var topic = allTopics + .OrderBy(x => x.Id) + .FirstOrDefault(x => x.SystemName.IsCaseInsensitiveEqual(systemName)); + + return topic; } /// @@ -111,28 +104,33 @@ orderby tGroup.Key /// Topics public virtual IList GetAllTopics(int storeId) { - var query = _topicRepository.Table; - query = query.OrderBy(t => t.Priority).ThenBy(t => t.SystemName); - - //Store mapping - if (storeId > 0 && !QuerySettings.IgnoreMultiStore) + var result = _cacheManager.Get(TOPICS_ALL_KEY.FormatInvariant(storeId), () => { - query = from t in query - join sm in _storeMappingRepository.Table - on new { c1 = t.Id, c2 = "Topic" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into t_sm - from sm in t_sm.DefaultIfEmpty() - where !t.LimitedToStores || storeId == sm.StoreId - select t; - - //only distinct items (group by ID) - query = from t in query - group t by t.Id into tGroup - orderby tGroup.Key - select tGroup.FirstOrDefault(); - query = query.OrderBy(t => t.SystemName); - } - - return query.ToList(); + var query = _topicRepository.Table; + + //Store mapping + if (storeId > 0 && !QuerySettings.IgnoreMultiStore) + { + query = from t in query + join sm in _storeMappingRepository.Table + on new { c1 = t.Id, c2 = "Topic" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into t_sm + from sm in t_sm.DefaultIfEmpty() + where !t.LimitedToStores || storeId == sm.StoreId + select t; + + //only distinct items (group by ID) + query = from t in query + group t by t.Id into tGroup + orderby tGroup.Key + select tGroup.FirstOrDefault(); + } + + query = query.OrderBy(t => t.Priority).ThenBy(t => t.SystemName); + + return query.ToList(); + }); + + return result; } /// @@ -146,8 +144,10 @@ public virtual void InsertTopic(Topic topic) _topicRepository.Insert(topic); - //event notification - _eventPublisher.EntityInserted(topic); + _cacheManager.RemoveByPattern(TOPICS_PATTERN_KEY); + + //event notification + _eventPublisher.EntityInserted(topic); } /// @@ -161,8 +161,10 @@ public virtual void UpdateTopic(Topic topic) _topicRepository.Update(topic); - //event notification - _eventPublisher.EntityUpdated(topic); + _cacheManager.RemoveByPattern(TOPICS_PATTERN_KEY); + + //event notification + _eventPublisher.EntityUpdated(topic); } #endregion diff --git a/src/Libraries/SmartStore.Services/app.config b/src/Libraries/SmartStore.Services/app.config index a376174d13..a9250b2fff 100644 --- a/src/Libraries/SmartStore.Services/app.config +++ b/src/Libraries/SmartStore.Services/app.config @@ -29,7 +29,7 @@ - + diff --git a/src/Libraries/SmartStore.Services/packages.config b/src/Libraries/SmartStore.Services/packages.config index 2419041a2c..d078b22719 100644 --- a/src/Libraries/SmartStore.Services/packages.config +++ b/src/Libraries/SmartStore.Services/packages.config @@ -1,20 +1,20 @@  - + - - - + + + - + - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Description.txt b/src/Plugins/SmartStore.AmazonPay/Description.txt index 383de5f866..8237c4a7ad 100644 --- a/src/Plugins/SmartStore.AmazonPay/Description.txt +++ b/src/Plugins/SmartStore.AmazonPay/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Pay with Amazon SystemName: SmartStore.AmazonPay -Version: 2.2.0.3 +Version: 2.6.0 Group: Payment -MinAppVersion: 2.2.0 +MinAppVersion: 2.5.0 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 63b8f6b811..fbe79d7119 100644 --- a/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml @@ -11,11 +11,11 @@ Registrieren Sie sich zunächst bei Amazon Payments.

    So richten Sie "Bezahlen mit Amazon" ein:

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

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

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

    • Option 1: Bezahlen mit Amazon: Zahlen Sie jetzt mit den Zahl- und Lieferinformationen aus Ihrem Amazon-Konto.
    • Option 2: Sie sind Amazon-Kunde? Zahlen Sie jetzt mit den Zahl- und Lieferinformationen aus Ihrem Amazon-Konto.
    • @@ -179,7 +179,7 @@ Textvorschläge:
        Button im Miniwarenkorb anzeigen - Legt fest, dass der "Bezahlen mit Amazon" Button auch im Miniwarenkorb angezeigt werden soll. + Legt fest, ob der "Bezahlen mit Amazon" Button auch im Miniwarenkorb angezeigt werden soll. Breite des Adressen-Widgets diff --git a/src/Plugins/SmartStore.AmazonPay/Localization/resources.en-us.xml b/src/Plugins/SmartStore.AmazonPay/Localization/resources.en-us.xml index d21b5bc3cb..d5163d3ef3 100644 --- a/src/Plugins/SmartStore.AmazonPay/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.AmazonPay/Localization/resources.en-us.xml @@ -11,11 +11,11 @@ Register now at Amazon Payments.

        How to set up "Pay with Amazon":

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

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

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

        • Option 1: Pay with Amazon: Pay now with the payment and shipping information from your Amazon account.
        • Option 2: Already Amazon customer? Pay now with the payment and shipping information from your Amazon account.
        • @@ -131,7 +131,7 @@ Text suggestions:
            Updating the payment status - Determines the method used to update the payment status. + Specifies the method used to update the payment status. IPN (instant payment notification) requires valid SSL certificate to be installed on this server. Pay attention that the SSL certificate must be issued by a trusted Certificate Authority, self-signed certificates are not permittted. @@ -152,7 +152,7 @@ Text suggestions:
              Payment action - Determines when to debit the customer account. + Specifies when to debit the customer account. Immediately debit @@ -167,7 +167,7 @@ Text suggestions:
                Apply customer data - Determines whether and when the Amazon email address and telephone number of the customer should be saved. + Specifies whether and when the Amazon email address and telephone number of the customer should be saved. Only if empty @@ -179,7 +179,7 @@ Text suggestions:
                  Show button in mini shopping cart - Determines to show the "Pay with Amazon" button in the mini shopping cart too. + Specifies to show the "Pay with Amazon" button in the mini shopping cart too. Width of address widget @@ -218,36 +218,36 @@ Text suggestions:
                    Additional fee percentage - Determines whether to apply a percentage additional fee to the order total. If not enabled, a fixed value is used. + Specifies whether to apply a percentage additional fee to the order total. If not enabled, a fixed value is used. Create order notes - Determines that order notes should be created in context of the data exchange with Amazon Payments. + Specifies that order notes should be created in context of the data exchange with Amazon Payments. Frequency (in minutes) - Determines how often status of the different object shall be polled from Amazon Payments servers. + Specifies how often status of the different object shall be polled from Amazon Payments servers. Maximal order age (in days) - Determines that only orders which are not older than x days to be included in payment data updates. + Specifies that only orders which are not older than x days to be included in payment data updates. Inform about a refusal of an authorization - Determines to create order notes in case of a declination of an Amazon payment, which are visible for customers too. In addition the customer is informed by email about the case. + Specifies to create order notes in case of a declination of an Amazon payment, which are visible for customers too. In addition the customer is informed by email about the case. Append error message - Determines to append the error message to the order note and email. + Specifies to append the error message to the order note and email. \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Models/AmazonPayViewModel.cs b/src/Plugins/SmartStore.AmazonPay/Models/AmazonPayViewModel.cs index 8fce222739..066e9dd9db 100644 --- a/src/Plugins/SmartStore.AmazonPay/Models/AmazonPayViewModel.cs +++ b/src/Plugins/SmartStore.AmazonPay/Models/AmazonPayViewModel.cs @@ -1,6 +1,6 @@ using System; using SmartStore.AmazonPay.Services; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.AmazonPay.Models { diff --git a/src/Plugins/SmartStore.AmazonPay/Models/ConfigurationModel.cs b/src/Plugins/SmartStore.AmazonPay/Models/ConfigurationModel.cs index fd1775b85a..aa40e32f0f 100644 --- a/src/Plugins/SmartStore.AmazonPay/Models/ConfigurationModel.cs +++ b/src/Plugins/SmartStore.AmazonPay/Models/ConfigurationModel.cs @@ -3,7 +3,7 @@ using SmartStore.AmazonPay.Services; using SmartStore.AmazonPay.Settings; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.AmazonPay.Models { diff --git a/src/Plugins/SmartStore.AmazonPay/RouteProvider.cs b/src/Plugins/SmartStore.AmazonPay/RouteProvider.cs index 5f5bbe5f90..a44b2d50f3 100644 --- a/src/Plugins/SmartStore.AmazonPay/RouteProvider.cs +++ b/src/Plugins/SmartStore.AmazonPay/RouteProvider.cs @@ -1,7 +1,7 @@ using System.Web.Mvc; using System.Web.Routing; using SmartStore.AmazonPay.Services; -using SmartStore.Web.Framework.Mvc.Routes; +using SmartStore.Web.Framework.Routing; namespace SmartStore.AmazonPay { diff --git a/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj b/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj index f436a67f06..3cbc8fcb83 100644 --- a/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj +++ b/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj @@ -36,6 +36,7 @@ + true @@ -73,12 +74,11 @@ MinimumRecommendedRules.ruleset - - ..\..\packages\Autofac.3.4.1\lib\net40\Autofac.dll - False + + ..\..\packages\Autofac.3.5.2\lib\net40\Autofac.dll - - ..\..\packages\Autofac.Mvc5.3.3.1\lib\net45\Autofac.Integration.Mvc.dll + + ..\..\packages\Autofac.Mvc5.3.3.4\lib\net45\Autofac.Integration.Mvc.dll ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll diff --git a/src/Plugins/SmartStore.AmazonPay/Views/Web.config b/src/Plugins/SmartStore.AmazonPay/Views/Web.config index 3bd96ceca5..52f7e9b6f7 100644 --- a/src/Plugins/SmartStore.AmazonPay/Views/Web.config +++ b/src/Plugins/SmartStore.AmazonPay/Views/Web.config @@ -14,7 +14,7 @@ - + diff --git a/src/Plugins/SmartStore.AmazonPay/packages.config b/src/Plugins/SmartStore.AmazonPay/packages.config index 504ef05535..dd197b961a 100644 --- a/src/Plugins/SmartStore.AmazonPay/packages.config +++ b/src/Plugins/SmartStore.AmazonPay/packages.config @@ -1,7 +1,7 @@  - - + + diff --git a/src/Plugins/SmartStore.AmazonPay/web.config b/src/Plugins/SmartStore.AmazonPay/web.config index 1e50c947f6..3fd5b09a23 100644 --- a/src/Plugins/SmartStore.AmazonPay/web.config +++ b/src/Plugins/SmartStore.AmazonPay/web.config @@ -1,124 +1,132 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + - \ No newline at end of file + diff --git a/src/Plugins/SmartStore.Clickatell/AdminMenu.cs b/src/Plugins/SmartStore.Clickatell/AdminMenu.cs index f01a091812..27ea086787 100644 --- a/src/Plugins/SmartStore.Clickatell/AdminMenu.cs +++ b/src/Plugins/SmartStore.Clickatell/AdminMenu.cs @@ -1,11 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Web.Routing; -using System.Web.Mvc; +using SmartStore.Collections; using SmartStore.Web.Framework.UI; -using SmartStore.Collections; namespace SmartStore.Clickatell { @@ -15,7 +9,7 @@ protected override void BuildMenuCore(TreeNode pluginsNode) { var menuItem = new MenuItem().ToBuilder() .Text("Clickatell SMS Provider") - .ResKey("Plugins.FriendlyName.Mobile.SMS.Clickatell") + .ResKey("Plugins.FriendlyName.SmartStore.Clickatell") .Icon("send-o") .Action("ConfigurePlugin", "Plugin", new { systemName = "SmartStore.Clickatell", area = "Admin" }) .ToItem(); diff --git a/src/Plugins/SmartStore.Clickatell/ClickatellSmsProvider.cs b/src/Plugins/SmartStore.Clickatell/ClickatellSmsProvider.cs index e5ac70a69e..cd7fca5a22 100644 --- a/src/Plugins/SmartStore.Clickatell/ClickatellSmsProvider.cs +++ b/src/Plugins/SmartStore.Clickatell/ClickatellSmsProvider.cs @@ -1,18 +1,16 @@ using System; using System.ServiceModel; using System.Web.Routing; -using SmartStore.Core; -using SmartStore.Core.Plugins; using SmartStore.Clickatell.Clickatell; -using SmartStore.Services.Common; -using SmartStore.Services.Localization; using SmartStore.Core.Logging; +using SmartStore.Core.Plugins; +using SmartStore.Services.Localization; namespace SmartStore.Clickatell { - /// - /// Represents the Clickatell SMS provider - /// + /// + /// Represents the Clickatell SMS provider + /// public class ClickatellSmsProvider : BasePlugin, IConfigurable { private readonly ILogger _logger; @@ -84,7 +82,6 @@ public void GetConfigurationRoute(out string actionName, out string controllerNa ///
    public override void Install() { - //locales _localizationService.ImportPluginResourcesFromXml(this.PluginDescriptor); base.Install(); @@ -95,9 +92,7 @@ public override void Install() ///
    public override void Uninstall() { - //locales _localizationService.DeleteLocaleStringResources(this.PluginDescriptor.ResourceRootKey); - _localizationService.DeleteLocaleStringResources("Plugins.FriendlyName.Mobile.SMS.Clickatell", false); base.Uninstall(); } diff --git a/src/Plugins/SmartStore.Clickatell/Controllers/SmsClickatellController.cs b/src/Plugins/SmartStore.Clickatell/Controllers/SmsClickatellController.cs index 4b77ca2c8b..82be6e7725 100644 --- a/src/Plugins/SmartStore.Clickatell/Controllers/SmsClickatellController.cs +++ b/src/Plugins/SmartStore.Clickatell/Controllers/SmsClickatellController.cs @@ -6,6 +6,8 @@ using SmartStore.Services.Configuration; using SmartStore.Services.Localization; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Filters; +using SmartStore.Web.Framework.Security; namespace SmartStore.Clickatell.Controllers { diff --git a/src/Plugins/SmartStore.Clickatell/Description.txt b/src/Plugins/SmartStore.Clickatell/Description.txt index 9b5152e1bc..177b509843 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: 2.2.0 -MinAppVersion: 2.2.0 +Version: 2.6.0 +MinAppVersion: 2.5.0 DisplayOrder: 1 FileName: SmartStore.Clickatell.dll ResourceRootKey: Plugins.Sms.Clickatell \ No newline at end of file diff --git a/src/Plugins/SmartStore.Clickatell/Localization/resources.de-de.xml b/src/Plugins/SmartStore.Clickatell/Localization/resources.de-de.xml index 33f5697b26..6a88b543c5 100644 --- a/src/Plugins/SmartStore.Clickatell/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.Clickatell/Localization/resources.de-de.xml @@ -1,5 +1,5 @@  - + Clickatell SMS-Anbieter diff --git a/src/Plugins/SmartStore.Clickatell/Localization/resources.en-us.xml b/src/Plugins/SmartStore.Clickatell/Localization/resources.en-us.xml index 428c16bbe2..4a42186f01 100644 --- a/src/Plugins/SmartStore.Clickatell/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.Clickatell/Localization/resources.en-us.xml @@ -1,5 +1,5 @@  - + Clickatell SMS Provider diff --git a/src/Plugins/SmartStore.Clickatell/RouteProvider.cs b/src/Plugins/SmartStore.Clickatell/RouteProvider.cs index ef267ba987..956b559e91 100644 --- a/src/Plugins/SmartStore.Clickatell/RouteProvider.cs +++ b/src/Plugins/SmartStore.Clickatell/RouteProvider.cs @@ -1,6 +1,6 @@ using System.Web.Mvc; using System.Web.Routing; -using SmartStore.Web.Framework.Mvc.Routes; +using SmartStore.Web.Framework.Routing; namespace SmartStore.Clickatell { diff --git a/src/Plugins/SmartStore.Clickatell/Views/SmsClickatell/Configure.cshtml b/src/Plugins/SmartStore.Clickatell/Views/SmsClickatell/Configure.cshtml index adee89c127..03b16cb275 100644 --- a/src/Plugins/SmartStore.Clickatell/Views/SmsClickatell/Configure.cshtml +++ b/src/Plugins/SmartStore.Clickatell/Views/SmsClickatell/Configure.cshtml @@ -1,8 +1,8 @@ -@{ - Layout = ""; -} +@using SmartStore.Web.Framework; @model SmartStore.Clickatell.Models.SmsClickatellModel -@using SmartStore.Web.Framework; +@{ + Layout = ""; +}
    @@ -69,8 +69,8 @@ -
    -

    @T("Plugins.Sms.Clickatell.SendTest.Hint")

    +
    +
    @T("Plugins.Sms.Clickatell.SendTest.Hint")
    diff --git a/src/Plugins/SmartStore.Clickatell/Views/Web.config b/src/Plugins/SmartStore.Clickatell/Views/Web.config index ab3e708b7d..31b7dee187 100644 --- a/src/Plugins/SmartStore.Clickatell/Views/Web.config +++ b/src/Plugins/SmartStore.Clickatell/Views/Web.config @@ -14,7 +14,7 @@ - + diff --git a/src/Plugins/SmartStore.Clickatell/web.config b/src/Plugins/SmartStore.Clickatell/web.config index 46b8ba77d4..ba87d0f098 100644 --- a/src/Plugins/SmartStore.Clickatell/web.config +++ b/src/Plugins/SmartStore.Clickatell/web.config @@ -1,117 +1,117 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - \ 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 d83af505bb..5700d55470 100644 --- a/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs +++ b/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs @@ -2,6 +2,7 @@ using SmartStore.DevTools.Models; using SmartStore.Services; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.Settings; namespace SmartStore.DevTools.Controllers diff --git a/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs b/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs index 91f0a57419..4e4cb6d96d 100644 --- a/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs +++ b/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs @@ -1,12 +1,5 @@ -using System; -using System.Linq; -using System.Linq.Expressions; -using System.Web.Mvc; -using System.Reflection; using Autofac; -using Autofac.Core; using Autofac.Integration.Mvc; -using SmartStore.Core.Plugins; using SmartStore.Core.Data; using SmartStore.Core.Infrastructure; using SmartStore.Core.Infrastructure.DependencyManagement; @@ -16,13 +9,13 @@ namespace SmartStore.DevTools { - public class DependencyRegistrar : IDependencyRegistrar + public class DependencyRegistrar : IDependencyRegistrar { public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, bool isActiveModule) { builder.RegisterType().As().InstancePerRequest(); - if (isActiveModule) + if (isActiveModule && DataSettings.DatabaseIsInstalled()) { // intercept ALL public store controller actions builder.RegisterType().AsActionFilterFor(); diff --git a/src/Plugins/SmartStore.DevTools/Description.txt b/src/Plugins/SmartStore.DevTools/Description.txt index c2fd9f3fd6..b169d8b16a 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: 2.2.0 -MinAppVersion: 2.2.0 +Version: 2.6.0 +MinAppVersion: 2.5.0 DisplayOrder: 1 FileName: SmartStore.DevTools.dll ResourceRootKey: Plugins.Developer.DevTools \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleResultFilter.cs b/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleResultFilter.cs index 4c565260c9..941ef81a1a 100644 --- a/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleResultFilter.cs +++ b/src/Plugins/SmartStore.DevTools/Filters/Samples/SampleResultFilter.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using System.Web; using System.Web.Mvc; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.DevTools.Filters { diff --git a/src/Plugins/SmartStore.DevTools/Models/BackendExtensionModel.cs b/src/Plugins/SmartStore.DevTools/Models/BackendExtensionModel.cs index d8ecaa66c0..2db540028f 100644 --- a/src/Plugins/SmartStore.DevTools/Models/BackendExtensionModel.cs +++ b/src/Plugins/SmartStore.DevTools/Models/BackendExtensionModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.DevTools.Models { diff --git a/src/Plugins/SmartStore.DevTools/ProfilerHttpModule.cs b/src/Plugins/SmartStore.DevTools/ProfilerHttpModule.cs index e1c5693558..c2e585d0eb 100644 --- a/src/Plugins/SmartStore.DevTools/ProfilerHttpModule.cs +++ b/src/Plugins/SmartStore.DevTools/ProfilerHttpModule.cs @@ -1,5 +1,6 @@ using System; using System.Web; +using SmartStore.Core.Data; using SmartStore.Core.Infrastructure; using StackExchange.Profiling; @@ -43,6 +44,11 @@ private static bool ShouldProfile(HttpApplication app) if (app.Context == null || app.Context.Request == null) return false; + if (!DataSettings.DatabaseIsInstalled()) + { + return false; + } + var url = app.Context.Request.AppRelativeCurrentExecutionFilePath; if (url.StartsWith("~/admin", StringComparison.InvariantCultureIgnoreCase) || url.StartsWith("~/mini-profiler", StringComparison.InvariantCultureIgnoreCase) || url.StartsWith("~/bundles", StringComparison.InvariantCultureIgnoreCase)) { diff --git a/src/Plugins/SmartStore.DevTools/RouteProvider.cs b/src/Plugins/SmartStore.DevTools/RouteProvider.cs index 004bea97c8..607fff468d 100644 --- a/src/Plugins/SmartStore.DevTools/RouteProvider.cs +++ b/src/Plugins/SmartStore.DevTools/RouteProvider.cs @@ -1,6 +1,6 @@ using System.Web.Mvc; using System.Web.Routing; -using SmartStore.Web.Framework.Mvc.Routes; +using SmartStore.Web.Framework.Routing; namespace SmartStore.DevTools { diff --git a/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj b/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj index bb3a9cace1..c8f46ee502 100644 --- a/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj +++ b/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj @@ -35,6 +35,7 @@ + true @@ -74,11 +75,11 @@ MinimumRecommendedRules.ruleset - - ..\..\packages\Autofac.3.4.1\lib\net40\Autofac.dll + + ..\..\packages\Autofac.3.5.2\lib\net40\Autofac.dll - - ..\..\packages\Autofac.Mvc5.3.3.1\lib\net45\Autofac.Integration.Mvc.dll + + ..\..\packages\Autofac.Mvc5.3.3.4\lib\net45\Autofac.Integration.Mvc.dll False diff --git a/src/Plugins/SmartStore.DevTools/Views/Web.config b/src/Plugins/SmartStore.DevTools/Views/Web.config index d6ec073c8e..1812533ab9 100644 --- a/src/Plugins/SmartStore.DevTools/Views/Web.config +++ b/src/Plugins/SmartStore.DevTools/Views/Web.config @@ -14,7 +14,7 @@ - + diff --git a/src/Plugins/SmartStore.DevTools/Web.config b/src/Plugins/SmartStore.DevTools/Web.config index e0593e522a..b076d239ac 100644 --- a/src/Plugins/SmartStore.DevTools/Web.config +++ b/src/Plugins/SmartStore.DevTools/Web.config @@ -1,135 +1,135 @@ - + -
    +
    - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - + - \ No newline at end of file + diff --git a/src/Plugins/SmartStore.DevTools/packages.config b/src/Plugins/SmartStore.DevTools/packages.config index 8a4fe05ffb..325d93b6d4 100644 --- a/src/Plugins/SmartStore.DevTools/packages.config +++ b/src/Plugins/SmartStore.DevTools/packages.config @@ -1,7 +1,7 @@  - - + + diff --git a/src/Plugins/SmartStore.DiscountRules/Controllers/DiscountRulesController.cs b/src/Plugins/SmartStore.DiscountRules/Controllers/DiscountRulesController.cs index 2892c3c0b3..7e6605a2f3 100644 --- a/src/Plugins/SmartStore.DiscountRules/Controllers/DiscountRulesController.cs +++ b/src/Plugins/SmartStore.DiscountRules/Controllers/DiscountRulesController.cs @@ -9,6 +9,7 @@ using SmartStore.Services.Discounts; using SmartStore.Services.Stores; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Security; namespace SmartStore.DiscountRules.Controllers { diff --git a/src/Plugins/SmartStore.DiscountRules/Description.txt b/src/Plugins/SmartStore.DiscountRules/Description.txt index a8f07ab2e8..f4bb29d728 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: 2.2.0 -MinAppVersion: 2.2.0 +Version: 2.6.0 +MinAppVersion: 2.5.0 DisplayOrder: 0 FileName: SmartStore.DiscountRules.dll ResourceRootKey: Plugins.SmartStore.DiscountRules diff --git a/src/Plugins/SmartStore.DiscountRules/Providers/HadSpentAmountRule.cs b/src/Plugins/SmartStore.DiscountRules/Providers/HadSpentAmountRule.cs index b67d2a7efd..69d7f2614b 100644 --- a/src/Plugins/SmartStore.DiscountRules/Providers/HadSpentAmountRule.cs +++ b/src/Plugins/SmartStore.DiscountRules/Providers/HadSpentAmountRule.cs @@ -1,15 +1,16 @@ using System; +using System.Collections.Generic; using System.Linq; +using Newtonsoft.Json; using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Logging; using SmartStore.Core.Plugins; -using SmartStore.Services.Discounts; +using SmartStore.DiscountRules.Settings; +using SmartStore.Services.Catalog; using SmartStore.Services.Customers; +using SmartStore.Services.Discounts; using SmartStore.Services.Orders; -using SmartStore.Services.Catalog; using SmartStore.Services.Tax; -using SmartStore.Core.Localization; -using SmartStore.DiscountRules.Settings; -using Newtonsoft.Json; namespace SmartStore.DiscountRules { @@ -21,15 +22,18 @@ public partial class HadSpentAmountRule : DiscountRequirementRuleBase private readonly IOrderService _orderService; private readonly IPriceCalculationService _priceCalculationService; private readonly ITaxService _taxService; + private readonly ILogger _logger; public HadSpentAmountRule( IOrderService orderService, IPriceCalculationService priceCalculationService, - ITaxService taxService) + ITaxService taxService, + ILogger logger) { - this._orderService = orderService; - this._priceCalculationService = priceCalculationService; - this._taxService = taxService; + _orderService = orderService; + _priceCalculationService = priceCalculationService; + _taxService = taxService; + _logger = logger; } public override bool CheckRequirement(CheckDiscountRequirementRequest request) @@ -88,14 +92,34 @@ private bool CheckTotalHistoryRequirement(CheckDiscountRequirementRequest reques private bool CheckCurrentSubTotalRequirement(CheckDiscountRequirementRequest request) { - var cartItems = request.Customer.GetCartItems(ShoppingCartType.ShoppingCart, request.Store.Id); + var spentAmount = decimal.Zero; - decimal spentAmount = decimal.Zero; - decimal taxRate = decimal.Zero; - foreach (var sci in cartItems) + try { - // includeDiscounts == true produces a stack overflow! - spentAmount += sci.Item.Quantity * _taxService.GetProductPrice(sci.Item.Product, _priceCalculationService.GetUnitPrice(sci, false), out taxRate); + var taxRate = decimal.Zero; + var cartItems = request.Customer.GetCartItems(ShoppingCartType.ShoppingCart, request.Store.Id); + + foreach (var cartItem in cartItems) + { + var product = cartItem.Item.Product; + Dictionary mergedValuesClone = null; + + // we must reapply merged values because CheckCurrentSubTotalRequirement uses price calculation and is called by it itself. + // this can cause wrong discount calculation if the cart contains a product several times. + if (product.MergedDataValues != null) + mergedValuesClone = new Dictionary(product.MergedDataValues); + + // includeDiscounts == true produces a stack overflow! + spentAmount += cartItem.Item.Quantity * _taxService.GetProductPrice(product, _priceCalculationService.GetUnitPrice(cartItem, false), out taxRate); + + if (mergedValuesClone != null) + product.MergedDataValues = new Dictionary(mergedValuesClone); + } + } + catch (Exception exception) + { + _logger.Error(exception); + return false; } return spentAmount >= request.DiscountRequirement.SpentAmount; diff --git a/src/Plugins/SmartStore.DiscountRules/RouteProvider.cs b/src/Plugins/SmartStore.DiscountRules/RouteProvider.cs index 996e585ae6..4f6c8fb679 100644 --- a/src/Plugins/SmartStore.DiscountRules/RouteProvider.cs +++ b/src/Plugins/SmartStore.DiscountRules/RouteProvider.cs @@ -1,6 +1,6 @@ using System.Web.Mvc; using System.Web.Routing; -using SmartStore.Web.Framework.Mvc.Routes; +using SmartStore.Web.Framework.Routing; namespace SmartStore.DiscountRules { diff --git a/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj b/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj index 236ce4de62..dc162de8da 100644 --- a/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj +++ b/src/Plugins/SmartStore.DiscountRules/SmartStore.DiscountRules.csproj @@ -42,6 +42,7 @@ + true @@ -84,8 +85,9 @@ ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True @@ -192,6 +194,7 @@ PreserveNewest + Designer PreserveNewest diff --git a/src/Plugins/SmartStore.DiscountRules/Views/Web.config b/src/Plugins/SmartStore.DiscountRules/Views/Web.config index 31dc8df754..e9d36a3c51 100644 --- a/src/Plugins/SmartStore.DiscountRules/Views/Web.config +++ b/src/Plugins/SmartStore.DiscountRules/Views/Web.config @@ -14,7 +14,7 @@ - + diff --git a/src/Plugins/SmartStore.DiscountRules/packages.config b/src/Plugins/SmartStore.DiscountRules/packages.config index 669284b1aa..0325f33297 100644 --- a/src/Plugins/SmartStore.DiscountRules/packages.config +++ b/src/Plugins/SmartStore.DiscountRules/packages.config @@ -4,5 +4,5 @@ - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.DiscountRules/web.config b/src/Plugins/SmartStore.DiscountRules/web.config index 46b8ba77d4..ba87d0f098 100644 --- a/src/Plugins/SmartStore.DiscountRules/web.config +++ b/src/Plugins/SmartStore.DiscountRules/web.config @@ -1,117 +1,117 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - \ No newline at end of file + diff --git a/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs b/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs index 9ac4d52308..5a99884991 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs +++ b/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs @@ -7,6 +7,7 @@ using SmartStore.Services.Security; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.Settings; namespace SmartStore.FacebookAuth.Controllers @@ -137,10 +138,10 @@ private ActionResult LoginInternal(string returnUrl, bool verifyResponse) if (result.Result != null) return result.Result; - return HttpContext.Request.IsAuthenticated ? - new RedirectResult(!string.IsNullOrEmpty(returnUrl) ? returnUrl : "~/") : + return HttpContext.Request.IsAuthenticated ? + RedirectToReferrer(returnUrl, "~/") : new RedirectResult(Url.LogOn(returnUrl)); - } + } public ActionResult Login(string returnUrl) { diff --git a/src/Plugins/SmartStore.FacebookAuth/Core/FacebookOAuth2Client.cs b/src/Plugins/SmartStore.FacebookAuth/Core/FacebookOAuth2Client.cs new file mode 100644 index 0000000000..5f0960101b --- /dev/null +++ b/src/Plugins/SmartStore.FacebookAuth/Core/FacebookOAuth2Client.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Data; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Web; +using System.Web.Script.Serialization; +using DotNetOpenAuth.AspNet; +using DotNetOpenAuth.AspNet.Clients; +using Newtonsoft.Json; + +namespace SmartStore.FacebookAuth.Core +{ + /// + /// + internal class FacebookOAuth2Client : OAuth2Client + { + /// + /// The authorization endpoint. + /// + private const string AuthorizationEndpoint = "https://www.facebook.com/v2.8/dialog/oauth"; + /// + /// The token endpoint. + /// + private const string TokenEndpoint = "https://graph.facebook.com/v2.8/oauth/access_token"; + /// + /// The user info endpoint. + /// + private const string UserInfoEndpoint = "https://graph.facebook.com/v2.8/me"; + /// + /// The app id. + /// + private readonly string _appId; + /// + /// The app secret. + /// + private readonly string _appSecret; + + /// + /// The requested scopes. + /// + private readonly string[] _requestedScopes; + + + /// + /// Creates a new Facebook OAuth2 client, requesting the default "email" scope. + /// + /// The Facebook App Id + /// The Facebook App Secret + public FacebookOAuth2Client(string appId, string appSecret) + : this(appId, appSecret, new[] { "email" }) { } + + /// + /// Creates a new Facebook OAuth2 client. + /// + /// The Facebook App Id + /// The Facebook App Secret + /// One or more requested scopes, passed without the base URI. + public FacebookOAuth2Client(string appId, string appSecret, params string[] requestedScopes) + : base("facebook") + { + if (string.IsNullOrWhiteSpace(appId)) + throw new ArgumentNullException("appId"); + + if (string.IsNullOrWhiteSpace(appSecret)) + throw new ArgumentNullException("appSecret"); + + if (requestedScopes == null) + throw new ArgumentNullException("requestedScopes"); + + if (requestedScopes.Length == 0) + throw new ArgumentException("One or more scopes must be requested.", "requestedScopes"); + + _appId = appId; + _appSecret = appSecret; + _requestedScopes = requestedScopes; + } + + public override void RequestAuthentication(HttpContextBase context, Uri returnUrl) + { + string redirectUrl = this.GetServiceLoginUrl(returnUrl).AbsoluteUri; + context.Response.Redirect(redirectUrl, endResponse: true); + } + + public new AuthenticationResult VerifyAuthentication(HttpContextBase context) + { + throw new NoNullAllowedException(); + } + + public override AuthenticationResult VerifyAuthentication(HttpContextBase context, Uri returnPageUrl) + { + string code = context.Request.QueryString["code"]; + if (string.IsNullOrEmpty(code)) + { + return AuthenticationResult.Failed; + } + + string accessToken = this.QueryAccessToken(returnPageUrl, code); + if (accessToken == null) + { + return AuthenticationResult.Failed; + } + + IDictionary userData = this.GetUserData(accessToken); + if (userData == null) + { + return AuthenticationResult.Failed; + } + + string id = userData["id"]; + string name; + + // Some oAuth providers do not return value for the 'username' attribute. + // In that case, try the 'name' attribute. If it's still unavailable, fall back to 'id' + if (!userData.TryGetValue("username", out name) && !userData.TryGetValue("name", out name)) + { + name = id; + } + + // add the access token to the user data dictionary just in case page developers want to use it + userData["accesstoken"] = accessToken; + + return new AuthenticationResult( + isSuccessful: true, provider: this.ProviderName, providerUserId: id, userName: name, extraData: userData); + } + + protected override Uri GetServiceLoginUrl(Uri returnUrl) + { + var state = string.IsNullOrEmpty(returnUrl.Query) ? string.Empty : returnUrl.Query.Substring(1); + + return BuildUri(AuthorizationEndpoint, new NameValueCollection + { + { "client_id", _appId }, + { "scope", string.Join(" ", _requestedScopes) }, + { "redirect_uri", returnUrl.GetLeftPart(UriPartial.Path) }, + { "state", state }, + }); + } + + protected override IDictionary GetUserData(string accessToken) + { + var uri = BuildUri(UserInfoEndpoint, new NameValueCollection { { "access_token", accessToken } }); + + var webRequest = (HttpWebRequest)WebRequest.Create(uri); + + using (var webResponse = webRequest.GetResponse()) + using (var stream = webResponse.GetResponseStream()) + { + if (stream == null) + return null; + + using (var textReader = new StreamReader(stream)) + { + var json = textReader.ReadToEnd(); + var extraData = JsonConvert.DeserializeObject>(json); + var data = extraData.ToDictionary(x => x.Key, x => x.Value.ToString()); + + data.Add("picture", string.Format("https://graph.facebook.com/{0}/picture", data["id"])); + + return data; + } + } + } + + public string QueryAccessTokenByCode(Uri returnUrl, string authorizationCode) + { + return this.QueryAccessToken(returnUrl, authorizationCode); + } + + protected override string QueryAccessToken(Uri returnUrl, string authorizationCode) + { + var uri = BuildUri(TokenEndpoint, new NameValueCollection + { + { "code", authorizationCode }, + { "client_id", _appId }, + { "client_secret", _appSecret }, + { "redirect_uri", returnUrl.GetLeftPart(UriPartial.Path) }, + }); + + var webRequest = (HttpWebRequest)WebRequest.Create(uri); + string accessToken = null; + HttpWebResponse response = (HttpWebResponse)webRequest.GetResponse(); + + // handle response from FB + // this will not be a url with params like the first request to get the 'code' + Encoding rEncoding = Encoding.GetEncoding(response.CharacterSet); + + using (StreamReader sr = new StreamReader(response.GetResponseStream(), rEncoding)) + { + var serializer = new JavaScriptSerializer(); + var jsonObject = serializer.DeserializeObject(sr.ReadToEnd()); + var jConvert = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(jsonObject)); + + Dictionary desirializedJsonObject = JsonConvert.DeserializeObject>(jConvert.ToString()); + accessToken = desirializedJsonObject["access_token"].ToString(); + } + return accessToken; + } + + private static Uri BuildUri(string baseUri, NameValueCollection queryParameters) + { + var keyValuePairs = queryParameters.AllKeys.Select(k => HttpUtility.UrlEncode(k) + "=" + HttpUtility.UrlEncode(queryParameters[k])); + var qs = String.Join("&", keyValuePairs); + + var builder = new UriBuilder(baseUri) { Query = qs }; + return builder.Uri; + } + + /// + /// Facebook works best when return data be packed into a "state" parameter. + /// This should be called before verifying the request, so that the url is rewritten to support this. + /// + public static void RewriteRequest() + { + var ctx = HttpContext.Current; + + var stateString = HttpUtility.UrlDecode(ctx.Request.QueryString["state"]); + if (stateString == null || !stateString.Contains("__provider__=facebook")) + return; + + var q = HttpUtility.ParseQueryString(stateString); + q.Add(ctx.Request.QueryString); + q.Remove("state"); + + ctx.RewritePath(ctx.Request.Path + "?" + q); + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs b/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs index 7bb6234362..378599e6d5 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs +++ b/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs @@ -2,19 +2,22 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Net; using System.Text; using System.Web; using System.Web.Mvc; using DotNetOpenAuth.AspNet; using DotNetOpenAuth.AspNet.Clients; +using Newtonsoft.Json.Linq; using SmartStore.Core.Domain.Customers; using SmartStore.Services; using SmartStore.Services.Authentication.External; namespace SmartStore.FacebookAuth.Core { - public class FacebookProviderAuthorizer : IOAuthProviderFacebookAuthorizer + public class FacebookProviderAuthorizer : IOAuthProviderFacebookAuthorizer { #region Fields @@ -24,7 +27,7 @@ public class FacebookProviderAuthorizer : IOAuthProviderFacebookAuthorizer private readonly HttpContextBase _httpContext; private readonly ICommonServices _services; - private FacebookClient _facebookApplication; + private FacebookOAuth2Client _facebookApplication; #endregion @@ -47,7 +50,7 @@ public FacebookProviderAuthorizer(IExternalAuthorizer authorizer, #region Utilities - private FacebookClient FacebookApplication + private FacebookOAuth2Client FacebookApplication { get { @@ -55,7 +58,7 @@ private FacebookClient FacebookApplication { var settings = _services.Settings.LoadSetting(_services.StoreContext.CurrentStore.Id); - _facebookApplication = new FacebookClient(settings.ClientKeyIdentifier, settings.ClientSecret); + _facebookApplication = new FacebookOAuth2Client(settings.ClientKeyIdentifier, settings.ClientSecret); } return _facebookApplication; @@ -90,18 +93,47 @@ private AuthorizeState VerifyAuthentication(string returnUrl) } var state = new AuthorizeState(returnUrl, OpenAuthenticationStatus.Error); - var error = authResult.Error != null ? authResult.Error.Message : "Unknown error"; - state.AddError(error); - return state; + + state.AddError(authResult.Error != null + ? authResult.Error.Message + : _services.Localization.GetResource("Admin.Common.UnknownError")); + + return state; } + private string GetEmailFromFacebook(string accessToken) + { + var result = ""; + var webRequest = WebRequest.Create("https://graph.facebook.com/me?fields=email&access_token=" + EscapeUriDataStringRfc3986(accessToken)); + + using (var webResponse = webRequest.GetResponse()) + using (var stream = webResponse.GetResponseStream()) + using (var reader = new StreamReader(stream)) + { + var strResponse = reader.ReadToEnd(); + var info = JObject.Parse(strResponse); + + if (info["email"] != null) + { + result = info["email"].ToString(); + } + } + return result; + } + private void ParseClaims(AuthenticationResult authenticationResult, OAuthAuthenticationParameters parameters) { var claims = new UserClaims(); claims.Contact = new ContactClaims(); - + if (authenticationResult.ExtraData.ContainsKey("username")) + { claims.Contact.Email = authenticationResult.ExtraData["username"]; + } + else + { + claims.Contact.Email = GetEmailFromFacebook(authenticationResult.ExtraData["accesstoken"]); + } claims.Name = new NameClaims(); diff --git a/src/Plugins/SmartStore.FacebookAuth/Description.txt b/src/Plugins/SmartStore.FacebookAuth/Description.txt index f7cc05d671..d60a7a363b 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Description.txt +++ b/src/Plugins/SmartStore.FacebookAuth/Description.txt @@ -1,8 +1,8 @@ FriendlyName: Facebook SystemName: SmartStore.FacebookAuth Group: Security -Version: 2.2.0.1 -MinAppVersion: 2.2.0 +Version: 2.6.0.1 +MinAppVersion: 2.5.0 DisplayOrder: 5 FileName: SmartStore.FacebookAuth.dll ResourceRootKey: Plugins.ExternalAuth.Facebook \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/Localization/resources.de-de.xml b/src/Plugins/SmartStore.FacebookAuth/Localization/resources.de-de.xml index 5fa1ebdcdb..34d1ab5e11 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.FacebookAuth/Localization/resources.de-de.xml @@ -1,5 +1,5 @@  - + Facebook diff --git a/src/Plugins/SmartStore.FacebookAuth/Localization/resources.en-us.xml b/src/Plugins/SmartStore.FacebookAuth/Localization/resources.en-us.xml index b8bb50d689..e1594d213b 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.FacebookAuth/Localization/resources.en-us.xml @@ -1,5 +1,5 @@  - + Facebook diff --git a/src/Plugins/SmartStore.FacebookAuth/Models/ConfigurationModel.cs b/src/Plugins/SmartStore.FacebookAuth/Models/ConfigurationModel.cs index 4676db9b6b..928feab5b2 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Models/ConfigurationModel.cs +++ b/src/Plugins/SmartStore.FacebookAuth/Models/ConfigurationModel.cs @@ -1,5 +1,5 @@ using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.FacebookAuth.Models { diff --git a/src/Plugins/SmartStore.FacebookAuth/RouteProvider.cs b/src/Plugins/SmartStore.FacebookAuth/RouteProvider.cs index 0353105d2b..90465eb41a 100644 --- a/src/Plugins/SmartStore.FacebookAuth/RouteProvider.cs +++ b/src/Plugins/SmartStore.FacebookAuth/RouteProvider.cs @@ -1,7 +1,7 @@ using System.Web.Mvc; using System.Web.Routing; using SmartStore.FacebookAuth.Core; -using SmartStore.Web.Framework.Mvc.Routes; +using SmartStore.Web.Framework.Routing; namespace SmartStore.FacebookAuth { diff --git a/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj b/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj index 9037add387..85992eb451 100644 --- a/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj +++ b/src/Plugins/SmartStore.FacebookAuth/SmartStore.FacebookAuth.csproj @@ -42,6 +42,7 @@ + true @@ -81,11 +82,11 @@ MinimumRecommendedRules.ruleset - - ..\..\packages\Autofac.3.4.1\lib\net40\Autofac.dll + + ..\..\packages\Autofac.3.5.2\lib\net40\Autofac.dll - - ..\..\packages\Autofac.Mvc5.3.3.1\lib\net45\Autofac.Integration.Mvc.dll + + ..\..\packages\Autofac.Mvc5.3.3.4\lib\net45\Autofac.Integration.Mvc.dll False @@ -120,8 +121,9 @@ ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True @@ -178,6 +180,7 @@ Properties\AssemblyVersionInfo.cs + diff --git a/src/Plugins/SmartStore.FacebookAuth/Views/Web.config b/src/Plugins/SmartStore.FacebookAuth/Views/Web.config index 31dc8df754..e9d36a3c51 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Views/Web.config +++ b/src/Plugins/SmartStore.FacebookAuth/Views/Web.config @@ -14,7 +14,7 @@ - + diff --git a/src/Plugins/SmartStore.FacebookAuth/packages.config b/src/Plugins/SmartStore.FacebookAuth/packages.config index 894e492c07..2cd39833bf 100644 --- a/src/Plugins/SmartStore.FacebookAuth/packages.config +++ b/src/Plugins/SmartStore.FacebookAuth/packages.config @@ -1,7 +1,7 @@  - - + + @@ -15,5 +15,5 @@ - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.FacebookAuth/web.config b/src/Plugins/SmartStore.FacebookAuth/web.config index 10ad5c862a..68523a15bc 100644 --- a/src/Plugins/SmartStore.FacebookAuth/web.config +++ b/src/Plugins/SmartStore.FacebookAuth/web.config @@ -1,121 +1,121 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - \ No newline at end of file + diff --git a/src/Plugins/SmartStore.GoogleAnalytics/Controllers/WidgetsGoogleAnalyticsController.cs b/src/Plugins/SmartStore.GoogleAnalytics/Controllers/WidgetsGoogleAnalyticsController.cs index 52a8a60a0d..6e14ec3d1b 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/Controllers/WidgetsGoogleAnalyticsController.cs +++ b/src/Plugins/SmartStore.GoogleAnalytics/Controllers/WidgetsGoogleAnalyticsController.cs @@ -14,6 +14,7 @@ using SmartStore.Services.Orders; using SmartStore.Services.Stores; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.Settings; namespace SmartStore.GoogleAnalytics.Controllers diff --git a/src/Plugins/SmartStore.GoogleAnalytics/Description.txt b/src/Plugins/SmartStore.GoogleAnalytics/Description.txt index 2e32945c39..59bf21b018 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: 2.2.2.2 -MinAppVersion: 2.2.0 +Version: 2.6.0 +MinAppVersion: 2.5.0 DisplayOrder: 1 FileName: SmartStore.GoogleAnalytics.dll ResourceRootKey: Plugins.Widgets.GoogleAnalytics diff --git a/src/Plugins/SmartStore.GoogleAnalytics/Localization/resources.de-de.xml b/src/Plugins/SmartStore.GoogleAnalytics/Localization/resources.de-de.xml index dd63f56b5f..d7f6055f92 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.GoogleAnalytics/Localization/resources.de-de.xml @@ -1,17 +1,18 @@  - + Google Analytics Google Analytics ist ein kostenloser Statistikdienst von Google. Der Dienst erfasst Statistiken über Besucher und Ecommerce-Konversion auf Ihrer Webseite

    +

    Google Analytics ist ein kostenloser Statistikdienst von Google. Der Dienst erfasst Statistiken über Besucher und Ecommerce-Konversion auf Ihrer Webseite.

    Führen Sie die folgenden Schritte aus um Google Analytics auf Ihrer Webseite einzubinden:

      -
    • erstellen Sie hier einen "Google Analytics"-Account und folgen Sie dem Wizard um Ihre Webseite zuzufügen
    • -
    • Kopieren Sie die "Google Analytics"-ID in das entspechende Feld in folgendem Formular
    • -
    • Kopieren Sie den Tracking-Code in die "Tracking-Code"-Box in folgendem Formular
    • -
    • KLicken Sie den "Speichern"-Button und Google Analytics wird in Ihre Webseite integriert
    • +
    • Erstellen Sie hier einen Google-Analytics-Account und folgen Sie dem Wizard um Ihre Webseite zuzufügen.
    • +
    • Kopieren Sie die Google-Analytics-ID in das entspechende Feld in folgendem Formular.
    • +
    • Kopieren Sie den Tracking-Code in die Tracking-Code-Box in folgendem Formular.
    • +
    • Klicken Sie den Speichern-Button.
    • +
    • Aktivieren Sie das Google Analytics Widget unter CMS > Widgets und Google Analytics wird in Ihre Webseite integriert.
    ]]>
    diff --git a/src/Plugins/SmartStore.GoogleAnalytics/Localization/resources.en-us.xml b/src/Plugins/SmartStore.GoogleAnalytics/Localization/resources.en-us.xml index 2e97f01971..d54877d501 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.GoogleAnalytics/Localization/resources.en-us.xml @@ -1,18 +1,18 @@  - + Google Analytics Google Analytics is a free website stats tool from Google. It keeps track of statistics - about the visitors and ecommerce conversion on your website.

    +

    Google Analytics is a free website statistics tool from Google. It keeps track of statistics about the visitors and ecommerce conversion on your website.

    Follow the next steps to enable Google Analytics integration:

      -
    • Create a Google Analytics account and follow the wizard to add your website
    • -
    • Copy the Google Analytics ID into the 'ID' box below
    • -
    • Copy the tracking code from Google Analytics into the 'Tracking Code' box below
    • -
    • Click the 'Save' button below and Google Analytics will be integrated into your store
    • +
    • Create a Google Analytics account and follow the wizard to add your website.
    • +
    • Copy the Google Analytics ID into the ID field below.
    • +
    • Copy the tracking code from Google Analytics into the Tracking Code field below.
    • +
    • Click the Save button below.
    • +
    • Activate the Google Analytics widget under CMS > Widgets to integrate Google Analytics into your store.
    ]]>
    diff --git a/src/Plugins/SmartStore.GoogleAnalytics/Models/ConfigurationModel.cs b/src/Plugins/SmartStore.GoogleAnalytics/Models/ConfigurationModel.cs index a644421c92..61a1f37fda 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/Models/ConfigurationModel.cs +++ b/src/Plugins/SmartStore.GoogleAnalytics/Models/ConfigurationModel.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Web.Mvc; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.GoogleAnalytics.Models { diff --git a/src/Plugins/SmartStore.GoogleAnalytics/RouteProvider.cs b/src/Plugins/SmartStore.GoogleAnalytics/RouteProvider.cs index 73a1a9a68c..02140b8d4e 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/RouteProvider.cs +++ b/src/Plugins/SmartStore.GoogleAnalytics/RouteProvider.cs @@ -1,6 +1,6 @@ using System.Web.Mvc; using System.Web.Routing; -using SmartStore.Web.Framework.Mvc.Routes; +using SmartStore.Web.Framework.Routing; namespace SmartStore.GoogleAnalytics { diff --git a/src/Plugins/SmartStore.GoogleAnalytics/Views/Web.config b/src/Plugins/SmartStore.GoogleAnalytics/Views/Web.config index 31dc8df754..e9d36a3c51 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/Views/Web.config +++ b/src/Plugins/SmartStore.GoogleAnalytics/Views/Web.config @@ -14,7 +14,7 @@ - + diff --git a/src/Plugins/SmartStore.GoogleAnalytics/web.config b/src/Plugins/SmartStore.GoogleAnalytics/web.config index ad003b3e26..c0f82db177 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/web.config +++ b/src/Plugins/SmartStore.GoogleAnalytics/web.config @@ -1,116 +1,116 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - \ No newline at end of file + diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/AdminMenu.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/AdminMenu.cs index b6c3684093..2dbc226f57 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/AdminMenu.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/AdminMenu.cs @@ -7,17 +7,13 @@ public class AdminMenu : AdminMenuProvider { protected override void BuildMenuCore(TreeNode pluginsNode) { - var root = pluginsNode.SelectNode(x => x.Value.Id == "promotion-feeds"); - if (root == null) - return; - var menuItem = new MenuItem().ToBuilder() .Text("Google Merchant Center") .ResKey("Plugins.FriendlyName.SmartStore.GoogleMerchantCenter") .Action("ConfigurePlugin", "Plugin", new { systemName = GoogleMerchantCenterFeedPlugin.SystemName, area = "Admin" }) .ToItem(); - root.Append(menuItem); + pluginsNode.Prepend(menuItem); } } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedGoogleMerchantCenterController.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedGoogleMerchantCenterController.cs index 86eb437175..6c37461c87 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedGoogleMerchantCenterController.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedGoogleMerchantCenterController.cs @@ -7,8 +7,9 @@ using SmartStore.GoogleMerchantCenter.Models; using SmartStore.GoogleMerchantCenter.Providers; using SmartStore.GoogleMerchantCenter.Services; -using SmartStore.Services.DataExchange; +using SmartStore.Services.DataExchange.Export; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Security; using Telerik.Web.Mvc; namespace SmartStore.GoogleMerchantCenter.Controllers @@ -18,12 +19,12 @@ public class FeedGoogleMerchantCenterController : PluginControllerBase { private readonly IGoogleFeedService _googleFeedService; private readonly AdminAreaSettings _adminAreaSettings; - private readonly IExportService _exportService; + private readonly IExportProfileService _exportService; public FeedGoogleMerchantCenterController( IGoogleFeedService googleFeedService, AdminAreaSettings adminAreaSettings, - IExportService exportService) + IExportProfileService exportService) { _googleFeedService = googleFeedService; _adminAreaSettings = adminAreaSettings; @@ -35,21 +36,31 @@ public ActionResult ProductEditTab(int productId) var culture = CultureInfo.InvariantCulture; var model = new GoogleProductModel { ProductId = productId }; var entity = _googleFeedService.GetGoogleProductRecord(productId); + string notSpecified = T("Common.Unspecified"); if (entity != null) { model.Taxonomy = entity.Taxonomy; model.Gender = entity.Gender; model.AgeGroup = entity.AgeGroup; + model.IsAdult = entity.IsAdult; model.Color = entity.Color; model.Size = entity.Size; model.Material = entity.Material; model.Pattern = entity.Pattern; - model.Exporting = entity.Export; + model.Export2 = entity.Export; + model.Multipack2 = entity.Multipack; + model.IsBundle = entity.IsBundle; + model.EnergyEfficiencyClass = entity.EnergyEfficiencyClass; + model.CustomLabel0 = entity.CustomLabel0; + model.CustomLabel1 = entity.CustomLabel1; + model.CustomLabel2 = entity.CustomLabel2; + model.CustomLabel3 = entity.CustomLabel3; + model.CustomLabel4 = entity.CustomLabel4; } else { - model.Exporting = true; + model.Export2 = true; } ViewBag.DefaultCategory = ""; @@ -57,11 +68,16 @@ public ActionResult ProductEditTab(int productId) ViewBag.DefaultSize = ""; ViewBag.DefaultMaterial = ""; ViewBag.DefaultPattern = ""; - ViewBag.DefaultGender = T("Common.Auto"); - ViewBag.DefaultAgeGroup = T("Common.Auto"); + ViewBag.DefaultGender = notSpecified; + ViewBag.DefaultAgeGroup = notSpecified; + ViewBag.DefaultIsAdult = ""; + ViewBag.DefaultMultipack2 = ""; + ViewBag.DefaultIsBundle = ""; + ViewBag.DefaultEnergyEfficiencyClass = notSpecified; + ViewBag.DefaultCustomLabel = ""; // we do not have export profile context here, so we simply use the first profile - var profile = _exportService.GetExportProfilesBySystemName(ProductExportXmlProvider.SystemName).FirstOrDefault(); + var profile = _exportService.GetExportProfilesBySystemName(GmcXmlExportProvider.SystemName).FirstOrDefault(); if (profile != null) { var config = XmlHelper.Deserialize(profile.ProviderConfigData, typeof(ProfileConfigurationModel)) as ProfileConfigurationModel; @@ -73,12 +89,12 @@ public ActionResult ProductEditTab(int productId) ViewBag.DefaultMaterial = config.Material; ViewBag.DefaultPattern = config.Pattern; - if (config.Gender.HasValue() && config.Gender != ProductExportXmlProvider.Unspecified) + if (config.Gender.HasValue() && config.Gender != GmcXmlExportProvider.Unspecified) { ViewBag.DefaultGender = T("Plugins.Feed.Froogle.Gender" + culture.TextInfo.ToTitleCase(config.Gender)); } - if (config.AgeGroup.HasValue() && config.AgeGroup != ProductExportXmlProvider.Unspecified) + if (config.AgeGroup.HasValue() && config.AgeGroup != GmcXmlExportProvider.Unspecified) { ViewBag.DefaultAgeGroup = T("Plugins.Feed.Froogle.AgeGroup" + culture.TextInfo.ToTitleCase(config.AgeGroup)); } @@ -98,6 +114,11 @@ public ActionResult ProductEditTab(int productId) new SelectListItem { Value = "kids", Text = T("Plugins.Feed.Froogle.AgeGroupKids") }, }; + ViewBag.AvailableEnergyEfficiencyClasses = T("Plugins.Feed.Froogle.EnergyEfficiencyClasses").Text + .SplitSafe(",") + .Select(x => new SelectListItem { Value = x, Text = x }) + .ToList(); + var result = PartialView(model); result.ViewData.TemplateInfo = new TemplateInfo { HtmlFieldPrefix = "CustomProperties[GMC]" }; return result; @@ -113,8 +134,9 @@ public ActionResult Configure() { var model = new FeedGoogleMerchantCenterModel(); - model.AvailableGoogleCategories = _googleFeedService.GetTaxonomyList(); model.GridPageSize = _adminAreaSettings.GridPageSize; + model.AvailableGoogleCategories = _googleFeedService.GetTaxonomyList(); + model.EnergyEfficiencyClasses = T("Plugins.Feed.Froogle.EnergyEfficiencyClasses").Text.SplitSafe(","); return View(model); } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/GoogleProductRecordMap.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/GoogleProductRecordMap.cs index 5b4e0817ab..98aaec1aa4 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/GoogleProductRecordMap.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/GoogleProductRecordMap.cs @@ -9,6 +9,14 @@ public GoogleProductRecordMap() { this.ToTable("GoogleProduct"); this.HasKey(x => x.Id); - } + + this.Property(x => x.EnergyEfficiencyClass).HasMaxLength(50); + + this.Property(x => x.CustomLabel0).HasMaxLength(100); + this.Property(x => x.CustomLabel1).HasMaxLength(100); + this.Property(x => x.CustomLabel2).HasMaxLength(100); + this.Property(x => x.CustomLabel3).HasMaxLength(100); + this.Property(x => x.CustomLabel4).HasMaxLength(100); + } } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.Designer.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.Designer.cs new file mode 100644 index 0000000000..652bba012c --- /dev/null +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.Designer.cs @@ -0,0 +1,29 @@ +// +namespace SmartStore.GoogleMerchantCenter.Data.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class IsBundle : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(IsBundle)); + + string IMigrationMetadata.Id + { + get { return "201601061649324_IsBundle"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.cs new file mode 100644 index 0000000000..bc8af7c35e --- /dev/null +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.cs @@ -0,0 +1,34 @@ +namespace SmartStore.GoogleMerchantCenter.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class IsBundle : DbMigration + { + public override void Up() + { + AddColumn("dbo.GoogleProduct", "Multipack", c => c.Int(nullable: false)); + AddColumn("dbo.GoogleProduct", "IsBundle", c => c.Boolean()); + AddColumn("dbo.GoogleProduct", "IsAdult", c => c.Boolean()); + AddColumn("dbo.GoogleProduct", "EnergyEfficiencyClass", c => c.String(maxLength: 50)); + AddColumn("dbo.GoogleProduct", "CustomLabel0", c => c.String(maxLength: 100)); + AddColumn("dbo.GoogleProduct", "CustomLabel1", c => c.String(maxLength: 100)); + AddColumn("dbo.GoogleProduct", "CustomLabel2", c => c.String(maxLength: 100)); + AddColumn("dbo.GoogleProduct", "CustomLabel3", c => c.String(maxLength: 100)); + AddColumn("dbo.GoogleProduct", "CustomLabel4", c => c.String(maxLength: 100)); + } + + public override void Down() + { + DropColumn("dbo.GoogleProduct", "CustomLabel4"); + DropColumn("dbo.GoogleProduct", "CustomLabel3"); + DropColumn("dbo.GoogleProduct", "CustomLabel2"); + DropColumn("dbo.GoogleProduct", "CustomLabel1"); + DropColumn("dbo.GoogleProduct", "CustomLabel0"); + DropColumn("dbo.GoogleProduct", "EnergyEfficiencyClass"); + DropColumn("dbo.GoogleProduct", "IsAdult"); + DropColumn("dbo.GoogleProduct", "IsBundle"); + DropColumn("dbo.GoogleProduct", "Multipack"); + } + } +} diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.resx b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.resx new file mode 100644 index 0000000000..ebf95a7c54 --- /dev/null +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Data/Migrations/201601061649324_IsBundle.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 + + + H4sIAAAAAAAEAM2a227jNhCG7wv0HQRdtUDW8iEF2sDeReIki6BxHETJ3tPS2FGXIlWSCuy+Wi/6SH2Fjs6iJNuSHQe9s0fkNwdS1K+x//37n/GXtU+NNxDS42xiDnp90wDmcNdjq4kZquWnX80vn3/8YXzj+mvjWzZuFI3DmUxOzFelggvLks4r+ET2fM8RXPKl6jnct4jLrWG//5s1GFiACBNZhjF+CpnyfIi/4NcpZw4EKiR0xl2gMrXjFTumGg/EBxkQByam7ROhbMUF9L5yvqIwA+G8EqamwBSI3jVRxDQuqUcwNhvo0jQIY1wRhZFfvEiwleBsZQdoIPR5EwCOWxIqIc3oohjeNrn+MErOKiZmKCeUivsdgYNRWi2rOv2gmpt5NbGeN1h3tYmyjms6MZMaPgruho56AocL1zSqfi+mVERzWlQfp3ms10A9M/bMPcu317A37PV7/TNjGlIVCpgwCJUg9Mx4DBfUc36HzTP/DmzCQkrL6WGCeE0zoAnDCECozRMs06TvMEdLn2dVJ+bTSnOSGtwxNRqaxgM6JwsK+e4p1SvO8iswEESB+0gU5sciBsT1r3mv+EoLt9/lbswzWXPG/U1GwZ2Pt7VpzMj6HthKvU5M/Ggat94a3MySkl+Yh6cATlIi3OsIM3VBnNzN5Qq+Ch4GJ3c05ZSfPh3b+wtO7mSGG1DgQXdyR+kmP7mfOwV+vAuKu+N0vuQzD/GMzT1dcU6BsM534lRAdBDM2YtyMhY+qOAZH4KdYS+B+36wm3XAhTo2vxke1B4+n78fd2DdyauQuRRq4eybduliAB1n3eDpvNrcLJee46Hk2UwpkXLHlvql/x4nS/xkvScLoP0dvgb9d3Y2+Ehnw490NvpIZ+fv7GxsFYKsLtNQFitUUyCatNp88Qc4KhoCa9Ug2VDppqpNpp71JBMXNqjtQhDvhiK8REw3KbvmlPLgCzFvJWo+U/3WFtk/npEgwOqWXgNSi2En7wDTT3Z3KewnDMuRDYo4jzb3hAqOrKByNZIGLtx6QqroPWNBovWdun5t2I6l2rIMmeMdq1EVtsXiZJOjz6m4aPGa1LiYFSdF8W+xHj5OjksDedzNrxA1SvwuRygRDbIa5Vbos23SfNfsklAuQ0rm9qxCLZdRhbU9KZPDZU5ma08p1G6ZU1jbk1I5W8akpvaMRKyWEYmlPaFQomVKYe2w6pnU1NY8M7bnaFJS24TlCx14hVzUaIW5w5ppilFbOu1Ke6IuG8tE/Up7YqYdy6zM1mFfFNpR2xiFucsKZAJSX4DM2oWUakodlBo71KhZZWolax7SYa9oolLbK9qVg4iDrcTBgcThVuLwQOJoK3F0IPF8K/G8iTi2Kg/J6qPaqj2rK+2nqg7YpaiqQ3LvubKqKKhxqmb2d1dr8iYZYhpYqjfPjaSNvZF4PCYCwv6TTiluWlUMmBHmLUGqpENnDvuDYaUd+/9pjVpSurR7f/TDO45eVOC9PcWOL9u1JmPs5cgWI3sjkcwUP/lk/XOZ1r2NeBSq2io8Cqa1A48ilVt+R4Gqbb2jYJXW3VGshvbccbxqC27hdd+lTe23SPKo92q/HQzT22+H5FZrvR1yG1cbb5VA2i2U1oQ7gLCzIZdtolpLruOt3NB/a0TH3ZyD2YMTsocnZI9OyD4/jt2tVVZv03TufrVtfiUKCc+ABccMk/C1gQf3x+qqbWyVfzkfX4P0VgUi+h2dgRPJoQKajbljS54tDuZdjigbUj1ZQBE82MilUN6SOAovOyBl3Pf8RmgY3bL+Atw7Ng9VEKpLKcFfUK1VMrZ2+4+bgHrM43kQfZPvkQKG6UVn85xdhR5187hv64fiNkS0k1LVhVHZKlJfq01OeuCsJSgt3zUEkcxh6hn8gCJMzplN3uCQ2F4k3MOKOJtMfG+H7F8Ivezja4+sBPFlyijmR/8GsaK/g3z+D2Zr4jJAIgAA + + + dbo + + \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt b/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt index 043c9f3476..6f343b6b69 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: 2.2.0.4 -MinAppVersion: 2.2.0 +Version: 2.6.0.1 +MinAppVersion: 2.5.0 Author: SmartStore AG DisplayOrder: 1 FileName: SmartStore.GoogleMerchantCenter.dll diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Domain/GoogleProductRecord.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Domain/GoogleProductRecord.cs index cca0f73827..7064e84880 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Domain/GoogleProductRecord.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Domain/GoogleProductRecord.cs @@ -29,5 +29,16 @@ public GoogleProductRecord() public DateTime UpdatedOnUtc { get; set; } public bool Export { get; set; } - } + + public int Multipack { get; set; } + public bool? IsBundle { get; set; } + public bool? IsAdult { get; set; } + public string EnergyEfficiencyClass { get; set; } + + public string CustomLabel0 { get; set; } + public string CustomLabel1 { get; set; } + public string CustomLabel2 { get; set; } + public string CustomLabel3 { get; set; } + public string CustomLabel4 { get; set; } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs index 5ca8aeb5dc..10b5732247 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Events.cs @@ -4,7 +4,7 @@ using SmartStore.GoogleMerchantCenter.Models; using SmartStore.GoogleMerchantCenter.Services; using SmartStore.Web.Framework.Events; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.GoogleMerchantCenter { @@ -18,19 +18,20 @@ public Events(IGoogleFeedService googleService) { this._googleService = googleService; } - + public void HandleEvent(TabStripCreated eventMessage) { if (eventMessage.TabStripName == "product-edit") { var productId = ((TabbableModel)eventMessage.Model).Id; - eventMessage.ItemFactory.Add().Text("GMC") - .Name("tab-gmc") - .Icon("fa fa-google fa-lg fa-fw") - .LinkHtmlAttributes(new { data_tab_name = "GMC" }) - .Route("SmartStore.GoogleMerchantCenter", new { action = "ProductEditTab", productId = productId }) - .Ajax(); - } + + eventMessage.ItemFactory.Add().Text("GMC") + .Name("tab-gmc") + .Icon("fa fa-google fa-lg fa-fw") + .LinkHtmlAttributes(new { data_tab_name = "GMC" }) + .Route("SmartStore.GoogleMerchantCenter", new { action = "ProductEditTab", productId = productId }) + .Ajax(); + } } public void HandleEvent(ModelBoundEvent eventMessage) @@ -63,8 +64,17 @@ public void HandleEvent(ModelBoundEvent eventMessage) entity.Taxonomy = model.Taxonomy; entity.Material = model.Material; entity.Pattern = model.Pattern; - entity.Export = model.Exporting; + entity.Export = model.Export2; entity.UpdatedOnUtc = utcNow; + entity.Multipack = model.Multipack2 ?? 0; + entity.IsBundle = model.IsBundle; + entity.IsAdult = model.IsAdult; + entity.EnergyEfficiencyClass = model.EnergyEfficiencyClass; + entity.CustomLabel0 = model.CustomLabel0; + entity.CustomLabel1 = model.CustomLabel1; + entity.CustomLabel2 = model.CustomLabel2; + entity.CustomLabel3 = model.CustomLabel3; + entity.CustomLabel4 = model.CustomLabel4; entity.IsTouched = entity.IsTouched(); diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Extensions/MiscExtensions.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Extensions/MiscExtensions.cs index c140111c1e..5a8dd0168d 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Extensions/MiscExtensions.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Extensions/MiscExtensions.cs @@ -9,7 +9,7 @@ public static string XEditableLink(this HtmlHelper hlp, string fieldName, string { string displayText = null; - if (fieldName == "Gender" || fieldName == "AgeGroup" || fieldName == "Exporting") + if (fieldName == "Gender" || fieldName == "AgeGroup" || fieldName == "Export2" || fieldName == "IsBundle" || fieldName == "IsAdult") displayText = "<#= {0}Localize #>".FormatInvariant(fieldName); else displayText = "<#= {0} #>".FormatInvariant(fieldName); @@ -22,12 +22,15 @@ public static string XEditableLink(this HtmlHelper hlp, string fieldName, string return skeleton.FormatInvariant(fieldName, fieldName.ToLower(), type, displayText); } - public static bool IsTouched(this GoogleProductRecord product) + public static bool IsTouched(this GoogleProductRecord p) { - if (product != null) + if (p != null) { - return product.Taxonomy.HasValue() || product.Gender.HasValue() || product.AgeGroup.HasValue() || product.Color.HasValue() || - product.Size.HasValue() || product.Material.HasValue() || product.Pattern.HasValue() || product.ItemGroupId.HasValue() || !product.Export; + return + p.Taxonomy.HasValue() || p.Gender.HasValue() || p.AgeGroup.HasValue() || p.Color.HasValue() || + p.Size.HasValue() || p.Material.HasValue() || p.Pattern.HasValue() || p.ItemGroupId.HasValue() || + !p.Export || p.Multipack != 0 || p.IsBundle.HasValue || p.IsAdult.HasValue || p.EnergyEfficiencyClass.HasValue() || + p.CustomLabel0.HasValue() || p.CustomLabel1.HasValue() || p.CustomLabel2.HasValue() || p.CustomLabel3.HasValue() || p.CustomLabel4.HasValue(); } return false; } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.de-de.xml b/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.de-de.xml index e4aa14c576..17b90e856c 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.de-de.xml @@ -37,24 +37,9 @@ Google-Kategorie - - Geschlecht - - - Altersgruppe - - - Farbe - - - Größe - - - Material - - - Muster - + + Legt die dem Artikel entsprechende Google-Kategorie fest. Zwingend erforderlich. + Zusätzliche Bilder @@ -148,12 +133,6 @@ Muster oder grafisches Druckdesign eines Produktes. Angabe ist z.B. bei Bekleidung sinnvoll. - - Nur online zu kaufen - - - Legt fest, ob Produkte nur online erworben werden können. Aktivieren Sie diese Option nicht, falls Produkte auch in Ihrem Ladengeschäft erhältlich sind. - Leer @@ -196,4 +175,61 @@ Legt fest, ob der Grundpreis eines Produktes exportiert werden soll. + + Multipack + + + Anzahl identischer Produkte in einem händlerdefinierten Multipack. Muss größer 1 sein. + + + Bundle + + + Händlerdefiniertes Produktpaket bestehend aus einem Haupt- und mehreren Zubehörartikel bzw. Add-ons. + + + Nicht jugendfrei + + + Produkt, welches nur für erwachsene Nutzer bestimmt ist. + + + Energieeffizienz + + + Die Energieeffizienzklasse des Produktes gemäß EU-Richtlinie 2010/30/EU. Mögliche Werte sind G, F, E, D, C, B, A, A+, A++, A+++. + + + A+++,A++,A+,A,B,C,D,E,F,G + + + Label 0 + + + Benutzerdefiniertes Label 0. Dient der individuellen Gruppierung von Produkten. + + + Label 1 + + + Benutzerdefiniertes Label 1. Dient der individuellen Gruppierung von Produkten. + + + Label 2 + + + Benutzerdefiniertes Label 2. Dient der individuellen Gruppierung von Produkten. + + + Label 3 + + + Benutzerdefiniertes Label 3. Dient der individuellen Gruppierung von Produkten. + + + Label 4 + + + Benutzerdefiniertes Label 4. Dient der individuellen Gruppierung von Produkten. +
    \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.en-us.xml b/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.en-us.xml index 8e7528f68e..3b75a8c306 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.en-us.xml @@ -9,7 +9,7 @@ Google Merchant Center XML feed
    - Allows you to export product data in the XML feed format of Google Merchant Center (GMC). + Allows to export product data in XML feed format of Google Merchant Center (GMC). @@ -37,24 +37,9 @@ Google Category - - Gender - - - Age group - - - Color - - - Size - - - Material - - - Pattern - + + Specifies the Google category corresponding to the product. Mandatory. + Additional images @@ -148,12 +133,6 @@ The pattern or graphic print featured on a product. Usefull for clothes for instance. - - Only online available - - - Check the box if products can only be purchased online. - Empty @@ -196,4 +175,61 @@ Activate this option if you want to export the base price of the product. + + Multipack + + + Number of identical products in a merchant defined multipack. Must be greater than 1. + + + Bundle + + + Merchant defined product package consisting of one main and several accessories or add-ons. + + + Adult + + + Product, which is intended only for adult users. + + + Energy efficiency + + + The energy efficiency class of the product in accordance with EU directive 2010/30/EU. Possible values are G, F, E, D, C, B, A, A +, A++, A+++. + + + A+++,A++,A+,A,B,C,D,E,F,G + + + Label 0 + + + Custom label 0 serves the individual grouping of products. + + + Label 1 + + + Custom label 1 serves the individual grouping of products. + + + Label 2 + + + Custom label 2 serves the individual grouping of products. + + + Label 3 + + + Custom label 3 serves the individual grouping of products. + + + Label 4 + + + Custom label 4 serves the individual grouping of products. +
    \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Models/FeedGoogleMerchantCenterModel.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Models/FeedGoogleMerchantCenterModel.cs index ec99430213..36c5f02799 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Models/FeedGoogleMerchantCenterModel.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Models/FeedGoogleMerchantCenterModel.cs @@ -1,9 +1,10 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Text; using Newtonsoft.Json; using SmartStore.Core.Domain.Catalog; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.GoogleMerchantCenter.Models { @@ -18,10 +19,12 @@ public string AvailableGoogleCategoriesAsJson { if (AvailableGoogleCategories != null && AvailableGoogleCategories.Length > 0) return JsonConvert.SerializeObject(AvailableGoogleCategories); - return ""; + return "[ ]"; } } + public string[] EnergyEfficiencyClasses { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.SearchProductName")] public string SearchProductName { get; set; } @@ -29,7 +32,6 @@ public string AvailableGoogleCategoriesAsJson public string SearchIsTouched { get; set; } } - public class GoogleProductModel : ModelBase { public int TotalCount { get; set; } @@ -74,35 +76,66 @@ public string ProductTypeLabelHint [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.GoogleCategory")] public string Taxonomy { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.Gender")] + [SmartResourceDisplayName("Plugins.Feed.Froogle.Gender")] public string Gender { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.AgeGroup")] + [SmartResourceDisplayName("Plugins.Feed.Froogle.AgeGroup")] public string AgeGroup { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.Color")] + [SmartResourceDisplayName("Plugins.Feed.Froogle.Color")] public string Color { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.Size")] + [SmartResourceDisplayName("Plugins.Feed.Froogle.Size")] public string Size { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.Material")] + [SmartResourceDisplayName("Plugins.Feed.Froogle.Material")] public string Material { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.Pattern")] + [SmartResourceDisplayName("Plugins.Feed.Froogle.Pattern")] public string Pattern { get; set; } [SmartResourceDisplayName("Common.Export")] public int Export { get; set; } [SmartResourceDisplayName("Common.Export")] - public bool Exporting + public bool Export2 { get { return Export != 0; } set { Export = (value ? 1 : 0); } } + [SmartResourceDisplayName("Plugins.Feed.Froogle.Multipack")] + public int Multipack { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.Multipack")] + public int? Multipack2 + { + get { return Multipack > 0 ? Multipack : (int?)null; } + set { Multipack = (value ?? 0); } + } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.IsBundle")] + public bool? IsBundle { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.IsAdult")] + public bool? IsAdult { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.EnergyEfficiencyClass")] + public string EnergyEfficiencyClass { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.CustomLabel0")] + public string CustomLabel0 { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.CustomLabel1")] + public string CustomLabel1 { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.CustomLabel2")] + public string CustomLabel2 { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.CustomLabel3")] + public string CustomLabel3 { get; set; } + [SmartResourceDisplayName("Plugins.Feed.Froogle.CustomLabel4")] + public string CustomLabel4 { get; set; } + public string GenderLocalize { get; set; } public string AgeGroupLocalize { get; set; } - public string ExportingLocalize { get; set; } + public string Export2Localize { get; set; } + public string IsBundleLocalize { get; set; } + public string IsAdultLocalize { get; set; } } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Models/ProfileConfigurationModel.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Models/ProfileConfigurationModel.cs index 6b96944375..69d81d50dd 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Models/ProfileConfigurationModel.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Models/ProfileConfigurationModel.cs @@ -14,7 +14,6 @@ public class ProfileConfigurationModel public ProfileConfigurationModel() { Condition = "new"; - OnlineOnly = true; AdditionalImages = true; SpecialPrice = true; } @@ -66,9 +65,6 @@ public string AvailableGoogleCategoriesAsJson [SmartResourceDisplayName("Plugins.Feed.Froogle.Pattern")] public string Pattern { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.OnlineOnly")] - public bool OnlineOnly { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.ExpirationDays")] public int ExpirationDays { get; set; } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/ProductExportXmlProvider.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs similarity index 69% rename from src/Plugins/SmartStore.GoogleMerchantCenter/Providers/ProductExportXmlProvider.cs rename to src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs index ed7eb62390..3edd31e361 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/ProductExportXmlProvider.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs @@ -1,7 +1,5 @@ using System; -using System.IO; using System.Linq; -using System.Text; using System.Xml; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.DataExchange; @@ -11,7 +9,7 @@ using SmartStore.Core.Plugins; using SmartStore.GoogleMerchantCenter.Models; using SmartStore.GoogleMerchantCenter.Services; -using SmartStore.Services.DataExchange; +using SmartStore.Services.DataExchange.Export; using SmartStore.Services.Directory; namespace SmartStore.GoogleMerchantCenter.Providers @@ -19,16 +17,16 @@ namespace SmartStore.GoogleMerchantCenter.Providers [SystemName("Feeds.GoogleMerchantCenterProductXml")] [FriendlyName("Google Merchant Center XML product feed")] [DisplayOrder(1)] - [ExportSupporting( - ExportSupport.CreateInitialPublicDeployment, - ExportSupport.ProjectionNoGroupedProducts, - ExportSupport.ProjectionAttributeCombinationAsProduct, - ExportSupport.ProjectionDescription, - ExportSupport.ProjectionUseOwnProductNo, - ExportSupport.ProjectionBrand, - ExportSupport.ProjectionMainPictureUrl, - ExportSupport.ProjectionSpecialPrice)] - public class ProductExportXmlProvider : IExportProvider + [ExportFeatures(Features = + ExportFeatures.CreatesInitialPublicDeployment | + ExportFeatures.CanOmitGroupedProducts | + ExportFeatures.CanProjectAttributeCombinations | + ExportFeatures.CanProjectDescription | + ExportFeatures.UsesSkuAsMpnFallback | + ExportFeatures.OffersBrandFallback | + ExportFeatures.CanIncludeMainPicture | + ExportFeatures.UsesSpecialPrice)] + public class GmcXmlExportProvider : ExportProviderBase { private const string _googleNamespace = "http://base.google.com/ns/1.0"; @@ -36,7 +34,7 @@ public class ProductExportXmlProvider : IExportProvider private readonly IMeasureService _measureService; private readonly MeasureSettings _measureSettings; - public ProductExportXmlProvider( + public GmcXmlExportProvider( IGoogleFeedService googleFeedService, IMeasureService measureService, MeasureSettings measureSettings) @@ -132,7 +130,7 @@ public static string Unspecified get { return "__nospec__"; } } - public ExportConfigurationInfo ConfigurationInfo + public override ExportConfigurationInfo ConfigurationInfo { get { @@ -150,27 +148,16 @@ public ExportConfigurationInfo ConfigurationInfo } } - public ExportEntityType EntityType - { - get { return ExportEntityType.Product; } - } - - public string FileExtension + public override string FileExtension { get { return "XML"; } } - public void Execute(IExportExecuteContext context) + protected override void Export(ExportExecuteContext context) { - var settings = new XmlWriterSettings - { - Encoding = Encoding.UTF8, - CheckCharacters = false - }; - - var path = context.FilePath; dynamic currency = context.Currency; string measureWeightSystemKey = ""; + var dateFormat = "yyyy-MM-ddTHH:mmZ"; var measureWeight = _measureService.GetMeasureWeightById(_measureSettings.BaseWeightId); @@ -179,10 +166,7 @@ public void Execute(IExportExecuteContext context) var config = (context.ConfigurationData as ProfileConfigurationModel) ?? new ProfileConfigurationModel(); - context.Log.Information("Creating file " + path); - - using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite)) - using (var writer = XmlWriter.Create(stream, settings)) + using (var writer = XmlWriter.Create(context.DataStream, ExportXmlHelper.DefaultSettings)) { writer.WriteStartDocument(); writer.WriteStartElement("rss"); @@ -193,20 +177,20 @@ public void Execute(IExportExecuteContext context) writer.WriteElementString("link", "http://base.google.com/base/"); writer.WriteElementString("description", "Information about products"); - while (context.Abort == ExportAbortion.None && context.Segmenter.ReadNextSegment()) + while (context.Abort == DataExchangeAbortion.None && context.DataSegmenter.ReadNextSegment()) { - var segment = context.Segmenter.CurrentSegment; + var segment = context.DataSegmenter.CurrentSegment; int[] productIds = segment.Select(x => (int)((dynamic)x).Id).ToArray(); var googleProducts = _googleFeedService.GetGoogleProductRecords(productIds); foreach (dynamic product in segment) { - if (context.Abort != ExportAbortion.None) + if (context.Abort != DataExchangeAbortion.None) break; - int productId = product.Id; - var gmc = googleProducts.FirstOrDefault(x => x.ProductId == productId); + Product entity = product.Entity; + var gmc = googleProducts.FirstOrDefault(x => x.ProductId == entity.Id); if (gmc != null && !gmc.Export) continue; @@ -218,14 +202,18 @@ public void Execute(IExportExecuteContext context) string category = (gmc == null ? null : gmc.Taxonomy); string productType = product._CategoryPath; string mainImageUrl = product._MainPictureUrl; - var specialPrice = product._SpecialPrice as decimal?; var price = (decimal)product.Price; + var uniqueId = (string)product._UniqueId; string brand = product._Brand; string gtin = product.Gtin; string mpn = product.ManufacturerPartNumber; string condition = "new"; string availability = "in stock"; + var specialPrice = product._FutureSpecialPrice as decimal?; + if (!specialPrice.HasValue) + specialPrice = product._SpecialPrice; + if (category.IsEmpty()) category = config.DefaultGoogleCategory; @@ -251,17 +239,16 @@ public void Execute(IExportExecuteContext context) } else { - if ((int)product.ManageInventoryMethodId == (int)ManageInventoryMethod.ManageStock && (int)product.StockQuantity <= 0) + if (entity.ManageInventoryMethod == ManageInventoryMethod.ManageStock && entity.StockQuantity <= 0) { - var backorderId = (int)product.BackorderModeId; - if (backorderId == (int)BackorderMode.NoBackorders) + if (entity.BackorderMode == BackorderMode.NoBackorders) availability = "out of stock"; - else if (backorderId == (int)BackorderMode.AllowQtyBelow0 || backorderId == (int)BackorderMode.AllowQtyBelow0AndNotifyCustomer) - availability = ((bool)product.AvailableForPreOrder ? "preorder" : "out of stock"); + else if (entity.BackorderMode == BackorderMode.AllowQtyBelow0 || entity.BackorderMode == BackorderMode.AllowQtyBelow0AndNotifyCustomer) + availability = (entity.AvailableForPreOrder ? "preorder" : "out of stock"); } } - writer.WriteElementString("g", "id", _googleNamespace, productId.ToString()); + writer.WriteElementString("g", "id", _googleNamespace, uniqueId); writer.WriteStartElement("title"); writer.WriteCData(((string)product.Name).Truncate(70).RemoveInvalidXmlChars()); @@ -305,20 +292,26 @@ public void Execute(IExportExecuteContext context) writer.WriteElementString("g", "condition", _googleNamespace, condition); writer.WriteElementString("g", "availability", _googleNamespace, availability); + if (availability == "preorder" && entity.AvailableStartDateTimeUtc.HasValue && entity.AvailableStartDateTimeUtc.Value > DateTime.UtcNow) + { + var availabilityDate = entity.AvailableStartDateTimeUtc.Value.ToString(dateFormat); + + writer.WriteElementString("g", "availability_date", _googleNamespace, availabilityDate); + } + if (config.SpecialPrice && specialPrice.HasValue) { - DateTime? specialPriceStart = product.SpecialPriceStartDateTimeUtc; - DateTime? specialPriceEnd = product.SpecialPriceEndDateTimeUtc; - if (specialPriceStart.HasValue && specialPriceEnd.HasValue) + writer.WriteElementString("g", "sale_price", _googleNamespace, specialPrice.Value.FormatInvariant() + " " + (string)currency.CurrencyCode); + + if (entity.SpecialPriceStartDateTimeUtc.HasValue && entity.SpecialPriceEndDateTimeUtc.HasValue) { - var dateFormat = "yyyy-MM-ddTHH:mmZ"; - var specialPriceDate = "{0}/{1}".FormatInvariant(specialPriceStart.Value.ToString(dateFormat), specialPriceEnd.Value.ToString(dateFormat)); + var specialPriceDate = "{0}/{1}".FormatInvariant( + entity.SpecialPriceStartDateTimeUtc.Value.ToString(dateFormat), entity.SpecialPriceEndDateTimeUtc.Value.ToString(dateFormat)); - writer.WriteElementString("g", "sale_price", _googleNamespace, price.FormatInvariant() + " " + (string)currency.CurrencyCode); writer.WriteElementString("g", "sale_price_effective_date", _googleNamespace, specialPriceDate); - - price = (product._RegularPrice as decimal?) ?? price; } + + price = (product._RegularPrice as decimal?) ?? price; } writer.WriteElementString("g", "price", _googleNamespace, price.FormatInvariant() + " " + (string)currency.CurrencyCode); @@ -343,7 +336,6 @@ public void Execute(IExportExecuteContext context) writer.WriteCData("pattern", gmc != null && gmc.Pattern.HasValue() ? gmc.Pattern : config.Pattern, "g", _googleNamespace); writer.WriteCData("item_group_id", gmc != null && gmc.ItemGroupId.HasValue() ? gmc.ItemGroupId : "", "g", _googleNamespace); - writer.WriteElementString("g", "online_only", _googleNamespace, config.OnlineOnly ? "y" : "n"); writer.WriteElementString("g", "identifier_exists", _googleNamespace, gtin.HasValue() || brand.HasValue() || mpn.HasValue() ? "TRUE" : "FALSE"); if (config.ExpirationDays > 0) @@ -368,27 +360,70 @@ public void Execute(IExportExecuteContext context) writer.WriteElementString("g", "shipping_weight", _googleNamespace, weightInfo); } - if (config.ExportBasePrice && (bool)product.BasePriceHasValue) + if (config.ExportBasePrice && entity.BasePriceHasValue) { var measureUnit = BasePriceUnits((string)product.BasePriceMeasureUnit); - var basePriceBaseAmount = product.BasePriceBaseAmount as int?; - if (BasePriceSupported(basePriceBaseAmount ?? 0, measureUnit)) + if (BasePriceSupported(entity.BasePriceBaseAmount ?? 0, measureUnit)) { - var basePriceAmount = product.BasePriceAmount as decimal?; - var basePriceMeasure = "{0} {1}".FormatInvariant((basePriceAmount ?? decimal.Zero).FormatInvariant(), measureUnit); - var basePriceBaseMeasure = "{0} {1}".FormatInvariant(basePriceBaseAmount ?? 1, measureUnit); + var basePriceMeasure = "{0} {1}".FormatInvariant((entity.BasePriceAmount ?? decimal.Zero).FormatInvariant(), measureUnit); + var basePriceBaseMeasure = "{0} {1}".FormatInvariant(entity.BasePriceBaseAmount ?? 1, measureUnit); writer.WriteElementString("g", "unit_pricing_measure", _googleNamespace, basePriceMeasure); writer.WriteElementString("g", "unit_pricing_base_measure", _googleNamespace, basePriceBaseMeasure); } } + if (gmc != null && gmc.Multipack > 1) + { + writer.WriteElementString("g", "multipack", _googleNamespace, gmc.Multipack.ToString()); + } + + if (gmc != null && gmc.IsBundle.HasValue) + { + writer.WriteElementString("g", "is_bundle", _googleNamespace, gmc.IsBundle.Value ? "TRUE" : "FALSE"); + } + + if (gmc != null && gmc.IsAdult.HasValue) + { + writer.WriteElementString("g", "adult", _googleNamespace, gmc.IsAdult.Value ? "TRUE" : "FALSE"); + } + + if (gmc != null && gmc.EnergyEfficiencyClass.HasValue()) + { + writer.WriteElementString("g", "energy_efficiency_class", _googleNamespace, gmc.EnergyEfficiencyClass); + } + + if (gmc != null && gmc.CustomLabel0.HasValue()) + { + writer.WriteElementString("g", "custom_label_0", _googleNamespace, gmc.CustomLabel0); + } + + if (gmc != null && gmc.CustomLabel1.HasValue()) + { + writer.WriteElementString("g", "custom_label_1", _googleNamespace, gmc.CustomLabel1); + } + + if (gmc != null && gmc.CustomLabel2.HasValue()) + { + writer.WriteElementString("g", "custom_label_2", _googleNamespace, gmc.CustomLabel2); + } + + if (gmc != null && gmc.CustomLabel3.HasValue()) + { + writer.WriteElementString("g", "custom_label_3", _googleNamespace, gmc.CustomLabel3); + } + + if (gmc != null && gmc.CustomLabel4.HasValue()) + { + writer.WriteElementString("g", "custom_label_4", _googleNamespace, gmc.CustomLabel4); + } + ++context.RecordsSucceeded; } - catch (Exception exc) + catch (Exception exception) { - context.RecordException(exc, productId); + context.RecordException(exception, entity.Id); } writer.WriteEndElement(); // item @@ -400,10 +435,5 @@ public void Execute(IExportExecuteContext context) writer.WriteEndDocument(); } } - - public void ExecuteEnded(IExportExecuteContext context) - { - // nothing to do - } } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/RouteProvider.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/RouteProvider.cs index 2a58266db7..d0e3b85340 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/RouteProvider.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/RouteProvider.cs @@ -1,6 +1,6 @@ using System.Web.Mvc; using System.Web.Routing; -using SmartStore.Web.Framework.Mvc.Routes; +using SmartStore.Web.Framework.Routing; namespace SmartStore.GoogleMerchantCenter { diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs index 48d574215e..b5fab3af04 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs @@ -125,9 +125,36 @@ public void Upsert(int pk, string name, string value) case "Pattern": product.Pattern = value; break; - case "Exporting": + case "Export2": product.Export = value.ToBool(true); break; + case "Multipack2": + product.Multipack = value.ToInt(); + break; + case "IsBundle": + product.IsBundle = (value.IsEmpty() ? (bool?)null : value.ToBool()); + break; + case "IsAdult": + product.IsAdult = (value.IsEmpty() ? (bool?)null : value.ToBool()); + break; + case "EnergyEfficiencyClass": + product.EnergyEfficiencyClass = value; + break; + case "CustomLabel0": + product.CustomLabel0 = value; + break; + case "CustomLabel1": + product.CustomLabel1 = value; + break; + case "CustomLabel2": + product.CustomLabel2 = value; + break; + case "CustomLabel3": + product.CustomLabel3 = value; + break; + case "CustomLabel4": + product.CustomLabel4 = value; + break; } product.UpdatedOnUtc = utcNow; @@ -156,7 +183,8 @@ public GridModel GetGridModel(GridCommand command, string se string yes = T("Admin.Common.Yes"); string no = T("Admin.Common.No"); - // there's no way to share a context instance across repositories which makes GoogleProductObjectContext pretty useless here. + // there's no way to share a context instance across repositories in EF. + // so we have to fallback to pure SQL here to get the data paged and filtered. var whereClause = new StringBuilder("(NOT ([t2].[Deleted] = 1)) AND ([t2].[VisibleIndividually] = 1)"); @@ -181,11 +209,11 @@ public GridModel GetGridModel(GridCommand command, string se { // fastest possible paged data query sql = - "SELECT [TotalCount], [t3].[Id], [t3].[Name], [t3].[SKU], [t3].[ProductTypeId], [t3].[value] AS [Taxonomy], [t3].[value2] AS [Gender], [t3].[value3] AS [AgeGroup], [t3].[value4] AS [Color], [t3].[value5] AS [Size], [t3].[value6] AS [Material], [t3].[value7] AS [Pattern], [t3].[value8] AS [Export]" + + "SELECT [TotalCount], [t3].[Id], [t3].[Name], [t3].[SKU], [t3].[ProductTypeId], [t3].[value] AS [Taxonomy], [t3].[value2] AS [Gender], [t3].[value3] AS [AgeGroup], [t3].[value4] AS [Color], [t3].[value5] AS [Size], [t3].[value6] AS [Material], [t3].[value7] AS [Pattern], [t3].[value8] AS [Export], [t3].[value9] AS [Multipack], [t3].[value10] AS [IsBundle], [t3].[value11] AS [IsAdult], [t3].[value12] AS [EnergyEfficiencyClass], [t3].[value13] AS [CustomLabel0], [t3].[value14] AS [CustomLabel1], [t3].[value15] AS [CustomLabel2], [t3].[value16] AS [CustomLabel3], [t3].[value17] AS [CustomLabel4]" + " FROM (" + - " SELECT COUNT(id) OVER() [TotalCount], ROW_NUMBER() OVER (ORDER BY [t2].[Name]) AS [ROW_NUMBER], [t2].[Id], [t2].[Name], [t2].[SKU], [t2].[ProductTypeId], [t2].[value], [t2].[value2], [t2].[value3], [t2].[value4], [t2].[value5], [t2].[value6], [t2].[value7], [t2].[value8]" + + " SELECT COUNT(id) OVER() [TotalCount], ROW_NUMBER() OVER (ORDER BY [t2].[Name]) AS [ROW_NUMBER], [t2].[Id], [t2].[Name], [t2].[SKU], [t2].[ProductTypeId], [t2].[value], [t2].[value2], [t2].[value3], [t2].[value4], [t2].[value5], [t2].[value6], [t2].[value7], [t2].[value8], [t2].[value9], [t2].[value10], [t2].[value11], [t2].[value12], [t2].[value13], [t2].[value14], [t2].[value15], [t2].[value16], [t2].[value17]" + " FROM (" + - " SELECT [t0].[Id], [t0].[Name], [t0].[SKU], [t0].[ProductTypeId], [t1].[Taxonomy] AS [value], [t1].[Gender] AS [value2], [t1].[AgeGroup] AS [value3], [t1].[Color] AS [value4], [t1].[Size] AS [value5], [t1].[Material] AS [value6], [t1].[Pattern] AS [value7], COALESCE([t1].[Export],1) AS [value8], [t0].[Deleted], [t0].[VisibleIndividually], [t1].[IsTouched]" + + " SELECT [t0].[Id], [t0].[Name], [t0].[SKU], [t0].[ProductTypeId], [t1].[Taxonomy] AS [value], [t1].[Gender] AS [value2], [t1].[AgeGroup] AS [value3], [t1].[Color] AS [value4], [t1].[Size] AS [value5], [t1].[Material] AS [value6], [t1].[Pattern] AS [value7], COALESCE([t1].[Export],1) AS [value8], COALESCE([t1].[Multipack],0) AS [value9], [t1].[IsBundle] AS [value10], [t1].[IsAdult] AS [value11], [t1].[EnergyEfficiencyClass] AS [value12], [t1].[CustomLabel0] AS [value13], [t1].[CustomLabel1] AS [value14], [t1].[CustomLabel2] AS [value15], [t1].[CustomLabel3] AS [value16], [t1].[CustomLabel4] AS [value17], [t0].[Deleted], [t0].[VisibleIndividually], [t1].[IsTouched]" + " FROM [Product] AS [t0]" + " LEFT OUTER JOIN [GoogleProduct] AS [t1] ON [t0].[Id] = [t1].[ProductId]" + " ) AS [t2]" + @@ -198,9 +226,9 @@ public GridModel GetGridModel(GridCommand command, string se { // OFFSET... FETCH NEXT requires SQL Server 2012 or SQL CE 4 sql = - "SELECT [t2].[Id], [t2].[Name], [t2].[SKU], [t2].[ProductTypeId], [t2].[value] AS [Taxonomy], [t2].[value2] AS [Gender], [t2].[value3] AS [AgeGroup], [t2].[value4] AS [Color], [t2].[value5] AS [Size], [t2].[value6] AS [Material], [t2].[value7] AS [Pattern], [t2].[value8] AS [Export]" + + "SELECT [t2].[Id], [t2].[Name], [t2].[SKU], [t2].[ProductTypeId], [t2].[value] AS [Taxonomy], [t2].[value2] AS [Gender], [t2].[value3] AS [AgeGroup], [t2].[value4] AS [Color], [t2].[value5] AS [Size], [t2].[value6] AS [Material], [t2].[value7] AS [Pattern], [t2].[value8] AS [Export], [t2].[value9] AS [Multipack], [t2].[value10] AS [IsBundle], [t2].[value11] AS [IsAdult], [t2].[value12] AS [EnergyEfficiencyClass], [t2].[value13] AS [CustomLabel0], [t2].[value14] AS [CustomLabel1], [t2].[value15] AS [CustomLabel2], [t2].[value16] AS [CustomLabel3], [t2].[value17] AS [CustomLabel4]" + " FROM (" + - " SELECT [t0].[Id], [t0].[Name], [t0].[SKU], [t0].[ProductTypeId], [t1].[Taxonomy] AS [value], [t1].[Gender] AS [value2], [t1].[AgeGroup] AS [value3], [t1].[Color] AS [value4], [t1].[Size] AS [value5], [t1].[Material] AS [value6], [t1].[Pattern] AS [value7], COALESCE([t1].[Export],1) AS [value8], [t0].[Deleted], [t0].[VisibleIndividually], [t1].[IsTouched] AS [IsTouched]" + + " SELECT [t0].[Id], [t0].[Name], [t0].[SKU], [t0].[ProductTypeId], [t1].[Taxonomy] AS [value], [t1].[Gender] AS [value2], [t1].[AgeGroup] AS [value3], [t1].[Color] AS [value4], [t1].[Size] AS [value5], [t1].[Material] AS [value6], [t1].[Pattern] AS [value7], COALESCE([t1].[Export],1) AS [value8], COALESCE([t1].[Multipack],0) AS [value9], [t1].[IsBundle] AS [value10], [t1].[IsAdult] AS [value11], [t1].[EnergyEfficiencyClass] AS [value12], [t1].[CustomLabel0] AS [value13], [t1].[CustomLabel1] AS [value14], [t1].[CustomLabel2] AS [value15], [t1].[CustomLabel3] AS [value16], [t1].[CustomLabel4] AS [value17], [t0].[Deleted], [t0].[VisibleIndividually], [t1].[IsTouched] AS [IsTouched]" + " FROM [Product] AS [t0]" + " LEFT OUTER JOIN [GoogleProduct] AS [t1] ON [t0].[Id] = [t1].[ProductId]" + " ) AS [t2]" + @@ -232,7 +260,17 @@ public GridModel GetGridModel(GridCommand command, string se if (x.AgeGroup.HasValue()) x.AgeGroupLocalize = T("Plugins.Feed.Froogle.AgeGroup" + textInfo.ToTitleCase(x.AgeGroup)); - x.ExportingLocalize = (x.Export == 0 ? no : yes); + x.Export2Localize = (x.Export == 0 ? no : yes); + + if (x.IsBundle.HasValue) + x.IsBundleLocalize = (x.IsBundle.Value ? yes : no); + else + x.IsBundleLocalize = null; + + if (x.IsAdult.HasValue) + x.IsAdultLocalize = (x.IsAdult.Value ? yes : no); + else + x.IsAdultLocalize = null; }); model.Data = data; diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj b/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj index e571790b5d..f743627997 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj @@ -42,6 +42,7 @@ + true @@ -81,11 +82,11 @@ MinimumRecommendedRules.ruleset - - ..\..\packages\Autofac.3.4.1\lib\net40\Autofac.dll + + ..\..\packages\Autofac.3.5.2\lib\net40\Autofac.dll - - ..\..\packages\Autofac.Mvc5.3.3.1\lib\net45\Autofac.Integration.Mvc.dll + + ..\..\packages\Autofac.Mvc5.3.3.4\lib\net45\Autofac.Integration.Mvc.dll False @@ -95,14 +96,15 @@ False ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll - - ..\..\packages\FluentValidation.5.0.0.1\lib\Net40\FluentValidation.dll + + ..\..\packages\FluentValidation.5.6.2.0\lib\Net45\FluentValidation.dll ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True @@ -168,6 +170,10 @@ 201504211854125_IsActive.cs + + + 201601061649324_IsBundle.cs + @@ -175,7 +181,7 @@ - + @@ -217,6 +223,9 @@ 201504211854125_IsActive.cs + + 201601061649324_IsBundle.cs + diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml index 8d01b7baeb..d91ec16571 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml @@ -3,7 +3,6 @@ @using SmartStore.GoogleMerchantCenter.Models; @using SmartStore.GoogleMerchantCenter.Providers; @using SmartStore.Web.Framework; -@using SmartStore.Web.Framework.Plugins; @using Telerik.Web.Mvc.UI; @using SmartStore.Web.Framework.UI; @@ -22,7 +21,7 @@ @Html.Raw(@T("Plugins.Feed.Froogle.AdminInstruction"))
    - @Html.Action("InfoProfile", "Export", new { systemName = ProductExportXmlProvider.SystemName, returnUrl = Request.RawUrl, area = "admin" }) + @Html.Action("InfoProfile", "Export", new { systemName = GmcXmlExportProvider.SystemName, returnUrl = Request.RawUrl, area = "admin" })
    @@ -53,7 +52,8 @@ @Html.SmartLabelFor(m => m.SearchIsTouched) - @Html.DropDownList("SearchIsTouched", new List { + @Html.DropDownList("SearchIsTouched", new List + { new SelectListItem { Text = T("Common.Unspecified"), Value = "" }, new SelectListItem { Text = T("Plugins.Feed.Froogle.SearchIsTouched.Touched"), Value = "touched" }, new SelectListItem { Text = T("Plugins.Feed.Froogle.SearchIsTouched.Untouched"), Value = "untouched" } @@ -82,25 +82,56 @@ }) .Columns(c => { - c.Bound(x => x.ProductId).ReadOnly().Visible(false); + c.Bound(x => x.ProductId) + .ReadOnly() + .Visible(false); c.Bound(x => x.Name) - .ReadOnly().Visible(true).Width(420) + .ReadOnly().Visible(true) .Template(x => @Html.LabeledProductName(x.ProductId, x.Name, x.ProductTypeName, x.ProductTypeLabelHint)) .ClientTemplate(@Html.LabeledProductName("ProductId", "Name")); - c.Bound(x => x.SKU).ReadOnly().Visible(true); - c.Bound(x => x.Exporting).ClientTemplate(Html.XEditableLink("Exporting", "select2")); - c.Bound(x => x.Taxonomy).ClientTemplate(Html.XEditableLink("Taxonomy", "typeahead")); - c.Bound(x => x.Gender).ClientTemplate(Html.XEditableLink("Gender", "select2")).Width(100); - c.Bound(x => x.AgeGroup).ClientTemplate(Html.XEditableLink("AgeGroup", "select2")).Width(100); - c.Bound(x => x.Color).ClientTemplate(Html.XEditableLink("Color", "text")); - c.Bound(x => x.Size).ClientTemplate(Html.XEditableLink("Size", "text")); - c.Bound(x => x.Material).ClientTemplate(Html.XEditableLink("Material", "text")); - c.Bound(x => x.Pattern).ClientTemplate(Html.XEditableLink("Pattern", "text")); + c.Bound(x => x.SKU) + .ReadOnly() + .Visible(true); + c.Bound(x => x.Export2) + .ClientTemplate(Html.XEditableLink("Export2", "select2")); + c.Bound(x => x.Taxonomy) + .ClientTemplate(Html.XEditableLink("Taxonomy", "typeahead")); + c.Bound(x => x.Gender) + .ClientTemplate(Html.XEditableLink("Gender", "select2")); + c.Bound(x => x.AgeGroup) + .ClientTemplate(Html.XEditableLink("AgeGroup", "select2")); + c.Bound(x => x.IsAdult) + .ClientTemplate(Html.XEditableLink("IsAdult", "select2")); + c.Bound(x => x.Color) + .ClientTemplate(Html.XEditableLink("Color", "text")); + c.Bound(x => x.Size) + .ClientTemplate(Html.XEditableLink("Size", "text")); + c.Bound(x => x.Material) + .ClientTemplate(Html.XEditableLink("Material", "text")); + c.Bound(x => x.Pattern) + .ClientTemplate(Html.XEditableLink("Pattern", "text")); + c.Bound(x => x.Multipack2) + .ClientTemplate(Html.XEditableLink("Multipack2", "text")); + c.Bound(x => x.IsBundle) + .ClientTemplate(Html.XEditableLink("IsBundle", "select2")); + c.Bound(x => x.EnergyEfficiencyClass) + .ClientTemplate(Html.XEditableLink("EnergyEfficiencyClass", "select2")); + c.Bound(x => x.CustomLabel0) + .ClientTemplate(Html.XEditableLink("CustomLabel0", "text")); + c.Bound(x => x.CustomLabel1) + .ClientTemplate(Html.XEditableLink("CustomLabel1", "text")); + c.Bound(x => x.CustomLabel2) + .ClientTemplate(Html.XEditableLink("CustomLabel2", "text")); + c.Bound(x => x.CustomLabel3) + .ClientTemplate(Html.XEditableLink("CustomLabel3", "text")); + c.Bound(x => x.CustomLabel4) + .ClientTemplate(Html.XEditableLink("CustomLabel4", "text")); }) .ClientEvents(e => { e.OnDataBound("OnGridDataBound"); e.OnDataBinding("OnGridDataBinding"); + e.OnError("OnGridError"); }) .DataBinding(dataBinding => { @@ -127,6 +158,11 @@ return false; }); + function OnGridError(e) { + e.preventDefault(); + alert(e.XMLHttpRequest.responseText); + } + function OnGridDataBinding(e) { e.data = { searchProductName: $('#SearchProductName').val(), @@ -135,28 +171,32 @@ } function OnGridDataBound(e) { - var grid = $('#gmc-products-grid'), - options = { - emptytext: '@T("Plugins.Feed.Froogle.EmptyGridCell")', - url: '@Url.Action("GoogleProductEdit", "FeedGoogleMerchantCenter", new { Namespaces = "SmartStore.GoogleMerchantCenter.Controllers", area = GoogleMerchantCenterFeedPlugin.SystemName })' - }, - select2Options = { - placeholder: '@T("Plugins.Feed.Froogle.EmptyGridCell")', - minimumResultsForSearch: 20, - allowClear: true - }; + var grid = $('#gmc-products-grid'); + + var emptyCell = '@T("Plugins.Feed.Froogle.EmptyGridCell")'; + + var options = { + emptytext: emptyCell, + url: '@Url.Action("GoogleProductEdit", "FeedGoogleMerchantCenter", new { Namespaces = "SmartStore.GoogleMerchantCenter.Controllers", area = GoogleMerchantCenterFeedPlugin.SystemName })' + }; + var select2Options = { + placeholder: emptyCell, + minimumResultsForSearch: 20, + allowClear: true + }; + var boolSource = [ + { id: 'true', text: '@T("Admin.Common.Yes")' }, + { id: 'false', text: '@T("Admin.Common.No")' } + ]; - grid.find('a.edit-link-exporting').editable($.extend(options, { + grid.find('a.edit-link-export2').editable($.extend({}, options, { select2: { minimumResultsForSearch: 20 }, - source: [ - { id: 'true', text: '@T("Admin.Common.Yes")' }, - { id: 'false', text: '@T("Admin.Common.No")' } - ] + source: boolSource })); - grid.find('a.edit-link-gender').editable($.extend(options, { + grid.find('a.edit-link-gender').editable($.extend({}, options, { select2: select2Options, source: [ { id: 'male', text: '@T("Plugins.Feed.Froogle.GenderMale")' }, @@ -165,7 +205,7 @@ ] })); - grid.find('a.edit-link-agegroup').editable($.extend(options, { + grid.find('a.edit-link-agegroup').editable($.extend({}, options, { select2: select2Options, source: [ { id: 'adult', text: '@T("Plugins.Feed.Froogle.AgeGroupAdult").Text' }, @@ -173,6 +213,11 @@ ] })); + grid.find('a.edit-link-isadult').editable($.extend({}, options, { + select2: select2Options, + source: boolSource + })); + grid.find('a.edit-link-color').editable(options); grid.find('a.edit-link-size').editable(options); @@ -181,7 +226,46 @@ grid.find('a.edit-link-pattern').editable(options); - grid.find('a.edit-link-taxonomy').editable($.extend(options, { + grid.find('a.edit-link-multipack2').editable($.extend({}, options, { + validate: function(value) { + if (value.length !== 0) { + var regex = /^[0-9]+$/; + if (!regex.test(value)) { + return '@T("Admin.System.Warnings.DigitsOnly")'; + } + } + return null; + } + })); + + grid.find('a.edit-link-isbundle').editable($.extend({}, options, { + select2: select2Options, + source: boolSource + })); + + grid.find('a.edit-link-energyefficiencyclass').editable($.extend({}, options, { + select2: select2Options, + source: [ + @if (Model.EnergyEfficiencyClasses != null && Model.EnergyEfficiencyClasses.Length > 0) + { + foreach (var str in Model.EnergyEfficiencyClasses.Select(x => x.Replace("'", "\""))) + { + { id: '@str', text: '@str' }, + } + } + ] + })); + + grid.find('a.edit-link-customlabel0').editable(options); + grid.find('a.edit-link-customlabel1').editable(options); + grid.find('a.edit-link-customlabel2').editable(options); + grid.find('a.edit-link-customlabel3').editable(options); + grid.find('a.edit-link-customlabel4').editable(options); + + grid.find('a.edit-link-taxonomy').editable($.extend({}, options, { + select2: { + minimumResultsForSearch: 20 + }, source: @Html.Raw(Model.AvailableGoogleCategoriesAsJson) })); } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProductEditTab.cshtml b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProductEditTab.cshtml index fa3273da09..c20ef9acd7 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProductEditTab.cshtml +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProductEditTab.cshtml @@ -6,24 +6,24 @@ + \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/Configure.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/Configure.cshtml index 70868da9e5..5846c1ed43 100644 --- a/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/Configure.cshtml +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/Configure.cshtml @@ -14,7 +14,7 @@
    @@ -24,6 +24,14 @@ @using (Html.BeginForm()) { + + + + + + + + + +
    + @Html.SmartLabelFor(model => model.SecurityProtocol) + + @Html.DropDownListFor(model => model.SecurityProtocol, Model.AvailableSecurityProtocols, T("Common.Unspecified")) +
    @Html.SmartLabelFor(model => model.UseSandbox) @@ -106,6 +114,15 @@ @Html.ValidationMessageFor(model => model.EnableIpn)
    + @Html.SmartLabelFor(model => model.IpnChangesPaymentStatus) + + @Html.SettingEditorFor(model => model.IpnChangesPaymentStatus) + @Html.ValidationMessageFor(model => model.IpnChangesPaymentStatus) +
    @Html.SmartLabelFor(model => model.IpnUrl) diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/PaymentInfo.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/PaymentInfo.cshtml index c5b73f2d11..ee77251801 100644 --- a/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/PaymentInfo.cshtml +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/PaymentInfo.cshtml @@ -5,7 +5,7 @@
    - PayPal + PayPal
    @T("Plugins.Payments.PayPalStandard.Fields.RedirectionTip") diff --git a/src/Plugins/SmartStore.PayPal/Views/Web.config b/src/Plugins/SmartStore.PayPal/Views/Web.config index 31dc8df754..e9d36a3c51 100644 --- a/src/Plugins/SmartStore.PayPal/Views/Web.config +++ b/src/Plugins/SmartStore.PayPal/Views/Web.config @@ -14,7 +14,7 @@ - + diff --git a/src/Plugins/SmartStore.PayPal/changelog.md b/src/Plugins/SmartStore.PayPal/changelog.md index 80afe49fdd..e074479a1d 100644 --- a/src/Plugins/SmartStore.PayPal/changelog.md +++ b/src/Plugins/SmartStore.PayPal/changelog.md @@ -1,7 +1,42 @@ -#Release Notes# +#Release Notes + +##Paypal 2.6.0.5 +###Bugfixes +* PayPal PLUS: Fixes "Cannot perform runtime binding on a null reference" when rendering the payment wall. + +##Paypal 2.6.0.4 +###Bugfixes +* PayPal PLUS: Excluding tax issue. Fixes "Transaction amount details (subtotal, tax, shipping) must add up to specified amount total". + +##Paypal 2.6.0.3 +###Bugfixes +* PayPal PLUS: Integration review through PayPal +* PayPal PLUS: Generic attribute caching problem. Fixes "Item amount must add up to specified amount subtotal (or total if amount details not specified)". + +##PayPal 2.6.0.1 +###Improvements +* Added PayPal partner attribution Id as request header + +##Paypal 2.5.0.2 +###New Features +* PayPal PLUS payment provider + +##Paypal 2.5.0.1 +###Bugfixes +* PayPal Standard: The order amount transmitted to PayPal was wrong if gift cards or reward points were applied + +##Paypal 2.2.0.4 +###New Features +* Option for API security protocol +* Option to display express checkout button in mini shopping cart +* Support for partial refunds +* Option whether IPD may change the payment status of an order +###Bugfixes +* "The request was aborted: Could not create SSL/TLS secure channel." See https://devblog.paypal.com/upcoming-security-changes-notice/ +* PayPal Express: Void and refund out of function ("The transaction id is not valid") ##Paypal 2.2.0.3 -### New Features +###New Features * Option to add order note when order total validation fails ##PayPal 2.2.0.2 @@ -9,7 +44,7 @@ * Redirecting to payment provider performed by core instead of plugin ##Paypal 2.2.0.1 -### New Features +###New Features * Supports order list label for new incoming IPNs ##Paypal 1.22 diff --git a/src/Plugins/SmartStore.PayPal/packages.config b/src/Plugins/SmartStore.PayPal/packages.config index 6025774f20..eb91eb3011 100644 --- a/src/Plugins/SmartStore.PayPal/packages.config +++ b/src/Plugins/SmartStore.PayPal/packages.config @@ -1,10 +1,11 @@  - - - + + + + \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/web.config b/src/Plugins/SmartStore.PayPal/web.config index 009e149619..f6daa0fb4b 100644 --- a/src/Plugins/SmartStore.PayPal/web.config +++ b/src/Plugins/SmartStore.PayPal/web.config @@ -50,7 +50,7 @@ - + @@ -66,7 +66,7 @@ - + @@ -82,7 +82,7 @@ - + @@ -90,11 +90,11 @@ - + - + @@ -132,6 +132,14 @@ + + + + + + + + - \ No newline at end of file + diff --git a/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs b/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs index 92b915c7d5..5c5e03f8ab 100644 --- a/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs +++ b/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs @@ -7,6 +7,7 @@ using SmartStore.Shipping.Models; using SmartStore.Shipping.Services; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Security; using Telerik.Web.Mvc; namespace SmartStore.Shipping.Controllers @@ -42,7 +43,7 @@ public ActionResult Configure() var shippingMethods = _shippingService.GetAllShippingMethods(); if (shippingMethods.Count == 0) { - return Content("No shipping methods can be loaded"); + return Content(T("Admin.Configuration.Shipping.Methods.NoMethodsLoaded")); } var model = new ByTotalListModel(); @@ -99,10 +100,11 @@ public ActionResult RateUpdate(ByTotalModel model, GridCommand command) { if (!ModelState.IsValid) { - return new JsonResult { Data = "error" }; + return new JsonResult { Data = T("Admin.Common.UnknownError").Text }; } var shippingByTotalRecord = _shippingByTotalService.GetShippingByTotalRecordById(model.Id); + shippingByTotalRecord.Zip = model.Zip == "*" ? null : model.Zip; shippingByTotalRecord.From = model.From; shippingByTotalRecord.To = model.To; @@ -111,6 +113,7 @@ public ActionResult RateUpdate(ByTotalModel model, GridCommand command) shippingByTotalRecord.ShippingChargePercentage = model.ShippingChargePercentage; shippingByTotalRecord.BaseCharge = model.BaseCharge; shippingByTotalRecord.MaxCharge = model.MaxCharge; + _shippingByTotalService.UpdateShippingByTotalRecord(shippingByTotalRecord); return RatesList(command); diff --git a/src/Plugins/SmartStore.Shipping/Controllers/FixedRateController.cs b/src/Plugins/SmartStore.Shipping/Controllers/FixedRateController.cs index 1522e7f545..353bafe59d 100644 --- a/src/Plugins/SmartStore.Shipping/Controllers/FixedRateController.cs +++ b/src/Plugins/SmartStore.Shipping/Controllers/FixedRateController.cs @@ -8,6 +8,7 @@ using SmartStore.Services.Shipping; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Security; using Telerik.Web.Mvc; namespace SmartStore.Shipping.Controllers @@ -28,7 +29,7 @@ public ActionResult Configure() { var shippingMethods = _shippingService.GetAllShippingMethods(); if (shippingMethods.Count == 0) - return Content("No shipping methods can be loaded"); + return Content(T("Admin.Configuration.Shipping.Methods.NoMethodsLoaded")); var tmp = new List(); foreach (var shippingMethod in shippingMethods) diff --git a/src/Plugins/SmartStore.Shipping/Description.txt b/src/Plugins/SmartStore.Shipping/Description.txt index 533761599a..f241173e84 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: 2.2.0.1 -MinAppVersion: 2.2.0 +Version: 2.6.0 +MinAppVersion: 2.5.0 DisplayOrder: 1 FileName: SmartStore.Shipping.dll ResourceRootKey: Plugins.SmartStore.Shipping \ No newline at end of file diff --git a/src/Plugins/SmartStore.Shipping/Models/ByTotalListModel.cs b/src/Plugins/SmartStore.Shipping/Models/ByTotalListModel.cs index dec18c67ac..848836b1af 100644 --- a/src/Plugins/SmartStore.Shipping/Models/ByTotalListModel.cs +++ b/src/Plugins/SmartStore.Shipping/Models/ByTotalListModel.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Web.Mvc; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Shipping.Models { diff --git a/src/Plugins/SmartStore.Shipping/Models/ByTotalModel.cs b/src/Plugins/SmartStore.Shipping/Models/ByTotalModel.cs index 5287eb5df4..dc7a5108cc 100644 --- a/src/Plugins/SmartStore.Shipping/Models/ByTotalModel.cs +++ b/src/Plugins/SmartStore.Shipping/Models/ByTotalModel.cs @@ -1,5 +1,5 @@ using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Shipping.Models { diff --git a/src/Plugins/SmartStore.Shipping/Providers/ByTotalProvider.cs b/src/Plugins/SmartStore.Shipping/Providers/ByTotalProvider.cs index 882a576f15..82ec989cf4 100644 --- a/src/Plugins/SmartStore.Shipping/Providers/ByTotalProvider.cs +++ b/src/Plugins/SmartStore.Shipping/Providers/ByTotalProvider.cs @@ -1,20 +1,20 @@ using System; -using System.Data.Entity.Migrations; using System.Web.Routing; using SmartStore.Core; using SmartStore.Core.Domain.Shipping; +using SmartStore.Core.Localization; +using SmartStore.Core.Logging; using SmartStore.Core.Plugins; -using SmartStore.Shipping.Services; using SmartStore.Services.Catalog; using SmartStore.Services.Configuration; using SmartStore.Services.Localization; -using SmartStore.Core.Logging; using SmartStore.Services.Shipping; using SmartStore.Services.Shipping.Tracking; +using SmartStore.Shipping.Services; namespace SmartStore.Shipping { - [SystemName("Shipping.ByTotal")] + [SystemName("Shipping.ByTotal")] [FriendlyName("Shipping by total")] [DisplayOrder(1)] public class ByTotalProvider : IShippingRateComputationMethod, IConfigurable @@ -28,7 +28,6 @@ public class ByTotalProvider : IShippingRateComputationMethod, IConfigurable private readonly ISettingService _settingService; private readonly ILocalizationService _localizationService; - /// /// Ctor /// @@ -56,14 +55,18 @@ public ByTotalProvider(IShippingService shippingService, this._logger = logger; this._settingService = settingService; this._localizationService = localizationService; - } - #region Properties + T = NullLocalizer.Instance; + } - /// - /// Gets a shipping rate computation method type - /// - public ShippingRateComputationMethodType ShippingRateComputationMethodType + #region Properties + + public Localizer T { get; set; } + + /// + /// Gets a shipping rate computation method type + /// + public ShippingRateComputationMethodType ShippingRateComputationMethodType { get { @@ -158,7 +161,7 @@ public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest get if (getShippingOptionRequest.Items == null || getShippingOptionRequest.Items.Count == 0) { - response.AddError("No shipment items"); + response.AddError(T("Admin.System.Warnings.NoShipmentItems")); return response; } diff --git a/src/Plugins/SmartStore.Shipping/Providers/FixedRateProvider.cs b/src/Plugins/SmartStore.Shipping/Providers/FixedRateProvider.cs index a4ae31c94e..cc303cee6e 100644 --- a/src/Plugins/SmartStore.Shipping/Providers/FixedRateProvider.cs +++ b/src/Plugins/SmartStore.Shipping/Providers/FixedRateProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Web.Routing; using SmartStore.Core.Domain.Shipping; +using SmartStore.Core.Localization; using SmartStore.Core.Plugins; using SmartStore.Services.Configuration; using SmartStore.Services.Localization; @@ -10,27 +11,29 @@ namespace SmartStore.Shipping { - /// - /// Fixed rate shipping computation provider - /// - [SystemName("Shipping.FixedRate")] + /// + /// Fixed rate shipping computation provider + /// + [SystemName("Shipping.FixedRate")] [FriendlyName("Fixed Rate Shipping")] [DisplayOrder(0)] public class FixedRateProvider : IShippingRateComputationMethod, IConfigurable { private readonly ISettingService _settingService; private readonly IShippingService _shippingService; - private readonly ILocalizationService _localizationService; public FixedRateProvider(ISettingService settingService, - IShippingService shippingService, ILocalizationService localizationService) + IShippingService shippingService) { this._settingService = settingService; this._shippingService = shippingService; - _localizationService = localizationService; - } - - private decimal GetRate(int shippingMethodId) + + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + private decimal GetRate(int shippingMethodId) { string key = string.Format("ShippingRateComputationMethod.FixedRate.Rate.ShippingMethodId{0}", shippingMethodId); decimal rate = this._settingService.GetSettingByKey(key); @@ -51,7 +54,7 @@ public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest get if (getShippingOptionRequest.Items == null || getShippingOptionRequest.Items.Count == 0) { - response.AddError("No shipment items"); + response.AddError(T("Admin.System.Warnings.NoShipmentItems")); return response; } diff --git a/src/Plugins/SmartStore.Shipping/RouteProvider.cs b/src/Plugins/SmartStore.Shipping/RouteProvider.cs index 6771a7781f..38dead4bf3 100644 --- a/src/Plugins/SmartStore.Shipping/RouteProvider.cs +++ b/src/Plugins/SmartStore.Shipping/RouteProvider.cs @@ -1,6 +1,6 @@ using System.Web.Mvc; using System.Web.Routing; -using SmartStore.Web.Framework.Mvc.Routes; +using SmartStore.Web.Framework.Routing; namespace SmartStore.Shipping { diff --git a/src/Plugins/SmartStore.Shipping/SmartStore.Shipping.csproj b/src/Plugins/SmartStore.Shipping/SmartStore.Shipping.csproj index 3de6559bd2..3a9b8863dd 100644 --- a/src/Plugins/SmartStore.Shipping/SmartStore.Shipping.csproj +++ b/src/Plugins/SmartStore.Shipping/SmartStore.Shipping.csproj @@ -42,6 +42,7 @@ + true @@ -81,11 +82,11 @@ MinimumRecommendedRules.ruleset - - ..\..\packages\Autofac.3.4.1\lib\net40\Autofac.dll + + ..\..\packages\Autofac.3.5.2\lib\net40\Autofac.dll - - ..\..\packages\Autofac.Mvc5.3.3.1\lib\net45\Autofac.Integration.Mvc.dll + + ..\..\packages\Autofac.Mvc5.3.3.4\lib\net45\Autofac.Integration.Mvc.dll False diff --git a/src/Plugins/SmartStore.Shipping/Views/Web.config b/src/Plugins/SmartStore.Shipping/Views/Web.config index 31dc8df754..e9d36a3c51 100644 --- a/src/Plugins/SmartStore.Shipping/Views/Web.config +++ b/src/Plugins/SmartStore.Shipping/Views/Web.config @@ -14,7 +14,7 @@ - + diff --git a/src/Plugins/SmartStore.Shipping/packages.config b/src/Plugins/SmartStore.Shipping/packages.config index bbc22c8227..1a1ea4e95b 100644 --- a/src/Plugins/SmartStore.Shipping/packages.config +++ b/src/Plugins/SmartStore.Shipping/packages.config @@ -1,7 +1,7 @@  - - + + diff --git a/src/Plugins/SmartStore.Shipping/web.config b/src/Plugins/SmartStore.Shipping/web.config index ad003b3e26..c0f82db177 100644 --- a/src/Plugins/SmartStore.Shipping/web.config +++ b/src/Plugins/SmartStore.Shipping/web.config @@ -1,116 +1,116 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - \ No newline at end of file + diff --git a/src/Plugins/SmartStore.ShippingByWeight/ByWeightShippingComputationMethod.cs b/src/Plugins/SmartStore.ShippingByWeight/ByWeightShippingComputationMethod.cs index 0c59828d2a..a147a1855d 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/ByWeightShippingComputationMethod.cs +++ b/src/Plugins/SmartStore.ShippingByWeight/ByWeightShippingComputationMethod.cs @@ -3,15 +3,16 @@ using System.Web.Routing; using SmartStore.Core; using SmartStore.Core.Domain.Shipping; +using SmartStore.Core.Localization; using SmartStore.Core.Plugins; -using SmartStore.ShippingByWeight.Data; -using SmartStore.ShippingByWeight.Data.Migrations; -using SmartStore.ShippingByWeight.Services; using SmartStore.Services; using SmartStore.Services.Catalog; using SmartStore.Services.Localization; using SmartStore.Services.Shipping; using SmartStore.Services.Shipping.Tracking; +using SmartStore.ShippingByWeight.Data; +using SmartStore.ShippingByWeight.Data.Migrations; +using SmartStore.ShippingByWeight.Services; namespace SmartStore.ShippingByWeight { @@ -32,6 +33,7 @@ public class ByWeightShippingComputationMethod : BasePlugin, IShippingRateComput #endregion #region Ctor + public ByWeightShippingComputationMethod(IShippingService shippingService, IStoreContext storeContext, IShippingByWeightService shippingByWeightService, @@ -51,12 +53,17 @@ public ByWeightShippingComputationMethod(IShippingService shippingService, this._localizationService = localizationService; this._priceFormatter = priceFormatter; this._services = services; - } - #endregion - #region Utilities + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + #endregion - private decimal? GetRate(decimal subTotal, decimal weight, int shippingMethodId, int storeId, int countryId, string zip) + #region Utilities + + private decimal? GetRate(decimal subTotal, decimal weight, int shippingMethodId, int storeId, int countryId, string zip) { decimal? shippingTotal = null; @@ -109,41 +116,41 @@ public ByWeightShippingComputationMethod(IShippingService shippingService, /// /// Gets available shipping options /// - /// A request for getting shipping options + /// A request for getting shipping options /// Represents a response of getting shipping rate options - public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest getShippingOptionRequest) + public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest request) { - if (getShippingOptionRequest == null) + if (request == null) throw new ArgumentNullException("getShippingOptionRequest"); var response = new GetShippingOptionResponse(); - if (getShippingOptionRequest.Items == null || getShippingOptionRequest.Items.Count == 0) + if (request.Items == null || request.Items.Count == 0) { - response.AddError("No shipment items"); + response.AddError(T("Admin.System.Warnings.NoShipmentItems")); return response; } - - int storeId = _storeContext.CurrentStore.Id; + + int storeId = request.StoreId > 0 ? request.StoreId : _storeContext.CurrentStore.Id; decimal subTotal = decimal.Zero; int countryId = 0; string zip = null; - if (getShippingOptionRequest.ShippingAddress != null) + if (request.ShippingAddress != null) { - countryId = getShippingOptionRequest.ShippingAddress.CountryId ?? 0; - zip = getShippingOptionRequest.ShippingAddress.ZipPostalCode; + countryId = request.ShippingAddress.CountryId ?? 0; + zip = request.ShippingAddress.ZipPostalCode; } - foreach (var shoppingCartItem in getShippingOptionRequest.Items) + foreach (var shoppingCartItem in request.Items) { if (shoppingCartItem.Item.IsFreeShipping || !shoppingCartItem.Item.IsShipEnabled) continue; subTotal += _priceCalculationService.GetSubTotal(shoppingCartItem, true); } - decimal weight = _shippingService.GetShoppingCartTotalWeight(getShippingOptionRequest.Items); + decimal weight = _shippingService.GetShoppingCartTotalWeight(request.Items); - var shippingMethods = _shippingService.GetAllShippingMethods(getShippingOptionRequest); + var shippingMethods = _shippingService.GetAllShippingMethods(request); foreach (var shippingMethod in shippingMethods) { var record = _shippingByWeightService.FindRecord(shippingMethod.Id, storeId, countryId, weight, zip); diff --git a/src/Plugins/SmartStore.ShippingByWeight/Controllers/ShippingByWeightController.cs b/src/Plugins/SmartStore.ShippingByWeight/Controllers/ShippingByWeightController.cs index 3d805090f6..383d77dfa2 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Controllers/ShippingByWeightController.cs +++ b/src/Plugins/SmartStore.ShippingByWeight/Controllers/ShippingByWeightController.cs @@ -8,6 +8,8 @@ using SmartStore.ShippingByWeight.Models; using SmartStore.ShippingByWeight.Services; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Filters; +using SmartStore.Web.Framework.Security; using Telerik.Web.Mvc; namespace SmartStore.ShippingByWeight.Controllers @@ -48,8 +50,10 @@ public ShippingByWeightController( public ActionResult Configure() { var shippingMethods = _shippingService.GetAllShippingMethods(); - if (shippingMethods.Count == 0) - return Content("No shipping methods can be loaded"); + if (shippingMethods.Count == 0) + { + return Content(T("Admin.Configuration.Shipping.Methods.NoMethodsLoaded")); + } var model = new ShippingByWeightListModel(); var countries = _countryService.GetAllCountries(true); diff --git a/src/Plugins/SmartStore.ShippingByWeight/Description.txt b/src/Plugins/SmartStore.ShippingByWeight/Description.txt index 65f72e227d..218a277c2b 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: 2.2.0.1 -MinAppVersion: 2.2.0 +Version: 2.6.0 +MinAppVersion: 2.5.0 DisplayOrder: 1 FileName: SmartStore.ShippingByWeight.dll ResourceRootKey: Plugins.Shipping.ByWeight \ No newline at end of file diff --git a/src/Plugins/SmartStore.ShippingByWeight/Models/ShippingByWeightListModel.cs b/src/Plugins/SmartStore.ShippingByWeight/Models/ShippingByWeightListModel.cs index eacf5b06bc..eb7710b070 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Models/ShippingByWeightListModel.cs +++ b/src/Plugins/SmartStore.ShippingByWeight/Models/ShippingByWeightListModel.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Web.Mvc; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.ShippingByWeight.Models { diff --git a/src/Plugins/SmartStore.ShippingByWeight/Models/ShippingByWeightModel.cs b/src/Plugins/SmartStore.ShippingByWeight/Models/ShippingByWeightModel.cs index 2cf20b9b6d..516bf71071 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Models/ShippingByWeightModel.cs +++ b/src/Plugins/SmartStore.ShippingByWeight/Models/ShippingByWeightModel.cs @@ -1,5 +1,5 @@ using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.ShippingByWeight.Models { diff --git a/src/Plugins/SmartStore.ShippingByWeight/RouteProvider.cs b/src/Plugins/SmartStore.ShippingByWeight/RouteProvider.cs index 89fe6dab66..89c87cf6b2 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/RouteProvider.cs +++ b/src/Plugins/SmartStore.ShippingByWeight/RouteProvider.cs @@ -1,6 +1,6 @@ using System.Web.Mvc; using System.Web.Routing; -using SmartStore.Web.Framework.Mvc.Routes; +using SmartStore.Web.Framework.Routing; namespace SmartStore.ShippingByWeight { diff --git a/src/Plugins/SmartStore.ShippingByWeight/Services/ShippingByWeightService.cs b/src/Plugins/SmartStore.ShippingByWeight/Services/ShippingByWeightService.cs index 06d9b7ff55..f60212f80c 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Services/ShippingByWeightService.cs +++ b/src/Plugins/SmartStore.ShippingByWeight/Services/ShippingByWeightService.cs @@ -48,7 +48,7 @@ public virtual IQueryable GetShippingByWeightRecords() { var query = from x in _sbwRepository.Table - orderby x.StoreId, x.CountryId, x.ShippingMethodId, x.From, x.Zip + orderby x.StoreId, x.CountryId, x.ShippingMethodId, x.From select x; return query; @@ -83,7 +83,7 @@ public virtual IList GetShippingByWeightModels(int pageIn var shippingMethod = _shippingService.GetShippingMethodById(x.ShippingMethodId); var country = _countryService.GetCountryById(x.CountryId); - var model = new ShippingByWeightModel() + var model = new ShippingByWeightModel { Id = x.Id, StoreId = x.StoreId, diff --git a/src/Plugins/SmartStore.ShippingByWeight/SmartStore.ShippingByWeight.csproj b/src/Plugins/SmartStore.ShippingByWeight/SmartStore.ShippingByWeight.csproj index f51ffb1713..990c0f2f61 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/SmartStore.ShippingByWeight.csproj +++ b/src/Plugins/SmartStore.ShippingByWeight/SmartStore.ShippingByWeight.csproj @@ -42,6 +42,7 @@ + true @@ -81,12 +82,11 @@ MinimumRecommendedRules.ruleset - - ..\..\packages\Autofac.3.4.1\lib\net40\Autofac.dll - False + + ..\..\packages\Autofac.3.5.2\lib\net40\Autofac.dll - - ..\..\packages\Autofac.Mvc5.3.3.1\lib\net45\Autofac.Integration.Mvc.dll + + ..\..\packages\Autofac.Mvc5.3.3.4\lib\net45\Autofac.Integration.Mvc.dll False diff --git a/src/Plugins/SmartStore.ShippingByWeight/Views/ShippingByWeight/Configure.cshtml b/src/Plugins/SmartStore.ShippingByWeight/Views/ShippingByWeight/Configure.cshtml index 08b5ea8062..3da335400c 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Views/ShippingByWeight/Configure.cshtml +++ b/src/Plugins/SmartStore.ShippingByWeight/Views/ShippingByWeight/Configure.cshtml @@ -1,11 +1,10 @@ -@{ - Layout = ""; -} -@model ShippingByWeightListModel -@using SmartStore.ShippingByWeight.Models; +@using SmartStore.ShippingByWeight.Models; @using SmartStore.Web.Framework; @using Telerik.Web.Mvc.UI; -@using System.Linq; +@model ShippingByWeightListModel +@{ + Layout = ""; +} @@ -62,13 +61,11 @@

    @using (Html.BeginForm()) -{ - +{ -
    - @T("Plugins.Shipping.ByWeight.AddNewRecordTitle") -
    +
    + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + - + - -
    +
    +
    @T("Plugins.Shipping.ByWeight.AddNewRecordTitle")
    +
    +
    @Html.SmartLabelFor(model => model.AddStoreId) @@ -96,143 +98,146 @@ @Html.ValidationMessageFor(model => model.AddStoreId)
    - @Html.SmartLabelFor(model => model.AddCountryId) - - @Html.DropDownListFor(model => model.AddCountryId, Model.AvailableCountries) - @Html.ValidationMessageFor(model => model.AddCountryId) -
    - @Html.SmartLabelFor(model => model.AddZip) - - @Html.EditorFor(model => model.AddZip) - @Html.ValidationMessageFor(model => model.AddZip) -
    - @Html.SmartLabelFor(model => model.AddShippingMethodId) - - @Html.DropDownListFor(model => model.AddShippingMethodId, Model.AvailableShippingMethods) - @Html.ValidationMessageFor(model => model.AddShippingMethodId) -
    - @Html.SmartLabelFor(model => model.AddFrom) - - @Html.EditorFor(model => model.AddFrom) [@Model.BaseWeightIn] - @Html.ValidationMessageFor(model => model.AddFrom) -
    - @Html.SmartLabelFor(model => model.AddTo) - - @Html.EditorFor(model => model.AddTo) [@Model.BaseWeightIn] - @Html.ValidationMessageFor(model => model.AddTo) -
    - @Html.SmartLabelFor(model => model.AddUsePercentage) - - @Html.EditorFor(model => model.AddUsePercentage) - @Html.ValidationMessageFor(model => model.AddUsePercentage) -
    - @Html.SmartLabelFor(model => model.AddShippingChargePercentage) - - @Html.EditorFor(model => model.AddShippingChargePercentage) - @Html.ValidationMessageFor(model => model.AddShippingChargePercentage) -
    - @Html.SmartLabelFor(model => model.AddShippingChargeAmount) - - @Html.EditorFor(model => model.AddShippingChargeAmount) [@Model.PrimaryStoreCurrencyCode] - @Html.ValidationMessageFor(model => model.AddShippingChargeAmount) -
    + @Html.SmartLabelFor(model => model.AddCountryId) + + @Html.DropDownListFor(model => model.AddCountryId, Model.AvailableCountries) + @Html.ValidationMessageFor(model => model.AddCountryId) +
    + @Html.SmartLabelFor(model => model.AddZip) + + @Html.EditorFor(model => model.AddZip) + @Html.ValidationMessageFor(model => model.AddZip) +
    + @Html.SmartLabelFor(model => model.AddShippingMethodId) + + @Html.DropDownListFor(model => model.AddShippingMethodId, Model.AvailableShippingMethods) + @Html.ValidationMessageFor(model => model.AddShippingMethodId) +
    + @Html.SmartLabelFor(model => model.AddFrom) + + @Html.EditorFor(model => model.AddFrom) [@Model.BaseWeightIn] + @Html.ValidationMessageFor(model => model.AddFrom) +
    + @Html.SmartLabelFor(model => model.AddTo) + + @Html.EditorFor(model => model.AddTo) [@Model.BaseWeightIn] + @Html.ValidationMessageFor(model => model.AddTo) +
    + @Html.SmartLabelFor(model => model.AddUsePercentage) + + @Html.EditorFor(model => model.AddUsePercentage) + @Html.ValidationMessageFor(model => model.AddUsePercentage) +
    + @Html.SmartLabelFor(model => model.AddShippingChargePercentage) + + @Html.EditorFor(model => model.AddShippingChargePercentage) + @Html.ValidationMessageFor(model => model.AddShippingChargePercentage) +
    + @Html.SmartLabelFor(model => model.AddShippingChargeAmount) + + @Html.EditorFor(model => model.AddShippingChargeAmount) [@Model.PrimaryStoreCurrencyCode] + @Html.ValidationMessageFor(model => model.AddShippingChargeAmount) +
    - @Html.SmartLabelFor(model => model.SmallQuantitySurcharge) - - @Html.EditorFor(model => model.SmallQuantitySurcharge) [@Model.PrimaryStoreCurrencyCode] - @Html.ValidationMessageFor(model => model.SmallQuantitySurcharge) -
    - @Html.SmartLabelFor(model => model.SmallQuantityThreshold) - - @Html.EditorFor(model => model.SmallQuantityThreshold) - @Html.ValidationMessageFor(model => model.SmallQuantityThreshold) -
    + @Html.SmartLabelFor(model => model.SmallQuantitySurcharge) + + @Html.EditorFor(model => model.SmallQuantitySurcharge) [@Model.PrimaryStoreCurrencyCode] + @Html.ValidationMessageFor(model => model.SmallQuantitySurcharge) +
    + @Html.SmartLabelFor(model => model.SmallQuantityThreshold) + + @Html.EditorFor(model => model.SmallQuantityThreshold) + @Html.ValidationMessageFor(model => model.SmallQuantityThreshold) +
      - -
    - - -
    - @T("Plugins.Shipping.ByWeight.SettingsTitle") - - - - - - - - - - + + + +
    - @Html.SmartLabelFor(model => model.CalculatePerWeightUnit) - - @Html.EditorFor(model => model.CalculatePerWeightUnit) - @Html.ValidationMessageFor(model => model.CalculatePerWeightUnit) -
    - @Html.SmartLabelFor(model => model.LimitMethodsToCreated) - - @Html.EditorFor(model => model.LimitMethodsToCreated) - @Html.ValidationMessageFor(model => model.LimitMethodsToCreated) -
    + + + + + + + + + + + + + + - -
    +
    +
    @T("Plugins.Shipping.ByWeight.SettingsTitle")
    +
    +
    + @Html.SmartLabelFor(model => model.CalculatePerWeightUnit) + + @Html.EditorFor(model => model.CalculatePerWeightUnit) + @Html.ValidationMessageFor(model => model.CalculatePerWeightUnit) +
    + @Html.SmartLabelFor(model => model.LimitMethodsToCreated) + + @Html.EditorFor(model => model.LimitMethodsToCreated) + @Html.ValidationMessageFor(model => model.LimitMethodsToCreated) +
      - -
    -
    + +
    } \ No newline at end of file diff --git a/src/Plugins/SmartStore.ShippingByWeight/Views/Web.config b/src/Plugins/SmartStore.ShippingByWeight/Views/Web.config index 31dc8df754..e9d36a3c51 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Views/Web.config +++ b/src/Plugins/SmartStore.ShippingByWeight/Views/Web.config @@ -14,7 +14,7 @@ - + diff --git a/src/Plugins/SmartStore.ShippingByWeight/packages.config b/src/Plugins/SmartStore.ShippingByWeight/packages.config index bbc22c8227..1a1ea4e95b 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/packages.config +++ b/src/Plugins/SmartStore.ShippingByWeight/packages.config @@ -1,7 +1,7 @@  - - + + diff --git a/src/Plugins/SmartStore.ShippingByWeight/web.config b/src/Plugins/SmartStore.ShippingByWeight/web.config index ad003b3e26..c0f82db177 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/web.config +++ b/src/Plugins/SmartStore.ShippingByWeight/web.config @@ -1,116 +1,116 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - \ No newline at end of file + diff --git a/src/Plugins/SmartStore.Tax/Controllers/TaxByRegionController.cs b/src/Plugins/SmartStore.Tax/Controllers/TaxByRegionController.cs index 8117766089..ca43d31f14 100644 --- a/src/Plugins/SmartStore.Tax/Controllers/TaxByRegionController.cs +++ b/src/Plugins/SmartStore.Tax/Controllers/TaxByRegionController.cs @@ -9,6 +9,8 @@ using SmartStore.Services.Directory; using SmartStore.Services.Tax; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Filters; +using SmartStore.Web.Framework.Security; using Telerik.Web.Mvc; namespace SmartStore.Tax.Controllers @@ -68,7 +70,7 @@ public ActionResult Configure() var tc = _taxCategoryService.GetTaxCategoryById(x.TaxCategoryId); m.TaxCategoryName = (tc != null) ? tc.Name : ""; var c = _countryService.GetCountryById(x.CountryId); - m.CountryName = (c != null) ? c.Name : "Unavailable"; + m.CountryName = (c != null) ? c.Name : T("Common.Unavailable").Text; var s = _stateProvinceService.GetStateProvinceById(x.StateProvinceId); m.StateProvinceName = (s != null) ? s.Name : "*"; m.Zip = (!String.IsNullOrEmpty(x.Zip)) ? x.Zip : "*"; @@ -97,7 +99,7 @@ public ActionResult RatesList(GridCommand command) var tc = _taxCategoryService.GetTaxCategoryById(x.TaxCategoryId); m.TaxCategoryName = (tc != null) ? tc.Name : ""; var c = _countryService.GetCountryById(x.CountryId); - m.CountryName = (c != null) ? c.Name : "Unavailable"; + m.CountryName = (c != null) ? c.Name : T("Common.Unavailable").Text; var s = _stateProvinceService.GetStateProvinceById(x.StateProvinceId); m.StateProvinceName = (s != null) ? s.Name : "*"; m.Zip = (!String.IsNullOrEmpty(x.Zip)) ? x.Zip : "*"; diff --git a/src/Plugins/SmartStore.Tax/Controllers/TaxFixedRateController.cs b/src/Plugins/SmartStore.Tax/Controllers/TaxFixedRateController.cs index 563e7ff424..1340565989 100644 --- a/src/Plugins/SmartStore.Tax/Controllers/TaxFixedRateController.cs +++ b/src/Plugins/SmartStore.Tax/Controllers/TaxFixedRateController.cs @@ -8,6 +8,7 @@ using SmartStore.Services.Tax; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Security; using Telerik.Web.Mvc; namespace SmartStore.Tax.Controllers diff --git a/src/Plugins/SmartStore.Tax/Description.txt b/src/Plugins/SmartStore.Tax/Description.txt index 245ae96884..ec211f9efe 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: 2.2.0 -MinAppVersion: 2.2.0 +Version: 2.6.0 +MinAppVersion: 2.5.0 DisplayOrder: 0 FileName: SmartStore.Tax.dll ResourceRootKey: Plugins.SmartStore.Tax diff --git a/src/Plugins/SmartStore.Tax/Models/ByRegionTaxRateListModel.cs b/src/Plugins/SmartStore.Tax/Models/ByRegionTaxRateListModel.cs index 75af2400f8..35cd18a325 100644 --- a/src/Plugins/SmartStore.Tax/Models/ByRegionTaxRateListModel.cs +++ b/src/Plugins/SmartStore.Tax/Models/ByRegionTaxRateListModel.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Web.Mvc; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Tax.Models { diff --git a/src/Plugins/SmartStore.Tax/Models/ByRegionTaxRateModel.cs b/src/Plugins/SmartStore.Tax/Models/ByRegionTaxRateModel.cs index 058f50a138..70d4decd1e 100644 --- a/src/Plugins/SmartStore.Tax/Models/ByRegionTaxRateModel.cs +++ b/src/Plugins/SmartStore.Tax/Models/ByRegionTaxRateModel.cs @@ -1,5 +1,5 @@ using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Tax.Models { diff --git a/src/Plugins/SmartStore.Tax/RouteProvider.cs b/src/Plugins/SmartStore.Tax/RouteProvider.cs index 4b45008a97..120a251043 100644 --- a/src/Plugins/SmartStore.Tax/RouteProvider.cs +++ b/src/Plugins/SmartStore.Tax/RouteProvider.cs @@ -1,6 +1,6 @@ using System.Web.Mvc; using System.Web.Routing; -using SmartStore.Web.Framework.Mvc.Routes; +using SmartStore.Web.Framework.Routing; namespace SmartStore.Tax { diff --git a/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj b/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj index 08e8670fa0..3f4f325f97 100644 --- a/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj +++ b/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj @@ -42,6 +42,7 @@ + true @@ -81,9 +82,8 @@ MinimumRecommendedRules.ruleset - - ..\..\packages\Autofac.3.4.1\lib\net40\Autofac.dll - False + + ..\..\packages\Autofac.3.5.2\lib\net40\Autofac.dll False @@ -96,8 +96,9 @@ ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True diff --git a/src/Plugins/SmartStore.Tax/Views/TaxByRegion/Configure.cshtml b/src/Plugins/SmartStore.Tax/Views/TaxByRegion/Configure.cshtml index 5b8d09d71d..f3b670b3b2 100644 --- a/src/Plugins/SmartStore.Tax/Views/TaxByRegion/Configure.cshtml +++ b/src/Plugins/SmartStore.Tax/Views/TaxByRegion/Configure.cshtml @@ -1,10 +1,11 @@ -@{ - Layout = ""; -} -@model SmartStore.Tax.Models.ByRegionTaxRateListModel -@using SmartStore.Web.Framework; +@using SmartStore.Web.Framework; @using Telerik.Web.Mvc.UI; @using System.Linq; +@model SmartStore.Tax.Models.ByRegionTaxRateListModel +@{ + Layout = ""; +} +
    @@ -46,8 +47,8 @@
    -

    -

    + +

    @using (Html.BeginForm()) -{ -
    -

    @T("Plugins.Tax.CountryStateZip.AddRecord.Hint")

    -
    - +{ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + +
    - @Html.SmartLabelFor(model => model.AddCountryId) - - @Html.DropDownListFor(model => model.AddCountryId, Model.AvailableCountries) - @Html.ValidationMessageFor(model => model.AddCountryId) -
    - @Html.SmartLabelFor(model => model.AddStateProvinceId) - - @Html.DropDownListFor(model => model.AddStateProvinceId, Model.AvailableStates) - @Html.ValidationMessageFor(model => model.AddStateProvinceId) -
    - @Html.SmartLabelFor(model => model.AddZip) - - @Html.EditorFor(model => model.AddZip) - @Html.ValidationMessageFor(model => model.AddZip) -
    - @Html.SmartLabelFor(model => model.AddTaxCategoryId) - - @Html.DropDownListFor(model => model.AddTaxCategoryId, Model.AvailableTaxCategories) - @Html.ValidationMessageFor(model => model.AddTaxCategoryId) -
    - @Html.SmartLabelFor(model => model.AddPercentage) - - @Html.EditorFor(model => model.AddPercentage) - @Html.ValidationMessageFor(model => model.AddPercentage) -
    -   - - -
    +
    +
    @T("Plugins.Tax.CountryStateZip.AddRecord.Hint")
    +
    +
    + @Html.SmartLabelFor(model => model.AddCountryId) + + @Html.DropDownListFor(model => model.AddCountryId, Model.AvailableCountries) + @Html.ValidationMessageFor(model => model.AddCountryId) +
    + @Html.SmartLabelFor(model => model.AddStateProvinceId) + + @Html.DropDownListFor(model => model.AddStateProvinceId, Model.AvailableStates) + @Html.ValidationMessageFor(model => model.AddStateProvinceId) +
    + @Html.SmartLabelFor(model => model.AddZip) + + @Html.EditorFor(model => model.AddZip) + @Html.ValidationMessageFor(model => model.AddZip) +
    + @Html.SmartLabelFor(model => model.AddTaxCategoryId) + + @Html.DropDownListFor(model => model.AddTaxCategoryId, Model.AvailableTaxCategories) + @Html.ValidationMessageFor(model => model.AddTaxCategoryId) +
    + @Html.SmartLabelFor(model => model.AddPercentage) + + @Html.EditorFor(model => model.AddPercentage) + @Html.ValidationMessageFor(model => model.AddPercentage) +
    +   + + +
    } \ No newline at end of file diff --git a/src/Plugins/SmartStore.Tax/Views/Web.config b/src/Plugins/SmartStore.Tax/Views/Web.config index 31dc8df754..e9d36a3c51 100644 --- a/src/Plugins/SmartStore.Tax/Views/Web.config +++ b/src/Plugins/SmartStore.Tax/Views/Web.config @@ -14,7 +14,7 @@ - + diff --git a/src/Plugins/SmartStore.Tax/packages.config b/src/Plugins/SmartStore.Tax/packages.config index bce2006cf4..7f794ac209 100644 --- a/src/Plugins/SmartStore.Tax/packages.config +++ b/src/Plugins/SmartStore.Tax/packages.config @@ -1,10 +1,10 @@  - + - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.Tax/web.config b/src/Plugins/SmartStore.Tax/web.config index 46b8ba77d4..ba87d0f098 100644 --- a/src/Plugins/SmartStore.Tax/web.config +++ b/src/Plugins/SmartStore.Tax/web.config @@ -1,117 +1,117 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - \ No newline at end of file + diff --git a/src/Plugins/SmartStore.WebApi/Controllers/Api/UploadsController.cs b/src/Plugins/SmartStore.WebApi/Controllers/Api/UploadsController.cs index 2d4e879f22..d2b92900b9 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/Api/UploadsController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/Api/UploadsController.cs @@ -1,16 +1,24 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Compression; using System.Linq; using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; using System.Threading.Tasks; using System.Web.Http; using SmartStore.Core; +using SmartStore.Core.Domain; using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.DataExchange; using SmartStore.Core.Domain.Media; +using SmartStore.Core.IO; using SmartStore.Services.Catalog; +using SmartStore.Services.DataExchange.Import; using SmartStore.Services.Media; using SmartStore.Utilities; +using SmartStore.Utilities.Threading; using SmartStore.Web.Framework.WebApi; using SmartStore.Web.Framework.WebApi.OData; using SmartStore.Web.Framework.WebApi.Security; @@ -18,29 +26,56 @@ namespace SmartStore.WebApi.Controllers.Api { + /// public class UploadsController : ApiController { + private static readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); + private readonly Lazy _productService; private readonly Lazy _pictureService; + private readonly Lazy _importProfileService; private readonly Lazy _storeContext; private readonly Lazy _mediaSettings; public UploadsController( Lazy productService, Lazy pictureService, + Lazy importProfileService, Lazy storeContext, Lazy mediaSettings) { _productService = productService; _pictureService = pictureService; + _importProfileService = importProfileService; _storeContext = storeContext; _mediaSettings = mediaSettings; } - /// + #region Utilities + + private StringContent CloneHeaderContent(string path, MultipartFileData origin) + { + var content = new StringContent(path); + + ContentDispositionHeaderValue disposition; + ContentDispositionHeaderValue.TryParse(origin.Headers.ContentDisposition.ToString(), out disposition); + + content.Headers.ContentDisposition = disposition; + + content.Headers.ContentDisposition.Name = origin.Headers.ContentDisposition.Name.ToUnquoted(); + content.Headers.ContentDisposition.FileName = Path.GetFileName(path); + + content.Headers.ContentType.MediaType = MimeTypes.MapNameToMimeType(path); + + return content; + } + + #endregion + + [HttpPost] [WebApiAuthenticate(Permission = "ManageCatalog")] [WebApiQueryable(PagingOptional = true)] - public async Task> PostProductImages() + public async Task> ProductImages() { if (!Request.Content.IsMimeMultipartContent()) { @@ -56,10 +91,10 @@ public async Task> PostProductImages() { await Request.Content.ReadAsMultipartAsync(provider); } - catch (Exception exc) + catch (Exception exception) { provider.DeleteLocalFiles(); - throw this.ExceptionInternalServerError(exc); + throw this.ExceptionInternalServerError(exception); } // find product entity @@ -82,7 +117,7 @@ public async Task> PostProductImages() if (entity == null) { provider.DeleteLocalFiles(); - throw this.ExceptionNotFound(WebApiGlobal.Error.EntityNotFound.FormatWith(identifier.NaIfEmpty())); + throw this.ExceptionNotFound(WebApiGlobal.Error.EntityNotFound.FormatInvariant(identifier.NaIfEmpty())); } // process images @@ -97,15 +132,7 @@ public async Task> PostProductImages() foreach (var file in provider.FileData) { - var image = new UploadImage - { - FileName = file.Headers.ContentDisposition.FileName.ToUnquoted(), - Name = file.Headers.ContentDisposition.Name.ToUnquoted(), - ContentDisposition = file.Headers.ContentDisposition.Parameters - }; - - if (file.Headers.ContentType != null) - image.MediaType = file.Headers.ContentType.MediaType.ToUnquoted(); + var image = new UploadImage(file.Headers); if (image.FileName.IsEmpty()) image.FileName = entity.Name; @@ -115,7 +142,6 @@ public async Task> PostProductImages() if (pictureBinary != null && pictureBinary.Length > 0) { pictureBinary = _pictureService.Value.ValidatePicture(pictureBinary); - pictureBinary = _pictureService.Value.FindEqualPicture(pictureBinary, pictures, out equalPictureId); if (pictureBinary != null) @@ -157,5 +183,121 @@ public async Task> PostProductImages() provider.DeleteLocalFiles(); return result.AsQueryable(); } + + [HttpPost] + [WebApiAuthenticate(Permission = "ManageImports")] + [WebApiQueryable(PagingOptional = true)] + public async Task> ImportFiles() + { + if (!Request.Content.IsMimeMultipartContent()) + { + throw this.ExceptionUnsupportedMediaType(); + } + + ImportProfile profile = null; + string identifier = null; + var tempDir = FileSystemHelper.TempDir(Guid.NewGuid().ToString()); + var provider = new MultipartFormDataStreamProvider(tempDir); + + try + { + await Request.Content.ReadAsMultipartAsync(provider); + } + catch (Exception exception) + { + FileSystemHelper.ClearDirectory(tempDir, true); + throw this.ExceptionInternalServerError(exception); + } + + // find import profile + if (provider.FormData.AllKeys.Contains("Id")) + { + identifier = provider.FormData.GetValues("Id").FirstOrDefault(); + profile = _importProfileService.Value.GetImportProfileById(identifier.ToInt()); + } + else if (provider.FormData.AllKeys.Contains("Name")) + { + identifier = provider.FormData.GetValues("Name").FirstOrDefault(); + profile = _importProfileService.Value.GetImportProfileByName(identifier); + } + + if (profile == null) + { + FileSystemHelper.ClearDirectory(tempDir, true); + throw this.ExceptionNotFound(WebApiGlobal.Error.EntityNotFound.FormatInvariant(identifier.NaIfEmpty())); + } + + var deleteExisting = false; + var result = new List(); + var unzippedFiles = new List(); + var importFolder = profile.GetImportFolder(true, true); + var csvTypes = new string[] { ".csv", ".txt", ".tab" }; + + if (provider.FormData.AllKeys.Contains("deleteExisting")) + { + var strDeleteExisting = provider.FormData.GetValues("deleteExisting").FirstOrDefault(); + deleteExisting = (strDeleteExisting.HasValue() && strDeleteExisting.ToBool()); + } + + // unzip files + foreach (var file in provider.FileData) + { + var import = new UploadImportFile(file.Headers); + + if (import.FileExtension.IsCaseInsensitiveEqual(".zip")) + { + var subDir = Path.Combine(tempDir, Guid.NewGuid().ToString()); + ZipFile.ExtractToDirectory(file.LocalFileName, subDir); + FileSystemHelper.Delete(file.LocalFileName); + + foreach (var unzippedFile in Directory.GetFiles(subDir, "*.*")) + { + var content = CloneHeaderContent(unzippedFile, file); + unzippedFiles.Add(new MultipartFileData(content.Headers, unzippedFile)); + } + } + else + { + unzippedFiles.Add(new MultipartFileData(file.Headers, file.LocalFileName)); + } + } + + // copy files to import folder + if (unzippedFiles.Any()) + { + using (_rwLock.GetWriteLock()) + { + if (deleteExisting) + { + FileSystemHelper.ClearDirectory(importFolder, false); + } + + foreach (var file in unzippedFiles) + { + var import = new UploadImportFile(file.Headers); + var destPath = Path.Combine(importFolder, import.FileName); + + import.Exists = File.Exists(destPath); + + switch (profile.FileType) + { + case ImportFileType.XLSX: + import.IsSupportedByProfile = import.FileExtension.IsCaseInsensitiveEqual(".xlsx"); + break; + case ImportFileType.CSV: + import.IsSupportedByProfile = csvTypes.Contains(import.FileExtension, StringComparer.OrdinalIgnoreCase); + break; + } + + import.Inserted = FileSystemHelper.Copy(file.LocalFileName, destPath); + + result.Add(import); + } + } + } + + FileSystemHelper.ClearDirectory(tempDir, true); + return result.AsQueryable(); + } } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/OrdersController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/OrdersController.cs index d633274612..f0f1c0eb9b 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/OrdersController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/OrdersController.cs @@ -95,24 +95,26 @@ public IQueryable GetOrderItems(int key) // actions [HttpPost] - public Order PaymentPending(int key) + public SingleResult PaymentPending(int key) { - var entity = GetEntityByKeyNotNull(key); + var result = GetSingleResult(key); + var order = GetExpandedEntity(key, result, null); this.ProcessEntity(() => { - entity.PaymentStatus = PaymentStatus.Pending; - Service.UpdateOrder(entity); + order.PaymentStatus = PaymentStatus.Pending; + Service.UpdateOrder(order); return null; }); - return entity; + return result; } [HttpPost] - public Order PaymentPaid(int key, ODataActionParameters parameters) + public SingleResult PaymentPaid(int key, ODataActionParameters parameters) { - var entity = GetEntityByKeyNotNull(key); + var result = GetSingleResult(key); + var order = GetExpandedEntity(key, result, null); this.ProcessEntity(() => { @@ -120,20 +122,23 @@ public Order PaymentPaid(int key, ODataActionParameters parameters) if (paymentMethodName != null) { - entity.PaymentMethodSystemName = paymentMethodName; - Service.UpdateOrder(entity); + order.PaymentMethodSystemName = paymentMethodName; + Service.UpdateOrder(order); } - _orderProcessingService.Value.MarkOrderAsPaid(entity); + _orderProcessingService.Value.MarkOrderAsPaid(order); + return null; }); - return entity; + + return result; } [HttpPost] - public Order PaymentRefund(int key, ODataActionParameters parameters) + public SingleResult PaymentRefund(int key, ODataActionParameters parameters) { - var entity = GetEntityByKeyNotNull(key); + var result = GetSingleResult(key); + var order = GetExpandedEntity(key, result, null); this.ProcessEntity(() => { @@ -141,32 +146,61 @@ public Order PaymentRefund(int key, ODataActionParameters parameters) if (online) { - var errors = _orderProcessingService.Value.Refund(entity); + var errors = _orderProcessingService.Value.Refund(order); if (errors.Count > 0) return errors[0]; } else { - _orderProcessingService.Value.RefundOffline(entity); + _orderProcessingService.Value.RefundOffline(order); } return null; }); - return entity; + + return result; + } + + [HttpPost] + public SingleResult Cancel(int key) + { + var result = GetSingleResult(key); + var order = GetExpandedEntity(key, result, "OrderItems, OrderItems.Product"); + + this.ProcessEntity(() => + { + _orderProcessingService.Value.CancelOrder(order, true); + + return null; + }); + + return result; } [HttpPost] - public Order Cancel(int key) + public SingleResult AddShipment(int key, ODataActionParameters parameters) { - var entity = GetEntityByKeyNotNull(key); + var result = GetSingleResult(key); + var order = GetExpandedEntity(key, result, "OrderItems, OrderItems.Product, Shipments, Shipments.ShipmentItems"); this.ProcessEntity(() => { - _orderProcessingService.Value.CancelOrder(entity, true); + if (order.HasItemsToAddToShipment()) + { + var trackingNumber = parameters.GetValue("TrackingNumber"); + + var shipment = _orderProcessingService.Value.AddShipment(order, trackingNumber, null); + if (shipment != null) + { + if (parameters.ContainsKey("SetAsShipped") && parameters.GetValue("SetAsShipped")) + _orderProcessingService.Value.Ship(shipment, true); + } + } return null; }); - return entity; + + return result; } } } diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductsController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductsController.cs index 84d3f2fc09..df8dfd389d 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductsController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductsController.cs @@ -236,6 +236,7 @@ public IQueryable GetProductBundleItems(int key) { var searchContext = new ProductSearchContext { + OrderBy = ProductSortingEnum.Position, Query = this.GetExpandedEntitySet(requiredProperties), ParentGroupedProductId = entity.Id, PageSize = int.MaxValue, diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/ShipmentItemsController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/ShipmentItemsController.cs new file mode 100644 index 0000000000..47fdf9982a --- /dev/null +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/ShipmentItemsController.cs @@ -0,0 +1,32 @@ +using System.Web.Http; +using SmartStore.Core.Domain.Shipping; +using SmartStore.Services.Shipping; +using SmartStore.Web.Framework.WebApi; +using SmartStore.Web.Framework.WebApi.OData; +using SmartStore.Web.Framework.WebApi.Security; + +namespace SmartStore.WebApi.Controllers.OData +{ + [WebApiAuthenticate(Permission = "ManageOrders")] + public class ShipmentItemsController : WebApiEntityController + { + protected override void Insert(ShipmentItem entity) + { + Service.InsertShipmentItem(entity); + } + protected override void Update(ShipmentItem entity) + { + Service.UpdateShipmentItem(entity); + } + protected override void Delete(ShipmentItem entity) + { + Service.DeleteShipmentItem(entity); + } + + [WebApiQueryable] + public SingleResult GetShipmentItem(int key) + { + return GetSingleResult(key); + } + } +} diff --git a/src/Plugins/SmartStore.WebApi/Controllers/WebApiController.cs b/src/Plugins/SmartStore.WebApi/Controllers/WebApiController.cs index 58e3d49117..4fb254bbff 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/WebApiController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/WebApiController.cs @@ -4,6 +4,8 @@ using SmartStore.Core.Domain.Common; using SmartStore.Services; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Filters; +using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.WebApi; using SmartStore.Web.Framework.WebApi.Caching; using SmartStore.WebApi.Models; @@ -95,7 +97,7 @@ public ActionResult SaveGeneralSettings(WebApiConfigModel model) public ActionResult GridUserData(GridCommand command) { if (!HasPermission()) - return new JsonResult { Data = new GridModel { Data = new List() }}; + return new JsonResult { Data = new GridModel { Data = new List() } }; var model = _webApiPluginService.GetGridModel(command.Page - 1, command.PageSize); diff --git a/src/Plugins/SmartStore.WebApi/Description.txt b/src/Plugins/SmartStore.WebApi/Description.txt index cbe8fe9af5..646ac2fd0d 100644 --- a/src/Plugins/SmartStore.WebApi/Description.txt +++ b/src/Plugins/SmartStore.WebApi/Description.txt @@ -1,8 +1,8 @@ FriendlyName: SmartStore.NET Web Api SystemName: SmartStore.WebApi -Version: 2.2.0.3 +Version: 2.6.0 Group: Api -MinAppVersion: 2.2.0 +MinAppVersion: 2.5.0 Author: SmartStore AG DisplayOrder: 1 FileName: SmartStore.WebApi.dll diff --git a/src/Plugins/SmartStore.WebApi/Localization/resources.de-de.xml b/src/Plugins/SmartStore.WebApi/Localization/resources.de-de.xml index a7215f44e6..650e50d1ab 100644 --- a/src/Plugins/SmartStore.WebApi/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.WebApi/Localization/resources.de-de.xml @@ -1,5 +1,5 @@  - + Web-API @@ -30,10 +30,10 @@ Aus Sicherheitsgründen darf die Zeit, zu der die Anfrage an die API gesendet wurde, nicht zu weit von der aktuellen Server-Zeit abweichen. Mit dieser Einstellung legen Sie dieses Zeitfenster in Minuten fest. - Unauthorisierte Zugriffe speichern + Unautorisierte Zugriffe speichern - Jeder unauthorisierte Zugriff wird in der Ereignisliste gespeichert. + Jeder unautorisierte Zugriff wird in der Ereignisliste gespeichert. Keine Zeitstempelprüfung @@ -41,6 +41,12 @@ Legt fest, ob geprüft werden soll, ob der Zeitstempel der letzten Anfrage kleiner gleich der der aktuellen ist. Verhindert das Auftreten von HmacResult.TimestampOlderThanLastRequest. + + Authentifizierung ohne MD5-Hash erlauben + + + Legt fest, ob Authentifizierungen ohne MD5 Inhalts-Hash erlaubt sind. + Öffentlicher Schlüssel diff --git a/src/Plugins/SmartStore.WebApi/Localization/resources.en-us.xml b/src/Plugins/SmartStore.WebApi/Localization/resources.en-us.xml index 253bdb5938..7aafa97635 100644 --- a/src/Plugins/SmartStore.WebApi/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.WebApi/Localization/resources.en-us.xml @@ -1,5 +1,5 @@  - + Web-API @@ -40,6 +40,12 @@ Specifies whether to validate that the timestamp of the previous request is less or equal than the one of the current. Prevents the occurrence of HmacResult.TimestampOlderThanLastRequest. + + + Allow authentification without MD5 hash + + + Specifies whether to allow authentifications without MD5 content hash. Public key diff --git a/src/Plugins/SmartStore.WebApi/Models/Api/UploadFileBase.cs b/src/Plugins/SmartStore.WebApi/Models/Api/UploadFileBase.cs new file mode 100644 index 0000000000..d0884fa6a3 --- /dev/null +++ b/src/Plugins/SmartStore.WebApi/Models/Api/UploadFileBase.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.IO; +using System.Net.Http.Headers; +using System.Runtime.Serialization; + +namespace SmartStore.WebApi.Models.Api +{ + [DataContract] + public abstract partial class UploadFileBase + { + public UploadFileBase() + { + } + + public UploadFileBase(HttpContentHeaders headers) + { + Name = headers.ContentDisposition.Name.ToUnquoted(); + FileName = headers.ContentDisposition.FileName.ToUnquoted(); + ContentDisposition = headers.ContentDisposition.Parameters; + + if (headers.ContentType != null) + { + MediaType = headers.ContentType.MediaType.ToUnquoted(); + } + + if (FileName.HasValue()) + { + FileExtension = Path.GetExtension(FileName); + } + } + + /// + /// Unquoted name attribute of content-disposition multipart header + /// + [DataMember] + public string Name { get; set; } + + /// + /// Unquoted filename attribute of content-disposition multipart header + /// + [DataMember] + public string FileName { get; set; } + + /// + /// Extension of FileName + /// + [DataMember] + public string FileExtension { get; set; } + + /// + /// Media (mime) type of content-type multipart header + /// + [DataMember] + public string MediaType { get; set; } + + /// + /// Indicates whether the uploaded file already exist + /// + [DataMember] + public bool Exists { get; set; } + + /// + /// Indicates whether the uploaded file has been inserted + /// + [DataMember] + public bool Inserted { get; set; } + + /// + /// Raw custom parameters of the content-disposition multipart header + /// + [DataMember] + public ICollection ContentDisposition { get; set; } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.WebApi/Models/Api/UploadImage.cs b/src/Plugins/SmartStore.WebApi/Models/Api/UploadImage.cs index 5ad5933fc9..cca701246b 100644 --- a/src/Plugins/SmartStore.WebApi/Models/Api/UploadImage.cs +++ b/src/Plugins/SmartStore.WebApi/Models/Api/UploadImage.cs @@ -1,42 +1,19 @@ -using System.Collections.Generic; -using System.Net.Http.Headers; +using System.Net.Http.Headers; using System.Runtime.Serialization; using SmartStore.Core.Domain.Media; namespace SmartStore.WebApi.Models.Api { [DataContract] - public partial class UploadImage + public partial class UploadImage : UploadFileBase { - /// - /// Unquoted name attribute of content-disposition multipart header - /// - [DataMember] - public string Name { get; set; } - - /// - /// Unquoted filename attribute of content-disposition multipart header - /// - [DataMember] - public string FileName { get; set; } - - /// - /// Media (mime) type of content-type multipart header - /// - [DataMember] - public string MediaType { get; set; } + public UploadImage() + { + } - /// - /// Indicates whether the uploaded image already exist and therefore has been skipped - /// - [DataMember] - public bool Exists { get; set; } - - /// - /// Indicates whether the uploaded image has been inserted - /// - [DataMember] - public bool Inserted { get; set; } + public UploadImage(HttpContentHeaders headers) : base(headers) + { + } /// /// Url of the default size image @@ -56,12 +33,6 @@ public partial class UploadImage [DataMember] public string FullSizeImageUrl { get; set; } - /// - /// Raw custom parameters of the content-disposition multipart header - /// - [DataMember] - public ICollection ContentDisposition { get; set; } - /// /// The picture entity. Can be null. /// diff --git a/src/Plugins/SmartStore.WebApi/Models/Api/UploadImportFile.cs b/src/Plugins/SmartStore.WebApi/Models/Api/UploadImportFile.cs new file mode 100644 index 0000000000..ac43524577 --- /dev/null +++ b/src/Plugins/SmartStore.WebApi/Models/Api/UploadImportFile.cs @@ -0,0 +1,23 @@ +using System.Net.Http.Headers; +using System.Runtime.Serialization; + +namespace SmartStore.WebApi.Models.Api +{ + [DataContract] + public partial class UploadImportFile : UploadFileBase + { + public UploadImportFile() + { + } + + public UploadImportFile(HttpContentHeaders headers) : base(headers) + { + } + + /// + /// Whether the file type is supported by the import profile + /// + [DataMember] + public bool IsSupportedByProfile { get; set; } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.WebApi/Models/WebApiConfigModel.cs b/src/Plugins/SmartStore.WebApi/Models/WebApiConfigModel.cs index 7dfb61fc69..23882c149e 100644 --- a/src/Plugins/SmartStore.WebApi/Models/WebApiConfigModel.cs +++ b/src/Plugins/SmartStore.WebApi/Models/WebApiConfigModel.cs @@ -1,5 +1,5 @@ using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Framework.WebApi; namespace SmartStore.WebApi.Models @@ -18,7 +18,10 @@ public class WebApiConfigModel : ModelBase [SmartResourceDisplayName("Plugins.Api.WebApi.NoRequestTimestampValidation")] public bool NoRequestTimestampValidation { get; set; } - [SmartResourceDisplayName("Plugins.Api.WebApi.LogUnauthorized")] + [SmartResourceDisplayName("Plugins.Api.WebApi.AllowEmptyMd5Hash")] + public bool AllowEmptyMd5Hash { get; set; } + + [SmartResourceDisplayName("Plugins.Api.WebApi.LogUnauthorized")] public bool LogUnauthorized { get; set; } public int GridPageSize { get; set; } @@ -29,12 +32,14 @@ public void Copy(WebApiSettings settings, bool fromSettings) { ValidMinutePeriod = settings.ValidMinutePeriod; NoRequestTimestampValidation = settings.NoRequestTimestampValidation; + AllowEmptyMd5Hash = settings.AllowEmptyMd5Hash; LogUnauthorized = settings.LogUnauthorized; } else { settings.ValidMinutePeriod = ValidMinutePeriod; settings.NoRequestTimestampValidation = NoRequestTimestampValidation; + settings.AllowEmptyMd5Hash = AllowEmptyMd5Hash; settings.LogUnauthorized = LogUnauthorized; } } diff --git a/src/Plugins/SmartStore.WebApi/Models/WebApiUserModel.cs b/src/Plugins/SmartStore.WebApi/Models/WebApiUserModel.cs index 8888f3f803..2694f9166b 100644 --- a/src/Plugins/SmartStore.WebApi/Models/WebApiUserModel.cs +++ b/src/Plugins/SmartStore.WebApi/Models/WebApiUserModel.cs @@ -1,6 +1,6 @@ using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; using System; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.WebApi.Models { diff --git a/src/Plugins/SmartStore.WebApi/RouteProvider.cs b/src/Plugins/SmartStore.WebApi/RouteProvider.cs index 032a806c71..1ce5fb95cb 100644 --- a/src/Plugins/SmartStore.WebApi/RouteProvider.cs +++ b/src/Plugins/SmartStore.WebApi/RouteProvider.cs @@ -1,6 +1,6 @@ using System.Web.Mvc; using System.Web.Routing; -using SmartStore.Web.Framework.Mvc.Routes; +using SmartStore.Web.Framework.Routing; using SmartStore.Web.Framework.WebApi; namespace SmartStore.WebApi diff --git a/src/Plugins/SmartStore.WebApi/SmartStore.WebApi.csproj b/src/Plugins/SmartStore.WebApi/SmartStore.WebApi.csproj index feaadf8fcc..73e315200d 100644 --- a/src/Plugins/SmartStore.WebApi/SmartStore.WebApi.csproj +++ b/src/Plugins/SmartStore.WebApi/SmartStore.WebApi.csproj @@ -78,28 +78,23 @@ MinimumRecommendedRules.ruleset - - ..\..\packages\Autofac.3.4.1\lib\net40\Autofac.dll + + ..\..\packages\Autofac.3.5.2\lib\net40\Autofac.dll - - ..\..\packages\Autofac.Mvc5.3.3.1\lib\net45\Autofac.Integration.Mvc.dll + + ..\..\packages\Autofac.Mvc5.3.3.4\lib\net45\Autofac.Integration.Mvc.dll ..\..\packages\Autofac.WebApi.3.1.0\lib\net40\Autofac.Integration.WebApi.dll - - ..\..\packages\AutoMapper.3.2.1\lib\net40\AutoMapper.dll - - - ..\..\packages\AutoMapper.3.2.1\lib\net40\AutoMapper.Net4.dll + + ..\..\packages\AutoMapper.4.1.1\lib\net45\AutoMapper.dll ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll - True ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll - True False @@ -112,12 +107,14 @@ ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True + False @@ -215,6 +212,7 @@ + @@ -233,7 +231,9 @@ + + @@ -323,7 +323,6 @@ - - +
    @Html.Partial("_LastRun", task)
    - +
    @Html.Partial("_NextRun", task)
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/MinimalTask.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/MinimalTask.cshtml index 5842a4185d..530a08f62e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/MinimalTask.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/MinimalTask.cshtml @@ -5,6 +5,7 @@ var returnUrl = (string)ViewBag.ReturnUrl; var hasPermission = ViewBag.HasPermission == true; var cancellable = ViewBag.Cancellable == true; + var reloadPage = ViewBag.ReloadPage == true; }
    @@ -33,16 +34,25 @@ onTaskCompleted: function (taskId, el) { el.prev().removeClass('hide'); // .task-info el.next().addClass('hide'); // .btn-cancel-task - $.ajax({ - cache: false, - global: false, - type: 'POST', - url: '@Url.Action("GetMinimalTaskWidget")', - data: { taskId: taskId, returnUrl: '@returnUrl' }, - success: function (data) { - el.prev().html(data); - } - }); + + @if (reloadPage) + { + location.href = '@returnUrl'; + } + else + { + $.ajax({ + cache: false, + global: false, + type: 'POST', + url: '@Url.Action("GetMinimalTaskWidget")', + data: { taskId: taskId, returnUrl: '@returnUrl' }, + success: function (data) { + el.prev().html(data); + } + }); + } + } }); }); diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Security/Permissions.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Security/Permissions.cshtml index 0d02bfd4b2..743c8f9d8f 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Security/Permissions.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Security/Permissions.cshtml @@ -1,6 +1,6 @@ -@model PermissionMappingModel +@using SmartStore.Utilities; +@model PermissionMappingModel @{ - //page title ViewBag.Title = T("Admin.Configuration.ACL").Text; } @using (Html.BeginForm()) @@ -11,7 +11,9 @@ @T("Admin.Configuration.ACL")
    - +
    @@ -19,12 +21,16 @@ @if (Model.AvailablePermissions.Count == 0) - { - No permissions defined - } - else if (Model.AvailableCustomerRoles.Count == 0) - { - No customer roles available + { +
    + @T("Admin.System.Warnings.NoPermissionsDefined") +
    + } + else if (Model.AvailableCustomerRoles.Count == 0) + { +
    + @T("Admin.System.Warnings.NoCustomerRolesDefined") +
    } else { @@ -48,6 +54,7 @@ { + @Inflector.Titleize(pr.Category).NaIfEmpty() @pr.Name @foreach (var cr in Model.AvailableCustomerRoles) @@ -66,5 +73,4 @@ - } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml index 3e8d9e4a19..56664aafbb 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml @@ -30,7 +30,10 @@ $("#@Html.FieldIdFor(model => model.ProductSearchAutoCompleteEnabled)").click(toggleProductSearchAutoComplete); $("#@Html.FieldIdFor(model => model.CompareProductsEnabled)").click(toggleCompareProducts); $("#@Html.FieldIdFor(model => model.EnableHtmlTextCollapser)").click(toggleHtmlTextCollapsedHeight); - $("#@Html.FieldIdFor(model => model.FilterEnabled)").click(toggleFilterEnabled); + + $("#@Html.FieldIdFor(model => model.FilterEnabled)").change(function () { + $('#ProductFilterTable').find('.product-filter-option').toggle($(this).is(':checked')); + }).trigger('change'); toggleShowCategoryProductNumberIncludingSubcategories(); toggleEmailAFriend(); @@ -43,7 +46,6 @@ toggleProductSearchAutoComplete(); toggleCompareProducts(); toggleHtmlTextCollapsedHeight(); - toggleFilterEnabled(); toggleManufacturersOnHomepage(); }); @@ -157,16 +159,6 @@ $('#pnlHtmlTextCollapsedHeight').hide(); } } - - function toggleFilterEnabled() { - if ($('#@Html.FieldIdFor(model => model.FilterEnabled)').is(':checked')) { - $('#pnlMaxFilterItemsToDisplay').show(); - $('#pnlExpandAllFilterCriteria').show(); - } else { - $('#pnlMaxFilterItemsToDisplay').hide(); - $('#pnlExpandAllFilterCriteria').hide(); - } - } @Html.Action("StoreScopeConfiguration", "Setting") @@ -257,8 +249,7 @@ @Html.ValidationMessageFor(model => model.IgnoreFeaturedProducts) - - + @Html.SmartLabelFor(model => model.CompareProductsEnabled) @@ -331,7 +322,33 @@ @Html.ValidationMessageFor(model => model.ShowManufacturerPictures) - + + + @Html.SmartLabelFor(model => model.HideManufacturerDefaultPictures) + + + @Html.SettingEditorFor(model => model.HideManufacturerDefaultPictures) + @Html.ValidationMessageFor(model => model.HideManufacturerDefaultPictures) + + + + + @Html.SmartLabelFor(model => model.HideCategoryDefaultPictures) + + + @Html.SettingEditorFor(model => model.HideCategoryDefaultPictures) + @Html.ValidationMessageFor(model => model.HideCategoryDefaultPictures) + + + + + @Html.SmartLabelFor(model => model.HideProductDefaultPictures) + + + @Html.SettingEditorFor(model => model.HideProductDefaultPictures) + @Html.ValidationMessageFor(model => model.HideProductDefaultPictures) + + @@ -365,257 +382,289 @@ @helper TabProductListSettings() { -
    - @T("Common.Navigation") - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
    - @Html.SmartLabelFor(model => model.ShowProductsFromSubcategories) - - @Html.SettingEditorFor(model => model.ShowProductsFromSubcategories) - @Html.ValidationMessageFor(model => model.ShowProductsFromSubcategories) -
    - @Html.SmartLabelFor(model => model.IncludeFeaturedProductsInNormalLists) - - @Html.SettingEditorFor(model => model.IncludeFeaturedProductsInNormalLists) - @Html.ValidationMessageFor(model => model.IncludeFeaturedProductsInNormalLists) -
    - @Html.SmartLabelFor(model => model.ShowCategoryProductNumber) - - @Html.SettingEditorFor(model => model.ShowCategoryProductNumber) - @Html.ValidationMessageFor(model => model.ShowCategoryProductNumber) -
    - @Html.SmartLabelFor(model => model.ShowCategoryProductNumberIncludingSubcategories) - - @Html.SettingEditorFor(model => model.ShowCategoryProductNumberIncludingSubcategories) - @Html.ValidationMessageFor(model => model.ShowCategoryProductNumberIncludingSubcategories) -
    - @Html.SmartLabelFor(model => model.CategoryBreadcrumbEnabled) - - @Html.SettingEditorFor(model => model.CategoryBreadcrumbEnabled) - @Html.ValidationMessageFor(model => model.CategoryBreadcrumbEnabled) -
    - @Html.SmartLabelFor(model => model.FilterEnabled) - - @Html.SettingEditorFor(model => model.FilterEnabled) - @Html.ValidationMessageFor(model => model.FilterEnabled) -
    - @Html.SmartLabelFor(model => model.MaxFilterItemsToDisplay) - - @Html.SettingEditorFor(model => model.MaxFilterItemsToDisplay) - @Html.ValidationMessageFor(model => model.MaxFilterItemsToDisplay) -
    - @Html.SmartLabelFor(model => model.ExpandAllFilterCriteria) - - @Html.SettingEditorFor(model => model.ExpandAllFilterCriteria) - @Html.ValidationMessageFor(model => model.ExpandAllFilterCriteria) -
    - @Html.SmartLabelFor(model => model.SubCategoryDisplayType) - - @Html.SettingOverrideCheckbox(model => Model.SubCategoryDisplayType) - @Html.DropDownListFor(model => model.SubCategoryDisplayType, Model.AvailableSubCategoryDisplayTypes) - @Html.ValidationMessageFor(model => model.SubCategoryDisplayType) -
    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    @T("Common.Navigation")
    +
    +
    + @Html.SmartLabelFor(model => model.ShowProductsFromSubcategories) + + @Html.SettingEditorFor(model => model.ShowProductsFromSubcategories) + @Html.ValidationMessageFor(model => model.ShowProductsFromSubcategories) +
    + @Html.SmartLabelFor(model => model.IncludeFeaturedProductsInNormalLists) + + @Html.SettingEditorFor(model => model.IncludeFeaturedProductsInNormalLists) + @Html.ValidationMessageFor(model => model.IncludeFeaturedProductsInNormalLists) +
    + @Html.SmartLabelFor(model => model.ShowCategoryProductNumber) + + @Html.SettingEditorFor(model => model.ShowCategoryProductNumber) + @Html.ValidationMessageFor(model => model.ShowCategoryProductNumber) +
    + @Html.SmartLabelFor(model => model.ShowCategoryProductNumberIncludingSubcategories) + + @Html.SettingEditorFor(model => model.ShowCategoryProductNumberIncludingSubcategories) + @Html.ValidationMessageFor(model => model.ShowCategoryProductNumberIncludingSubcategories) +
    + @Html.SmartLabelFor(model => model.CategoryBreadcrumbEnabled) + + @Html.SettingEditorFor(model => model.CategoryBreadcrumbEnabled) + @Html.ValidationMessageFor(model => model.CategoryBreadcrumbEnabled) +
    + @Html.SmartLabelFor(model => model.SubCategoryDisplayType) + + @Html.SettingOverrideCheckbox(model => Model.SubCategoryDisplayType) + @Html.DropDownListFor(model => model.SubCategoryDisplayType, Model.AvailableSubCategoryDisplayTypes) + @Html.ValidationMessageFor(model => model.SubCategoryDisplayType) +
    - -
    + + + + + + + + + + + + + + + + + + + + +
    +
    +
    @T("Filtering.FilterResults")
    +
    +
    + @Html.SmartLabelFor(model => model.FilterEnabled) + + @Html.SettingEditorFor(model => model.FilterEnabled) + @Html.ValidationMessageFor(model => model.FilterEnabled) +
    + @Html.SmartLabelFor(model => model.MaxFilterItemsToDisplay) + + @Html.SettingEditorFor(model => model.MaxFilterItemsToDisplay) + @Html.ValidationMessageFor(model => model.MaxFilterItemsToDisplay) +
    + @Html.SmartLabelFor(model => model.ExpandAllFilterCriteria) + + @Html.SettingEditorFor(model => model.ExpandAllFilterCriteria) + @Html.ValidationMessageFor(model => model.ExpandAllFilterCriteria) +
    + @Html.SmartLabelFor(model => model.SortFilterResultsByMatches) + + @Html.SettingEditorFor(model => model.SortFilterResultsByMatches) + @Html.ValidationMessageFor(model => model.SortFilterResultsByMatches) +
    -
    - @T("Common.List") - - - - - - - - - - - - - - - - - - - - - - - - - -
    - @Html.SmartLabelFor(model => model.AllowProductSorting) - - @Html.SettingEditorFor(model => model.AllowProductSorting) - @Html.ValidationMessageFor(model => model.AllowProductSorting) -
    - @Html.SmartLabelFor(model => model.DefaultViewMode) - - @Html.SettingOverrideCheckbox(model => Model.DefaultViewMode) - @Html.DropDownListFor(model => model.DefaultViewMode, Model.AvailableDefaultViewModes) - @Html.ValidationMessageFor(model => model.DefaultViewMode) -
    - @Html.SmartLabelFor(model => model.DefaultSortOrder) - - @Html.SettingOverrideCheckbox(model => Model.DefaultSortOrder) - @Html.DropDownListFor(model => model.DefaultSortOrder, Model.AvailableSortOrderModes) - @Html.ValidationMessageFor(model => model.DefaultSortOrder) -
    - @Html.SmartLabelFor(model => model.AllowProductViewModeChanging) - - @Html.SettingEditorFor(model => model.AllowProductViewModeChanging) - @Html.ValidationMessageFor(model => model.AllowProductViewModeChanging) -
    - @Html.SmartLabelFor(model => model.DefaultPageSizeOptions) - - @Html.SettingEditorFor(model => model.DefaultPageSizeOptions) - @Html.ValidationMessageFor(model => model.DefaultPageSizeOptions) -
    - @Html.SmartLabelFor(model => model.PriceDisplayType) - - @Html.SettingOverrideCheckbox(model => Model.PriceDisplayType) - @Html.DropDownListFor(model => model.PriceDisplayType, Model.AvailablePriceDisplayTypes) - @Html.ValidationMessageFor(model => model.PriceDisplayType) -
    -
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    @T("Common.List")
    +
    +
    + @Html.SmartLabelFor(model => model.AllowProductSorting) + + @Html.SettingEditorFor(model => model.AllowProductSorting) + @Html.ValidationMessageFor(model => model.AllowProductSorting) +
    + @Html.SmartLabelFor(model => model.DefaultViewMode) + + @Html.SettingOverrideCheckbox(model => Model.DefaultViewMode) + @Html.DropDownListFor(model => model.DefaultViewMode, Model.AvailableDefaultViewModes) + @Html.ValidationMessageFor(model => model.DefaultViewMode) +
    + @Html.SmartLabelFor(model => model.DefaultSortOrder) + + @Html.SettingOverrideCheckbox(model => Model.DefaultSortOrder) + @Html.DropDownListFor(model => model.DefaultSortOrder, Model.AvailableSortOrderModes) + @Html.ValidationMessageFor(model => model.DefaultSortOrder) +
    + @Html.SmartLabelFor(model => model.AllowProductViewModeChanging) + + @Html.SettingEditorFor(model => model.AllowProductViewModeChanging) + @Html.ValidationMessageFor(model => model.AllowProductViewModeChanging) +
    + @Html.SmartLabelFor(model => model.DefaultPageSizeOptions) + + @Html.SettingEditorFor(model => model.DefaultPageSizeOptions) + @Html.ValidationMessageFor(model => model.DefaultPageSizeOptions) +
    + @Html.SmartLabelFor(model => model.PriceDisplayType) + + @Html.SettingOverrideCheckbox(model => Model.PriceDisplayType) + @Html.DropDownListFor(model => model.PriceDisplayType, Model.AvailablePriceDisplayTypes) + @Html.ValidationMessageFor(model => model.PriceDisplayType) +
    -
    - @T("Admin.Catalog.Products") - - - - - - - - - - - - - - - - - - - - - -
    - @Html.SmartLabelFor(model => model.ShowDeliveryTimesInProductLists) - - @Html.SettingEditorFor(model => model.ShowDeliveryTimesInProductLists) - @Html.ValidationMessageFor(model => model.ShowDeliveryTimesInProductLists) -
    - @Html.SmartLabelFor(model => model.ShowBasePriceInProductLists) - - @Html.SettingEditorFor(model => model.ShowBasePriceInProductLists) - @Html.ValidationMessageFor(model => model.ShowBasePriceInProductLists) -
    - @Html.SmartLabelFor(model => model.ShowColorSquaresInLists) - - @Html.SettingEditorFor(model => model.ShowColorSquaresInLists) - @Html.ValidationMessageFor(model => model.ShowColorSquaresInLists) -
    - @Html.SmartLabelFor(model => model.HideBuyButtonInLists) - - @Html.SettingEditorFor(model => model.HideBuyButtonInLists) - @Html.ValidationMessageFor(model => model.HideBuyButtonInLists) -
    - @Html.SmartLabelFor(model => model.LabelAsNewForMaxDays) - - @Html.SettingEditorFor(model => model.LabelAsNewForMaxDays) - @Html.ValidationMessageFor(model => model.LabelAsNewForMaxDays) -
    -
    + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    @T("Admin.Catalog.Products")
    +
    +
    + @Html.SmartLabelFor(model => model.ShowDeliveryTimesInProductLists) + + @Html.SettingEditorFor(model => model.ShowDeliveryTimesInProductLists) + @Html.ValidationMessageFor(model => model.ShowDeliveryTimesInProductLists) +
    + @Html.SmartLabelFor(model => model.ShowBasePriceInProductLists) + + @Html.SettingEditorFor(model => model.ShowBasePriceInProductLists) + @Html.ValidationMessageFor(model => model.ShowBasePriceInProductLists) +
    + @Html.SmartLabelFor(model => model.ShowColorSquaresInLists) + + @Html.SettingEditorFor(model => model.ShowColorSquaresInLists) + @Html.ValidationMessageFor(model => model.ShowColorSquaresInLists) +
    + @Html.SmartLabelFor(model => model.HideBuyButtonInLists) + + @Html.SettingEditorFor(model => model.HideBuyButtonInLists) + @Html.ValidationMessageFor(model => model.HideBuyButtonInLists) +
    + @Html.SmartLabelFor(model => model.LabelAsNewForMaxDays) + + @Html.SettingEditorFor(model => model.LabelAsNewForMaxDays) + @Html.ValidationMessageFor(model => model.LabelAsNewForMaxDays) +
    -
    - @T("Admin.Catalog.ProductTags") - - - - - - - - - - - - - - - - - -
    - @Html.SmartLabelFor(model => model.NumberOfProductTags) - - @Html.SettingEditorFor(model => model.NumberOfProductTags) - @Html.ValidationMessageFor(model => model.NumberOfProductTags) -
    - @Html.SmartLabelFor(model => model.ProductsByTagPageSize) - - @Html.SettingEditorFor(model => model.ProductsByTagPageSize) - @Html.ValidationMessageFor(model => model.ProductsByTagPageSize) -
    - @Html.SmartLabelFor(model => model.ProductsByTagAllowCustomersToSelectPageSize) - - @Html.SettingEditorFor(model => model.ProductsByTagAllowCustomersToSelectPageSize) - @Html.ValidationMessageFor(model => model.ProductsByTagAllowCustomersToSelectPageSize) -
    - @Html.SmartLabelFor(model => model.ProductsByTagPageSizeOptions) - - @Html.SettingEditorFor(model => model.ProductsByTagPageSizeOptions) - @Html.ValidationMessageFor(model => model.ProductsByTagPageSizeOptions) -
    -
    + + + + + + + + + + + + + + + + + + + + +
    +
    +
    @T("Admin.Catalog.ProductTags")
    +
    +
    + @Html.SmartLabelFor(model => model.NumberOfProductTags) + + @Html.SettingEditorFor(model => model.NumberOfProductTags) + @Html.ValidationMessageFor(model => model.NumberOfProductTags) +
    + @Html.SmartLabelFor(model => model.ProductsByTagPageSize) + + @Html.SettingEditorFor(model => model.ProductsByTagPageSize) + @Html.ValidationMessageFor(model => model.ProductsByTagPageSize) +
    + @Html.SmartLabelFor(model => model.ProductsByTagAllowCustomersToSelectPageSize) + + @Html.SettingEditorFor(model => model.ProductsByTagAllowCustomersToSelectPageSize) + @Html.ValidationMessageFor(model => model.ProductsByTagAllowCustomersToSelectPageSize) +
    + @Html.SmartLabelFor(model => model.ProductsByTagPageSizeOptions) + + @Html.SettingEditorFor(model => model.ProductsByTagPageSizeOptions) + @Html.ValidationMessageFor(model => model.ProductsByTagPageSizeOptions) +
    } @helper TabUserSettings() { - +
    - @@ -465,9 +538,9 @@ - + - + - - -
    @Html.SmartLabelFor(model => model.ShowProductReviewsInProductLists) @@ -693,7 +742,7 @@ @helper TabProductDetailSettings() { - +
    + + + + - - + @@ -169,8 +168,8 @@ @Html.ValidationMessageFor(model => model.NotifyAboutPrivateMessages) - - + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/GeneralCommon.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/GeneralCommon.cshtml index 60c57ccd7f..5162311aec 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/GeneralCommon.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/GeneralCommon.cshtml @@ -27,8 +27,8 @@ $("#@Html.FieldIdFor(model => model.StoreInformationSettings.StoreClosed)").click(toggleStoreClosed); $("#@Html.FieldIdFor(model => model.CaptchaSettings.Enabled)").change(function () { $('#SecuritySettingTable').find('.captcha-setting').toggle($(this).is(':checked')); - }).trigger('change'); - $("#@Html.FieldIdFor(model => model.LocalizationSettings.SeoFriendlyUrlsForLanguagesEnabled)").click(toggleSeoLanguageUrls); + }).trigger('change'); + $("#@Html.FieldIdFor(model => model.LocalizationSettings.SeoFriendlyUrlsForLanguagesEnabled)").click(toggleSeoLanguageUrls); toggleStoreClosed(); toggleSeoLanguageUrls(); @@ -36,6 +36,17 @@ $("#@Html.FieldIdFor(model => model.SocialSettings.ShowSocialLinksInFooter)").change(function () { $('#SocialLinkTable').find('.social-link').toggle($(this).is(':checked')); }).trigger('change'); + + // test creation of SEO names + $('#TestSeoNameCreationButton').click(function () { + $(this).closest('form').doAjax({ + type: 'POST', + url: '@Url.Action("TestSeoNameCreation")', + callbackSuccess: function (resp) { + $('#TestSeoNameCreationResult').text(resp); + } + }); + }); }); function toggleStoreClosed() { @@ -148,15 +159,24 @@ @Html.ValidationMessageFor(model => model.SeoSettings.DefaultMetaDescription) - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + +
    @Html.SmartLabelFor(model => model.RecentlyViewedProductsEnabled) @@ -766,6 +815,15 @@ @Html.ValidationMessageFor(model => model.DisplayAllImagesNumber)
    + @Html.SmartLabelFor(model => model.ShowManufacturerPicturesInProductDetail) + + @Html.SettingEditorFor(model => model.ShowManufacturerPicturesInProductDetail) + @Html.ValidationMessageFor(model => model.ShowManufacturerPicturesInProductDetail) +
    @Html.SmartLabelFor(model => model.ShowDeliveryTimesInProductDetail) @@ -834,8 +892,7 @@ } @helper TabProductSearchSettings() { - - +
    - + + + + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/CustomerUser.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/CustomerUser.cshtml index b22985819c..ccb0eb2876 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/CustomerUser.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/CustomerUser.cshtml @@ -3,7 +3,6 @@ @using SmartStore.Core.Domain.Customers; @using SmartStore.Core.Domain.Security; @{ - //page title ViewBag.Title = T("Admin.Configuration.Settings.CustomerUser").Text; } @using (Html.BeginForm()) @@ -44,6 +43,10 @@ toggleAvatar(); toggleUsername(); + + $('#@Html.FieldIdFor(model => model.CustomerSettings.CustomerNumberMethod)').change(function () { + $('#pnlCustomerNumberVisibility').toggle($(this).val() !== '@((int)CustomerNumberMethod.Disabled)'); + }).trigger('change'); }); function toggleAvatar() { @@ -77,29 +80,6 @@ @Html.ValidationMessageFor(model => model.CustomerSettings.UsernamesEnabled) - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + +
    @Html.SmartLabelFor(model => model.SearchPageProductsPerPage) @@ -873,7 +930,16 @@
    + + @Html.SmartLabelFor(model => model.SearchDescriptions) + + @Html.SettingEditorFor(model => model.SearchDescriptions) + @Html.ValidationMessageFor(model => model.SearchDescriptions) +

    - @Html.SmartLabelFor(model => model.CustomerSettings.CustomerNumberMethod) - - @Html.SettingOverrideCheckbox(model => Model.CustomerSettings.CustomerNumberMethod) - @Html.DropDownListFor(model => model.CustomerSettings.CustomerNumberMethod, Model.CustomerSettings.AvailableCustomerNumberMethods) - @Html.ValidationMessageFor(model => model.CustomerSettings.CustomerNumberMethod) -
    - @Html.SmartLabelFor(model => model.CustomerSettings.CustomerNumberVisibility) - - @Html.SettingOverrideCheckbox(model => Model.CustomerSettings.CustomerNumberVisibility) - @Html.DropDownListFor(model => model.CustomerSettings.CustomerNumberVisibility, Model.CustomerSettings.AvailableCustomerNumberVisibilities) - @Html.ValidationMessageFor(model => model.CustomerSettings.CustomerNumberVisibility) -
    @Html.SmartLabelFor(model => model.CustomerSettings.AllowUsersToChangeUsernames) @@ -118,6 +98,47 @@ @Html.ValidationMessageFor(model => model.CustomerSettings.CheckUsernameAvailabilityEnabled)
    + @Html.SmartLabelFor(model => model.CustomerSettings.CustomerNameFormat) + + @Html.SettingOverrideCheckbox(model => Model.CustomerSettings.UserRegistrationType) + @Html.DropDownListFor(model => model.CustomerSettings.CustomerNameFormat, ((CustomerNameFormat)Model.CustomerSettings.CustomerNameFormat).ToSelectList()) + @Html.ValidationMessageFor(model => model.CustomerSettings.CustomerNameFormat) +
    + @Html.SmartLabelFor(model => model.CustomerSettings.CustomerNameFormatMaxLength) + + @Html.SettingEditorFor(model => model.CustomerSettings.CustomerNameFormatMaxLength) + @Html.ValidationMessageFor(model => model.CustomerSettings.CustomerNameFormatMaxLength) +
    + @Html.SmartLabelFor(model => model.CustomerSettings.CustomerNumberMethod) + + @Html.SettingOverrideCheckbox(model => Model.CustomerSettings.CustomerNumberMethod) + @Html.DropDownListFor(model => model.CustomerSettings.CustomerNumberMethod, Model.CustomerSettings.AvailableCustomerNumberMethods) + @Html.ValidationMessageFor(model => model.CustomerSettings.CustomerNumberMethod) +
    + @Html.SmartLabelFor(model => model.CustomerSettings.CustomerNumberVisibility) + + @Html.SettingOverrideCheckbox(model => Model.CustomerSettings.CustomerNumberVisibility) + @Html.DropDownListFor(model => model.CustomerSettings.CustomerNumberVisibility, Model.CustomerSettings.AvailableCustomerNumberVisibilities) + @Html.ValidationMessageFor(model => model.CustomerSettings.CustomerNumberVisibility) +
    @Html.SmartLabelFor(model => model.CustomerSettings.UserRegistrationType) @@ -128,6 +149,26 @@ @Html.ValidationMessageFor(model => model.CustomerSettings.UserRegistrationType)
    + @Html.SmartLabelFor(model => model.CustomerSettings.DefaultPasswordFormat) + + @Html.DropDownListFor(model => model.CustomerSettings.DefaultPasswordFormat, ((PasswordFormat)Model.CustomerSettings.DefaultPasswordFormat).ToSelectList()) + @Html.ValidationMessageFor(model => model.CustomerSettings.DefaultPasswordFormat) +
    + @Html.SmartLabelFor(model => model.CustomerSettings.RegisterCustomerRoleId) + + @Html.SettingOverrideCheckbox(model => Model.CustomerSettings.RegisterCustomerRoleId) + @Html.DropDownListFor(model => model.CustomerSettings.RegisterCustomerRoleId, Model.CustomerSettings.AvailableRegisterCustomerRoles, T("Common.Unspecified")) + @Html.ValidationMessageFor(model => model.CustomerSettings.RegisterCustomerRoleId) +
    @Html.SmartLabelFor(model => model.CustomerSettings.AllowCustomersToUploadAvatars) @@ -200,25 +241,6 @@ @Html.ValidationMessageFor(model => model.CustomerSettings.HideBackInStockSubscriptionsTab)
    - @Html.SmartLabelFor(model => model.CustomerSettings.CustomerNameFormat) - - @Html.SettingOverrideCheckbox(model => Model.CustomerSettings.UserRegistrationType) - @Html.DropDownListFor(model => model.CustomerSettings.CustomerNameFormat, ((CustomerNameFormat)Model.CustomerSettings.CustomerNameFormat).ToSelectList()) - @Html.ValidationMessageFor(model => model.CustomerSettings.CustomerNameFormat) -
    - @Html.SmartLabelFor(model => model.CustomerSettings.CustomerNameFormatMaxLength) - - @Html.SettingEditorFor(model => model.CustomerSettings.CustomerNameFormatMaxLength) - @Html.ValidationMessageFor(model => model.CustomerSettings.CustomerNameFormatMaxLength) -
    @Html.SmartLabelFor(model => model.CustomerSettings.HideNewsletterBlock) @@ -237,15 +259,15 @@ @Html.ValidationMessageFor(model => model.CustomerSettings.StoreLastVisitedPage)
    - @Html.SmartLabelFor(model => model.CustomerSettings.DefaultPasswordFormat) - - @Html.DropDownListFor(model => model.CustomerSettings.DefaultPasswordFormat, ((PasswordFormat)Model.CustomerSettings.DefaultPasswordFormat).ToSelectList()) - @Html.ValidationMessageFor(model => model.CustomerSettings.DefaultPasswordFormat) -
    + @Html.SmartLabelFor(model => model.CustomerSettings.DisplayPrivacyAgreementOnContactUs) + + @Html.SettingEditorFor(model => model.CustomerSettings.DisplayPrivacyAgreementOnContactUs) + @Html.ValidationMessageFor(model => model.CustomerSettings.DisplayPrivacyAgreementOnContactUs) +
    } @helper TabCustomerFormFields() diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/DataExchange.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/DataExchange.cshtml new file mode 100644 index 0000000000..88bcdab917 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/DataExchange.cshtml @@ -0,0 +1,56 @@ +@model DataExchangeSettingsModel +@{ + ViewBag.Title = T("Admin.Common.DataExchange").Text; +} +@using (Html.BeginForm()) +{ +
    +
    + + @T("Admin.Common.DataExchange") +
    +
    + +
    +
    + + @Html.Action("StoreScopeConfiguration", "Setting") + @Html.ValidationSummary(false) + + + + + + + + + + + + + + + + + +
    + @Html.SmartLabelFor(model => model.MaxFileNameLength) + + @Html.SettingEditorFor(model => model.MaxFileNameLength) + @Html.ValidationMessageFor(model => model.MaxFileNameLength) +
    +
    +
    @T("Common.Import")
    +
    +
    + @Html.SmartLabelFor(model => model.ImageImportFolder) + + @Html.SettingEditorFor(model => model.ImageImportFolder) + @Html.ValidationMessageFor(model => model.ImageImportFolder) +
    + @Html.SmartLabelFor(model => model.ImageDownloadTimeout) + + @Html.SettingEditorFor(model => model.ImageDownloadTimeout) + @Html.ValidationMessageFor(model => model.ImageDownloadTimeout) +
    +} diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Forum.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Forum.cshtml index b89e7f267a..960758edfd 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Forum.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Forum.cshtml @@ -1,7 +1,6 @@ @model ForumSettingsModel @using Telerik.Web.Mvc.UI; @{ - //page title ViewBag.Title = T("Admin.Configuration.Settings.Forums").Text; } @using (Html.BeginForm()) @@ -136,8 +135,8 @@ @Html.ValidationMessageFor(model => model.SignaturesEnabled)
    +

    +

    - @Html.SmartLabelFor(model => model.SeoSettings.ConvertNonWesternChars) - - @Html.SettingEditorFor(model => model.SeoSettings.ConvertNonWesternChars) - @Html.ValidationMessageFor(model => model.SeoSettings.ConvertNonWesternChars) -
    + @Html.SmartLabelFor(model => model.SeoSettings.MetaRobotsContent) + + @Html.SettingOverrideCheckbox(model => model.SeoSettings.MetaRobotsContent) + @Html.DropDownListFor(model => model.SeoSettings.MetaRobotsContent, new List + { + new SelectListItem { Text = "index", Value = "index" }, + new SelectListItem { Text = "noindex", Value = "noindex" }, + new SelectListItem { Text = "index, follow", Value = "index, follow" }, + new SelectListItem { Text = "index, nofollow", Value = "index, nofollow" }, + new SelectListItem { Text = "noindex, follow", Value = "noindex, follow" }, + new SelectListItem { Text = "noindex, nofollow", Value = "noindex, nofollow" } + }, T("Common.Unspecified")) + @Html.ValidationMessageFor(model => model.SeoSettings.MetaRobotsContent) +
    @Html.SmartLabelFor(model => model.SeoSettings.CanonicalUrlsEnabled) @@ -185,6 +205,59 @@ @Html.ValidationMessageFor(model => model.SeoSettings.ExtraRobotsDisallows)
    +
    +
    @T("Admin.System.SeNames")
    +
    +
    + @Html.SmartLabelFor(model => model.SeoSettings.ConvertNonWesternChars) + + @Html.SettingEditorFor(model => model.SeoSettings.ConvertNonWesternChars) + @Html.ValidationMessageFor(model => model.SeoSettings.ConvertNonWesternChars) +
    + @Html.SmartLabelFor(model => model.SeoSettings.AllowUnicodeCharsInUrls) + + @Html.SettingEditorFor(model => model.SeoSettings.AllowUnicodeCharsInUrls) + @Html.ValidationMessageFor(model => model.SeoSettings.AllowUnicodeCharsInUrls) +
    + @Html.SmartLabelFor(model => model.SeoSettings.SeoNameCharConversion) + + @Html.TextAreaFor(model => model.SeoSettings.SeoNameCharConversion, new { @class = "input-large", style = "height:250px" }) + @Html.ValidationMessageFor(model => model.SeoSettings.SeoNameCharConversion) +
    + @Html.SmartLabelFor(model => model.SeoSettings.TestSeoNameCreation) + + @Html.EditorFor(model => model.SeoSettings.TestSeoNameCreation) + + +
    +   + +
    } @helper TabSecuritySettings() @@ -369,7 +442,7 @@
    +
    -
    -
    +
    +
    @@ -583,9 +656,9 @@
    -
    -
    +
    +
    @@ -614,9 +687,6 @@ @Html.ValidationMessageFor(model => model.CompanyInformationSettings.TaxNumber)
    } @@ -661,9 +731,9 @@ - -
    - + +
    + @@ -702,9 +772,9 @@ - -
    - + +
    + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Media.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Media.cshtml index 566f49c981..fbd72c4cd3 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Media.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Media.cshtml @@ -1,7 +1,6 @@ @model MediaSettingsModel @using Telerik.Web.Mvc.UI; @{ - //page title ViewBag.Title = T("Admin.Configuration.Settings.Media").Text; } @using (Html.BeginForm()) @@ -62,6 +61,15 @@ @Html.ValidationMessageFor(model => model.ProductThumbPictureSizeOnProductDetailsPage) + + + @Html.SmartLabelFor(model => model.MessageProductThumbPictureSize) + + + @Html.SettingEditorFor(model => model.MessageProductThumbPictureSize) + @Html.ValidationMessageFor(model => model.MessageProductThumbPictureSize) + + @Html.SmartLabelFor(model => model.ProductDetailsPictureSize) @@ -162,38 +170,31 @@ @Html.ValidationMessageFor(model => model.MaximumImageSize) - - -
    - @T("Admin.Configuration.Settings.Media.PicturesStoredIntoDatabase") -
    -
    - @T("Admin.Configuration.Settings.Media.MovePicturesNote") -
    + + +
    +
    @T("Admin.Configuration.Settings.Media.PicturesStoredIntoDatabase")
    +
    +
    + @T("Admin.Configuration.Settings.Media.MovePicturesNote") +
    + + @T(Model.PicturesStoredIntoDatabase ? "Admin.Configuration.Settings.Media.PicturesStoredIntoDatabase.Database" : "Admin.Configuration.Settings.Media.PicturesStoredIntoDatabase.FileSystem") + - - @if (Model.PicturesStoredIntoDatabase) - { - @T("Admin.Configuration.Settings.Media.PicturesStoredIntoDatabase.Database") - } - else - { - @T("Admin.Configuration.Settings.Media.PicturesStoredIntoDatabase.FileSystem") - } - - - -
    -
    + + + + } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Order.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Order.cshtml index 3c29b263a1..c10c4c8f52 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Order.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Order.cshtml @@ -105,6 +105,15 @@ @Html.ValidationMessageFor(model => model.GiftCards_Deactivated_OrderStatusId) + + + @Html.SmartLabelFor(model => model.OrderListPageSize) + + + @Html.EditorFor(model => model.OrderListPageSize) + @Html.ValidationMessageFor(model => model.OrderListPageSize) + + @if (Model.OrderIdent.HasValue) { @@ -116,6 +125,18 @@ } + @if (Model.StoreCount > 1) + { + + + @Html.SmartLabelFor(model => model.DisplayOrdersOfAllStores) + + + @Html.SettingEditorFor(model => model.DisplayOrdersOfAllStores) + @Html.ValidationMessageFor(model => model.DisplayOrdersOfAllStores) + + + } } @@ -158,7 +179,7 @@ @Html.SmartLabelFor(model => model.Locales[item].ReturnRequestReasons) - @Html.TextBoxFor(model => Model.Locales[item].ReturnRequestReasons, new { style = "min-width: 800px;" }) + @Html.TextBoxFor(model => Model.Locales[item].ReturnRequestReasons, new { @class = "input-xlarge" }) @Html.ValidationMessageFor(model => model.Locales[item].ReturnRequestReasons) @@ -167,7 +188,7 @@ @Html.SmartLabelFor(model => model.Locales[item].ReturnRequestActions) - @Html.TextBoxFor(model => model.Locales[item].ReturnRequestActions, new { style = "min-width: 800px;" }) + @Html.TextBoxFor(model => model.Locales[item].ReturnRequestActions, new { @class = "input-xlarge" }) @Html.ValidationMessageFor(model => model.Locales[item].ReturnRequestActions) @@ -184,7 +205,7 @@ @Html.SmartLabelFor(model => model.ReturnRequestReasons) - @Html.TextBoxFor(model => model.ReturnRequestReasons, new { style = "min-width: 800px;" }) + @Html.TextBoxFor(model => model.ReturnRequestReasons, new { @class = "input-xlarge" }) @Html.ValidationMessageFor(model => model.ReturnRequestReasons) @@ -193,7 +214,7 @@ @Html.SmartLabelFor(model => model.ReturnRequestActions) - @Html.TextBoxFor(x => x.ReturnRequestActions, new { style = "min-width: 800px;" }) + @Html.TextBoxFor(x => x.ReturnRequestActions, new { @class = "input-xlarge" }) @Html.ValidationMessageFor(model => model.ReturnRequestActions) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/RewardPoints.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/RewardPoints.cshtml index c6bf2240dd..2db648327b 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/RewardPoints.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/RewardPoints.cshtml @@ -2,7 +2,6 @@ @using Telerik.Web.Mvc.UI; @using SmartStore.Core.Domain.Orders; @{ - //page title ViewBag.Title = T("Admin.Configuration.Settings.RewardPoints").Text; } @using (Html.BeginForm()) @@ -16,8 +15,9 @@ - @Html.ValidationSummary(false) + @Html.Action("StoreScopeConfiguration", "Setting") + @Html.ValidationSummary(false)
    @@ -46,69 +46,81 @@ @Html.ValidationMessageFor(model => model.ExchangeRate) + + + @Html.SmartLabelFor(model => model.RoundDownRewardPoints) + + + @Html.SettingEditorFor(model => model.RoundDownRewardPoints) + @Html.ValidationMessageFor(model => model.RoundDownRewardPoints) + + -
    - @T("Admin.Configuration.Settings.RewardPoints.Earning") - - - - - - - - - - - - - - - - - - - - - - -
    - @Html.SmartLabelFor(model => model.PointsForRegistration) - - @Html.SettingOverrideCheckbox(model => Model.PointsForRegistration) - @Html.EditorFor(model => model.PointsForRegistration, new { Small = true }) - @Html.ValidationMessageFor(model => model.PointsForRegistration) -
    - @Html.SmartLabelFor(model => model.PointsForProductReview) - - @Html.SettingOverrideCheckbox(model => Model.PointsForProductReview) - @Html.EditorFor(model => model.PointsForProductReview, new { Small = true }) - @Html.ValidationMessageFor(model => model.PointsForProductReview) -
    - @Html.SmartLabelFor(model => model.PointsForPurchases_Amount) - - @Html.SettingOverrideCheckbox(model => Model.PointsForPurchases_OverrideForStore, "#pnlPointsForPurchases") - @T("Admin.Configuration.Settings.RewardPoints.Earning.Hint1") - @Html.EditorFor(model => model.PointsForPurchases_Amount, new { Small = true }) - @Model.PrimaryStoreCurrencyCode -   - @T("Admin.Configuration.Settings.RewardPoints.Earning.Hint2") - @Html.EditorFor(model => model.PointsForPurchases_Points, new { Small = true }) - @T("Admin.Configuration.Settings.RewardPoints.Earning.Hint3") - @Html.ValidationMessageFor(model => model.PointsForPurchases_Amount) - @Html.ValidationMessageFor(model => model.PointsForPurchases_Points) -
    - @Html.SmartLabelFor(model => model.PointsForPurchases_Awarded) - - @Html.SettingOverrideCheckbox(model => Model.PointsForPurchases_Awarded) - @Html.DropDownListFor(model => model.PointsForPurchases_Awarded, ((OrderStatus)Model.PointsForPurchases_Awarded).ToSelectList()) - @Html.ValidationMessageFor(model => model.PointsForPurchases_Awarded) -
    - @Html.SmartLabelFor(model => model.PointsForPurchases_Canceled) - - @Html.SettingOverrideCheckbox(model => Model.PointsForPurchases_Canceled) - @Html.DropDownListFor(model => model.PointsForPurchases_Canceled, ((OrderStatus)Model.PointsForPurchases_Canceled).ToSelectList()) - @Html.ValidationMessageFor(model => model.PointsForPurchases_Canceled) -
    -
    + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    @T("Admin.Configuration.Settings.RewardPoints.Earning")
    +
    +
    + @Html.SmartLabelFor(model => model.PointsForRegistration) + + @Html.SettingOverrideCheckbox(model => Model.PointsForRegistration) + @Html.EditorFor(model => model.PointsForRegistration, new { Small = true }) + @Html.ValidationMessageFor(model => model.PointsForRegistration) +
    + @Html.SmartLabelFor(model => model.PointsForProductReview) + + @Html.SettingOverrideCheckbox(model => Model.PointsForProductReview) + @Html.EditorFor(model => model.PointsForProductReview, new { Small = true }) + @Html.ValidationMessageFor(model => model.PointsForProductReview) +
    + @Html.SmartLabelFor(model => model.PointsForPurchases_Amount) + + @Html.SettingOverrideCheckbox(model => Model.PointsForPurchases_OverrideForStore, "#pnlPointsForPurchases") + @T("Admin.Configuration.Settings.RewardPoints.Earning.Hint1") + @Html.EditorFor(model => model.PointsForPurchases_Amount, new { Small = true }) + @Model.PrimaryStoreCurrencyCode +   + @T("Admin.Configuration.Settings.RewardPoints.Earning.Hint2") + @Html.EditorFor(model => model.PointsForPurchases_Points, new { Small = true }) + @T("Admin.Configuration.Settings.RewardPoints.Earning.Hint3") + @Html.ValidationMessageFor(model => model.PointsForPurchases_Amount) + @Html.ValidationMessageFor(model => model.PointsForPurchases_Points) +
    + @Html.SmartLabelFor(model => model.PointsForPurchases_Awarded) + + @Html.SettingOverrideCheckbox(model => Model.PointsForPurchases_Awarded) + @Html.DropDownListFor(model => model.PointsForPurchases_Awarded, ((OrderStatus)Model.PointsForPurchases_Awarded).ToSelectList()) + @Html.ValidationMessageFor(model => model.PointsForPurchases_Awarded) +
    + @Html.SmartLabelFor(model => model.PointsForPurchases_Canceled) + + @Html.SettingOverrideCheckbox(model => Model.PointsForPurchases_Canceled) + @Html.DropDownListFor(model => model.PointsForPurchases_Canceled, ((OrderStatus)Model.PointsForPurchases_Canceled).ToSelectList()) + @Html.ValidationMessageFor(model => model.PointsForPurchases_Canceled) +
    } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Shipping.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Shipping.cshtml index 56a5139003..65f9355676 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Shipping.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Shipping.cshtml @@ -1,6 +1,5 @@ @model ShippingSettingsModel @{ - //page title ViewBag.Title = T("Admin.Configuration.Settings.Shipping").Text; } @using (Html.BeginForm()) @@ -82,14 +81,14 @@ @Html.ValidationMessageFor(model => model.DisplayShipmentEventsToCustomers) - - - @Html.LabelFor(model => model.ShippingOriginAddress, new { style="font-weight: bold" }) - + + + @Html.SmartLabelFor(model => model.ShippingOriginAddress) + @Html.SettingOverrideCheckbox(model => Model.ShippingOriginAddress, "#pnlShippingOriginAddress") - + @Html.EditorFor(model => model.ShippingOriginAddress, "Address") diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/ShoppingCart.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/ShoppingCart.cshtml index 2a4d51b0b4..8c1db1d995 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/ShoppingCart.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/ShoppingCart.cshtml @@ -1,6 +1,5 @@ @model ShoppingCartSettingsModel @{ - //page title ViewBag.Title = T("Admin.Configuration.Settings.ShoppingCart").Text; } @using (Html.BeginForm()) @@ -22,6 +21,12 @@ toggleMiniShoppingCartEnabled(); toggleEmailWishlistEnabled(); + + // toggle third party email hand over label text + $('#@Html.FieldIdFor(model => model.ThirdPartyEmailHandOver)').change(function () { + $('#ThirdPartyEmailHandOverTextLocalized').toggle($(this).val() !== '0'); + }).trigger('change'); + }); function toggleMiniShoppingCartEnabled() { @@ -52,8 +57,8 @@ @(Html.SmartStore().TabStrip().Name("catalogsettings-edit").Items(x => { x.Add().Text(T("Admin.Configuration.Settings.ShoppingCart.CartSettings").Text).Content(@TabCartSettings()).Selected(true); - x.Add().Text(T("Admin.Configuration.Settings.ShoppingCart.WishlistSettings").Text).Content(@TabWishlistSettings()); x.Add().Text(T("Admin.Configuration.Settings.ShoppingCart.Checkout").Text).Content(@TabCheckoutSettings()); + x.Add().Text(T("Admin.Configuration.Settings.ShoppingCart.WishlistSettings").Text).Content(@TabWishlistSettings()); })) } @@ -142,8 +147,8 @@ - - + +
    @@ -176,10 +181,10 @@ - - -
    - + + +
    + @@ -293,6 +298,32 @@ @helper TabCheckoutSettings() { + + + + + + + + + + + - - - - + + + + + + + +
    +
    +
    @T("Admin.Configuration.Settings.ShoppingCart.OrderConfirmationPage")
    +
    +
    + @Html.SmartLabelFor(model => model.ShowCommentBox) + + @Html.SettingEditorFor(model => model.ShowCommentBox) + @Html.ValidationMessageFor(model => model.ShowCommentBox) +
    + @Html.SmartLabelFor(model => model.NewsLetterSubscription) + + @Html.SettingOverrideCheckbox(model => Model.NewsLetterSubscription) + @Html.DropDownListFor(model => model.NewsLetterSubscription, Model.AvailableNewsLetterSubscriptions) + @Html.ValidationMessageFor(model => model.NewsLetterSubscription) +
    @Html.SmartLabelFor(model => model.ShowConfirmOrderLegalHint) @@ -302,14 +333,59 @@ @Html.ValidationMessageFor(model => model.ShowConfirmOrderLegalHint)
    - @Html.SmartLabelFor(model => model.ShowCommentBox) - - @Html.SettingEditorFor(model => model.ShowCommentBox) - @Html.ValidationMessageFor(model => model.ShowCommentBox) -
    + @Html.SmartLabelFor(model => model.ShowEsdRevocationWaiverBox) + + @Html.SettingEditorFor(model => model.ShowEsdRevocationWaiverBox) + @Html.ValidationMessageFor(model => model.ShowEsdRevocationWaiverBox) +
    + @Html.SmartLabelFor(model => model.ThirdPartyEmailHandOver) + + @Html.SettingOverrideCheckbox(model => Model.ThirdPartyEmailHandOver) + @Html.DropDownListFor(model => model.ThirdPartyEmailHandOver, Model.AvailableThirdPartyEmailHandOver) + @Html.ValidationMessageFor(model => model.ThirdPartyEmailHandOver) +
    + +
    +

    + + @(Html.LocalizedEditor("setting-shopping-cart-localized", + @ + + + + + + + +
    + @Html.SmartLabelFor(model => model.Locales[item].ThirdPartyEmailHandOverLabel) + + @Html.TextAreaFor(model => Model.Locales[item].ThirdPartyEmailHandOverLabel, new { @class = "input-large" }) + @Html.ValidationMessageFor(model => model.Locales[item].ThirdPartyEmailHandOverLabel) +
    + @Html.HiddenFor(model => model.Locales[item].LanguageId) +
    + , + @ + + + + +
    + @Html.SmartLabelFor(model => model.ThirdPartyEmailHandOverLabel) + + @Html.TextAreaFor(model => model.ThirdPartyEmailHandOverLabel, new { @class = "input-large" }) + @Html.ValidationMessageFor(model => model.ThirdPartyEmailHandOverLabel) +
    + )) +
    } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Tax.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Tax.cshtml index bb8a6a430e..7f2e872994 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Tax.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Tax.cshtml @@ -1,7 +1,6 @@ @model TaxSettingsModel @using Telerik.Web.Mvc.UI; @{ - //page title ViewBag.Title = T("Admin.Configuration.Settings.Tax").Text; } @using (Html.BeginForm()) @@ -59,12 +58,14 @@ $('#pnlEuVatAllowVATExemption').show(); $('#pnlEuVatUseWebService').show(); $('#pnlEuVatEmailAdminWhenNewVATSubmitted').show(); + $('#pnlVatRequired').show(); } else { $('#pnlEuVatShopCountry').hide(); $('#pnlEuVatAllowVATExemption').hide(); $('#pnlEuVatUseWebService').hide(); $('#pnlEuVatEmailAdminWhenNewVATSubmitted').hide(); + $('#pnlVatRequired').hide(); } } @@ -148,11 +149,6 @@ @Html.ValidationMessageFor(model => model.HideTaxInOrderSummary) - - -
    - - @Html.SmartLabelFor(model => model.ShowLegalHintsInProductList) @@ -180,8 +176,8 @@ @Html.ValidationMessageFor(model => model.ShowLegalHintsInFooter) - - + +
    @@ -195,21 +191,21 @@ @Html.ValidationMessageFor(model => model.TaxBasedOn) - + - @Html.LabelFor(model => model.DefaultTaxAddress, new { style="font-weight: bold" }) + @Html.SmartLabelFor(model => model.DefaultTaxAddress) @Html.SettingOverrideCheckbox(model => Model.DefaultTaxAddress, "#pnlDefaultTaxAddress") - + @Html.EditorFor(x => x.DefaultTaxAddress, "Address") - - + +
    @@ -241,8 +237,8 @@ @Html.ValidationMessageFor(model => model.ShippingTaxClassId) - - + +
    @@ -274,8 +270,8 @@ @Html.ValidationMessageFor(model => model.PaymentMethodAdditionalFeeTaxClassId) - - + +
    @@ -325,5 +321,14 @@ @Html.ValidationMessageFor(model => model.EuVatEmailAdminWhenNewVatSubmitted) + + + @Html.SmartLabelFor(model => model.VatRequired) + + + @Html.SettingEditorFor(model => model.VatRequired) + @Html.ValidationMessageFor(model => model.VatRequired) + + } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/CsvConfiguration.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/CsvConfiguration.cshtml new file mode 100644 index 0000000000..9a1a8f1622 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/CsvConfiguration.cshtml @@ -0,0 +1,68 @@ +@using SmartStore.Admin.Models.DataExchange; +@model CsvConfigurationModel + + @if (ViewData["ShowGroupCaption"] != null && (bool)ViewData["ShowGroupCaption"]) + { + + + + } + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    @T("Admin.Common.CsvConfiguration")
    +
    +
    + @Html.SmartLabelFor(x => x.QuoteAllFields) + + @Html.EditorFor(x => x.QuoteAllFields) + @Html.ValidationMessageFor(x => x.QuoteAllFields) +
    + @Html.SmartLabelFor(x => x.TrimValues) + + @Html.EditorFor(x => x.TrimValues) + @Html.ValidationMessageFor(x => x.TrimValues) +
    + @Html.SmartLabelFor(x => x.SupportsMultiline) + + @Html.EditorFor(x => x.SupportsMultiline) + @Html.ValidationMessageFor(x => x.SupportsMultiline) +
    + @Html.SmartLabelFor(x => x.Delimiter) + + @Html.TextBoxFor(x => x.Delimiter, new { style = "width: 30px;", maxlength = "2" }) + @Html.ValidationMessageFor(x => x.Delimiter) +
    + @Html.SmartLabelFor(x => x.Quote) + + @Html.TextBoxFor(x => x.Quote, new { style = "width: 30px;", maxlength = "2" }) + @Html.ValidationMessageFor(x => x.Quote) +
    + @Html.SmartLabelFor(x => x.Escape) + + @Html.TextBoxFor(x => x.Escape, new { style = "width: 30px;", maxlength = "2" }) + @Html.ValidationMessageFor(x => x.Escape) +
    \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/Delete.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/Delete.cshtml index f88dd27dae..2ecbda038a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/Delete.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/Delete.cshtml @@ -1,4 +1,4 @@ -@model SmartStore.Web.Framework.Mvc.DeleteConfirmationModel +@model SmartStore.Web.Framework.Modelling.DeleteConfirmationModel @*codehint: sm-edit*@ @using (Html.BeginForm(Model.ActionName, Model.ControllerName, new { id = Model.Id }, FormMethod.Post, new { style = "margin:0;" })) {
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/RichEditor.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/RichEditor.cshtml index f22d41d211..04db8e0e26 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/RichEditor.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/RichEditor.cshtml @@ -29,7 +29,7 @@ diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/WidgetZone.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/WidgetZone.cshtml index 086cbd34a2..feba06977d 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/WidgetZone.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/WidgetZone.cshtml @@ -50,7 +50,9 @@ 'admin_breadcrumb', 'admin_content_before', 'admin_content_after', - 'admin_menu_before' + 'admin_menu_before', + 'admin_button_toolbar_before', + 'admin_button_toolbar_after' ] }, { diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/Navbar.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/Navbar.cshtml index 5188aa5b37..eae6d73f32 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/Navbar.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/Navbar.cshtml @@ -1,6 +1,7 @@ @using SmartStore.Core.Domain.Stores; @using SmartStore.Services.Customers; @using SmartStore.Services.Stores; +@using SmartStore.Core; @{ var currentCustomer = this.WorkContext.CurrentCustomer; @@ -11,7 +12,7 @@ string communityUrl = (currentLanguageCode == "de" ? "http://community.smartstore.com" : "http://community.smartstore.com/index.php?/&k=880ea6a14ea49e853634fbdc5015a024&setlanguage=1&cal_id=&langid=1"); - string docsUrl = "http://docs.smartstore.com/display/SMNET25/SmartStore.NET+Documentation+Home"; + string docsUrl = GenerateHelpUrl(""); var checkUpdateResult = ViewBag.CheckUpdateResult as CheckUpdateResult; } @@ -33,7 +34,7 @@
    - + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/_AdminRoot.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/_AdminRoot.cshtml index a66ddaadb3..f5a5e22b33 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/_AdminRoot.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/_AdminRoot.cshtml @@ -3,15 +3,15 @@ @using SmartStore.Web.Framework.UI; @{ - var currentUICulture = System.Threading.Thread.CurrentThread.CurrentUICulture; - - // page title - string adminPageTitle = ""; - if (!string.IsNullOrWhiteSpace(ViewBag.Title)) - { - adminPageTitle = ViewBag.Title + " / "; - } - adminPageTitle += T("Admin.PageTitle").Text; + var currentUICulture = System.Threading.Thread.CurrentThread.CurrentUICulture; + + // page title + string adminPageTitle = ""; + if (!string.IsNullOrWhiteSpace(ViewBag.Title)) + { + adminPageTitle = ViewBag.Title + " / "; + } + adminPageTitle += T("Admin.PageTitle").Text; // add css assets var telerikCssRootPath = "~/Content/2012.2.607/"; @@ -20,77 +20,79 @@ telerikCssRootPath + "telerik.common.css", "~/Content/jquery.pnotify.default.css", "~/Content/jquery.pnotify.default.icons.css", + "~/Content/smartstore.entitypicker.css", "~/Administration/Content/theme.less"); - // add js assets (Head) - Html.AppendScriptParts(ResourceLocation.Head, - "~/Scripts/modernizr-2.7.2.js", + // add js assets (Head) + Html.AppendScriptParts(ResourceLocation.Head, + "~/Scripts/modernizr-2.7.2.js", // Telerik doesn't like jQuery 1.9+, a shame! - "~/Administration/Scripts/jquery-1.8.3.js", - "~/Scripts/globalize/globalize.js", + "~/Administration/Scripts/jquery-1.8.3.js", + "~/Scripts/globalize/globalize.js", "~/Scripts/smartstore.globalize.adapter.js"); - - Html.AddScriptParts(ResourceLocation.Head, true, - "~/Scripts/globalize/cultures/globalize.culture.{0}.js".FormatInvariant(currentUICulture.ToString())); - // add js assets (Foot) - var bootstrapJsRoot = "~/Content/bootstrap/js/"; - Html.AppendScriptParts(ResourceLocation.Foot, - // jQuery UI Core - "~/Scripts/jquery-ui/effect.js", - "~/Scripts/jquery-ui/effect-transfer.js", - "~/Scripts/jquery-ui/effect-shake.js", - "~/Scripts/jquery-ui/position.js", - // jQuery Validation - "~/Scripts/jquery.unobtrusive-ajax.js", - "~/Scripts/jquery.validate.js", - "~/Scripts/jquery.validate.unobtrusive.js", - // SmartStore system - "~/Scripts/smartstore.system.js", - "~/Scripts/underscore.js", - "~/Scripts/underscore.string.js", - "~/Scripts/underscore.mixins.js", - "~/Scripts/smartstore.jquery.utils.js", - "~/Scripts/jquery.ba-outside-events.js", - "~/Scripts/jquery.preload.js", - "~/Scripts/jquery.transit.js", - "~/Scripts/jquery.menu-aim.js", - "~/Scripts/smartstore.doAjax.js", - "~/Scripts/jquery.addeasing.js", - "~/Scripts/smartstore.eventbroker.js", - "~/Scripts/smartstore.hacks.js", + Html.AddScriptParts(ResourceLocation.Head, true, + "~/Scripts/globalize/cultures/globalize.culture.{0}.js".FormatInvariant(currentUICulture.ToString())); + + // add js assets (Foot) + var bootstrapJsRoot = "~/Content/bootstrap/js/"; + Html.AppendScriptParts(ResourceLocation.Foot, + // jQuery UI Core + "~/Scripts/jquery-ui/effect.js", + "~/Scripts/jquery-ui/effect-transfer.js", + "~/Scripts/jquery-ui/effect-shake.js", + "~/Scripts/jquery-ui/position.js", + // jQuery Validation + "~/Scripts/jquery.unobtrusive-ajax.js", + "~/Scripts/jquery.validate.js", + "~/Scripts/jquery.validate.unobtrusive.js", + // SmartStore system + "~/Scripts/smartstore.system.js", + "~/Scripts/underscore.js", + "~/Scripts/underscore.string.js", + "~/Scripts/underscore.mixins.js", + "~/Scripts/smartstore.jquery.utils.js", + "~/Scripts/jquery.ba-outside-events.js", + "~/Scripts/jquery.preload.js", + "~/Scripts/jquery.transit.js", + "~/Scripts/jquery.menu-aim.js", + "~/Scripts/smartstore.doAjax.js", + "~/Scripts/smartstore.entityPicker.js", + "~/Scripts/jquery.addeasing.js", + "~/Scripts/smartstore.eventbroker.js", + "~/Scripts/smartstore.hacks.js", "~/Scripts/smartstore.common.js", - // Bootstrap - bootstrapJsRoot + "bootstrap-transition.js", - bootstrapJsRoot + "bootstrap-alert.js", - bootstrapJsRoot + "bootstrap-button.js", - bootstrapJsRoot + "bootstrap-collapse.js", - bootstrapJsRoot + "bootstrap-dropdown.js", - bootstrapJsRoot + "bootstrap-modal.js", - bootstrapJsRoot + "bootstrap-tooltip.js", - bootstrapJsRoot + "bootstrap-popover.js", - bootstrapJsRoot + "bootstrap-tab.js", + // Bootstrap + bootstrapJsRoot + "bootstrap-transition.js", + bootstrapJsRoot + "bootstrap-alert.js", + bootstrapJsRoot + "bootstrap-button.js", + bootstrapJsRoot + "bootstrap-collapse.js", + bootstrapJsRoot + "bootstrap-dropdown.js", + bootstrapJsRoot + "bootstrap-modal.js", + bootstrapJsRoot + "bootstrap-tooltip.js", + bootstrapJsRoot + "bootstrap-popover.js", + bootstrapJsRoot + "bootstrap-tab.js", bootstrapJsRoot + "bootstrap-typeahead.js", - // Bootstrap custom - bootstrapJsRoot + "custom/bootstrap-datetimepicker.js", - // Shared UI - "~/Scripts/smartstore.placeholder.js", - "~/Scripts/select2.js", - "~/Scripts/smartstore.selectwrapper.js", - "~/Scripts/smartstore.throbber.js", - "~/Scripts/smartstore.navbar.js", - "~/Scripts/smartstore.thumbzoomer.js", - "~/Scripts/smartstore.column-equalizer.js", - "~/Scripts/smartstore.shrinkmenu.js", - "~/Scripts/smartstore.scrollbutton.js", - "~/Scripts/smartstore.tabselector.js", - "~/Scripts/jquery.pnotify.js", - "~/Scripts/jquery.scrollTo.js", - "~/Scripts/jquery.serialScroll.js", + // Bootstrap custom + bootstrapJsRoot + "custom/bootstrap-datetimepicker.js", + // Shared UI + "~/Scripts/smartstore.placeholder.js", + "~/Scripts/select2.js", + "~/Scripts/smartstore.selectwrapper.js", + "~/Scripts/smartstore.throbber.js", + "~/Scripts/smartstore.navbar.js", + "~/Scripts/smartstore.thumbzoomer.js", + "~/Scripts/smartstore.column-equalizer.js", + "~/Scripts/smartstore.shrinkmenu.js", + "~/Scripts/smartstore.scrollbutton.js", + "~/Scripts/smartstore.tabselector.js", + "~/Scripts/jquery.pnotify.js", + "~/Scripts/jquery.scrollTo.js", + "~/Scripts/jquery.serialScroll.js", "~/Scripts/jquery.sortable.js", - // Admin - "~/Administration/Scripts/admin.common.js", - "~/Administration/Scripts/admin.globalinit.js"); + // Admin + "~/Administration/Scripts/admin.common.js", + "~/Administration/Scripts/admin.globalinit.js"); } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/_Providers.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/_Providers.cshtml index c0907267c1..d0305ed5fc 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/_Providers.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/_Providers.cshtml @@ -1,5 +1,6 @@ @model ProviderModelList @using SmartStore.Core +@using SmartStore.Web.Framework.Plugins @helper ActiveLabel(ProviderModel item) { diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shipping/EditMethod.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shipping/EditMethod.cshtml index 1146f02c4a..a07638227c 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shipping/EditMethod.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shipping/EditMethod.cshtml @@ -1,6 +1,5 @@ @model ShippingMethodModel @{ - //page title ViewBag.Title = T("Admin.Configuration.Shipping.Methods.EditMethodDetails").Text; } @using (Html.BeginForm()) @@ -10,9 +9,13 @@ @T("Admin.Configuration.Shipping.Methods.EditMethodDetails") - @Model.Name @Html.ActionLink("(" + T("Admin.Configuration.Shipping.Methods.BackToList") + ")", "Methods")
    + @Html.Widget("admin_button_toolbar_before") + + + @Html.Widget("admin_button_toolbar_after")
    @Html.Partial("_CreateOrUpdateMethod", Model) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shipping/Providers.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shipping/Providers.cshtml index 094d0278fd..33faf37843 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shipping/Providers.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shipping/Providers.cshtml @@ -8,6 +8,11 @@ @T("Admin.Configuration.Shipping.Providers")
    +
    + @Html.Widget("admin_button_toolbar_before") + + @Html.Widget("admin_button_toolbar_after") +
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shipping/_CreateOrUpdateMethod.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shipping/_CreateOrUpdateMethod.cshtml index fa6e0fa7e8..8d63b6e67b 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shipping/_CreateOrUpdateMethod.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shipping/_CreateOrUpdateMethod.cshtml @@ -87,45 +87,20 @@ @helper TabRestrictions() { -
    - @Html.Raw(T("Admin.Configuration.Shipping.Methods.RestrictionNote")) -
    + if (Model.Id == 0) + { +
    + @T("Admin.Configuration.Restriction.SaveBeforeEdit") +
    + } + else if (Model.FilterConfigurationUrls.Count == 0) + { +
    + @T("Admin.Configuration.Shipping.Methods.RestrictionNote") +
    + }
    -
    - - - - - - - - - - - - -
    - @Html.SmartLabelFor(model => model.ExcludedCustomerRoleIds) - - @Html.ListBoxFor(model => model.ExcludedCustomerRoleIds, - new MultiSelectList(Model.AvailableCustomerRoles, "Value", "Text"), - new { multiple = "multiple", @class = "control-xlarge" }) - @Html.ValidationMessageFor(model => model.ExcludedCustomerRoleIds) -
    - @Html.SmartLabelFor(model => model.ExcludedCountryIds) - - @Html.ListBoxFor(model => model.ExcludedCountryIds, - new MultiSelectList(Model.AvailableCountries, "Value", "Text"), - new { multiple = "multiple", @class = "control-xlarge" }) - @Html.ValidationMessageFor(model => model.ExcludedCountryIds) -
    - @Html.SmartLabelFor(model => model.CountryExclusionContext) - - @Html.DropDownListFor(model => model.CountryExclusionContext, Model.AvailableCountryExclusionContextTypes) - @Html.ValidationMessageFor(model => model.CountryExclusionContext) -
    -
    } @@ -136,14 +111,19 @@ i = 0; if (configUrls.length > 0) { - getFilterConfiguration(configUrls[i], $('#FilterOutShippingMethodContainer')); + getFilterConfiguration(configUrls[0], $('#FilterOutShippingMethodContainer')); } function getFilterConfiguration(url, container) { - $.get(url, function (html) { - container.append(html); - if (++i < configUrls.length) - getFilterConfiguration(configUrls[i], container); + $.ajax({ + url: url, + cache: false, + dataType: 'html', + success: function (html) { + container.append(html); + if (++i < configUrls.length) + getFilterConfiguration(configUrls[i], container); + } }); } }); diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ShoppingCart/CurrentWishlists.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ShoppingCart/CurrentWishlists.cshtml index 57b381adaa..1f401b3bf6 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/ShoppingCart/CurrentWishlists.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/ShoppingCart/CurrentWishlists.cshtml @@ -10,7 +10,7 @@ {
    - + @T("Admin.CurrentWishlists")
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Theme/Configure.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Theme/Configure.cshtml index a84848f8b0..627eb3ba88 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Theme/Configure.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Theme/Configure.cshtml @@ -27,6 +27,8 @@ @ViewBag.Title @Html.ActionLink("(" + T("Admin.Common.BackToList") + ")", "List", new { storeId = Model.StoreId })
    + @Html.Widget("admin_button_toolbar_before") +
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Theme/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Theme/List.cshtml index ffcf8688ca..15e975c4ce 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Theme/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Theme/List.cshtml @@ -5,7 +5,6 @@ @using SmartStore.Core.Themes; @{ - //page title ViewBag.Title = T("Admin.Configuration.Themes").Text; } @@ -17,6 +16,8 @@ @T("Admin.Configuration.Themes")
    + @Html.Widget("admin_button_toolbar_before") +
    @@ -227,7 +230,7 @@ - +
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Topic/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Topic/List.cshtml index 00f7b28cee..18fda90102 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Topic/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Topic/List.cshtml @@ -1,7 +1,6 @@ @model TopicListModel @using Telerik.Web.Mvc.UI @{ - //page title ViewBag.Title = T("Admin.ContentManagement.Topics").Text; }
    @@ -16,68 +15,69 @@
    - - - - - - - - - -
    - @Html.SmartLabelFor(model => model.SearchStoreId) - - @Html.DropDownList("SearchStoreId", Model.AvailableStores) -
    -   - - -
    -

    -

    + +@if (Model.AvailableStores.Count > 1) +{ + + + + + + + + + +
    + @Html.SmartLabelFor(model => model.SearchStoreId) + + @Html.DropDownList("SearchStoreId", Model.AvailableStores, T("Admin.Common.All")) +
    +   + + +
    +} + +

    +
    @(Html.Telerik().Grid() - .Name("topics-grid") - .Columns(columns => - { - columns.Bound(x => x.SystemName) - .Width(200) - .Template(x => Html.ActionLink(x.SystemName, "Edit", new { id = x.Id })) + .Name("topics-grid") + .Columns(columns => + { + columns.Bound(x => x.SystemName) + .Width(280) + .Template(x => Html.ActionLink(x.SystemName, "Edit", new { id = x.Id })) .ClientTemplate("<#= SystemName #>"); columns.Bound(x => x.Title); - columns.Bound(x => x.IsPasswordProtected) - .Width(100) - .Template(item => @Html.SymbolForBool(item.IsPasswordProtected)) - .ClientTemplate(@Html.SymbolForBool("IsPasswordProtected")) - .Centered(); - columns.Bound(x => x.IncludeInSitemap) - .Width(100) - .Template(item => @Html.SymbolForBool(item.IncludeInSitemap)) - .ClientTemplate(@Html.SymbolForBool("IncludeInSitemap")) - .Centered(); - columns.Bound(x => x.RenderAsWidget) - .Width(100) - .Template(item => @Html.SymbolForBool(item.RenderAsWidget)) - .ClientTemplate(@Html.SymbolForBool("RenderAsWidget")) - .Centered(); - columns.Bound(x => x.WidgetZone); - columns.Bound(x => x.Priority) + columns.Bound(x => x.IsPasswordProtected) + .Template(item => @Html.SymbolForBool(item.IsPasswordProtected)) + .ClientTemplate(@Html.SymbolForBool("IsPasswordProtected")) + .Centered(); + columns.Bound(x => x.IncludeInSitemap) + .Template(item => @Html.SymbolForBool(item.IncludeInSitemap)) + .ClientTemplate(@Html.SymbolForBool("IncludeInSitemap")) + .Centered(); + columns.Bound(x => x.RenderAsWidget) + .Template(item => @Html.SymbolForBool(item.RenderAsWidget)) + .ClientTemplate(@Html.SymbolForBool("RenderAsWidget")) + .Centered(); + columns.Bound(x => x.WidgetZone); + columns.Bound(x => x.LimitedToStores) + .Template(item => @Html.SymbolForBool(item.LimitedToStores)) + .ClientTemplate(@Html.SymbolForBool("LimitedToStores")) + .Hidden(Model.AvailableStores.Count <= 1) + .Centered(); + columns.Bound(x => x.Priority) .Centered(); - columns.Bound(x => x.Id) - .Width(50) - .Centered() - .Template(x => Html.ActionLink(T("Admin.Common.Edit").Text, "Edit", new { id = x.Id })) - .ClientTemplate("\">" + T("Admin.Common.Edit").Text + "") - .Title(T("Admin.Common.Edit").Text); - }) - .DataBinding(dataBinding => dataBinding.Ajax().Select("List", "Topic")) + }) + .DataBinding(dataBinding => dataBinding.Ajax().Select("List", "Topic")) .ClientEvents(events => events.OnDataBinding("onDataBinding")) - .EnableCustomBinding(true)) + .EnableCustomBinding(true))
    diff --git a/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/Edit.cshtml index ea6dfc9cdf..4914d5c1ea 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/Edit.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/Edit.cshtml @@ -11,6 +11,9 @@ @title - @Model.Slug @Html.ActionLink("(" + T("Admin.Common.BackToList") + ")", "List")
    + +  @T("Admin.Common.Entity") + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/List.cshtml index 648f7bfef5..fa2a560a7d 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/List.cshtml @@ -52,6 +52,14 @@ @Html.EditorFor(model => Model.IsActive) + + + @Html.SmartLabelFor(x => x.LanguageId) + + + @Html.DropDownListFor(x => x.LanguageId, Model.AvailableLanguages, T("Common.Unspecified")) + +   @@ -92,7 +100,8 @@ .Width(200); columns.Bound(x => x.EntityId) .Width(140) - .Centered(); + .Centered() + .ClientTemplate("<#= EntityId #>"); columns.Bound(x => x.IsActive) .Template(item => @Html.SymbolForBool(item.IsActive)) .ClientTemplate(@Html.SymbolForBool("IsActive")) @@ -204,7 +213,8 @@ SeName: $('#@Html.FieldIdFor(model => model.SeName)').val(), EntityName: $('#@Html.FieldIdFor(model => model.EntityName)').val(), EntityId: $('#@Html.FieldIdFor(model => model.EntityId)').val(), - IsActive: $('#@Html.FieldIdFor(model => model.IsActive)').val() + IsActive: $('#@Html.FieldIdFor(model => model.IsActive)').val(), + LanguageId: $('#@Html.FieldIdFor(model => model.LanguageId)').val() }; e.data = searchModel; } diff --git a/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/NamesPerEntity.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/NamesPerEntity.cshtml new file mode 100644 index 0000000000..14e9654c01 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/NamesPerEntity.cshtml @@ -0,0 +1,6 @@ +@{ + Layout = null; +} + +  @T("Common.ShowAll") (@ViewBag.CountSlugsPerEntity) + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/_CreateOrUpdate.cshtml index 3d147687f9..ba361be39e 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/_CreateOrUpdate.cshtml @@ -2,6 +2,7 @@ @using SmartStore.Admin.Models.UrlRecord; @using Telerik.Web.Mvc.UI; +@Html.ValidationSummary() @Html.HiddenFor(model => model.Id) @@ -13,14 +14,6 @@ @Html.DisplayFor(model => model.Id) - - - - diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Web.config b/src/Presentation/SmartStore.Web/Administration/Views/Web.config index 3da158da4f..a78708ea5a 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Web.config +++ b/src/Presentation/SmartStore.Web/Administration/Views/Web.config @@ -10,7 +10,7 @@ - + @@ -49,7 +49,7 @@ - + diff --git a/src/Presentation/SmartStore.Web/Administration/Web.config b/src/Presentation/SmartStore.Web/Administration/Web.config index 7d3a217dd0..7f12a993a8 100644 --- a/src/Presentation/SmartStore.Web/Administration/Web.config +++ b/src/Presentation/SmartStore.Web/Administration/Web.config @@ -44,7 +44,7 @@ - + @@ -64,7 +64,7 @@ - + @@ -92,7 +92,7 @@ - + @@ -100,12 +100,16 @@ - + + + + + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/packages.config b/src/Presentation/SmartStore.Web/Administration/packages.config index cf47679773..7e1b1648ae 100644 --- a/src/Presentation/SmartStore.Web/Administration/packages.config +++ b/src/Presentation/SmartStore.Web/Administration/packages.config @@ -1,11 +1,11 @@  - - - + + + - + @@ -15,6 +15,6 @@ - + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/sitemap.config b/src/Presentation/SmartStore.Web/Administration/sitemap.config index 573d1227b0..a1501d7653 100644 --- a/src/Presentation/SmartStore.Web/Administration/sitemap.config +++ b/src/Presentation/SmartStore.Web/Administration/sitemap.config @@ -94,6 +94,7 @@ + @@ -127,7 +128,9 @@ - + + + @@ -144,7 +147,6 @@ - diff --git a/src/Presentation/SmartStore.Web/App_Data/Localization/App/de/all.smres.xml b/src/Presentation/SmartStore.Web/App_Data/Localization/App/de/all.smres.xml index cc58de88a2..8e379a8055 100644 --- a/src/Presentation/SmartStore.Web/App_Data/Localization/App/de/all.smres.xml +++ b/src/Presentation/SmartStore.Web/App_Data/Localization/App/de/all.smres.xml @@ -135,6 +135,9 @@ Keine Aufträge + + Dieser Auftrag konnte Ihnen nicht zugeordnet werden. + Auftragsdatum @@ -489,6 +492,15 @@ Registrierung + + Der Kunde ist bereits registriert. + + + Eine Suchmaschine kann nicht registriert werden. + + + Das Konto einer geplanten Aufgabe kann nicht registriert werden. + Die eingegebene E-Mail-Adresse wird bereits verwendet. @@ -597,6 +609,9 @@ Hersteller ('{0}') gelöscht + + Auftrag {0} gelöscht + Produkt ('{0}') gelöscht @@ -1365,6 +1380,9 @@ Die Warengruppe wurde gelöscht. + + Andere Beschreibung anzeigen + Rabatte @@ -1411,10 +1429,10 @@ Gelöscht - Beschreibung + Obere Beschreibung - Beschreibung der Warengruppe + Beschreibung der Warengruppe, die auf der Warengruppenseite oberhalb der Produkte angezeigt wird. Reihenfolge @@ -1473,6 +1491,9 @@ Legt die Vater-Warengruppe fest. Lassen Sie das Feld leer, um eine Warengruppe erster Ebene zu erzeugen. + + Übergeordnete Warengruppe + Bild @@ -1930,6 +1951,9 @@ Produkte, die Attributwerte vom Typ "Produkt" haben, können nicht Bestandteil eines Bundles sein. + + Hinweise zu Produkt-Bundles + Das Produkt muss gespeichert werden, bevor Produkte zur Stückliste hinzugefügt werden können. @@ -1976,7 +2000,7 @@ Checkout-Selling - Hinzufügen + Checkout-Selling-Produkt hinzufügen Produkt @@ -2032,6 +2056,12 @@ Eine kommagetrennte Liste der zulässigen Bestellmengen, die für dieses Produkt gelten. Kunden wählen i.d.F. eine Bestellmenge aus einem Dropdown-Menü aus, anstatt eine freie Eingabe zu tätigen. + + Summe genehmigter Bewertungen + + + Summe genehmigter Rezensionen + Verknüpftes Produkt @@ -2096,7 +2126,7 @@ {0} pro Einheit (Grundpreis: {1} pro {2}) - "Maßeinheit" ist erforderlich, wenn der Grundpreis berechnet werden soll. + Grundpreis Maßeinheit "Maßeinheit" ist erforderlich, wenn der Grundpreis berechnet werden soll. @@ -2203,12 +2233,18 @@ EAN (Europa), GTIN (global trade item number), UPC (USA), JAN (Japan) oder ISBN (Bücher). + + Hat angewendete Rabatte + Hat Probedownload Legt fest, ob der Kunde eine Beispieldatei vor dem Checkout runterladen kann. + + Hat Staffelpreise + Hat Benutzervereinbarung @@ -2221,6 +2257,12 @@ Die Höhe des Produktes. + + Homepage Reihenfolge + + + Legt die Anzeige-Reihenfolge der Produkte auf der Homepage fest (1 steht bspw. für das erste Element in der Liste). + ID @@ -2275,6 +2317,9 @@ Die Länge des Produktes. + + Niedrigster Attributkombinationspreis + Aktion bei Erreichen des Meldebestandes @@ -2344,6 +2389,12 @@ Produktname ist erforderlich + + Summe nicht genehmigter Bewertungen + + + Summe nicht genehmigter Rezensionen + Benachrichtigt den Administrator, wenn die Mindestmenge unterschritten wird. @@ -2566,6 +2617,18 @@ Warengruppe eingrenzen + + Auf Homepage angezeigt + + + Filtert nach Produkten, die auf der Homepage angezeigt oder nicht angezeigt werden. + + + Veröffentlicht + + + Filtert nach veröffentlichten bzw. unveröffentlichten Produkten. + Hersteller @@ -2831,7 +2894,7 @@ Cross-Selling - Hinzufügen + Cross-Selling-Produkt hinzufügen Reihenfolge @@ -2840,7 +2903,7 @@ Produkt - Sie müssen das Produkt speichern, bevor Sie verwandte Produkte festlegen können + Sie müssen das Produkt speichern, bevor Sie Cross-Selling-Produkte hinzufügen können. Spezifikationsattribute @@ -3034,12 +3097,15 @@ Es wurden {0} gegenseitige Zuordnung(en) erstellt. - - CSV-Datei + + CSV Konfiguration Die Daten wurden erfolgreich geändert. + + Datenaustausch + Die Daten wurden erfolgreich gespeichert. @@ -3052,38 +3118,59 @@ Ausgewählte Löschen + + Alle löschen + Möchten Sie dieses Element wirklich löschen? Möchten Sie "{0}" wirklich löschen? + + Gelöscht + Ausgewählte löschen Bearbeiten + + Die E-Mail wurde erfolgreich versendet. + + + Bitte geben Sie eine E-Mail-Adresse ein. + + + Objekt + ID Die eindeutige nummerische ID des Datensatzes + + Fehler + Fehler beim Senden einer E-Mail - - Excel-Datei + + PDF Export Bitte haben Sie einen Augenblick Geduld, während der Export durchgeführt wird + + Alle exportieren + Keine Daten zu exportieren. - - Als CSV-Datei exportieren + + Ausgewählte exportieren Nach Excel exportieren @@ -3100,20 +3187,29 @@ Nur Ausgewählte nach PDF exportieren - - Inhaltsverzeichnis - Zu viele Objekte! Mehr als 500 Objekte können nicht konvertiert werden. Bitte reduzieren Sie die Anzahl ausgewählter Datensätze. Als XML exportieren - - Alles als XML exportieren + + Die Datei ist in Benutzung und kann daher nicht geöffnet werden. + + + Datei nicht gefunden + + + {0} Dateien wurden gelöscht + + + Die Datei muss vom Typ {0} sein. - - Nur Ausgewählte als XML exportieren + + {0} Verzeichnisse wurden gelöscht + + + FTP-Status {0} ({1}). Allgemein @@ -3139,41 +3235,17 @@ Verbergen - - Von CSV-Datei importieren - - - Von Excel importieren - - - Aktiv seit: {0}. - - - Import abbrechen - - - Import wurde abgebrochen - - - Soll der aktive Importvorgang wirklich abgebrochen werden? Bislang importierte Produkte werden nicht gelöscht. + + HTTP-Status {0} ({1}). - - Vollständigen Bericht runterladen... + + Ignorieren - - Der Import läuft jetzt im Hintergrund . Sie können den Fortschritt bzw. das Ergebnis des letzten Importvorganges jederzeit im Import-Dialog einsehen. + + Importdatei - - <b>Letzter Import</b>: {0}{1}. - - - Kein Bericht verfügbar - - - {0} von {1} Zeilen verarbeitet. - - - {0} neu, {1} aktualisiert - bei {2} Warnung(en) und {3} Fehler(n). + + Importdateien Bitte warten Sie, der Importvorgang wird durchgeführt @@ -3181,6 +3253,9 @@ Info + + Letzte Ausführung + Lizenzieren @@ -3193,18 +3268,51 @@ Alle nicht gespeicherten Änderungen gehen verloren. Sind Sie sicher? + + Neue Datensätze + Nein Nein, abbrechen + + Es wurden keine Einträge ausgewählt. + + + von + + + Platzhalter + Bitte auswählen Vorschau + + Der Provider {0} konnte nicht geladen werden. + + + Öffentliche Dateien + + + Es wurden {0} Datensätze gelöscht. + + + Überspringen + + + Legt die Anzahl der zu überspringenden Datensätze fest. + + + Begrenzen + + + Legt die maximale Anzahl der zu verarbeitenden Datensätze fest. + Auf Standardwerte zurücksetzen @@ -3214,6 +3322,9 @@ Sie müssen die Anwendung neu starten, damit die Änderungen wirksam werden. + + Einschränkungen + Speichern @@ -3223,6 +3334,9 @@ Suchen + + Ausgewählte + Jetzt senden @@ -3235,6 +3349,12 @@ Anzeigen + + Werte für Überspringen und Begrenzen müssen größer oder gleich 0 sein. + + + Ausgelassen + Standard @@ -3265,18 +3385,30 @@ Alle Shops + + Erfolgreich am + Der Vorgang wurde erfolgreich ausgeführt. + + Zeilen insgesamt + Baumansicht Unlizenziert + + Nicht unterstützter Entitätstyp '{0}' + Aktualisieren + + Aktualisiert + Bitte eine Datei hochladen. @@ -3286,6 +3418,9 @@ Bitte warten, die Verarbeitung läuft... + + Warnungen + Falsche E-Mail @@ -3319,9 +3454,6 @@ Filtert Ereignisse nach Ereignistypen. - - Ereignistyp - Nachricht @@ -3346,6 +3478,15 @@ Kunden-E-Mail + + Filtert Ergebnisse nach E-Mail-Adresse der Kunden. + + + Kundensystemkonto + + + Filtert Ergebnisse nach Kundenystemkonten. + Aktivitätstypen @@ -3523,6 +3664,9 @@ Zurück zur Länderliste + + Das Land kann nicht gelöscht werden, weil ihm Adressen zugeordnet sind. + Das Land wurde gelöscht. @@ -3611,7 +3755,7 @@ Neue Region hinzufügen - Das Land kann nicht gelöscht werden. Es wird bei vorhandenen Adressen verwendet. + Das Bundesland\Region kann nicht gelöscht werden, weil ihm Adressen zugeordnet sind. Region bearbeiten @@ -3664,15 +3808,12 @@ Zurück zur Währungsliste - - Die Leitwährung für Währungsumrechnungen kann nicht gelöscht werden. - - - Die Leitwährung des Shops kann nicht gelöscht werden. - Die Währung wurde gelöscht. + + Die Währung kann nicht gelöscht oder deaktiviert werden, weil sie dem Shop "{0}" als Leit- oder Umrechnungswährung zugeordnet ist. + Währungsdetails bearbeiten @@ -3731,16 +3872,10 @@ Online Wechselkursdienst - Hauptwährung für Währungsumrechnung + Umrechnungswährung - Leitwährung im Shop - - - Als Hauptwährung für Währungsumrechnung festlegen - - - Als Leitwährung im Shop festlegen + Leitwährung Name @@ -3754,6 +3889,18 @@ Der Name ist erforderlich. + + Ist Umrechnungswährung für + + + Eine Liste mit Shops, in denen die Währung Umrechnungswährung ist. + + + Ist Leitwährung für + + + Eine Liste mit Shops, in denen die Währung Leitwährung ist. + Veröffentlicht @@ -3922,6 +4069,9 @@ Die E-Mail wurde erfolgreich versandt. + + Test der E-Mail-Funktion. + Das E-Mail-Konto wurde erfolgreich bearbeitet. @@ -3962,10 +4112,10 @@ Legt die Anzeige-Priorität fest (1 steht bspw. für das erste Element in der Liste). - Dateiname des Flaggenbildes + Flaggenbild - Dateiname des Flaggenbildes. Die Datei sollte in folgendem Verzeichnis gespeichert werden: \\images\\flags. + Legt das Flaggenbild fest. Die Dateien der Flaggenbilder müssen in /Content/Images/flags/ liegen. Gebietsschema @@ -4007,7 +4157,7 @@ Der SEO-Code muss 2 Zeichen haben. - Der SEO-Code muss eindeutig sein. + Bitte legen Sie einen SEO Sprach-Code fest. Ressourcen importieren @@ -4174,6 +4324,21 @@ Stornierung möglich + + Langtext + + + Legt eine vollständige Beschreibung der Zahlungsmethode fest. Sie erscheint in der Zahlungsliste im Kassenbereich. + + + Es wurden keine Möglichkeiten zur Einschränkung von Zahlungsarten gefunden. + + + Kurzbeschreibung + + + Legt eine Kurzbeschreibung der Zahlungsmethode fest. + Plugins @@ -4303,6 +4468,9 @@ Das Plugin wurde deinstalliert. + + Beim Aufruf eines Plugins ist ein unbekannter Fehler aufgetreten. Details entnehmen Sie bitte der folgenden Meldung. + Verpackungseinheiten @@ -4333,6 +4501,9 @@ Regionale Einstellungen + + Sie müssen zunächst speichern, bevor Sie Einschränkungen festlegen können. + Einstellungen @@ -4372,6 +4543,12 @@ Aktiviert den Blog. + + Maximales Alter (in Tagen) + + + Legt das maximale Blog-Alter in Tagen fest. Ältere Blog-Einträge werden im RSS-Feed nicht exportiert. + Benachrichtigung bei neuen Kommentaren @@ -4394,7 +4571,7 @@ RSS-Feed URL im Browser in der Adressleiste anzeigen - Legt fest, ob der RSS-Feed-Link in der Browser-Adressleiste zu angezeigt werden soll. + Legt fest, ob der RSS-Feed-Link in der Adressleiste des Browsers angezeigt werden soll. Katalog-Einstellungen @@ -4453,6 +4630,12 @@ Legt die Anzahl der dargestellten Produkte pro Seite fest. + + Standardsortierreihenfolge für Produkte + + + Legt die Standardsortierreihenfolge für Produkte fest. + Standard Listendarstellung @@ -4507,6 +4690,24 @@ Legt fest, ob der Kaufen-Button in Produktlisten ausgeblendet werden soll. + + Standardbild bei Warengruppen ausblenden + + + Legt fest, ob das Standardbild bei Warengruppen ausgeblendet werden soll. Das Standardbild wird angezeigt, wenn der Warengruppe kein Bild zugeordnet ist. + + + Standardbild bei Herstellern ausblenden + + + Legt fest, ob das Standardbild bei Herstellern ausgeblendet werden soll. Das Standardbild wird angezeigt, wenn dem Hersteller kein Bild zugeordnet ist. + + + Standardbild bei Produkten ausblenden + + + Legt fest, ob das Standardbild bei Produkten ausgeblendet werden soll. Das Standardbild wird angezeigt, wenn dem Produkt kein Bild zugeordnet ist. + Höhe des gekürzten Langtextes @@ -4582,6 +4783,12 @@ Anzahl der angezeigten Produkt-Tags + + Preisanzeige + + + Legt fest, ob bzw. welcher Typ von Preis in Produktlisten angezeigt werden soll. + Produktdetail @@ -4699,6 +4906,12 @@ Legt die Anzahl der sichtbaren Produkte in der Liste der kürzlich angesehenen Produkte fest. + + Produktbeschreibung durchsuchen + + + Legt fest, ob die Produktbeschreibung in der Suche einbezogen werden soll. + Anzahl Ergebnisse pro Seite auf Suchseite @@ -4795,6 +5008,12 @@ Bestimmt ob Hersteller auf der Homepage als Bilder oder textuelle Links angezeigt werden. + + Bilder von Herstellern anzeigen + + + Legt fest, ob Herstellerbilder auf der Produktdetailseite angezeigt werden sollen. + Zeige Hersteller auf der Homepage @@ -4849,6 +5068,12 @@ Zeigt das Gewicht des Produktes an. + + Filterergebnisse nach Trefferanzahl sortieren + + + Legt fest, das Filterergebnisse absteigend nach der Anzahl an Übereinstimmungen sortiert werden. Ist diese Option deaktiviert, so wird in der für die Werte festgelegten Reihenfolge sortiert. + Unterwarengruppen anzeigen @@ -5029,6 +5254,12 @@ Legt fest, ob das Eingabefeld "Land" während der Registrierung aktiviert ist. + + Kunden können Kundennummer hinterlegen + + + Bestimmt ob Kunden eine Kundennummer angeben können, wenn für diese noch kein Wert hinterlegt wurde. + Benutzerformular-Felder @@ -5053,6 +5284,18 @@ Bestimmt ob Kundennummern hinterlegt werden können. + + Kundennummern + + + Legt fest, ob Kundennummern vergeben werden und ob diese automatisch vergeben werden sollen. + + + Darstellung der Kundennummer + + + Legt die Darstellung und Handhabung der Kundennummer gegenüber dem Kunden fest. + Kundeneinstellungen @@ -5083,6 +5326,18 @@ Legt die Standard-Zeitzone für den Shop fest. + + Kundennummern im Frontend anzeigen + + + Bestimmt ob Kunden ihre Kundennummer in Ihrem Account-Bereich einsehen können. + + + Einwilligungserklärung im Kontaktformular fordern + + + Bestimmt ob im Kontaktformular eine Checkbox angezeigt wird, die den Benutzer auffordert der Speicherung seiner Daten zuzustimmen. + Automatische Registrierung aktiviert @@ -5152,6 +5407,12 @@ Legt fest, ob die Eingabe der Telefonnummer erforderlich ist. + + Kundengruppe bei Registrierungen + + + Legt eine Kundengruppe fest, die neu registrierten Kunden zugeordnet wird. + Anzeige des Registrierungsdatums eines Kunden @@ -5230,6 +5491,24 @@ Legt fest, ob Angabe der Postleitzahl erforderlich ist. + + Zeitlimit für Bilder-Download (Minuten) + + + Legt das Zeitlimit für den Bilder-Download in Minuten fest. + + + Bilderordner (relativer Pfad) + + + Legt einen relativen Pfad zu einem Ordner mit zu importierenden Bildern fest (z.B. Inhalt\Bilder). + + + Maximale Länge von Datei- und Ordnernamen + + + Legt die maximale Länge von Datei- und Ordnernamen fest, die im Rahmen eines Imports\Exports erzeugt wurden. + Forum @@ -5365,6 +5644,24 @@ Der Zugriff auf den Adminbereich kann nur noch über diese IP-Adresse erfolgen. + + Unicode-Zeichen erlauben + + + Legt fest, ob als Unicode-Zeichen eingestufte Buchstaben in SEO relevanten Namen erlaubt sind. + + + Bei Abschluss einer Bestellung PDF mitsenden + + + Erstellt bei Abschluss einer Bestellung das Auftrags-PDF-Dokument und hängt es der Kunden-Benachrichtigungs-Email an + + + Bei Bestelleingang PDF mitsenden + + + Erstellt bei Bestelleingang das Auftrags-PDF-Dokument und hängt es der Kunden-Benachrichtigungs-Email an + Bankverbindung @@ -5707,6 +6004,12 @@ Der Sicherheitsschlüssel muss 16 Zeichen haben. + + Extra Disallows für robots.txt + + + Geben Sie hier zusätzliche Pfade an, die als Disallow-Einträge zur robots.txt hinzugefügt werden sollen. Jeder Eintrag muss in einer neuen Zeile erfolgen. + Einstellungen zur Volltextsuche @@ -5761,6 +6064,12 @@ Lokalisierung + + Meta Robots + + + Legt fest, ob und wie Suchmaschinen die Seiten Ihres Shops indexieren. + Seiten-Titel-Anpassung @@ -5815,6 +6124,12 @@ Wenn aktiviert, werden Suchmaschinenfreundliche URLs generiert. + + Zu konvertierende Zeichen + + + Ermöglicht das individuelle Konvertieren von Zeichen bei der Erstellung SEO Namen. Geben Sie hier durch Semikolon getrennt das alte und das neue Zeichen ein, z.B. ä;ae. Jeder Eintrag muss in einer neuen Zeile erfolgen. + Suchmaschinen @@ -5869,6 +6184,12 @@ Shop Information + + Zeichenkette prüfen + + + Geben Sie eine beliebige Zeichenkette ein, um daraus den SEO Namen zu erstellen. Geänderte Einstellungen müssen zuvor gespeichert werden. + Bilder zur Sprachauswahl verwenden @@ -5929,6 +6250,12 @@ Legt die maximal erlaubte Größe (längste Seite) für Bilduploads fest. + + Thumbnail-Größe von Produkten in E-Mails + + + Legt die Thumbnail-Bildgröße (in Pixel) von Produkten in E-Mails fest. Geben Sie 0 ein, um keine Thumbnails anzuzeigen. + Miniwarenkorb-Symbolbild-Größe @@ -6007,6 +6334,12 @@ Legt die Anzahl der angezeigten News auf der Startseite fest. + + Maximales Alter (in Tagen) + + + Legt das maximale News-Alter in Tagen fest. Ältere News werden im RSS-Feed nicht exportiert. + Seitengröße @@ -6049,6 +6382,12 @@ Der Kunde wird direkt auf die Auftrags-Detail-Seite geleitet, falls diese Einstellung aktiviert ist. + + Aufträge aller Shops anzeigen + + + Legt fest, ob dem Kunden die Aufträge aller Shops angezeigt werden sollen. Ist diese Option deaktiviert, so werden nur die Aufträge des aktuellen Shops angezeigt. + Geschenkgutschein ist eingelöst, wenn Auftragsstatus... @@ -6059,7 +6398,7 @@ Der Auftragsstatus konnte nicht auf "in Bearbeitung" gesetzt werden - Geschenkgutschein wird deactiviert, wenn Auftragsstatus ... + Geschenkgutschein wird deaktiviert, wenn Auftragsstatus... Geschenkgutscheine sind deaktiviert, wenn der Auftragsstatus ist … @@ -6103,6 +6442,12 @@ Legen Sie einen Startwert für Bestellnummern fest. Dies ist nützlich, wenn Sie Ihre Aufträge ab einem Bestimmten Wert starten lassen möchten. Der Wert muss größer sein, als die aktuell höchste Bestellnummer und gilt für zukünftige Bestellungen. + + Anzahl der Aufträge pro Seite + + + Legt die Anzahl der dargestellten Aufträge pro Seite fest. + Auftragseinstellungen @@ -6208,6 +6553,12 @@ Bonuspunkte, die für eine Registrierung gewährt werden. + + Punkte abrunden + + + Legt fest, ob bei der Punkteberechnung abgerundet werden soll. Ansonsten werden Bonuspunkte aufgerundet. + Versand-Einstellungen @@ -6322,6 +6673,15 @@ Aktivieren, um Artikel aus der Wunschliste in den Warenkorb zu verschieben. + + Abonnieren von Newslettern + + + Legt fest, ob Kunden bei einer Bestellung Newsletter abonnieren können und ob die Checkbox standardmäßig aktiviert ist. + + + Bestellabschlussseite + Preise bei der Berechnung runden @@ -6335,16 +6695,16 @@ Bestimmt ob der Grundpreis im Warenkorb angezeigt werden soll. - Zeige Kommentarbox auf Bestellabschlussseite + Kommentarbox anzeigen - Legt fest ob der Kunde auf der Bestellabschlussseite einen Kommentar zu seiner Bestellung hinterlegen kann. + Legt fest, ob der Kunde auf der Bestellabschlussseite einen Kommentar zu seiner Bestellung hinterlegen kann. - Rechtliche Hinweise in der Warenkorbübersicht auf der Bestellabschlußseite anzeigen + Rechtliche Hinweise in der Warenkorbübersicht anzeigen - Bestimmt, ob rechtliche Hinweise in der Warenkorbübersicht auf der Bestellabschlußseite angezeigt werden. Dieser Text kann in den Sprachresourcen geändert werden. + Legt fest, ob rechtliche Hinweise in der Warenkorbübersicht auf der Bestellabschlußseite angezeigt werden. Dieser Text kann in den Sprachresourcen geändert werden. Lieferzeiten anzeigen @@ -6358,6 +6718,12 @@ Zeigt die Rabattbox im Warenkorb an. + + Widerrufsverzichtbox für elektronische Leistungen anzeigen + + + Legt fest, ob der Kunde auf der Bestellabschlussseite einem Widerrufsverzicht für elektronische Leistungen zustimmen muss. + Zeige die Geschenkgutschein-Box im Warenkorb @@ -6412,6 +6778,21 @@ Legt fest ob das Produktgewicht im Warenkorb angezeigt wird. + + Zustimmung zur E-Mail Weitergabe an Dritte + + + Legt fest, ob Kunden bei einer Bestellung der Weitergabe ihrer E-Mail Adresse an Dritte zustimmen können und ob die Checkbox dafür standardmäßig aktiviert ist. + + + Text für E-Mail Weitergabe + + + Mit der Übermittlung und Speicherung meiner E-Mail-Adresse durch dritte Parteien bin ich einverstanden. + + + Legt den Text für die Zustimmung zur Weitergabe der E-Mail Adresse an Dritte fest. Wählen Sie bitte einen konkreten Grund für die Weitergabe, z.B. 'Mit der Übermittlung und Speicherung meiner E-Mail-Adresse zur Abwicklung des Käuferschutzes durch Trusted Shops bin ich einverstanden.' + Wunschzettel @@ -6622,6 +7003,12 @@ Der Name ist erforderlich + + Es konnten keine Versandarten geladen werden. + + + Es wurden keine Möglichkeiten zur Einschränkung von Versandarten gefunden. + Die Versandart wurde erfolgreich bearbeitet. @@ -6637,18 +7024,6 @@ Reihenfolge - - Versandart-Einschränkungen - - - Land - - - Markieren Sie die Länder, bei denen die Einschränkung gelten soll - - - Die Einstellungen wurden erfolgreich bearbeitet - SMS-Anbieter @@ -6718,6 +7093,18 @@ Bitte einen Namen angeben. + + Umrechnungswährung + + + Legt die Umrechnungswährung für diesen Shop fest. + + + Leitwährung + + + Legt die Leitwährung des Shops fest. + Gesicherte URL @@ -6725,10 +7112,10 @@ Die gesicherte URL des Shops, z.B. https://www.mein-shop.de/ or http://sharedssl.mein-shop.de/. Die gesicherte URL wird automatisch erkannt, wenn dieses Feld leer ist. - SSL aktivieren + SSL - Aktiviert SSL, falls der Shop SSL gesichert werden soll. + Legt fest, ob der Shop SSL gesichert werden soll. Shop Logo @@ -6823,6 +7210,9 @@ Name + + Theme benötigt keine Konfiguration + LESS CSS Parser Fehler: Ihre Änderungen wurden nicht gespeichert, da Ihre Konfiguration zu einem Fehler im Shop führen würde. Details siehe Fehlerbericht. @@ -7180,6 +7570,24 @@ Liste erlaubter Platzhalter. Diese können in E-Mail-Vorlagen benutzt werden. + + Anhang 1 + + + Eine Datei, die jedem gesendeten E-Mail angehangen werden soll (z.B. AGB, Widerrufsbelehrung etc.) + + + Anhang 2 + + + Eine Datei, die jedem gesendeten E-Mail angehangen werden soll (z.B. AGB, Widerrufsbelehrung etc.) + + + Anhang 3 + + + Eine Datei, die jedem gesendeten E-Mail angehangen werden soll (z.B. AGB, Widerrufsbelehrung etc.) + BCC-Adresse @@ -7660,6 +8068,9 @@ Zurück zur Gesamtliste + + Die Kundengruppe "{0}" wurde nicht gefunden. + Die Kundengruppe wurde gelöscht. @@ -7843,6 +8254,9 @@ Erstellt am + + Kunden GUID + Kundengruppen @@ -7900,6 +8314,9 @@ IP-Adresse, mit der sich der Kunde zuletzt im Shop aktiv war. + + Ist Systemkonto + Mehrwertsteuerfrei @@ -7912,6 +8329,9 @@ Zeigt an, wann der Kunde zuletzt im Shop aktiv war. + + Letztes Login-Datum + Nachname @@ -7933,6 +8353,9 @@ Passwort des Kunden. + + Passwort Salt + Telefon @@ -8287,97 +8710,820 @@ Dashboard - - Herunterladen der hochgeladenen Datei + + Neues Profil - - Download URL + + Standardwert - - Das Download-Objekt wurde gespeichert + + Eigenschaft des Objektes - - Download entfernen + + Importfeld - - Download speichern + + Sie können optional für jedes Feld der Importdatei festlegen, ob und nach welcher Objekteigenschaft dessen Daten zu importieren sind. Gleichnamige Felder werden grundsätzlich immer importiert, sofern sie nicht explizit ignoriert werden sollen. Noch nicht ausgewählte Eigenschaften sind in der Auswahlliste hervorgehoben. Zudem ist die Angabe eines Standardwertes möglich, der angewendet wird, wenn das Importfeld leer ist. Durch Änderung des Trennzeichens werden gespeicherte Zuordnungen ungültig und zurückgesetzt. - - Upload Datei + + Die gespeicherten Feldzuordnungen sind aufgrund der Änderung des Trennzeichens ungültig und wurden zurückgesetzt. - - Benutze Download-URL + + Trennzeichen - - Geschenkgutscheine + + Legt das zu verwendende Trennzeichen für die Felder fest. - - Der neue Geschenkgutschein wurde hinzugefügt + + Geben Sie bitte ein gültiges Trennzeichen ein. - - Geschenkgutschein hinzufügen + + Inneres Anführungszeichen - - Zurück zur Gesamtliste + + Legt das innere Anführungszeichen (Escaping) fest. - - Der Geschenkgutschein wurde gelöscht + + Geben Sie bitte ein gültiges, inneres Anführungszeichen (Escaping) ein. - - Geschenkgutscheindetails + + Trennzeichen und inneres Anführungszeichen können in CSV Dateien nicht gleich sein. - - Gesamtbetrag + + Anführungszeichen - - Wert des Geschenkgutscheines + + Legt das zu verwendende Anführungszeichen fest. - - Erstellt am + + Geben Sie bitte ein gültiges Anführungszeichen ein. - - Gibt an, an welchem Datum der Geschenkgutschein erstellt wurde. + + Alle Felder in Anführungszeichen - - Gutscheinnummer + + Legt fest, ob die Werte aller Felder in Anführungszeichen gestellt werden sollen. - - Gutscheinnummer generieren + + Trennzeichen und Anführungszeichen können in CSV Dateien nicht gleich sein. - - Gutscheinnummer. Kann während des Checkouts eingelöst werden. + + Mehrzeilen erlaubt - - Typ des Geschenkgutscheines + + Legt fest, ob mehrzeilige Feldwerte unterstützt werden. - - Gibt den Typ des Geschenkgutscheines an. + + Überflüssige Leerzeichen entfernen - - Aktiviert + + Legt fest, ob Leerzeichen am Anfang und am Ende eines Feldwertes entfernt werden sollen. - - Gibt an, ob der Geschenkgutschein aktiviert und einlösbar ist. + + Stapelgröße - - Empfänger ist benachrichtigt + + Legt die maximale Anzahl der Datensätze pro Exportdatei fest. 0 ist der Standard und bedeutet, dass alle Datensätze in eine Datei exportiert werden. - - Empfänger ist benachrichtigt + + Ein System-Exportprofil kann nicht gelöscht werden. - - Empfänger benachrichtigen + + Nach erfolgreicher Veröffentlichung aufräumen - - Nachricht + + Legt fest, ob nicht mehr benötigte Dateien gelöscht werden sollen, nachdem alle Veröffentlichungen erfolgreich waren. - - Nachricht eingeben. + + Einstellungen übernehmen von - - Auftrag + + Legt das Exportprofil fest, von welchem die Einstellungen übernommen werden sollen. - + + Dies ist eine automatische Benachrichtung von Shop "{0}" über einen erfolgten Datenexport. + + + Export von Profil "{0}" ist abgeschlossen + + + E-Mail Adressen an + + + Legt die E-Mail Adressen fest, an die die Benachrichtigung geschickt werden soll. + + + Die folgenden spezifischen Angaben werden durch den Provider beim Export berücksichtigt. + + + Der Export-Provider <b>{0}</b> benötigt keine weitergehende Konfiguration. + + + ZIP-Archiv erstellen + + + Legt fest, ob die Exportdateien in einem ZIP-Archiv zusammengefasst werden sollen. Das Archiv verbleibt im temporären Ordner des Exportprofils ohne weitere Vearbeitung. + + + Mindestens eine Datei konnte nicht kopiert werden. + + + Legt fest, ob die Exportdateien in einem ZIP-Archiv zusammengefasst und nur das Archiv bereitgestellt werden soll. + + + Art der Veröffentlichung + + + Legt die Art Veröffentlichung fest. + + + E-Mail Konto + + + Legt das E-Mail Konto fest, über welches die Daten verschickt werden sollen. + + + E-Mail Adressen an + + + Legt die E-Mail Adressen fest, an die die Daten verschickt werden soll. + + + E-Mail Betreff + + + Legt den Betreff der verschickten Daten fest. + + + Ordnerpfad + + + Legt den Pfad (relativ oder absolut) zu einem Ordner fest, in den die Daten bereitgestellt werden sollen. + + + HTTP Übertragungsart + + + Legt fest, aus welcher Art die Exportdateien per HTTP übertragen werden sollen. + + + Legt fest, ob die exportierten Daten in einen übers Internet zugänglichen Ordner kopiert werden sollen. + + + Name des Profils + + + Legt den Namen des Veröffentlichungsprofils fest. + + + Es liegen keine Veröffentlichungsprofile vor. + + + Legen Sie über <b>Neues Profil</b> ein oder mehrere Veröffentlichungsprofile an, um festzulegen wie mit den Exportdateien weiter zu verfahren ist. + + + Passiver Modus + + + Legt fest, ob Daten im aktiven oder passiven Modus ausgetauscht werden sollen. + + + Passwort + + + Legt das Passwort fest. + + + Veröffentlichungsprofile + + + Veröffentlichungsziel + + + Name des Unterordners + + + Legt den Namen eines Unterordners fest, in den die Daten veröffentlicht werden sollen. + + + URL\Host + + + Legt die URL bzw. den Host-Namen fest, an die die Daten übermittelt werden sollen. + + + Benutzername + + + Legt den Benutzernamen fest. + + + SSL verwenden + + + Legt fest, ob einen SSL (Secure Sockets Layer) Verbindung genutzt werden soll. + + + Bei einer großen Anzahl an Exportdateien wird empfohlen die Option <b>ZIP-Archiv erstellen</b> zu benutzen. Das spart Zeit und vermeidet Probleme, wie z.B. ein volles E-Mail Postfach. + + + E-Mail Benachrichtigung + + + Legt das E-Mail Konto fest, über welches eine Benachrichtigung über die Fertigstellung des Exports verschickt werden soll. + + + Das Exportprofil ist deaktiviert. Für eine Exportvorschau muss das Exportprofil aktiviert sein. + + + Objekt + + + Der Objekttyp, den der Provider verarbeitet. + + + Exportdateien + + + Dateityp + + + Der Dateityp der exportierten Daten. + + + Muster für Dateinamen + + + Legt das Muster fest, nach dem Dateinamen erzeugt werden. + + + Bitte ein gültiges Muster für Dateinamen eingeben. Beispiel: %Store.Id%-%Profile.Id%-%File.Index%-%Profile.SeoName% + + + ID des Exportprofils;Ordername des Exportprofils;SEO Name des Exportprofils;Shop ID;SEO Name des Shops;Mit 1 beginnender Dateiindex;Zufallszahl;UTC Zeitstempel + + + Verfügbar bis + + + Nach der Verfügbarkeitsmenge filtern. + + + Verfügbar von + + + Nach der Verfügbarkeitsmenge filtern. + + + Rechnungsländer + + + Nach Rechnungsländern filtern. + + + Warengruppen + + + Nach Warengruppen filtern. + + + Erstellt von + + + Nach dem Erstellungsdatum filtern. + + + Erstellt bis + + + Nach dem Erstellungsdatum filtern. + + + Kundengruppen + + + Nach Kundengruppen filtern. + + + Nur empfohlene Produkte + + + Nach empfohlenen Produkten filtern. Wird nur bei der Filterung nach Warengruppen und Hersteller angewendet. + + + Hat x Bestellungen + + + Nach der Anzahl der getätigten Bestellungen filtern. + + + Hat Betrag x ausgegeben + + + Nach dem insgesamt ausgegebenen Betrag filtern. + + + Produkt-ID bis + + + Nach der Produkt-ID filtern. + + + Produkt-ID von + + + Nach der Produkt-ID filtern. + + + Nur aktive Kunden + + + Nach aktiven bzw. inaktiven Kunden filtern. + + + Nur aktive Abonnenten + + + Nach aktiven bzw. inaktiven Newsletter Abonnenten filtern. + + + Veröffentlicht + + + Nach Veröffentlichung filtern. + + + Nur MwSt. befreite Kunden + + + Nach MwSt. befreiten Kunden filtern. + + + Zuletzt aktiv von + + + Nach dem Datum der letzten Shop Aktivität filtern. + + + Zuletzt aktiv bis + + + Nach dem Datum der letzten Shop-Aktivität filtern. + + + Hersteller + + + Nach Hersteller filtern. + + + Legen Sie individuelle Filter fest, um die zu exportierenden Daten einzugrenzen. + + + Auftragsstatus + + + Nach Auftragsstaus filtern. + + + Zahlungsstatus + + + Nach Zahlungsstatus filtern. + + + Preis bis + + + Nach dem Preis filtern. + + + Preis von + + + Nach dem Preis filtern. + + + Produkt-Tag + + + Nach Produkt-Tag filtern. + + + Produkttyp + + + Nach Produkttyp filtern. + + + Versandländer + + + Nach Versandländern filtern. + + + Versandstatus + + + Nach Versandstatus filtern. + + + Shop + + + Nach Shop filtern. + + + Ohne Warengruppenzuordnung + + + Nach fehlender Warengruppenzuordnung filtern. + + + Ohne Herstellerzuordnung + + + Nach fehlender Herstellerzuordnung filtern. + + + Ordnerpfad + + + Legt den relativen Pfad des Ordners fest, in den die Daten exportiert werden. + + + Bitte einen gültigen, relativen Ordnerpfad für die zu exportierenden Daten eingeben. + + + Systemprofil + + + Gibt an, ob es sich bei dem Exportprofil um eine Systemprofil handelt. Systemprofile können nicht entfernt werden. + + + Das System-Exportprofil {0} wurde nicht gefunden. + + + Name des Profils + + + Legt den Namen des Exportprofils fest. + + + Es wurden keine Export-Provider gefunden. + + + Möglichkeiten der Filterung stehen nicht zur Verfügung. + + + Der Export-Provider unterstützt keinen expliziten Dateityp. Für eine weitere Bereitstellung der Exportdaten ist daher der Export-Provider verantwortlich. + + + Eine Vorschau steht für diesen Entitätstyp nicht zur Verfügung. + + + Es wurden keine Exportprofile gefunden. + + + Es wurde kein Exportprofil vom Typ <b>{0}</b> gefunden. Jetzt ein <a href="{1}">neues Exportprofil anlegen</a>. + + + Möglichkeiten der Projektion stehen nicht zur Verfügung. + + + Diese Option wird in der Vorschau nicht berücksichtigt. + + + Mit den folgenden Einstellungen lassen sich die zu exportierenden Daten aufteilen. Dazu zählt<ul><li>Das Überspringen der ersten n Datensätze</li><li>Die maximale Anzahl zu exportierender Datensätze</li><li>Die Anzahl der Datensätze pro Exportdatei</li><li>Daten von jedem Shop in eine separate Datei exportieren</li></ul>Standardmäßig werden alle Daten eines Shops in eine Datei exportiert. + + + Einstellungen zur Aufteilung müssen größer oder gleich 0 sein. + + + Per Shop + + + Legt fest, ob für jeden Shop ein separater Verarbeitungsdurchlauf erfolgen soll. Für jeden Shop wird eine neue Datei erzeugt. + + + Exportprofil + + + Das Exportprofil für diesen Export-Provider. + + + {0} von {1} Datensätzen exportiert + + + Anzuhängender Text + + + Legt den an die Artikelbeschreibung anzuhängenden Text fest. Bei mehreren Texten wird einer per Zufall ausgewählt. + + + Attributkombinationen exportieren + + + Legt fest, ob für jede aktive Attributkombination ein eigenständiges Produkt exportiert werden soll. + + + Attributwerte + + + Legt fest, ob und wie die Werte der Attribute weiter verarbeitet werden sollen. + + + Hersteller\Marke + + + Legt den zu exportierenden Hersteller bzw. die Marke fest, wenn für ein Produkt kein Hersteller zugeordnet ist. + + + Netto- in Bruttopreise umrechnen + + + Legt fest, dass Netto- in Bruttopreise umgerechnet werden sollen. + + + Kritische Zeichen + + + Liste mit Zeichen, die aus der Detailbeschreibung entfernt werden sollen. + + + Währung + + + Legt die auf den Export anzuwendende Währung fest. + + + Kunden-ID + + + Legt die ID des Kunden fest, auf den sich der Export beziehen soll. Wird z.B. bei Preisberechnungen berücksichtigt. + + + Artikelbeschreibung + + + Legt fest, welche Informationen zur Beschreibung des Artikel wie verwendet werden sollen. + + + HTML aus der Beschreibung entfernen + + + Legt fest, ob für den Export alle HTML-Auszeichnungen aus der Artikelbeschreibung entfernt werden sollen. + + + Kostenloser Versand ab + + + Legt den Betrag fest, ab dem keine Versandkosten anfallen. + + + Sprache + + + Legt die auf den Export anzuwendende Sprache fest. + + + Keine Gruppenprodukte exportieren + + + Legt fest, ob Gruppenprodukte exportiert werden sollen. Ist diese Option aktiviert, so werden die zur Gruppe gehörenden Produkte exportiert. + + + Die folgenden Angaben werden beim Export berücksichtigt und an entsprechenden Stellen in den Vorgang eingebunden. + + + Anzahl der Bilder + + + Legt die Anzahl der zu exportierenden Bilder pro Objekt fest. + + + Auftragsstatus ändern in + + + Legt fest, ob und wie der Status der exportierten Aufträge geändert werden soll. + + + Produktbildgröße + + + Legt die Größe (in Pixel) des Produktbildes fest. + + + Produktpreis + + + Legt den zu exportierenden Produktpreis fest. + + + Kritische Zeichen entfernen + + + Legt fest, ob kritische Zeichen (wie z.B. ½) aus der Detailsbeschreibung entfernt werden sollen. + + + Versandkosten + + + Die zu exportierenden Versandkosten. + + + Lieferzeit + + + Legt die Lieferzeit für Produkte fest, wo diese nicht angegeben ist. + + + Shop + + + Legt den auf den Export anzuwendenden Shop fest. + + + Provider + + + Legt den Export-Provider fest. Er ist für die individuelle Formatierung der zu exportierenden Daten zuständig. + + + Systemname des Profils + + + Der Systemname des Exportprofils. + + + Die folgende Liste enthält Systemprofile, die von Plugins wie bspw. dem <a href='http://community.smartstore.com/index.php?/files/file/85-smartstorenet-common-export-providers/' target='_blank'>Datenexporte Plugin</a> bereitgestellt werden. Sie können Systemprofile nach Belieben anpassen, aber keine Neuen erstellen. Für diese Profile stehen außerdem zusätzliche Aktions-Buttons zur Verfügung. Sie finden diese über den entsprechenden Listen, wie z.B. der Produkt- oder Auftragsliste. + + + Systemprofile + + + Benutzerprofile + + + Importdatei hinzufügen... + + + Zuordnung der Importfelder + + + Dies ist eine automatische Benachrichtung von Shop "{0}" über einen erfolgten Datenimport. Zusammenfassung: + + + Import von "{0}" ist abgeschlossen + + + Mein Produktimport;Mein Warengruppenimport;Mein Kundenimport;Mein Newsletter-Abonnement-Import + + + Importdatei hochladen... + + + Schlüsselfelder + + + Anhand von Schlüsselfeldern können vorhandene Datensätze zwecks Aktualisierung identifiziert werden. Die Schlüsselfelder werden in der hier festgelegten Reihenfolge verarbeitet. + + + Benutzen Sie das ID-Feld bitte nur dann als Schlüsselfeld, wenn die Daten aus der derselben Datenbank stammen, in der sie importiert werden sollen. Ansonsten werden u.U. die falschen Datensätze aktualisiert. + + + Letztes Importergebnis + + + Bitte laden Sie eine Importdatei hoch. + + + Bei mehreren Importdateien ist darauf zu achten, dass diese vom selben Dateityp sind und deren Inhalt demselben Schema folgt (z.B. gleiche Spaltenüberschriften). + + + Name des Profils + + + Legt den Namen des Importprofils fest. + + + Es wurden keine Importprofile gefunden. + + + Anzahl der Bilder + + + Legt die Anzahl der zu importierenden Bilder pro Objekt fest. + + + Wählen Sie bitte das zu importierende Objekt und laden Sie eine Importdatei hoch. + + + {0} von {1} Datensätzen verarbeitet + + + Hier neue Zuordnung vornehmen + + + Nur aktualisieren + + + Ist diese Option aktiviert, werden nur vorhandene Daten aktualisiert, aber keine neue Datensätze hinzugefügt. + + + Es ist mindestens ein Schlüsselfeld erforderlich. + + + Herunterladen der hochgeladenen Datei + + + Download URL + + + Das Download-Objekt wurde gespeichert + + + Download entfernen + + + Download speichern + + + Upload Datei + + + Benutze Download-URL + + + Geschenkgutscheine + + + Der neue Geschenkgutschein wurde hinzugefügt + + + Geschenkgutschein hinzufügen + + + Zurück zur Gesamtliste + + + Der Geschenkgutschein wurde gelöscht + + + Geschenkgutscheindetails + + + Gesamtbetrag + + + Wert des Geschenkgutscheines + + + Erstellt am + + + Gibt an, an welchem Datum der Geschenkgutschein erstellt wurde. + + + Gutscheinnummer + + + Gutscheinnummer generieren + + + Gutscheinnummer. Kann während des Checkouts eingelöst werden. + + + Typ des Geschenkgutscheines + + + Gibt den Typ des Geschenkgutscheines an. + + + Aktiviert + + + Gibt an, ob der Geschenkgutschein aktiviert und einlösbar ist. + + + Empfänger ist benachrichtigt + + + Empfänger ist benachrichtigt + + + Empfänger benachrichtigen + + + Nachricht + + + Nachricht eingeben. + + + Auftrag + + Der Geschenkgutschein wurde mit diesem Auftrag gekauft. @@ -8512,6 +9658,12 @@ "Auftrag eingegangen" E-Mail (an Shopbetreiber) wurde gequeued. Queued Email ID: {0} + + Newsletter wurde abonniert + + + Newsletter-Abonnent wurde entfernt + Auftrag wurde storniert @@ -8519,7 +9671,7 @@ Zahlung wurde erfasst - Fehler bei Zahlungserfassung für Auftrag #{0}. Fehler: {1} + Es ist ein Fehler bei der Zahlungsbuchung zu Auftrag {0} aufgetreten. Auftrag wurde gelöscht @@ -8543,7 +9695,7 @@ Auftrag wurde teilweise erstattet. Betrag: {0} - Fehler bei Teilerstattung: {0} + Es ist ein Fehler bei einer Teilerstattung zu Auftrag {0} aufgetreten. Auftrag ist eingegangen @@ -8552,7 +9704,7 @@ Auftrag wurde erstattet. Betrag: {0} - Fehler bei Erstattung: {0} + Es ist ein Fehler bei einer Rückerstattung zu Auftrag {0} aufgetreten. Auftragsstatus geändert. Neuer Status: {0} @@ -8561,10 +9713,10 @@ Zahlungstransaktion wurde storniert - Fehler bei Stornierung der Zahlungstransktion: {0} + Es ist ein Fehler bei der Stornierung einer Zahlungstransaktion zu Auftrag {0} aufgetreten. - Konnte wiederkehrende Zahlung nicht stornieren. Fehler: {0} + Es ist ein Fehler bei der Stornierung einer wiederkehrenden Zahlung für Auftrag {0} aufgetreten. Wiederkehrende Zahlung wurde storniert @@ -8626,6 +9778,12 @@ Auftragsdetails bearbeiten + + Akzeptiert Weitergabe der E-Mail + + + Gibt an, ob der Kunde bei der Bestellung einer Weitergabe seiner E-Mail Adresse an Dritte zugestimmt hat oder nicht. + Partner @@ -8633,10 +9791,10 @@ Das Partner-Unternehmen, dem dieser Auftrag zugeordnet ist. - Authorizations-Transaktions-ID + Transaktions-ID für Autorisierung - Vom Zahlungsdienstleister erhaltene Authorizations-Transaktions-ID. + Vom Zahlungsanbieter erhaltene Transaktions-ID für die Autorisierung. Transaktionsergebnis für Autorisierung @@ -8663,10 +9821,10 @@ Zieht eine zuvor reservierte Zahlung über den Zahlungsanbieter ein. - Transaction ID buchen + Transaktions-ID für Buchung - Über vom Zahlungsdienstleister erhaltenene transaction id buchen. + Vom Zahlungsanbieter erhaltene Transaktions-ID für die Buchung. Transaktionsergebnis für Buchung @@ -9838,8 +10996,8 @@ E-Mail-Adresse erforderlich - - Es wurden {0} E-Mail(s) importiert und {1} aktualisiert. + + Abonnement GUID Bereitstellendes Plugin @@ -10366,6 +11524,9 @@ Fügen Sie die SQL-Abfrage hier ein: + + Die SQL Anweisung wurde erfolgreich ausgeführt. + Ausführen @@ -10378,6 +11539,9 @@ Zurück zur Übersicht + + E-Mail Anhang konnte nicht herunterladen: Daten nicht verfügbar. + Die E-Mail wurde gelöscht @@ -10387,6 +11551,18 @@ E-Mail bearbeiten + + Während der Erstellung des E-Mail-Anhangs ist ein Fehler aufgetreten + + + Daten für den E-Mail Anhang konnten nicht heruntergeladen werden. Pfad: {0} + + + Der Inhaltstyp des E-Mail Anhangs muss 'application/pdf' sein + + + Anhänge + BCC @@ -10492,6 +11668,9 @@ Der Name des Empfängers. + + Anzahl Anhänge + Enddatum @@ -10549,77 +11728,164 @@ Die E-Mail wurde erfolgreich bearbeitet + + Geplante Aufgabe + - Aufgabenplanung + Geplante Aufgaben + + + Abbruch erzwungen durch Herunterfahren der Anwendung + + + Die geplante Aufgabe "{0}" wurde abgebrochen + + + Abbruchanforderung wurde übermittelt. + + + Cron Ausdruck + + + Ein Ausdruck, der den Zeitplan für die automatische Ausführung der Aufgabe festlegt. + + + Hilfe zu <a href='{0}' target='_blank'>Cron-Ausdrücken</a> + + + Dauer der letzten Ausführung ([Std.]:[Min.]:[Sek.]) + + + Aufgabe bearbeiten Aktiviert + + Aktiviert die geplante Ausführung der Aufgabe gemäß Cron Ausdruck + + + Zukünftige Zeitpläne + + + Der Cron-Ausdruck ist ungültig + Zuletzt beendet Letzte Ausführung + + Startdatum der letzten Ausführung + Letzte erfolgreiche Ausführung + + Startdatum der letzten erfolgreichen Ausführung + Name Ein Name für die Aufgabe ist erforderlich. - - Nach dem eine Aufgabe verändert wurde, ist ein Neustart erforderlich. + + Nächste Ausführung in + + + Datum der nächsten geplanten Ausführung + + + Fehler beim Ausführen der Aufgabe "{0}" Jetzt ausführen - - Ausführung der Aufgabe abgeschlossen - Wird ausgeführt... Aufgabe wird jetzt im Hintergrund ausgeführt - - Sekunden (Intervall) + + Die Aufgabe wird jetzt im Hintergrund ausgeführt. Sie erhalten eine E-Mail, sobald sie abgeschlossen ist. Den Fortschritt können Sie in der Exportprofilliste verfolgen. + + + Die Aufgabe wird jetzt im Hintergrund ausgeführt. Sie erhalten eine E-Mail, sobald sie abgeschlossen ist. Den Fortschritt können Sie in der Importprofilliste verfolgen. - - Sekunden müssen größer als 0 sein. + + Aufgabe wurde erfolgreich ausgeführt + + + Ausführung planen - Bei Fehler anhalten + Bei Fehler deaktivieren + + + Aktivieren Sie das Kästchen, wenn die Aufgabe bei Auftreten eines Fehlers während der Ausführung deaktiviert werden soll + + + Die Aufgabe kann nicht bearbeitet werden, während sie ausgeführt wird. + + + Die Aufgabe wurde erfolgreich bearbeitet SEO Namen - - Lösche die ausgewählten Elemente + + Pro Sprache darf nur ein aktiver SEO Name festgelegt werden. - ID der Entität + Objekt-ID + + + Legt die ID des zugehörigen Objektes fest. - Name der Entität + Objekt + + + Legt den Namen der zugehörigen Objektes fest. Ist aktiv + + Legt fest, ob der SEO Name aktiv oder inaktiv ist. + Sprache + + Legt die Sprache des SEO Namens fest. + Standard - Name + SEO Name - Der zu findende Name. + Legt den SEO Namen fest. + + + Namen pro Objekt + + + Die Anzahl der SEO Namen pro Objekt. + + + Geplante Aufgabe + + + PDF-Konvertierer + + + Suchmaschine Systeminformation @@ -10654,6 +11920,12 @@ Der Name des Daten-Providers. + + Aufräumen + + + Der Arbeitsspeicher wurde erfolgreich aufgeräumt. + HTTP_HOST @@ -10690,6 +11962,9 @@ Zeitzone des Servers + + Benutzter Speicher (RAM) + GMT/UTC @@ -10699,6 +11974,12 @@ Warnungen + + Zugriffsverweigerung durch anonyme Anfrage bei {0}. + + + Zugriffsverweigerung durch Kunde #{0} '{1}' bei {2}. + Die Standard-Maßeinheit wurde nicht festgelegt. @@ -10717,6 +11998,9 @@ Die Standard-Gewichtseinheit wurde festgelegt. + + Bitte nur Ziffern eingeben. + Alle Verzeichnisberechtigungen sind OK. @@ -10741,6 +12025,15 @@ '{0}' Plugin ist nicht kompatibel mit Ihrer SmartStore.NET-Version. Löschen Sie es oder installieren Sie die richtige Version. + + Es sind keine Kundengruppen festgelegt. + + + Es sind keine Zugriffsrechte festgelegt. + + + Keine Versand-Artikel + Es existieren keine aktiven Zahlungsarten. @@ -10756,6 +12049,15 @@ Es wird empfohlen, nur eine Versandkosten-Offline-Berechnungsmethode zu benutzen. + + Die Erreichbarkeit der Sitemap konnte nicht überprüft werden. + + + Die Sitemap für den Shop ist erreichbar. + + + Die Sitemap für den Shop ist nicht erreichbar. + Die angegebene URL stimmt mit der URL des Shops überein. @@ -11041,6 +12343,21 @@ Tabellen/Container + + Bitte geben Sie eine gültige E-Mail Adresse ein + + + Bitte geben Sie einen Namen ein + + + Bitte geben Sie eine gültige URL ein + + + Bitte geben Sie Benutzername und Passwort ein + + + Der Wert muss größer 0 sein. + API ist nicht erreichbar. @@ -11203,6 +12520,9 @@ Zur Kasse + + Ein anonymer Checkout ist nicht zulässig. + Rechnungsanschrift @@ -11227,12 +12547,21 @@ Weiter einkaufen + + Bitte stimmen Sie der Nutzungsvereinbarung für herunterladbare Produkte zu. + Rechnungsanschrift eingeben Versandanschrift eingeben + + Ja, ich möchte sofort Zugang zu dem digitalen Inhalt und weiß, dass mein Widerrufsrecht mit dem Zugang erlischt. + + + Bitte bestätigen Sie, dass Sie sofort Zugang zu dem digitalen Inhalt wünschen. + Bitte warten Sie einige Sekunden, bevor Sie einen neuen Auftrag platzieren. @@ -11326,6 +12655,9 @@ Übermittele Auftragsinformationen + + Newsletter abonnieren + Geschäftsbedingungen (AGB) @@ -11410,6 +12742,9 @@ Abgebrochen + + Der API-Aufruf zur Prüfung eines CAPTCHAs ist fehlgeschlagen. + Produkt bearbeiten @@ -11431,6 +12766,15 @@ Fortsetzen + + In die Zwischenablage kopieren + + + Kopieren ist fehlgeschlagen. + + + Kopiert! + Anzahl @@ -11443,6 +12787,9 @@ Querverweise + + Die von Ihnen gewählte Kundennummer existiert bereits. Bitte geben Sie eine andere Kundennummer an. + Datum @@ -11461,36 +12808,84 @@ Sind Sie sicher "{0}" zu löschen? + + Bereitstellung + Beschreibung Beschreibung + + Detailbeschreibung + Reihenfolge Download + + Für die Produktvariante ist der Download einer Beispieldatei nicht verfügbar. + + + Sie haben die maximale Anzahl an Downloads {0} erreicht. + + + Es sind keine Daten zum Herunterladen mehr verfügbar. + + + Downloads sind nicht gestattet. + + + Der Download ist nicht mehr verfügbar. + + + Der Download einer Beispieldatei ist nicht mehr verfügbar. + Dauer Bearbeiten + + Aktiviert + Wert + + Klicken Sie auf ein Element, um es aus- bzw. abzuwählen und OK, um die Auswahl zu übernehmen. + + + Es wurden keine weiteren Elemente gefunden. + + + Klicken Sie auf ein Element, um es auszuwählen und OK, um es zu übernehmen. + Fehler + + Die E-Mail-Adresse ist ungültig. + + + Es wurde keine aktive Sprache gefunden. + + + Es wurde kein E-Mail-Konto gefunden. + Die gewählte Zahlungsart verursachte leider einen Fehler. Bitte korrigieren Sie Ihre Eingaben, versuchen Sie es erneut oder wählen Sie eine andere Zahlungsart. Fehler beim Versenden der Email. Bitte versuchen Sie es später erneut. + + Beispiel + Ausführung @@ -11518,12 +12913,21 @@ Dateien für den Upload hier ablegen + + URL eingeben + Fehlgeschlagen Datei hochladen + + Filter + + + Versandkostenfrei + Anzeigename @@ -11539,6 +12943,9 @@ Startseite + + Bild + Importieren @@ -11548,12 +12955,21 @@ Aktiv + + Sprache + Liste + + Lade + Lade nächsten Schritt… + + Diese Funktion steht für Gäste nicht zur Verfügung. + Verschiedenes @@ -11563,6 +12979,9 @@ Mehr Info + + Mein + Navigation @@ -11578,6 +12997,12 @@ Nein, abbrechen + + Es sind keine Dateien vorhanden. + + + Es wurde keine Datei hochgeladen. + Der Vorgang wurde aus Sicherheitsgründen nicht ausgeführt. @@ -11587,6 +13012,9 @@ Benachrichtigung + + Nicht auswählbar + Aus @@ -11602,6 +13030,12 @@ Optional + + Optionen + + + Aufteilung + Capture wird nicht unterstützt @@ -11626,6 +13060,24 @@ Artikel + + Profil + + + Projektion + + + Provider + + + öffentlich + + + Veröffentlicht + + + Veröffentlichung + Frage @@ -11638,9 +13090,21 @@ Entfernen + + Ersetzen + + + Die Anfrage konnte nicht ausgeführt werden.<br />Controller: {0}, Action: {1}, Grund: {2}. + + + Regel + Speichern + + Geplant + Suchen @@ -11674,6 +13138,12 @@ Anzeigen + + Alle anzeigen + + + Mehr anzeigen + Verkleinern @@ -11686,11 +13156,23 @@ Systemname + + Nicht verfügbar + undefiniert + + Unbekannt + + + Unveröffentlicht + + + Ungeplant + - nicht spezifiziert + Nicht spezifiziert Geändert am @@ -11707,11 +13189,17 @@ Bitte warten... + + Wartend + Warnung + + Web-Seite + - Die von Ihnen eingegebenen Zeichen stimmen nicht mit dem Bild überein. Bitte versuchen Sie es erneut. + Bitte bestätigen Sie, dass Sie kein "Roboter" sind. Falsche E-Mail @@ -11761,6 +13249,15 @@ Namen eingeben + + Einwilligungserklärung Datenschutz + + + Ja, ich habe die <a href="{0}">Datenschutzerklärung</a> zur Kenntnis genommen und bin damit einverstanden, dass die von mir angegebenen Daten elektronisch erhoben und gespeichert werden. Meine Daten werden dabei nur zur Bearbeitung meiner Anfrage genutzt. + + + Bitte stimmen Sie der Speicherung Ihrer Daten zu. + Ihre Anfrage wurde erfolgreich übermittelt. @@ -11770,6 +13267,9 @@ Währungen + + Der Kunde existiert nicht. + Gast @@ -11791,6 +13291,9 @@ Produkt + + Das Produkt besitzt keine Nutzungsvereinbarung. + Ich bin einverstanden. @@ -11800,6 +13303,9 @@ Nutzungsvereinbarung + + Ja, ich stimme der <a href='javascript:void(0)' data-id='{0}' class='download-user-agreement'>Nutzungsvereinbarung</a> für dieses Produkt zu. + Kontrollkästchen @@ -11866,6 +13372,18 @@ Lagerbestand mit Attributen führen + + Keine Preisanzeige + + + Minimal realisierbarer Preis + + + Auf der Detailseite vorgewählter Preis + + + Preis ohne Rabatte und Attribute + Erstellt am @@ -11956,6 +13474,27 @@ Benutzernamen anzeigen + + Automatisch vergeben + + + Deaktiviert + + + Aktiviert + + + Anzeigen + + + Immer editierbar + + + Editierbar falls leer + + + Nicht anzeigen + Lesbar @@ -11965,6 +13504,102 @@ Gehashed + + Alle Werte an den Produktnamen anhängen + + + Nicht spezifiziert + + + E-Mail + + + Dateisystem + + + FTP + + + HTTP POST + + + Öffentlicher Ordner + + + Detailbeschreibung + + + Hersteller + Produktname + Detailbeschreibung + + + Hersteller + Produktname + Kurzbeschreibung + + + Produktname + Detailbeschreibung + + + Produktname + Kurzbeschreibung + + + Keine + + + Kurzbeschreibung + + + Kurzbeschreibung oder Name falls leer + + + Warengruppe + + + Kunde + + + Hersteller + + + Newsletter Abonnenten + + + Auftrag + + + Produkt + + + Multipart-Form-Data POST + + + Einfacher POST + + + Komplett + + + Keine + + + Wird bearbeitet + + + Warengruppe + + + Kunde + + + Newsletter Abonnent + + + Produkt + + + Trennzeichen getrennte Werte (.csv, .txt, .tab) + + + Excel (.xlsx) + Nur N mal @@ -12049,6 +13684,24 @@ Warnung + + Aktiviert anzeigen + + + Deaktiviert anzeigen + + + Nicht anzeigen + + + Aktiviert anzeigen + + + Deaktiviert anzeigen + + + Nicht anzeigen + Abgebrochen @@ -12338,9 +13991,6 @@ Forenthemen mit den neuesten Beiträgen - - {0} - Forum: {1} - Forumname @@ -12581,6 +14231,9 @@ Informationen + + Die Installationssprache '{0}' ist nicht registriert. + Bitte geben Sie eine gültige Kreditkartennummer ein. @@ -12830,12 +14483,48 @@ Rechnungsanschrift + + Die Rechnungsanschrift fehlt. + + + Die Auftragssumme konnte nicht berechnet werden. + + + Die Versandkosten konnten nicht berechnet werden. + + + Der Auftrag kann nicht storniert werden. + + + Der Auftrag kann nicht gebucht werden. + + + Der Auftrag kann nicht als abgeschlossen markiert werden. + + + Der Auftrag kann nicht als bezahlt markiert werden. + + + Eine Teilrückerstattung ist für diesen Auftrag nicht möglich. + + + Eine Rückerstattung ist für diesen Auftrag nicht möglich. + + + Eine Stornierung dieses Auftrages ist nicht möglich. + Zahlung veranlassen Der Auftrag wurde noch nicht bezahlt. Um die Zahlung nun vorzunehmen, klicken Sie den Button *Zahlung veranlassen* + + Eine Rechnungslegung ist für das Land '{0}' unzulässig. + + + Ein Versand ist für das Land '{0}' unzulässig. + E-Mail @@ -12848,6 +14537,12 @@ Geschenkgutschein ({0}) + + Für die wiederkehrende Zahlung existiert kein Ausgangsauftrag. + + + Keine wiederkehrenden Produkte. + Notizen @@ -12857,6 +14552,9 @@ Notiz + + Der Auftrag {0} wurde nicht gefunden. + Auftragsnummer @@ -12878,6 +14576,9 @@ Zahlungsgebühr + + bestellung-{0}.pdf + Telefon @@ -12989,6 +14690,9 @@ Versandanschrift + + Die Lieferanschrift fehlt. + Versandart @@ -13139,6 +14843,9 @@ Wunschliste per E-Mail an einen Freund senden + + Das Datum der nächsten Zahlung kann nicht ermittelt werden. + Kartencode @@ -13163,6 +14870,9 @@ Falsche Kartennummer + + Die Zahlungsart konnte nicht geladen werden. + Gültig bis @@ -13172,6 +14882,24 @@ Ablaufjahr ist erforderlich + + Die Zahlungsart steht nicht zur Verfügung. + + + Mindestens ein Zahlungsart-Provider muss aktiviert sein. + + + Leider können wir diesen Einkauf nicht über die gewünschte Zahlungsart abwickeln. Bitte wählen Sie eine alternative Zahlungsoption aus, um Ihre Bestellung abzuschließen. + + + Wiederkehrende Zahlung ist inaktiv. + + + Wiederkehrende Zahlungen sind für die gewählte Zahlungsart nicht möglich. + + + Der Typ von wiederkehrender Zahlung wird nicht unterstützt. + Wähle Kreditkarte aus @@ -13289,12 +15017,6 @@ Gewicht - - Gruppierte Produkte - - - Produktset besteht aus - E-Mail @@ -13310,32 +15032,14 @@ Kontaktdaten - - Höhe - - - Länge - - - Hersteller - - - Preis - - - Artikelnummer - - - Spezifikation - Lagermenge - - Gewicht + + Eine Umfrageantwort {0} wurde nicht gefunden. - - Breite + + Die Umfrage ist nicht verfügbar. Nur registrierte Benutzer können abstimmen @@ -13358,6 +15062,9 @@ Datenschutzerklärung + + Private Nachrichten sind deaktiviert. + Posteingang @@ -13488,7 +15195,10 @@ Nicht am Lager - Grundpreis: {0} pro {1} + Inhalt: {0} {1} ({2} / {3} {1}) + + + {0} {1} ({2} / {3} {1}) Home @@ -13689,7 +15399,10 @@ Dieses Produkt ist ausverkauft. - Dieses Produkt ist ausverkauft. + Keine Bundle-Bestandteile vorhanden + + + Das Produkt {0} wurde nicht gefunden. Preis: @@ -13769,6 +15482,9 @@ Menge + + Die Produktvariante {0} wurde nicht gefunden. + Gewicht @@ -13814,6 +15530,9 @@ Der EZB-Wechselkursdienst kann nur genutzt werden, wenn der Wechselkurs-Währungscode auf EUR gesetzt ist. + + Der EZB-Wechselkursdienst kann nur genutzt werden, wenn der Wechselkurs-Währungscode auf EUR gesetzt ist. + EZB-Wechselkursdienst @@ -13904,6 +15623,9 @@ Sie können nicht Ihre eigene Bewertung beurteilen. + + Die Produktbewertung {0} wurde nicht gefunden. + Nur registrierte Benutzer können eine Bewertung verfassen. @@ -14018,6 +15740,18 @@ Die Mindestlänge für den Suchbegriff beträgt {0} Buchstaben. + + Diese Sendung wird bereits zugestellt. + + + Diese Sendung wird bereits ausgeliefert. + + + Die Berechnungsmethode für Versandkosten konnte nicht geladen werden. + + + Mindestens ein Provider zur Berechnung von Versandkosten muss aktiviert sein. + Versandinfos @@ -14150,6 +15884,9 @@ Die eingegebene Nummer des Geschenkgutscheines konnte nicht angewendet werden. + + Der Warenkorb ist deaktiviert. + Gesamt @@ -14262,7 +15999,7 @@ Geben Sie einen gültigen Absender-Namen ein. - Die maximale Anzahl von Produkten in Ihrer Wunschliste wurde erreicht + Eine vollständige Liste aller Versandkosten finden Sie <a href="{0}">hier</a>. Artikelnummer @@ -14343,10 +16080,10 @@ Shop Name hinter Seiten Name - Dieser Shop ist zur Zeit geschlossen. + Wir sind bald wieder da. - Bitte Besuchen Sie uns später. + Wir aktualisieren gerade das Angebot in unserem Online-Shop. Die Seite ist demnächst wieder verfügbar. Theme für den Shop auswählen. @@ -14366,9 +16103,15 @@ * Alle Preise {0}, zzgl. <a href="{1}">Versandkosten</a> + + * Alle Preise {0}, zzgl. Versandkosten + {0} {1} {2} zzgl. <a href="{3}">Versandkosten</a> + + {0} {1} {2} zzgl. Versandkosten + {0} {1} {2} zzgl. <a href="{3}">Versandkosten</a> @@ -14735,15 +16478,57 @@ Well-Hintergrundfarbe + + Tag + + + Tg. + + + Tage + + + Tg. + Vor {0} Tagen + + Stunde + + + Std. + + + Stunden + + + Std. + Vor {0} Stunden + + Minute + + + Min. + + + Minuten + + + Min. + Vor {0} Minuten + + Monat + + + Monate + Vor {0} Monaten @@ -14765,9 +16550,33 @@ Vor einem Jahr + + Sekunde + + + Sek. + + + Sekunden + + + Sek. + Vor {0} Sekunden + + Woche + + + Wochen + + + Jahr + + + Jahre + Vor {0} Jahren @@ -14828,6 +16637,9 @@ Bitte geben Sie Ihre E-Mail-Adresse ein. + + Die Wunschliste ist deaktiviert. + Wunschliste ansehen diff --git a/src/Presentation/SmartStore.Web/App_Data/Localization/App/de/head.txt b/src/Presentation/SmartStore.Web/App_Data/Localization/App/de/head.txt index afe64e4f3b..f822a9cb76 100644 --- a/src/Presentation/SmartStore.Web/App_Data/Localization/App/de/head.txt +++ b/src/Presentation/SmartStore.Web/App_Data/Localization/App/de/head.txt @@ -1 +1 @@ -201506181858349_AclRecordCustomerRole \ No newline at end of file +201605201911421_ExportRevision \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/App_Data/Localization/App/en/all.smres.xml b/src/Presentation/SmartStore.Web/App_Data/Localization/App/en/all.smres.xml index 1faea7a6fb..734f05e1de 100644 --- a/src/Presentation/SmartStore.Web/App_Data/Localization/App/en/all.smres.xml +++ b/src/Presentation/SmartStore.Web/App_Data/Localization/App/en/all.smres.xml @@ -135,6 +135,9 @@ No orders + + This is not your order. + Order Date @@ -489,6 +492,15 @@ Register + + The customer is already registered. + + + A search engine can't be registered. + + + A background task account can't be registered. + The specified email already exists @@ -597,6 +609,9 @@ Deleted a manufacturer ('{0}') + + Deleted order {0} + Deleted a product ('{0}') @@ -1401,6 +1416,9 @@ The category has been deleted successfully. + + Show other description + Discounts @@ -1450,10 +1468,10 @@ Deleted - Description + Top description - The description of the category. + Description of the category that is displayed above products on the category page. Display order @@ -1512,6 +1530,9 @@ Select a parent category for this category. Leave this field empty to make this the root level category + + Parent category + Picture @@ -1990,6 +2011,9 @@ Products with attribute values of type "product" cannot be part of a bundle. + + Notes on product bundles + You need to save the product before you can add bundled products for this product page. @@ -2039,7 +2063,7 @@ Cross-sells - Add new cross-sell product + Add checkout-selling product Product @@ -2048,7 +2072,7 @@ Product - You need to save the product before you can add cross-sell products for this product page. + You need to save the product before you can add checkout-selling products for this product page. The product has been deleted successfully. @@ -2101,6 +2125,12 @@ Enter a comma separated list of quantities you want this product to be restricted to. Instead of a quantity textbox that allows them to enter any quantity, they will receive a dropdown list of the values you enter here. + + Approved rating sum + + + Approved total reviews + Associated to product @@ -2165,7 +2195,7 @@ {0} per unit (base price: {1} per {2}) - "Measure unit" is required to calculate the base price. + Base price measure unit "Measure unit" is required to calculate the base price. @@ -2272,12 +2302,18 @@ Enter global trade item number (GTIN). These identifiers include UPC (in North America), EAN (in Europe), JAN (in Japan), and ISBN (for books). + + Has discounts applied + Has sample download file Check the box if this product has a sample download file that can be downloaded before checkout. + + Has tier prices + Has user agreement @@ -2290,6 +2326,12 @@ The height of the product. + + Homepage display order + + + Specifies the display order for products displayed on homepage. 1 represents the first element in the list. + ID @@ -2344,6 +2386,9 @@ The length of the product. + + Lowest attribute combination price + Low stock activity @@ -2413,6 +2458,12 @@ Please provide a name. + + Not approved rating sum + + + Not approved total reviews + Notify admin for quantity below @@ -2638,6 +2689,18 @@ Search by a specific category. + + Showed on home page + + + Filters for products displayed or not displayed on homepage. + + + Published + + + Filters for published or unpublished products. + Manufacturer @@ -2909,7 +2972,7 @@ Related products - Add new related product + Add cross-selling product Display order @@ -2921,7 +2984,7 @@ Product - You need to save the product before you can add related products for this product page. + You need to save the product before you can add cross-selling products for this product page. Specification attributes @@ -3130,12 +3193,15 @@ There were {0} mutual association(s) created. - - CSV file + + CSV Configuration The data was successfully changed. + + Data exchange + The data were saved successfully. @@ -3148,38 +3214,59 @@ Delete (selected) + + Delete all + Are you sure you want to delete this item? Are you sure you want to delete "{0}"? + + Deleted + Delete selected Edit + + The email has been successfully sent. + + + Please enter an email address. + + + Object + ID The unique numeric id of the entity + + Errors + Error sending an email - - Excel file + + PDF Export Please wait while the export is being executed + + Export all + No data to export. - - Export to CSV + + Export selected Export to Excel @@ -3196,20 +3283,29 @@ Export to PDF (selected) - - Table of contents - Too many items! The PDF conversion is limited to 500 items. Please reduce the amount of selected records. Export to XML - - Export to XML (all) + + The file is in use and cannot be opened. + + + File not found + + + {0} files were deleted + + + The file must be of the type {0}. + + + {0} folders were deleted - - Export to XML (selected) + + FTP status {0} ({1}). General @@ -3235,41 +3331,17 @@ Hide - - Import from CSV - - - Import from Excel - - - Active since: {0}. - - - Cancel import - - - Import process has been cancelled - - - Do you really want to cancel the import? Products imported so far will not be removed. - - - Download full report... - - - The import is being performed in the background now. You can view the progress or the result of the last completed import in the import dialog at any time. - - - <b>Last import</b>: {0}{1}. + + HTTP status {0} ({1}). - - No report available + + Ignore - - {0} of {1} rows processed. + + Import file - - {0} new, {1} updated - with {2} warning(s) und {3} error(s). + + Import files Please wait while the import is being executed @@ -3277,6 +3349,9 @@ Info + + Last run + License @@ -3289,18 +3364,51 @@ You are going to lose any unsaved changes. Are you sure? + + New records + No No, cancel + + No entries have been selected. + + + of + + + Placeholder + Please select Preview + + Cannot load the provider {0}. + + + Public files + + + {0} records were deleted. + + + Skip + + + Specifies the number of records to be skipped. + + + Limit + + + Specifies the maximum number of records to be processed. + Restore defaults @@ -3310,6 +3418,9 @@ You must restart the application for the changes to take effect. + + Restrictions + Save @@ -3319,6 +3430,9 @@ Search + + Selected + Send now @@ -3331,6 +3445,12 @@ Show + + Values for skip and limit must be greater than or equal to 0. + + + Skipped + Standard @@ -3361,18 +3481,30 @@ All stores + + Successful on + The task was successfully processed. + + Total rows + Tree view Unlicensed + + Unsupported entity type '{0}' + Update + + Updated + Please upload a file @@ -3382,6 +3514,9 @@ Please wait while processing is running... + + Warnings + Wrong email @@ -3418,9 +3553,6 @@ The Activity Log Type. - - Activity Log Type - Message @@ -3445,6 +3577,15 @@ Customer Email + + Filters results by customer email address. + + + Customer system account + + + Filters results by customer system accounts. + Activity Types @@ -3628,6 +3769,9 @@ back to country list + + The country cannot be deleted because it has associated addresses. + The country has been deleted successfully. @@ -3719,7 +3863,7 @@ Add a new state/province - The state can't be deleted. It has associated addresses. + The state\province cannot be deleted because it has associated addresses. Edit state/province @@ -3775,15 +3919,12 @@ back to currency list - - The primary exchange rate currency can't be deleted. - - - The primary store currency can't be deleted. - The currency has been deleted successfully. + + The currency cannot be deleted or deactivated because it is attached to the store "{0}" as primary or exchange rate currency. + Edit currency details @@ -3845,16 +3986,10 @@ Current exchange rate provider - Is primary exchange rate currency + Exchange rate currency - Is primary store currency - - - Mark as primary exchange rate currency - - - Mark as primary store currency + Primary currency Name @@ -3868,6 +4003,18 @@ Please provide a name. + + Is exchange rate currency for + + + A list of stores where the currency is primary exchange rate currency. + + + Is primary store currency for + + + A list of stores where the currency is primary store currency. + Published @@ -4048,6 +4195,9 @@ Email has been successfully sent. + + Testing email functionality. + The email account has been updated successfully. @@ -4094,10 +4244,10 @@ The display order of this language. 1 represents the top of the list. - Flag image file name + Flag image - The flag image file name. The image should be saved into \\images\\flags\\ directory. + Specifies the flag image. The files for the flag images must be stored in /Content/Images/flags/. Language culture @@ -4139,7 +4289,7 @@ Two letter SEO code should be 2 characters long. - Please provide a unique SEO code. + Please select a SEO language code. Import resources @@ -4318,6 +4468,21 @@ Supports void + + Full description + + + Specifies a full description of the payment method. It appears in the payment list in checkout. + + + There were no possibilities found to restrict payment methods. + + + Short description + + + Specifies a short description of the payment method. + Plugins @@ -4456,6 +4621,9 @@ The plugin has been uninstalled. + + An unknown error occurred when calling a plugin. Please refer to the following message for details. + Quantity units @@ -4486,6 +4654,9 @@ Regional Settings + + You need to save before you can specify restrictions. + Settings @@ -4528,6 +4699,12 @@ Check the box to enable the blog in your store. + + Maximum age (in days) + + + Specifies the maximum news age in days. Older blog posts are not exported in the RSS feed. + Notify about new blog comments @@ -4550,7 +4727,7 @@ Display blog RSS feed link in the browser address bar - Check the box to enable the blog RSS feed link in customers browser address bar + Check to enable the blog RSS feed link in customers browser address bar. Catalog settings @@ -4604,11 +4781,17 @@ Check the box to allow customers to use the 'Compare Products' option in your store - Amount of displayed products per page + Number of displayed products per page Determines the amount of displayed products per page. + + Default product sort order + + + Specifies the default product sort order. + Default view mode @@ -4663,6 +4846,24 @@ Check the box to hide the buy-button in product lists. + + Hide default picture for categories + + + Specifies whether to hide the default image for categories. The default image is shown when no image is assigned to a category. + + + Hide default picture for manufacturers + + + Specifies whether to hide the default image for manufacturers. The default image is shown when no image is assigned to a manufacturer. + + + Hide default picture for products + + + Specifies whether to hide the default image for products. The default image is shown when no image is assigned to a product. + Height of truncated long text @@ -4738,6 +4939,12 @@ The number of product tags that appear in the tag cloud + + Price display + + + Specifies whether or what type of price to be displayed in product lists. + Product detail @@ -4855,6 +5062,12 @@ The number of 'Recently viewed products' to display when 'Recently viewed products' option is enabled. + + Search product description + + + Specifies whether the product description should be included in the search. + Search page. Products per page @@ -4951,6 +5164,12 @@ Specifies whether manufacturers will be displayed as images or textual links on the homepage. + + Show manufacturer pictures + + + Specifies whether to show manufacturer pictures on product detail page. + Show manufacturers on homepage @@ -5005,6 +5224,12 @@ Check the box to display the weight. When enabled, customers will see the weight of the product. + + Sort filter results by number of matches + + + Specifies to sort filter results by number of matches in descending order. If this option is deactivated then the result is sorted by the display order of the values. + Show subcategories @@ -5185,6 +5410,12 @@ Set if 'Country' is enabled. + + Customers can enter a customer number + + + Specifies whether customers can enter a customer number if the customer number doesn't contain a value yet. + Customer form fields @@ -5209,6 +5440,18 @@ Specifies whether customer numbers can be saved. + + Customer numbers + + + Specifies whether to assign customer numbers and whether they should be created automatically. + + + Customer number presentation + + + Specifies the presentation and handling of the customer number to the customer. + Customer settings @@ -5239,6 +5482,18 @@ The default store time zone used to display dates. + + Display customer numbers in frontend + + + Specifies whether customer numbers will be displayed to customers in their account area. + + + Get privacy consent for contact requests + + + Specifies whether a checkbox will be displayed on the contact page which requests the user to agree on storage of his data. + Auto register enabled @@ -5308,6 +5563,12 @@ Check the box if 'Phone number' is required. + + Customer role at registrations + + + Specifies a customer role that will be assigned to newly registered customers. + Show customers' join date @@ -5386,6 +5647,24 @@ Check the box if 'Zip / postal code' is required. + + Timeout for image download (minutes) + + + Specifies the timeout for the image download in minutes. + + + Image folder (relative path) + + + Specifies a relative path to a folder with images to be imported (e.g. Content\Images). + + + Maximum length of file and folder names + + + Specifies the maximum length of file and folder names created during an import or export. + Forum settings @@ -5530,6 +5809,24 @@ IP addresses allowed to access the Back End. Leave this field empty if you do not want to restrict access to the Back End. Use comma to separate them (e.g. 127.0.0.10,232.18.204.16) + + Allow unicode characters + + + Check whether SEO names can contain letters that are classified as unicode characters. + + + Attach order PDF to 'Order Completed' email + + + Dynamically creates and attaches the order PDF to the 'Order Completed' customer notification email. + + + Attach order PDF to 'Order Placed' email + + + Dynamically creates and attaches the order PDF to the 'Order Placed' customer notification email. + Bank connection @@ -5875,6 +6172,12 @@ Encryption private key must be 16 characters long + + Extra Disallows for robots.txt + + + Enter additional paths that should be included as Disallow entries in your robots.txt. Each entry has to be entered in a new line. + Full-Text settings @@ -5929,6 +6232,12 @@ Localization settings + + Meta robots + + + Specifies if and how search engines indexing the pages of your store. + Page title SEO adjustment @@ -5983,6 +6292,12 @@ When enabled, your URLs will be http://www.yourStore.com/en/ or http://www.yourStore.com/ru/ (SEO friendly) + + Characters to be converted + + + Allows an individual conversion of characters for SEO name creation. Enter the old and the new character separated by a semicolon, e.g. ä;ae. Each entry has to be entered in a new line. + SEO settings @@ -6037,6 +6352,12 @@ Store information + + Check string + + + Enter any string to check the SEO name creation. Changed settings must be saved before. + Use images for language selection @@ -6097,6 +6418,12 @@ The maximum image size (longest side) allowed for image uploads. + + Thumbnail size of products in emails + + + Specifies the thumbnail image size (pixels) of products in emails. Enter 0 to not display thumbnails. + Mini-shopping cart thumbnail image size @@ -6175,6 +6502,12 @@ The number of news items to display on your home page. + + Maximum age (in days) + + + Specifies the maximum news age in days. Older news are not exported in the RSS feed. + News archive page size @@ -6217,6 +6550,12 @@ When disabled, customers will be automatically redirected to the order details page. + + Display orders of all stores + + + Specifies whether to display the orders of all stores to the customer. If this option is disabled, only the orders of the current store are displayed. + Gift card activation order status @@ -6271,6 +6610,12 @@ Set the order ID counter. This is useful if you want your orders to start at a certain number. This only affects orders created going forward. The value must be greater than the current maximum order ID. + + Number of displayed orders per page + + + Specifies the number of displayed orders per page. + Order settings @@ -6376,6 +6721,12 @@ Specify number of points awarded for registration. + + Round down points + + + Specifies whether to round down calculated points. Otherwise the bonus points will be rounded up. + Shipping settings @@ -6490,6 +6841,15 @@ Check the box to move products from wishlist to the cart when clicking "Add to cart" button. Otherwise, they are copied. + + Subscribe to newsletters + + + Specifies id customers can subscribe to newsletters when ordering and if the checkbox is enabled by default. + + + Order confirmation page + Round prices during calculation @@ -6503,16 +6863,16 @@ Determines whether base price should be displayed in the shopping cart. - Show comment box on confirm order page + Show comment box - Determines whether comment box is displayed on confirm order page + Specifies whether comment box is displayed on the order confirmation page. - Show legal hints in order summary on the confirm order page + Show legal hints in order summary - Determines whether to show hints in order summary on the confirm order page. This text can be altered in the language resources. + Specifies whether to show hints in order summary on the confirm order page. This text can be altered in the language resources. Display delivery times @@ -6526,6 +6886,12 @@ Check the box if you want the discount coupon box to be displayed on shopping cart page + + Show revocation waiver box for electronic services + + + Specifies whether the customer must agree a revocation waiver for electronic services on the order confirmation page. + Show gift card box @@ -6580,6 +6946,21 @@ Determines whether the product weight is shown in shopping cart + + Consent for email transfer to third parties + + + Specifies whether customers can agree to a transferring of their email address to third parties when ordering, and whether the checkbox is enabled by default during checkout. + + + Text for email transfer consent + + + I agree to the transfer and storage of my email address by third parties. + + + Specifies the text to be displayed to the customer. Please choose a specific reason, e.g. 'I agree to the transfer and storage of my email address for TrustedShops buyer protection.' + Wishlist @@ -6793,6 +7174,12 @@ Please provide a name. + + No shipping methods could be loaded. + + + There were no possibilities found to restrict shipping methods. + The shipping method has been updated successfully. @@ -6811,19 +7198,6 @@ Display order - - Shipping method restrictions - - - Country - - - Please mark the checkbox(es) for the country or countries in which you want the - shipping method(s) not available - - - The settings have been updated successfully. - SMS providers @@ -6896,6 +7270,18 @@ Please provide a name. + + Exchange rate currency + + + Specifies the primary exchange rate currency for this store. + + + Primary store currency + + + Specifies the the primary store currency. + Secure URL @@ -6903,10 +7289,10 @@ The secure URL of your store e.g. https://www.yourstore.com/ or http://sharedssl.yourstore.com/. Leave it empty if you want secure URL to be detected automatically. - SSL enabled + SSL - Check the box if your store will be SSL secured. + Specifies whether the store should be SSL secured. Store logo @@ -7004,6 +7390,9 @@ Name + + Theme requires no configuration + Variables were successfully updated. @@ -7382,6 +7771,24 @@ This is a list of the message tokens you can use in your emails. + + Attachment 1 + + + A file that is to be appended to each sent email (eg Terms, Conditions etc.) + + + Attachment 2 + + + A file that is to be appended to each sent email (eg Terms, Conditions etc.) + + + Attachment 3 + + + A file that is to be appended to each sent email (eg Terms, Conditions etc.) + BCC @@ -7892,6 +8299,9 @@ back to customer role list + + The customer role "{0}" cannot be found. + The customer role has been deleted successfully. @@ -8084,6 +8494,9 @@ The date the customer record is created. + + Customer GUID + Customer roles @@ -8141,6 +8554,9 @@ The last used IP address. + + Is system account + Is tax exempt @@ -8153,6 +8569,9 @@ The last activity date. + + Last login date + Last name @@ -8174,6 +8593,9 @@ The customer's password. + + Password salt + Phone @@ -8546,101 +8968,824 @@ Store Statistics - - Download uploaded file + + New profile - - Download uploaded file + + Default Value - - Download URL + + Object property - - Download object is saved + + Import Field - - Remove download + + You can optionally set for each field of the import file whether and for which object property the data should be imported. Fields with equal names are always imported as long as they are not explicitly ignored. Not yet selected properties are highlighted in the selection list. It is also possible to define a default value which is applied when the import field is empty. Stored assignments becomes invalid and reset when the delimiter changes. - - Save download + + The stored field assignments are invalid due to the change of the delimiter and were reset. - - Upload file + + Delimiter - - Use download URL + + Specifies the field separator. - - Gift cards + + Please enter a valid delimiter. - - The new gift card has been added successfully. + + Inner quote character - - Add a new gift card + + Specifies the inner quote character used for escaping. - - back to gift card list + + Please enter a valid inner quote character (escaping). - - The gift card has been deleted successfully. + + Delimiter and inner quote character cannot be equal in CSV files. - - Edit gift card details + + Quote character - - Initial value + + Specifies the quotation character. - - Initial value + + Please enter a valid quote character. - - The initial value of this gift card. + + Quote all fields - - Creation date + + Specifies whether to set quotation marks around all field values. - - The date/time the gift card was created/purchased. + + Delimiter and quote character cannot be equal in CSV files. - - Coupon code + + Supports multilines - - Generate code + + Specifies whether field values with multilines are supported. - - The gift card coupon code (used during checkout). + + Trim values - - Gift card type + + Specifies whether to remove space characters at start and end of a field value. - - The gift card type. + + Batch size - - Is gift card activated + + Specifies the maximum number of records per export file. 0 is the default and means that all the records are exported in one file. - - Determines whether gift this card is activated and can be used. + + Cannot delete a system export profile. - - Is recipient notified + + Clean up after successful deployment - - Is recipient notified. + + Specifies whether to delete unneeded files after all deployments succeeded. - - Notify recipient + + Apply settings from - - Message + + Specifies an export profile from which to apply the settings. - - Enter message. + + This is an automatic notification of store "{0}" about a recent data export. - - Order + + Export of profile "{0}" has been finished + + + Email addresses to + + + Specifies the email addresses where to send the notification message. + + + The following specific information will be taken into account by the provider during the export. + + + The export provider <b>{0}</b> requires no further configuration. + + + Create ZIP archive + + + Specifies whether to combine the export files in temporary a ZIP archive. The archive remains in the temporary folder of the export profile without further processing. + + + At least one file could not be copied. + + + Specifies whether to combine the export files in a ZIP archive and only to deploy the archive. + + + Publishing type + + + Specifies the type of publishing. + + + Email account + + + Specifies the email account used to sent the data. + + + Email addresses to + + + Specifies the email addresses where to send the data. + + + Email subject + + + Specifies the subject of the email. + + + Directory path + + + Specifies the path (relative or absolute) where to deploy the data. + + + HTTP transmission type + + + Specifies how to transmit the export files via HTTP. + + + Specifies whether to copy the exported data into a folder that is accessible through the internet. + + + Name of profile + + + Specifies the name of the publishing profile. + + + There are no publishing profiles. + + + Click <b>New profile</b> to add one or multiple publishing profiles to specify how to further proceed with the export files. + + + Passive mode + + + Specifies whether to exchange data in active or passive mode. + + + Password + + + Specifies the password. + + + Publishing profiles + + + Publishing target + + + Name of subfolder + + + Specifies the name of a subfolder where to publish the data. + + + URL\Host + + + Specifies the URL or host name where to send the data. + + + User name + + + Specifies the user name. + + + Use SSL + + + Specifies whether to use a SSL (Secure Sockets Layer) connection. + + + If there are a large number of export files, it is recommended to use the option <b>Create ZIP archive</b>. This saves time and avoids problems, such as a full email mailbox. + + + Email notification + + + Specifies the email account used to send a notification message of the completion of the export. + + + The export profile is disabled. It must be enabled to preview the export data. + + + Object + + + The object type the provider processes. + + + Export files + + + File type + + + The file type of the exported data. + + + Pattern for file names + + + Specifies the pattern for creating file names. + + + Please enter a valid pattern for file names. Example for file names: %Store.Id%-%Profile.Id%-%File.Index%-%Profile.SeoName% + + + ID of export profil;Folder name of export profil;SEO name of export profil;Store ID;SEO name of store;One based file index;Random number;UTC timestamp + + + Availability to + + + Filter by availability quantity. + + + Availability from + + + Filter by availability quantity. + + + Billing countries + + + Filter by billing countries. + + + Categories + + + Filter by categtories. + + + Created from + + + Filter by created date. + + + Created to + + + Filter by created date. + + + Customer roles + + + Filter by customer roles. + + + Only featured products + + + Filter by featured products. Is only applied when the filtering by categories and manufacturers. + + + Has placed x orders + + + Filter by number of placed orders. + + + Has spent amount x + + + Filter by spent amount. + + + Product Id to + + + Filter by product identifier. + + + Product Id from + + + Filter by product identifier. + + + Only active customers + + + Filter by active or inactive customers. + + + Only active subscribers + + + Filter by active or inactive newsletter subscribers. + + + Published + + + Filter by publishing. + + + Only tax exempt customers + + + Filter by tax exempt customers. + + + Last activity from + + + Filter by date of last store activity. + + + Last active until + + + Filter by date of last store activity. + + + Manufacturer + + + Filter by manufacturer. + + + Specify individual filters to limit the exported data. + + + Order status + + + Filter by order status. + + + Payment status + + + Filter by payment status. + + + Price to + + + Filter by price. + + + Price from + + + Filter by price. + + + Product tag + + + Filter by product tag. + + + Product type + + + Filter by product type. + + + Shipping countries + + + Filter by shipping countries. + + + Shipping status + + + Filter by shipping status. + + + Store + + + Filter by store. + + + Without category mapping + + + Filter by missing category mapping. + + + Without manufacturer mapping + + + Filter by missing manufacturer mapping. + + + Folder path + + + Specifies the relative path of the folder where to export the data. + + + Please enter a valid, relative folder path for the export data. + + + System profile + + + Indicates whether the export profile is a system profile. System profiles cannot be removed. + + + The system export profile {0} was not found. + + + Name of profile + + + Specifies the name of the export profile. + + + There were no export provider found. + + + There is no filtering available. + + + The export provider does not explicit support any file type. Therefore, the export provider is responsible for futher deployment of export data. + + + There is no preview available for this entity type. + + + There were no export profiles found. + + + There was no export profile of type <b>{0}</b> found. Create now a <a href="{1}">new export profile</a>. + + + There is no projection available. + + + This option is not taken into account in the preview. + + + With the following settings you can partition the data to be exported. This includes<ul><li>Skipping the first n records</li><li>The maximum number of records to be exported</li><li>The number of records per export file</li><li>Export data for each shop in a separate file</li></ul>By default, all data of a store will be exported into one file. + + + Partitioning setting values must be greater than or equal to 0. + + + Per store + + + Specifies whether to start a separate run-through for each store. For each shop a new file will be created. + + + Export profile + + + The export profile for this export provider. + + + {0} of {1} records exported + + + Text to be appended + + + Specifies the text to be attached to the product description. If there are multiple texts then one of it is selected randomly. + + + Export attribute combinations + + + Specifies whether to export a standalone product for each active attribute combination. + + + Attribute values + + + Specifies if and how to further process the attribute values. + + + Manufacturer\Brand + + + Specifies the manufacturer or brand to be exported, if a product has no manufacturer assigned. + + + Convert net into gross prices + + + Specifies to convert net into gross prices. + + + Critical characters + + + List with characters to be removed from the detail description. + + + Currency + + + Specifies the currency to be applied to the export. + + + Customer ID + + + Specifies the ID of the customer to be applied to the export. Is taken into account for price calculations for example. + + + Product description + + + Specifies what information to use for the description of the product. + + + Remove HTML from description + + + Specifies whether to remove all HTML from the product description for the export. + + + Free shipping threshold + + + Specifies the amount as from shipping is free. + + + Language + + + Specifies the language to be applied to the export. + + + Do not export grouped products + + + Specifies whether to export grouped products. If this option is activated, then the associated products will be exported. + + + The following information will be taken into account during the export and integrated in the process. + + + Number of pictures + + + Specifies the number of images per object to be exported. + + + Change order status to + + + Specifies if and how to change the status of the exported orders. + + + Product picture size + + + Specifies the size (in pixel) of the product image. + + + Product price + + + Specifies the product price to be exported. + + + Remove critical characters + + + Specifies whether to remove critical characters (like ½) from the detail description. + + + Shipping costs + + + The shipping costs to be exported. + + + Shipping time + + + Specifies the shipping time for products where it is unspecified. + + + Store + + + Specifies the store to be applied to the export. + + + Provider + + + Specifies the export provider. It is responsible for the individual formatting of the export data. + + + System name of profile + + + The system name of the export profile. + + + The following list contains system profiles, which are provided by plugins such as the <a href='http://community.smartstore.com/index.php?/files/file/85-smartstorenet-common-export-providers/' target='_blank'>Data Export Plugin</a>. You can customize system profiles as desired, but cannot create new ones. These profiles also add additional action buttons. You will find these above data lists, such as the product or order list. + + + System profiles + + + User profiles + + + Add import file... + + + Assignment of import fields + + + This is an automatic notification of store "{0}" about a recent data import. Summary: + + + Import of "{0}" has been finished + + + My product import;My category import;My customer import;My newsletter subscription import + + + Upload import file... + + + Key fields + + + Existing records can be identified for updates on the basis of key fields. The key fields are processed in the order how they are defined here. + + + Please only use the ID field as a key field, if the data sourced from the same database to which it will be imported. Otherwise it is possible that the wrong records are updated. + + + Last import result + + + Please upload an import file. + + + For multiple import files please make sure that they are of the same file type and that the content follows the same pattern (e.g. same column headings). + + + Name of profile + + + Specifies the name of the import profile. + + + There were no import profiles found. + + + Number of pictures + + + Specifies the number of images per object to be imported. + + + Please select the import object and upload an import file. + + + {0} of {1} records processed + + + Create new assignment here + + + Only update + + + If this option is enabled, only existing data is updated but no new records are added. + + + At least one key field is required. + + + Download uploaded file + + + Download uploaded file + + + Download URL + + + Download object is saved + + + Remove download + + + Save download + + + Upload file + + + Use download URL + + + Gift cards + + + The new gift card has been added successfully. + + + Add a new gift card + + + back to gift card list + + + The gift card has been deleted successfully. + + + Edit gift card details + + + Initial value + + + Initial value + + + The initial value of this gift card. + + + Creation date + + + The date/time the gift card was created/purchased. + + + Coupon code + + + Generate code + + + The gift card coupon code (used during checkout). + + + Gift card type + + + The gift card type. + + + Is gift card activated + + + Determines whether gift this card is activated and can be used. + + + Is recipient notified + + + Is recipient notified. + + + Notify recipient + + + Message + + + Enter message. + + + Order The gift card was purchased with this order. @@ -8786,6 +9931,12 @@ "Order placed" email (to store owner) has been queued. Queued email identifier: {0} + + Subscribed to newsletter + + + Newsletter subscriber has been removed + Order cancelled @@ -8793,7 +9944,7 @@ Order has been captured - Error capturing order #{0}. Error: {1} + Unable to capture payment for order {0}. Order has been deleted @@ -8817,7 +9968,7 @@ Order has been partially refunded. Amount = {0} - Unable to partially refund order. {0} + Unable to partially refund order {0}. Order placed @@ -8826,7 +9977,7 @@ Order has been refunded. Amount = {0} - Unable to refund order. {0} + Unable to refund order {0}. Order status has been changed to {0} @@ -8835,10 +9986,10 @@ Order has been voided - Unable to void order. {0} + Unable to void payment transaction of order {0}. - Unable to cancel recurring payment. {0} + Unable to cancel recurring payment for order {0}. Recurring payment has been cancelled @@ -8906,6 +10057,12 @@ Affiliate + + Accepts transfer of email + + + Indicates whether the customer has agreed to a transfer of his email address to third parties. + Affiliate @@ -10145,8 +11302,8 @@ Email is required. - - {0} email(s) were imported and {1} updated. + + Subscription GUID Providing plugin @@ -10694,6 +11851,9 @@ Insert your SQL query here + + The SQL command was executed successfully. + Execute @@ -10706,6 +11866,9 @@ back to message queue + + Could not download e-mail attachment: no data. + The queued email has been deleted successfully. @@ -10715,9 +11878,21 @@ Edit message queue item + + An error occured while creating e-mail attachment + + + The e-mail attachment data could not be downloaded from path '{0}' + + + The content type of the e-mail attachment must be 'application/pdf' + Bcc + + Attachments + Bcc @@ -10826,6 +12001,9 @@ End date + + Number of attachments + End date @@ -10883,77 +12061,164 @@ The queued email has been updated successfully. + + Scheduled task + - Schedule tasks + Scheduled Tasks + + + Abnormally aborted due to application shutdown + + + The scheduled task "{0}" has been canceled + + + Cancellation request has been submitted. + + + Cron Expression + + + An expression that defines the schedule for the automatic execution of the task. + + + <a href='{0}' target='_blank'>Cron Expressions</a> help + + + Duration of the latest execution ([h]:[min]:[sec]) + + + Edit task Enabled + + Enables the scheduled execution of the task in accordance with the cron expression + + + Future schedules + + + The cron expression is invalid + Last end - Last run + Last Run + + + Start date of the last execution Last success + + Start date of the last successful execution + Name Name is required - - Do not forgot to restart the application once a task has been modified. + + Next Run in + + + Date of the next scheduled execution + + + Error while running scheduled task "{0}" Run now - - Task execution completed - Is running... Task is now running in the background - - Seconds (run period) + + The task is now running in the background. You will receive an email as soon as it is completed. The progress can be tracked in the export profile list. + + + The task is now running in the background. You will receive an email as soon as it is completed. The progress can be tracked in the import profile list. + + + The task has been executed successfully - - Seconds should be positive. + + Schedule execution - Stop on error + Disable on error + + + Check the box if the task should be disabled automatically when an error occurs during execution + + + The task can not be edited while it is running. + + + The task has been updated successfully - Search engine friendly page names + SEO Names - - Delete selected + + Only one active SEO name should be set per language. - Entity ID + Object ID + + + Specifies the ID of the associated object. - Entity name + Object + + + Specifies the name of the associated object. Is active + + Specifies whether the SEO name is active or inactive. + Language + + Specifies the language of the SEO name. + Standard - Name + SEO Name - A name to find. + Specifies the SEO name. + + + Names per object + + + The number of SEO names per object. + + + Background Task + + + PDF Converter + + + Search Engine System information @@ -10988,6 +12253,12 @@ The name of the data provider. + + Collect + + + The memory has been successfully cleaned up. + HTTP_HOST @@ -11024,6 +12295,9 @@ Server time zone + + Used memory (RAM) + Greenwich Mean Time (GMT/UTC) @@ -11033,6 +12307,12 @@ Warnings + + Access denied to anonymous request on {0}. + + + Access denied to user #{0} '{1}' on {2}. + Default dimension is not set @@ -11057,6 +12337,9 @@ Default weight is set + + Please enter digits only. + All directory permissions are OK @@ -11090,6 +12373,15 @@ '{0}' plugin is incompatible with your SmartStore.NET version. Delete it or update to the latest version. + + There are no customer roles defined. + + + There are no permissions defined. + + + No shipment items + You don't have active payment methods @@ -11114,6 +12406,15 @@ Only one offline shipping rate computation method is recommended to use + + The reachability of the sitemap could not be validated. + + + The sitemap for the store is reachable. + + + The sitemap for the store is not reachable. + Specified store URL matches this store URL @@ -11417,6 +12718,21 @@ Tables/Container + + Please enter a valid email address + + + Please enter a name + + + Please enter a valid URL + + + Please enter username and password + + + The value must be greater than 0. + API not available. @@ -11579,6 +12895,9 @@ Checkout + + An anonymous checkout is not allowed. + Billing address @@ -11603,12 +12922,21 @@ Continue shopping + + Please agree to the user agreement for downloadable products. + Enter billing address Enter shipping address + + Yes, I want access to the digital content immediately and know that my right of revocation expires with the access. + + + Please confirm that you would like access to the digital content immediately. + Please wait several seconds before placing a new order (already placed another order several seconds ago). @@ -11702,6 +13030,9 @@ Submitting order information... + + Subscribed to newsletter + Terms of service @@ -11786,6 +13117,9 @@ Cancelled + + The API call to verify a CAPTCHA has failed. + Edit product @@ -11807,6 +13141,15 @@ Continue + + Copy to clipboard + + + Failed to copy. + + + Copied! + Count @@ -11819,6 +13162,9 @@ Cross references + + Customer number already exists, please choose another. + Date @@ -11837,36 +13183,84 @@ Are you sure you want to delete "{0}"? + + Deployment + Description Description + + Detail description + Display order Download + + The product variant doesn't have a sample download. + + + You have reached the maximum number of downloads {0}. + + + Download data is not available anymore. + + + Downloads are not allowed. + + + Download is not available any more. + + + Sample download is not available anymore. + Duration Edit + + Enabled + Enter value + + Click on an item to select or deselect it and OK to apply the selection. + + + There were no more items found. + + + Click on an item to select it and OK to apply it. + Error + + The email address is not valid. + + + No active language could be loaded. + + + No email account could be loaded. + Unfortunately the selected payment method caused an error. Please correct your entries, try it again or select another payment method. Error while sending the email. Please try again later. + + Example + Execution @@ -11894,12 +13288,21 @@ Drop files here to upload + + Enter URL + Failed Upload a file + + Filter + + + Free shipping + Friendly name @@ -11915,6 +13318,9 @@ Home + + Image + Import @@ -11924,12 +13330,21 @@ Is active + + Language + List + + Loading + Loading next step... + + This function is not available for guests. + Miscellaneous @@ -11939,6 +13354,9 @@ More info + + My + Navigation @@ -11954,6 +13372,12 @@ No, cancel + + There are no files available. + + + There was no file uploaded. + The operation was not carried out for security reasons. @@ -11963,6 +13387,9 @@ Alert + + Not selectable + Off @@ -11978,6 +13405,12 @@ Optional + + Options + + + Partition + Capture method not supported @@ -12002,6 +13435,24 @@ Product + + Profile + + + Projection + + + Provider + + + public + + + Published + + + Publishing + Question @@ -12014,9 +13465,21 @@ Remove + + Replace + + + The request could not be processed.<br />Controller: {0}, Action: {1}, Reason: {2}. + + + Rule + Save + + Scheduled + Search @@ -12050,6 +13513,12 @@ Show + + Show all + + + Show more + Shrink @@ -12062,9 +13531,21 @@ System name + + Unavailable + Undefined + + Unknown + + + Unpublished + + + Unscheduled + Unspecified @@ -12083,11 +13564,17 @@ Wait... + + Waiting + Warning + + Website + - The characters didn't match the picture. Please try again. + Please confirm that you are not a "robot". Wrong email @@ -12137,6 +13624,15 @@ Enter your name + + Privacy consent + + + Yes I've read the <a href="{0}">privacy terms</a> and agree that my data given by me can be stored electronically. My data will thereby only be used to process my inquiry. + + + Please agree to the storage of your data. + Your enquiry has been successfully sent to the store owner. @@ -12146,6 +13642,9 @@ Currencies + + The customer does not exist. + Guest @@ -12167,6 +13666,9 @@ Product + + The product has no user agreement. + I agree @@ -12176,6 +13678,9 @@ User agreement + + Yes, I agree to the <a href='javascript:void(0)' data-id='{0}' class='download-user-agreement'>user agreement</a> for this product. + Checkboxes @@ -12239,6 +13744,18 @@ Track inventory by product attributes + + No price indication + + + Minimum feasible price + + + Price preselected on detail page + + + Price without discounts and attributes + Created on @@ -12329,6 +13846,27 @@ Show usernames + + Automatically assigned + + + Disabled + + + Enabled + + + Display + + + Always editable + + + Editable if empty + + + Do not display + Clear @@ -12338,6 +13876,102 @@ Hashed + + Append all values to the product name + + + Not specified + + + Email + + + File system + + + FTP + + + HTTP POST + + + Public folder + + + Description + + + Manufacturer + Product name + long description + + + Manufacturer + Product name + short description + + + Product name + long description + + + Product name + short description + + + None + + + Short description + + + Short description or name if empty + + + Category + + + Customer + + + Manufacturer + + + Newsletter Subscribers + + + Order + + + Product + + + Multipart form data POST + + + Simple POST + + + Complete + + + None + + + Processing + + + Category + + + Customer + + + Newsletter Subscriber + + + Product + + + Delimiter separated values (.csv, .txt, .tab) + + + Excel (.xlsx) + N times only @@ -12422,6 +14056,24 @@ Warning + + Show activated + + + Show deactivated + + + Do not show + + + Show activated + + + Show deactivated + + + Do not show + Cancelled @@ -12688,9 +14340,6 @@ Forum's topics with newest posts. - - {0} - Forum: {1} - Forum Name @@ -12931,6 +14580,9 @@ Information + + The install language '{0}' is not registered. + Please enter a valid credit card number. @@ -13180,12 +14832,48 @@ Billing Address + + The billing address is missing. + + + The order total could not be calculated. + + + The shipping total could not be calculated. + + + Cannot cancel order. + + + Cannot capture order. + + + Cannot mark order as completed. + + + Cannot mark order as paid. + + + Cannot do partial refund for order. + + + Cannot do refund for order. + + + Cannot do void for order. + Complete payment This order is not yet paid for. To pay now click the "Complete payment" button. + + The country '{0}' is not allowed for billing. + + + The country '{0}' is not allowed for shipping. + Email @@ -13198,6 +14886,12 @@ Gift card ({0}) + + No initial order exists for the recurring payment. + + + There are no recurring products. + Note(s) @@ -13207,6 +14901,9 @@ Note + + The order {0} was not found. + Order # @@ -13228,6 +14925,9 @@ Payment method additional fee + + order-{0}.pdf + Phone @@ -13339,6 +15039,9 @@ Shipping Address + + The shipping address is missing. + Shipping Method @@ -13489,6 +15192,9 @@ Email wishlist to a friend + + The next payment date could not be calculated. + Card code @@ -13513,6 +15219,9 @@ Wrong card number + + The payment method could not be loaded. + Valid until @@ -13522,6 +15231,24 @@ Expire year is required + + The payment method is not available. + + + At least one payment method provider is required to be active. + + + Unfortunately we can not handle this purchasing via your preferred payment method. Please select an alternate payment option to complete your order. + + + Recurring payment is not active. + + + Recurring payments are not supported by selected payment method. + + + The recurring payment type is not supported. + Select credit card @@ -13639,12 +15366,6 @@ Weight - - Associated products - - - Bundled items - Mail @@ -13660,32 +15381,14 @@ Contact data - - Height - - - Legth - - - Manufacturer - - - Price - - - SKU - - - Specification attributes - Stock quantity - - Weight + + The poll answer {0} was not found. - - Width + + The poll is not available. Only registered users can vote. @@ -13708,6 +15411,9 @@ Privacy Notice + + Private messages are disabled. + Inbox @@ -13838,7 +15544,10 @@ Out of stock - base price: {0} per {1} + Content: {0} {1} ({2} / {3} {1}) + + + {0} {1} ({2} / {3} {1}) Top @@ -14039,7 +15748,10 @@ This product is sold out. - This product is sold out. + No bundle items available + + + The product {0} was not found. Price: @@ -14116,6 +15828,9 @@ Quantity + + The product variant {0} was not found. + Weight @@ -14161,6 +15876,9 @@ You can use ECB (European central bank) exchange rate provider only when exchange rate currency code is set to EURO + + You can use ECB (European central bank) exchange rate provider only when exchange rate currency code is set to EURO. + ECB exchange rate provider @@ -14251,6 +15969,9 @@ You cannot vote for your own review + + The product review {0} was not found. + Only registered users can write reviews @@ -14365,6 +16086,18 @@ Search term minimum length is {0} characters + + This shipment is already delivered. + + + This shipment is already shipped. + + + The shipping rate computation method could not be loaded. + + + At least one shipping rate computation method provider is required to be active. + Shipping & Returns @@ -14497,6 +16230,9 @@ The coupon code you entered couldn't be applied to your order + + The shoping cart is disabled. + Total @@ -14609,7 +16345,7 @@ Enter valid sender name - For a complete listing of all shipping costs please click here. + For a complete listing of all shipping costs please click <a href="{0}">here</a>. SKU @@ -14690,10 +16426,10 @@ Store name comes after page name - This store is currently closed + We'll be back. - Please check back in a little while. + We're busy updating our online store for you and will be back soon. Select store theme @@ -14713,9 +16449,15 @@ * All prices {0}, plus <a href="{1}">shipping</a> + + * All prices {0}, plus shipping + {0} {1} {2}plus <a href="{3}">shipping</a> + + {0} {1} {2}plus shipping + {0} {1} {2}plus <a href="{3}">shipping</a> @@ -15082,15 +16824,57 @@ Well background + + Day + + + d + + + Days + + + d + {0} days ago + + Hour + + + h + + + Hours + + + h + {0} hours ago + + Minute + + + min + + + Minutes + + + min + {0} minutes ago + + Month + + + Months + {0} months ago @@ -15112,9 +16896,33 @@ one year ago + + Second + + + sec + + + Seconds + + + sec + {0} seconds ago + + Week + + + Weeks + + + Year + + + Years + {0} years ago @@ -15175,6 +16983,9 @@ Enter your email + + The wishlist is disabled. + View Wishlist diff --git a/src/Presentation/SmartStore.Web/App_Data/Localization/App/en/head.txt b/src/Presentation/SmartStore.Web/App_Data/Localization/App/en/head.txt index afe64e4f3b..f822a9cb76 100644 --- a/src/Presentation/SmartStore.Web/App_Data/Localization/App/en/head.txt +++ b/src/Presentation/SmartStore.Web/App_Data/Localization/App/en/head.txt @@ -1 +1 @@ -201506181858349_AclRecordCustomerRole \ No newline at end of file +201605201911421_ExportRevision \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/App_Data/Localization/Installation/installation.de.xml b/src/Presentation/SmartStore.Web/App_Data/Localization/Installation/installation.de.xml index acd910374c..3582e6f7d8 100644 --- a/src/Presentation/SmartStore.Web/App_Data/Localization/Installation/installation.de.xml +++ b/src/Presentation/SmartStore.Web/App_Data/Localization/Installation/installation.de.xml @@ -183,6 +183,9 @@ Schreibe Beispiel Daten - + + + Optionen + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/App_Data/Localization/Installation/installation.en.xml b/src/Presentation/SmartStore.Web/App_Data/Localization/Installation/installation.en.xml index a11658d45b..4603606855 100644 --- a/src/Presentation/SmartStore.Web/App_Data/Localization/Installation/installation.en.xml +++ b/src/Presentation/SmartStore.Web/App_Data/Localization/Installation/installation.en.xml @@ -183,5 +183,9 @@ Creating sample data - + + + Options + + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/App_Data/ua-parser.regexes.yaml b/src/Presentation/SmartStore.Web/App_Data/ua-parser.regexes.yaml new file mode 100644 index 0000000000..c160ade711 --- /dev/null +++ b/src/Presentation/SmartStore.Web/App_Data/ua-parser.regexes.yaml @@ -0,0 +1,4798 @@ +user_agent_parsers: + #### SPECIAL CASES TOP #### + + # @note: iOS / OSX Applications + - regex: '(CFNetwork)(?:/(\d+)\.(\d+)\.?(\d+)?)?' + family_replacement: 'CFNetwork' + + # Pingdom + - regex: '(Pingdom.com_bot_version_)(\d+)\.(\d+)' + family_replacement: 'PingdomBot' + + # Facebook + - regex: '(facebookexternalhit)/(\d+)\.(\d+)' + family_replacement: 'FacebookBot' + + # Google Plus + - regex: 'Google.*/\+/web/snippet' + family_replacement: 'GooglePlusBot' + + # Twitter + - regex: '(Twitterbot)/(\d+)\.(\d+)' + family_replacement: 'TwitterBot' + + # Bots Pattern '/name-0.0' + - regex: '/((?:Ant-)?Nutch|[A-z]+[Bb]ot|[A-z]+[Ss]pider|Axtaris|fetchurl|Isara|ShopSalad|Tailsweep)[ \-](\d+)(?:\.(\d+)(?:\.(\d+))?)?' + # Bots Pattern 'name/0.0' + - regex: '(008|Altresium|Argus|BaiduMobaider|BoardReader|DNSGroup|DataparkSearch|EDI|Goodzer|Grub|INGRID|Infohelfer|LinkedInBot|LOOQ|Nutch|PathDefender|Peew|PostPost|Steeler|Twitterbot|VSE|WebCrunch|WebZIP|Y!J-BR[A-Z]|YahooSeeker|envolk|sproose|wminer)/(\d+)(?:\.(\d+)(?:\.(\d+))?)?' + + # MSIECrawler + - regex: '(MSIE) (\d+)\.(\d+)([a-z]\d?)?;.* MSIECrawler' + family_replacement: 'MSIECrawler' + + # Downloader ... + - regex: '(Google-HTTP-Java-Client|Apache-HttpClient|http%20client|Python-urllib|HttpMonitor|TLSProber|WinHTTP|JNLP)(?:[ /](\d+)(?:\.(\d+)(?:\.(\d+))?)?)?' + + # Bots + - regex: '(1470\.net crawler|50\.nu|8bo Crawler Bot|Aboundex|Accoona-[A-z]+-Agent|AdsBot-Google(?:-[a-z]+)?|altavista|AppEngine-Google|archive.*?\.org_bot|archiver|Ask Jeeves|[Bb]ai[Dd]u[Ss]pider(?:-[A-Za-z]+)*|bingbot|BingPreview|blitzbot|BlogBridge|BoardReader(?: [A-Za-z]+)*|boitho.com-dc|BotSeer|\b\w*favicon\w*\b|\bYeti(?:-[a-z]+)?|Catchpoint bot|[Cc]harlotte|Checklinks|clumboot|Comodo HTTP\(S\) Crawler|Comodo-Webinspector-Crawler|ConveraCrawler|CRAWL-E|CrawlConvera|Daumoa(?:-feedfetcher)?|Feed Seeker Bot|findlinks|Flamingo_SearchEngine|FollowSite Bot|furlbot|Genieo|gigabot|GomezAgent|gonzo1|(?:[a-zA-Z]+-)?Googlebot(?:-[a-zA-Z]+)?|Google SketchUp|grub-client|gsa-crawler|heritrix|HiddenMarket|holmes|HooWWWer|htdig|ia_archiver|ICC-Crawler|Icarus6j|ichiro(?:/mobile)?|IconSurf|IlTrovatore(?:-Setaccio)?|InfuzApp|Innovazion Crawler|InternetArchive|IP2[a-z]+Bot|jbot\b|KaloogaBot|Kraken|Kurzor|larbin|LEIA|LesnikBot|Linguee Bot|LinkAider|LinkedInBot|Lite Bot|Llaut|lycos|Mail\.RU_Bot|masidani_bot|Mediapartners-Google|Microsoft .*? Bot|mogimogi|mozDex|MJ12bot|msnbot(?:-media *)?|msrbot|netresearch|Netvibes|NewsGator[^/]*|^NING|Nutch[^/]*|Nymesis|ObjectsSearch|Orbiter|OOZBOT|PagePeeker|PagesInventory|PaxleFramework|Peeplo Screenshot Bot|PlantyNet_WebRobot|Pompos|Read%20Later|Reaper|RedCarpet|Retreiver|Riddler|Rival IQ|scooter|Scrapy|Scrubby|searchsight|seekbot|semanticdiscovery|Simpy|SimplePie|SEOstats|SimpleRSS|SiteCon|Slurp|snappy|Speedy Spider|Squrl Java|TheUsefulbot|ThumbShotsBot|Thumbshots\.ru|TwitterBot|URL2PNG|Vagabondo|VoilaBot|^vortex|Votay bot|^voyager|WASALive.Bot|Web-sniffer|WebThumb|WeSEE:[A-z]+|WhatWeb|WIRE|WordPress|Wotbox|www\.almaden\.ibm\.com|Xenu(?:.s)? Link Sleuth|Xerka [A-z]+Bot|yacy(?:bot)?|Yahoo[a-z]*Seeker|Yahoo! Slurp|Yandex\w+|YodaoBot(?:-[A-z]+)?|YottaaMonitor|Yowedo|^Zao|^Zao-Crawler|ZeBot_www\.ze\.bz|ZooShot|ZyBorg)(?:[ /]v?(\d+)(?:\.(\d+)(?:\.(\d+))?)?)?' + + # Bots General matcher 'name/0.0' + - regex: '(?:\/[A-Za-z0-9\.]+)? *([A-Za-z0-9 \-_\!\[\]:]*(?:[Aa]rchiver|[Ii]ndexer|[Ss]craper|[Bb]ot|[Ss]pider|[Cc]rawl[a-z]*))/(\d+)(?:\.(\d+)(?:\.(\d+))?)?' + # Bots General matcher 'name 0.0' + - regex: '(?:\/[A-Za-z0-9\.]+)? *([A-Za-z0-9 _\!\[\]:]*(?:[Aa]rchiver|[Ii]ndexer|[Ss]craper|[Bb]ot|[Ss]pider|[Cc]rawl[a-z]*)) (\d+)(?:\.(\d+)(?:\.(\d+))?)?' + # Bots containing spider|scrape|bot(but not CUBOT)|Crawl + - regex: '((?:[A-z0-9]+|[A-z\-]+ ?)?(?: the )?(?:[Ss][Pp][Ii][Dd][Ee][Rr]|[Ss]crape|[A-Za-z0-9-]*(?:[^C][^Uu])[Bb]ot|[Cc][Rr][Aa][Ww][Ll])[A-z0-9]*)(?:(?:[ /]| v)(\d+)(?:\.(\d+)(?:\.(\d+))?)?)?' + + # HbbTV standard defines what features the browser should understand. + # but it's like targeting "HTML5 browsers", effective browser support depends on the model + # See os_parsers if you want to target a specific TV + - regex: '(HbbTV)/(\d+)\.(\d+)\.(\d+) \(' + + # must go before Firefox to catch Chimera/SeaMonkey/Camino + - regex: '(Chimera|SeaMonkey|Camino)/(\d+)\.(\d+)\.?([ab]?\d+[a-z]*)?' + + # Social Networks + # Facebook + - regex: '\[FB.*;(FBAV)/(\d+)(?:\.(\d+)(?:\.(\d)+)?)?' + family_replacement: 'Facebook' + # Pinterest + - regex: '\[(Pinterest)/[^\]]+\]' + - regex: '(Pinterest)(?: for Android(?: Tablet)?)?/(\d+)(?:\.(\d+)(?:\.(\d)+)?)?' + + # Firefox + - regex: '(Pale[Mm]oon)/(\d+)\.(\d+)\.?(\d+)?' + family_replacement: 'Pale Moon (Firefox Variant)' + - regex: '(Fennec)/(\d+)\.(\d+)\.?([ab]?\d+[a-z]*)' + family_replacement: 'Firefox Mobile' + - regex: '(Fennec)/(\d+)\.(\d+)(pre)' + family_replacement: 'Firefox Mobile' + - regex: '(Fennec)/(\d+)\.(\d+)' + family_replacement: 'Firefox Mobile' + - regex: '(?:Mobile|Tablet);.*(Firefox)/(\d+)\.(\d+)' + family_replacement: 'Firefox Mobile' + - regex: '(Namoroka|Shiretoko|Minefield)/(\d+)\.(\d+)\.(\d+(?:pre)?)' + family_replacement: 'Firefox ($1)' + - regex: '(Firefox)/(\d+)\.(\d+)(a\d+[a-z]*)' + family_replacement: 'Firefox Alpha' + - regex: '(Firefox)/(\d+)\.(\d+)(b\d+[a-z]*)' + family_replacement: 'Firefox Beta' + - regex: '(Firefox)-(?:\d+\.\d+)?/(\d+)\.(\d+)(a\d+[a-z]*)' + family_replacement: 'Firefox Alpha' + - regex: '(Firefox)-(?:\d+\.\d+)?/(\d+)\.(\d+)(b\d+[a-z]*)' + family_replacement: 'Firefox Beta' + - regex: '(Namoroka|Shiretoko|Minefield)/(\d+)\.(\d+)([ab]\d+[a-z]*)?' + family_replacement: 'Firefox ($1)' + - regex: '(Firefox).*Tablet browser (\d+)\.(\d+)\.(\d+)' + family_replacement: 'MicroB' + - regex: '(MozillaDeveloperPreview)/(\d+)\.(\d+)([ab]\d+[a-z]*)?' + - regex: '(FxiOS)/(\d+)\.(\d+)(\.(\d+))?(\.(\d+))?' + family_replacement: 'Firefox iOS' + + # e.g.: Flock/2.0b2 + - regex: '(Flock)/(\d+)\.(\d+)(b\d+?)' + + # RockMelt + - regex: '(RockMelt)/(\d+)\.(\d+)\.(\d+)' + + # e.g.: Fennec/0.9pre + - regex: '(Navigator)/(\d+)\.(\d+)\.(\d+)' + family_replacement: 'Netscape' + + - regex: '(Navigator)/(\d+)\.(\d+)([ab]\d+)' + family_replacement: 'Netscape' + + - regex: '(Netscape6)/(\d+)\.(\d+)\.?([ab]?\d+)?' + family_replacement: 'Netscape' + + - regex: '(MyIBrow)/(\d+)\.(\d+)' + family_replacement: 'My Internet Browser' + + # Opera will stop at 9.80 and hide the real version in the Version string. + # see: http://dev.opera.com/articles/view/opera-ua-string-changes/ + - regex: '(Opera Tablet).*Version/(\d+)\.(\d+)(?:\.(\d+))?' + - regex: '(Opera Mini)(?:/att)?/?(\d+)?(?:\.(\d+))?(?:\.(\d+))?' + - regex: '(Opera)/.+Opera Mobi.+Version/(\d+)\.(\d+)' + family_replacement: 'Opera Mobile' + - regex: '(Opera)/(\d+)\.(\d+).+Opera Mobi' + family_replacement: 'Opera Mobile' + - regex: 'Opera Mobi.+(Opera)(?:/|\s+)(\d+)\.(\d+)' + family_replacement: 'Opera Mobile' + - regex: 'Opera Mobi' + family_replacement: 'Opera Mobile' + - regex: '(Opera)/9.80.*Version/(\d+)\.(\d+)(?:\.(\d+))?' + + # Opera 14 for Android uses a WebKit render engine. + - regex: '(?:Mobile Safari).*(OPR)/(\d+)\.(\d+)\.(\d+)' + family_replacement: 'Opera Mobile' + + # Opera >=15 for Desktop is similar to Chrome but includes an "OPR" Version string. + - regex: '(?:Chrome).*(OPR)/(\d+)\.(\d+)\.(\d+)' + family_replacement: 'Opera' + + # Opera Coast + - regex: '(Coast)/(\d+).(\d+).(\d+)' + family_replacement: 'Opera Coast' + + # Opera Mini for iOS (from version 8.0.0) + - regex: '(OPiOS)/(\d+).(\d+).(\d+)' + family_replacement: 'Opera Mini' + + # Palm WebOS looks a lot like Safari. + - regex: '(hpw|web)OS/(\d+)\.(\d+)(?:\.(\d+))?' + family_replacement: 'webOS Browser' + + # LuaKit has no version info. + # http://luakit.org/projects/luakit/ + - regex: '(luakit)' + family_replacement: 'LuaKit' + + # Snowshoe + - regex: '(Snowshoe)/(\d+)\.(\d+).(\d+)' + + # Lightning (for Thunderbird) + # http://www.mozilla.org/projects/calendar/lightning/ + - regex: '(Lightning)/(\d+)\.(\d+)\.?((?:[ab]?\d+[a-z]*)|(?:\d*))' + + # Swiftfox + - regex: '(Firefox)/(\d+)\.(\d+)\.(\d+(?:pre)?) \(Swiftfox\)' + family_replacement: 'Swiftfox' + - regex: '(Firefox)/(\d+)\.(\d+)([ab]\d+[a-z]*)? \(Swiftfox\)' + family_replacement: 'Swiftfox' + + # Rekonq + - regex: '(rekonq)/(\d+)\.(\d+)\.?(\d+)? Safari' + family_replacement: 'Rekonq' + - regex: 'rekonq' + family_replacement: 'Rekonq' + + # Conkeror lowercase/uppercase + # http://conkeror.org/ + - regex: '(conkeror|Conkeror)/(\d+)\.(\d+)\.?(\d+)?' + family_replacement: 'Conkeror' + + # catches lower case konqueror + - regex: '(konqueror)/(\d+)\.(\d+)\.(\d+)' + family_replacement: 'Konqueror' + + - regex: '(WeTab)-Browser' + + - regex: '(Comodo_Dragon)/(\d+)\.(\d+)\.(\d+)' + family_replacement: 'Comodo Dragon' + + - regex: '(Symphony) (\d+).(\d+)' + + - regex: '(Minimo)' + + - regex: 'PLAYSTATION 3.+WebKit' + family_replacement: 'NetFront NX' + - regex: 'PLAYSTATION 3' + family_replacement: 'NetFront' + - regex: '(PlayStation Portable)' + family_replacement: 'NetFront' + - regex: '(PlayStation Vita)' + family_replacement: 'NetFront NX' + + - regex: 'AppleWebKit.+ (NX)/(\d+)\.(\d+)\.(\d+)' + family_replacement: 'NetFront NX' + - regex: '(Nintendo 3DS)' + family_replacement: 'NetFront NX' + + # Amazon Silk, should go before Safari and Chrome Mobile + - regex: '(Silk)/(\d+)\.(\d+)(?:\.([0-9\-]+))?' + family_replacement: 'Amazon Silk' + + + # @ref: http://www.puffinbrowser.com + - regex: '(Puffin)/(\d+)\.(\d+)(?:\.(\d+))?' + + # Edge Mobile + - regex: 'Windows Phone .*(Edge)/(\d+)\.(\d+)' + family_replacement: 'Edge Mobile' + + # Samsung Internet (based on Chrome, but lacking some features) + - regex: '(SamsungBrowser)/(\d+)\.(\d+)' + family_replacement: 'Samsung Internet' + + # Chrome Mobile + - regex: '(CrMo)/(\d+)\.(\d+)\.(\d+)\.(\d+)' + family_replacement: 'Chrome Mobile' + - regex: '(CriOS)/(\d+)\.(\d+)\.(\d+)\.(\d+)' + family_replacement: 'Chrome Mobile iOS' + - regex: '(Chrome)/(\d+)\.(\d+)\.(\d+)\.(\d+) Mobile' + family_replacement: 'Chrome Mobile' + + # Chrome Frame must come before MSIE. + - regex: '(chromeframe)/(\d+)\.(\d+)\.(\d+)' + family_replacement: 'Chrome Frame' + + # UC Browser + - regex: '(UCBrowser)[ /](\d+)\.(\d+)\.(\d+)' + family_replacement: 'UC Browser' + - regex: '(UC Browser)[ /](\d+)\.(\d+)\.(\d+)' + - regex: '(UC Browser|UCBrowser|UCWEB)(\d+)\.(\d+)\.(\d+)' + family_replacement: 'UC Browser' + + # Tizen Browser (second case included in browser/major.minor regex) + - regex: '(SLP Browser)/(\d+)\.(\d+)' + family_replacement: 'Tizen Browser' + + # Sogou Explorer 2.X + - regex: '(SE 2\.X) MetaSr (\d+)\.(\d+)' + family_replacement: 'Sogou Explorer' + + # Baidu Browsers (desktop spoofs chrome & IE, explorer is mobile) + - regex: '(baidubrowser)[/\s](\d+)' + family_replacement: 'Baidu Browser' + - regex: '(FlyFlow)/(\d+)\.(\d+)' + family_replacement: 'Baidu Explorer' + + # QQ Browsers + - regex: '(MQQBrowser/Mini)(?:(\d+)(?:\.(\d+)(?:\.(\d+))?)?)?' + family_replacement: 'QQ Browser Mini' + - regex: '(MQQBrowser)(?:/(\d+)(?:\.(\d+)(?:\.(\d+))?)?)?' + family_replacement: 'QQ Browser Mobile' + - regex: '(QQBrowser)(?:/(\d+)(?:\.(\d+)\.(\d+)(?:\.(\d+))?)?)?' + family_replacement: 'QQ Browser' + + # Rackspace Monitoring + - regex: '(Rackspace Monitoring)/(\d+)\.(\d+)' + family_replacement: 'RackspaceBot' + + # PyAMF + - regex: '(PyAMF)/(\d+)\.(\d+)\.(\d+)' + + # Yandex Browser + - regex: '(YaBrowser)/(\d+)\.(\d+)\.(\d+)' + family_replacement: 'Yandex Browser' + + # Mail.ru Amigo/Internet Browser (Chromium-based) + - regex: '(Chrome)/(\d+)\.(\d+)\.(\d+).* MRCHROME' + family_replacement: 'Mail.ru Chromium Browser' + + # AOL Browser (IE-based) + - regex: '(AOL) (\d+)\.(\d+); AOLBuild (\d+)' + + #### END SPECIAL CASES TOP #### + + #### MAIN CASES - this catches > 50% of all browsers #### + + # Browser/major_version.minor_version.beta_version + - regex: '(AdobeAIR|FireWeb|Jasmine|ANTGalio|Midori|Fresco|Lobo|PaleMoon|Maxthon|Lynx|OmniWeb|Dillo|Camino|Demeter|Fluid|Fennec|Epiphany|Shiira|Sunrise|Spotify|Flock|Netscape|Lunascape|WebPilot|NetFront|Netfront|Konqueror|SeaMonkey|Kazehakase|Vienna|Iceape|Iceweasel|IceWeasel|Iron|K-Meleon|Sleipnir|Galeon|GranParadiso|Opera Mini|iCab|NetNewsWire|ThunderBrowse|Iris|UP\.Browser|Bunjalloo|Google Earth|Raven for Mac|Openwave)/(\d+)\.(\d+)\.(\d+)' + + # Outlook 2007 + - regex: 'Microsoft Office Outlook 12\.\d+\.\d+|MSOffice 12' + family_replacement: 'Outlook' + v1_replacement: '2007' + + # Outlook 2010 + - regex: 'Microsoft Outlook 14\.\d+\.\d+|MSOffice 14' + family_replacement: 'Outlook' + v1_replacement: '2010' + + # Outlook 2013 + - regex: 'Microsoft Outlook 15\.\d+\.\d+' + family_replacement: 'Outlook' + v1_replacement: '2013' + + # Outlook 2016 + - regex: 'Microsoft Outlook (?:Mail )?16\.\d+\.\d+' + family_replacement: 'Outlook' + v1_replacement: '2016' + + # Windows Live Mail + - regex: 'Outlook-Express\/7\.0.*' + family_replacement: 'Windows Live Mail' + + # Apple Air Mail + - regex: '(Airmail) (\d+)\.(\d+)(?:\.(\d+))?' + + # Thunderbird + - regex: '(Thunderbird)/(\d+)\.(\d+)\.(\d+(?:pre)?)' + family_replacement: 'Thunderbird' + + # Vivaldi uses "Vivaldi" + - regex: '(Vivaldi)/(\d+)\.(\d+)\.(\d+)' + + # Edge/major_version.minor_version + - regex: '(Edge)/(\d+)\.(\d+)' + + # Brave Browser https://brave.com/ + - regex: '(brave)/(\d+)\.(\d+)\.(\d+) Chrome' + family_replacement: 'Brave' + + # Chrome/Chromium/major_version.minor_version.beta_version + - regex: '(Chromium|Chrome)/(\d+)\.(\d+)\.(\d+)' + + # Dolphin Browser + # @ref: http://www.dolphin.com + - regex: '\b(Dolphin)(?: |HDCN/|/INT\-)(\d+)\.(\d+)\.?(\d+)?' + + # Browser/major_version.minor_version + - regex: '(bingbot|Bolt|Jasmine|IceCat|Skyfire|Midori|Maxthon|Lynx|Arora|IBrowse|Dillo|Camino|Shiira|Fennec|Phoenix|Chrome|Flock|Netscape|Lunascape|Epiphany|WebPilot|Opera Mini|Opera|NetFront|Netfront|Konqueror|Googlebot|SeaMonkey|Kazehakase|Vienna|Iceape|Iceweasel|IceWeasel|Iron|K-Meleon|Sleipnir|Galeon|GranParadiso|iCab|iTunes|MacAppStore|NetNewsWire|Space Bison|Stainless|Orca|Dolfin|BOLT|Minimo|Tizen Browser|Polaris|Abrowser|Planetweb|ICE Browser|mDolphin|qutebrowser|Otter|QupZilla)/(\d+)\.(\d+)\.?(\d+)?' + + # Chrome/Chromium/major_version.minor_version + - regex: '(Chromium|Chrome)/(\d+)\.(\d+)' + + ########## + # IE Mobile needs to happen before Android to catch cases such as: + # Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 920)... + # Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 920; ANZ821)... + # Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 920; Orange)... + # Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 920; Vodafone)... + ########## + + # IE Mobile + - regex: '(IEMobile)[ /](\d+)\.(\d+)' + family_replacement: 'IE Mobile' + + # Browser major_version.minor_version.beta_version (space instead of slash) + - regex: '(iRider|Crazy Browser|SkipStone|iCab|Lunascape|Sleipnir|Maemo Browser) (\d+)\.(\d+)\.(\d+)' + # Browser major_version.minor_version (space instead of slash) + - regex: '(iCab|Lunascape|Opera|Android|Jasmine|Polaris) (\d+)\.(\d+)\.?(\d+)?' + + # Kindle WebKit + - regex: '(Kindle)/(\d+)\.(\d+)' + + # weird android UAs + - regex: '(Android) Donut' + v1_replacement: '1' + v2_replacement: '2' + + - regex: '(Android) Eclair' + v1_replacement: '2' + v2_replacement: '1' + + - regex: '(Android) Froyo' + v1_replacement: '2' + v2_replacement: '2' + + - regex: '(Android) Gingerbread' + v1_replacement: '2' + v2_replacement: '3' + + - regex: '(Android) Honeycomb' + v1_replacement: '3' + + # desktop mode + # http://www.anandtech.com/show/3982/windows-phone-7-review + - regex: '(MSIE) (\d+)\.(\d+).*XBLWP7' + family_replacement: 'IE Large Screen' + + #### END MAIN CASES #### + + #### SPECIAL CASES #### + - regex: '(Obigo)InternetBrowser' + - regex: '(Obigo)\-Browser' + - regex: '(Obigo|OBIGO)[^\d]*(\d+)(?:.(\d+))?' + family_replacement: 'Obigo' + + - regex: '(MAXTHON|Maxthon) (\d+)\.(\d+)' + family_replacement: 'Maxthon' + - regex: '(Maxthon|MyIE2|Uzbl|Shiira)' + v1_replacement: '0' + + - regex: '(BrowseX) \((\d+)\.(\d+)\.(\d+)' + + - regex: '(NCSA_Mosaic)/(\d+)\.(\d+)' + family_replacement: 'NCSA Mosaic' + + # Polaris/d.d is above + - regex: '(POLARIS)/(\d+)\.(\d+)' + family_replacement: 'Polaris' + - regex: '(Embider)/(\d+)\.(\d+)' + family_replacement: 'Polaris' + + - regex: '(BonEcho)/(\d+)\.(\d+)\.?([ab]?\d+)?' + family_replacement: 'Bon Echo' + + # @note: iOS / OSX Applications + - regex: '(iPod|iPhone|iPad).+Version/(\d+)\.(\d+)(?:\.(\d+))?.* Safari' + family_replacement: 'Mobile Safari' + - regex: '(iPod|iPhone|iPad).+Version/(\d+)\.(\d+)(?:\.(\d+))?' + family_replacement: 'Mobile Safari UI/WKWebView' + - regex: '(iPod|iPhone|iPad);.*CPU.*OS (\d+)_(\d+)(?:_(\d+))?.*Mobile.* Safari' + family_replacement: 'Mobile Safari' + - regex: '(iPod|iPhone|iPad);.*CPU.*OS (\d+)_(\d+)(?:_(\d+))?.*Mobile' + family_replacement: 'Mobile Safari UI/WKWebView' + - regex: '(iPod|iPhone|iPad).* Safari' + family_replacement: 'Mobile Safari' + - regex: '(iPod|iPhone|iPad)' + family_replacement: 'Mobile Safari UI/WKWebView' + + - regex: '(AvantGo) (\d+).(\d+)' + + - regex: '(OneBrowser)/(\d+).(\d+)' + family_replacement: 'ONE Browser' + + - regex: '(Avant)' + v1_replacement: '1' + + # This is the Tesla Model S (see similar entry in device parsers) + - regex: '(QtCarBrowser)' + v1_replacement: '1' + + - regex: '^(iBrowser/Mini)(\d+).(\d+)' + family_replacement: 'iBrowser Mini' + - regex: '^(iBrowser|iRAPP)/(\d+).(\d+)' + + # nokia browsers + # based on: http://www.developer.nokia.com/Community/Wiki/User-Agent_headers_for_Nokia_devices + - regex: '^(Nokia)' + family_replacement: 'Nokia Services (WAP) Browser' + - regex: '(NokiaBrowser)/(\d+)\.(\d+).(\d+)\.(\d+)' + family_replacement: 'Nokia Browser' + - regex: '(NokiaBrowser)/(\d+)\.(\d+).(\d+)' + family_replacement: 'Nokia Browser' + - regex: '(NokiaBrowser)/(\d+)\.(\d+)' + family_replacement: 'Nokia Browser' + - regex: '(BrowserNG)/(\d+)\.(\d+).(\d+)' + family_replacement: 'Nokia Browser' + - regex: '(Series60)/5\.0' + family_replacement: 'Nokia Browser' + v1_replacement: '7' + v2_replacement: '0' + - regex: '(Series60)/(\d+)\.(\d+)' + family_replacement: 'Nokia OSS Browser' + - regex: '(S40OviBrowser)/(\d+)\.(\d+)\.(\d+)\.(\d+)' + family_replacement: 'Ovi Browser' + - regex: '(Nokia)[EN]?(\d+)' + + # BlackBerry devices + - regex: '(PlayBook).+RIM Tablet OS (\d+)\.(\d+)\.(\d+)' + family_replacement: 'BlackBerry WebKit' + - regex: '(Black[bB]erry|BB10).+Version/(\d+)\.(\d+)\.(\d+)' + family_replacement: 'BlackBerry WebKit' + - regex: '(Black[bB]erry)\s?(\d+)' + family_replacement: 'BlackBerry' + + - regex: '(OmniWeb)/v(\d+)\.(\d+)' + + - regex: '(Blazer)/(\d+)\.(\d+)' + family_replacement: 'Palm Blazer' + + - regex: '(Pre)/(\d+)\.(\d+)' + family_replacement: 'Palm Pre' + + # fork of Links + - regex: '(ELinks)/(\d+)\.(\d+)' + - regex: '(ELinks) \((\d+)\.(\d+)' + - regex: '(Links) \((\d+)\.(\d+)' + + - regex: '(QtWeb) Internet Browser/(\d+)\.(\d+)' + + #- regex: '\(iPad;.+(Version)/(\d+)\.(\d+)(?:\.(\d+))?.*Safari/' + # family_replacement: 'iPad' + + # Phantomjs, should go before Safari + - regex: '(PhantomJS)/(\d+)\.(\d+)\.(\d+)' + + # WebKit Nightly + - regex: '(AppleWebKit)/(\d+)\.?(\d+)?\+ .* Safari' + family_replacement: 'WebKit Nightly' + + # Safari + - regex: '(Version)/(\d+)\.(\d+)(?:\.(\d+))?.*Safari/' + family_replacement: 'Safari' + # Safari didn't provide "Version/d.d.d" prior to 3.0 + - regex: '(Safari)/\d+' + + - regex: '(OLPC)/Update(\d+)\.(\d+)' + + - regex: '(OLPC)/Update()\.(\d+)' + v1_replacement: '0' + + - regex: '(SEMC\-Browser)/(\d+)\.(\d+)' + + - regex: '(Teleca)' + family_replacement: 'Teleca Browser' + + - regex: '(Phantom)/V(\d+)\.(\d+)' + family_replacement: 'Phantom Browser' + + - regex: 'Trident(.*)rv.(\d+)\.(\d+)' + family_replacement: 'IE' + + # Espial + - regex: '(Espial)/(\d+)(?:\.(\d+))?(?:\.(\d+))?' + + # Apple Mail + + # apple mail - not directly detectable, have it after Safari stuff + - regex: '(AppleWebKit)/(\d+)\.(\d+)\.(\d+)' + family_replacement: 'Apple Mail' + + # AFTER THE EDGE CASES ABOVE! + # AFTER IE11 + # BEFORE all other IE + - regex: '(Firefox)/(\d+)\.(\d+)\.(\d+)' + - regex: '(Firefox)/(\d+)\.(\d+)(pre|[ab]\d+[a-z]*)?' + + - regex: '([MS]?IE) (\d+)\.(\d+)' + family_replacement: 'IE' + + - regex: '(python-requests)/(\d+)\.(\d+)' + family_replacement: 'Python Requests' + + - regex: '(Java)[/ ]{0,1}\d+\.(\d+)\.(\d+)[_-]*([a-zA-Z0-9]+)*' + + # Roku Digital-Video-Players https://www.roku.com/ + - regex: '^(Roku)/DVP-(\d+)\.(\d+)' + +os_parsers: + ########## + # HbbTV vendors + ########## + + # starts with the easy one : Panasonic seems consistent across years, hope it will continue + #HbbTV/1.1.1 (;Panasonic;VIERA 2011;f.532;0071-0802 2000-0000;) + #HbbTV/1.1.1 (;Panasonic;VIERA 2012;1.261;0071-3103 2000-0000;) + #HbbTV/1.2.1 (;Panasonic;VIERA 2013;3.672;4101-0003 0002-0000;) + #- regex: 'HbbTV/\d+\.\d+\.\d+ \(;(Panasonic);VIERA ([0-9]{4});' + + # Sony is consistent too but do not place year like the other + # Opera/9.80 (Linux armv7l; HbbTV/1.1.1 (; Sony; KDL32W650A; PKG3.211EUA; 2013;); ) Presto/2.12.362 Version/12.11 + # Opera/9.80 (Linux mips; U; HbbTV/1.1.1 (; Sony; KDL40HX751; PKG1.902EUA; 2012;);; en) Presto/2.10.250 Version/11.60 + # Opera/9.80 (Linux mips; U; HbbTV/1.1.1 (; Sony; KDL22EX320; PKG4.017EUA; 2011;);; en) Presto/2.7.61 Version/11.00 + #- regex: 'HbbTV/\d+\.\d+\.\d+ \(; (Sony);.*;.*; ([0-9]{4});\)' + + + # LG is consistent too, but we need to add manually the year model + #Mozilla/5.0 (Unknown; Linux armv7l) AppleWebKit/537.1+ (KHTML, like Gecko) Safari/537.1+ HbbTV/1.1.1 ( ;LGE ;NetCast 4.0 ;03.20.30 ;1.0M ;) + #Mozilla/5.0 (DirectFB; Linux armv7l) AppleWebKit/534.26+ (KHTML, like Gecko) Version/5.0 Safari/534.26+ HbbTV/1.1.1 ( ;LGE ;NetCast 3.0 ;1.0 ;1.0M ;) + - regex: 'HbbTV/\d+\.\d+\.\d+ \( ;(LG)E ;NetCast 4.0' + os_v1_replacement: '2013' + - regex: 'HbbTV/\d+\.\d+\.\d+ \( ;(LG)E ;NetCast 3.0' + os_v1_replacement: '2012' + + # Samsung is on its way of normalizing their user-agent + # HbbTV/1.1.1 (;Samsung;SmartTV2013;T-FXPDEUC-1102.2;;) WebKit + # HbbTV/1.1.1 (;Samsung;SmartTV2013;T-MST12DEUC-1102.1;;) WebKit + # HbbTV/1.1.1 (;Samsung;SmartTV2012;;;) WebKit + # HbbTV/1.1.1 (;;;;;) Maple_2011 + - regex: 'HbbTV/1.1.1 \(;;;;;\) Maple_2011' + os_replacement: 'Samsung' + os_v1_replacement: '2011' + # manage the two models of 2013 + - regex: 'HbbTV/\d+\.\d+\.\d+ \(;(Samsung);SmartTV([0-9]{4});.*FXPDEUC' + os_v2_replacement: 'UE40F7000' + - regex: 'HbbTV/\d+\.\d+\.\d+ \(;(Samsung);SmartTV([0-9]{4});.*MST12DEUC' + os_v2_replacement: 'UE32F4500' + # generic Samsung (works starting in 2012) + #- regex: 'HbbTV/\d+\.\d+\.\d+ \(;(Samsung);SmartTV([0-9]{4});' + + # Philips : not found any other way than a manual mapping + # Opera/9.80 (Linux mips; U; HbbTV/1.1.1 (; Philips; ; ; ; ) CE-HTML/1.0 NETTV/4.1.3 PHILIPSTV/1.1.1; en) Presto/2.10.250 Version/11.60 + # Opera/9.80 (Linux mips ; U; HbbTV/1.1.1 (; Philips; ; ; ; ) CE-HTML/1.0 NETTV/3.2.1; en) Presto/2.6.33 Version/10.70 + - regex: 'HbbTV/1.1.1 \(; (Philips);.*NETTV/4' + os_v1_replacement: '2013' + - regex: 'HbbTV/1.1.1 \(; (Philips);.*NETTV/3' + os_v1_replacement: '2012' + - regex: 'HbbTV/1.1.1 \(; (Philips);.*NETTV/2' + os_v1_replacement: '2011' + + # the HbbTV emulator developers use HbbTV/1.1.1 (;;;;;) firetv-firefox-plugin 1.1.20 + - regex: 'HbbTV/\d+\.\d+\.\d+.*(firetv)-firefox-plugin (\d+).(\d+).(\d+)' + os_replacement: 'FireHbbTV' + + # generic HbbTV, hoping to catch manufacturer name (always after 2nd comma) and the first string that looks like a 2011-2019 year + - regex: 'HbbTV/\d+\.\d+\.\d+ \(.*; ?([a-zA-Z]+) ?;.*(201[1-9]).*\)' + + ########## + # @note: Windows Phone needs to come before Windows NT 6.1 *and* before Android to catch cases such as: + # Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 920)... + # Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 920; ANZ821)... + # Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 920; Orange)... + # Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 920; Vodafone)... + ########## + + - regex: '(Windows Phone) (?:OS[ /])?(\d+)\.(\d+)' + + ########## + # Android + # can actually detect rooted android os. do we care? + ########## + - regex: '(Android)[ \-/](\d+)\.(\d+)(?:[.\-]([a-z0-9]+))?' + + - regex: '(Android) Donut' + os_v1_replacement: '1' + os_v2_replacement: '2' + + - regex: '(Android) Eclair' + os_v1_replacement: '2' + os_v2_replacement: '1' + + - regex: '(Android) Froyo' + os_v1_replacement: '2' + os_v2_replacement: '2' + + - regex: '(Android) Gingerbread' + os_v1_replacement: '2' + os_v2_replacement: '3' + + - regex: '(Android) Honeycomb' + os_v1_replacement: '3' + + # UCWEB + - regex: '^UCWEB.*; (Adr) (\d+)\.(\d+)(?:[.\-]([a-z0-9]+))?;' + os_replacement: 'Android' + - regex: '^UCWEB.*; (iPad OS|iPh OS) (\d+)_(\d+)(?:_(\d+))?;' + os_replacement: 'iOS' + - regex: '^UCWEB.*; (wds) (\d+)\.(\d+)(?:\.(\d+))?;' + os_replacement: 'Windows Phone' + # JUC + - regex: '^(JUC).*; ?U; ?(?:Android)?(\d+)\.(\d+)(?:[\.\-]([a-z0-9]+))?' + os_replacement: 'Android' + + ########## + # Kindle Android + ########## + - regex: '(Silk-Accelerated=[a-z]{4,5})' + os_replacement: 'Android' + + ########## + # Windows + # http://en.wikipedia.org/wiki/Windows_NT#Releases + # possibility of false positive when different marketing names share same NT kernel + # e.g. windows server 2003 and windows xp + # lots of ua strings have Windows NT 4.1 !?!?!?!? !?!? !? !????!?! !!! ??? !?!?! ? + # (very) roughly ordered in terms of frequency of occurence of regex (win xp currently most frequent, etc) + ########## + + # ie mobile desktop mode + # spoofs nt 6.1. must come before windows 7 + - regex: '(XBLWP7)' + os_replacement: 'Windows Phone' + + # @note: This needs to come before Windows NT 6.1 + - regex: '(Windows ?Mobile)' + os_replacement: 'Windows Mobile' + + - regex: '(Windows (?:NT 5\.2|NT 5\.1))' + os_replacement: 'Windows XP' + + - regex: '(Windows NT 6\.1)' + os_replacement: 'Windows 7' + + - regex: '(Windows NT 6\.0)' + os_replacement: 'Windows Vista' + + - regex: '(Win 9x 4\.90)' + os_replacement: 'Windows ME' + + - regex: '(Windows 98|Windows XP|Windows ME|Windows 95|Windows CE|Windows 7|Windows NT 4\.0|Windows Vista|Windows 2000|Windows 3.1)' + + - regex: '(Windows NT 6\.2; ARM;)' + os_replacement: 'Windows RT' + - regex: '(Windows NT 6\.2)' + os_replacement: 'Windows 8' + + - regex: '(Windows NT 6\.3; ARM;)' + os_replacement: 'Windows RT 8.1' + - regex: '(Windows NT 6\.3)' + os_replacement: 'Windows 8.1' + + - regex: '(Windows NT 6\.4)' + os_replacement: 'Windows 10' + - regex: '(Windows NT 10\.0)' + os_replacement: 'Windows 10' + + - regex: '(Windows NT 5\.0)' + os_replacement: 'Windows 2000' + + - regex: '(WinNT4.0)' + os_replacement: 'Windows NT 4.0' + + - regex: '(Windows ?CE)' + os_replacement: 'Windows CE' + + - regex: 'Win ?(95|98|3.1|NT|ME|2000)' + os_replacement: 'Windows $1' + + - regex: 'Win16' + os_replacement: 'Windows 3.1' + + - regex: 'Win32' + os_replacement: 'Windows 95' + + ########## + # Tizen OS from Samsung + # spoofs Android so pushing it above + ########## + - regex: '(Tizen)/(\d+)\.(\d+)' + + ########## + # Mac OS + # @ref: http://en.wikipedia.org/wiki/Mac_OS_X#Versions + # @ref: http://www.puredarwin.org/curious/versions + ########## + - regex: '((?:Mac ?|; )OS X)[\s/](?:(\d+)[_.](\d+)(?:[_.](\d+))?|Mach-O)' + os_replacement: 'Mac OS X' + # Leopard + - regex: ' (Dar)(win)/(9).(\d+).*\((?:i386|x86_64|Power Macintosh)\)' + os_replacement: 'Mac OS X' + os_v1_replacement: '10' + os_v2_replacement: '5' + # Snow Leopard + - regex: ' (Dar)(win)/(10).(\d+).*\((?:i386|x86_64)\)' + os_replacement: 'Mac OS X' + os_v1_replacement: '10' + os_v2_replacement: '6' + # Lion + - regex: ' (Dar)(win)/(11).(\d+).*\((?:i386|x86_64)\)' + os_replacement: 'Mac OS X' + os_v1_replacement: '10' + os_v2_replacement: '7' + # Mountain Lion + - regex: ' (Dar)(win)/(12).(\d+).*\((?:i386|x86_64)\)' + os_replacement: 'Mac OS X' + os_v1_replacement: '10' + os_v2_replacement: '8' + # Mavericks + - regex: ' (Dar)(win)/(13).(\d+).*\((?:i386|x86_64)\)' + os_replacement: 'Mac OS X' + os_v1_replacement: '10' + os_v2_replacement: '9' + # Yosemite is Darwin/14.x but patch versions are inconsistent in the Darwin string; + # more accurately covered by CFNetwork regexes downstream + + # IE on Mac doesn't specify version number + - regex: 'Mac_PowerPC' + os_replacement: 'Mac OS' + + # builds before tiger don't seem to specify version? + + # ios devices spoof (mac os x), so including intel/ppc prefixes + - regex: '(?:PPC|Intel) (Mac OS X)' + + ########## + # iOS + # http://en.wikipedia.org/wiki/IOS_version_history + ########## + # keep this above generic iOS, since AppleTV UAs contain 'CPU OS' + - regex: '(Apple\s?TV)(?:/(\d+)\.(\d+))?' + os_replacement: 'ATV OS X' + + - regex: '(CPU OS|iPhone OS|CPU iPhone) +(\d+)[_\.](\d+)(?:[_\.](\d+))?' + os_replacement: 'iOS' + + # remaining cases are mostly only opera uas, so catch opera as to not catch iphone spoofs + - regex: '(iPhone|iPad|iPod); Opera' + os_replacement: 'iOS' + + # few more stragglers + - regex: '(iPhone|iPad|iPod).*Mac OS X.*Version/(\d+)\.(\d+)' + os_replacement: 'iOS' + + # CFNetwork/Darwin - The specific CFNetwork or Darwin version determines + # whether the os maps to Mac OS, or iOS, or just Darwin. + # See: http://user-agents.me/cfnetwork-version-list + - regex: '(CFNetwork)/(5)48\.0\.3.* Darwin/11\.0\.0' + os_replacement: 'iOS' + - regex: '(CFNetwork)/(5)48\.(0)\.4.* Darwin/(1)1\.0\.0' + os_replacement: 'iOS' + - regex: '(CFNetwork)/(5)48\.(1)\.4' + os_replacement: 'iOS' + - regex: '(CFNetwork)/(4)85\.1(3)\.9' + os_replacement: 'iOS' + - regex: '(CFNetwork)/(6)09\.(1)\.4' + os_replacement: 'iOS' + - regex: '(CFNetwork)/(6)(0)9' + os_replacement: 'iOS' + - regex: '(CFNetwork)/6(7)2\.(1)\.13' + os_replacement: 'iOS' + - regex: '(CFNetwork)/6(7)2\.(1)\.(1)4' + os_replacement: 'iOS' + - regex: '(CF)(Network)/6(7)(2)\.1\.15' + os_replacement: 'iOS' + os_v1_replacement: '7' + os_v2_replacement: '1' + - regex: '(CFNetwork)/6(7)2\.(0)\.(?:2|8)' + os_replacement: 'iOS' + - regex: '(CFNetwork)/709\.1' + os_replacement: 'iOS' + os_v1_replacement: '8' + os_v2_replacement: '0.b5' + - regex: '(CF)(Network)/711\.(\d)' + os_replacement: 'iOS' + os_v1_replacement: '8' + - regex: '(CF)(Network)/(720)\.(\d)' + os_replacement: 'Mac OS X' + os_v1_replacement: '10' + os_v2_replacement: '10' + - regex: '(CF)(Network)/758\.(\d)' + os_replacement: 'iOS' + os_v1_replacement: '9' + + ########## + # CFNetwork iOS Apps + # @ref: https://en.wikipedia.org/wiki/Darwin_(operating_system)#Release_history + ########## + - regex: 'CFNetwork/.* Darwin/(9)\.\d+' + os_replacement: 'iOS' + os_v1_replacement: '1' + - regex: 'CFNetwork/.* Darwin/(10)\.\d+' + os_replacement: 'iOS' + os_v1_replacement: '4' + - regex: 'CFNetwork/.* Darwin/(11)\.\d+' + os_replacement: 'iOS' + os_v1_replacement: '5' + - regex: 'CFNetwork/.* Darwin/(13)\.\d+' + os_replacement: 'iOS' + os_v1_replacement: '6' + - regex: 'CFNetwork/6.* Darwin/(14)\.\d+' + os_replacement: 'iOS' + os_v1_replacement: '7' + - regex: 'CFNetwork/7.* Darwin/(14)\.\d+' + os_replacement: 'iOS' + os_v1_replacement: '8' + os_v2_replacement: '0' + - regex: 'CFNetwork/7.* Darwin/(15)\.\d+' + os_replacement: 'iOS' + os_v1_replacement: '9' + os_v2_replacement: '0' + # iOS Apps + - regex: '\b(iOS[ /]|iPhone(?:/| v|[ _]OS[/,]|; | OS : |\d,\d/|\d,\d; )|iPad/)(\d{1,2})[_\.](\d{1,2})(?:[_\.](\d+))?' + os_replacement: 'iOS' + + ########## + # Apple TV + ########## + - regex: '(tvOS)/(\d+).(\d+)' + os_replacement: 'tvOS' + + ########## + # Chrome OS + # if version 0.0.0, probably this stuff: + # http://code.google.com/p/chromium-os/issues/detail?id=11573 + # http://code.google.com/p/chromium-os/issues/detail?id=13790 + ########## + - regex: '(CrOS) [a-z0-9_]+ (\d+)\.(\d+)(?:\.(\d+))?' + os_replacement: 'Chrome OS' + + ########## + # Linux distros + ########## + - regex: '([Dd]ebian)' + os_replacement: 'Debian' + - regex: '(Linux Mint)(?:/(\d+))?' + - regex: '(Mandriva)(?: Linux)?/(?:[\d.-]+m[a-z]{2}(\d+).(\d))?' + + ########## + # Symbian + Symbian OS + # http://en.wikipedia.org/wiki/History_of_Symbian + ########## + - regex: '(Symbian[Oo][Ss])[/ ](\d+)\.(\d+)' + os_replacement: 'Symbian OS' + - regex: '(Symbian/3).+NokiaBrowser/7\.3' + os_replacement: 'Symbian^3 Anna' + - regex: '(Symbian/3).+NokiaBrowser/7\.4' + os_replacement: 'Symbian^3 Belle' + - regex: '(Symbian/3)' + os_replacement: 'Symbian^3' + - regex: '\b(Series 60|SymbOS|S60Version|S60V\d|S60\b)' + os_replacement: 'Symbian OS' + - regex: '(MeeGo)' + - regex: 'Symbian [Oo][Ss]' + os_replacement: 'Symbian OS' + - regex: 'Series40;' + os_replacement: 'Nokia Series 40' + - regex: 'Series30Plus;' + os_replacement: 'Nokia Series 30 Plus' + + ########## + # BlackBerry devices + ########## + - regex: '(BB10);.+Version/(\d+)\.(\d+)\.(\d+)' + os_replacement: 'BlackBerry OS' + - regex: '(Black[Bb]erry)[0-9a-z]+/(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?' + os_replacement: 'BlackBerry OS' + - regex: '(Black[Bb]erry).+Version/(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?' + os_replacement: 'BlackBerry OS' + - regex: '(RIM Tablet OS) (\d+)\.(\d+)\.(\d+)' + os_replacement: 'BlackBerry Tablet OS' + - regex: '(Play[Bb]ook)' + os_replacement: 'BlackBerry Tablet OS' + - regex: '(Black[Bb]erry)' + os_replacement: 'BlackBerry OS' + + ########## + # Firefox OS + ########## + - regex: '\((?:Mobile|Tablet);.+Gecko/18.0 Firefox/\d+\.\d+' + os_replacement: 'Firefox OS' + os_v1_replacement: '1' + os_v2_replacement: '0' + os_v3_replacement: '1' + + - regex: '\((?:Mobile|Tablet);.+Gecko/18.1 Firefox/\d+\.\d+' + os_replacement: 'Firefox OS' + os_v1_replacement: '1' + os_v2_replacement: '1' + + - regex: '\((?:Mobile|Tablet);.+Gecko/26.0 Firefox/\d+\.\d+' + os_replacement: 'Firefox OS' + os_v1_replacement: '1' + os_v2_replacement: '2' + + - regex: '\((?:Mobile|Tablet);.+Gecko/28.0 Firefox/\d+\.\d+' + os_replacement: 'Firefox OS' + os_v1_replacement: '1' + os_v2_replacement: '3' + + - regex: '\((?:Mobile|Tablet);.+Gecko/30.0 Firefox/\d+\.\d+' + os_replacement: 'Firefox OS' + os_v1_replacement: '1' + os_v2_replacement: '4' + + - regex: '\((?:Mobile|Tablet);.+Gecko/32.0 Firefox/\d+\.\d+' + os_replacement: 'Firefox OS' + os_v1_replacement: '2' + os_v2_replacement: '0' + + - regex: '\((?:Mobile|Tablet);.+Gecko/34.0 Firefox/\d+\.\d+' + os_replacement: 'Firefox OS' + os_v1_replacement: '2' + os_v2_replacement: '1' + + # Firefox OS Generic + - regex: '\((?:Mobile|Tablet);.+Firefox/\d+\.\d+' + os_replacement: 'Firefox OS' + + + ########## + # BREW + # yes, Brew is lower-cased for Brew MP + ########## + - regex: '(BREW)[ /](\d+)\.(\d+)\.(\d+)' + - regex: '(BREW);' + - regex: '(Brew MP|BMP)[ /](\d+)\.(\d+)\.(\d+)' + os_replacement: 'Brew MP' + - regex: 'BMP;' + os_replacement: 'Brew MP' + + ########## + # Google TV + ########## + - regex: '(GoogleTV)(?: (\d+)\.(\d+)(?:\.(\d+))?|/[\da-z]+)' + + - regex: '(WebTV)/(\d+).(\d+)' + + ########## + # Misc mobile + ########## + - regex: '(hpw|web)OS/(\d+)\.(\d+)(?:\.(\d+))?' + os_replacement: 'webOS' + - regex: '(VRE);' + + ########## + # Generic patterns + # since the majority of os cases are very specific, these go last + ########## + - regex: '(Fedora|Red Hat|PCLinuxOS|Puppy|Ubuntu|Kindle|Bada|Lubuntu|BackTrack|Slackware|(?:Free|Open|Net|\b)BSD)[/ ](\d+)\.(\d+)(?:\.(\d+)(?:\.(\d+))?)?' + + # Gentoo Linux + Kernel Version + - regex: '(Linux)[ /](\d+)\.(\d+)(?:\.(\d+))?.*gentoo' + os_replacement: 'Gentoo' + + # Opera Mini Bada + - regex: '\((Bada);' + + # just os + - regex: '(Windows|Android|WeTab|Maemo)' + - regex: '(Ubuntu|Kubuntu|Arch Linux|CentOS|Slackware|Gentoo|openSUSE|SUSE|Red Hat|Fedora|PCLinuxOS|Mageia|(?:Free|Open|Net|\b)BSD)' + # Linux + Kernel Version + - regex: '(Linux)(?:[ /](\d+)\.(\d+)(?:\.(\d+))?)?' + - regex: 'SunOS' + os_replacement: 'Solaris' + + # Roku Digital-Video-Players https://www.roku.com/ + - regex: '^(Roku)/DVP-(\d+)\.(\d+)' + +device_parsers: + + ######### + # Mobile Spiders + # Catch the mobile crawler before checking for iPhones / Androids. + ######### + - regex: '(?:(?:iPhone|Windows CE|Android).*(?:(?:Bot|Yeti)-Mobile|YRSpider|bots?/\d|(?:bot|spider)\.html)|AdsBot-Google-Mobile.*iPhone)' + regex_flag: 'i' + device_replacement: 'Spider' + brand_replacement: 'Spider' + model_replacement: 'Smartphone' + - regex: '(?:DoCoMo|\bMOT\b|\bLG\b|Nokia|Samsung|SonyEricsson).*(?:(?:Bot|Yeti)-Mobile|bots?/\d|(?:bot|crawler)\.html|(?:jump|google|Wukong)bot|ichiro/mobile|/spider|YahooSeeker)' + regex_flag: 'i' + device_replacement: 'Spider' + brand_replacement: 'Spider' + model_replacement: 'Feature Phone' + + ######### + # WebBrowser for SmartWatch + # @ref: https://play.google.com/store/apps/details?id=se.vaggan.webbrowser&hl=en + ######### + - regex: '\bSmartWatch *\( *([^;]+) *; *([^;]+) *;' + device_replacement: '$1 $2' + brand_replacement: '$1' + model_replacement: '$2' + + ###################################################################### + # Android parsers + # + # @ref: https://support.google.com/googleplay/answer/1727131?hl=en + ###################################################################### + + # Android Application + - regex: 'Android Application[^\-]+ - (Sony) ?(Ericsson)? (.+) \w+ - ' + device_replacement: '$1 $2' + brand_replacement: '$1$2' + model_replacement: '$3' + - regex: 'Android Application[^\-]+ - (?:HTC|HUAWEI|LGE|LENOVO|MEDION|TCT) (HTC|HUAWEI|LG|LENOVO|MEDION|ALCATEL)[ _\-](.+) \w+ - ' + regex_flag: 'i' + device_replacement: '$1 $2' + brand_replacement: '$1' + model_replacement: '$2' + - regex: 'Android Application[^\-]+ - ([^ ]+) (.+) \w+ - ' + device_replacement: '$1 $2' + brand_replacement: '$1' + model_replacement: '$2' + + ######### + # 3Q + # @ref: http://www.3q-int.com/ + ######### + - regex: '; *([BLRQ]C\d{4}[A-Z]+) +Build/' + device_replacement: '3Q $1' + brand_replacement: '3Q' + model_replacement: '$1' + - regex: '; *(?:3Q_)([^;/]+) +Build' + device_replacement: '3Q $1' + brand_replacement: '3Q' + model_replacement: '$1' + + ######### + # Acer + # @ref: http://us.acer.com/ac/en/US/content/group/tablets + ######### + - regex: 'Android [34].*; *(A100|A101|A110|A200|A210|A211|A500|A501|A510|A511|A700(?: Lite| 3G)?|A701|B1-A71|A1-\d{3}|B1-\d{3}|V360|V370|W500|W500P|W501|W501P|W510|W511|W700|Slider SL101|DA22[^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Acer' + model_replacement: '$1' + - regex: '; *Acer Iconia Tab ([^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Acer' + model_replacement: '$1' + - regex: '; *(Z1[1235]0|E320[^/]*|S500|S510|Liquid[^;/]*|Iconia A\d+) Build' + device_replacement: '$1' + brand_replacement: 'Acer' + model_replacement: '$1' + - regex: '; *(Acer |ACER )([^;/]+) Build' + device_replacement: '$1$2' + brand_replacement: 'Acer' + model_replacement: '$2' + + ######### + # Advent + # @ref: https://en.wikipedia.org/wiki/Advent_Vega + # @note: VegaBean and VegaComb (names derived from jellybean, honeycomb) are + # custom ROM builds for Vega + ######### + - regex: '; *(Advent )?(Vega(?:Bean|Comb)?).* Build' + device_replacement: '$1$2' + brand_replacement: 'Advent' + model_replacement: '$2' + + ######### + # Ainol + # @ref: http://www.ainol.com/plugin.php?identifier=ainol&module=product + ######### + - regex: '; *(Ainol )?((?:NOVO|[Nn]ovo)[^;/]+) Build' + device_replacement: '$1$2' + brand_replacement: 'Ainol' + model_replacement: '$2' + + ######### + # Airis + # @ref: http://airis.es/Tienda/Default.aspx?idG=001 + ######### + - regex: '; *AIRIS[ _\-]?([^/;\)]+) *(?:;|\)|Build)' + regex_flag: 'i' + device_replacement: '$1' + brand_replacement: 'Airis' + model_replacement: '$1' + - regex: '; *(OnePAD[^;/]+) Build' + regex_flag: 'i' + device_replacement: '$1' + brand_replacement: 'Airis' + model_replacement: '$1' + + ######### + # Airpad + # @ref: ?? + ######### + - regex: '; *Airpad[ \-]([^;/]+) Build' + device_replacement: 'Airpad $1' + brand_replacement: 'Airpad' + model_replacement: '$1' + + ######### + # Alcatel - TCT + # @ref: http://www.alcatelonetouch.com/global-en/products/smartphones.html + ######### + - regex: '; *(one ?touch) (EVO7|T10|T20) Build' + device_replacement: 'Alcatel One Touch $2' + brand_replacement: 'Alcatel' + model_replacement: 'One Touch $2' + - regex: '; *(?:alcatel[ _])?(?:(?:one[ _]?touch[ _])|ot[ \-])([^;/]+);? Build' + regex_flag: 'i' + device_replacement: 'Alcatel One Touch $1' + brand_replacement: 'Alcatel' + model_replacement: 'One Touch $1' + - regex: '; *(TCL)[ _]([^;/]+) Build' + device_replacement: '$1 $2' + brand_replacement: '$1' + model_replacement: '$2' + # operator specific models + - regex: '; *(Vodafone Smart II|Optimus_Madrid) Build' + device_replacement: 'Alcatel $1' + brand_replacement: 'Alcatel' + model_replacement: '$1' + - regex: '; *BASE_Lutea_3 Build' + device_replacement: 'Alcatel One Touch 998' + brand_replacement: 'Alcatel' + model_replacement: 'One Touch 998' + - regex: '; *BASE_Varia Build' + device_replacement: 'Alcatel One Touch 918D' + brand_replacement: 'Alcatel' + model_replacement: 'One Touch 918D' + + ######### + # Allfine + # @ref: http://www.myallfine.com/Products.asp + ######### + - regex: '; *((?:FINE|Fine)\d[^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Allfine' + model_replacement: '$1' + + ######### + # Allview + # @ref: http://www.allview.ro/produse/droseries/lista-tablete-pc/ + ######### + - regex: '; *(ALLVIEW[ _]?|Allview[ _]?)((?:Speed|SPEED).*) Build/' + device_replacement: '$1$2' + brand_replacement: 'Allview' + model_replacement: '$2' + - regex: '; *(ALLVIEW[ _]?|Allview[ _]?)?(AX1_Shine|AX2_Frenzy) Build' + device_replacement: '$1$2' + brand_replacement: 'Allview' + model_replacement: '$2' + - regex: '; *(ALLVIEW[ _]?|Allview[ _]?)([^;/]*) Build' + device_replacement: '$1$2' + brand_replacement: 'Allview' + model_replacement: '$2' + + ######### + # Allwinner + # @ref: http://www.allwinner.com/ + # @models: A31 (13.3"),A20,A10, + ######### + - regex: '; *(A13-MID) Build' + device_replacement: '$1' + brand_replacement: 'Allwinner' + model_replacement: '$1' + - regex: '; *(Allwinner)[ _\-]?([^;/]+) Build' + device_replacement: '$1 $2' + brand_replacement: 'Allwinner' + model_replacement: '$1' + + ######### + # Amaway + # @ref: http://www.amaway.cn/ + ######### + - regex: '; *(A651|A701B?|A702|A703|A705|A706|A707|A711|A712|A713|A717|A722|A785|A801|A802|A803|A901|A902|A1002|A1003|A1006|A1007|A9701|A9703|Q710|Q80) Build' + device_replacement: '$1' + brand_replacement: 'Amaway' + model_replacement: '$1' + + ######### + # Amoi + # @ref: http://www.amoi.com/en/prd/prd_index.jspx + ######### + - regex: '; *(?:AMOI|Amoi)[ _]([^;/]+) Build' + device_replacement: 'Amoi $1' + brand_replacement: 'Amoi' + model_replacement: '$1' + - regex: '^(?:AMOI|Amoi)[ _]([^;/]+) Linux' + device_replacement: 'Amoi $1' + brand_replacement: 'Amoi' + model_replacement: '$1' + + ######### + # Aoc + # @ref: http://latin.aoc.com/media_tablet + ######### + - regex: '; *(MW(?:0[789]|10)[^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Aoc' + model_replacement: '$1' + + ######### + # Aoson + # @ref: http://www.luckystar.com.cn/en/mid.aspx?page=1 + # @ref: http://www.luckystar.com.cn/en/mobiletel.aspx?page=1 + # @note: brand owned by luckystar + ######### + - regex: '; *(G7|M1013|M1015G|M11[CG]?|M-?12[B]?|M15|M19[G]?|M30[ACQ]?|M31[GQ]|M32|M33[GQ]|M36|M37|M38|M701T|M710|M712B|M713|M715G|M716G|M71(?:G|GS|T)?|M72[T]?|M73[T]?|M75[GT]?|M77G|M79T|M7L|M7LN|M81|M810|M81T|M82|M92|M92KS|M92S|M717G|M721|M722G|M723|M725G|M739|M785|M791|M92SK|M93D) Build' + device_replacement: 'Aoson $1' + brand_replacement: 'Aoson' + model_replacement: '$1' + - regex: '; *Aoson ([^;/]+) Build' + regex_flag: 'i' + device_replacement: 'Aoson $1' + brand_replacement: 'Aoson' + model_replacement: '$1' + + ######### + # Apanda + # @ref: http://www.apanda.com.cn/ + ######### + - regex: '; *[Aa]panda[ _\-]([^;/]+) Build' + device_replacement: 'Apanda $1' + brand_replacement: 'Apanda' + model_replacement: '$1' + + ######### + # Archos + # @ref: http://www.archos.com/de/products/tablets.html + # @ref: http://www.archos.com/de/products/smartphones/index.html + ######### + - regex: '; *(?:ARCHOS|Archos) ?(GAMEPAD.*?)(?: Build|[;/\(\)\-])' + device_replacement: 'Archos $1' + brand_replacement: 'Archos' + model_replacement: '$1' + - regex: 'ARCHOS; GOGI; ([^;]+);' + device_replacement: 'Archos $1' + brand_replacement: 'Archos' + model_replacement: '$1' + - regex: '(?:ARCHOS|Archos)[ _]?(.*?)(?: Build|[;/\(\)\-]|$)' + device_replacement: 'Archos $1' + brand_replacement: 'Archos' + model_replacement: '$1' + - regex: '; *(AN(?:7|8|9|10|13)[A-Z0-9]{1,4}) Build' + device_replacement: 'Archos $1' + brand_replacement: 'Archos' + model_replacement: '$1' + - regex: '; *(A28|A32|A43|A70(?:BHT|CHT|HB|S|X)|A101(?:B|C|IT)|A7EB|A7EB-WK|101G9|80G9) Build' + device_replacement: 'Archos $1' + brand_replacement: 'Archos' + model_replacement: '$1' + + ######### + # A-rival + # @ref: http://www.a-rival.de/de/ + ######### + - regex: '; *(PAD-FMD[^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Arival' + model_replacement: '$1' + - regex: '; *(BioniQ) ?([^;/]+) Build' + device_replacement: '$1 $2' + brand_replacement: 'Arival' + model_replacement: '$1 $2' + + ######### + # Arnova + # @ref: http://arnovatech.com/ + ######### + - regex: '; *(AN\d[^;/]+|ARCHM\d+) Build' + device_replacement: 'Arnova $1' + brand_replacement: 'Arnova' + model_replacement: '$1' + - regex: '; *(?:ARNOVA|Arnova) ?([^;/]+) Build' + device_replacement: 'Arnova $1' + brand_replacement: 'Arnova' + model_replacement: '$1' + + ######### + # Assistant + # @ref: http://www.assistant.ua + ######### + - regex: '; *(?:ASSISTANT )?(AP)-?([1789]\d{2}[A-Z]{0,2}|80104) Build' + device_replacement: 'Assistant $1-$2' + brand_replacement: 'Assistant' + model_replacement: '$1-$2' + + ######### + # Asus + # @ref: http://www.asus.com/uk/Tablets_Mobile/ + ######### + - regex: '; *(ME17\d[^;/]*|ME3\d{2}[^;/]+|K00[A-Z]|Nexus 10|Nexus 7(?: 2013)?|PadFone[^;/]*|Transformer[^;/]*|TF\d{3}[^;/]*|eeepc) Build' + device_replacement: 'Asus $1' + brand_replacement: 'Asus' + model_replacement: '$1' + - regex: '; *ASUS[ _]*([^;/]+) Build' + device_replacement: 'Asus $1' + brand_replacement: 'Asus' + model_replacement: '$1' + + ######### + # Garmin-Asus + ######### + - regex: '; *Garmin-Asus ([^;/]+) Build' + device_replacement: 'Garmin-Asus $1' + brand_replacement: 'Garmin-Asus' + model_replacement: '$1' + - regex: '; *(Garminfone) Build' + device_replacement: 'Garmin $1' + brand_replacement: 'Garmin-Asus' + model_replacement: '$1' + + ######### + # Attab + # @ref: http://www.theattab.com/ + ######### + - regex: '; (@TAB-[^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Attab' + model_replacement: '$1' + + ######### + # Audiosonic + # @ref: ?? + # @note: Take care with Docomo T-01 Toshiba + ######### + - regex: '; *(T-(?:07|[^0]\d)[^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Audiosonic' + model_replacement: '$1' + + ######### + # Axioo + # @ref: http://www.axiooworld.com/ww/index.php + ######### + - regex: '; *(?:Axioo[ _\-]([^;/]+)|(picopad)[ _\-]([^;/]+)) Build' + regex_flag: 'i' + device_replacement: 'Axioo $1$2 $3' + brand_replacement: 'Axioo' + model_replacement: '$1$2 $3' + + ######### + # Azend + # @ref: http://azendcorp.com/index.php/products/portable-electronics + ######### + - regex: '; *(V(?:100|700|800)[^;/]*) Build' + device_replacement: '$1' + brand_replacement: 'Azend' + model_replacement: '$1' + + ######### + # Bak + # @ref: http://www.bakinternational.com/produtos.php?cat=80 + ######### + - regex: '; *(IBAK\-[^;/]*) Build' + regex_flag: 'i' + device_replacement: '$1' + brand_replacement: 'Bak' + model_replacement: '$1' + + ######### + # Bedove + # @ref: http://www.bedove.com/product.html + # @models: HY6501|HY5001|X12|X21|I5 + ######### + - regex: '; *(HY5001|HY6501|X12|X21|I5) Build' + device_replacement: 'Bedove $1' + brand_replacement: 'Bedove' + model_replacement: '$1' + + ######### + # Benss + # @ref: http://www.benss.net/ + ######### + - regex: '; *(JC-[^;/]*) Build' + device_replacement: 'Benss $1' + brand_replacement: 'Benss' + model_replacement: '$1' + + ######### + # Blackberry + # @ref: http://uk.blackberry.com/ + # @note: Android Apps seams to be used here + ######### + - regex: '; *(BB) ([^;/]+) Build' + device_replacement: '$1 $2' + brand_replacement: 'Blackberry' + model_replacement: '$2' + + ######### + # Blackbird + # @ref: http://iblackbird.co.kr + ######### + - regex: '; *(BlackBird)[ _](I8.*) Build' + device_replacement: '$1 $2' + brand_replacement: '$1' + model_replacement: '$2' + - regex: '; *(BlackBird)[ _](.*) Build' + device_replacement: '$1 $2' + brand_replacement: '$1' + model_replacement: '$2' + + ######### + # Blaupunkt + # @ref: http://www.blaupunkt.com + ######### + # Endeavour + - regex: '; *([0-9]+BP[EM][^;/]*|Endeavour[^;/]+) Build' + device_replacement: 'Blaupunkt $1' + brand_replacement: 'Blaupunkt' + model_replacement: '$1' + + ######### + # Blu + # @ref: http://bluproducts.com + ######### + - regex: '; *((?:BLU|Blu)[ _\-])([^;/]+) Build' + device_replacement: '$1$2' + brand_replacement: 'Blu' + model_replacement: '$2' + # BMOBILE = operator branded device + - regex: '; *(?:BMOBILE )?(Blu|BLU|DASH [^;/]+|VIVO 4\.3|TANK 4\.5) Build' + device_replacement: '$1' + brand_replacement: 'Blu' + model_replacement: '$1' + + ######### + # Blusens + # @ref: http://www.blusens.com/es/?sg=1&sv=al&roc=1 + ######### + # tablet + - regex: '; *(TOUCH\d[^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Blusens' + model_replacement: '$1' + + ######### + # Bmobile + # @ref: http://bmobile.eu.com/?categoria=smartphones-2 + # @note: Might collide with Maxx as AX is used also there. + ######### + # smartphone + - regex: '; *(AX5\d+) Build' + device_replacement: '$1' + brand_replacement: 'Bmobile' + model_replacement: '$1' + + ######### + # bq + # @ref: http://bqreaders.com + ######### + - regex: '; *([Bb]q) ([^;/]+);? Build' + device_replacement: '$1 $2' + brand_replacement: 'bq' + model_replacement: '$2' + - regex: '; *(Maxwell [^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'bq' + model_replacement: '$1' + + ######### + # Braun Phototechnik + # @ref: http://www.braun-phototechnik.de/en/products/list/~pcat.250/Tablet-PC.html + ######### + - regex: '; *((?:B-Tab|B-TAB) ?\d[^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Braun' + model_replacement: '$1' + + ######### + # Broncho + # @ref: http://www.broncho.cn/ + ######### + - regex: '; *(Broncho) ([^;/]+) Build' + device_replacement: '$1 $2' + brand_replacement: '$1' + model_replacement: '$2' + + ######### + # Captiva + # @ref: http://www.captiva-power.de + ######### + - regex: '; *CAPTIVA ([^;/]+) Build' + device_replacement: 'Captiva $1' + brand_replacement: 'Captiva' + model_replacement: '$1' + + ######### + # Casio + # @ref: http://www.casiogzone.com/ + ######### + - regex: '; *(C771|CAL21|IS11CA) Build' + device_replacement: '$1' + brand_replacement: 'Casio' + model_replacement: '$1' + + ######### + # Cat + # @ref: http://www.cat-sound.com + ######### + - regex: '; *(?:Cat|CAT) ([^;/]+) Build' + device_replacement: 'Cat $1' + brand_replacement: 'Cat' + model_replacement: '$1' + - regex: '; *(?:Cat)(Nova.*) Build' + device_replacement: 'Cat $1' + brand_replacement: 'Cat' + model_replacement: '$1' + - regex: '; *(INM8002KP|ADM8000KP_[AB]) Build' + device_replacement: '$1' + brand_replacement: 'Cat' + model_replacement: 'Tablet PHOENIX 8.1J0' + + ######### + # Celkon + # @ref: http://www.celkonmobiles.com/?_a=products + # @models: A10, A19Q, A101, A105, A107, A107\+, A112, A118, A119, A119Q, A15, A19, A20, A200, A220, A225, A22 Race, A27, A58, A59, A60, A62, A63, A64, A66, A67, A69, A75, A77, A79, A8\+, A83, A85, A86, A87, A89 Ultima, A9\+, A90, A900, A95, A97i, A98, AR 40, AR 45, AR 50, ML5 + ######### + - regex: '; *(?:[Cc]elkon[ _\*]|CELKON[ _\*])([^;/\)]+) ?(?:Build|;|\))' + device_replacement: '$1' + brand_replacement: 'Celkon' + model_replacement: '$1' + - regex: 'Build/(?:[Cc]elkon)+_?([^;/_\)]+)' + device_replacement: '$1' + brand_replacement: 'Celkon' + model_replacement: '$1' + - regex: '; *(CT)-?(\d+) Build' + device_replacement: '$1$2' + brand_replacement: 'Celkon' + model_replacement: '$1$2' + # smartphones + - regex: '; *(A19|A19Q|A105|A107[^;/\)]*) ?(?:Build|;|\))' + device_replacement: '$1' + brand_replacement: 'Celkon' + model_replacement: '$1' + + ######### + # ChangJia + # @ref: http://www.cjshowroom.com/eproducts.aspx?classcode=004001001 + # @brief: China manufacturer makes tablets for different small brands + # (eg. http://www.zeepad.net/index.html) + ######### + - regex: '; *(TPC[0-9]{4,5}) Build' + device_replacement: '$1' + brand_replacement: 'ChangJia' + model_replacement: '$1' + + ######### + # Cloudfone + # @ref: http://www.cloudfonemobile.com/ + ######### + - regex: '; *(Cloudfone)[ _](Excite)([^ ][^;/]+) Build' + device_replacement: '$1 $2 $3' + brand_replacement: 'Cloudfone' + model_replacement: '$1 $2 $3' + - regex: '; *(Excite|ICE)[ _](\d+[^;/]+) Build' + device_replacement: 'Cloudfone $1 $2' + brand_replacement: 'Cloudfone' + model_replacement: 'Cloudfone $1 $2' + - regex: '; *(Cloudfone|CloudPad)[ _]([^;/]+) Build' + device_replacement: '$1 $2' + brand_replacement: 'Cloudfone' + model_replacement: '$1 $2' + + ######### + # Cmx + # @ref: http://cmx.at/de/ + ######### + - regex: '; *((?:Aquila|Clanga|Rapax)[^;/]+) Build' + regex_flag: 'i' + device_replacement: '$1' + brand_replacement: 'Cmx' + model_replacement: '$1' + + ######### + # CobyKyros + # @ref: http://cobykyros.com + # @note: Be careful with MID\d{3} from MpMan or Manta + ######### + - regex: '; *(?:CFW-|Kyros )?(MID[0-9]{4}(?:[ABC]|SR|TV)?)(\(3G\)-4G| GB 8K| 3G| 8K| GB)? *(?:Build|[;\)])' + device_replacement: 'CobyKyros $1$2' + brand_replacement: 'CobyKyros' + model_replacement: '$1$2' + + ######### + # Coolpad + # @ref: ?? + ######### + - regex: '; *([^;/]*)Coolpad[ _]([^;/]+) Build' + device_replacement: '$1$2' + brand_replacement: 'Coolpad' + model_replacement: '$1$2' + + ######### + # Cube + # @ref: http://www.cube-tablet.com/buy-products.html + ######### + - regex: '; *(CUBE[ _])?([KU][0-9]+ ?GT.*|A5300) Build' + regex_flag: 'i' + device_replacement: '$1$2' + brand_replacement: 'Cube' + model_replacement: '$2' + + ######### + # Cubot + # @ref: http://www.cubotmall.com/ + ######### + - regex: '; *CUBOT ([^;/]+) Build' + regex_flag: 'i' + device_replacement: '$1' + brand_replacement: 'Cubot' + model_replacement: '$1' + - regex: '; *(BOBBY) Build' + regex_flag: 'i' + device_replacement: '$1' + brand_replacement: 'Cubot' + model_replacement: '$1' + + ######### + # Danew + # @ref: http://www.danew.com/produits-tablette.php + ######### + - regex: '; *(Dslide [^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Danew' + model_replacement: '$1' + + ######### + # Dell + # @ref: http://www.dell.com + # @ref: http://www.softbank.jp/mobile/support/product/101dl/ + # @ref: http://www.softbank.jp/mobile/support/product/001dl/ + # @ref: http://developer.emnet.ne.jp/android.html + # @ref: http://www.dell.com/in/p/mobile-xcd28/pd + # @ref: http://www.dell.com/in/p/mobile-xcd35/pd + ######### + - regex: '; *(XCD)[ _]?(28|35) Build' + device_replacement: 'Dell $1$2' + brand_replacement: 'Dell' + model_replacement: '$1$2' + - regex: '; *(001DL) Build' + device_replacement: 'Dell $1' + brand_replacement: 'Dell' + model_replacement: 'Streak' + - regex: '; *(?:Dell|DELL) (Streak) Build' + device_replacement: 'Dell $1' + brand_replacement: 'Dell' + model_replacement: 'Streak' + - regex: '; *(101DL|GS01|Streak Pro[^;/]*) Build' + device_replacement: 'Dell $1' + brand_replacement: 'Dell' + model_replacement: 'Streak Pro' + - regex: '; *([Ss]treak ?7) Build' + device_replacement: 'Dell $1' + brand_replacement: 'Dell' + model_replacement: 'Streak 7' + - regex: '; *(Mini-3iX) Build' + device_replacement: 'Dell $1' + brand_replacement: 'Dell' + model_replacement: '$1' + - regex: '; *(?:Dell|DELL)[ _](Aero|Venue|Thunder|Mini.*|Streak[ _]Pro) Build' + device_replacement: 'Dell $1' + brand_replacement: 'Dell' + model_replacement: '$1' + - regex: '; *Dell[ _]([^;/]+) Build' + device_replacement: 'Dell $1' + brand_replacement: 'Dell' + model_replacement: '$1' + - regex: '; *Dell ([^;/]+) Build' + device_replacement: 'Dell $1' + brand_replacement: 'Dell' + model_replacement: '$1' + + ######### + # Denver + # @ref: http://www.denver-electronics.com/tablets1/ + ######### + - regex: '; *(TA[CD]-\d+[^;/]*) Build' + device_replacement: '$1' + brand_replacement: 'Denver' + model_replacement: '$1' + + ######### + # Dex + # @ref: http://dex.ua/ + ######### + - regex: '; *(iP[789]\d{2}(?:-3G)?|IP10\d{2}(?:-8GB)?) Build' + device_replacement: '$1' + brand_replacement: 'Dex' + model_replacement: '$1' + + ######### + # DNS AirTab + # @ref: http://www.dns-shop.ru/ + ######### + - regex: '; *(AirTab)[ _\-]([^;/]+) Build' + device_replacement: '$1 $2' + brand_replacement: 'DNS' + model_replacement: '$1 $2' + + ######### + # Docomo (Operator Branded Device) + # @ref: http://www.ipentec.com/document/document.aspx?page=android-useragent + ######### + - regex: '; *(F\-\d[^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Fujitsu' + model_replacement: '$1' + - regex: '; *(HT-03A) Build' + device_replacement: '$1' + brand_replacement: 'HTC' + model_replacement: 'Magic' + - regex: '; *(HT\-\d[^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'HTC' + model_replacement: '$1' + - regex: '; *(L\-\d[^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'LG' + model_replacement: '$1' + - regex: '; *(N\-\d[^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Nec' + model_replacement: '$1' + - regex: '; *(P\-\d[^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Panasonic' + model_replacement: '$1' + - regex: '; *(SC\-\d[^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Samsung' + model_replacement: '$1' + - regex: '; *(SH\-\d[^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Sharp' + model_replacement: '$1' + - regex: '; *(SO\-\d[^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'SonyEricsson' + model_replacement: '$1' + - regex: '; *(T\-0[12][^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Toshiba' + model_replacement: '$1' + + ######### + # DOOV + # @ref: http://www.doov.com.cn/ + ######### + - regex: '; *(DOOV)[ _]([^;/]+) Build' + device_replacement: '$1 $2' + brand_replacement: 'DOOV' + model_replacement: '$2' + + ######### + # Enot + # @ref: http://www.enot.ua/ + ######### + - regex: '; *(Enot|ENOT)[ -]?([^;/]+) Build' + device_replacement: '$1 $2' + brand_replacement: 'Enot' + model_replacement: '$2' + + ######### + # Evercoss + # @ref: http://evercoss.com/android/ + ######### + - regex: '; *[^;/]+ Build/(?:CROSS|Cross)+[ _\-]([^\)]+)' + device_replacement: 'CROSS $1' + brand_replacement: 'Evercoss' + model_replacement: 'Cross $1' + - regex: '; *(CROSS|Cross)[ _\-]([^;/]+) Build' + device_replacement: '$1 $2' + brand_replacement: 'Evercoss' + model_replacement: 'Cross $2' + + ######### + # Explay + # @ref: http://explay.ru/ + ######### + - regex: '; *Explay[_ ](.+?)(?:[\)]| Build)' + device_replacement: '$1' + brand_replacement: 'Explay' + model_replacement: '$1' + + ######### + # Fly + # @ref: http://www.fly-phone.com/ + ######### + - regex: '; *(IQ.*) Build' + device_replacement: '$1' + brand_replacement: 'Fly' + model_replacement: '$1' + - regex: '; *(Fly|FLY)[ _](IQ[^;]+|F[34]\d+[^;]*);? Build' + device_replacement: '$1 $2' + brand_replacement: 'Fly' + model_replacement: '$2' + + ######### + # Fujitsu + # @ref: http://www.fujitsu.com/global/ + ######### + - regex: '; *(M532|Q572|FJL21) Build/' + device_replacement: '$1' + brand_replacement: 'Fujitsu' + model_replacement: '$1' + + ######### + # Galapad + # @ref: http://www.galapad.net/product.html + ######### + - regex: '; *(G1) Build' + device_replacement: '$1' + brand_replacement: 'Galapad' + model_replacement: '$1' + + ######### + # Geeksphone + # @ref: http://www.geeksphone.com/ + ######### + - regex: '; *(Geeksphone) ([^;/]+) Build' + device_replacement: '$1 $2' + brand_replacement: '$1' + model_replacement: '$2' + + ######### + # Gfive + # @ref: http://www.gfivemobile.com/en + ######### + #- regex: '; *(G\'?FIVE) ([^;/]+) Build' # there is a problem with python yaml parser here + - regex: '; *(G[^F]?FIVE) ([^;/]+) Build' + device_replacement: '$1 $2' + brand_replacement: 'Gfive' + model_replacement: '$2' + + ######### + # Gionee + # @ref: http://www.gionee.com/ + ######### + - regex: '; *(Gionee)[ _\-]([^;/]+)(?:/[^;/]+)? Build' + regex_flag: 'i' + device_replacement: '$1 $2' + brand_replacement: 'Gionee' + model_replacement: '$2' + - regex: '; *(GN\d+[A-Z]?|INFINITY_PASSION|Ctrl_V1) Build' + device_replacement: 'Gionee $1' + brand_replacement: 'Gionee' + model_replacement: '$1' + - regex: '; *(E3) Build/JOP40D' + device_replacement: 'Gionee $1' + brand_replacement: 'Gionee' + model_replacement: '$1' + + ######### + # GoClever + # @ref: http://www.goclever.com + ######### + - regex: '; *((?:FONE|QUANTUM|INSIGNIA) \d+[^;/]*|PLAYTAB) Build' + device_replacement: 'GoClever $1' + brand_replacement: 'GoClever' + model_replacement: '$1' + - regex: '; *GOCLEVER ([^;/]+) Build' + device_replacement: 'GoClever $1' + brand_replacement: 'GoClever' + model_replacement: '$1' + + ######### + # Google + # @ref: http://www.google.de/glass/start/ + ######### + - regex: '; *(Glass \d+) Build' + device_replacement: '$1' + brand_replacement: 'Google' + model_replacement: '$1' + + ######### + # Gigabyte + # @ref: http://gsmart.gigabytecm.com/en/ + ######### + - regex: '; *(GSmart)[ -]([^/]+) Build' + device_replacement: '$1 $2' + brand_replacement: 'Gigabyte' + model_replacement: '$1 $2' + + ######### + # Freescale development boards + # @ref: http://www.freescale.com/webapp/sps/site/prod_summary.jsp?code=IMX53QSB + ######### + - regex: '; *(imx5[13]_[^/]+) Build' + device_replacement: 'Freescale $1' + brand_replacement: 'Freescale' + model_replacement: '$1' + + ######### + # Haier + # @ref: http://www.haier.com/ + # @ref: http://www.haier.com/de/produkte/tablet/ + ######### + - regex: '; *Haier[ _\-]([^/]+) Build' + device_replacement: 'Haier $1' + brand_replacement: 'Haier' + model_replacement: '$1' + - regex: '; *(PAD1016) Build' + device_replacement: 'Haipad $1' + brand_replacement: 'Haipad' + model_replacement: '$1' + + ######### + # Haipad + # @ref: http://www.haipad.net/ + # @models: V7P|M7SM7S|M9XM9X|M7XM7X|M9|M8|M7-M|M1002|M7|M701 + ######### + - regex: '; *(M701|M7|M8|M9) Build' + device_replacement: 'Haipad $1' + brand_replacement: 'Haipad' + model_replacement: '$1' + + ######### + # Hannspree + # @ref: http://www.hannspree.eu/ + # @models: SN10T1|SN10T2|SN70T31B|SN70T32W + ######### + - regex: '; *(SN\d+T[^;\)/]*)(?: Build|[;\)])' + device_replacement: 'Hannspree $1' + brand_replacement: 'Hannspree' + model_replacement: '$1' + + ######### + # HCLme + # @ref: http://www.hclmetablet.com/india/ + ######### + - regex: 'Build/HCL ME Tablet ([^;\)]+)[\);]' + device_replacement: 'HCLme $1' + brand_replacement: 'HCLme' + model_replacement: '$1' + - regex: '; *([^;\/]+) Build/HCL' + device_replacement: 'HCLme $1' + brand_replacement: 'HCLme' + model_replacement: '$1' + + ######### + # Hena + # @ref: http://www.henadigital.com/en/product/index.asp?id=6 + ######### + - regex: '; *(MID-?\d{4}C[EM]) Build' + device_replacement: 'Hena $1' + brand_replacement: 'Hena' + model_replacement: '$1' + + ######### + # Hisense + # @ref: http://www.hisense.com/ + ######### + - regex: '; *(EG\d{2,}|HS-[^;/]+|MIRA[^;/]+) Build' + device_replacement: 'Hisense $1' + brand_replacement: 'Hisense' + model_replacement: '$1' + - regex: '; *(andromax[^;/]+) Build' + regex_flag: 'i' + device_replacement: 'Hisense $1' + brand_replacement: 'Hisense' + model_replacement: '$1' + + ######### + # hitech + # @ref: http://www.hitech-mobiles.com/ + ######### + - regex: '; *(?:AMAZE[ _](S\d+)|(S\d+)[ _]AMAZE) Build' + device_replacement: 'AMAZE $1$2' + brand_replacement: 'hitech' + model_replacement: 'AMAZE $1$2' + + ######### + # HP + # @ref: http://www.hp.com/ + ######### + - regex: '; *(PlayBook) Build' + device_replacement: 'HP $1' + brand_replacement: 'HP' + model_replacement: '$1' + - regex: '; *HP ([^/]+) Build' + device_replacement: 'HP $1' + brand_replacement: 'HP' + model_replacement: '$1' + - regex: '; *([^/]+_tenderloin) Build' + device_replacement: 'HP TouchPad' + brand_replacement: 'HP' + model_replacement: 'TouchPad' + + ######### + # Huawei + # @ref: http://www.huaweidevice.com + # @note: Needs to be before HTC due to Desire HD Build on U8815 + ######### + - regex: '; *(HUAWEI |Huawei-)?([UY][^;/]+) Build/(?:Huawei|HUAWEI)([UY][^\);]+)\)' + device_replacement: '$1$2' + brand_replacement: 'Huawei' + model_replacement: '$2' + - regex: '; *([^;/]+) Build[/ ]Huawei(MT1-U06|[A-Z]+\d+[^\);]+)[^\);]*\)' + device_replacement: '$1' + brand_replacement: 'Huawei' + model_replacement: '$2' + - regex: '; *(S7|M860) Build' + device_replacement: '$1' + brand_replacement: 'Huawei' + model_replacement: '$1' + - regex: '; *((?:HUAWEI|Huawei)[ \-]?)(MediaPad) Build' + device_replacement: '$1$2' + brand_replacement: 'Huawei' + model_replacement: '$2' + - regex: '; *((?:HUAWEI[ _]?|Huawei[ _])?Ascend[ _])([^;/]+) Build' + device_replacement: '$1$2' + brand_replacement: 'Huawei' + model_replacement: '$2' + - regex: '; *((?:HUAWEI|Huawei)[ _\-]?)((?:G700-|MT-)[^;/]+) Build' + device_replacement: '$1$2' + brand_replacement: 'Huawei' + model_replacement: '$2' + - regex: '; *((?:HUAWEI|Huawei)[ _\-]?)([^;/]+) Build' + device_replacement: '$1$2' + brand_replacement: 'Huawei' + model_replacement: '$2' + - regex: '; *(MediaPad[^;]+|SpringBoard) Build/Huawei' + device_replacement: '$1' + brand_replacement: 'Huawei' + model_replacement: '$1' + - regex: '; *([^;]+) Build/Huawei' + device_replacement: '$1' + brand_replacement: 'Huawei' + model_replacement: '$1' + - regex: '; *([Uu])([89]\d{3}) Build' + device_replacement: '$1$2' + brand_replacement: 'Huawei' + model_replacement: 'U$2' + - regex: '; *(?:Ideos |IDEOS )(S7) Build' + device_replacement: 'Huawei Ideos$1' + brand_replacement: 'Huawei' + model_replacement: 'Ideos$1' + - regex: '; *(?:Ideos |IDEOS )([^;/]+\s*|\s*)Build' + device_replacement: 'Huawei Ideos$1' + brand_replacement: 'Huawei' + model_replacement: 'Ideos$1' + - regex: '; *(Orange Daytona|Pulse|Pulse Mini|Vodafone 858|C8500|C8600|C8650|C8660|Nexus 6P) Build' + device_replacement: 'Huawei $1' + brand_replacement: 'Huawei' + model_replacement: '$1' + + ######### + # HTC + # @ref: http://www.htc.com/www/products/ + # @ref: http://en.wikipedia.org/wiki/List_of_HTC_phones + ######### + + - regex: '; *HTC[ _]([^;]+); Windows Phone' + device_replacement: 'HTC $1' + brand_replacement: 'HTC' + model_replacement: '$1' + + # Android HTC with Version Number matcher + # ; HTC_0P3Z11/1.12.161.3 Build + # ;HTC_A3335 V2.38.841.1 Build + - regex: '; *(?:HTC[ _/])+([^ _/]+)(?:[/\\]1\.0 | V|/| +)\d+\.\d[\d\.]*(?: *Build|\))' + device_replacement: 'HTC $1' + brand_replacement: 'HTC' + model_replacement: '$1' + - regex: '; *(?:HTC[ _/])+([^ _/]+)(?:[ _/]([^ _/]+))?(?:[/\\]1\.0 | V|/| +)\d+\.\d[\d\.]*(?: *Build|\))' + device_replacement: 'HTC $1 $2' + brand_replacement: 'HTC' + model_replacement: '$1 $2' + - regex: '; *(?:HTC[ _/])+([^ _/]+)(?:[ _/]([^ _/]+)(?:[ _/]([^ _/]+))?)?(?:[/\\]1\.0 | V|/| +)\d+\.\d[\d\.]*(?: *Build|\))' + device_replacement: 'HTC $1 $2 $3' + brand_replacement: 'HTC' + model_replacement: '$1 $2 $3' + - regex: '; *(?:HTC[ _/])+([^ _/]+)(?:[ _/]([^ _/]+)(?:[ _/]([^ _/]+)(?:[ _/]([^ _/]+))?)?)?(?:[/\\]1\.0 | V|/| +)\d+\.\d[\d\.]*(?: *Build|\))' + device_replacement: 'HTC $1 $2 $3 $4' + brand_replacement: 'HTC' + model_replacement: '$1 $2 $3 $4' + + # Android HTC without Version Number matcher + - regex: '; *(?:(?:HTC|htc)(?:_blocked)*[ _/])+([^ _/;]+)(?: *Build|[;\)]| - )' + device_replacement: 'HTC $1' + brand_replacement: 'HTC' + model_replacement: '$1' + - regex: '; *(?:(?:HTC|htc)(?:_blocked)*[ _/])+([^ _/]+)(?:[ _/]([^ _/;\)]+))?(?: *Build|[;\)]| - )' + device_replacement: 'HTC $1 $2' + brand_replacement: 'HTC' + model_replacement: '$1 $2' + - regex: '; *(?:(?:HTC|htc)(?:_blocked)*[ _/])+([^ _/]+)(?:[ _/]([^ _/]+)(?:[ _/]([^ _/;\)]+))?)?(?: *Build|[;\)]| - )' + device_replacement: 'HTC $1 $2 $3' + brand_replacement: 'HTC' + model_replacement: '$1 $2 $3' + - regex: '; *(?:(?:HTC|htc)(?:_blocked)*[ _/])+([^ _/]+)(?:[ _/]([^ _/]+)(?:[ _/]([^ _/]+)(?:[ _/]([^ /;]+))?)?)?(?: *Build|[;\)]| - )' + device_replacement: 'HTC $1 $2 $3 $4' + brand_replacement: 'HTC' + model_replacement: '$1 $2 $3 $4' + + # HTC Streaming Player + - regex: 'HTC Streaming Player [^\/]*/[^\/]*/ htc_([^/]+) /' + device_replacement: 'HTC $1' + brand_replacement: 'HTC' + model_replacement: '$1' + # general matcher for anything else + - regex: '(?:[;,] *|^)(?:htccn_chs-)?HTC[ _-]?([^;]+?)(?: *Build|clay|Android|-?Mozilla| Opera| Profile| UNTRUSTED|[;/\(\)]|$)' + regex_flag: 'i' + device_replacement: 'HTC $1' + brand_replacement: 'HTC' + model_replacement: '$1' + # Android matchers without HTC + - regex: '; *(A6277|ADR6200|ADR6300|ADR6350|ADR6400[A-Z]*|ADR6425[A-Z]*|APX515CKT|ARIA|Desire[^_ ]*|Dream|EndeavorU|Eris|Evo|Flyer|HD2|Hero|HERO200|Hero CDMA|HTL21|Incredible|Inspire[A-Z0-9]*|Legend|Liberty|Nexus ?(?:One|HD2)|One|One S C2|One[ _]?(?:S|V|X\+?)\w*|PC36100|PG06100|PG86100|S31HT|Sensation|Wildfire)(?: Build|[/;\(\)])' + regex_flag: 'i' + device_replacement: 'HTC $1' + brand_replacement: 'HTC' + model_replacement: '$1' + - regex: '; *(ADR6200|ADR6400L|ADR6425LVW|Amaze|DesireS?|EndeavorU|Eris|EVO|Evo\d[A-Z]+|HD2|IncredibleS?|Inspire[A-Z0-9]*|Inspire[A-Z0-9]*|Sensation[A-Z0-9]*|Wildfire)[ _-](.+?)(?:[/;\)]|Build|MIUI|1\.0)' + regex_flag: 'i' + device_replacement: 'HTC $1 $2' + brand_replacement: 'HTC' + model_replacement: '$1 $2' + + ######### + # Hyundai + # @ref: http://www.hyundaitechnologies.com + ######### + - regex: '; *HYUNDAI (T\d[^/]*) Build' + device_replacement: 'Hyundai $1' + brand_replacement: 'Hyundai' + model_replacement: '$1' + - regex: '; *HYUNDAI ([^;/]+) Build' + device_replacement: 'Hyundai $1' + brand_replacement: 'Hyundai' + model_replacement: '$1' + # X900? http://www.amazon.com/Hyundai-X900-Retina-Android-Bluetooth/dp/B00AO07H3O + - regex: '; *(X700|Hold X|MB-6900) Build' + device_replacement: 'Hyundai $1' + brand_replacement: 'Hyundai' + model_replacement: '$1' + + ######### + # iBall + # @ref: http://www.iball.co.in/Category/Mobiles/22 + ######### + - regex: '; *(?:iBall[ _\-])?(Andi)[ _]?(\d[^;/]*) Build' + regex_flag: 'i' + device_replacement: '$1 $2' + brand_replacement: 'iBall' + model_replacement: '$1 $2' + - regex: '; *(IBall)(?:[ _]([^;/]+)|) Build' + regex_flag: 'i' + device_replacement: '$1 $2' + brand_replacement: 'iBall' + model_replacement: '$2' + + ######### + # IconBIT + # @ref: http://www.iconbit.com/catalog/tablets/ + ######### + - regex: '; *(NT-\d+[^ ;/]*|Net[Tt]AB [^;/]+|Mercury [A-Z]+|iconBIT)(?: S/N:[^;/]+)? Build' + device_replacement: '$1' + brand_replacement: 'IconBIT' + model_replacement: '$1' + + ######### + # IMO + # @ref: http://www.ponselimo.com/ + ######### + - regex: '; *(IMO)[ _]([^;/]+) Build' + regex_flag: 'i' + device_replacement: '$1 $2' + brand_replacement: 'IMO' + model_replacement: '$2' + + ######### + # i-mobile + # @ref: http://www.i-mobilephone.com/ + ######### + - regex: '; *i-?mobile[ _]([^/]+) Build/' + regex_flag: 'i' + device_replacement: 'i-mobile $1' + brand_replacement: 'imobile' + model_replacement: '$1' + - regex: '; *(i-(?:style|note)[^/]*) Build/' + regex_flag: 'i' + device_replacement: 'i-mobile $1' + brand_replacement: 'imobile' + model_replacement: '$1' + + ######### + # Impression + # @ref: http://impression.ua/planshetnye-kompyutery + ######### + - regex: '; *(ImPAD) ?(\d+(?:.)*) Build' + device_replacement: '$1 $2' + brand_replacement: 'Impression' + model_replacement: '$1 $2' + + ######### + # Infinix + # @ref: http://www.infinixmobility.com/index.html + ######### + - regex: '; *(Infinix)[ _]([^;/]+) Build' + device_replacement: '$1 $2' + brand_replacement: 'Infinix' + model_replacement: '$2' + + ######### + # Informer + # @ref: ?? + ######### + - regex: '; *(Informer)[ \-]([^;/]+) Build' + device_replacement: '$1 $2' + brand_replacement: 'Informer' + model_replacement: '$2' + + ######### + # Intenso + # @ref: http://www.intenso.de + # @models: 7":TAB 714,TAB 724;8":TAB 814,TAB 824;10":TAB 1004 + ######### + - regex: '; *(TAB) ?([78][12]4) Build' + device_replacement: 'Intenso $1' + brand_replacement: 'Intenso' + model_replacement: '$1 $2' + + ######### + # Intex + # @ref: http://intexmobile.in/index.aspx + # @note: Zync also offers a "Cloud Z5" device + ######### + # smartphones + - regex: '; *(?:Intex[ _])?(AQUA|Aqua)([ _\.\-])([^;/]+) *(?:Build|;)' + device_replacement: '$1$2$3' + brand_replacement: 'Intex' + model_replacement: '$1 $3' + # matches "INTEX CLOUD X1" + - regex: '; *(?:INTEX|Intex)(?:[_ ]([^\ _;/]+))(?:[_ ]([^\ _;/]+))? *(?:Build|;)' + device_replacement: '$1 $2' + brand_replacement: 'Intex' + model_replacement: '$1 $2' + # tablets + - regex: '; *([iI]Buddy)[ _]?(Connect)(?:_|\?_| )?([^;/]*) *(?:Build|;)' + device_replacement: '$1 $2 $3' + brand_replacement: 'Intex' + model_replacement: 'iBuddy $2 $3' + - regex: '; *(I-Buddy)[ _]([^;/]+) *(?:Build|;)' + device_replacement: '$1 $2' + brand_replacement: 'Intex' + model_replacement: 'iBuddy $2' + + ######### + # iOCEAN + # @ref: http://www.iocean.cc/ + ######### + - regex: '; *(iOCEAN) ([^/]+) Build' + regex_flag: 'i' + device_replacement: '$1 $2' + brand_replacement: 'iOCEAN' + model_replacement: '$2' + + ######### + # i.onik + # @ref: http://www.i-onik.de/ + ######### + - regex: '; *(TP\d+(?:\.\d+)?\-\d[^;/]+) Build' + device_replacement: 'ionik $1' + brand_replacement: 'ionik' + model_replacement: '$1' + + ######### + # IRU.ru + # @ref: http://www.iru.ru/catalog/soho/planetable/ + ######### + - regex: '; *(M702pro) Build' + device_replacement: '$1' + brand_replacement: 'Iru' + model_replacement: '$1' + + ######### + # Ivio + # @ref: http://www.ivio.com/mobile.php + # @models: DG80,DG20,DE38,DE88,MD70 + ######### + - regex: '; *(DE88Plus|MD70) Build' + device_replacement: '$1' + brand_replacement: 'Ivio' + model_replacement: '$1' + - regex: '; *IVIO[_\-]([^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Ivio' + model_replacement: '$1' + + ######### + # Jaytech + # @ref: http://www.jay-tech.de/jaytech/servlet/frontend/ + ######### + - regex: '; *(TPC-\d+|JAY-TECH) Build' + device_replacement: '$1' + brand_replacement: 'Jaytech' + model_replacement: '$1' + + ######### + # Jiayu + # @ref: http://www.ejiayu.com/en/Product.html + ######### + - regex: '; *(JY-[^;/]+|G[234]S?) Build' + device_replacement: '$1' + brand_replacement: 'Jiayu' + model_replacement: '$1' + + ######### + # JXD + # @ref: http://www.jxd.hk/ + ######### + - regex: '; *(JXD)[ _\-]([^;/]+) Build' + device_replacement: '$1 $2' + brand_replacement: 'JXD' + model_replacement: '$2' + + ######### + # Karbonn + # @ref: http://www.karbonnmobiles.com/products_tablet.php + ######### + - regex: '; *Karbonn[ _]?([^;/]+) *(?:Build|;)' + regex_flag: 'i' + device_replacement: '$1' + brand_replacement: 'Karbonn' + model_replacement: '$1' + - regex: '; *([^;]+) Build/Karbonn' + device_replacement: '$1' + brand_replacement: 'Karbonn' + model_replacement: '$1' + - regex: '; *(A11|A39|A37|A34|ST8|ST10|ST7|Smart Tab3|Smart Tab2|Titanium S\d) +Build' + device_replacement: '$1' + brand_replacement: 'Karbonn' + model_replacement: '$1' + + ######### + # KDDI (Operator Branded Device) + # @ref: http://www.ipentec.com/document/document.aspx?page=android-useragent + ######### + - regex: '; *(IS01|IS03|IS05|IS\d{2}SH) Build' + device_replacement: '$1' + brand_replacement: 'Sharp' + model_replacement: '$1' + - regex: '; *(IS04) Build' + device_replacement: '$1' + brand_replacement: 'Regza' + model_replacement: '$1' + - regex: '; *(IS06|IS\d{2}PT) Build' + device_replacement: '$1' + brand_replacement: 'Pantech' + model_replacement: '$1' + - regex: '; *(IS11S) Build' + device_replacement: '$1' + brand_replacement: 'SonyEricsson' + model_replacement: 'Xperia Acro' + - regex: '; *(IS11CA) Build' + device_replacement: '$1' + brand_replacement: 'Casio' + model_replacement: 'GzOne $1' + - regex: '; *(IS11LG) Build' + device_replacement: '$1' + brand_replacement: 'LG' + model_replacement: 'Optimus X' + - regex: '; *(IS11N) Build' + device_replacement: '$1' + brand_replacement: 'Medias' + model_replacement: '$1' + - regex: '; *(IS11PT) Build' + device_replacement: '$1' + brand_replacement: 'Pantech' + model_replacement: 'MIRACH' + - regex: '; *(IS12F) Build' + device_replacement: '$1' + brand_replacement: 'Fujitsu' + model_replacement: 'Arrows ES' + # @ref: https://ja.wikipedia.org/wiki/IS12M + - regex: '; *(IS12M) Build' + device_replacement: '$1' + brand_replacement: 'Motorola' + model_replacement: 'XT909' + - regex: '; *(IS12S) Build' + device_replacement: '$1' + brand_replacement: 'SonyEricsson' + model_replacement: 'Xperia Acro HD' + - regex: '; *(ISW11F) Build' + device_replacement: '$1' + brand_replacement: 'Fujitsu' + model_replacement: 'Arrowz Z' + - regex: '; *(ISW11HT) Build' + device_replacement: '$1' + brand_replacement: 'HTC' + model_replacement: 'EVO' + - regex: '; *(ISW11K) Build' + device_replacement: '$1' + brand_replacement: 'Kyocera' + model_replacement: 'DIGNO' + - regex: '; *(ISW11M) Build' + device_replacement: '$1' + brand_replacement: 'Motorola' + model_replacement: 'Photon' + - regex: '; *(ISW11SC) Build' + device_replacement: '$1' + brand_replacement: 'Samsung' + model_replacement: 'GALAXY S II WiMAX' + - regex: '; *(ISW12HT) Build' + device_replacement: '$1' + brand_replacement: 'HTC' + model_replacement: 'EVO 3D' + - regex: '; *(ISW13HT) Build' + device_replacement: '$1' + brand_replacement: 'HTC' + model_replacement: 'J' + - regex: '; *(ISW?[0-9]{2}[A-Z]{0,2}) Build' + device_replacement: '$1' + brand_replacement: 'KDDI' + model_replacement: '$1' + - regex: '; *(INFOBAR [^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'KDDI' + model_replacement: '$1' + + ######### + # Kingcom + # @ref: http://www.e-kingcom.com + ######### + - regex: '; *(JOYPAD|Joypad)[ _]([^;/]+) Build/' + device_replacement: '$1 $2' + brand_replacement: 'Kingcom' + model_replacement: '$1 $2' + + ######### + # Kobo + # @ref: https://en.wikipedia.org/wiki/Kobo_Inc. + # @ref: http://www.kobo.com/devices#tablets + ######### + - regex: '; *(Vox|VOX|Arc|K080) Build/' + regex_flag: 'i' + device_replacement: '$1' + brand_replacement: 'Kobo' + model_replacement: '$1' + - regex: '\b(Kobo Touch)\b' + device_replacement: '$1' + brand_replacement: 'Kobo' + model_replacement: '$1' + + ######### + # K-Touch + # @ref: ?? + ######### + - regex: '; *(K-Touch)[ _]([^;/]+) Build' + regex_flag: 'i' + device_replacement: '$1 $2' + brand_replacement: 'Ktouch' + model_replacement: '$2' + + ######### + # KT Tech + # @ref: http://www.kttech.co.kr + ######### + - regex: '; *((?:EV|KM)-S\d+[A-Z]?) Build' + regex_flag: 'i' + device_replacement: '$1' + brand_replacement: 'KTtech' + model_replacement: '$1' + + ######### + # Kyocera + # @ref: http://www.android.com/devices/?country=all&m=kyocera + ######### + - regex: '; *(Zio|Hydro|Torque|Event|EVENT|Echo|Milano|Rise|URBANO PROGRESSO|WX04K|WX06K|WX10K|KYL21|101K|C5[12]\d{2}) Build/' + device_replacement: '$1' + brand_replacement: 'Kyocera' + model_replacement: '$1' + + ######### + # Lava + # @ref: http://www.lavamobiles.com/ + ######### + - regex: '; *(?:LAVA[ _])?IRIS[ _\-]?([^/;\)]+) *(?:;|\)|Build)' + regex_flag: 'i' + device_replacement: 'Iris $1' + brand_replacement: 'Lava' + model_replacement: 'Iris $1' + - regex: '; *LAVA[ _]([^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Lava' + model_replacement: '$1' + + ######### + # Lemon + # @ref: http://www.lemonmobiles.com/products.php?type=1 + ######### + - regex: '; *(?:(Aspire A1)|(?:LEMON|Lemon)[ _]([^;/]+))_? Build' + device_replacement: 'Lemon $1$2' + brand_replacement: 'Lemon' + model_replacement: '$1$2' + + ######### + # Lenco + # @ref: http://www.lenco.com/c/tablets/ + ######### + - regex: '; *(TAB-1012) Build/' + device_replacement: 'Lenco $1' + brand_replacement: 'Lenco' + model_replacement: '$1' + - regex: '; Lenco ([^;/]+) Build/' + device_replacement: 'Lenco $1' + brand_replacement: 'Lenco' + model_replacement: '$1' + + ######### + # Lenovo + # @ref: http://support.lenovo.com/en_GB/downloads/default.page?# + ######### + - regex: '; *(A1_07|A2107A-H|S2005A-H|S1-37AH0) Build' + device_replacement: '$1' + brand_replacement: 'Lenovo' + model_replacement: '$1' + - regex: '; *(Idea[Tp]ab)[ _]([^;/]+);? Build' + device_replacement: 'Lenovo $1 $2' + brand_replacement: 'Lenovo' + model_replacement: '$1 $2' + - regex: '; *(Idea(?:Tab|pad)) ?([^;/]+) Build' + device_replacement: 'Lenovo $1 $2' + brand_replacement: 'Lenovo' + model_replacement: '$1 $2' + - regex: '; *(ThinkPad) ?(Tablet) Build/' + device_replacement: 'Lenovo $1 $2' + brand_replacement: 'Lenovo' + model_replacement: '$1 $2' + - regex: '; *(?:LNV-)?(?:=?[Ll]enovo[ _\-]?|LENOVO[ _])+(.+?)(?:Build|[;/\)])' + device_replacement: 'Lenovo $1' + brand_replacement: 'Lenovo' + model_replacement: '$1' + - regex: '[;,] (?:Vodafone )?(SmartTab) ?(II) ?(\d+) Build/' + device_replacement: 'Lenovo $1 $2 $3' + brand_replacement: 'Lenovo' + model_replacement: '$1 $2 $3' + - regex: '; *(?:Ideapad )?K1 Build/' + device_replacement: 'Lenovo Ideapad K1' + brand_replacement: 'Lenovo' + model_replacement: 'Ideapad K1' + - regex: '; *(3GC101|3GW10[01]|A390) Build/' + device_replacement: '$1' + brand_replacement: 'Lenovo' + model_replacement: '$1' + - regex: '\b(?:Lenovo|LENOVO)+[ _\-]?([^,;:/ ]+)' + device_replacement: 'Lenovo $1' + brand_replacement: 'Lenovo' + model_replacement: '$1' + + ######### + # Lexibook + # @ref: http://www.lexibook.com/fr + ######### + - regex: '; *(MFC\d+)[A-Z]{2}([^;,/]*),? Build' + device_replacement: '$1$2' + brand_replacement: 'Lexibook' + model_replacement: '$1$2' + + ######### + # LG + # @ref: http://www.lg.com/uk/mobile + ######### + - regex: '; *(E[34][0-9]{2}|LS[6-8][0-9]{2}|VS[6-9][0-9]+[^;/]+|Nexus 4|Nexus 5X?|GT540f?|Optimus (?:2X|G|4X HD)|OptimusX4HD) *(?:Build|;)' + device_replacement: '$1' + brand_replacement: 'LG' + model_replacement: '$1' + - regex: '[;:] *(L-\d+[A-Z]|LGL\d+[A-Z]?)(?:/V\d+)? *(?:Build|[;\)])' + device_replacement: '$1' + brand_replacement: 'LG' + model_replacement: '$1' + - regex: '; *(LG-)([A-Z]{1,2}\d{2,}[^,;/\)\(]*?)(?:Build| V\d+|[,;/\)\(]|$)' + device_replacement: '$1$2' + brand_replacement: 'LG' + model_replacement: '$2' + - regex: '; *(LG[ \-]|LG)([^;/]+)[;/]? Build' + device_replacement: '$1$2' + brand_replacement: 'LG' + model_replacement: '$2' + - regex: '^(LG)-([^;/]+)/ Mozilla/.*; Android' + device_replacement: '$1 $2' + brand_replacement: 'LG' + model_replacement: '$2' + + ######### + # Malata + # @ref: http://www.malata.com/en/products.aspx?classid=680 + ######### + - regex: '; *((?:SMB|smb)[^;/]+) Build/' + device_replacement: '$1' + brand_replacement: 'Malata' + model_replacement: '$1' + - regex: '; *(?:Malata|MALATA) ([^;/]+) Build/' + device_replacement: '$1' + brand_replacement: 'Malata' + model_replacement: '$1' + + ######### + # Manta + # @ref: http://www.manta.com.pl/en + ######### + - regex: '; *(MS[45][0-9]{3}|MID0[568][NS]?|MID[1-9]|MID[78]0[1-9]|MID970[1-9]|MID100[1-9]) Build/' + device_replacement: '$1' + brand_replacement: 'Manta' + model_replacement: '$1' + + ######### + # Match + # @ref: http://www.match.net.cn/products.asp + ######### + - regex: '; *(M1052|M806|M9000|M9100|M9701|MID100|MID120|MID125|MID130|MID135|MID140|MID701|MID710|MID713|MID727|MID728|MID731|MID732|MID733|MID735|MID736|MID737|MID760|MID800|MID810|MID820|MID830|MID833|MID835|MID860|MID900|MID930|MID933|MID960|MID980) Build/' + device_replacement: '$1' + brand_replacement: 'Match' + model_replacement: '$1' + + ######### + # Maxx + # @ref: http://www.maxxmobile.in/ + # @models: Maxx MSD7-Play, Maxx MX245+ Trance, Maxx AX8 Race, Maxx MSD7 3G- AX50, Maxx Genx Droid 7 - AX40, Maxx AX5 Duo, + # Maxx AX3 Duo, Maxx AX3, Maxx AX8 Note II (Note 2), Maxx AX8 Note I, Maxx AX8, Maxx AX5 Plus, Maxx MSD7 Smarty, + # Maxx AX9Z Race, + # Maxx MT150, Maxx MQ601, Maxx M2020, Maxx Sleek MX463neo, Maxx MX525, Maxx MX192-Tune, Maxx Genx Droid 7 AX353, + # @note: Need more User-Agents!!! + ######### + - regex: '; *(GenxDroid7|MSD7.*|AX\d.*|Tab 701|Tab 722) Build/' + device_replacement: 'Maxx $1' + brand_replacement: 'Maxx' + model_replacement: '$1' + + ######### + # Mediacom + # @ref: http://www.mediacomeurope.it/ + ######### + - regex: '; *(M-PP[^;/]+|PhonePad ?\d{2,}[^;/]+) Build' + device_replacement: 'Mediacom $1' + brand_replacement: 'Mediacom' + model_replacement: '$1' + - regex: '; *(M-MP[^;/]+|SmartPad ?\d{2,}[^;/]+) Build' + device_replacement: 'Mediacom $1' + brand_replacement: 'Mediacom' + model_replacement: '$1' + + ######### + # Medion + # @ref: http://www.medion.com/en/ + ######### + - regex: '; *(?:MD_)?LIFETAB[ _]([^;/]+) Build' + regex_flag: 'i' + device_replacement: 'Medion Lifetab $1' + brand_replacement: 'Medion' + model_replacement: 'Lifetab $1' + - regex: '; *MEDION ([^;/]+) Build' + device_replacement: 'Medion $1' + brand_replacement: 'Medion' + model_replacement: '$1' + + ######### + # Meizu + # @ref: http://www.meizu.com + ######### + - regex: '; *(M030|M031|M035|M040|M065|m9) Build' + device_replacement: 'Meizu $1' + brand_replacement: 'Meizu' + model_replacement: '$1' + - regex: '; *(?:meizu_|MEIZU )(.+?) *(?:Build|[;\)])' + device_replacement: 'Meizu $1' + brand_replacement: 'Meizu' + model_replacement: '$1' + + ######### + # Micromax + # @ref: http://www.micromaxinfo.com + ######### + - regex: '; *(?:Micromax[ _](A111|A240)|(A111|A240)) Build' + regex_flag: 'i' + device_replacement: 'Micromax $1$2' + brand_replacement: 'Micromax' + model_replacement: '$1$2' + - regex: '; *Micromax[ _](A\d{2,3}[^;/]*) Build' + regex_flag: 'i' + device_replacement: 'Micromax $1' + brand_replacement: 'Micromax' + model_replacement: '$1' + # be carefull here with Acer e.g. A500 + - regex: '; *(A\d{2}|A[12]\d{2}|A90S|A110Q) Build' + regex_flag: 'i' + device_replacement: 'Micromax $1' + brand_replacement: 'Micromax' + model_replacement: '$1' + - regex: '; *Micromax[ _](P\d{3}[^;/]*) Build' + regex_flag: 'i' + device_replacement: 'Micromax $1' + brand_replacement: 'Micromax' + model_replacement: '$1' + - regex: '; *(P\d{3}|P\d{3}\(Funbook\)) Build' + regex_flag: 'i' + device_replacement: 'Micromax $1' + brand_replacement: 'Micromax' + model_replacement: '$1' + + ######### + # Mito + # @ref: http://new.mitomobile.com/ + ######### + - regex: '; *(MITO)[ _\-]?([^;/]+) Build' + regex_flag: 'i' + device_replacement: '$1 $2' + brand_replacement: 'Mito' + model_replacement: '$2' + + ######### + # Mobistel + # @ref: http://www.mobistel.com/ + ######### + - regex: '; *(Cynus)[ _](F5|T\d|.+?) *(?:Build|[;/\)])' + regex_flag: 'i' + device_replacement: '$1 $2' + brand_replacement: 'Mobistel' + model_replacement: '$1 $2' + + ######### + # Modecom + # @ref: http://www.modecom.eu/tablets/portal/ + ######### + - regex: '; *(MODECOM )?(FreeTab) ?([^;/]+) Build' + regex_flag: 'i' + device_replacement: '$1$2 $3' + brand_replacement: 'Modecom' + model_replacement: '$2 $3' + - regex: '; *(MODECOM )([^;/]+) Build' + regex_flag: 'i' + device_replacement: '$1 $2' + brand_replacement: 'Modecom' + model_replacement: '$2' + + ######### + # Motorola + # @ref: http://www.motorola.com/us/shop-all-mobile-phones/ + ######### + - regex: '; *(MZ\d{3}\+?|MZ\d{3} 4G|Xoom|XOOM[^;/]*) Build' + device_replacement: 'Motorola $1' + brand_replacement: 'Motorola' + model_replacement: '$1' + - regex: '; *(Milestone )(XT[^;/]*) Build' + device_replacement: 'Motorola $1$2' + brand_replacement: 'Motorola' + model_replacement: '$2' + - regex: '; *(Motoroi ?x|Droid X|DROIDX) Build' + regex_flag: 'i' + device_replacement: 'Motorola $1' + brand_replacement: 'Motorola' + model_replacement: 'DROID X' + - regex: '; *(Droid[^;/]*|DROID[^;/]*|Milestone[^;/]*|Photon|Triumph|Devour|Titanium) Build' + device_replacement: 'Motorola $1' + brand_replacement: 'Motorola' + model_replacement: '$1' + - regex: '; *(A555|A85[34][^;/]*|A95[356]|ME[58]\d{2}\+?|ME600|ME632|ME722|MB\d{3}\+?|MT680|MT710|MT870|MT887|MT917|WX435|WX453|WX44[25]|XT\d{3,4}[A-Z\+]*|CL[iI]Q|CL[iI]Q XT) Build' + device_replacement: '$1' + brand_replacement: 'Motorola' + model_replacement: '$1' + - regex: '; *(Motorola MOT-|Motorola[ _\-]|MOT\-?)([^;/]+) Build' + device_replacement: '$1$2' + brand_replacement: 'Motorola' + model_replacement: '$2' + - regex: '; *(Moto[_ ]?|MOT\-)([^;/]+) Build' + device_replacement: '$1$2' + brand_replacement: 'Motorola' + model_replacement: '$2' + + ######### + # MpMan + # @ref: http://www.mpmaneurope.com + ######### + - regex: '; *((?:MP[DQ]C|MPG\d{1,4}|MP\d{3,4}|MID(?:(?:10[234]|114|43|7[247]|8[24]|7)C|8[01]1))[^;/]*) Build' + device_replacement: '$1' + brand_replacement: 'Mpman' + model_replacement: '$1' + + ######### + # MSI + # @ref: http://www.msi.com/product/windpad/ + ######### + - regex: '; *(?:MSI[ _])?(Primo\d+|Enjoy[ _\-][^;/]+) Build' + regex_flag: 'i' + device_replacement: '$1' + brand_replacement: 'Msi' + model_replacement: '$1' + + ######### + # Multilaser + # http://www.multilaser.com.br/listagem_produtos.php?cat=5 + ######### + - regex: '; *Multilaser[ _]([^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Multilaser' + model_replacement: '$1' + + ######### + # MyPhone + # @ref: http://myphone.com.ph/ + ######### + - regex: '; *(My)[_]?(Pad)[ _]([^;/]+) Build' + device_replacement: '$1$2 $3' + brand_replacement: 'MyPhone' + model_replacement: '$1$2 $3' + - regex: '; *(My)\|?(Phone)[ _]([^;/]+) Build' + device_replacement: '$1$2 $3' + brand_replacement: 'MyPhone' + model_replacement: '$3' + - regex: '; *(A\d+)[ _](Duo)? Build' + regex_flag: 'i' + device_replacement: '$1 $2' + brand_replacement: 'MyPhone' + model_replacement: '$1 $2' + + ######### + # Mytab + # @ref: http://www.mytab.eu/en/category/mytab-products/ + ######### + - regex: '; *(myTab[^;/]*) Build' + device_replacement: '$1' + brand_replacement: 'Mytab' + model_replacement: '$1' + + ######### + # Nabi + # @ref: https://www.nabitablet.com + ######### + - regex: '; *(NABI2?-)([^;/]+) Build/' + device_replacement: '$1$2' + brand_replacement: 'Nabi' + model_replacement: '$2' + + ######### + # Nec Medias + # @ref: http://www.n-keitai.com/ + ######### + - regex: '; *(N-\d+[CDE]) Build/' + device_replacement: '$1' + brand_replacement: 'Nec' + model_replacement: '$1' + - regex: '; ?(NEC-)(.*) Build/' + device_replacement: '$1$2' + brand_replacement: 'Nec' + model_replacement: '$2' + - regex: '; *(LT-NA7) Build/' + device_replacement: '$1' + brand_replacement: 'Nec' + model_replacement: 'Lifetouch Note' + + ######### + # Nextbook + # @ref: http://nextbookusa.com + ######### + - regex: '; *(NXM\d+[A-z0-9_]*|Next\d[A-z0-9_ \-]*|NEXT\d[A-z0-9_ \-]*|Nextbook [A-z0-9_ ]*|DATAM803HC|M805)(?: Build|[\);])' + device_replacement: '$1' + brand_replacement: 'Nextbook' + model_replacement: '$1' + + ######### + # Nokia + # @ref: http://www.nokia.com + ######### + - regex: '; *(Nokia)([ _\-]*)([^;/]*) Build' + regex_flag: 'i' + device_replacement: '$1$2$3' + brand_replacement: 'Nokia' + model_replacement: '$3' + + ######### + # Nook + # @ref: + # TODO nook browser/1.0 + ######### + - regex: '; *(Nook ?|Barnes & Noble Nook |BN )([^;/]+) Build' + device_replacement: '$1$2' + brand_replacement: 'Nook' + model_replacement: '$2' + - regex: '; *(NOOK )?(BNRV200|BNRV200A|BNTV250|BNTV250A|BNTV400|BNTV600|LogicPD Zoom2) Build' + device_replacement: '$1$2' + brand_replacement: 'Nook' + model_replacement: '$2' + - regex: '; Build/(Nook)' + device_replacement: '$1' + brand_replacement: 'Nook' + model_replacement: 'Tablet' + + ######### + # Olivetti + # @ref: http://www.olivetti.de/EN/Page/t02/view_html?idp=348 + ######### + - regex: '; *(OP110|OliPad[^;/]+) Build' + device_replacement: 'Olivetti $1' + brand_replacement: 'Olivetti' + model_replacement: '$1' + + ######### + # Omega + # @ref: http://omega-technology.eu/en/produkty/346/tablets + # @note: MID tablets might get matched by CobyKyros first + # @models: (T107|MID(?:700[2-5]|7031|7108|7132|750[02]|8001|8500|9001|971[12]) + ######### + - regex: '; *OMEGA[ _\-](MID[^;/]+) Build' + device_replacement: 'Omega $1' + brand_replacement: 'Omega' + model_replacement: '$1' + - regex: '^(MID7500|MID\d+) Mozilla/5\.0 \(iPad;' + device_replacement: 'Omega $1' + brand_replacement: 'Omega' + model_replacement: '$1' + + ######### + # OpenPeak + # @ref: https://support.google.com/googleplay/answer/1727131?hl=en + ######### + - regex: '; *((?:CIUS|cius)[^;/]*) Build' + device_replacement: 'Openpeak $1' + brand_replacement: 'Openpeak' + model_replacement: '$1' + + ######### + # Oppo + # @ref: http://en.oppo.com/products/ + ######### + - regex: '; *(Find ?(?:5|7a)|R8[012]\d{1,2}|T703\d{0,1}|U70\d{1,2}T?|X90\d{1,2}) Build' + device_replacement: 'Oppo $1' + brand_replacement: 'Oppo' + model_replacement: '$1' + - regex: '; *OPPO ?([^;/]+) Build/' + device_replacement: 'Oppo $1' + brand_replacement: 'Oppo' + model_replacement: '$1' + + ######### + # Odys + # @ref: http://odys.de + ######### + - regex: '; *(?:Odys\-|ODYS\-|ODYS )([^;/]+) Build' + device_replacement: 'Odys $1' + brand_replacement: 'Odys' + model_replacement: '$1' + - regex: '; *(SELECT) ?(7) Build' + device_replacement: 'Odys $1 $2' + brand_replacement: 'Odys' + model_replacement: '$1 $2' + - regex: '; *(PEDI)_(PLUS)_(W) Build' + device_replacement: 'Odys $1 $2 $3' + brand_replacement: 'Odys' + model_replacement: '$1 $2 $3' + # Weltbild - Tablet PC 4 = Cat Phoenix = Odys Tablet PC 4? + - regex: '; *(AEON|BRAVIO|FUSION|FUSION2IN1|Genio|EOS10|IEOS[^;/]*|IRON|Loox|LOOX|LOOX Plus|Motion|NOON|NOON_PRO|NEXT|OPOS|PEDI[^;/]*|PRIME[^;/]*|STUDYTAB|TABLO|Tablet-PC-4|UNO_X8|XELIO[^;/]*|Xelio ?\d+ ?[Pp]ro|XENO10|XPRESS PRO) Build' + device_replacement: 'Odys $1' + brand_replacement: 'Odys' + model_replacement: '$1' + + ######### + # Orion + # @ref: http://www.orion.ua/en/products/computer-products/tablet-pcs.html + ######### + - regex: '; *(TP-\d+) Build/' + device_replacement: 'Orion $1' + brand_replacement: 'Orion' + model_replacement: '$1' + + ######### + # PackardBell + # @ref: http://www.packardbell.com/pb/en/AE/content/productgroup/tablets + ######### + - regex: '; *(G100W?) Build/' + device_replacement: 'PackardBell $1' + brand_replacement: 'PackardBell' + model_replacement: '$1' + + ######### + # Panasonic + # @ref: http://panasonic.jp/mobile/ + # @models: T11, T21, T31, P11, P51, Eluga Power, Eluga DL1 + # @models: (tab) Toughpad FZ-A1, Toughpad JT-B1 + ######### + - regex: '; *(Panasonic)[_ ]([^;/]+) Build' + device_replacement: '$1 $2' + brand_replacement: '$1' + model_replacement: '$2' + # Toughpad + - regex: '; *(FZ-A1B|JT-B1) Build' + device_replacement: 'Panasonic $1' + brand_replacement: 'Panasonic' + model_replacement: '$1' + # Eluga Power + - regex: '; *(dL1|DL1) Build' + device_replacement: 'Panasonic $1' + brand_replacement: 'Panasonic' + model_replacement: '$1' + + ######### + # Pantech + # @href: http://www.pantech.co.kr/en/prod/prodList.do?gbrand=PANTECH + # @href: http://www.pantech.co.kr/en/prod/prodList.do?gbrand=VEGA + # @models: ADR8995, ADR910L, ADR930VW, C790, CDM8992, CDM8999, IS06, IS11PT, P2000, P2020, P2030, P4100, P5000, P6010, P6020, P6030, P7000, P7040, P8000, P8010, P9020, P9050, P9060, P9070, P9090, PT001, PT002, PT003, TXT8040, TXT8045, VEGA PTL21 + ######### + - regex: '; *(SKY[ _])?(IM\-[AT]\d{3}[^;/]+).* Build/' + device_replacement: 'Pantech $1$2' + brand_replacement: 'Pantech' + model_replacement: '$1$2' + - regex: '; *((?:ADR8995|ADR910L|ADR930L|ADR930VW|PTL21|P8000)(?: 4G)?) Build/' + device_replacement: '$1' + brand_replacement: 'Pantech' + model_replacement: '$1' + - regex: '; *Pantech([^;/]+).* Build/' + device_replacement: 'Pantech $1' + brand_replacement: 'Pantech' + model_replacement: '$1' + + ######### + # Papayre + # @ref: http://grammata.es/ + ######### + - regex: '; *(papyre)[ _\-]([^;/]+) Build/' + regex_flag: 'i' + device_replacement: '$1 $2' + brand_replacement: 'Papyre' + model_replacement: '$2' + + ######### + # Pearl + # @ref: http://www.pearl.de/c-1540.shtml + ######### + - regex: '; *(?:Touchlet )?(X10\.[^;/]+) Build/' + device_replacement: 'Pearl $1' + brand_replacement: 'Pearl' + model_replacement: '$1' + + ######### + # Phicomm + # @ref: http://www.phicomm.com.cn/ + ######### + - regex: '; PHICOMM (i800) Build/' + device_replacement: 'Phicomm $1' + brand_replacement: 'Phicomm' + model_replacement: '$1' + - regex: '; PHICOMM ([^;/]+) Build/' + device_replacement: 'Phicomm $1' + brand_replacement: 'Phicomm' + model_replacement: '$1' + - regex: '; *(FWS\d{3}[^;/]+) Build/' + device_replacement: 'Phicomm $1' + brand_replacement: 'Phicomm' + model_replacement: '$1' + + ######### + # Philips + # @ref: http://www.support.philips.com/support/catalog/products.jsp?_dyncharset=UTF-8&country=&categoryid=MOBILE_PHONES_SMART_SU_CN_CARE&userLanguage=en&navCount=2&groupId=PC_PRODUCTS_AND_PHONES_GR_CN_CARE&catalogType=&navAction=push&userCountry=cn&title=Smartphones&cateId=MOBILE_PHONES_CA_CN_CARE + # @TODO: Philips Tablets User-Agents missing! + # @ref: http://www.support.philips.com/support/catalog/products.jsp?_dyncharset=UTF-8&country=&categoryid=ENTERTAINMENT_TABLETS_SU_CN_CARE&userLanguage=en&navCount=0&groupId=&catalogType=&navAction=push&userCountry=cn&title=Entertainment+Tablets&cateId=TABLETS_CA_CN_CARE + ######### + # @note: this a best guess according to available philips models. Need more User-Agents + - regex: '; *(D633|D822|D833|T539|T939|V726|W335|W336|W337|W3568|W536|W5510|W626|W632|W6350|W6360|W6500|W732|W736|W737|W7376|W820|W832|W8355|W8500|W8510|W930) Build' + device_replacement: '$1' + brand_replacement: 'Philips' + model_replacement: '$1' + - regex: '; *(?:Philips|PHILIPS)[ _]([^;/]+) Build' + device_replacement: 'Philips $1' + brand_replacement: 'Philips' + model_replacement: '$1' + + ######### + # Pipo + # @ref: http://www.pipo.cn/En/ + ######### + - regex: 'Android 4\..*; *(M[12356789]|U[12368]|S[123])\ ?(pro)? Build' + device_replacement: 'Pipo $1$2' + brand_replacement: 'Pipo' + model_replacement: '$1$2' + + ######### + # Ployer + # @ref: http://en.ployer.cn/ + ######### + - regex: '; *(MOMO[^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Ployer' + model_replacement: '$1' + + ######### + # Polaroid/ Acho + # @ref: http://polaroidstore.com/store/start.asp?category_id=382&category_id2=0&order=title&filter1=&filter2=&filter3=&view=all + ######### + - regex: '; *(?:Polaroid[ _])?((?:MIDC\d{3,}|PMID\d{2,}|PTAB\d{3,})[^;/]*)(\/[^;/]*)? Build/' + device_replacement: '$1' + brand_replacement: 'Polaroid' + model_replacement: '$1' + - regex: '; *(?:Polaroid )(Tablet) Build/' + device_replacement: '$1' + brand_replacement: 'Polaroid' + model_replacement: '$1' + + ######### + # Pomp + # @ref: http://pompmobileshop.com/ + ######### + #~ TODO + - regex: '; *(POMP)[ _\-](.+?) *(?:Build|[;/\)])' + device_replacement: '$1 $2' + brand_replacement: 'Pomp' + model_replacement: '$2' + + ######### + # Positivo + # @ref: http://www.positivoinformatica.com.br/www/pessoal/tablet-ypy/ + ######### + - regex: '; *(TB07STA|TB10STA|TB07FTA|TB10FTA) Build/' + device_replacement: '$1' + brand_replacement: 'Positivo' + model_replacement: '$1' + - regex: '; *(?:Positivo )?((?:YPY|Ypy)[^;/]+) Build/' + device_replacement: '$1' + brand_replacement: 'Positivo' + model_replacement: '$1' + + ######### + # POV + # @ref: http://www.pointofview-online.com/default2.php + # @TODO: Smartphone Models MOB-3515, MOB-5045-B missing + ######### + - regex: '; *(MOB-[^;/]+) Build/' + device_replacement: '$1' + brand_replacement: 'POV' + model_replacement: '$1' + - regex: '; *POV[ _\-]([^;/]+) Build/' + device_replacement: 'POV $1' + brand_replacement: 'POV' + model_replacement: '$1' + - regex: '; *((?:TAB-PLAYTAB|TAB-PROTAB|PROTAB|PlayTabPro|Mobii[ _\-]|TAB-P)[^;/]*) Build/' + device_replacement: 'POV $1' + brand_replacement: 'POV' + model_replacement: '$1' + + ######### + # Prestigio + # @ref: http://www.prestigio.com/catalogue/MultiPhones + # @ref: http://www.prestigio.com/catalogue/MultiPads + ######### + - regex: '; *(?:Prestigio )?((?:PAP|PMP)\d[^;/]+) Build/' + device_replacement: 'Prestigio $1' + brand_replacement: 'Prestigio' + model_replacement: '$1' + + ######### + # Proscan + # @ref: http://www.proscanvideo.com/products-search.asp?itemClass=TABLET&itemnmbr= + ######### + - regex: '; *(PLT[0-9]{4}.*) Build/' + device_replacement: '$1' + brand_replacement: 'Proscan' + model_replacement: '$1' + + ######### + # QMobile + # @ref: http://www.qmobile.com.pk/ + ######### + - regex: '; *(A2|A5|A8|A900)_?(Classic)? Build' + device_replacement: '$1 $2' + brand_replacement: 'Qmobile' + model_replacement: '$1 $2' + - regex: '; *(Q[Mm]obile)_([^_]+)_([^_]+) Build' + device_replacement: 'Qmobile $2 $3' + brand_replacement: 'Qmobile' + model_replacement: '$2 $3' + - regex: '; *(Q\-?[Mm]obile)[_ ](A[^;/]+) Build' + device_replacement: 'Qmobile $2' + brand_replacement: 'Qmobile' + model_replacement: '$2' + + ######### + # Qmobilevn + # @ref: http://qmobile.vn/san-pham.html + ######### + - regex: '; *(Q\-Smart)[ _]([^;/]+) Build/' + device_replacement: '$1 $2' + brand_replacement: 'Qmobilevn' + model_replacement: '$2' + - regex: '; *(Q\-?[Mm]obile)[ _\-](S[^;/]+) Build/' + device_replacement: '$1 $2' + brand_replacement: 'Qmobilevn' + model_replacement: '$2' + + ######### + # Quanta + # @ref: ? + ######### + - regex: '; *(TA1013) Build' + device_replacement: '$1' + brand_replacement: 'Quanta' + model_replacement: '$1' + + ######### + # Rockchip + # @ref: http://www.rock-chips.com/a/cn/product/index.html + # @note: manufacturer sells chipsets - I assume that these UAs are dev-boards + ######### + - regex: '; *(RK\d+),? Build/' + device_replacement: '$1' + brand_replacement: 'Rockchip' + model_replacement: '$1' + - regex: ' Build/(RK\d+)' + device_replacement: '$1' + brand_replacement: 'Rockchip' + model_replacement: '$1' + + ######### + # Samsung Android Devices + # @ref: http://www.samsung.com/us/mobile/cell-phones/all-products + ######### + - regex: '; *(SAMSUNG |Samsung )?((?:Galaxy (?:Note II|S\d)|GT-I9082|GT-I9205|GT-N7\d{3}|SM-N9005)[^;/]*)\/?[^;/]* Build/' + device_replacement: 'Samsung $1$2' + brand_replacement: 'Samsung' + model_replacement: '$2' + - regex: '; *(Google )?(Nexus [Ss](?: 4G)?) Build/' + device_replacement: 'Samsung $1$2' + brand_replacement: 'Samsung' + model_replacement: '$2' + - regex: '; *(SAMSUNG |Samsung )([^\/]*)\/[^ ]* Build/' + device_replacement: 'Samsung $2' + brand_replacement: 'Samsung' + model_replacement: '$2' + - regex: '; *(Galaxy(?: Ace| Nexus| S ?II+|Nexus S| with MCR 1.2| Mini Plus 4G)?) Build/' + device_replacement: 'Samsung $1' + brand_replacement: 'Samsung' + model_replacement: '$1' + - regex: '; *(SAMSUNG[ _\-] *)+([^;/]+) Build' + device_replacement: 'Samsung $2' + brand_replacement: 'Samsung' + model_replacement: '$2' + - regex: '; *(SAMSUNG-)?(GT\-[BINPS]\d{4}[^\/]*)(\/[^ ]*) Build' + device_replacement: 'Samsung $1$2$3' + brand_replacement: 'Samsung' + model_replacement: '$2' + - regex: '(?:; *|^)((?:GT\-[BIiNPS]\d{4}|I9\d{2}0[A-Za-z\+]?\b)[^;/\)]*?)(?:Build|Linux|MIUI|[;/\)])' + device_replacement: 'Samsung $1' + brand_replacement: 'Samsung' + model_replacement: '$1' + - regex: '; (SAMSUNG-)([A-Za-z0-9\-]+).* Build/' + device_replacement: 'Samsung $1$2' + brand_replacement: 'Samsung' + model_replacement: '$2' + - regex: '; *((?:SCH|SGH|SHV|SHW|SPH|SC|SM)\-[A-Za-z0-9 ]+)(/?[^ ]*)? Build' + device_replacement: 'Samsung $1' + brand_replacement: 'Samsung' + model_replacement: '$1' + - regex: ' ((?:SCH)\-[A-Za-z0-9 ]+)(/?[^ ]*)? Build' + device_replacement: 'Samsung $1' + brand_replacement: 'Samsung' + model_replacement: '$1' + - regex: '; *(Behold ?(?:2|II)|YP\-G[^;/]+|EK-GC100|SCL21|I9300) Build' + device_replacement: 'Samsung $1' + brand_replacement: 'Samsung' + model_replacement: '$1' + + ######### + # Sharp + # @ref: http://www.sharp-phone.com/en/index.html + # @ref: http://www.android.com/devices/?country=all&m=sharp + ######### + - regex: '; *(SH\-?\d\d[^;/]+|SBM\d[^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Sharp' + model_replacement: '$1' + - regex: '; *(SHARP[ -])([^;/]+) Build' + device_replacement: '$1$2' + brand_replacement: 'Sharp' + model_replacement: '$2' + + ######### + # Simvalley + # @ref: http://www.simvalley-mobile.de/ + ######### + - regex: '; *(SPX[_\-]\d[^;/]*) Build/' + device_replacement: '$1' + brand_replacement: 'Simvalley' + model_replacement: '$1' + - regex: '; *(SX7\-PEARL\.GmbH) Build/' + device_replacement: '$1' + brand_replacement: 'Simvalley' + model_replacement: '$1' + - regex: '; *(SP[T]?\-\d{2}[^;/]*) Build/' + device_replacement: '$1' + brand_replacement: 'Simvalley' + model_replacement: '$1' + + ######### + # SK Telesys + # @ref: http://www.sk-w.com/phone/phone_list.jsp + # @ref: http://www.android.com/devices/?country=all&m=sk-telesys + ######### + - regex: '; *(SK\-.*) Build/' + device_replacement: '$1' + brand_replacement: 'SKtelesys' + model_replacement: '$1' + + ######### + # Skytex + # @ref: http://skytex.com/android + ######### + - regex: '; *(?:SKYTEX|SX)-([^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Skytex' + model_replacement: '$1' + - regex: '; *(IMAGINE [^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Skytex' + model_replacement: '$1' + + ######### + # SmartQ + # @ref: http://en.smartdevices.com.cn/Products/ + # @models: Z8, X7, U7H, U7, T30, T20, Ten3, V5-II, T7-3G, SmartQ5, K7, S7, Q8, T19, Ten2, Ten, R10, T7, R7, V5, V7, SmartQ7 + ######### + - regex: '; *(SmartQ) ?([^;/]+) Build/' + device_replacement: '$1 $2' + brand_replacement: '$1' + model_replacement: '$2' + + ######### + # Smartbitt + # @ref: http://www.smartbitt.com/ + # @missing: SBT Useragents + ######### + - regex: '; *(WF7C|WF10C|SBT[^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Smartbitt' + model_replacement: '$1' + + ######### + # Softbank (Operator Branded Devices) + # @ref: http://www.ipentec.com/document/document.aspx?page=android-useragent + ######### + - regex: '; *(SBM(?:003SH|005SH|006SH|007SH|102SH)) Build' + device_replacement: '$1' + brand_replacement: 'Sharp' + model_replacement: '$1' + - regex: '; *(003P|101P|101P11C|102P) Build' + device_replacement: '$1' + brand_replacement: 'Panasonic' + model_replacement: '$1' + - regex: '; *(00\dZ) Build/' + device_replacement: '$1' + brand_replacement: 'ZTE' + model_replacement: '$1' + - regex: '; HTC(X06HT) Build' + device_replacement: '$1' + brand_replacement: 'HTC' + model_replacement: '$1' + - regex: '; *(001HT|X06HT) Build' + device_replacement: '$1' + brand_replacement: 'HTC' + model_replacement: '$1' + - regex: '; *(201M) Build' + device_replacement: '$1' + brand_replacement: 'Motorola' + model_replacement: 'XT902' + + ######### + # Trekstor + # @ref: http://www.trekstor.co.uk/surftabs-en.html + # @note: Must come before SonyEricsson + ######### + - regex: '; *(ST\d{4}.*)Build/ST' + device_replacement: 'Trekstor $1' + brand_replacement: 'Trekstor' + model_replacement: '$1' + - regex: '; *(ST\d{4}.*) Build/' + device_replacement: 'Trekstor $1' + brand_replacement: 'Trekstor' + model_replacement: '$1' + + ######### + # SonyEricsson + # @note: Must come before nokia since they also use symbian + # @ref: http://www.android.com/devices/?country=all&m=sony-ericssons + # @TODO: type! + ######### + # android matchers + - regex: '; *(Sony ?Ericsson ?)([^;/]+) Build' + device_replacement: '$1$2' + brand_replacement: 'SonyEricsson' + model_replacement: '$2' + - regex: '; *((?:SK|ST|E|X|LT|MK|MT|WT)\d{2}[a-z0-9]*(?:-o)?|R800i|U20i) Build' + device_replacement: '$1' + brand_replacement: 'SonyEricsson' + model_replacement: '$1' + # TODO X\d+ is wrong + - regex: '; *(Xperia (?:A8|Arc|Acro|Active|Live with Walkman|Mini|Neo|Play|Pro|Ray|X\d+)[^;/]*) Build' + regex_flag: 'i' + device_replacement: '$1' + brand_replacement: 'SonyEricsson' + model_replacement: '$1' + + ######### + # Sony + # @ref: http://www.sonymobile.co.jp/index.html + # @ref: http://www.sonymobile.com/global-en/products/phones/ + # @ref: http://www.sony.jp/tablet/ + ######### + - regex: '; Sony (Tablet[^;/]+) Build' + device_replacement: 'Sony $1' + brand_replacement: 'Sony' + model_replacement: '$1' + - regex: '; Sony ([^;/]+) Build' + device_replacement: 'Sony $1' + brand_replacement: 'Sony' + model_replacement: '$1' + - regex: '; *(Sony)([A-Za-z0-9\-]+) Build' + device_replacement: '$1 $2' + brand_replacement: '$1' + model_replacement: '$2' + - regex: '; *(Xperia [^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Sony' + model_replacement: '$1' + - regex: '; *(C(?:1[0-9]|2[0-9]|53|55|6[0-9])[0-9]{2}|D[25]\d{3}|D6[56]\d{2}) Build' + device_replacement: '$1' + brand_replacement: 'Sony' + model_replacement: '$1' + - regex: '; *(SGP\d{3}|SGPT\d{2}) Build' + device_replacement: '$1' + brand_replacement: 'Sony' + model_replacement: '$1' + - regex: '; *(NW-Z1000Series) Build' + device_replacement: '$1' + brand_replacement: 'Sony' + model_replacement: '$1' + + ########## + # Sony PlayStation + # @ref: http://playstation.com + # The Vita spoofs the Kindle + ########## + - regex: 'PLAYSTATION 3' + device_replacement: 'PlayStation 3' + brand_replacement: 'Sony' + model_replacement: 'PlayStation 3' + - regex: '(PlayStation (?:Portable|Vita|\d+))' + device_replacement: '$1' + brand_replacement: 'Sony' + model_replacement: '$1' + + ######### + # Spice + # @ref: http://www.spicemobilephones.co.in/ + ######### + - regex: '; *((?:CSL_Spice|Spice|SPICE|CSL)[ _\-]?)?([Mm][Ii])([ _\-])?(\d{3}[^;/]*) Build/' + device_replacement: '$1$2$3$4' + brand_replacement: 'Spice' + model_replacement: 'Mi$4' + + ######### + # Sprint (Operator Branded Devices) + # @ref: + ######### + - regex: '; *(Sprint )(.+?) *(?:Build|[;/])' + device_replacement: '$1$2' + brand_replacement: 'Sprint' + model_replacement: '$2' + - regex: '\b(Sprint)[: ]([^;,/ ]+)' + device_replacement: '$1$2' + brand_replacement: 'Sprint' + model_replacement: '$2' + + ######### + # Tagi + # @ref: ?? + ######### + - regex: '; *(TAGI[ ]?)(MID) ?([^;/]+) Build/' + device_replacement: '$1$2$3' + brand_replacement: 'Tagi' + model_replacement: '$2$3' + + ######### + # Tecmobile + # @ref: http://www.tecmobile.com/ + ######### + - regex: '; *(Oyster500|Opal 800) Build' + device_replacement: 'Tecmobile $1' + brand_replacement: 'Tecmobile' + model_replacement: '$1' + + ######### + # Tecno + # @ref: www.tecno-mobile.com/‎ + ######### + - regex: '; *(TECNO[ _])([^;/]+) Build/' + device_replacement: '$1$2' + brand_replacement: 'Tecno' + model_replacement: '$2' + + ######### + # Telechips, Techvision evaluation boards + # @ref: + ######### + - regex: '; *Android for (Telechips|Techvision) ([^ ]+) ' + regex_flag: 'i' + device_replacement: '$1 $2' + brand_replacement: '$1' + model_replacement: '$2' + + ######### + # Telstra + # @ref: http://www.telstra.com.au/home-phone/thub-2/ + # @ref: https://support.google.com/googleplay/answer/1727131?hl=en + ######### + - regex: '; *(T-Hub2) Build/' + device_replacement: '$1' + brand_replacement: 'Telstra' + model_replacement: '$1' + + ######### + # Terra + # @ref: http://www.wortmann.de/ + ######### + - regex: '; *(PAD) ?(100[12]) Build/' + device_replacement: 'Terra $1$2' + brand_replacement: 'Terra' + model_replacement: '$1$2' + + ######### + # Texet + # @ref: http://www.texet.ru/tablet/ + ######### + - regex: '; *(T[BM]-\d{3}[^;/]+) Build/' + device_replacement: '$1' + brand_replacement: 'Texet' + model_replacement: '$1' + + ######### + # Thalia + # @ref: http://www.thalia.de/shop/tolino-shine-ereader/show/ + ######### + - regex: '; *(tolino [^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Thalia' + model_replacement: '$1' + - regex: '; *Build/.* (TOLINO_BROWSER)' + device_replacement: '$1' + brand_replacement: 'Thalia' + model_replacement: 'Tolino Shine' + + ######### + # Thl + # @ref: http://en.thl.com.cn/Mobile + # @ref: http://thlmobilestore.com + ######### + - regex: '; *(?:CJ[ -])?(ThL|THL)[ -]([^;/]+) Build/' + device_replacement: '$1 $2' + brand_replacement: 'Thl' + model_replacement: '$2' + - regex: '; *(T100|T200|T5|W100|W200|W8s) Build/' + device_replacement: '$1' + brand_replacement: 'Thl' + model_replacement: '$1' + + ######### + # T-Mobile (Operator Branded Devices) + ######### + # @ref: https://en.wikipedia.org/wiki/HTC_Hero + - regex: '; *(T-Mobile[ _]G2[ _]Touch) Build' + device_replacement: '$1' + brand_replacement: 'HTC' + model_replacement: 'Hero' + # @ref: https://en.wikipedia.org/wiki/HTC_Desire_Z + - regex: '; *(T-Mobile[ _]G2) Build' + device_replacement: '$1' + brand_replacement: 'HTC' + model_replacement: 'Desire Z' + - regex: '; *(T-Mobile myTouch Q) Build' + device_replacement: '$1' + brand_replacement: 'Huawei' + model_replacement: 'U8730' + - regex: '; *(T-Mobile myTouch) Build' + device_replacement: '$1' + brand_replacement: 'Huawei' + model_replacement: 'U8680' + - regex: '; *(T-Mobile_Espresso) Build' + device_replacement: '$1' + brand_replacement: 'HTC' + model_replacement: 'Espresso' + - regex: '; *(T-Mobile G1) Build' + device_replacement: '$1' + brand_replacement: 'HTC' + model_replacement: 'Dream' + - regex: '\b(T-Mobile ?)?(myTouch)[ _]?([34]G)[ _]?([^\/]*) (?:Mozilla|Build)' + device_replacement: '$1$2 $3 $4' + brand_replacement: 'HTC' + model_replacement: '$2 $3 $4' + - regex: '\b(T-Mobile)_([^_]+)_(.*) Build' + device_replacement: '$1 $2 $3' + brand_replacement: 'Tmobile' + model_replacement: '$2 $3' + - regex: '\b(T-Mobile)[_ ]?(.*?)Build' + device_replacement: '$1 $2' + brand_replacement: 'Tmobile' + model_replacement: '$2' + + ######### + # Tomtec + # @ref: http://www.tom-tec.eu/pages/tablets.php + ######### + - regex: ' (ATP[0-9]{4}) Build' + device_replacement: '$1' + brand_replacement: 'Tomtec' + model_replacement: '$1' + + ######### + # Tooky + # @ref: http://www.tookymobile.com/ + ######### + - regex: ' *(TOOKY)[ _\-]([^;/]+) ?(?:Build|;)' + regex_flag: 'i' + device_replacement: '$1 $2' + brand_replacement: 'Tooky' + model_replacement: '$2' + + ######### + # Toshiba + # @ref: http://www.toshiba.co.jp/ + # @missing: LT170, Thrive 7, TOSHIBA STB10 + ######### + - regex: '\b(TOSHIBA_AC_AND_AZ|TOSHIBA_FOLIO_AND_A|FOLIO_AND_A)' + device_replacement: '$1' + brand_replacement: 'Toshiba' + model_replacement: 'Folio 100' + - regex: '; *([Ff]olio ?100) Build/' + device_replacement: '$1' + brand_replacement: 'Toshiba' + model_replacement: 'Folio 100' + - regex: '; *(AT[0-9]{2,3}(?:\-A|LE\-A|PE\-A|SE|a)?|AT7-A|AT1S0|Hikari-iFrame/WDPF-[^;/]+|THRiVE|Thrive) Build/' + device_replacement: 'Toshiba $1' + brand_replacement: 'Toshiba' + model_replacement: '$1' + + ######### + # Touchmate + # @ref: http://touchmatepc.com/new/ + ######### + - regex: '; *(TM-MID\d+[^;/]+|TOUCHMATE|MID-750) Build' + device_replacement: '$1' + brand_replacement: 'Touchmate' + model_replacement: '$1' + # @todo: needs verification user-agents missing + - regex: '; *(TM-SM\d+[^;/]+) Build' + device_replacement: '$1' + brand_replacement: 'Touchmate' + model_replacement: '$1' + + ######### + # Treq + # @ref: http://www.treq.co.id/product + ######### + - regex: '; *(A10 [Bb]asic2?) Build/' + device_replacement: '$1' + brand_replacement: 'Treq' + model_replacement: '$1' + - regex: '; *(TREQ[ _\-])([^;/]+) Build' + regex_flag: 'i' + device_replacement: '$1$2' + brand_replacement: 'Treq' + model_replacement: '$2' + + ######### + # Umeox + # @ref: http://umeox.com/ + # @models: A936|A603|X-5|X-3 + ######### + # @todo: guessed markers + - regex: '; *(X-?5|X-?3) Build/' + device_replacement: '$1' + brand_replacement: 'Umeox' + model_replacement: '$1' + # @todo: guessed markers + - regex: '; *(A502\+?|A936|A603|X1|X2) Build/' + device_replacement: '$1' + brand_replacement: 'Umeox' + model_replacement: '$1' + + ######### + # Versus + # @ref: http://versusuk.com/support.html + ######### + - regex: '(TOUCH(?:TAB|PAD).+?) Build/' + regex_flag: 'i' + device_replacement: 'Versus $1' + brand_replacement: 'Versus' + model_replacement: '$1' + + ######### + # Vertu + # @ref: http://www.vertu.com/ + ######### + - regex: '(VERTU) ([^;/]+) Build/' + device_replacement: '$1 $2' + brand_replacement: 'Vertu' + model_replacement: '$2' + + ######### + # Videocon + # @ref: http://www.videoconmobiles.com + ######### + - regex: '; *(Videocon)[ _\-]([^;/]+) *(?:Build|;)' + device_replacement: '$1 $2' + brand_replacement: 'Videocon' + model_replacement: '$2' + - regex: ' (VT\d{2}[A-Za-z]*) Build' + device_replacement: '$1' + brand_replacement: 'Videocon' + model_replacement: '$1' + + ######### + # Viewsonic + # @ref: http://viewsonic.com + ######### + - regex: '; *((?:ViewPad|ViewPhone|VSD)[^;/]+) Build/' + device_replacement: '$1' + brand_replacement: 'Viewsonic' + model_replacement: '$1' + - regex: '; *(ViewSonic-)([^;/]+) Build/' + device_replacement: '$1$2' + brand_replacement: 'Viewsonic' + model_replacement: '$2' + - regex: '; *(GTablet.*) Build/' + device_replacement: '$1' + brand_replacement: 'Viewsonic' + model_replacement: '$1' + + ######### + # vivo + # @ref: http://vivo.cn/ + ######### + - regex: '; *([Vv]ivo)[ _]([^;/]+) Build' + device_replacement: '$1 $2' + brand_replacement: 'vivo' + model_replacement: '$2' + + ######### + # Vodafone (Operator Branded Devices) + # @ref: ?? + ######### + - regex: '(Vodafone) (.*) Build/' + device_replacement: '$1 $2' + brand_replacement: '$1' + model_replacement: '$2' + + ######### + # Walton + # @ref: http://www.waltonbd.com/ + ######### + - regex: '; *(?:Walton[ _\-])?(Primo[ _\-][^;/]+) Build' + regex_flag: 'i' + device_replacement: 'Walton $1' + brand_replacement: 'Walton' + model_replacement: '$1' + + ######### + # Wiko + # @ref: http://fr.wikomobile.com/collection.php?s=Smartphones + ######### + - regex: '; *(?:WIKO[ \-])?(CINK\+?|BARRY|BLOOM|DARKFULL|DARKMOON|DARKNIGHT|DARKSIDE|FIZZ|HIGHWAY|IGGY|OZZY|RAINBOW|STAIRWAY|SUBLIM|WAX|CINK [^;/]+) Build/' + regex_flag: 'i' + device_replacement: 'Wiko $1' + brand_replacement: 'Wiko' + model_replacement: '$1' + + ######### + # WellcoM + # @ref: ?? + ######### + - regex: '; *WellcoM-([^;/]+) Build' + device_replacement: 'Wellcom $1' + brand_replacement: 'Wellcom' + model_replacement: '$1' + + ########## + # WeTab + # @ref: http://wetab.mobi/ + ########## + - regex: '(?:(WeTab)-Browser|; (wetab) Build)' + device_replacement: '$1' + brand_replacement: 'WeTab' + model_replacement: 'WeTab' + + ######### + # Wolfgang + # @ref: http://wolfgangmobile.com/ + ######### + - regex: '; *(AT-AS[^;/]+) Build' + device_replacement: 'Wolfgang $1' + brand_replacement: 'Wolfgang' + model_replacement: '$1' + + ######### + # Woxter + # @ref: http://www.woxter.es/es-es/categories/index + ######### + - regex: '; *(?:Woxter|Wxt) ([^;/]+) Build' + device_replacement: 'Woxter $1' + brand_replacement: 'Woxter' + model_replacement: '$1' + + ######### + # Yarvik Zania + # @ref: http://yarvik.com + ######### + - regex: '; *(?:Xenta |Luna )?(TAB[234][0-9]{2}|TAB0[78]-\d{3}|TAB0?9-\d{3}|TAB1[03]-\d{3}|SMP\d{2}-\d{3}) Build/' + device_replacement: 'Yarvik $1' + brand_replacement: 'Yarvik' + model_replacement: '$1' + + ######### + # Yifang + # @note: Needs to be at the very last as manufacturer builds for other brands. + # @ref: http://www.yifangdigital.com/ + # @models: M1010, M1011, M1007, M1008, M1005, M899, M899LP, M909, M8000, + # M8001, M8002, M8003, M849, M815, M816, M819, M805, M878, M780LPW, + # M778, M7000, M7000AD, M7000NBD, M7001, M7002, M7002KBD, M777, M767, + # M789, M799, M769, M757, M755, M753, M752, M739, M729, M723, M712, M727 + ######### + - regex: '; *([A-Z]{2,4})(M\d{3,}[A-Z]{2})([^;\)\/]*)(?: Build|[;\)])' + device_replacement: 'Yifang $1$2$3' + brand_replacement: 'Yifang' + model_replacement: '$2' + + ######### + # XiaoMi + # @ref: http://www.xiaomi.com/event/buyphone + ######### + - regex: '; *((MI|HM|MI-ONE|Redmi)[ -](NOTE |Note )?[^;/]*) (Build|MIUI)/' + device_replacement: 'XiaoMi $1' + brand_replacement: 'XiaoMi' + model_replacement: '$1' + + ######### + # Xolo + # @ref: http://www.xolo.in/ + ######### + - regex: '; *XOLO[ _]([^;/]*tab.*) Build' + regex_flag: 'i' + device_replacement: 'Xolo $1' + brand_replacement: 'Xolo' + model_replacement: '$1' + - regex: '; *XOLO[ _]([^;/]+) Build' + regex_flag: 'i' + device_replacement: 'Xolo $1' + brand_replacement: 'Xolo' + model_replacement: '$1' + - regex: '; *(q\d0{2,3}[a-z]?) Build' + regex_flag: 'i' + device_replacement: 'Xolo $1' + brand_replacement: 'Xolo' + model_replacement: '$1' + + ######### + # Xoro + # @ref: http://www.xoro.de/produkte/ + ######### + - regex: '; *(PAD ?[79]\d+[^;/]*|TelePAD\d+[^;/]) Build' + device_replacement: 'Xoro $1' + brand_replacement: 'Xoro' + model_replacement: '$1' + + ######### + # Zopo + # @ref: http://www.zopomobiles.com/products.html + ######### + - regex: '; *(?:(?:ZOPO|Zopo)[ _]([^;/]+)|(ZP ?(?:\d{2}[^;/]+|C2))|(C[2379])) Build' + device_replacement: '$1$2$3' + brand_replacement: 'Zopo' + model_replacement: '$1$2$3' + + ######### + # ZiiLabs + # @ref: http://www.ziilabs.com/products/platforms/androidreferencetablets.php + ######### + - regex: '; *(ZiiLABS) (Zii[^;/]*) Build' + device_replacement: '$1 $2' + brand_replacement: 'ZiiLabs' + model_replacement: '$2' + - regex: '; *(Zii)_([^;/]*) Build' + device_replacement: '$1 $2' + brand_replacement: 'ZiiLabs' + model_replacement: '$2' + + ######### + # ZTE + # @ref: http://www.ztedevices.com/ + ######### + - regex: '; *(ARIZONA|(?:ATLAS|Atlas) W|D930|Grand (?:[SX][^;]*|Era|Memo[^;]*)|JOE|(?:Kis|KIS)\b[^;]*|Libra|Light [^;]*|N8[056][01]|N850L|N8000|N9[15]\d{2}|N9810|NX501|Optik|(?:Vip )Racer[^;]*|RacerII|RACERII|San Francisco[^;]*|V9[AC]|V55|V881|Z[679][0-9]{2}[A-z]?) Build' + device_replacement: '$1' + brand_replacement: 'ZTE' + model_replacement: '$1' + - regex: '; *([A-Z]\d+)_USA_[^;]* Build' + device_replacement: '$1' + brand_replacement: 'ZTE' + model_replacement: '$1' + - regex: '; *(SmartTab\d+)[^;]* Build' + device_replacement: '$1' + brand_replacement: 'ZTE' + model_replacement: '$1' + - regex: '; *(?:Blade|BLADE|ZTE-BLADE)([^;/]*) Build' + device_replacement: 'ZTE Blade$1' + brand_replacement: 'ZTE' + model_replacement: 'Blade$1' + - regex: '; *(?:Skate|SKATE|ZTE-SKATE)([^;/]*) Build' + device_replacement: 'ZTE Skate$1' + brand_replacement: 'ZTE' + model_replacement: 'Skate$1' + - regex: '; *(Orange |Optimus )(Monte Carlo|San Francisco) Build' + device_replacement: '$1$2' + brand_replacement: 'ZTE' + model_replacement: '$1$2' + - regex: '; *(?:ZXY-ZTE_|ZTE\-U |ZTE[\- _]|ZTE-C[_ ])([^;/]+) Build' + device_replacement: 'ZTE $1' + brand_replacement: 'ZTE' + model_replacement: '$1' + # operator specific + - regex: '; (BASE) (lutea|Lutea 2|Tab[^;]*) Build' + device_replacement: '$1 $2' + brand_replacement: 'ZTE' + model_replacement: '$1 $2' + - regex: '; (Avea inTouch 2|soft stone|tmn smart a7|Movistar[ _]Link) Build' + regex_flag: 'i' + device_replacement: '$1' + brand_replacement: 'ZTE' + model_replacement: '$1' + - regex: '; *(vp9plus)\)' + device_replacement: '$1' + brand_replacement: 'ZTE' + model_replacement: '$1' + + ########## + # Zync + # @ref: http://www.zync.in/index.php/our-products/tablet-phablets + ########## + - regex: '; ?(Cloud[ _]Z5|z1000|Z99 2G|z99|z930|z999|z990|z909|Z919|z900) Build/' + device_replacement: '$1' + brand_replacement: 'Zync' + model_replacement: '$1' + + ########## + # Kindle + # @note: Needs to be after Sony Playstation Vita as this UA contains Silk/3.2 + # @ref: https://developer.amazon.com/sdk/fire/specifications.html + # @ref: http://amazonsilk.wordpress.com/useful-bits/silk-user-agent/ + ########## + - regex: '; ?(KFOT|Kindle Fire) Build\b' + device_replacement: 'Kindle Fire' + brand_replacement: 'Amazon' + model_replacement: 'Kindle Fire' + - regex: '; ?(KFOTE|Amazon Kindle Fire2) Build\b' + device_replacement: 'Kindle Fire 2' + brand_replacement: 'Amazon' + model_replacement: 'Kindle Fire 2' + - regex: '; ?(KFTT) Build\b' + device_replacement: 'Kindle Fire HD' + brand_replacement: 'Amazon' + model_replacement: 'Kindle Fire HD 7"' + - regex: '; ?(KFJWI) Build\b' + device_replacement: 'Kindle Fire HD 8.9" WiFi' + brand_replacement: 'Amazon' + model_replacement: 'Kindle Fire HD 8.9" WiFi' + - regex: '; ?(KFJWA) Build\b' + device_replacement: 'Kindle Fire HD 8.9" 4G' + brand_replacement: 'Amazon' + model_replacement: 'Kindle Fire HD 8.9" 4G' + - regex: '; ?(KFSOWI) Build\b' + device_replacement: 'Kindle Fire HD 7" WiFi' + brand_replacement: 'Amazon' + model_replacement: 'Kindle Fire HD 7" WiFi' + - regex: '; ?(KFTHWI) Build\b' + device_replacement: 'Kindle Fire HDX 7" WiFi' + brand_replacement: 'Amazon' + model_replacement: 'Kindle Fire HDX 7" WiFi' + - regex: '; ?(KFTHWA) Build\b' + device_replacement: 'Kindle Fire HDX 7" 4G' + brand_replacement: 'Amazon' + model_replacement: 'Kindle Fire HDX 7" 4G' + - regex: '; ?(KFAPWI) Build\b' + device_replacement: 'Kindle Fire HDX 8.9" WiFi' + brand_replacement: 'Amazon' + model_replacement: 'Kindle Fire HDX 8.9" WiFi' + - regex: '; ?(KFAPWA) Build\b' + device_replacement: 'Kindle Fire HDX 8.9" 4G' + brand_replacement: 'Amazon' + model_replacement: 'Kindle Fire HDX 8.9" 4G' + - regex: '; ?Amazon ([^;/]+) Build\b' + device_replacement: '$1' + brand_replacement: 'Amazon' + model_replacement: '$1' + - regex: '; ?(Kindle) Build\b' + device_replacement: 'Kindle' + brand_replacement: 'Amazon' + model_replacement: 'Kindle' + - regex: '; ?(Silk)/(\d+)\.(\d+)(?:\.([0-9\-]+))? Build\b' + device_replacement: 'Kindle Fire' + brand_replacement: 'Amazon' + model_replacement: 'Kindle Fire$2' + - regex: ' (Kindle)/(\d+\.\d+)' + device_replacement: 'Kindle' + brand_replacement: 'Amazon' + model_replacement: '$1 $2' + - regex: ' (Silk|Kindle)/(\d+)\.' + device_replacement: 'Kindle' + brand_replacement: 'Amazon' + model_replacement: 'Kindle' + + ######### + # Devices from chinese manufacturer(s) + # @note: identified by x-wap-profile http://218.249.47.94/Xianghe/.* + ######### + - regex: '(sprd)\-([^/]+)/' + device_replacement: '$1 $2' + brand_replacement: '$1' + model_replacement: '$2' + # @ref: http://eshinechina.en.alibaba.com/ + - regex: '; *(H\d{2}00\+?) Build' + device_replacement: '$1' + brand_replacement: 'Hero' + model_replacement: '$1' + - regex: '; *(iphone|iPhone5) Build/' + device_replacement: 'Xianghe $1' + brand_replacement: 'Xianghe' + model_replacement: '$1' + - regex: '; *(e\d{4}[a-z]?_?v\d+|v89_[^;/]+)[^;/]+ Build/' + device_replacement: 'Xianghe $1' + brand_replacement: 'Xianghe' + model_replacement: '$1' + + ######### + # Cellular + # @ref: + # @note: Operator branded devices + ######### + - regex: '\bUSCC[_\-]?([^ ;/\)]+)' + device_replacement: '$1' + brand_replacement: 'Cellular' + model_replacement: '$1' + + ###################################################################### + # Windows Phone Parsers + ###################################################################### + + ######### + # Alcatel Windows Phones + ######### + - regex: 'Windows Phone [^;]+; .*?IEMobile/[^;\)]+[;\)] ?(?:ARM; ?Touch; ?|Touch; ?)?(?:ALCATEL)[^;]*; *([^;,\)]+)' + device_replacement: 'Alcatel $1' + brand_replacement: 'Alcatel' + model_replacement: '$1' + + ######### + # Asus Windows Phones + ######### + #~ - regex: 'Windows Phone [^;]+; .*?IEMobile/[^;\)]+[;\)] ?(?:ARM; ?Touch; ?|Touch; ?|WpsLondonTest; ?)?(?:ASUS|Asus)[^;]*; *([^;,\)]+)' + - regex: 'Windows Phone [^;]+; .*?IEMobile/[^;\)]+[;\)] ?(?:ARM; ?Touch; ?|Touch; ?|WpsLondonTest; ?)?(?:ASUS|Asus)[^;]*; *([^;,\)]+)' + device_replacement: 'Asus $1' + brand_replacement: 'Asus' + model_replacement: '$1' + + ######### + # Dell Windows Phones + ######### + - regex: 'Windows Phone [^;]+; .*?IEMobile/[^;\)]+[;\)] ?(?:ARM; ?Touch; ?|Touch; ?)?(?:DELL|Dell)[^;]*; *([^;,\)]+)' + device_replacement: 'Dell $1' + brand_replacement: 'Dell' + model_replacement: '$1' + + ######### + # HTC Windows Phones + ######### + - regex: 'Windows Phone [^;]+; .*?IEMobile/[^;\)]+[;\)] ?(?:ARM; ?Touch; ?|Touch; ?|WpsLondonTest; ?)?(?:HTC|Htc|HTC_blocked[^;]*)[^;]*; *(?:HTC)?([^;,\)]+)' + device_replacement: 'HTC $1' + brand_replacement: 'HTC' + model_replacement: '$1' + + ######### + # Huawei Windows Phones + ######### + - regex: 'Windows Phone [^;]+; .*?IEMobile/[^;\)]+[;\)] ?(?:ARM; ?Touch; ?|Touch; ?)?(?:HUAWEI)[^;]*; *(?:HUAWEI )?([^;,\)]+)' + device_replacement: 'Huawei $1' + brand_replacement: 'Huawei' + model_replacement: '$1' + + ######### + # LG Windows Phones + ######### + - regex: 'Windows Phone [^;]+; .*?IEMobile/[^;\)]+[;\)] ?(?:ARM; ?Touch; ?|Touch; ?)?(?:LG|Lg)[^;]*; *(?:LG[ \-])?([^;,\)]+)' + device_replacement: 'LG $1' + brand_replacement: 'LG' + model_replacement: '$1' + + ######### + # Noka Windows Phones + ######### + - regex: 'Windows Phone [^;]+; .*?IEMobile/[^;\)]+[;\)] ?(?:ARM; ?Touch; ?|Touch; ?)?(?:NOKIA|Nokia)[^;]*; *(?:NOKIA ?|Nokia ?|LUMIA ?|[Ll]umia ?)*(\d{3,}[^;\)]*)' + device_replacement: 'Lumia $1' + brand_replacement: 'Nokia' + model_replacement: 'Lumia $1' + - regex: 'Windows Phone [^;]+; .*?IEMobile/[^;\)]+[;\)] ?(?:ARM; ?Touch; ?|Touch; ?)?(?:NOKIA|Nokia)[^;]*; *(RM-\d{3,})' + device_replacement: 'Nokia $1' + brand_replacement: 'Nokia' + model_replacement: '$1' + - regex: '(?:Windows Phone [^;]+; .*?IEMobile/[^;\)]+[;\)]|WPDesktop;) ?(?:ARM; ?Touch; ?|Touch; ?)?(?:NOKIA|Nokia)[^;]*; *(?:NOKIA ?|Nokia ?|LUMIA ?|[Ll]umia ?)*([^;\)]+)' + device_replacement: 'Nokia $1' + brand_replacement: 'Nokia' + model_replacement: '$1' + + ######### + # Microsoft Windows Phones + ######### + - regex: 'Windows Phone [^;]+; .*?IEMobile/[^;\)]+[;\)] ?(?:ARM; ?Touch; ?|Touch; ?)?(?:Microsoft(?: Corporation)?)[^;]*; *([^;,\)]+)' + device_replacement: 'Microsoft $1' + brand_replacement: 'Microsoft' + model_replacement: '$1' + + ######### + # Samsung Windows Phones + ######### + - regex: 'Windows Phone [^;]+; .*?IEMobile/[^;\)]+[;\)] ?(?:ARM; ?Touch; ?|Touch; ?|WpsLondonTest; ?)?(?:SAMSUNG)[^;]*; *(?:SAMSUNG )?([^;,\.\)]+)' + device_replacement: 'Samsung $1' + brand_replacement: 'Samsung' + model_replacement: '$1' + + ######### + # Toshiba Windows Phones + ######### + - regex: 'Windows Phone [^;]+; .*?IEMobile/[^;\)]+[;\)] ?(?:ARM; ?Touch; ?|Touch; ?|WpsLondonTest; ?)?(?:TOSHIBA|FujitsuToshibaMobileCommun)[^;]*; *([^;,\)]+)' + device_replacement: 'Toshiba $1' + brand_replacement: 'Toshiba' + model_replacement: '$1' + + ######### + # Generic Windows Phones + ######### + - regex: 'Windows Phone [^;]+; .*?IEMobile/[^;\)]+[;\)] ?(?:ARM; ?Touch; ?|Touch; ?|WpsLondonTest; ?)?([^;]+); *([^;,\)]+)' + device_replacement: '$1 $2' + brand_replacement: '$1' + model_replacement: '$2' + + ###################################################################### + # Other Devices Parser + ###################################################################### + + ######### + # Samsung Bada Phones + ######### + - regex: '(?:^|; )SAMSUNG\-([A-Za-z0-9\-]+).* Bada/' + device_replacement: 'Samsung $1' + brand_replacement: 'Samsung' + model_replacement: '$1' + + ######### + # Firefox OS + ######### + - regex: '\(Mobile; ALCATEL ?(One|ONE) ?(Touch|TOUCH) ?([^;/]+)(?:/[^;]+)?; rv:[^\)]+\) Gecko/[^\/]+ Firefox/' + device_replacement: 'Alcatel $1 $2 $3' + brand_replacement: 'Alcatel' + model_replacement: 'One Touch $3' + - regex: '\(Mobile; (?:ZTE([^;]+)|(OpenC)); rv:[^\)]+\) Gecko/[^\/]+ Firefox/' + device_replacement: 'ZTE $1$2' + brand_replacement: 'ZTE' + model_replacement: '$1$2' + + ########## + # NOKIA + # @note: NokiaN8-00 comes before iphone. Sometimes spoofs iphone + ########## + - regex: 'Nokia(N[0-9]+)([A-z_\-][A-z0-9_\-]*)' + device_replacement: 'Nokia $1' + brand_replacement: 'Nokia' + model_replacement: '$1$2' + - regex: '(?:NOKIA|Nokia)(?:\-| *)(?:([A-Za-z0-9]+)\-[0-9a-f]{32}|([A-Za-z0-9\-]+)(?:UCBrowser)|([A-Za-z0-9\-]+))' + device_replacement: 'Nokia $1$2$3' + brand_replacement: 'Nokia' + model_replacement: '$1$2$3' + - regex: 'Lumia ([A-Za-z0-9\-]+)' + device_replacement: 'Lumia $1' + brand_replacement: 'Nokia' + model_replacement: 'Lumia $1' + # UCWEB Browser on Symbian + - regex: '\(Symbian; U; S60 V5; [A-z]{2}\-[A-z]{2}; (SonyEricsson|Samsung|Nokia|LG)([^;/]+)\)' + device_replacement: '$1 $2' + brand_replacement: '$1' + model_replacement: '$2' + # Nokia Symbian + - regex: '\(Symbian(?:/3)?; U; ([^;]+);' + device_replacement: 'Nokia $1' + brand_replacement: 'Nokia' + model_replacement: '$1' + + ########## + # BlackBerry + # @ref: http://www.useragentstring.com/pages/BlackBerry/ + ########## + - regex: 'BB10; ([A-Za-z0-9\- ]+)\)' + device_replacement: 'BlackBerry $1' + brand_replacement: 'BlackBerry' + model_replacement: '$1' + - regex: 'Play[Bb]ook.+RIM Tablet OS' + device_replacement: 'BlackBerry Playbook' + brand_replacement: 'BlackBerry' + model_replacement: 'Playbook' + - regex: 'Black[Bb]erry ([0-9]+);' + device_replacement: 'BlackBerry $1' + brand_replacement: 'BlackBerry' + model_replacement: '$1' + - regex: 'Black[Bb]erry([0-9]+)' + device_replacement: 'BlackBerry $1' + brand_replacement: 'BlackBerry' + model_replacement: '$1' + - regex: 'Black[Bb]erry;' + device_replacement: 'BlackBerry' + brand_replacement: 'BlackBerry' + + ########## + # PALM / HP + # @note: some palm devices must come before iphone. sometimes spoofs iphone in ua + ########## + - regex: '(Pre|Pixi)/\d+\.\d+' + device_replacement: 'Palm $1' + brand_replacement: 'Palm' + model_replacement: '$1' + - regex: 'Palm([0-9]+)' + device_replacement: 'Palm $1' + brand_replacement: 'Palm' + model_replacement: '$1' + - regex: 'Treo([A-Za-z0-9]+)' + device_replacement: 'Palm Treo $1' + brand_replacement: 'Palm' + model_replacement: 'Treo $1' + - regex: 'webOS.*(P160U(?:NA)?)/(\d+).(\d+)' + device_replacement: 'HP Veer' + brand_replacement: 'HP' + model_replacement: 'Veer' + - regex: '(Touch[Pp]ad)/\d+\.\d+' + device_replacement: 'HP TouchPad' + brand_replacement: 'HP' + model_replacement: 'TouchPad' + - regex: 'HPiPAQ([A-Za-z0-9]+)/\d+.\d+' + device_replacement: 'HP iPAQ $1' + brand_replacement: 'HP' + model_replacement: 'iPAQ $1' + - regex: 'PDA; (PalmOS)/sony/model ([a-z]+)/Revision' + device_replacement: '$1' + brand_replacement: 'Sony' + model_replacement: '$1 $2' + + ########## + # AppleTV + # No built in browser that I can tell + # Stack Overflow indicated iTunes-AppleTV/4.1 as a known UA for app available and I'm seeing it in live traffic + ########## + - regex: '(Apple\s?TV)' + device_replacement: 'AppleTV' + brand_replacement: 'Apple' + model_replacement: 'AppleTV' + + ######### + # Tesla Model S + ######### + - regex: '(QtCarBrowser)' + device_replacement: 'Tesla Model S' + brand_replacement: 'Tesla' + model_replacement: 'Model S' + + ########## + # iSTUFF + # @note: complete but probably catches spoofs + # ipad and ipod must be parsed before iphone + # cannot determine specific device type from ua string. (3g, 3gs, 4, etc) + ########## + # @note: on some ua the device can be identified e.g. iPhone5,1 + - regex: '((?:iPhone|iPad|iPod)\d+,\d+)' + device_replacement: '$1' + brand_replacement: 'Apple' + model_replacement: '$1' + # @note: iPad needs to be before iPhone + - regex: '(iPad)(?:;| Simulator;)' + device_replacement: '$1' + brand_replacement: 'Apple' + model_replacement: '$1' + - regex: '(iPod)(?:;| touch;| Simulator;)' + device_replacement: '$1' + brand_replacement: 'Apple' + model_replacement: '$1' + - regex: '(iPhone)(?:;| Simulator;)' + device_replacement: '$1' + brand_replacement: 'Apple' + model_replacement: '$1' + # @note: desktop applications show device info + - regex: 'CFNetwork/.* Darwin/\d.*\(((?:Mac|iMac|PowerMac|PowerBook)[^\d]*)(\d+)(?:,|%2C)(\d+)' + device_replacement: '$1$2,$3' + brand_replacement: 'Apple' + model_replacement: '$1$2,$3' + # @note: iOS applications do not show device info + - regex: 'CFNetwork/.* Darwin/\d' + device_replacement: 'iOS-Device' + brand_replacement: 'Apple' + model_replacement: 'iOS-Device' + + ########## + # Acer + ########## + - regex: 'acer_([A-Za-z0-9]+)_' + device_replacement: 'Acer $1' + brand_replacement: 'Acer' + model_replacement: '$1' + + ########## + # Alcatel + ########## + - regex: '(?:ALCATEL|Alcatel)-([A-Za-z0-9\-]+)' + device_replacement: 'Alcatel $1' + brand_replacement: 'Alcatel' + model_replacement: '$1' + + ########## + # Amoi + ########## + - regex: '(?:Amoi|AMOI)\-([A-Za-z0-9]+)' + device_replacement: 'Amoi $1' + brand_replacement: 'Amoi' + model_replacement: '$1' + + ########## + # Asus + ########## + - regex: '(?:; |\/|^)((?:Transformer (?:Pad|Prime) |Transformer |PadFone[ _]?)[A-Za-z0-9]*)' + device_replacement: 'Asus $1' + brand_replacement: 'Asus' + model_replacement: '$1' + - regex: '(?:asus.*?ASUS|Asus|ASUS|asus)[\- ;]*((?:Transformer (?:Pad|Prime) |Transformer |Padfone |Nexus[ _])?[A-Za-z0-9]+)' + device_replacement: 'Asus $1' + brand_replacement: 'Asus' + model_replacement: '$1' + + + ########## + # Bird + ########## + - regex: '\bBIRD[ \-\.]([A-Za-z0-9]+)' + device_replacement: 'Bird $1' + brand_replacement: 'Bird' + model_replacement: '$1' + + ########## + # Dell + ########## + - regex: '\bDell ([A-Za-z0-9]+)' + device_replacement: 'Dell $1' + brand_replacement: 'Dell' + model_replacement: '$1' + + ########## + # DoCoMo + ########## + - regex: 'DoCoMo/2\.0 ([A-Za-z0-9]+)' + device_replacement: 'DoCoMo $1' + brand_replacement: 'DoCoMo' + model_replacement: '$1' + - regex: '([A-Za-z0-9]+)_W;FOMA' + device_replacement: 'DoCoMo $1' + brand_replacement: 'DoCoMo' + model_replacement: '$1' + - regex: '([A-Za-z0-9]+);FOMA' + device_replacement: 'DoCoMo $1' + brand_replacement: 'DoCoMo' + model_replacement: '$1' + + ########## + # htc + ########## + - regex: '\b(?:HTC/|HTC/[a-z0-9]+/)?HTC[ _\-;]? *(.*?)(?:-?Mozilla|fingerPrint|[;/\(\)]|$)' + device_replacement: 'HTC $1' + brand_replacement: 'HTC' + model_replacement: '$1' + + ########## + # Huawei + ########## + - regex: 'Huawei([A-Za-z0-9]+)' + device_replacement: 'Huawei $1' + brand_replacement: 'Huawei' + model_replacement: '$1' + - regex: 'HUAWEI-([A-Za-z0-9]+)' + device_replacement: 'Huawei $1' + brand_replacement: 'Huawei' + model_replacement: '$1' + - regex: 'vodafone([A-Za-z0-9]+)' + device_replacement: 'Huawei Vodafone $1' + brand_replacement: 'Huawei' + model_replacement: 'Vodafone $1' + + ########## + # i-mate + ########## + - regex: 'i\-mate ([A-Za-z0-9]+)' + device_replacement: 'i-mate $1' + brand_replacement: 'i-mate' + model_replacement: '$1' + + ########## + # kyocera + ########## + - regex: 'Kyocera\-([A-Za-z0-9]+)' + device_replacement: 'Kyocera $1' + brand_replacement: 'Kyocera' + model_replacement: '$1' + - regex: 'KWC\-([A-Za-z0-9]+)' + device_replacement: 'Kyocera $1' + brand_replacement: 'Kyocera' + model_replacement: '$1' + + ########## + # lenovo + ########## + - regex: 'Lenovo[_\-]([A-Za-z0-9]+)' + device_replacement: 'Lenovo $1' + brand_replacement: 'Lenovo' + model_replacement: '$1' + + ########## + # HbbTV (European and Australian standard) + # written before the LG regexes, as LG is making HbbTV too + ########## + - regex: '(HbbTV)/[0-9]+\.[0-9]+\.[0-9]+ \([^;]*; *(LG)E *; *([^;]*) *;[^;]*;[^;]*;\)' + device_replacement: '$1' + brand_replacement: '$2' + model_replacement: '$3' + - regex: '(HbbTV)/1\.1\.1.*CE-HTML/1\.\d;(Vendor/)*(THOM[^;]*?)[;\s](?:.*SW-Version/.*)*(LF[^;]+);?' + device_replacement: '$1' + brand_replacement: 'Thomson' + model_replacement: '$4' + - regex: '(HbbTV)(?:/1\.1\.1)?(?: ?\(;;;;;\))?; *CE-HTML(?:/1\.\d)?; *([^ ]+) ([^;]+);' + device_replacement: '$1' + brand_replacement: '$2' + model_replacement: '$3' + - regex: '(HbbTV)/1\.1\.1 \(;;;;;\) Maple_2011' + device_replacement: '$1' + brand_replacement: 'Samsung' + - regex: '(HbbTV)/[0-9]+\.[0-9]+\.[0-9]+ \([^;]*; *(?:CUS:([^;]*)|([^;]+)) *; *([^;]*) *;.*;' + device_replacement: '$1' + brand_replacement: '$2$3' + model_replacement: '$4' + - regex: '(HbbTV)/[0-9]+\.[0-9]+\.[0-9]+' + device_replacement: '$1' + + ########## + # LGE NetCast TV + ########## + - regex: 'LGE; (?:Media\/)?([^;]*);[^;]*;[^;]*;?\); "?LG NetCast(\.TV|\.Media|)-\d+' + device_replacement: 'NetCast$2' + brand_replacement: 'LG' + model_replacement: '$1' + + ########## + # InettvBrowser + ########## + - regex: 'InettvBrowser/[0-9]+\.[0-9A-Z]+ \([^;]*;(Sony)([^;]*);[^;]*;[^\)]*\)' + device_replacement: 'Inettv' + brand_replacement: '$1' + model_replacement: '$2' + - regex: 'InettvBrowser/[0-9]+\.[0-9A-Z]+ \([^;]*;([^;]*);[^;]*;[^\)]*\)' + device_replacement: 'Inettv' + brand_replacement: 'Generic_Inettv' + model_replacement: '$1' + - regex: '(?:InettvBrowser|TSBNetTV|NETTV|HBBTV)' + device_replacement: 'Inettv' + brand_replacement: 'Generic_Inettv' + + ########## + # lg + ########## + # LG Symbian Phones + - regex: 'Series60/\d\.\d (LG)[\-]?([A-Za-z0-9 \-]+)' + device_replacement: '$1 $2' + brand_replacement: '$1' + model_replacement: '$2' + # other LG phones + - regex: '\b(?:LGE[ \-]LG\-(?:AX)?|LGE |LGE?-LG|LGE?[ \-]|LG[ /\-]|lg[\-])([A-Za-z0-9]+)\b' + device_replacement: 'LG $1' + brand_replacement: 'LG' + model_replacement: '$1' + - regex: '(?:^LG[\-]?|^LGE[\-/]?)([A-Za-z]+[0-9]+[A-Za-z]*)' + device_replacement: 'LG $1' + brand_replacement: 'LG' + model_replacement: '$1' + - regex: '^LG([0-9]+[A-Za-z]*)' + device_replacement: 'LG $1' + brand_replacement: 'LG' + model_replacement: '$1' + + ########## + # microsoft + ########## + - regex: '(KIN\.[^ ]+) (\d+)\.(\d+)' + device_replacement: 'Microsoft $1' + brand_replacement: 'Microsoft' + model_replacement: '$1' + - regex: '(?:MSIE|XBMC).*\b(Xbox)\b' + device_replacement: '$1' + brand_replacement: 'Microsoft' + model_replacement: '$1' + - regex: '; ARM; Trident/6\.0; Touch[\);]' + device_replacement: 'Microsoft Surface RT' + brand_replacement: 'Microsoft' + model_replacement: 'Surface RT' + + ########## + # motorola + ########## + - regex: 'Motorola\-([A-Za-z0-9]+)' + device_replacement: 'Motorola $1' + brand_replacement: 'Motorola' + model_replacement: '$1' + - regex: 'MOTO\-([A-Za-z0-9]+)' + device_replacement: 'Motorola $1' + brand_replacement: 'Motorola' + model_replacement: '$1' + - regex: 'MOT\-([A-z0-9][A-z0-9\-]*)' + device_replacement: 'Motorola $1' + brand_replacement: 'Motorola' + model_replacement: '$1' + + ########## + # nintendo + ########## + - regex: 'Nintendo WiiU' + device_replacement: 'Nintendo Wii U' + brand_replacement: 'Nintendo' + model_replacement: 'Wii U' + - regex: 'Nintendo (DS|3DS|DSi|Wii);' + device_replacement: 'Nintendo $1' + brand_replacement: 'Nintendo' + model_replacement: '$1' + + ########## + # pantech + ########## + - regex: '(?:Pantech|PANTECH)[ _-]?([A-Za-z0-9\-]+)' + device_replacement: 'Pantech $1' + brand_replacement: 'Pantech' + model_replacement: '$1' + + ########## + # philips + ########## + - regex: 'Philips([A-Za-z0-9]+)' + device_replacement: 'Philips $1' + brand_replacement: 'Philips' + model_replacement: '$1' + - regex: 'Philips ([A-Za-z0-9]+)' + device_replacement: 'Philips $1' + brand_replacement: 'Philips' + model_replacement: '$1' + + ########## + # Samsung + ########## + # Samsung Symbian Devices + - regex: 'SymbianOS/9\.\d.* Samsung[/\-]([A-Za-z0-9 \-]+)' + device_replacement: 'Samsung $1' + brand_replacement: 'Samsung' + model_replacement: '$1' + - regex: '(Samsung)(SGH)(i[0-9]+)' + device_replacement: '$1 $2$3' + brand_replacement: '$1' + model_replacement: '$2-$3' + - regex: 'SAMSUNG-ANDROID-MMS/([^;/]+)' + device_replacement: '$1' + brand_replacement: 'Samsung' + model_replacement: '$1' + # Other Samsung + #- regex: 'SAMSUNG(?:; |-)([A-Za-z0-9\-]+)' + - regex: 'SAMSUNG(?:; |[ -/])([A-Za-z0-9\-]+)' + regex_flag: 'i' + device_replacement: 'Samsung $1' + brand_replacement: 'Samsung' + model_replacement: '$1' + + ########## + # Sega + ########## + - regex: '(Dreamcast)' + device_replacement: 'Sega $1' + brand_replacement: 'Sega' + model_replacement: '$1' + + ########## + # Siemens mobile + ########## + - regex: '^SIE-([A-Za-z0-9]+)' + device_replacement: 'Siemens $1' + brand_replacement: 'Siemens' + model_replacement: '$1' + + ########## + # Softbank + ########## + - regex: 'Softbank/[12]\.0/([A-Za-z0-9]+)' + device_replacement: 'Softbank $1' + brand_replacement: 'Softbank' + model_replacement: '$1' + + ########## + # SonyEricsson + ########## + - regex: 'SonyEricsson ?([A-Za-z0-9\-]+)' + device_replacement: 'Ericsson $1' + brand_replacement: 'SonyEricsson' + model_replacement: '$1' + + ########## + # Sony + ########## + - regex: 'Android [^;]+; ([^ ]+) (Sony)/' + device_replacement: '$2 $1' + brand_replacement: '$2' + model_replacement: '$1' + - regex: '(Sony)(?:BDP\/|\/)?([^ /;\)]+)[ /;\)]' + device_replacement: '$1 $2' + brand_replacement: '$1' + model_replacement: '$2' + + ######### + # Puffin Browser Device detect + # A=Android, I=iOS, P=Phone, T=Tablet + # AT=Android+Tablet + ######### + - regex: 'Puffin/[\d\.]+IT' + device_replacement: 'iPad' + brand_replacement: 'Apple' + model_replacement: 'iPad' + - regex: 'Puffin/[\d\.]+IP' + device_replacement: 'iPhone' + brand_replacement: 'Apple' + model_replacement: 'iPhone' + - regex: 'Puffin/[\d\.]+AT' + device_replacement: 'Generic Tablet' + brand_replacement: 'Generic' + model_replacement: 'Tablet' + - regex: 'Puffin/[\d\.]+AP' + device_replacement: 'Generic Smartphone' + brand_replacement: 'Generic' + model_replacement: 'Smartphone' + + ######### + # Android General Device Matching (far from perfect) + ######### + - regex: 'Android[\- ][\d]+\.[\d]+; [A-Za-z]{2}\-[A-Za-z]{0,2}; WOWMobile (.+) Build' + brand_replacement: 'Generic_Android' + model_replacement: '$1' + - regex: 'Android[\- ][\d]+\.[\d]+\-update1; [A-Za-z]{2}\-[A-Za-z]{0,2} *; *(.+?) Build' + brand_replacement: 'Generic_Android' + model_replacement: '$1' + - regex: 'Android[\- ][\d]+(?:\.[\d]+){1,2}; *[A-Za-z]{2}[_\-][A-Za-z]{0,2}\-? *; *(.+?) Build' + brand_replacement: 'Generic_Android' + model_replacement: '$1' + - regex: 'Android[\- ][\d]+(?:\.[\d]+){1,2}; *[A-Za-z]{0,2}\- *; *(.+?) Build' + brand_replacement: 'Generic_Android' + model_replacement: '$1' + # No build info at all - "Build" follows locale immediately + - regex: 'Android[\- ][\d]+(?:\.[\d]+){1,2}; *[a-z]{0,2}[_\-]?[A-Za-z]{0,2};? Build' + device_replacement: 'Generic Smartphone' + brand_replacement: 'Generic' + model_replacement: 'Smartphone' + - regex: 'Android[\- ][\d]+(?:\.[\d]+){1,2}; *\-?[A-Za-z]{2}; *(.+?) Build' + brand_replacement: 'Generic_Android' + model_replacement: '$1' + - regex: 'Android[\- ][\d]+(?:\.[\d]+){1,2}(?:;.*)?; *(.+?) Build' + brand_replacement: 'Generic_Android' + model_replacement: '$1' + + ########## + # Google TV + ########## + - regex: '(GoogleTV)' + brand_replacement: 'Generic_Inettv' + model_replacement: '$1' + + ########## + # WebTV + ########## + - regex: '(WebTV)/\d+.\d+' + brand_replacement: 'Generic_Inettv' + model_replacement: '$1' + # Roku Digital-Video-Players https://www.roku.com/ + - regex: '^(Roku)/DVP-\d+\.\d+' + brand_replacement: 'Generic_Inettv' + model_replacement: '$1' + + ########## + # Generic Tablet + ########## + - regex: '(Android 3\.\d|Opera Tablet|Tablet; .+Firefox/|Android.*(?:Tab|Pad))' + regex_flag: 'i' + device_replacement: 'Generic Tablet' + brand_replacement: 'Generic' + model_replacement: 'Tablet' + + ########## + # Generic Smart Phone + ########## + - regex: '(Symbian|\bS60(Version|V\d)|\bS60\b|\((Series 60|Windows Mobile|Palm OS|Bada); Opera Mini|Windows CE|Opera Mobi|BREW|Brew|Mobile; .+Firefox/|iPhone OS|Android|MobileSafari|Windows *Phone|\(webOS/|PalmOS)' + device_replacement: 'Generic Smartphone' + brand_replacement: 'Generic' + model_replacement: 'Smartphone' + - regex: '(hiptop|avantgo|plucker|xiino|blazer|elaine)' + regex_flag: 'i' + device_replacement: 'Generic Smartphone' + brand_replacement: 'Generic' + model_replacement: 'Smartphone' + + ########## + # Spiders (this is hack...) + ########## + - regex: '(bot|zao|borg|DBot|oegp|silk|Xenu|zeal|^NING|CCBot|crawl|htdig|lycos|slurp|teoma|voila|yahoo|Sogou|CiBra|Nutch|^Java/|^JNLP/|Daumoa|Genieo|ichiro|larbin|pompos|Scrapy|snappy|speedy|spider|msnbot|msrbot|vortex|^vortex|crawler|favicon|indexer|Riddler|scooter|scraper|scrubby|WhatWeb|WinHTTP|bingbot|openbot|gigabot|furlbot|polybot|seekbot|^voyager|archiver|Icarus6j|mogimogi|Netvibes|blitzbot|altavista|charlotte|findlinks|Retreiver|TLSProber|WordPress|SeznamBot|ProoXiBot|wsr\-agent|Squrl Java|EtaoSpider|PaperLiBot|SputnikBot|A6\-Indexer|netresearch|searchsight|baiduspider|YisouSpider|ICC\-Crawler|http%20client|Python-urllib|dataparksearch|converacrawler|Screaming Frog|AppEngine-Google|YahooCacheSystem|fast\-webcrawler|Sogou Pic Spider|semanticdiscovery|Innovazion Crawler|facebookexternalhit|Google.*/\+/web/snippet|Google-HTTP-Java-Client|BlogBridge|IlTrovatore-Setaccio|InternetArchive|GomezAgent|WebThumbnail|heritrix|NewsGator|PagePeeker|Reaper|ZooShot|holmes)' + regex_flag: 'i' + device_replacement: 'Spider' + brand_replacement: 'Spider' + model_replacement: 'Desktop' + + ########## + # Generic Feature Phone + # take care to do case insensitive matching + ########## + - regex: '^(1207|3gso|4thp|501i|502i|503i|504i|505i|506i|6310|6590|770s|802s|a wa|acer|acs\-|airn|alav|asus|attw|au\-m|aur |aus |abac|acoo|aiko|alco|alca|amoi|anex|anny|anyw|aptu|arch|argo|bmobile|bell|bird|bw\-n|bw\-u|beck|benq|bilb|blac|c55/|cdm\-|chtm|capi|comp|cond|dall|dbte|dc\-s|dica|ds\-d|ds12|dait|devi|dmob|doco|dopo|dorado|el(?:38|39|48|49|50|55|58|68)|el[3456]\d{2}dual|erk0|esl8|ex300|ez40|ez60|ez70|ezos|ezze|elai|emul|eric|ezwa|fake|fly\-|fly_|g\-mo|g1 u|g560|gf\-5|grun|gene|go.w|good|grad|hcit|hd\-m|hd\-p|hd\-t|hei\-|hp i|hpip|hs\-c|htc |htc\-|htca|htcg)' + regex_flag: 'i' + device_replacement: 'Generic Feature Phone' + brand_replacement: 'Generic' + model_replacement: 'Feature Phone' + - regex: '^(htcp|htcs|htct|htc_|haie|hita|huaw|hutc|i\-20|i\-go|i\-ma|i\-mobile|i230|iac|iac\-|iac/|ig01|im1k|inno|iris|jata|kddi|kgt|kgt/|kpt |kwc\-|klon|lexi|lg g|lg\-a|lg\-b|lg\-c|lg\-d|lg\-f|lg\-g|lg\-k|lg\-l|lg\-m|lg\-o|lg\-p|lg\-s|lg\-t|lg\-u|lg\-w|lg/k|lg/l|lg/u|lg50|lg54|lge\-|lge/|leno|m1\-w|m3ga|m50/|maui|mc01|mc21|mcca|medi|meri|mio8|mioa|mo01|mo02|mode|modo|mot |mot\-|mt50|mtp1|mtv |mate|maxo|merc|mits|mobi|motv|mozz|n100|n101|n102|n202|n203|n300|n302|n500|n502|n505|n700|n701|n710|nec\-|nem\-|newg|neon)' + regex_flag: 'i' + device_replacement: 'Generic Feature Phone' + brand_replacement: 'Generic' + model_replacement: 'Feature Phone' + - regex: '^(netf|noki|nzph|o2 x|o2\-x|opwv|owg1|opti|oran|ot\-s|p800|pand|pg\-1|pg\-2|pg\-3|pg\-6|pg\-8|pg\-c|pg13|phil|pn\-2|pt\-g|palm|pana|pire|pock|pose|psio|qa\-a|qc\-2|qc\-3|qc\-5|qc\-7|qc07|qc12|qc21|qc32|qc60|qci\-|qwap|qtek|r380|r600|raks|rim9|rove|s55/|sage|sams|sc01|sch\-|scp\-|sdk/|se47|sec\-|sec0|sec1|semc|sgh\-|shar|sie\-|sk\-0|sl45|slid|smb3|smt5|sp01|sph\-|spv |spv\-|sy01|samm|sany|sava|scoo|send|siem|smar|smit|soft|sony|t\-mo|t218|t250|t600|t610|t618|tcl\-|tdg\-|telm|tim\-|ts70|tsm\-|tsm3|tsm5|tx\-9|tagt)' + regex_flag: 'i' + device_replacement: 'Generic Feature Phone' + brand_replacement: 'Generic' + model_replacement: 'Feature Phone' + - regex: '^(talk|teli|topl|tosh|up.b|upg1|utst|v400|v750|veri|vk\-v|vk40|vk50|vk52|vk53|vm40|vx98|virg|vertu|vite|voda|vulc|w3c |w3c\-|wapj|wapp|wapu|wapm|wig |wapi|wapr|wapv|wapy|wapa|waps|wapt|winc|winw|wonu|x700|xda2|xdag|yas\-|your|zte\-|zeto|aste|audi|avan|blaz|brew|brvw|bumb|ccwa|cell|cldc|cmd\-|dang|eml2|fetc|hipt|http|ibro|idea|ikom|ipaq|jbro|jemu|jigs|keji|kyoc|kyok|libw|m\-cr|midp|mmef|moto|mwbp|mywa|newt|nok6|o2im|pant|pdxg|play|pluc|port|prox|rozo|sama|seri|smal|symb|treo|upsi|vx52|vx53|vx60|vx61|vx70|vx80|vx81|vx83|vx85|wap\-|webc|whit|wmlb|xda\-|xda_)' + regex_flag: 'i' + device_replacement: 'Generic Feature Phone' + brand_replacement: 'Generic' + model_replacement: 'Feature Phone' + - regex: '^(Ice)$' + device_replacement: 'Generic Feature Phone' + brand_replacement: 'Generic' + model_replacement: 'Feature Phone' + - regex: '(wap[\-\ ]browser|maui|netfront|obigo|teleca|up\.browser|midp|Opera Mini)' + regex_flag: 'i' + device_replacement: 'Generic Feature Phone' + brand_replacement: 'Generic' + model_replacement: 'Feature Phone' \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/App_GlobalResources/MvcLocalization.Designer.cs b/src/Presentation/SmartStore.Web/App_GlobalResources/MvcLocalization.Designer.cs index 07cb03a819..1eda45cf26 100644 --- a/src/Presentation/SmartStore.Web/App_GlobalResources/MvcLocalization.Designer.cs +++ b/src/Presentation/SmartStore.Web/App_GlobalResources/MvcLocalization.Designer.cs @@ -1,10 +1,10 @@ //------------------------------------------------------------------------------ // -// Dieser Code wurde von einem Tool generiert. -// Laufzeitversion:4.0.30319.34209 +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 // -// Änderungen an dieser Datei können falsches Verhalten verursachen und gehen verloren, wenn -// der Code erneut generiert wird. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. // //------------------------------------------------------------------------------ @@ -13,12 +13,12 @@ namespace Resources { /// - /// Eine stark typisierte Ressourcenklasse zum Suchen von lokalisierten Zeichenfolgen usw. + /// A strongly-typed resource class, for looking up localized strings, etc. /// - // Diese Klasse wurde von der StronglyTypedResourceBuilder-Klasse - // über ein Tool wie ResGen oder Visual Studio automatisch generiert. - // Um einen Member hinzuzufügen oder zu entfernen, bearbeiten Sie die .ResX-Datei und führen dann ResGen - // mit der /str-Option erneut aus, oder Sie erstellen das Visual Studio-Projekt neu. + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option or rebuild the Visual Studio project. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Web.Application.StronglyTypedResourceProxyBuilder", "12.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] @@ -33,7 +33,7 @@ internal MvcLocalization() { } /// - /// Gibt die zwischengespeicherte ResourceManager-Instanz zurück, die von dieser Klasse verwendet wird. + /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Resources.ResourceManager ResourceManager { @@ -47,8 +47,8 @@ internal MvcLocalization() { } /// - /// Überschreibt die CurrentUICulture-Eigenschaft des aktuellen Threads für alle - /// Ressourcenlookups, die diese stark typisierte Ressourcenklasse verwenden. + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Globalization.CultureInfo Culture { @@ -61,7 +61,7 @@ internal MvcLocalization() { } /// - /// Sucht eine lokalisierte Zeichenfolge, die The field {0} must be a date. ähnelt. + /// Looks up a localized string similar to The field '{0}' must be a date.. /// internal static string FieldMustBeDate { get { @@ -70,7 +70,7 @@ internal static string FieldMustBeDate { } /// - /// Sucht eine lokalisierte Zeichenfolge, die The field {0} must be a number. ähnelt. + /// Looks up a localized string similar to The field '{0}' must be a number.. /// internal static string FieldMustBeNumeric { get { @@ -79,7 +79,7 @@ internal static string FieldMustBeNumeric { } /// - /// Sucht eine lokalisierte Zeichenfolge, die The value '{0}' is not valid for {1}. ähnelt. + /// Looks up a localized string similar to The value '{0}' is not valid for '{1}'.. /// internal static string PropertyValueInvalid { get { @@ -88,12 +88,21 @@ internal static string PropertyValueInvalid { } /// - /// Sucht eine lokalisierte Zeichenfolge, die A value is required. ähnelt. + /// Looks up a localized string similar to A value is required.. /// internal static string PropertyValueRequired { get { return ResourceManager.GetString("PropertyValueRequired", resourceCulture); } } + + /// + /// Looks up a localized string similar to The field '{0}' is required.. + /// + internal static string Required { + get { + return ResourceManager.GetString("Required", resourceCulture); + } + } } } diff --git a/src/Presentation/SmartStore.Web/App_GlobalResources/MvcLocalization.de.resx b/src/Presentation/SmartStore.Web/App_GlobalResources/MvcLocalization.de.resx index e1d0fa764f..fee5893b3b 100644 --- a/src/Presentation/SmartStore.Web/App_GlobalResources/MvcLocalization.de.resx +++ b/src/Presentation/SmartStore.Web/App_GlobalResources/MvcLocalization.de.resx @@ -118,15 +118,18 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Das Feld {0} muss ein Datum sein. + Das Feld '{0}' muss ein Datum sein. - Das Feld {0} muss numerisch sein. + Das Feld '{0}' muss numerisch sein. - Der Wert '{0}' für {1} ist ungültig. + Der Wert '{0}' für '{1}' ist ungültig. Ein Wert ist zwingend erforderlich. + + Das Feld '{0}' ist erforderlich. + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/App_GlobalResources/MvcLocalization.resx b/src/Presentation/SmartStore.Web/App_GlobalResources/MvcLocalization.resx index da4a85fa32..0e272fccd2 100644 --- a/src/Presentation/SmartStore.Web/App_GlobalResources/MvcLocalization.resx +++ b/src/Presentation/SmartStore.Web/App_GlobalResources/MvcLocalization.resx @@ -118,15 +118,18 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - The field {0} must be a date. + The field '{0}' must be a date. - The field {0} must be a number. + The field '{0}' must be a number. - The value '{0}' is not valid for {1}. + The value '{0}' is not valid for '{1}'. A value is required. + + The field '{0}' is required. + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000019_5_virtual_gift_card_75.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000019_5_virtual_gift_card_75.jpeg deleted file mode 100644 index b46a43618f..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000019_5_virtual_gift_card_75.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000020_25_virtual_gift_card.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000020_25_virtual_gift_card.jpeg deleted file mode 100644 index 231fcaca34..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000020_25_virtual_gift_card.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000020_25_virtual_gift_card_125.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000020_25_virtual_gift_card_125.jpeg deleted file mode 100644 index 23b629526d..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000020_25_virtual_gift_card_125.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000020_25_virtual_gift_card_75.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000020_25_virtual_gift_card_75.jpeg deleted file mode 100644 index b46a43618f..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000020_25_virtual_gift_card_75.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000021_50_physical_gift_card_75.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000021_50_physical_gift_card_75.jpeg deleted file mode 100644 index b46a43618f..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000021_50_physical_gift_card_75.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000022_100_physical_gift_card_75.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000022_100_physical_gift_card_75.jpeg deleted file mode 100644 index b46a43618f..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000022_100_physical_gift_card_75.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000024_acer_aspire_one_89_mini_notebook_case_black_75.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000024_acer_aspire_one_89_mini_notebook_case_black_75.jpeg deleted file mode 100644 index 4b9ef163cf..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000024_acer_aspire_one_89_mini_notebook_case_black_75.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000026_adidas_womens_supernova_csh_7_running_shoe_75.jpg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000026_adidas_womens_supernova_csh_7_running_shoe_75.jpg deleted file mode 100644 index 72b7a3b370..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000026_adidas_womens_supernova_csh_7_running_shoe_75.jpg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000028_adobe_photoshop_elements_7_75.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000028_adobe_photoshop_elements_7_75.jpeg deleted file mode 100644 index 6482f9153e..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000028_adobe_photoshop_elements_7_75.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000029_apc_back_ups_rs_800va_ups_800_va_ups_battery_lead_acid_br800blk__75.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000029_apc_back_ups_rs_800va_ups_800_va_ups_battery_lead_acid_br800blk__75.jpeg deleted file mode 100644 index efb2e46f2c..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000029_apc_back_ups_rs_800va_ups_800_va_ups_battery_lead_acid_br800blk__75.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000034_black_white_diamond_heart_75.jpg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000034_black_white_diamond_heart_75.jpg deleted file mode 100644 index feac89b5fb..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000034_black_white_diamond_heart_75.jpg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000035_blackberry_bold_9000_phone_black_att_75.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000035_blackberry_bold_9000_phone_black_att_75.jpeg deleted file mode 100644 index ae48a960b3..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000035_blackberry_bold_9000_phone_black_att_75.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000036_build_your_own_computer.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000036_build_your_own_computer.jpeg deleted file mode 100644 index af6447c685..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000036_build_your_own_computer.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000036_build_your_own_computer_125.jpeg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000036_build_your_own_computer_125.jpeg deleted file mode 100644 index 41ddd6848a..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000036_build_your_own_computer_125.jpeg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000052_etnies_mens_digit_sneaker.jpg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000052_etnies_mens_digit_sneaker.jpg deleted file mode 100644 index 67198f7d85..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000052_etnies_mens_digit_sneaker.jpg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000052_etnies_mens_digit_sneaker_125.jpg b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000052_etnies_mens_digit_sneaker_125.jpg deleted file mode 100644 index 80885934b0..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/0000052_etnies_mens_digit_sneaker_125.jpg and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/placeholder.txt b/src/Presentation/SmartStore.Web/Content/Images/Thumbs/placeholder.txt deleted file mode 100644 index 5f282702bb..0000000000 --- a/src/Presentation/SmartStore.Web/Content/Images/Thumbs/placeholder.txt +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/Images/uploaded/placeholder.txt b/src/Presentation/SmartStore.Web/Content/Images/uploaded/placeholder.txt deleted file mode 100644 index 5f282702bb..0000000000 --- a/src/Presentation/SmartStore.Web/Content/Images/uploaded/placeholder.txt +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/custom.less b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/custom.less index 46611f8534..67aeef972b 100644 --- a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/custom.less +++ b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/custom.less @@ -211,6 +211,9 @@ span.field-validation-error { .btn-below { margin-top: 5px !important; } +.btn-right { + margin-left: 10px !important; +} // FORMS (GENERAL) @@ -487,10 +490,81 @@ th label { .modal-backdrop, .modal-backdrop.fade.in { - .opacity(60); + opacity: 0.6; } .modal-header .close { font-size: 30px; text-shadow: none; +} + +.modal-large { + width: 800px !important; + margin: -250px 0 0 -400px !important; +} + + +// XLARGE MODAL +// ------------------------------ + +.modal-xlarge { + width: 960px !important; + margin: -250px 0 0 -480px !important; +} + + +// FLEX MODAL +// ------------------------------ + +.modal-flex { + top: 3% !important; + bottom: 3% !important; + margin-top: 0 !important; + margin-bottom: 0 !important; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + + &.fade.in { + display: -webkit-box !important; + display: -webkit-flex !important; + display: -ms-flexbox !important; + display: flex !important; + } + + .modal-body { + max-height: none; + overflow-y: auto; + overflow-x: hidden; + .display-flex(); + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-box-align: stretch; + -webkit-align-items: stretch; + -ms-flex-align: stretch; + align-items: stretch; + -webkit-align-content: stretch; + -ms-flex-line-pack: stretch; + align-content: stretch; + } + + .modal-flex-fill-area { + //overflow: hidden; + min-height: 100%; + -webkit-box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-black-16.gif b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-black-16.gif deleted file mode 100644 index edcef28713..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-black-16.gif and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-black-32.gif b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-black-32.gif deleted file mode 100644 index 9fafab009a..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-black-32.gif and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-white-16.gif b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-white-16.gif deleted file mode 100644 index 152084b58c..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-white-16.gif and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-white-32-alt.gif b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-white-32-alt.gif deleted file mode 100644 index f201c9a0a0..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-white-32-alt.gif and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-white-32.gif b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-white-32.gif deleted file mode 100644 index d663892719..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/images/throbber-progress-white-32.gif and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/mixins.less b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/mixins.less index 298d86b427..f0e9eb81ea 100644 --- a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/mixins.less +++ b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/mixins.less @@ -140,10 +140,8 @@ @bg: ~'top, @{c1} 0%, @{c2} @{stop1}, @{c3} @{stop}, @{c4} 100%'; background-color: mix(@c2, @c3, 50%); background-image: -webkit-linear-gradient(@bg); - background-image: -o-linear-gradient(@bg); background-image: linear-gradient(@bg); background-repeat: repeat-x; - filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@c1),argb(@c4))); } } @@ -152,13 +150,11 @@ // Drop shadow extras .box-shadow(@shadow1, @shadow2) { - -webkit-box-shadow: @shadow1, @shadow2; - box-shadow: @shadow1, @shadow2; + box-shadow: @shadow1, @shadow2; } .box-shadow(@shadow1, @shadow2, @shadow3) { - -webkit-box-shadow: @shadow1, @shadow2, @shadow3; - box-shadow: @shadow1, @shadow2, @shadow3; + box-shadow: @shadow1, @shadow2, @shadow3; } @@ -171,13 +167,11 @@ .transition(@transition1, @transition2) { -webkit-transition: @transition1, @transition2; - -o-transition: @transition1, @transition2; transition: @transition1, @transition2; } .transition(@transition1, @transition2, @transition3) { -webkit-transition: @transition1, @transition2, @transition3; - -o-transition: @transition1, @transition2, @transition3; transition: @transition1, @transition2, @transition3; } @@ -186,7 +180,6 @@ .animation(@animation) { -webkit-animation: @animation; - -o-animation: @animation; animation: @animation; } @@ -194,7 +187,27 @@ .transform(@expr) { -webkit-transform: @expr; - -moz-transform: @expr; - -ms-transform: @expr; transform: @expr; +} + + +/* Flexbox Extras +----------------------------------------*/ + +.display-flex() { + display: -webkit-flex; + display: -webkit-box; + display: -ms-flexbox; + display: flex; +} + +.flex-center() { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/spinner.less b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/spinner.less new file mode 100644 index 0000000000..660161e472 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/spinner.less @@ -0,0 +1,116 @@ +/* +SmartStore Circular Spinner +--------------------------- + Version: 1, + Timestamp: 10.02.2016 + Author: Murat Cakir +*/ + +.spinner-container { + // for centering the spinner + position: relative; + display: none; + .flex-center(); +} + +.spinner { + position: relative; + text-align: center; + vertical-align: middle; + display: none; +} + +.spinner-container.active > .spinner, +.spinner.active { + background-color: transparent; + border: none; + display: inline-block; +} + +.spinner.spinner-boxed { + border-radius: 50%; + padding: 0.2em; + background-color: #fff !important; + box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), + 0px 2px 2px 0px rgba(0, 0, 0, 0.14), + 0px 1px 5px 0px rgba(0, 0, 0, 0.12); +} + +.spinner-container.active { + .display-flex(); +} + +.spinner svg { + -webkit-transform-origin: 50% 50% 0; + transform-origin: 50% 50% 0; + + -webkit-animation: spinner-rotate 1.333s linear infinite; + animation: spinner-rotate 1.333s linear infinite; + + html.ie & { + animation: spinner-rotate-ie 2.5s linear infinite; + } +} + +.spinner circle { + fill: transparent; + stroke: #ff9800; + stroke-linecap: round; + stroke-dasharray: 200.96; + stroke-dashoffset: 58px; + + html.ie & { + stroke-dashoffset: 80px; + } + + -webkit-animation: spinner-dash 1.333s linear infinite, spinner-colors 10.644s linear infinite; + animation: spinner-dash 1.333s linear infinite, spinner-colors 10.644s linear infinite; +} + +@keyframes spinner-dash { + 0% { stroke-dashoffset: 58px; } + 50% { stroke-dashoffset: 200.96px; } + 100% { stroke-dashoffset: 58px; } +} +@-webkit-keyframes spinner-dash { + 0% { stroke-dashoffset: 58px; } + 50% { stroke-dashoffset: 200.96px; } + 100% { stroke-dashoffset: 58px; } +} + +@keyframes spinner-rotate { + 50% { transform: rotate(600deg); } + 100% { transform: rotate(720deg); } +} +@-webkit-keyframes spinner-rotate { + 50% { transform: rotate(600deg); } + 100% { transform: rotate(720deg); } +} +@keyframes spinner-rotate-ie { + 50% { transform: rotate(360deg); } + 100% { transform: rotate(720deg); } +} + +@keyframes spinner-colors { + 0% { stroke: #3F51B5; } + 20% { stroke: #09b7bf; } + 40% { stroke: #90d36b; } + 60% { stroke: #F44336; } + 80% { stroke: #f90; } + 100% { stroke: #3F51B5; } +} +@-webkit-keyframes spinner-colors { + 0% { stroke: #3F51B5; } + 20% { stroke: #09b7bf; } + 40% { stroke: #90d36b; } + 60% { stroke: #F44336; } + 80% { stroke: #f90; } + 100% { stroke: #3F51B5; } +} + +.spinner.white circle { + stroke: #fff; + -webkit-animation-name: spinner-dash, spinner-rotate; + animation-name: spinner-dash, spinner-rotate; +} + diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/telerik/telerik.smartstore.less b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/telerik/telerik.smartstore.less index 6b333e4237..a0c24195c8 100644 --- a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/telerik/telerik.smartstore.less +++ b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/telerik/telerik.smartstore.less @@ -17,10 +17,6 @@ .reset-filter(); color: @grayDark; text-shadow: 0 1px 1px rgba(255,255,255, .75); - - .no-cssgradients & { - background: #e9e9e9 /*@btnBackgroundHighlight*/ url('gradient.png') repeat-x 0 center; - } } .t-menu-vertical, .t-editor, .t-tooltip, .t-tabstrip { background-position: 0 -48px; @@ -89,7 +85,7 @@ color: #3b3b3b; } .t-button, button.t-button.t-state-disabled:hover, a.t-button.t-state-disabled:hover, .t-state-disabled .t-button:hover { - background: #d2d5d9 url('gradient.png') repeat-x 0 center; + background: #d2d5d9; } .t-button:hover { border-color: #f3d64a; @@ -153,7 +149,7 @@ } .t-numerictextbox .t-input { - width: 252px; + width: 301px; padding: 6px 8px 6px 3px; } .t-numerictextbox.small .t-input { diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/throbber.less b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/throbber.less index 31aa8cdfbd..f579532a64 100644 --- a/src/Presentation/SmartStore.Web/Content/bootstrap/custom/throbber.less +++ b/src/Presentation/SmartStore.Web/Content/bootstrap/custom/throbber.less @@ -5,65 +5,57 @@ Version: 1, Timestamp: 18.08.2012 .throbber { display: none; - .opacity(0); - - & .throbber-overlay { - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - background-color: #000; - .opacity(70); - z-index: 2147483640; + opacity: 0; + overflow: hidden; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + z-index: 2147483640; + background-color: rgba(0,0,0, 0.7); + + &.white { + background-color: rgba(255,255,255, 0.85); } - - &.white .throbber-overlay { - background-color: #fff; + + body > & { + position: fixed; } - - & .throbber-content { + + .throbber-flex { + width: 100%; + height: 100%; position: absolute; - z-index: 2147483640; - color: #fff; - text-shadow: 1px 1px 0 #666; - background: url('images/throbber-progress-white-32.gif') 4px 6px no-repeat; + .display-flex(); + .flex-center(); + + > * { + text-align: center; + } + } + + .throbber-content { + display: block; + overflow: hidden; + line-height: 1.3; + + &:not(:empty) { + margin-bottom: 30px; + } } - + &.large .throbber-content { - min-height: 38px; - line-height: 32px; - max-width: 400px; - font-size: 24px; - font-family: 'Segoe UI Light', 'Segoe UI', Tahoma, 'Helvetica Neue', Helvetica, Arial, 'sans-serif'; + font-size: 32px; font-weight: 100; - padding-left: 44px; } + &.small .throbber-content { - .text-overflow(); - height: 16px; - font-size: @baseFontSize; - line-height: @baseLineHeight; - vertical-align: middle; - padding-left: 24px; - background: url('images/throbber-progress-white-32.gif') 0 6px no-repeat; + font-size: 16px; + font-weight: 600; } - &.white .throbber-content { - color: #000; - text-shadow: none; - } - - &.large .throbber-content { background-image: url('images/throbber-progress-white-32.gif'); } - &.small .throbber-content { background-image: url('images/throbber-progress-white-16.gif'); background-position: 0 0; } - &.white.large .throbber-content { background-image: url('images/throbber-progress-black-32.gif'); } - &.white.small .throbber-content { background-image: url('images/throbber-progress-black-16.gif'); background-position: 0 0; } - - &.global .throbber-overlay { - position: fixed; - width: 100%; - height: 100%; - } - &.global .throbber-content { - position: fixed; + + &:not(.white) .throbber-content { + color: #fff; } -} \ No newline at end of file +} diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/forms.less b/src/Presentation/SmartStore.Web/Content/bootstrap/forms.less index 50e2ad9aed..917cc2e396 100644 --- a/src/Presentation/SmartStore.Web/Content/bootstrap/forms.less +++ b/src/Presentation/SmartStore.Web/Content/bootstrap/forms.less @@ -124,7 +124,7 @@ input[type="color"], .uneditable-input { background-color: @inputBackground; border: 1px solid @inputBorder; - .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); + //.box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Focus state &:focus { @@ -195,7 +195,7 @@ input[type="checkbox"]:focus { color: @grayLight; background-color: darken(@inputBackground, 1%); border-color: @inputBorder; - .box-shadow(inset 0 1px 2px rgba(0,0,0,.025)); + //.box-shadow(inset 0 1px 2px rgba(0,0,0,.025)); cursor: not-allowed; } @@ -385,8 +385,8 @@ select:focus:required:invalid { border-color: #ee5f5b; &:focus { border-color: darken(#ee5f5b, 10%); - @shadow: 0 0 6px lighten(#ee5f5b, 20%); - .box-shadow(@shadow); + //@shadow: 0 0 6px lighten(#ee5f5b, 20%); + //.box-shadow(@shadow); } } diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/mixins.less b/src/Presentation/SmartStore.Web/Content/bootstrap/mixins.less index 8b0edb15cb..c1d9950469 100644 --- a/src/Presentation/SmartStore.Web/Content/bootstrap/mixins.less +++ b/src/Presentation/SmartStore.Web/Content/bootstrap/mixins.less @@ -240,14 +240,12 @@ // Drop shadows .box-shadow(@shadow) { - -webkit-box-shadow: @shadow; - box-shadow: @shadow; + box-shadow: @shadow; } // Transitions .transition(@transition) { -webkit-transition: @transition; - -o-transition: @transition; transition: @transition; } .transition-delay(@transition-delay) { @@ -258,33 +256,23 @@ // Transformations .rotate(@degrees) { -webkit-transform: rotate(@degrees); - -ms-transform: rotate(@degrees); - -o-transform: rotate(@degrees); transform: rotate(@degrees); } .scale(@ratio) { -webkit-transform: scale(@ratio); - -ms-transform: scale(@ratio); - -o-transform: scale(@ratio); transform: scale(@ratio); } .translate(@x, @y) { -webkit-transform: translate(@x, @y); - -ms-transform: translate(@x, @y); - -o-transform: translate(@x, @y); transform: translate(@x, @y); } .skew(@x, @y) { -webkit-transform: skew(@x, @y); - -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twitter/bootstrap/issues/4885 - -o-transform: skew(@x, @y); transform: skew(@x, @y); -webkit-backface-visibility: hidden; // See https://github.com/twitter/bootstrap/issues/5319 } .translate3d(@x, @y, @z) { -webkit-transform: translate3d(@x, @y, @z); - -moz-transform: translate3d(@x, @y, @z); - -o-transform: translate3d(@x, @y, @z); transform: translate3d(@x, @y, @z); } @@ -294,30 +282,24 @@ // See git pull https://github.com/dannykeane/bootstrap.git backface-visibility for examples .backface-visibility(@visibility){ -webkit-backface-visibility: @visibility; - -moz-backface-visibility: @visibility; backface-visibility: @visibility; } // Background clipping // Heads up: FF 3.6 and under need "padding" instead of "padding-box" .background-clip(@clip) { - -webkit-background-clip: @clip; - background-clip: @clip; + background-clip: @clip; } // Background sizing .background-size(@size) { - -webkit-background-size: @size; - -o-background-size: @size; - background-size: @size; + background-size: @size; } // Box sizing .box-sizing(@boxmodel) { - -webkit-box-sizing: @boxmodel; - -moz-box-sizing: @boxmodel; - box-sizing: @boxmodel; + box-sizing: @boxmodel; } // User select @@ -351,14 +333,12 @@ -webkit-hyphens: @mode; -moz-hyphens: @mode; -ms-hyphens: @mode; - -o-hyphens: @mode; hyphens: @mode; } // Opacity .opacity(@opacity) { - opacity: @opacity / 100; - filter: ~"alpha(opacity=@{opacity})"; + opacity: @opacity / 100; } @@ -390,33 +370,26 @@ .horizontal(@startColor: #555, @endColor: #333) { background-color: @endColor; background-image: -webkit-linear-gradient(left, @startColor, @endColor); // Safari 5.1+, Chrome 10+ - background-image: -o-linear-gradient(left, @startColor, @endColor); // Opera 11.10 background-image: linear-gradient(to right, @startColor, @endColor); // Standard, IE10 background-repeat: repeat-x; - filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)",argb(@startColor),argb(@endColor))); // IE9 and down } .vertical(@startColor: #555, @endColor: #333) { background-color: mix(@startColor, @endColor, 60%); background-image: -webkit-linear-gradient(top, @startColor, @endColor); // Safari 5.1+, Chrome 10+ - background-image: -o-linear-gradient(top, @startColor, @endColor); // Opera 11.10 background-image: linear-gradient(to bottom, @startColor, @endColor); // Standard, IE10 background-repeat: repeat-x; - filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@startColor),argb(@endColor))); // IE9 and down } .directional(@startColor: #555, @endColor: #333, @deg: 45deg) { background-color: @endColor; background-repeat: repeat-x; background-image: -webkit-linear-gradient(@deg, @startColor, @endColor); // Safari 5.1+, Chrome 10+ - background-image: -o-linear-gradient(@deg, @startColor, @endColor); // Opera 11.10 background-image: linear-gradient(@deg, @startColor, @endColor); // Standard, IE10 } .vertical-three-colors(@startColor: #00b3ee, @midColor: #7a43b6, @colorStop: 50%, @endColor: #c3325f) { background-color: mix(@midColor, @endColor, 80%); background-image: -webkit-linear-gradient(@startColor, @midColor @colorStop, @endColor); - background-image: -o-linear-gradient(@startColor, @midColor @colorStop, @endColor); background-image: linear-gradient(@startColor, @midColor @colorStop, @endColor); background-repeat: no-repeat; - filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@startColor),argb(@endColor))); // IE9 and down, gets no color-stop at all for proper fallback } .radial(@innerColor: #555, @outerColor: #333) { background-color: @outerColor; @@ -427,7 +400,6 @@ .striped(@color: #555, @angle: 45deg) { background-color: @color; background-image: -webkit-linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); background-image: linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent); } } diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/asp_net/main.ashx b/src/Presentation/SmartStore.Web/Content/filemanager/asp_net/main.ashx deleted file mode 100644 index 72c5f5feb9..0000000000 --- a/src/Presentation/SmartStore.Web/Content/filemanager/asp_net/main.ashx +++ /dev/null @@ -1,719 +0,0 @@ -<%@ WebHandler Language="C#" Class="RoxyFilemanHandler" Debug="true" %> -/* - RoxyFileman - web based file manager. Ready to use with CKEditor, TinyMCE. - Can be easily integrated with any other WYSIWYG editor or CMS. - - Copyright (C) 2013, RoxyFileman.com - Lyubomir Arsov. All rights reserved. - For licensing, see LICENSE.txt or http://RoxyFileman.com/license - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - - Contact: Lyubomir Arsov, liubo (at) web-lobby.com -*/ -using System; -using System.Reflection; -using System.Drawing; -using System.Collections; -using System.IO; -using System.Collections.Generic; -using System.Web; -using System.Text.RegularExpressions; -using System.Drawing.Imaging; -using System.IO.Compression; - -public class RoxyFilemanHandler : IHttpHandler, System.Web.SessionState.IRequiresSessionState -{ - Dictionary _settings = null; - Dictionary _lang = null; - HttpResponse _r = null; - HttpContext _context = null; - string confFile = "../conf.json"; - public void ProcessRequest (HttpContext context) { - _context = context; - _r = context.Response; - string action = "DIRLIST"; - try{ - if (_context.Request["a"] != null) - action = (string)_context.Request["a"]; - - VerifyAction(action); - switch (action.ToUpper()) - { - case "DIRLIST": - ListDirTree(_context.Request["type"]); - break; - case "FILESLIST": - ListFiles(_context.Request["d"], _context.Request["type"]); - break; - case "COPYDIR": - CopyDir(_context.Request["d"], _context.Request["n"]); - break; - case "COPYFILE": - CopyFile(_context.Request["f"], _context.Request["n"]); - break; - case "CREATEDIR": - CreateDir(_context.Request["d"], _context.Request["n"]); - break; - case "DELETEDIR": - DeleteDir(_context.Request["d"]); - break; - case "DELETEFILE": - DeleteFile(_context.Request["f"]); - break; - case "DOWNLOAD": - DownloadFile(_context.Request["f"]); - break; - case "DOWNLOADDIR": - DownloadDir(_context.Request["d"]); - break; - case "MOVEDIR": - MoveDir(_context.Request["d"], _context.Request["n"]); - break; - case "MOVEFILE": - MoveFile(_context.Request["f"], _context.Request["n"]); - break; - case "RENAMEDIR": - RenameDir(_context.Request["d"], _context.Request["n"]); - break; - case "RENAMEFILE": - RenameFile(_context.Request["f"], _context.Request["n"]); - break; - case "GENERATETHUMB": - int w = 140, h = 0; - int.TryParse(_context.Request["width"].Replace("px", ""), out w); - int.TryParse(_context.Request["height"].Replace("px", ""), out h); - ShowThumbnail(_context.Request["f"], w, h); - break; - case "UPLOAD": - Upload(_context.Request["d"]); - break; - default: - _r.Write(GetErrorRes("This action is not implemented.")); - break; - } - - } - catch(Exception ex){ - if (action == "UPLOAD"){ - _r.Write(""); - } - else{ - _r.Write(GetErrorRes(ex.Message)); - } - } - - } - private string FixPath(string path) - { - if (!path.StartsWith("~")){ - if (!path.StartsWith("/")) - path = "/" + path; - path = "~" + path; - } - return _context.Server.MapPath(path); - } - private string GetLangFile(){ - return "../lang/en.json"; - } - protected string LangRes(string name) - { - string ret = name; - if (_lang == null) - _lang = ParseJSON(GetLangFile()); - if (_lang.ContainsKey(name)) - ret = _lang[name]; - - return ret; - } - protected string GetFileType(string ext){ - string ret = "file"; - ext = ext.ToLower(); - if(ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".gif") - ret = "image"; - else if(ext == ".swf" || ext == ".flv") - ret = "flash"; - return ret; - } - protected bool CanHandleFile(string filename) - { - bool ret = false; - FileInfo file = new FileInfo(filename); - string ext = file.Extension.Replace(".", "").ToLower(); - string setting = GetSetting("FORBIDDEN_UPLOADS").Trim().ToLower(); - if (setting != "") - { - ArrayList tmp = new ArrayList(); - tmp.AddRange(Regex.Split(setting, "\\s+")); - if (!tmp.Contains(ext)) - ret = true; - } - setting = GetSetting("ALLOWED_UPLOADS").Trim().ToLower(); - if (setting != "") - { - ArrayList tmp = new ArrayList(); - tmp.AddRange(Regex.Split(setting, "\\s+")); - if (!tmp.Contains(ext)) - ret = false; - } - - return ret; - } - protected Dictionary ParseJSON(string file){ - Dictionary ret = new Dictionary(); - string json = ""; - try{ - json = File.ReadAllText(_context.Server.MapPath(file), System.Text.Encoding.UTF8); - } - catch {} - - json = json.Trim(); - if(json != ""){ - if (json.StartsWith("{")) - json = json.Substring(1, json.Length - 2); - json = json.Trim(); - json = json.Substring(1, json.Length - 2); - string[] lines = Regex.Split(json, "\"\\s*,\\s*\""); - foreach(string line in lines){ - string[] tmp = Regex.Split(line, "\"\\s*:\\s*\""); - try{ - if (tmp[0] != "" && !ret.ContainsKey(tmp[0])) - { - ret.Add(tmp[0], tmp[1]); - } - } - catch {} - } - } - return ret; - } - protected string GetFilesRoot(){ - string ret = GetSetting("FILES_ROOT"); - if (GetSetting("SESSION_PATH_KEY") != "" && _context.Session[GetSetting("SESSION_PATH_KEY")] != null) - ret = (string)_context.Session[GetSetting("SESSION_PATH_KEY")]; - - if(ret == "") - ret = _context.Server.MapPath("../Uploads"); - else - ret = FixPath(ret); - return ret; - } - protected void LoadConf(){ - if(_settings == null) - _settings = ParseJSON(confFile); - } - protected string GetSetting(string name){ - string ret = ""; - LoadConf(); - if(_settings.ContainsKey(name)) - ret = _settings[name]; - - return ret; - } - protected void CheckPath(string path) - { - if (FixPath(path).IndexOf(GetFilesRoot()) != 0) - { - throw new Exception("Access to " + path + " is denied"); - } - } - protected void VerifyAction(string action) - { - string setting = GetSetting(action); - if (setting.IndexOf("?") > -1) - setting = setting.Substring(0, setting.IndexOf("?")); - if (!setting.StartsWith("/")) - setting = "/" + setting; - setting = ".." + setting; - - if (_context.Server.MapPath(setting) != _context.Server.MapPath(_context.Request.Url.LocalPath)) - throw new Exception(LangRes("E_ActionDisabled")); - } - protected string GetResultStr(string type, string msg) - { - return "{\"res\":\"" + type + "\",\"msg\":\"" + msg.Replace("\"","\\\"") + "\"}"; - } - protected string GetSuccessRes(string msg) - { - return GetResultStr("ok", msg); - } - protected string GetSuccessRes() - { - return GetSuccessRes(""); - } - protected string GetErrorRes(string msg) - { - return GetResultStr("error", msg); - } - private void _copyDir(string path, string dest){ - if(!Directory.Exists(dest)) - Directory.CreateDirectory(dest); - foreach(string f in Directory.GetFiles(path)){ - FileInfo file = new FileInfo(f); - if (!File.Exists(Path.Combine(dest, file.Name))){ - File.Copy(f, Path.Combine(dest, file.Name)); - } - } - foreach (string d in Directory.GetDirectories(path)) - { - DirectoryInfo dir = new DirectoryInfo(d); - _copyDir(d, Path.Combine(dest, dir.Name)); - } - } - protected void CopyDir(string path, string newPath) - { - CheckPath(path); - CheckPath(newPath); - DirectoryInfo dir = new DirectoryInfo(FixPath(path)); - DirectoryInfo newDir = new DirectoryInfo(FixPath(newPath + "/" + dir.Name)); - - if (!dir.Exists) - { - throw new Exception(LangRes("E_CopyDirInvalidPath")); - } - else if (newDir.Exists) - { - throw new Exception(LangRes("E_DirAlreadyExists")); - } - else{ - _copyDir(dir.FullName, newDir.FullName); - } - _r.Write(GetSuccessRes()); - } - protected string MakeUniqueFilename(string dir, string filename){ - string ret = filename; - int i = 0; - while (File.Exists(Path.Combine(dir, ret))) - { - i++; - ret = Path.GetFileNameWithoutExtension(filename) + " - Copy " + i.ToString() + Path.GetExtension(filename); - } - return ret; - } - protected void CopyFile(string path, string newPath) - { - CheckPath(path); - FileInfo file = new FileInfo(FixPath(path)); - newPath = FixPath(newPath); - if (!file.Exists) - throw new Exception(LangRes("E_CopyFileInvalisPath")); - else{ - string newName = MakeUniqueFilename(newPath, file.Name); - try{ - File.Copy(file.FullName, Path.Combine(newPath, newName)); - _r.Write(GetSuccessRes()); - } - catch { - throw new Exception(LangRes("E_CopyFile")); - } - } - } - protected void CreateDir(string path, string name) - { - CheckPath(path); - path = FixPath(path); - if(!Directory.Exists(path)) - throw new Exception(LangRes("E_CreateDirInvalidPath")); - else{ - try - { - path = Path.Combine(path, name); - if(!Directory.Exists(path)) - Directory.CreateDirectory(path); - _r.Write(GetSuccessRes()); - } - catch - { - throw new Exception(LangRes("E_CreateDirFailed")); - } - } - } - protected void DeleteDir(string path) - { - CheckPath(path); - path = FixPath(path); - if (!Directory.Exists(path)) - throw new Exception(LangRes("E_DeleteDirInvalidPath")); - else if (path == GetFilesRoot()) - throw new Exception(LangRes("E_CannotDeleteRoot")); - else if(Directory.GetDirectories(path).Length > 0 || Directory.GetFiles(path).Length > 0) - throw new Exception(LangRes("E_DeleteNonEmpty")); - else - { - try - { - Directory.Delete(path); - _r.Write(GetSuccessRes()); - } - catch - { - throw new Exception(LangRes("E_CannotDeleteDir")); - } - } - } - protected void DeleteFile(string path) - { - CheckPath(path); - path = FixPath(path); - if (!File.Exists(path)) - throw new Exception(LangRes("E_DeleteFileInvalidPath")); - else - { - try - { - File.Delete(path); - _r.Write(GetSuccessRes()); - } - catch - { - throw new Exception(LangRes("E_DeletеFile")); - } - } - } - private List GetFiles(string path, string type){ - List ret = new List(); - if(type == "#") - type = ""; - string[] files = Directory.GetFiles(path); - foreach(string f in files){ - if ((GetFileType(new FileInfo(f).Extension) == type) || (type == "")) - ret.Add(f); - } - return ret; - } - private ArrayList ListDirs(string path){ - string[] dirs = Directory.GetDirectories(path); - ArrayList ret = new ArrayList(); - foreach(string dir in dirs){ - ret.Add(dir); - ret.AddRange(ListDirs(dir)); - } - return ret; - } - protected void ListDirTree(string type) - { - DirectoryInfo d = new DirectoryInfo(GetFilesRoot()); - if(!d.Exists) - throw new Exception("Invalid files root directory. Check your configuration."); - - ArrayList dirs = ListDirs(d.FullName); - dirs.Insert(0, d.FullName); - - string localPath = _context.Server.MapPath("~/"); - _r.Write("["); - for(int i = 0; i files = GetFiles(fullPath, type); - _r.Write("["); - for(int i = 0; i < files.Count; i++){ - FileInfo f = new FileInfo(files[i]); - int w = 0, h = 0; - if (GetFileType(f.Extension) == "image"){ - try{ - FileStream fs = new FileStream(f.FullName, FileMode.Open); - Image img = Image.FromStream(fs); - w = img.Width; - h = img.Height; - fs.Close(); - fs.Dispose(); - img.Dispose(); - } - catch(Exception ex) - { - throw ex; - } - } - _r.Write("{"); - _r.Write("\"p\":\""+path + "/" + f.Name+"\""); - _r.Write(",\"t\":\"" + Math.Ceiling(LinuxTimestamp(f.LastWriteTime)).ToString() + "\""); - _r.Write(",\"s\":\""+f.Length.ToString()+"\""); - _r.Write(",\"w\":\""+w.ToString()+"\""); - _r.Write(",\"h\":\""+h.ToString()+"\""); - _r.Write("}"); - if (i < files.Count - 1) - _r.Write(","); - } - _r.Write("]"); - } - public void DownloadDir(string path) - { - path = FixPath(path); - if(!Directory.Exists(path)) - throw new Exception(LangRes("E_CreateArchive")); - string dirName = new FileInfo(path).Name; - string tmpZip = _context.Server.MapPath("../tmp/" + dirName + ".zip"); - if(File.Exists(tmpZip)) - File.Delete(tmpZip); - ZipFile.CreateFromDirectory(path, tmpZip,CompressionLevel.Fastest, true); - _r.Clear(); - _r.Headers.Add("Content-Disposition", "attachment; filename=\"" + dirName + ".zip\""); - _r.ContentType = "application/force-download"; - _r.TransmitFile(tmpZip); - _r.Flush(); - File.Delete(tmpZip); - _r.End(); - } - protected void DownloadFile(string path) - { - CheckPath(path); - FileInfo file = new FileInfo(FixPath(path)); - if(file.Exists){ - _r.Clear(); - _r.Headers.Add("Content-Disposition", "attachment; filename=\"" + file.Name + "\""); - _r.ContentType = "application/force-download"; - _r.TransmitFile(file.FullName); - _r.Flush(); - _r.End(); - } - } - protected void MoveDir(string path, string newPath) - { - CheckPath(path); - CheckPath(newPath); - DirectoryInfo source = new DirectoryInfo(FixPath(path)); - DirectoryInfo dest = new DirectoryInfo(FixPath(Path.Combine(newPath, source.Name))); - if(dest.FullName.IndexOf(source.FullName) == 0) - throw new Exception(LangRes("E_CannotMoveDirToChild")); - else if (!source.Exists) - throw new Exception(LangRes("E_MoveDirInvalisPath")); - else if (dest.Exists) - throw new Exception(LangRes("E_DirAlreadyExists")); - else{ - try{ - source.MoveTo(dest.FullName); - _r.Write(GetSuccessRes()); - } - catch { - throw new Exception(LangRes("E_MoveDir") + " \"" + path + "\""); - } - } - - } - protected void MoveFile(string path, string newPath) - { - CheckPath(path); - CheckPath(newPath); - FileInfo source = new FileInfo(FixPath(path)); - FileInfo dest = new FileInfo(FixPath(newPath)); - if (!source.Exists) - throw new Exception(LangRes("E_MoveFileInvalisPath")); - else if (dest.Exists) - throw new Exception(LangRes("E_MoveFileAlreadyExists")); - else - { - try - { - source.MoveTo(dest.FullName); - _r.Write(GetSuccessRes()); - } - catch - { - throw new Exception(LangRes("E_MoveFile") + " \"" + path + "\""); - } - } - } - protected void RenameDir(string path, string name) - { - CheckPath(path); - DirectoryInfo source = new DirectoryInfo(FixPath(path)); - DirectoryInfo dest = new DirectoryInfo(Path.Combine(source.Parent.FullName, name)); - if(source.FullName == GetFilesRoot()) - throw new Exception(LangRes("E_CannotRenameRoot")); - else if (!source.Exists) - throw new Exception(LangRes("E_RenameDirInvalidPath")); - else if (dest.Exists) - throw new Exception(LangRes("E_DirAlreadyExists")); - else - { - try - { - source.MoveTo(dest.FullName); - _r.Write(GetSuccessRes()); - } - catch - { - throw new Exception(LangRes("E_RenameDir") + " \"" + path + "\""); - } - } - } - protected void RenameFile(string path, string name) - { - CheckPath(path); - FileInfo source = new FileInfo(FixPath(path)); - FileInfo dest = new FileInfo(Path.Combine(source.Directory.FullName, name)); - if (!source.Exists) - throw new Exception(LangRes("E_RenameFileInvalidPath")); - else if (!CanHandleFile(name)) - throw new Exception(LangRes("E_FileExtensionForbidden")); - else - { - try - { - source.MoveTo(dest.FullName); - _r.Write(GetSuccessRes()); - } - catch (Exception ex) - { - throw new Exception(ex.Message + "; " + LangRes("E_RenameFile") + " \"" + path + "\""); - } - } - } - public bool ThumbnailCallback() - { - return false; - } - - protected void ShowThumbnail(string path, int width, int height) - { - CheckPath(path); - FileStream fs = new FileStream(FixPath(path), FileMode.Open); - Bitmap img = new Bitmap(Bitmap.FromStream(fs)); - fs.Close(); - fs.Dispose(); - int cropWidth = img.Width, cropHeight = img.Height; - int cropX = 0, cropY = 0; - - double imgRatio = (double)img.Width / (double)img.Height; - - if(height == 0) - height = Convert.ToInt32(Math.Floor((double)width / imgRatio)); - - if (width > img.Width) - width = img.Width; - if (height > img.Height) - height = img.Height; - - double cropRatio = (double)width / (double)height; - cropWidth = Convert.ToInt32(Math.Floor((double)img.Height * cropRatio)); - cropHeight = Convert.ToInt32(Math.Floor((double)cropWidth / cropRatio)); - if (cropWidth > img.Width) - { - cropWidth = img.Width; - cropHeight = Convert.ToInt32(Math.Floor((double)cropWidth / cropRatio)); - } - if (cropHeight > img.Height) - { - cropHeight = img.Height; - cropWidth = Convert.ToInt32(Math.Floor((double)cropHeight * cropRatio)); - } - if(cropWidth < img.Width){ - cropX = Convert.ToInt32(Math.Floor((double)(img.Width - cropWidth) / 2)); - } - if(cropHeight < img.Height){ - cropY = Convert.ToInt32(Math.Floor((double)(img.Height - cropHeight) / 2)); - } - - Rectangle area = new Rectangle(cropX, cropY, cropWidth, cropHeight); - Bitmap cropImg = img.Clone(area, System.Drawing.Imaging.PixelFormat.DontCare); - img.Dispose(); - Image.GetThumbnailImageAbort imgCallback = new Image.GetThumbnailImageAbort(ThumbnailCallback); - - _r.AddHeader("Content-Type", "image/png"); - cropImg.GetThumbnailImage(width, height, imgCallback, IntPtr.Zero).Save(_r.OutputStream, ImageFormat.Png); - _r.OutputStream.Close(); - cropImg.Dispose(); - } - private ImageFormat GetImageFormat(string filename){ - ImageFormat ret = ImageFormat.Jpeg; - switch(new FileInfo(filename).Extension.ToLower()){ - case ".png": ret = ImageFormat.Png; break; - case ".gif": ret = ImageFormat.Gif; break; - } - return ret; - } - protected void ImageResize(string path, string dest, int width, int height) - { - FileStream fs = new FileStream(path, FileMode.Open); - Image img = Image.FromStream(fs); - fs.Close(); - fs.Dispose(); - float ratio = (float)img.Width / (float)img.Height; - if ((img.Width <= width && img.Height <= height) || (width == 0 && height == 0)) - return; - - int newWidth = width; - int newHeight = Convert.ToInt16(Math.Floor((float)newWidth / ratio)); - if ((height > 0 && newHeight > height) || (width == 0)) - { - newHeight = height; - newWidth = Convert.ToInt16(Math.Floor((float)newHeight * ratio)); - } - Bitmap newImg = new Bitmap(newWidth, newHeight); - Graphics g = Graphics.FromImage((Image)newImg); - g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; - g.DrawImage(img, 0, 0, newWidth, newHeight); - img.Dispose(); - g.Dispose(); - if(dest != ""){ - newImg.Save(dest, GetImageFormat(dest)); - } - newImg.Dispose(); - } - - protected void Upload(string path) - { - CheckPath(path); - path = FixPath(path); - string res = GetSuccessRes(); - try{ - for(int i = 0; i < HttpContext.Current.Request.Files.Count; i++){ - if (CanHandleFile(HttpContext.Current.Request.Files[i].FileName)) - { - string filename = MakeUniqueFilename(path, HttpContext.Current.Request.Files[i].FileName); - string dest = Path.Combine(path, filename); - HttpContext.Current.Request.Files[i].SaveAs(dest); - if(GetFileType(new FileInfo(filename).Extension) == "image"){ - int w = 0; - int h = 0; - int.TryParse(GetSetting("MAX_IMAGE_WIDTH"), out w); - int.TryParse(GetSetting("MAX_IMAGE_HEIGHT"), out h); - ImageResize(dest, dest, w, h); - } - } - else - res = GetSuccessRes(LangRes("E_UploadNotAll")); - } - } - catch(Exception ex){ - res = GetErrorRes(ex.Message); - } - _r.Write(""); - } - - public bool IsReusable { - get { - return false; - } - } - -} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/filemanager/conf.json b/src/Presentation/SmartStore.Web/Content/filemanager/conf.json deleted file mode 100644 index 3df29bf505..0000000000 --- a/src/Presentation/SmartStore.Web/Content/filemanager/conf.json +++ /dev/null @@ -1,34 +0,0 @@ -{ -"FILES_ROOT": "~/Media/Uploaded", -"SESSION_PATH_KEY": "", -"THUMBS_VIEW_WIDTH": "140", -"THUMBS_VIEW_HEIGHT": "120", -"PREVIEW_THUMB_WIDTH":"300", -"PREVIEW_THUMB_HEIGHT":"200", -"MAX_IMAGE_WIDTH": "0", -"MAX_IMAGE_HEIGHT": "0", -"INTEGRATION": "ckeditor", -"DIRLIST": "asp_net/main.ashx?a=DIRLIST", -"CREATEDIR": "asp_net/main.ashx?a=CREATEDIR", -"DELETEDIR": "asp_net/main.ashx?a=DELETEDIR", -"MOVEDIR": "asp_net/main.ashx?a=MOVEDIR", -"COPYDIR": "asp_net/main.ashx?a=COPYDIR", -"RENAMEDIR": "asp_net/main.ashx?a=RENAMEDIR", -"FILESLIST": "asp_net/main.ashx?a=FILESLIST", -"UPLOAD": "asp_net/main.ashx?a=UPLOAD", -"DOWNLOAD": "asp_net/main.ashx?a=DOWNLOAD", -"DOWNLOADDIR": "asp_net/main.ashx?a=DOWNLOADDIR", -"DOWNLOADDIR": "asp_net/main.ashx?a=DOWNLOADDIR", -"DELETEFILE": "asp_net/main.ashx?a=DELETEFILE", -"MOVEFILE": "asp_net/main.ashx?a=MOVEFILE", -"COPYFILE": "asp_net/main.ashx?a=COPYFILE", -"RENAMEFILE": "asp_net/main.ashx?a=RENAMEFILE", -"GENERATETHUMB": "asp_net/main.ashx?a=GENERATETHUMB", -"DEFAULTVIEW": "list", -"FORBIDDEN_UPLOADS": "zip js jsp jsb mhtml mht xhtml xht php phtml php3 php4 php5 phps shtml jhtml pl sh py cgi exe scr dll msi vbs bat com pif cmd vxd cpl htpasswd htaccess", -"ALLOWED_UPLOADS": "", -"FILEPERMISSIONS": "0644", -"DIRPERMISSIONS": "0755", -"LANG": "auto", -"DATEFORMAT": "dd.MM.yyyy HH:mm" -} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/font-awesome.css b/src/Presentation/SmartStore.Web/Content/font-awesome.css index 5bea9bf0d5..ba49171638 100644 --- a/src/Presentation/SmartStore.Web/Content/font-awesome.css +++ b/src/Presentation/SmartStore.Web/Content/font-awesome.css @@ -1,13 +1,13 @@ /*! - * Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome + * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) */ /* FONT PATH * -------------------------- */ @font-face { font-family: 'FontAwesome'; - src: url('fonts/fontawesome-webfont.eot?v=4.2.0'); - src: url('fonts/fontawesome-webfont.eot?#iefix&v=4.2.0') format('embedded-opentype'), url('fonts/fontawesome-webfont.woff?v=4.2.0') format('woff'), url('fonts/fontawesome-webfont.ttf?v=4.2.0') format('truetype'), url('fonts/fontawesome-webfont.svg?v=4.2.0#fontawesomeregular') format('svg'); + src: url('fonts/fontawesome-webfont.eot?v=4.5.0'); + src: url('fonts/fontawesome-webfont.eot?#iefix&v=4.5.0') format('embedded-opentype'), url('fonts/fontawesome-webfont.woff2?v=4.5.0') format('woff2'), url('fonts/fontawesome-webfont.woff?v=4.5.0') format('woff'), url('fonts/fontawesome-webfont.ttf?v=4.5.0') format('truetype'), url('fonts/fontawesome-webfont.svg?v=4.5.0#fontawesomeregular') format('svg'); font-weight: normal; font-style: normal; } @@ -64,6 +64,19 @@ border: solid 0.08em #eeeeee; border-radius: .1em; } +.fa-pull-left { + float: left; +} +.fa-pull-right { + float: right; +} +.fa.fa-pull-left { + margin-right: .3em; +} +.fa.fa-pull-right { + margin-left: .3em; +} +/* Deprecated as of 4.4.0 */ .pull-right { float: right; } @@ -80,6 +93,10 @@ -webkit-animation: fa-spin 2s infinite linear; animation: fa-spin 2s infinite linear; } +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); +} @-webkit-keyframes fa-spin { 0% { -webkit-transform: rotate(0deg); @@ -610,6 +627,7 @@ .fa-twitter:before { content: "\f099"; } +.fa-facebook-f:before, .fa-facebook:before { content: "\f09a"; } @@ -622,6 +640,7 @@ .fa-credit-card:before { content: "\f09d"; } +.fa-feed:before, .fa-rss:before { content: "\f09e"; } @@ -1259,7 +1278,8 @@ .fa-male:before { content: "\f183"; } -.fa-gittip:before { +.fa-gittip:before, +.fa-gratipay:before { content: "\f184"; } .fa-sun-o:before { @@ -1502,6 +1522,8 @@ .fa-git:before { content: "\f1d3"; } +.fa-y-combinator-square:before, +.fa-yc-square:before, .fa-hacker-news:before { content: "\f1d4"; } @@ -1670,3 +1692,395 @@ .fa-meanpath:before { content: "\f20c"; } +.fa-buysellads:before { + content: "\f20d"; +} +.fa-connectdevelop:before { + content: "\f20e"; +} +.fa-dashcube:before { + content: "\f210"; +} +.fa-forumbee:before { + content: "\f211"; +} +.fa-leanpub:before { + content: "\f212"; +} +.fa-sellsy:before { + content: "\f213"; +} +.fa-shirtsinbulk:before { + content: "\f214"; +} +.fa-simplybuilt:before { + content: "\f215"; +} +.fa-skyatlas:before { + content: "\f216"; +} +.fa-cart-plus:before { + content: "\f217"; +} +.fa-cart-arrow-down:before { + content: "\f218"; +} +.fa-diamond:before { + content: "\f219"; +} +.fa-ship:before { + content: "\f21a"; +} +.fa-user-secret:before { + content: "\f21b"; +} +.fa-motorcycle:before { + content: "\f21c"; +} +.fa-street-view:before { + content: "\f21d"; +} +.fa-heartbeat:before { + content: "\f21e"; +} +.fa-venus:before { + content: "\f221"; +} +.fa-mars:before { + content: "\f222"; +} +.fa-mercury:before { + content: "\f223"; +} +.fa-intersex:before, +.fa-transgender:before { + content: "\f224"; +} +.fa-transgender-alt:before { + content: "\f225"; +} +.fa-venus-double:before { + content: "\f226"; +} +.fa-mars-double:before { + content: "\f227"; +} +.fa-venus-mars:before { + content: "\f228"; +} +.fa-mars-stroke:before { + content: "\f229"; +} +.fa-mars-stroke-v:before { + content: "\f22a"; +} +.fa-mars-stroke-h:before { + content: "\f22b"; +} +.fa-neuter:before { + content: "\f22c"; +} +.fa-genderless:before { + content: "\f22d"; +} +.fa-facebook-official:before { + content: "\f230"; +} +.fa-pinterest-p:before { + content: "\f231"; +} +.fa-whatsapp:before { + content: "\f232"; +} +.fa-server:before { + content: "\f233"; +} +.fa-user-plus:before { + content: "\f234"; +} +.fa-user-times:before { + content: "\f235"; +} +.fa-hotel:before, +.fa-bed:before { + content: "\f236"; +} +.fa-viacoin:before { + content: "\f237"; +} +.fa-train:before { + content: "\f238"; +} +.fa-subway:before { + content: "\f239"; +} +.fa-medium:before { + content: "\f23a"; +} +.fa-yc:before, +.fa-y-combinator:before { + content: "\f23b"; +} +.fa-optin-monster:before { + content: "\f23c"; +} +.fa-opencart:before { + content: "\f23d"; +} +.fa-expeditedssl:before { + content: "\f23e"; +} +.fa-battery-4:before, +.fa-battery-full:before { + content: "\f240"; +} +.fa-battery-3:before, +.fa-battery-three-quarters:before { + content: "\f241"; +} +.fa-battery-2:before, +.fa-battery-half:before { + content: "\f242"; +} +.fa-battery-1:before, +.fa-battery-quarter:before { + content: "\f243"; +} +.fa-battery-0:before, +.fa-battery-empty:before { + content: "\f244"; +} +.fa-mouse-pointer:before { + content: "\f245"; +} +.fa-i-cursor:before { + content: "\f246"; +} +.fa-object-group:before { + content: "\f247"; +} +.fa-object-ungroup:before { + content: "\f248"; +} +.fa-sticky-note:before { + content: "\f249"; +} +.fa-sticky-note-o:before { + content: "\f24a"; +} +.fa-cc-jcb:before { + content: "\f24b"; +} +.fa-cc-diners-club:before { + content: "\f24c"; +} +.fa-clone:before { + content: "\f24d"; +} +.fa-balance-scale:before { + content: "\f24e"; +} +.fa-hourglass-o:before { + content: "\f250"; +} +.fa-hourglass-1:before, +.fa-hourglass-start:before { + content: "\f251"; +} +.fa-hourglass-2:before, +.fa-hourglass-half:before { + content: "\f252"; +} +.fa-hourglass-3:before, +.fa-hourglass-end:before { + content: "\f253"; +} +.fa-hourglass:before { + content: "\f254"; +} +.fa-hand-grab-o:before, +.fa-hand-rock-o:before { + content: "\f255"; +} +.fa-hand-stop-o:before, +.fa-hand-paper-o:before { + content: "\f256"; +} +.fa-hand-scissors-o:before { + content: "\f257"; +} +.fa-hand-lizard-o:before { + content: "\f258"; +} +.fa-hand-spock-o:before { + content: "\f259"; +} +.fa-hand-pointer-o:before { + content: "\f25a"; +} +.fa-hand-peace-o:before { + content: "\f25b"; +} +.fa-trademark:before { + content: "\f25c"; +} +.fa-registered:before { + content: "\f25d"; +} +.fa-creative-commons:before { + content: "\f25e"; +} +.fa-gg:before { + content: "\f260"; +} +.fa-gg-circle:before { + content: "\f261"; +} +.fa-tripadvisor:before { + content: "\f262"; +} +.fa-odnoklassniki:before { + content: "\f263"; +} +.fa-odnoklassniki-square:before { + content: "\f264"; +} +.fa-get-pocket:before { + content: "\f265"; +} +.fa-wikipedia-w:before { + content: "\f266"; +} +.fa-safari:before { + content: "\f267"; +} +.fa-chrome:before { + content: "\f268"; +} +.fa-firefox:before { + content: "\f269"; +} +.fa-opera:before { + content: "\f26a"; +} +.fa-internet-explorer:before { + content: "\f26b"; +} +.fa-tv:before, +.fa-television:before { + content: "\f26c"; +} +.fa-contao:before { + content: "\f26d"; +} +.fa-500px:before { + content: "\f26e"; +} +.fa-amazon:before { + content: "\f270"; +} +.fa-calendar-plus-o:before { + content: "\f271"; +} +.fa-calendar-minus-o:before { + content: "\f272"; +} +.fa-calendar-times-o:before { + content: "\f273"; +} +.fa-calendar-check-o:before { + content: "\f274"; +} +.fa-industry:before { + content: "\f275"; +} +.fa-map-pin:before { + content: "\f276"; +} +.fa-map-signs:before { + content: "\f277"; +} +.fa-map-o:before { + content: "\f278"; +} +.fa-map:before { + content: "\f279"; +} +.fa-commenting:before { + content: "\f27a"; +} +.fa-commenting-o:before { + content: "\f27b"; +} +.fa-houzz:before { + content: "\f27c"; +} +.fa-vimeo:before { + content: "\f27d"; +} +.fa-black-tie:before { + content: "\f27e"; +} +.fa-fonticons:before { + content: "\f280"; +} +.fa-reddit-alien:before { + content: "\f281"; +} +.fa-edge:before { + content: "\f282"; +} +.fa-credit-card-alt:before { + content: "\f283"; +} +.fa-codiepie:before { + content: "\f284"; +} +.fa-modx:before { + content: "\f285"; +} +.fa-fort-awesome:before { + content: "\f286"; +} +.fa-usb:before { + content: "\f287"; +} +.fa-product-hunt:before { + content: "\f288"; +} +.fa-mixcloud:before { + content: "\f289"; +} +.fa-scribd:before { + content: "\f28a"; +} +.fa-pause-circle:before { + content: "\f28b"; +} +.fa-pause-circle-o:before { + content: "\f28c"; +} +.fa-stop-circle:before { + content: "\f28d"; +} +.fa-stop-circle-o:before { + content: "\f28e"; +} +.fa-shopping-bag:before { + content: "\f290"; +} +.fa-shopping-basket:before { + content: "\f291"; +} +.fa-hashtag:before { + content: "\f292"; +} +.fa-bluetooth:before { + content: "\f293"; +} +.fa-bluetooth-b:before { + content: "\f294"; +} +.fa-percent:before { + content: "\f295"; +} diff --git a/src/Presentation/SmartStore.Web/Content/font-awesome.min.css b/src/Presentation/SmartStore.Web/Content/font-awesome.min.css index 08e68ec0c0..acce2ef232 100644 --- a/src/Presentation/SmartStore.Web/Content/font-awesome.min.css +++ b/src/Presentation/SmartStore.Web/Content/font-awesome.min.css @@ -1,4 +1,4 @@ /*! - * Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome + * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:'FontAwesome';src:url('fonts/fontawesome-webfont.eot?v=4.2.0');src:url('fonts/fontawesome-webfont.eot?#iefix&v=4.2.0') format('embedded-opentype'),url('fonts/fontawesome-webfont.woff?v=4.2.0') format('woff'),url('fonts/fontawesome-webfont.ttf?v=4.2.0') format('truetype'),url('fonts/fontawesome-webfont.svg?v=4.2.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"} \ No newline at end of file + */@font-face{font-family:'FontAwesome';src:url('fonts/fontawesome-webfont.eot?v=4.5.0');src:url('fonts/fontawesome-webfont.eot?#iefix&v=4.5.0') format('embedded-opentype'),url('fonts/fontawesome-webfont.woff2?v=4.5.0') format('woff2'),url('fonts/fontawesome-webfont.woff?v=4.5.0') format('woff'),url('fonts/fontawesome-webfont.ttf?v=4.5.0') format('truetype'),url('fonts/fontawesome-webfont.svg?v=4.5.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"} diff --git a/src/Presentation/SmartStore.Web/Content/fonts/FontAwesome.otf b/src/Presentation/SmartStore.Web/Content/fonts/FontAwesome.otf index 81c9ad949b..3ed7f8b48a 100644 Binary files a/src/Presentation/SmartStore.Web/Content/fonts/FontAwesome.otf and b/src/Presentation/SmartStore.Web/Content/fonts/FontAwesome.otf differ diff --git a/src/Presentation/SmartStore.Web/Content/fonts/Fontastic.eot b/src/Presentation/SmartStore.Web/Content/fonts/Fontastic.eot deleted file mode 100644 index e179d5fe77..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/fonts/Fontastic.eot and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/fonts/Fontastic.svg b/src/Presentation/SmartStore.Web/Content/fonts/Fontastic.svg deleted file mode 100644 index 95f295b61e..0000000000 --- a/src/Presentation/SmartStore.Web/Content/fonts/Fontastic.svg +++ /dev/null @@ -1,391 +0,0 @@ - - - - -This is a custom SVG font generated by IcoMoon. -9 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/fonts/Fontastic.ttf b/src/Presentation/SmartStore.Web/Content/fonts/Fontastic.ttf deleted file mode 100644 index 9817e5cf4e..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/fonts/Fontastic.ttf and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/fonts/Fontastic.woff b/src/Presentation/SmartStore.Web/Content/fonts/Fontastic.woff deleted file mode 100644 index 3a60828b77..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/fonts/Fontastic.woff and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/fonts/fontastic.less b/src/Presentation/SmartStore.Web/Content/fonts/fontastic.less deleted file mode 100644 index eaa4cfc2da..0000000000 --- a/src/Presentation/SmartStore.Web/Content/fonts/fontastic.less +++ /dev/null @@ -1,202 +0,0 @@ -@font-face { - font-family: 'Fontastic'; - src:url('Fontastic.eot'); - src:url('Fontastic.eot?#iefix') format('embedded-opentype'), - url('Fontastic.woff') format('woff'), - url('Fontastic.ttf') format('truetype'), - url('Fontastic.svg#Fontastic') format('svg'); - font-weight: normal; - font-style: normal; -} - -/* Use the following CSS code if you want to use data attributes for inserting your icons */ -[data-icon]:before { - font-family: 'Fontastic'; - content: attr(data-icon); - speak: none; - font-weight: normal; - -webkit-font-smoothing: auto; -} - -/* Use the following CSS code if you want to have a class per icon */ -[class^="sm-icon-"]:before, [class*=" sm-icon-"]:before { - font-family: 'Fontastic'; - font-style: normal; - speak: none; - font-weight: normal; - -webkit-font-smoothing: antialiased; -} -.sm-icon-database:before { - content: "\21"; -} -.sm-icon-user:before { - content: "\22"; -} -.sm-icon-cog:before { - content: "\23"; -} -.sm-icon-stats-up:before { - content: "\24"; -} -.sm-icon-gift:before { - content: "\25"; -} -.sm-icon-cube:before { - content: "\26"; -} -.sm-icon-help:before { - content: "\27"; -} -.sm-icon-loop:before { - content: "\28"; -} -.sm-icon-tag:before { - content: "\29"; -} -.sm-icon-tab:before { - content: "\2a"; -} -.sm-icon-gift-2:before { - content: "\2b"; -} -.sm-icon-home:before { - content: "\2c"; -} -.sm-icon-home-2:before { - content: "\2d"; -} -.sm-icon-earth:before { - content: "\2e"; -} -.sm-icon-database-2:before { - content: "\2f"; -} -.sm-icon-cog-2:before { - content: "\30"; -} -.sm-icon-loop-alt3:before { - content: "\31"; -} -.sm-icon-loop-alt4:before { - content: "\32"; -} -.sm-icon-loop-alt2:before { - content: "\33"; -} -.sm-icon-layers:before { - content: "\34"; -} -.sm-icon-user-2:before { - content: "\35"; -} -.sm-icon-tag-stroke:before { - content: "\36"; -} -.sm-icon-tag-fill:before { - content: "\37"; -} -.sm-icon-home-3:before { - content: "\38"; -} -.sm-icon-user-3:before { - content: "\39"; -} -.sm-icon-user-4:before { - content: "\3a"; -} -.sm-icon-users:before { - content: "\3b"; -} -.sm-icon-archive:before { - content: "\3c"; -} -.sm-icon-cog-3:before { - content: "\3d"; -} -.sm-icon-refresh:before { - content: "\3e"; -} -.sm-icon-tag-2:before { - content: "\3f"; -} -.sm-icon-tag-3:before { - content: "\40"; -} -.sm-icon-cog-4:before { - content: "\41"; -} -.sm-icon-folder:before { - content: "\42"; -} -.sm-icon-chart:before { - content: "\43"; -} -.sm-icon-cube-2:before { - content: "\44"; -} -.sm-icon-megaphone:before { - content: "\45"; -} -.sm-icon-box:before { - content: "\46"; -} -.sm-icon-layout:before { - content: "\47"; -} -.sm-icon-layout-2:before { - content: "\48"; -} -.sm-icon-layout-3:before { - content: "\49"; -} -.sm-icon-layout-4:before { - content: "\4a"; -} -.sm-icon-stats:before { - content: "\4b"; -} -.sm-icon-discout:before { - content: "\4c"; -} -.sm-icon-retweet:before { - content: "\4d"; -} -.sm-icon-tags:before { - content: "\4e"; -} -.sm-icon-users-2:before { - content: "\4f"; -} -.sm-icon-user-5:before { - content: "\50"; -} -.sm-icon-price:before { - content: "\51"; -} -.sm-icon-retweet-2:before { - content: "\52"; -} -.sm-icon-download:before { - content: "\53"; -} -.sm-icon-upload:before { - content: "\54"; -} -.sm-icon-home-4:before { - content: "\55"; -} -.sm-icon-dots-three:before { - content: "\56"; -} -.sm-icon-users-3:before { - content: "\e029"; -} -.sm-icon-user-6:before { - content: "\e028"; -} -.sm-icon-cog-5:before { - content: "\e000"; -} -.sm-icon-user-7:before { - content: "\e001"; -} diff --git a/src/Presentation/SmartStore.Web/Content/fonts/fontawesome-webfont.eot b/src/Presentation/SmartStore.Web/Content/fonts/fontawesome-webfont.eot index 84677bc0c5..9b6afaedc0 100644 Binary files a/src/Presentation/SmartStore.Web/Content/fonts/fontawesome-webfont.eot and b/src/Presentation/SmartStore.Web/Content/fonts/fontawesome-webfont.eot differ diff --git a/src/Presentation/SmartStore.Web/Content/fonts/fontawesome-webfont.svg b/src/Presentation/SmartStore.Web/Content/fonts/fontawesome-webfont.svg index d907b25ae6..d05688e9e2 100644 --- a/src/Presentation/SmartStore.Web/Content/fonts/fontawesome-webfont.svg +++ b/src/Presentation/SmartStore.Web/Content/fonts/fontawesome-webfont.svg @@ -147,14 +147,14 @@ - + - + @@ -219,8 +219,8 @@ - - + + @@ -275,7 +275,7 @@ - + @@ -362,7 +362,7 @@ - + @@ -399,7 +399,7 @@ - + @@ -410,9 +410,9 @@ - - - + + + @@ -438,7 +438,7 @@ - + @@ -454,12 +454,12 @@ - + - + @@ -483,13 +483,13 @@ - + - + @@ -513,8 +513,143 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/fonts/fontawesome-webfont.ttf b/src/Presentation/SmartStore.Web/Content/fonts/fontawesome-webfont.ttf index 96a3639cdd..26dea7951a 100644 Binary files a/src/Presentation/SmartStore.Web/Content/fonts/fontawesome-webfont.ttf and b/src/Presentation/SmartStore.Web/Content/fonts/fontawesome-webfont.ttf differ diff --git a/src/Presentation/SmartStore.Web/Content/fonts/fontawesome-webfont.woff b/src/Presentation/SmartStore.Web/Content/fonts/fontawesome-webfont.woff index 628b6a52a8..dc35ce3c2c 100644 Binary files a/src/Presentation/SmartStore.Web/Content/fonts/fontawesome-webfont.woff and b/src/Presentation/SmartStore.Web/Content/fonts/fontawesome-webfont.woff differ diff --git a/src/Presentation/SmartStore.Web/Content/fonts/fontawesome-webfont.woff2 b/src/Presentation/SmartStore.Web/Content/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000000..500e517253 Binary files /dev/null and b/src/Presentation/SmartStore.Web/Content/fonts/fontawesome-webfont.woff2 differ diff --git a/src/Presentation/SmartStore.Web/Content/fonts/raw/IcoMoon Session.json b/src/Presentation/SmartStore.Web/Content/fonts/raw/IcoMoon Session.json deleted file mode 100644 index b0e4ef45ab..0000000000 --- a/src/Presentation/SmartStore.Web/Content/fonts/raw/IcoMoon Session.json +++ /dev/null @@ -1 +0,0 @@ -{"share":"6", "iconsVersion":"1.4", "icomoon":"{\"selected\":[{\"idx\":\"591\",\"unicode\":\"21\"},{\"idx\":\"577\",\"unicode\":\"22\"},{\"idx\":\"563\",\"unicode\":\"23\"},{\"idx\":\"560\",\"unicode\":\"24\"},{\"idx\":\"559\",\"unicode\":\"25\"},{\"idx\":\"541\",\"unicode\":\"26\"},{\"idx\":\"497\",\"unicode\":\"27\"},{\"idx\":\"488\",\"unicode\":\"28\"},{\"idx\":\"270\",\"unicode\":\"29\"},{\"idx\":\"259\",\"unicode\":\"2b\"},{\"idx\":\"642\",\"unicode\":\"2d\"},{\"idx\":\"285\",\"unicode\":\"2e\"},{\"idx\":\"109\",\"unicode\":\"33\"},{\"idx\":\"36\",\"unicode\":\"34\"},{\"idx\":\"19\",\"unicode\":\"36\"},{\"idx\":\"816\",\"unicode\":\"3c\"},{\"idx\":\"803\",\"unicode\":\"3d\"},{\"idx\":\"788\",\"unicode\":\"3e\"},{\"idx\":\"1149\",\"unicode\":\"3f\"},{\"idx\":\"1148\",\"unicode\":\"40\"},{\"idx\":\"1065\",\"unicode\":\"43\"},{\"idx\":\"983\",\"unicode\":\"44\"},{\"idx\":\"991\",\"unicode\":\"45\"},{\"idx\":\"981\",\"unicode\":\"46\"},{\"idx\":\"962\",\"unicode\":\"47\"},{\"idx\":\"961\",\"unicode\":\"48\"},{\"idx\":\"959\",\"unicode\":\"49\"},{\"idx\":\"958\",\"unicode\":\"4a\"},{\"idx\":\"933\",\"unicode\":\"4b\"},{\"idx\":\"924\",\"unicode\":\"4c\"},{\"idx\":\"914\",\"unicode\":\"4d\"},{\"idx\":\"830\",\"unicode\":\"4e\"},{\"idx\":\"1393\",\"unicode\":\"4f\"},{\"idx\":\"1394\",\"unicode\":\"50\"},{\"idx\":\"1368\",\"unicode\":\"51\"},{\"idx\":\"1331\",\"unicode\":\"52\"},{\"idx\":\"1316\",\"unicode\":\"53\"},{\"idx\":\"1317\",\"unicode\":\"54\"},{\"idx\":\"1253\",\"unicode\":\"55\"},{\"idx\":\"1248\",\"unicode\":\"56\"},{\"idx\":\"1453\",\"unicode\":\"57\"},{\"idx\":\"1392\",\"unicode\":\"2a\"},{\"idx\":\"1367\",\"unicode\":\"2c\"},{\"idx\":\"1674\",\"unicode\":\"2f\"},{\"idx\":\"1673\",\"unicode\":\"30\"},{\"idx\":\"1362\",\"unicode\":\"31\"},{\"idx\":\"616\",\"unicode\":\"32\"},{\"idx\":\"721\",\"unicode\":\"35\"},{\"idx\":\"318\",\"unicode\":\"37\"},{\"idx\":\"255\",\"unicode\":\"38\"}],\"customIcons\":[{\"metadata\":{\"id\":\"iconic\",\"name\":\"Iconic\",\"link\":\"http://somerandomdude.com/work/iconic/\",\"author\":\"P.J. Onori\",\"authorLink\":\"http://somerandomdude.com\",\"license\":\"CC BY-SA 3.0\",\"licenseLink\":\"http://creativecommons.org/licenses/by-sa/3.0/us/\",\"defaultunicode\":false},\"svgs\":[\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\"]},{\"metadata\":{\"id\":\"meteocons\",\"name\":\"Meteocons\",\"link\":\"http://www.alessioatzeni.com/meteocons/\",\"author\":\"Alessio Atzeni\",\"authorLink\":\"http://www.alessioatzeni.com/\",\"license\":\"Arbitrary\",\"licenseLink\":\"http://www.alessioatzeni.com/meteocons/#about\",\"defaultunicode\":false},\"svgs\":[\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\"]},{\"metadata\":{\"id\":\"broccolidry\",\"name\":\"Broccolidry\",\"link\":\"http://dribbble.com/shots/587469-Free-16px-Broccolidryiconsaniconsetitisfullof-icons\",\"author\":\"Visual Idiot\",\"authorLink\":\"http://idiot.vc/\",\"license\":\"Aribitrary\",\"licenseLink\":\"http://licence.visualidiot.com/\",\"defaultunicode\":false},\"svgs\":[\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\"]},{\"metadata\":{\"id\":\"icomoon\",\"name\":\"IcoMoon - Free\",\"link\":\"http://keyamoon.com/icomoon/\",\"author\":\"Keyamoon\",\"authorLink\":\"http://keyamoon.com/\",\"license\":\"CC BY-SA 3.0\",\"licenseLink\":\"http://creativecommons.org/licenses/by-sa/3.0/\",\"defaultunicode\":false},\"svgs\":[\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\"]},{\"metadata\":{\"id\":\"iconminia\",\"name\":\"Icon Minia\",\"link\":\"http://dribbble.com/shots/598215-Icon-Minia-139-Vector-Icons\",\"author\":\"Egemen Kapusuz\",\"authorLink\":\"https://twitter.com/#!/egemem\",\"license\":\"GPL V3\",\"licenseLink\":\"http://www.gnu.org/copyleft/gpl.html\",\"defaultunicode\":false},\"svgs\":[\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\"]},{\"metadata\":{\"id\":\"ecoico\",\"name\":\"Eco Ico\",\"link\":\"http://dribbble.com/shots/665585-Eco-Ico\",\"author\":\"Matthew Skiles\",\"authorLink\":\"http://www.dvq.co.nz/\",\"license\":\"CC0\",\"licenseLink\":\"http://creativecommons.org/publicdomain/zero/1.0/\",\"defaultunicode\":false},\"svgs\":[\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\"]},{\"metadata\":{\"id\":\"brankic1979\",\"name\":\"Brankic1979\",\"link\":\"http://brankic1979.com/icons/\",\"author\":\"Keyamoon\",\"authorLink\":\"http://brankic1979.com\",\"license\":\"Custom\",\"licenseLink\":\"http://brankic1979.com/icons/\",\"defaultunicode\":false},\"svgs\":[\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\"]},{\"metadata\":{\"id\":\"cuticons\",\"name\":\"Cuticons\",\"link\":\"http://dribbble.com/shots/631056-Cuticons-You-wanted-free-icons-right\",\"author\":\"Vaibhav Bhat\",\"authorLink\":\"https://twitter.com/vabhat\",\"license\":\"CC0\",\"licenseLink\":\"http://creativecommons.org/publicdomain/zero/1.0/\",\"defaultunicode\":false},\"svgs\":[\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\"]},{\"metadata\":{\"id\":\"entypo\",\"name\":\"Entypo\",\"link\":\"http://www.entypo.com/\",\"author\":\"Daniel Bruce\",\"authorLink\":\"mailto:daniel@precinct.net\",\"license\":\"CC BY-SA 3.0\",\"licenseLink\":\"http://creativecommons.org/licenses/by-sa/3.0/\",\"defaultunicode\":false},\"svgs\":[\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\"]},{\"metadata\":{\"id\":\"typicons\",\"name\":\"Typicons\",\"link\":\"http://typicons.com/\",\"author\":\"Stephen Hutchings\",\"license\":\"CC BY-SA 3.0\",\"licenseLink\":\"http://creativecommons.org/licenses/by-sa/3.0/\",\"defaultunicode\":true},\"svgs\":[\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\"]},{\"metadata\":{\"id\":\"silkcons\",\"name\":\"Silkcons\",\"link\":\"http://dribbble.com/shots/632219-Silkcons-You-can-t-do-with-just-one-icon-set\",\"author\":\"Vaibhav Bhat\",\"authorLink\":\"https://twitter.com/vabhat\",\"license\":\"CC0\",\"licenseLink\":\"http://creativecommons.org/publicdomain/zero/1.0/\",\"defaultunicode\":false},\"svgs\":[\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\"]},{\"metadata\":{\"id\":\"wpzoom\",\"name\":\"WPZOOM Developer Icon Set\",\"link\":\"http://www.wpzoom.com/wpzoom/new-freebie-wpzoom-developer-icon-set-154-free-icons/\",\"author\":\"David Ferreira\",\"authorLink\":\"http://cargocollective.com/davidferreira\",\"license\":\"CC BY-SA 3.0\",\"licenseLink\":\"http://creativecommons.org/licenses/by-sa/3.0/\",\"defaultunicode\":false},\"svgs\":[\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\"]},{\"metadata\":{\"id\":\"loops105\",\"name\":\"105 Loops\",\"link\":\"http://dribbble.com/shots/707117-105-Loops-with-PSD\",\"author\":\"Pranav\",\"authorLink\":\"http://dribbble.com/pranav\",\"license\":\"Custom\",\"licenseLink\":\"http://dribbble.com/shots/707117-105-Loops-with-PSD\",\"defaultunicode\":false},\"svgs\":[\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\"]}],\"IDs\":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,19,20,21,22,23,24,25,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1288,1289,1290,1291,1292,1293,1294,1295,1296,1297,1298,1299,1300,1301,1302,1303,1304,1305,1306,1307,1308,1309,1310,1311,1312,1313,1314,1315,1316,1317,1318,1319,1320,1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1335,1336,1337,1338,1339,1340,1341,1342,1343,1344,1345,1346,1347,1348,1349,1350,1351,1352,1353,1354,1355,1356,1357,1358,1359,1360,1361,1362,1363,1364,1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540,1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562,1563,1564,1565,1566,1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,1611,1612,1613,1614,1615,1616,1617,1618,1619,1620,1621,1622,1623,1624,1625,1626,1627,1628,1629,1630,1631,1632,1633,1634,1635,1636,1637,1638,1639,1640,1641,1642,1643,1644,1645,1646,1647,1648,1649,1650,1651,1652,1653,1654,1655,1656,1657,1658,1659,1660,1661,1662,1663,1664,1665,1666,1667,1668,1669,1670,1671,1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1682,1683,1684,1685,1686,1687,1688,1689,1690,1691,1692,1693,1694,1695,1696,1697,1698,1699,1700,1701,1702,1703,1704,1705,1706,1707,1708,1709,1710,1711,1712,1713,1714,1715,1716,1717,1718,1719,1720,1721,1722,1723,1724,1725,1726,1727,1728,1729,1730,1731,1732,1733,1734,1735,1736,1737,1738,1739,1740,1741,1742,1743,1744,1745,1746,1747,1748,1749,1750,1751,1752,1753,1754,1755,1756,1757,1758,1759,1760,1761,1762,1763,1764,1765,1766,1767,1768,1769,1770,1771,1772,1773,1774,1775,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1786,1787,1788,1789,1790,1791,1792,1793,1794,1795,1796,1797,1798,1799,1800,1801,1802,1803,1804,1805,1806,1807,1808,1809,1810,1811,1812,1813,1814,1815,1816,1817,1818,1819,1820,1821,1822,1823,1824,1825,1826,1827,1828,1829,1830,1831,1832,1833,1834,1835,1836,1837,1838,1839,1840,1841,1842,1843,1844,1845,1846,1847,1848,1849,1850,1851,1852,1853,1854,1855,1856,1857,1858,1859,1860,1861,1862,1863,1864,1865,1866,1867,1868,1869],\"user\":{\"email\":\"keyamoon@gmail.com\",\"newsletter\":true,\"secret\":\"29769e48fe4bab8807b024a41d770900e18015c1af12cf4ad63d2d19009e6a90aead0d2885d7a32787d7336b442a44662cdfddfa2a2b0c74445becb1f50a8998\",\"uid\":4}}","inputCache":"{\"baseline\":\"0\",\"emSize\":\"512\",\"prev_size\":\"32\",\"hdr-imported\":\"checked\",\"iconAlignment\":\"0\",\"showGrid\":\"checked\",\"fi_name\":\"Fontastic\",\"fi_id\":\"\",\"fi_link\":\"\",\"fi_author\":\"\",\"fi_authorLink\":\"\",\"fi_license\":\"\",\"fi_licenseLink\":\"\",\"include_metadata\":false,\"base64\":false,\"img-height\":\"24\",\"img-color\":\"000000\",\"include_png\":\"checked\",\"fi_class\":\"sm-icon-\",\"showCloudLinks\":false,\"hdr-iconic\":\"checked\",\"hdr-meteocons\":\"checked\",\"hdr-broccolidry\":\"checked\",\"hdr-icomoon\":\"checked\",\"hdr-iconminia\":\"checked\",\"hdr-ecoico\":\"checked\",\"hdr-brankic1979\":\"checked\",\"hdr-cuticons\":\"checked\",\"hdr-entypo\":\"checked\",\"hdr-typicons\":\"checked\",\"hdr-silkcons\":\"checked\",\"hdr-wpzoom\":\"checked\",\"hdr-loops105\":\"checked\",\"sprites-cols\":\"16\",\"include_sprites\":false}"} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/fonts/raw/fonts/Fontastic.eot b/src/Presentation/SmartStore.Web/Content/fonts/raw/fonts/Fontastic.eot deleted file mode 100644 index e179d5fe77..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/fonts/raw/fonts/Fontastic.eot and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/fonts/raw/fonts/Fontastic.svg b/src/Presentation/SmartStore.Web/Content/fonts/raw/fonts/Fontastic.svg deleted file mode 100644 index 95f295b61e..0000000000 --- a/src/Presentation/SmartStore.Web/Content/fonts/raw/fonts/Fontastic.svg +++ /dev/null @@ -1,391 +0,0 @@ - - - - -This is a custom SVG font generated by IcoMoon. -9 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/fonts/raw/fonts/Fontastic.ttf b/src/Presentation/SmartStore.Web/Content/fonts/raw/fonts/Fontastic.ttf deleted file mode 100644 index 9817e5cf4e..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/fonts/raw/fonts/Fontastic.ttf and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/fonts/raw/fonts/Fontastic.woff b/src/Presentation/SmartStore.Web/Content/fonts/raw/fonts/Fontastic.woff deleted file mode 100644 index 3a60828b77..0000000000 Binary files a/src/Presentation/SmartStore.Web/Content/fonts/raw/fonts/Fontastic.woff and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Content/fonts/raw/index.html b/src/Presentation/SmartStore.Web/Content/fonts/raw/index.html deleted file mode 100644 index e628421a3b..0000000000 --- a/src/Presentation/SmartStore.Web/Content/fonts/raw/index.html +++ /dev/null @@ -1,508 +0,0 @@ - - - -Your Font/Glyphs - - - - - -
    -
    -
    -

    Your font contains the following glyphs

    -

    The generated SVG font can be imported back to IcoMoon for modification.

    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    -
    -
    -

    CSS Class Names

    -
    - - -  sm-icon-database - - - -  sm-icon-user - - - -  sm-icon-cog - - - -  sm-icon-stats-up - - - -  sm-icon-gift - - - -  sm-icon-cube - - - -  sm-icon-help - - - -  sm-icon-loop - - - -  sm-icon-tag - - - -  sm-icon-gift-2 - - - -  sm-icon-home - - - -  sm-icon-earth - - - -  sm-icon-loop-alt2 - - - -  sm-icon-layers - - - -  sm-icon-tag-stroke - - - -  sm-icon-archive - - - -  sm-icon-cog-2 - - - -  sm-icon-refresh - - - -  sm-icon-tag-2 - - - -  sm-icon-tag-3 - - - -  sm-icon-chart - - - -  sm-icon-cube-2 - - - -  sm-icon-megaphone - - - -  sm-icon-box - - - -  sm-icon-layout - - - -  sm-icon-layout-2 - - - -  sm-icon-layout-3 - - - -  sm-icon-layout-4 - - - -  sm-icon-stats - - - -  sm-icon-discout - - - -  sm-icon-retweet - - - -  sm-icon-tags - - - -  sm-icon-users - - - -  sm-icon-user-2 - - - -  sm-icon-price - - - -  sm-icon-retweet-2 - - - -  sm-icon-download - - - -  sm-icon-upload - - - -  sm-icon-home-2 - - - -  sm-icon-dots-three - - - -  sm-icon-users-2 - - - -  sm-icon-contact - - - -  sm-icon-camera - - - -  sm-icon-cart - - - -  sm-icon-bag - - - -  sm-icon-shopping - - - -  sm-icon-cart-2 - - - -  sm-icon-cart-3 - - - -  sm-icon-cart-4 - - - -  sm-icon-basket - -
    - -
    - - - \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/fonts/raw/license.txt b/src/Presentation/SmartStore.Web/Content/fonts/raw/license.txt deleted file mode 100644 index f963b349f2..0000000000 --- a/src/Presentation/SmartStore.Web/Content/fonts/raw/license.txt +++ /dev/null @@ -1,34 +0,0 @@ -Icon Set: WPZOOM Developer Icon Set -- http://www.wpzoom.com/wpzoom/new-freebie-wpzoom-developer-icon-set-154-free-icons/ -License: CC BY-SA 3.0 -- http://creativecommons.org/licenses/by-sa/3.0/ - - -Icon Set: Typicons -- http://typicons.com/ -License: CC BY-SA 3.0 -- http://creativecommons.org/licenses/by-sa/3.0/ - - -Icon Set: Entypo -- http://www.entypo.com/ -License: CC BY-SA 3.0 -- http://creativecommons.org/licenses/by-sa/3.0/ - - -Icon Set: Brankic1979 -- http://brankic1979.com/icons/ -License: Custom -- http://brankic1979.com/icons/ - - -Icon Set: Eco Ico -- http://dribbble.com/shots/665585-Eco-Ico -License: CC0 -- http://creativecommons.org/publicdomain/zero/1.0/ - - -Icon Set: Icon Minia -- http://dribbble.com/shots/598215-Icon-Minia-139-Vector-Icons -License: GPL V3 -- http://www.gnu.org/copyleft/gpl.html - - -Icon Set: IcoMoon - Free -- http://keyamoon.com/icomoon/ -License: CC BY-SA 3.0 -- http://creativecommons.org/licenses/by-sa/3.0/ - - -Icon Set: Broccolidry -- http://dribbble.com/shots/587469-Free-16px-Broccolidryiconsaniconsetitisfullof-icons -License: Aribitrary -- http://licence.visualidiot.com/ - - -Icon Set: Iconic -- http://somerandomdude.com/work/iconic/ -License: CC BY-SA 3.0 -- http://creativecommons.org/licenses/by-sa/3.0/us/ \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/fonts/raw/lte-ie7.js b/src/Presentation/SmartStore.Web/Content/fonts/raw/lte-ie7.js deleted file mode 100644 index b109d62b28..0000000000 --- a/src/Presentation/SmartStore.Web/Content/fonts/raw/lte-ie7.js +++ /dev/null @@ -1,74 +0,0 @@ -/* Use this script if you need to support IE 7 and IE 6. */ - -window.onload = function() { - function addIcon(el, entity) { - var html = el.innerHTML; - el.innerHTML = '' + entity + '' + html; - } - var icons = { - 'sm-icon-database' : '!', - 'sm-icon-user' : '"', - 'sm-icon-cog' : '#', - 'sm-icon-stats-up' : '$', - 'sm-icon-gift' : '%', - 'sm-icon-cube' : '&', - 'sm-icon-help' : ''', - 'sm-icon-loop' : '(', - 'sm-icon-tag' : ')', - 'sm-icon-gift-2' : '+', - 'sm-icon-home' : '-', - 'sm-icon-earth' : '.', - 'sm-icon-loop-alt2' : '3', - 'sm-icon-layers' : '4', - 'sm-icon-tag-stroke' : '6', - 'sm-icon-archive' : '<', - 'sm-icon-cog-2' : '=', - 'sm-icon-refresh' : '>', - 'sm-icon-tag-2' : '?', - 'sm-icon-tag-3' : '@', - 'sm-icon-chart' : 'C', - 'sm-icon-cube-2' : 'D', - 'sm-icon-megaphone' : 'E', - 'sm-icon-box' : 'F', - 'sm-icon-layout' : 'G', - 'sm-icon-layout-2' : 'H', - 'sm-icon-layout-3' : 'I', - 'sm-icon-layout-4' : 'J', - 'sm-icon-stats' : 'K', - 'sm-icon-discout' : 'L', - 'sm-icon-retweet' : 'M', - 'sm-icon-tags' : 'N', - 'sm-icon-users' : 'O', - 'sm-icon-user-2' : 'P', - 'sm-icon-price' : 'Q', - 'sm-icon-retweet-2' : 'R', - 'sm-icon-download' : 'S', - 'sm-icon-upload' : 'T', - 'sm-icon-home-2' : 'U', - 'sm-icon-dots-three' : 'V', - 'sm-icon-users-2' : 'W', - 'sm-icon-contact' : '*', - 'sm-icon-camera' : ',', - 'sm-icon-cart' : '/', - 'sm-icon-bag' : '0', - 'sm-icon-shopping' : '1', - 'sm-icon-cart-2' : '2', - 'sm-icon-cart-3' : '5', - 'sm-icon-cart-4' : '7', - 'sm-icon-basket' : '8' - }, - els = document.getElementsByTagName('*'), - i, attr, html, c, el; - for (i = 0; i < els.length; i += 1) { - el = els[i]; - attr = el.getAttribute('data-icon'); - if (attr) { - addIcon(el, attr); - } - c = el.className; - c = c.match(/sm-icon-[^\s'"]+/); - if (c && icons[c[0]]) { - addIcon(el, icons[c[0]]); - } - } -}; \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Content/fonts/raw/style.css b/src/Presentation/SmartStore.Web/Content/fonts/raw/style.css deleted file mode 100644 index cf8e62665b..0000000000 --- a/src/Presentation/SmartStore.Web/Content/fonts/raw/style.css +++ /dev/null @@ -1,180 +0,0 @@ -@font-face { - font-family: 'Fontastic'; - src:url('fonts/Fontastic.eot'); - src:url('fonts/Fontastic.eot?#iefix') format('embedded-opentype'), - url('fonts/Fontastic.svg#Fontastic') format('svg'), - url('fonts/Fontastic.woff') format('woff'), - url('fonts/Fontastic.ttf') format('truetype'); - font-weight: normal; - font-style: normal; -} - -/* Use the following CSS code if you want to use data attributes for inserting your icons */ -[data-icon]:before { - font-family: 'Fontastic'; - content: attr(data-icon); - speak: none; - font-weight: normal; - line-height: 1; - -webkit-font-smoothing: antialiased; -} - -/* Use the following CSS code if you want to have a class per icon */ -[class^="sm-icon-"]:before, [class*=" sm-icon-"]:before { - font-family: 'Fontastic'; - font-style: normal; - speak: none; - font-weight: normal; - line-height: 1; - -webkit-font-smoothing: antialiased; -} -.sm-icon-database:before { - content: "\21"; -} -.sm-icon-user:before { - content: "\22"; -} -.sm-icon-cog:before { - content: "\23"; -} -.sm-icon-stats-up:before { - content: "\24"; -} -.sm-icon-gift:before { - content: "\25"; -} -.sm-icon-cube:before { - content: "\26"; -} -.sm-icon-help:before { - content: "\27"; -} -.sm-icon-loop:before { - content: "\28"; -} -.sm-icon-tag:before { - content: "\29"; -} -.sm-icon-gift-2:before { - content: "\2b"; -} -.sm-icon-home:before { - content: "\2d"; -} -.sm-icon-earth:before { - content: "\2e"; -} -.sm-icon-loop-alt2:before { - content: "\33"; -} -.sm-icon-layers:before { - content: "\34"; -} -.sm-icon-tag-stroke:before { - content: "\36"; -} -.sm-icon-archive:before { - content: "\3c"; -} -.sm-icon-cog-2:before { - content: "\3d"; -} -.sm-icon-refresh:before { - content: "\3e"; -} -.sm-icon-tag-2:before { - content: "\3f"; -} -.sm-icon-tag-3:before { - content: "\40"; -} -.sm-icon-chart:before { - content: "\43"; -} -.sm-icon-cube-2:before { - content: "\44"; -} -.sm-icon-megaphone:before { - content: "\45"; -} -.sm-icon-box:before { - content: "\46"; -} -.sm-icon-layout:before { - content: "\47"; -} -.sm-icon-layout-2:before { - content: "\48"; -} -.sm-icon-layout-3:before { - content: "\49"; -} -.sm-icon-layout-4:before { - content: "\4a"; -} -.sm-icon-stats:before { - content: "\4b"; -} -.sm-icon-discout:before { - content: "\4c"; -} -.sm-icon-retweet:before { - content: "\4d"; -} -.sm-icon-tags:before { - content: "\4e"; -} -.sm-icon-users:before { - content: "\4f"; -} -.sm-icon-user-2:before { - content: "\50"; -} -.sm-icon-price:before { - content: "\51"; -} -.sm-icon-retweet-2:before { - content: "\52"; -} -.sm-icon-download:before { - content: "\53"; -} -.sm-icon-upload:before { - content: "\54"; -} -.sm-icon-home-2:before { - content: "\55"; -} -.sm-icon-dots-three:before { - content: "\56"; -} -.sm-icon-users-2:before { - content: "\57"; -} -.sm-icon-contact:before { - content: "\2a"; -} -.sm-icon-camera:before { - content: "\2c"; -} -.sm-icon-cart:before { - content: "\2f"; -} -.sm-icon-bag:before { - content: "\30"; -} -.sm-icon-shopping:before { - content: "\31"; -} -.sm-icon-cart-2:before { - content: "\32"; -} -.sm-icon-cart-3:before { - content: "\35"; -} -.sm-icon-cart-4:before { - content: "\37"; -} -.sm-icon-basket:before { - content: "\38"; -} diff --git a/src/Presentation/SmartStore.Web/Content/install/install.less b/src/Presentation/SmartStore.Web/Content/install/install.less index e079affb89..bdb13586e8 100644 --- a/src/Presentation/SmartStore.Web/Content/install/install.less +++ b/src/Presentation/SmartStore.Web/Content/install/install.less @@ -11,17 +11,6 @@ /* Fonts (Chrome DirectWrite & FF fix) -------------------------------------------------------------- */ -@font-face { - font-family: "Segoe UI Light"; - font-weight: 100; - src: local("Segoe UI Light"), url("../fonts/segoeui-semilight.eot") format('embedded-opentype'), url("../fonts/segoeui-semilight.woff") format('woff'), url("../fonts/segoeui-semilight.ttf") format('truetype'); -} -@font-face { - font-family: "Segoe UI Semibold"; - font-weight: 100; - src: local("Segoe UI Semibold"), url("../fonts/segoeui-semibold.eot") format('embedded-opentype'), url("../fonts/segoeui-semibold.woff") format('woff'), url("../fonts/segoeui-semibold.ttf") format('truetype'); -} - // CSS Reset @import "~/Content/bootstrap/reset.less"; @@ -66,6 +55,7 @@ @import "~/Content/bootstrap/progress-bars.less"; // (MC) extra 3rd party or own components +@import "~/Content/bootstrap/custom/spinner.less"; @import "~/Content/bootstrap/custom/throbber.less"; // (MC) extra tweaks and corrections @@ -77,63 +67,132 @@ /* INSTALL STYLES --------------------------------------------------------*/ -@installBaseColor: #29b6d9; body { - background: @installBaseColor url('images/bg.png') 50% 0% no-repeat; - background-attachment: fixed; + background: #f5f5f5; +} + +input:not([type=checkbox]):not([type=radio]), +select { + display: block; + box-sizing: border-box; + width: 100%; + height: 34px; + box-shadow: none; + border-color: #d2d2d2; +} + +textarea { + height: initial; + min-height: 34px; + box-shadow: none; + border-color: #d2d2d2; +} + +.bg-top { + position: fixed; + z-index: 0; + background: #3F51B5; + width: 100%; + height: 140px; +} + +.dropdown-menu li > a { + padding: 8px 20px 8px 12px; + > .fa { margin-right: 4px; } +} + +.help-block { + color: #a1a1a1; + margin-top: 8px; + font-size: 12px; } .install-head { - padding: 8px 0; - line-height: 40px; + position: relative; + z-index: 1; + margin: 20px 0; vertical-align: middle; } .install-panel { - margin-top: 4px; + position: relative; + z-index: 1; + margin-top: 20px; margin-bottom: 20px; padding: 30px; + padding-bottom: 0; background-color: #fff; - background-color: rgba(255,255,255, .5); - .box-shadow(~'0 0 15px rgba(0,0,0, .15), 0 10px 6px -10px rgba(0,0,0, .4)'); - .border-radius(4px); + .box-shadow(~'0 1px 1px 0 rgba(0,0,0,.06),0 2px 5px 0 rgba(0,0,0,.2)'); + .border-radius(3px); +} + +.buttonbar { + text-align: left; + background: #fafafa; + padding: 20px 30px; + margin-left: -30px; + margin-right: -30px; + border-radius: 0 0 3px 3px; +} + +.btn-install { + font-size: 22px; + line-height: 32px; + font-weight: 600; + padding-left: 1.5em; + padding-right: 1.5em; } .install-title { margin: 0; - color: #fff; - color: lighten(@installBaseColor, 40%); - text-shadow: 0 1px 0 darken(@installBaseColor, 12%); font-family: @pageTitleFontFamily; font-weight: @pageTitleFontWeight; + font-size: 22px; + color: #fff; + padding-left: 8px; + vertical-align: bottom; +} + +.install-intro { + font-size: 16px; + line-height: 22px; + color: #818181; + margin-bottom: 30px; } -.install-content { - //padding: 0 30px; +.install-content fieldset { + margin-bottom: 30px; } .install-content legend { - font-family: @headingsFontFamily; - font-weight: @headingsFontWeight; + font-weight: 600; font-size: 22px; - border-bottom-color: lighten(@installBaseColor, 12%); border-bottom-color: rgba(0,0,0, 0.1); color: @headingsColor; margin-bottom: 0; padding-bottom: 8px; + + > .fa { + color: #717171; + margin-right: 4px; + } } .install-icon { - font-size: 32px; - line-height: 32px; - vertical-align: bottom; - margin-right: 4px; - display: none; + margin-right: 10px; +} + +.install-icon .fa-circle { + color: #f90; +} +.install-icon .fa-inverse { + font-size: 0.7em; } .form-horizontal .control-label { width: 240px; + text-align: left; } .form-horizontal .controls { @@ -145,8 +204,10 @@ label.control-label { } .field-validation-error { - color: red; + display: inline-block; + color: @red; text-shadow: none !important; + padding-top: 4px; } label.control-label { @@ -154,3 +215,54 @@ label.control-label { } +#navbar-tools { + + .active-tool() { + .box-shadow(~'inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05)'); + } + + position: absolute; + z-index: 100; + right: 0; + top: 15px; + margin: 0; + + .btn, + .btn-group { + margin-top: 0; + } + + .open .navbar-tool { + .active-tool(); + } + + .navbar-tool { + color: rgba(255,255,255, .87); + border: none; + background: transparent; + .box-shadow(none); + padding: 8px 6px; + .transition(color 0.1s linear); + + &:hover { + color: #fff; + } + + &:active { + .active-tool(); + } + + > .fa { + font-size: 20px; + } + + span { + display: inline-block; + margin-left: 3px; + vertical-align: top; + } + + } +} + + diff --git a/src/Presentation/SmartStore.Web/Content/install/variables.custom.less b/src/Presentation/SmartStore.Web/Content/install/variables.custom.less index 76db5fcef8..2bcad0ceeb 100644 --- a/src/Presentation/SmartStore.Web/Content/install/variables.custom.less +++ b/src/Presentation/SmartStore.Web/Content/install/variables.custom.less @@ -5,6 +5,6 @@ // Nicer page titles // ------------------------- -@pageTitleFontFamily: 'Segoe UI Light', 'Segoe UI', Arial, Helvetica, sans-serif; -@pageTitleFontWeight: 100; +@pageTitleFontFamily: 'Segoe UI', Arial, Helvetica, sans-serif; +@pageTitleFontWeight: 400; @pageTitleColor: @infoText; // @linkColor; // #009fff; diff --git a/src/Presentation/SmartStore.Web/Content/install/variables.less b/src/Presentation/SmartStore.Web/Content/install/variables.less index 4d85eb8b75..d4daaab546 100644 --- a/src/Presentation/SmartStore.Web/Content/install/variables.less +++ b/src/Presentation/SmartStore.Web/Content/install/variables.less @@ -71,9 +71,9 @@ @paddingSmall: 2px 10px; // 26px @paddingMini: 1px 6px; // 24px -@baseBorderRadius: 0; +@baseBorderRadius: 3px; @borderRadiusLarge: 4px; -@borderRadiusSmall: 0; +@borderRadiusSmall: 2px; // Tables @@ -128,10 +128,10 @@ @dropdownLinkColor: @grayDark; -@dropdownLinkColorHover: @white; -@dropdownLinkColorActive: @dropdownLinkColor; -@dropdownLinkBackgroundActive: #666; //@linkColor; -@dropdownLinkBackgroundHover: @dropdownLinkBackgroundActive; +@dropdownLinkColorHover: inherit; +@dropdownLinkColorActive: inherit; +@dropdownLinkBackgroundActive: #f5f5f5; +@dropdownLinkBackgroundHover: #f5f5f5; diff --git a/src/Presentation/SmartStore.Web/Content/smartstore.entitypicker.css b/src/Presentation/SmartStore.Web/Content/smartstore.entitypicker.css new file mode 100644 index 0000000000..2c821195fb --- /dev/null +++ b/src/Presentation/SmartStore.Web/Content/smartstore.entitypicker.css @@ -0,0 +1,124 @@ +/* -------------------------------------------------------------- + SmartStore Component: smartstore.entitypicker.css +-------------------------------------------------------------- */ + +.entity-picker-list { + position: relative; + margin-left: -5px; + margin-right: -5px; +} + +.entity-picker-list .item-wrap { + position: relative; + box-sizing: border-box; + display: block; + float: left; + width: 33.3332%; + padding: 0 5px; +} + +.entity-picker-list .item { + position: relative; + box-sizing: border-box; + display: block; + height: 70px; + padding: 8px; + margin: 5px 0; + border-radius: 3px; +} +.entity-picker-list .item:not(.disable):hover { + cursor: pointer; +} +.entity-picker-list .item:not(.selected):hover { + background-color: #f5f5f5; +} + +.entity-picker-list .disable { + opacity: 0.4; +} + +.entity-picker-list .title { + font-weight: 400; + max-height: 36px; + overflow: hidden; + text-overflow: ellipsis; +} + +.entity-picker-list .highlight { + font-weight: 700; +} + +.entity-picker-list .summary { + color: #aaa; + font-size: 12px; + height: 18px; + line-height: 18px; + vertical-align: middle; + overflow: hidden; +} +.entity-picker-list .published { + margin-left: 1px; + margin-right: 6px; + font-size: 15px; + line-height: 18px; + vertical-align: sub; +} +.entity-picker-list .published.fa-globe { + color: inherit; +} +.entity-picker-list .published.fa-eye-slash { + color: #aaa; +} +.entity-picker-list .item.selected .published { + color: #fff; +} + +.entity-picker-list .list-footer { + clear: both; + padding: 12px; + text-align: center; +} + +.entity-picker-list .thumb, +.entity-picker-list .data { + position: relative; + box-sizing: border-box; + display: block; +} +.entity-picker-list .thumb { + display: table-cell; + width: 54px; + height: 54px; + max-width: 54px; + max-height: 54px; + padding: 2px; + text-align: center; + vertical-align: middle; +} +.entity-picker-list .item:hover .thumb, +.entity-picker-list .item.selected .thumb { + background: #fff; + box-shadow: 1px 1px 2px rgba(0,0,0, 0.1); +} +.entity-picker-list .thumb img { + max-width: 100%; + max-height: 100%; +} +.entity-picker-list .thumb + .data { + position: absolute; + left: 72px; + right: 8px; + top: 8px; + bottom: 8px; +} + +.entity-picker-list .item.selected { + background-color: #4060c0; +} +.entity-picker-list .item.selected .title { + color: #fff; +} +.entity-picker-list .item.selected .summary { + color: rgba(255,255,255, 0.6); +} + diff --git a/src/Presentation/SmartStore.Web/Content/smartstore.smartgallery.css b/src/Presentation/SmartStore.Web/Content/smartstore.smartgallery.css index 2e196f8168..3ab48b4a6b 100644 --- a/src/Presentation/SmartStore.Web/Content/smartstore.smartgallery.css +++ b/src/Presentation/SmartStore.Web/Content/smartstore.smartgallery.css @@ -9,6 +9,7 @@ } .sg-image-wrapper { + position: relative !important; width: 100%; min-height: 300px; /* just to avoid page load flickering. This is overwritten per inline css anyway */ margin-bottom: 5px; @@ -16,8 +17,6 @@ overflow: hidden; outline: 1px solid transparent; text-align: center; - /*background-color: #fff; - border: 1px solid #d5d5d5;*/ } .sg-image-wrapper > img { @@ -27,9 +26,10 @@ .sg-loader { position: absolute; z-index: 10; - top: 48%; - left: 48%; - border: 1px solid #ccc; + left: 0; + top: 0; + right: 0; + bottom: 0; } .sg-image { diff --git a/src/Presentation/SmartStore.Web/Controllers/BlogController.cs b/src/Presentation/SmartStore.Web/Controllers/BlogController.cs index ad683bda9f..bae872f9e9 100644 --- a/src/Presentation/SmartStore.Web/Controllers/BlogController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/BlogController.cs @@ -6,28 +6,28 @@ using System.Web.Routing; using SmartStore.Core; using SmartStore.Core.Caching; -using SmartStore.Core.Domain; using SmartStore.Core.Domain.Blogs; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Media; +using SmartStore.Core.Logging; using SmartStore.Services.Blogs; using SmartStore.Services.Common; using SmartStore.Services.Customers; using SmartStore.Services.Helpers; using SmartStore.Services.Localization; -using SmartStore.Core.Logging; using SmartStore.Services.Media; using SmartStore.Services.Messages; using SmartStore.Services.Seo; using SmartStore.Services.Stores; -using SmartStore.Web.Framework; +using SmartStore.Utilities; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Filters; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.UI.Captcha; using SmartStore.Web.Infrastructure.Cache; using SmartStore.Web.Models.Blogs; -using SmartStore.Web.Framework.Mvc; namespace SmartStore.Web.Controllers { @@ -48,6 +48,7 @@ public partial class BlogController : PublicControllerBase private readonly ICacheManager _cacheManager; private readonly ICustomerActivityService _customerActivityService; private readonly IStoreMappingService _storeMappingService; + private readonly ILanguageService _languageService; private readonly MediaSettings _mediaSettings; private readonly BlogSettings _blogSettings; @@ -71,6 +72,7 @@ public BlogController(IBlogService blogService, ICacheManager cacheManager, ICustomerActivityService customerActivityService, IStoreMappingService storeMappingService, + ILanguageService languageService, MediaSettings mediaSettings, BlogSettings blogSettings, LocalizationSettings localizationSettings, @@ -89,6 +91,7 @@ public BlogController(IBlogService blogService, this._cacheManager = cacheManager; this._customerActivityService = customerActivityService; this._storeMappingService = storeMappingService; + this._languageService = languageService; this._mediaSettings = mediaSettings; this._blogSettings = blogSettings; @@ -232,7 +235,38 @@ public ActionResult BlogByMonth(BlogPagingFilteringModel command) [Compress] public ActionResult ListRss(int languageId) { - var feed = _blogService.CreateRssFeed(Url, languageId); + DateTime? maxAge = null; + var protocol = _webHelper.IsCurrentConnectionSecured() ? "https" : "http"; + var selfLink = Url.RouteUrl("BlogRSS", new { languageId = languageId }, protocol); + var blogLink = Url.RouteUrl("Blog", null, protocol); + + var title = "{0} - Blog".FormatInvariant(_storeContext.CurrentStore.Name); + + if (_blogSettings.MaxAgeInDays > 0) + maxAge = DateTime.UtcNow.Subtract(new TimeSpan(_blogSettings.MaxAgeInDays, 0, 0, 0)); + + var language = _languageService.GetLanguageById(languageId); + var feed = new SmartSyndicationFeed(new Uri(blogLink), title); + + feed.AddNamespaces(false); + feed.Init(selfLink, language); + + if (!_blogSettings.Enabled) + return new RssActionResult { Feed = feed }; + + var items = new List(); + var blogPosts = _blogService.GetAllBlogPosts(_storeContext.CurrentStore.Id, languageId, null, null, 0, int.MaxValue, false, maxAge); + + foreach (var blogPost in blogPosts) + { + var blogPostUrl = Url.RouteUrl("BlogPost", new { SeName = blogPost.GetSeName(blogPost.LanguageId, ensureTwoPublishedLanguages: false) }, "http"); + + var item = feed.CreateItem(blogPost.Title, blogPost.Body, blogPostUrl, blogPost.CreatedOnUtc); + + items.Add(item); + } + + feed.Items = items; return new RssActionResult { Feed = feed }; } @@ -305,12 +339,9 @@ public ActionResult BlogCommentAdd(int blogPostId, BlogPostModel model, bool cap //activity log _customerActivityService.InsertActivity("PublicStore.AddBlogComment", _localizationService.GetResource("ActivityLog.PublicStore.AddBlogComment")); - //The text boxes should be cleared after a comment has been posted - //That' why we reload the page - TempData["sm.blog.addcomment.result"] = _localizationService.GetResource("Blog.Comments.SuccessfullyAdded"); + NotifySuccess(T("Blog.Comments.SuccessfullyAdded")); - // codehint: sm-add (MC) > append url fragment to route url - string url = UrlHelper.GenerateUrl( + var url = UrlHelper.GenerateUrl( routeName: "BlogPost", actionName: null, controllerName: null, @@ -322,6 +353,7 @@ public ActionResult BlogCommentAdd(int blogPostId, BlogPostModel model, bool cap requestContext: this.ControllerContext.RequestContext, includeImplicitMvcValues: true /*helps fill in the nulls above*/ ); + return Redirect(url); } diff --git a/src/Presentation/SmartStore.Web/Controllers/BoardsController.cs b/src/Presentation/SmartStore.Web/Controllers/BoardsController.cs index 441d1a6b9d..ed096936d8 100644 --- a/src/Presentation/SmartStore.Web/Controllers/BoardsController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/BoardsController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.ServiceModel.Syndication; using System.Web.Mvc; using SmartStore.Core; using SmartStore.Core.Domain.Customers; @@ -15,9 +16,11 @@ using SmartStore.Services.Localization; using SmartStore.Services.Media; using SmartStore.Services.Seo; +using SmartStore.Utilities; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Controllers; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Filters; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Framework.Security; using SmartStore.Web.Models.Boards; @@ -34,6 +37,7 @@ public partial class BoardsController : PublicControllerBase private readonly ICountryService _countryService; private readonly IWebHelper _webHelper; private readonly IWorkContext _workContext; + private readonly IStoreContext _storeContext; private readonly ForumSettings _forumSettings; private readonly CustomerSettings _customerSettings; private readonly MediaSettings _mediaSettings; @@ -49,6 +53,7 @@ public BoardsController(IForumService forumService, ICountryService countryService, IWebHelper webHelper, IWorkContext workContext, + IStoreContext storeContext, ForumSettings forumSettings, CustomerSettings customerSettings, MediaSettings mediaSettings, @@ -60,6 +65,7 @@ public BoardsController(IForumService forumService, this._countryService = countryService; this._webHelper = webHelper; this._workContext = workContext; + this._storeContext = storeContext; this._forumSettings = forumSettings; this._customerSettings = customerSettings; this._mediaSettings = mediaSettings; @@ -266,7 +272,38 @@ public ActionResult ActiveDiscussionsRss(int forumId = 0) if (!_forumSettings.ForumsEnabled) return HttpNotFound(); - var feed = _forumService.CreateActiveDiscussionsRssFeed(Url, forumId); + var language = _workContext.WorkingLanguage; + var protocol = _webHelper.IsCurrentConnectionSecured() ? "https" : "http"; + var selfLink = Url.Action("ActiveDiscussionsRSS", "Boards", null, protocol); + var discussionLink = Url.Action("ActiveDiscussions", "Boards", null, protocol); + + var title = "{0} - {1}".FormatInvariant(_storeContext.CurrentStore.Name, T("Forum.ActiveDiscussionsFeedTitle")); + + var feed = new SmartSyndicationFeed(new Uri(discussionLink), title, T("Forum.ActiveDiscussionsFeedDescription")); + + feed.AddNamespaces(false); + feed.Init(selfLink, language); + + if (!_forumSettings.ActiveDiscussionsFeedEnabled) + return new RssActionResult { Feed = feed }; + + var items = new List(); + var topics = _forumService.GetActiveTopics(forumId, _forumSettings.ActiveDiscussionsFeedCount); + + var viewsText = T("Forum.Views"); + var repliesText = T("Forum.Replies"); + + foreach (var topic in topics) + { + string topicUrl = Url.RouteUrl("TopicSlug", new { id = topic.Id, slug = topic.GetSeName() }, "http"); + var synopsis = "{0}: {1}, {2}: {3}".FormatInvariant(repliesText, topic.NumReplies, viewsText, topic.Views); + + var item = feed.CreateItem(topic.Subject, synopsis, topicUrl, topic.LastPostTime ?? topic.UpdatedOnUtc); + + items.Add(item); + } + + feed.Items = items; return new RssActionResult { Feed = feed }; } @@ -349,7 +386,43 @@ public ActionResult ForumRss(int id) if (!_forumSettings.ForumsEnabled) return HttpNotFound(); - var feed = _forumService.CreateForumRssFeed(Url, id); + var language = _workContext.WorkingLanguage; + var protocol = _webHelper.IsCurrentConnectionSecured() ? "https" : "http"; + var selfLink = Url.Action("ForumRSS", "Boards", null, protocol); + var forumLink = Url.Action("Forum", "Boards", new { id = id }, protocol); + + var feed = new SmartSyndicationFeed(new Uri(forumLink), _storeContext.CurrentStore.Name, T("Forum.ForumFeedDescription")); + + feed.AddNamespaces(false); + feed.Init(selfLink, language); + + if (!_forumSettings.ForumFeedsEnabled) + return new RssActionResult { Feed = feed }; + + var forum = _forumService.GetForumById(id); + + if (forum == null) + return new RssActionResult { Feed = feed }; + + feed.Title = new TextSyndicationContent("{0} - {1}".FormatInvariant(_storeContext.CurrentStore.Name, forum.GetLocalized(x => x.Name, language.Id))); + + var items = new List(); + var topics = _forumService.GetAllTopics(id, 0, string.Empty, ForumSearchType.All, 0, 0, _forumSettings.ForumFeedCount); + + var viewsText = T("Forum.Views"); + var repliesText = T("Forum.Replies"); + + foreach (var topic in topics) + { + string topicUrl = Url.RouteUrl("TopicSlug", new { id = topic.Id, slug = topic.GetSeName() }, "http"); + var synopsis = "{0}: {1}, {2}: {3}".FormatInvariant(repliesText, topic.NumReplies, viewsText, topic.Views); + + var item = feed.CreateItem(topic.Subject, synopsis, topicUrl, topic.LastPostTime ?? topic.UpdatedOnUtc); + + items.Add(item); + } + + feed.Items = items; return new RssActionResult { Feed = feed }; } diff --git a/src/Presentation/SmartStore.Web/Controllers/CatalogController.cs b/src/Presentation/SmartStore.Web/Controllers/CatalogController.cs index 7c18223245..9db9fa78bb 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CatalogController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CatalogController.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.ServiceModel.Syndication; using System.Web.Mvc; @@ -9,7 +8,6 @@ using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Media; -using SmartStore.Core.Localization; using SmartStore.Services; using SmartStore.Services.Catalog; using SmartStore.Services.Common; @@ -21,8 +19,10 @@ using SmartStore.Services.Security; using SmartStore.Services.Seo; using SmartStore.Services.Stores; +using SmartStore.Utilities; using SmartStore.Web.Framework.Controllers; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Filters; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.UI; using SmartStore.Web.Infrastructure.Cache; @@ -31,7 +31,7 @@ namespace SmartStore.Web.Controllers { - public partial class CatalogController : PublicControllerBase + public partial class CatalogController : PublicControllerBase { #region Fields @@ -133,10 +133,13 @@ public ActionResult Category(int categoryId, CatalogPagingFilteringModel command return HttpNotFound(); //'Continue shopping' URL - _genericAttributeService.SaveAttribute(_services.WorkContext.CurrentCustomer, - SystemCustomerAttributeNames.LastContinueShoppingPage, - _services.WebHelper.GetThisPageUrl(false), - _services.StoreContext.CurrentStore.Id); + if (!_services.WorkContext.CurrentCustomer.IsSystemAccount) + { + _genericAttributeService.SaveAttribute(_services.WorkContext.CurrentCustomer, + SystemCustomerAttributeNames.LastContinueShoppingPage, + _services.WebHelper.GetThisPageUrl(false), + _services.StoreContext.CurrentStore.Id); + } if (command.PageNumber <= 0) command.PageNumber = 1; @@ -210,7 +213,7 @@ public ActionResult Category(int categoryId, CatalogPagingFilteringModel command { PictureId = x.PictureId.GetValueOrDefault(), FullSizeImageUrl = _pictureService.GetPictureUrl(picture), - ImageUrl = _pictureService.GetPictureUrl(picture, targetSize: pictureSize), + ImageUrl = _pictureService.GetPictureUrl(picture, pictureSize, !_catalogSettings.HideCategoryDefaultPictures), Title = string.Format(T("Media.Category.ImageLinkTitleFormat"), subCatName), AlternateText = string.Format(T("Media.Category.ImageAlternateTextFormat"), subCatName) }; @@ -282,6 +285,7 @@ public ActionResult Category(int categoryId, CatalogPagingFilteringModel command products, prepareColorAttributes: true, prepareManufacturers: command.ViewMode.IsCaseInsensitiveEqual("list")).ToList(); + model.PagingFilteringContext.LoadPagedList(products); } else @@ -362,7 +366,7 @@ public ActionResult ProductBreadcrumb(int productId) { var product = _productService.GetProductById(productId); if (product == null) - throw new ArgumentException("No product found with the specified id"); + throw new ArgumentException(T("Products.NotFound", productId)); if (!_catalogSettings.CategoryBreadcrumbEnabled) return Content(""); @@ -402,7 +406,7 @@ public ActionResult HomepageCategories() { PictureId = x.PictureId.GetValueOrDefault(), FullSizeImageUrl = _pictureService.GetPictureUrl(x.PictureId.GetValueOrDefault()), - ImageUrl = _pictureService.GetPictureUrl(x.PictureId.GetValueOrDefault(), pictureSize), + ImageUrl = _pictureService.GetPictureUrl(x.PictureId.GetValueOrDefault(), pictureSize, !_catalogSettings.HideCategoryDefaultPictures), Title = string.Format(T("Media.Category.ImageLinkTitleFormat"), catModel.Name), AlternateText = string.Format(T("Media.Category.ImageAlternateTextFormat"), catModel.Name) }; @@ -439,11 +443,14 @@ public ActionResult Manufacturer(int manufacturerId, CatalogPagingFilteringModel if (!_storeMappingService.Authorize(manufacturer)) return HttpNotFound(); - //'Continue shopping' URL - _genericAttributeService.SaveAttribute(_services.WorkContext.CurrentCustomer, - SystemCustomerAttributeNames.LastContinueShoppingPage, - _services.WebHelper.GetThisPageUrl(false), - _services.StoreContext.CurrentStore.Id); + //'Continue shopping' URL + if (!_services.WorkContext.CurrentCustomer.IsSystemAccount) + { + _genericAttributeService.SaveAttribute(_services.WorkContext.CurrentCustomer, + SystemCustomerAttributeNames.LastContinueShoppingPage, + _services.WebHelper.GetThisPageUrl(false), + _services.StoreContext.CurrentStore.Id); + } if (command.PageNumber <= 0) command.PageNumber = 1; @@ -560,7 +567,7 @@ public ActionResult Manufacturer(int manufacturerId, CatalogPagingFilteringModel public ActionResult ManufacturerAll() { var model = new List(); - var manufacturers = _manufacturerService.GetAllManufacturers(); + var manufacturers = _manufacturerService.GetAllManufacturers(null, _services.StoreContext.CurrentStore.Id); foreach (var manufacturer in manufacturers) { var modelMan = manufacturer.ToModel(); @@ -579,13 +586,19 @@ public ActionResult ManufacturerNavigation(int currentManufacturerId) if (_catalogSettings.ManufacturersBlockItemsToDisplay == 0 || _catalogSettings.ShowManufacturersOnHomepage == false) return Content(""); - string cacheKey = string.Format(ModelCacheEventConsumer.MANUFACTURER_NAVIGATION_MODEL_KEY, currentManufacturerId, _services.WorkContext.WorkingLanguage.Id, _services.StoreContext.CurrentStore.Id); + var cacheKey = string.Format(ModelCacheEventConsumer.MANUFACTURER_NAVIGATION_MODEL_KEY, + currentManufacturerId, + !_catalogSettings.HideManufacturerDefaultPictures, + _services.WorkContext.WorkingLanguage.Id, + _services.StoreContext.CurrentStore.Id); + var cacheModel = _services.Cache.Get(cacheKey, () => { var currentManufacturer = _manufacturerService.GetManufacturerById(currentManufacturerId); - var manufacturers = _manufacturerService.GetAllManufacturers(); - var model = new ManufacturerNavigationModel() + var manufacturers = _manufacturerService.GetAllManufacturers(null, _services.StoreContext.CurrentStore.Id); + + var model = new ManufacturerNavigationModel { TotalManufacturers = manufacturers.Count, DisplayManufacturers = _catalogSettings.ShowManufacturersOnHomepage, @@ -599,7 +612,8 @@ public ActionResult ManufacturerNavigation(int currentManufacturerId) Id = manufacturer.Id, Name = manufacturer.GetLocalized(x => x.Name), SeName = manufacturer.GetSeName(), - PictureUrl = _pictureService.GetPictureUrl(manufacturer.PictureId.GetValueOrDefault(), _mediaSettings.ManufacturerThumbPictureSize), + PictureUrl = _pictureService.GetPictureUrl(manufacturer.PictureId.GetValueOrDefault(), _mediaSettings.ManufacturerThumbPictureSize, + !_catalogSettings.HideManufacturerDefaultPictures), IsActive = currentManufacturer != null && currentManufacturer.Id == manufacturer.Id, }; model.Manufacturers.Add(modelMan); @@ -847,7 +861,62 @@ public ActionResult RecentlyAddedProducts(CatalogPagingFilteringModel command) [Compress] public ActionResult RecentlyAddedProductsRss() { - var feed = _productService.CreateRecentlyAddedProductsRssFeed(Url); + var protocol = _services.WebHelper.IsCurrentConnectionSecured() ? "https" : "http"; + var selfLink = Url.RouteUrl("RecentlyAddedProductsRSS", null, protocol); + var recentProductsLink = Url.RouteUrl("RecentlyAddedProducts", null, protocol); + + var title = "{0} - {1}".FormatInvariant(_services.StoreContext.CurrentStore.Name, T("RSS.RecentlyAddedProducts")); + + var feed = new SmartSyndicationFeed(new Uri(recentProductsLink), title, T("RSS.InformationAboutProducts")); + + feed.AddNamespaces(true); + feed.Init(selfLink, _services.WorkContext.WorkingLanguage); + + if (!_catalogSettings.RecentlyAddedProductsEnabled) + return new RssActionResult { Feed = feed }; + + var items = new List(); + var searchContext = new ProductSearchContext + { + LanguageId = _services.WorkContext.WorkingLanguage.Id, + OrderBy = ProductSortingEnum.CreatedOn, + PageSize = _catalogSettings.RecentlyAddedProductsNumber, + StoreId = _services.StoreContext.CurrentStoreIdIfMultiStoreMode, + VisibleIndividuallyOnly = true + }; + + var products = _productService.SearchProducts(searchContext); + var storeUrl = _services.StoreContext.CurrentStore.Url; + + foreach (var product in products) + { + string productUrl = Url.RouteUrl("Product", new { SeName = product.GetSeName() }, "http"); + if (productUrl.HasValue()) + { + var item = feed.CreateItem( + product.GetLocalized(x => x.Name), + product.GetLocalized(x => x.ShortDescription), + productUrl, + product.CreatedOnUtc, + product.FullDescription); + + try + { + // we add only the first picture + var picture = product.ProductPictures.OrderBy(x => x.DisplayOrder).Select(x => x.Picture).FirstOrDefault(); + + if (picture != null) + { + feed.AddEnclosue(item, picture, _pictureService.GetPictureUrl(picture, _mediaSettings.ProductDetailsPictureSize, false, storeUrl)); + } + } + catch { } + + items.Add(item); + } + } + + feed.Items = items; return new RssActionResult { Feed = feed }; } @@ -954,7 +1023,7 @@ public ActionResult CompareProducts() var products = _compareProductsService.GetComparedProducts(); - _helper.PrepareProductOverviewModels(products, prepareSpecificationAttributes: true, prepareFullDescription: true) + _helper.PrepareProductOverviewModels(products, prepareSpecificationAttributes: true, prepareFullDescription: true, isCompareList: true) .ToList() .ForEach(model.Products.Add); @@ -1004,7 +1073,7 @@ public ActionResult FlyoutCompare() var products = _compareProductsService.GetComparedProducts(); - _helper.PrepareProductOverviewModels(products, prepareSpecificationAttributes: true) + _helper.PrepareProductOverviewModels(products, prepareSpecificationAttributes: true, isCompareList: true) .ToList() .ForEach(model.Products.Add); @@ -1023,10 +1092,13 @@ public ActionResult Search(SearchModel model, SearchPagingFilteringModel command model = new SearchModel(); // 'Continue shopping' URL - _genericAttributeService.SaveAttribute(_services.WorkContext.CurrentCustomer, - SystemCustomerAttributeNames.LastContinueShoppingPage, - _services.WebHelper.GetThisPageUrl(false), - _services.StoreContext.CurrentStore.Id); + if (!_services.WorkContext.CurrentCustomer.IsSystemAccount) + { + _genericAttributeService.SaveAttribute(_services.WorkContext.CurrentCustomer, + SystemCustomerAttributeNames.LastContinueShoppingPage, + _services.WebHelper.GetThisPageUrl(false), + _services.StoreContext.CurrentStore.Id); + } if (command.PageSize <= 0) command.PageSize = _catalogSettings.SearchPageProductsPerPage; @@ -1034,9 +1106,7 @@ public ActionResult Search(SearchModel model, SearchPagingFilteringModel command command.PageNumber = 1; if (command.OrderBy == (int)ProductSortingEnum.Initial) - { command.OrderBy = (int)_catalogSettings.DefaultSortOrder; - } _helper.PreparePagingFilteringModel(model.PagingFilteringContext, command, new PageSizeContext { @@ -1045,17 +1115,10 @@ public ActionResult Search(SearchModel model, SearchPagingFilteringModel command PageSizeOptions = _catalogSettings.ProductSearchPageSizeOptions }); - if (model.Q == null) - model.Q = ""; - model.Q = model.Q.Trim(); + model.Q = model.Q.EmptyNull().Trim(); // Build AvailableCategories - // first empty entry - model.AvailableCategories.Add(new SelectListItem - { - Value = "0", - Text = T("Common.All") - }); + model.AvailableCategories.Add(new SelectListItem { Value = "0", Text = T("Common.All") }); var navModel = _helper.PrepareCategoryNavigationModel(0, 0); @@ -1064,7 +1127,7 @@ public ActionResult Search(SearchModel model, SearchPagingFilteringModel command if (node.IsRoot) return; - int id = node.Value.EntityId; + var id = node.Value.EntityId; var breadcrumb = node.GetBreadcrumb().Select(x => x.Text).ToArray(); model.AvailableCategories.Add(new SelectListItem @@ -1078,21 +1141,21 @@ public ActionResult Search(SearchModel model, SearchPagingFilteringModel command var manufacturers = _manufacturerService.GetAllManufacturers(); if (manufacturers.Count > 0) { - model.AvailableManufacturers.Add(new SelectListItem - { - Value = "0", - Text = T("Common.All") - }); + model.AvailableManufacturers.Add(new SelectListItem { Value = "0", Text = T("Common.All") }); + foreach (var m in manufacturers) + { model.AvailableManufacturers.Add(new SelectListItem { Value = m.Id.ToString(), Text = m.GetLocalized(x => x.Name), Selected = model.Mid == m.Id }); + } } IPagedList products = new PagedList(new List(), 0, 1); + // only search if query string search keyword is set (used to avoid searching or displaying search term min length error message on /search page load) if (Request.Params["Q"] != null) { @@ -1103,10 +1166,11 @@ public ActionResult Search(SearchModel model, SearchPagingFilteringModel command else { var categoryIds = new List(); - int manufacturerId = 0; + var manufacturerId = 0; decimal? minPriceConverted = null; decimal? maxPriceConverted = null; - bool searchInDescriptions = false; + var searchInDescriptions = _catalogSettings.SearchDescriptions; + if (model.As) { // advanced search @@ -1124,16 +1188,16 @@ public ActionResult Search(SearchModel model, SearchPagingFilteringModel command manufacturerId = model.Mid; // min price - if (!string.IsNullOrEmpty(model.Pf)) + if (model.Pf.HasValue()) { - decimal minPrice = decimal.Zero; + var minPrice = decimal.Zero; if (decimal.TryParse(model.Pf, out minPrice)) minPriceConverted = _currencyService.ConvertToPrimaryStoreCurrency(minPrice, _services.WorkContext.WorkingCurrency); } // max price - if (!string.IsNullOrEmpty(model.Pt)) + if (model.Pt.HasValue()) { - decimal maxPrice = decimal.Zero; + var maxPrice = decimal.Zero; if (decimal.TryParse(model.Pt, out maxPrice)) maxPriceConverted = _currencyService.ConvertToPrimaryStoreCurrency(maxPrice, _services.WorkContext.WorkingCurrency); } @@ -1144,8 +1208,6 @@ public ActionResult Search(SearchModel model, SearchPagingFilteringModel command //var searchInProductTags = false; var searchInProductTags = searchInDescriptions; - //products - var ctx = new ProductSearchContext(); ctx.CategoryIds = categoryIds; ctx.ManufacturerId = manufacturerId; @@ -1165,13 +1227,17 @@ public ActionResult Search(SearchModel model, SearchPagingFilteringModel command products = _productService.SearchProducts(ctx); model.Products = _helper.PrepareProductOverviewModels( - products, - prepareColorAttributes: true, + products, + prepareColorAttributes: true, prepareManufacturers: command.ViewMode.IsCaseInsensitiveEqual("list")).ToList(); model.NoResults = !model.Products.Any(); } } + else + { + model.Sid = _catalogSettings.SearchDescriptions; + } model.PagingFilteringContext.LoadPagedList(products); return View(model); @@ -1194,35 +1260,32 @@ public ActionResult SearchTermAutoComplete(string term) if (String.IsNullOrWhiteSpace(term) || term.Length < _catalogSettings.ProductSearchTermMinimumLength) return Content(""); - // products - var pageSize = _catalogSettings.ProductSearchAutoCompleteNumberOfProducts > 0 ? _catalogSettings.ProductSearchAutoCompleteNumberOfProducts : 10; - var ctx = new ProductSearchContext(); ctx.LanguageId = _services.WorkContext.WorkingLanguage.Id; ctx.Keywords = term; ctx.SearchSku = !_catalogSettings.SuppressSkuSearch; + ctx.SearchDescriptions = _catalogSettings.SearchDescriptions; ctx.OrderBy = ProductSortingEnum.Position; - ctx.PageSize = pageSize; + ctx.PageSize = (_catalogSettings.ProductSearchAutoCompleteNumberOfProducts > 0 ? _catalogSettings.ProductSearchAutoCompleteNumberOfProducts : 10); ctx.StoreId = _services.StoreContext.CurrentStoreIdIfMultiStoreMode; ctx.VisibleIndividuallyOnly = true; var products = _productService.SearchProducts(ctx); - var models = _helper.PrepareProductOverviewModels( - products, + var models = _helper.PrepareProductOverviewModels(products, false, _catalogSettings.ShowProductImagesInSearchAutoComplete, - _mediaSettings.ProductThumbPictureSizeOnProductDetailsPage).ToList(); - - var result = (from p in models - select new - { - label = p.Name, - secondary = p.ShortDescription.Truncate(70, "...") ?? "", - producturl = Url.RouteUrl("Product", new { SeName = p.SeName }), - productpictureurl = p.DefaultPictureModel.ImageUrl - }) - .ToList(); + _mediaSettings.ProductThumbPictureSizeOnProductDetailsPage + ).ToList(); + + var result = models.Select(x => new + { + label = x.Name, + secondary = x.ShortDescription.Truncate(70, "...") ?? "", + producturl = Url.RouteUrl("Product", new { SeName = x.SeName }), + productpictureurl = x.DefaultPictureModel.ImageUrl + }).ToList(); + return Json(result, JsonRequestBehavior.AllowGet); } diff --git a/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.cs b/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.cs index beb33343b0..ebc506887c 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Diagnostics; using System.Linq; -using System.Linq.Expressions; using System.Web; using System.Web.Mvc; using SmartStore.Collections; @@ -25,13 +25,12 @@ using SmartStore.Services.Security; using SmartStore.Services.Seo; using SmartStore.Services.Tax; +using SmartStore.Services.Topics; using SmartStore.Web.Framework.UI; using SmartStore.Web.Framework.UI.Captcha; using SmartStore.Web.Infrastructure.Cache; using SmartStore.Web.Models.Catalog; using SmartStore.Web.Models.Media; -using SmartStore.Core.Data; -using System.Collections.Specialized; namespace SmartStore.Web.Controllers { @@ -68,6 +67,7 @@ public class CatalogHelper private readonly IDeliveryTimeService _deliveryTimeService; private readonly ISettingService _settingService; private readonly Lazy _menuPublisher; + private readonly Lazy _topicService; private readonly HttpRequestBase _httpRequest; private readonly UrlHelper _urlHelper; @@ -101,6 +101,7 @@ public CatalogHelper( IDeliveryTimeService deliveryTimeService, ISettingService settingService, Lazy _menuPublisher, + Lazy topicService, HttpRequestBase httpRequest, UrlHelper urlHelper) { @@ -133,6 +134,7 @@ public CatalogHelper( this._customerSettings = customerSettings; this._captchaSettings = captchaSettings; this._menuPublisher = _menuPublisher; + this._topicService = topicService; this._httpRequest = httpRequest; this._urlHelper = urlHelper; @@ -148,7 +150,8 @@ public ProductDetailsModel PrepareProductDetailsPageModel( bool isAssociatedProduct = false, ProductBundleItemData productBundleItem = null, IList productBundleItems = null, - NameValueCollection selectedAttributes = null) + NameValueCollection selectedAttributes = null, + NameValueCollection queryData = null) { if (product == null) throw new ArgumentNullException("product"); @@ -166,7 +169,7 @@ public ProductDetailsModel PrepareProductDetailsPageModel( ProductType = product.ProductType, VisibleIndividually = product.VisibleIndividually, //Manufacturers = _manufacturerService.GetProductManufacturersByProductId(product.Id), - Manufacturers = PrepareManufacturersOverviewModel(_manufacturerService.GetProductManufacturersByProductId(product.Id)), + Manufacturers = PrepareManufacturersOverviewModel(_manufacturerService.GetProductManufacturersByProductId(product.Id), null, true), ReviewCount = product.ApprovedTotalReviews, DisplayAdminLink = _services.Permissions.Authorize(StandardPermissionProvider.AccessAdminPanel), //EnableHtmlTextCollapser = Convert.ToBoolean(_settingService.GetSettingByKey("CatalogSettings.EnableHtmlTextCollapser")), @@ -183,6 +186,30 @@ public ProductDetailsModel PrepareProductDetailsPageModel( IsCurrentCustomerRegistered = _services.WorkContext.CurrentCustomer.IsRegistered() }; + // get gift card values from query string + if (queryData != null && queryData.Count > 0) + { + var giftCardItems = queryData.AllKeys + .Where(x => x.EmptyNull().StartsWith("giftcard_")) + .SelectMany(queryData.GetValues, (k, v) => new { key = k, value = v.TrimSafe() }); + + foreach (var item in giftCardItems) + { + var key = item.key.EmptyNull().ToLower(); + + if (key.EndsWith("recipientname")) + model.GiftCard.RecipientName = item.value; + else if (key.EndsWith("recipientemail")) + model.GiftCard.RecipientEmail = item.value; + else if (key.EndsWith("sendername")) + model.GiftCard.SenderName = item.value; + else if (key.EndsWith("senderemail")) + model.GiftCard.SenderEmail = item.value; + else if (key.EndsWith("message")) + model.GiftCard.Message = item.value; + } + } + // Back in stock subscriptions if (product.ManageInventoryMethod == ManageInventoryMethod.ManageStock && product.BackorderMode == BackorderMode.NoBackorders && @@ -212,6 +239,7 @@ public ProductDetailsModel PrepareProductDetailsPageModel( { var searchContext = new ProductSearchContext { + OrderBy = ProductSortingEnum.Position, StoreId = _services.StoreContext.CurrentStore.Id, ParentGroupedProductId = product.Id, PageSize = int.MaxValue, @@ -231,11 +259,15 @@ public ProductDetailsModel PrepareProductDetailsPageModel( { var item = itemData.Item; var bundleItemAttributes = new NameValueCollection(); - var keyPrefix = "product_attribute_{0}_{1}".FormatInvariant(item.ProductId, item.Id); - foreach (var key in selectedAttributes.AllKeys.Where(x => x.HasValue() && x.StartsWith(keyPrefix))) + if (selectedAttributes != null) { - bundleItemAttributes.Add(key, selectedAttributes[key]); + var keyPrefix = "product_attribute_{0}_{1}".FormatInvariant(item.ProductId, item.Id); + + foreach (var key in selectedAttributes.AllKeys.Where(x => x.HasValue() && x.StartsWith(keyPrefix))) + { + bundleItemAttributes.Add(key, selectedAttributes[key]); + } } var bundledProductModel = PrepareProductDetailsPageModel(item.Product, false, itemData, null, bundleItemAttributes); @@ -316,11 +348,11 @@ public void PrepareProductReviewsModel(ProductReviewsModel model, Product produc private PictureModel CreatePictureModel(ProductDetailsPictureModel model, Picture picture, int pictureSize) { - var result = new PictureModel() + var result = new PictureModel { PictureId = picture.Id, ThumbImageUrl = _pictureService.GetPictureUrl(picture, _mediaSettings.ProductThumbPictureSizeOnProductDetailsPage), - ImageUrl = _pictureService.GetPictureUrl(picture, pictureSize), + ImageUrl = _pictureService.GetPictureUrl(picture, pictureSize, !_catalogSettings.HideProductDefaultPictures), FullSizeImageUrl = _pictureService.GetPictureUrl(picture), Title = model.Name, AlternateText = model.AlternateText @@ -407,12 +439,16 @@ public void PrepareProductDetailsPictureModel( { model.DefaultPictureModel = new PictureModel { - ThumbImageUrl = _pictureService.GetDefaultPictureUrl(_mediaSettings.ProductThumbPictureSizeOnProductDetailsPage), - ImageUrl = _pictureService.GetDefaultPictureUrl(defaultPictureSize), - FullSizeImageUrl = _pictureService.GetDefaultPictureUrl(), Title = T("Media.Product.ImageLinkTitleFormat", model.Name), AlternateText = model.AlternateText }; + + if (!_catalogSettings.HideProductDefaultPictures) + { + model.DefaultPictureModel.ThumbImageUrl = _pictureService.GetDefaultPictureUrl(_mediaSettings.ProductThumbPictureSizeOnProductDetailsPage); + model.DefaultPictureModel.ImageUrl = _pictureService.GetDefaultPictureUrl(defaultPictureSize); + model.DefaultPictureModel.FullSizeImageUrl = _pictureService.GetDefaultPictureUrl(); + } } else { @@ -439,6 +475,10 @@ public ProductDetailsModel PrepareProductDetailModel( if (selectedAttributes == null) selectedAttributes = new NameValueCollection(); + var store = _services.StoreContext.CurrentStore; + var customer = _services.WorkContext.CurrentCustomer; + var currency = _services.WorkContext.WorkingCurrency; + decimal preSelectedPriceAdjustmentBase = decimal.Zero; decimal preSelectedWeightAdjustment = decimal.Zero; bool displayPrices = _services.Permissions.Authorize(StandardPermissionProvider.DisplayPrices); @@ -488,9 +528,31 @@ public ProductDetailsModel PrepareProductDetailModel( pvaModel.BeginYear = match.Groups[1].Value.ToInt(); pvaModel.EndYear = match.Groups[2].Value.ToInt(); } + + if (hasSelectedAttributes) + { + var attributeKey = "product_attribute_{0}_{1}_{2}_{3}".FormatInvariant(product.Id, bundleItemId, attribute.ProductAttributeId, attribute.Id); + var day = selectedAttributes[attributeKey + "_day"].ToInt(); + var month = selectedAttributes[attributeKey + "_month"].ToInt(); + var year = selectedAttributes[attributeKey + "_year"].ToInt(); + if (day > 0 && month > 0 && year > 0) + { + pvaModel.SelectedDay = day; + pvaModel.SelectedMonth = month; + pvaModel.SelectedYear = year; + } + } + } + else if (attribute.AttributeControlType == AttributeControlType.TextBox || attribute.AttributeControlType == AttributeControlType.MultilineTextbox) + { + if (hasSelectedAttributes) + { + var attributeKey = "product_attribute_{0}_{1}_{2}_{3}".FormatInvariant(product.Id, bundleItemId, attribute.ProductAttributeId, attribute.Id); + pvaModel.TextValue = selectedAttributes[attributeKey]; + } } - int preSelectedValueId = 0; + var preSelectedValueId = 0; var pvaValues = (attribute.ShouldHaveValues() ? _productAttributeService.GetProductVariantAttributeValues(attribute.Id) : new List()); foreach (var pvaValue in pvaValues) @@ -524,7 +586,7 @@ public ProductDetailsModel PrepareProductDetailModel( decimal taxRate = decimal.Zero; decimal attributeValuePriceAdjustment = _priceCalculationService.GetProductVariantAttributeValuePriceAdjustment(pvaValue); decimal priceAdjustmentBase = _taxService.GetProductPrice(product, attributeValuePriceAdjustment, out taxRate); - decimal priceAdjustment = _currencyService.ConvertFromPrimaryStoreCurrency(priceAdjustmentBase, _services.WorkContext.WorkingCurrency); + decimal priceAdjustment = _currencyService.ConvertFromPrimaryStoreCurrency(priceAdjustmentBase, currency); if (priceAdjustmentBase > decimal.Zero) pvaValueModel.PriceAdjustment = "+" + _priceFormatter.FormatPrice(priceAdjustment, true, false); @@ -554,7 +616,7 @@ public ProductDetailsModel PrepareProductDetailModel( { var linkagePicture = _pictureService.GetPicturesByProductId(pvaValue.LinkedProductId, 1).FirstOrDefault(); if (linkagePicture != null) - pvaValueModel.ImageUrl = _pictureService.GetPictureUrl(linkagePicture, _mediaSettings.AutoCompleteSearchThumbPictureSize, false); + pvaValueModel.ImageUrl = _pictureService.GetPictureUrl(linkagePicture, _mediaSettings.VariantValueThumbPictureSize, false); } pvaModel.Values.Add(pvaValueModel); @@ -613,7 +675,7 @@ public ProductDetailsModel PrepareProductDetailModel( if (isBundlePricing) { - model.AttributeInfo = _productAttributeFormatter.FormatAttributes(product, attributeXml, _services.WorkContext.CurrentCustomer, + model.AttributeInfo = _productAttributeFormatter.FormatAttributes(product, attributeXml, customer, renderPrices: false, renderGiftCardAttributes: false, allowHyperlinks: false); } @@ -677,9 +739,9 @@ public ProductDetailsModel PrepareProductDetailModel( model.ShowGtin = _catalogSettings.ShowGtin; model.Gtin = product.Gtin; model.HasSampleDownload = product.IsDownload && product.HasSampleDownload; - model.IsCurrentCustomerRegistered = _services.WorkContext.CurrentCustomer.IsRegistered(); + model.IsCurrentCustomerRegistered = customer.IsRegistered(); model.IsBasePriceEnabled = product.BasePriceEnabled; - model.BasePriceInfo = product.GetBasePriceInfo(_localizationService, _priceFormatter, _currencyService, _taxService, _priceCalculationService, _services.WorkContext.WorkingCurrency); + model.BasePriceInfo = product.GetBasePriceInfo(_localizationService, _priceFormatter, _currencyService, _taxService, _priceCalculationService, currency); model.ShowLegalInfo = _taxSettings.ShowLegalHintsInProductDetails; model.BundleTitleText = product.GetLocalized(x => x.BundleTitleText); model.BundlePerItemPricing = product.BundlePerItemPricing; @@ -688,43 +750,53 @@ public ProductDetailsModel PrepareProductDetailModel( //_taxSettings.TaxDisplayType == TaxDisplayType.ExcludingTax; - string taxInfo = (_services.WorkContext.GetTaxDisplayTypeFor(_services.WorkContext.CurrentCustomer, _services.StoreContext.CurrentStore.Id) == TaxDisplayType.IncludingTax) - ? T("Tax.InclVAT") - : T("Tax.ExclVAT"); + var taxDisplayType = _services.WorkContext.GetTaxDisplayTypeFor(customer, store.Id); + string taxInfo = T(taxDisplayType == TaxDisplayType.IncludingTax ? "Tax.InclVAT" : "Tax.ExclVAT"); - string defaultTaxRate = ""; - var taxrate = Convert.ToString(_taxService.GetTaxRate(product, _services.WorkContext.CurrentCustomer)); + var defaultTaxRate = ""; + var taxrate = Convert.ToString(_taxService.GetTaxRate(product, customer)); if (_taxSettings.DisplayTaxRates && !taxrate.Equals("0", StringComparison.InvariantCultureIgnoreCase)) { defaultTaxRate = "({0}%)".FormatWith(taxrate); } - var addShippingPrice = _currencyService.ConvertFromPrimaryStoreCurrency(product.AdditionalShippingCharge, _services.WorkContext.WorkingCurrency); - string additionalShippingCosts = String.Empty; + var additionalShippingCosts = String.Empty; + var addShippingPrice = _currencyService.ConvertFromPrimaryStoreCurrency(product.AdditionalShippingCharge, currency); + if (addShippingPrice > 0) { - additionalShippingCosts = T("Common.AdditionalShippingSurcharge").Text.FormatWith(_priceFormatter.FormatPrice(addShippingPrice, true, false)) + ", "; + additionalShippingCosts = T("Common.AdditionalShippingSurcharge").Text.FormatInvariant(_priceFormatter.FormatPrice(addShippingPrice, true, false)) + ", "; } - string shippingInfoLink = _urlHelper.RouteUrl("Topic", new { SystemName = "shippinginfo" }); - if (!product.IsShipEnabled || (addShippingPrice == 0 && product.IsFreeShipping)) { - model.LegalInfo += "{0} {1}, {2}".FormatWith( + model.LegalInfo += "{0} {1}, {2}".FormatInvariant( product.IsTaxExempt ? "" : taxInfo, product.IsTaxExempt ? "" : defaultTaxRate, T("Common.FreeShipping")); } else { - model.LegalInfo = T("Tax.LegalInfoProductDetail", - product.IsTaxExempt ? "" : taxInfo, - product.IsTaxExempt ? "" : defaultTaxRate, - additionalShippingCosts, - shippingInfoLink); + var topic = _topicService.Value.GetTopicBySystemName("ShippingInfo", store.Id); + + if (topic == null) + { + model.LegalInfo = T("Tax.LegalInfoProductDetail2", + product.IsTaxExempt ? "" : taxInfo, + product.IsTaxExempt ? "" : defaultTaxRate, + additionalShippingCosts); + } + else + { + model.LegalInfo = T("Tax.LegalInfoProductDetail", + product.IsTaxExempt ? "" : taxInfo, + product.IsTaxExempt ? "" : defaultTaxRate, + additionalShippingCosts, + _urlHelper.RouteUrl("Topic", new { SystemName = "shippinginfo" })); + } } - string dimension = _measureService.GetMeasureDimensionById(_measureSettings.BaseDimensionId).Name; + var dimension = _measureService.GetMeasureDimensionById(_measureSettings.BaseDimensionId).Name; model.WeightValue = product.Weight; if (!isBundle) @@ -783,8 +855,7 @@ public ProductDetailsModel PrepareProductDetailModel( { //out of stock model.DisplayBackInStockSubscription = true; - model.BackInStockAlreadySubscribed = _backInStockSubscriptionService - .FindSubscription(_services.WorkContext.CurrentCustomer.Id, product.Id, _services.StoreContext.CurrentStore.Id) != null; + model.BackInStockAlreadySubscribed = _backInStockSubscriptionService.FindSubscription(customer.Id, product.Id, store.Id) != null; } #endregion @@ -837,18 +908,18 @@ public ProductDetailsModel PrepareProductDetailModel( } finalPriceWithoutDiscountBase = _priceCalculationService.GetFinalPrice(product, productBundleItems, - _services.WorkContext.CurrentCustomer, attributesTotalPriceBase, false, selectedQuantity, productBundleItem); + customer, attributesTotalPriceBase, false, selectedQuantity, productBundleItem); finalPriceWithDiscountBase = _priceCalculationService.GetFinalPrice(product, productBundleItems, - _services.WorkContext.CurrentCustomer, attributesTotalPriceBase, true, selectedQuantity, productBundleItem); + customer, attributesTotalPriceBase, true, selectedQuantity, productBundleItem); finalPriceWithoutDiscountBase = _taxService.GetProductPrice(product, finalPriceWithoutDiscountBase, out taxRate); finalPriceWithDiscountBase = _taxService.GetProductPrice(product, finalPriceWithDiscountBase, out taxRate); oldPrice = _currencyService.ConvertFromPrimaryStoreCurrency(oldPriceBase, _services.WorkContext.WorkingCurrency); - finalPriceWithoutDiscount = _currencyService.ConvertFromPrimaryStoreCurrency(finalPriceWithoutDiscountBase, _services.WorkContext.WorkingCurrency); - finalPriceWithDiscount = _currencyService.ConvertFromPrimaryStoreCurrency(finalPriceWithDiscountBase, _services.WorkContext.WorkingCurrency); + finalPriceWithoutDiscount = _currencyService.ConvertFromPrimaryStoreCurrency(finalPriceWithoutDiscountBase, currency); + finalPriceWithDiscount = _currencyService.ConvertFromPrimaryStoreCurrency(finalPriceWithDiscountBase, currency); if (productBundleItem == null || isBundleItemPricing) { @@ -869,7 +940,7 @@ public ProductDetailsModel PrepareProductDetailModel( _currencyService, _taxService, _priceCalculationService, - _services.WorkContext.WorkingCurrency, + currency, attributesTotalPriceBase); if (!string.IsNullOrWhiteSpace(model.ProductPrice.OldPrice) || !string.IsNullOrWhiteSpace(model.ProductPrice.PriceWithDiscount)) @@ -883,13 +954,14 @@ public ProductDetailsModel PrepareProductDetailModel( { model.ProductPrice.NoteWithDiscount = T("Products.Bundle.PriceWithDiscount.Note"); } + model.BasePriceInfo = product.GetBasePriceInfo( _localizationService, _priceFormatter, _currencyService, _taxService, _priceCalculationService, - _services.WorkContext.WorkingCurrency, + currency, (product.Price - finalPriceWithDiscount) * (-1)); } } @@ -925,19 +997,21 @@ public ProductDetailsModel PrepareProductDetailModel( model.AddToCart.CustomerEntersPrice = product.CustomerEntersPrice; if (model.AddToCart.CustomerEntersPrice) { - decimal minimumCustomerEnteredPrice = _currencyService.ConvertFromPrimaryStoreCurrency(product.MinimumCustomerEnteredPrice, _services.WorkContext.WorkingCurrency); - decimal maximumCustomerEnteredPrice = _currencyService.ConvertFromPrimaryStoreCurrency(product.MaximumCustomerEnteredPrice, _services.WorkContext.WorkingCurrency); + var minimumCustomerEnteredPrice = _currencyService.ConvertFromPrimaryStoreCurrency(product.MinimumCustomerEnteredPrice, currency); + var maximumCustomerEnteredPrice = _currencyService.ConvertFromPrimaryStoreCurrency(product.MaximumCustomerEnteredPrice, currency); model.AddToCart.CustomerEnteredPrice = minimumCustomerEnteredPrice; + model.AddToCart.CustomerEnteredPriceRange = string.Format(T("Products.EnterProductPrice.Range"), _priceFormatter.FormatPrice(minimumCustomerEnteredPrice, true, false), _priceFormatter.FormatPrice(maximumCustomerEnteredPrice, true, false)); } + //allowed quantities var allowedQuantities = product.ParseAllowedQuatities(); foreach (var qty in allowedQuantities) { - model.AddToCart.AllowedQuantities.Add(new SelectListItem() + model.AddToCart.AllowedQuantities.Add(new SelectListItem { Text = qty.ToString(), Value = qty.ToString() @@ -952,8 +1026,8 @@ public ProductDetailsModel PrepareProductDetailModel( if (model.GiftCard.IsGiftCard) { model.GiftCard.GiftCardType = product.GiftCardType; - model.GiftCard.SenderName = _services.WorkContext.CurrentCustomer.GetFullName(); - model.GiftCard.SenderEmail = _services.WorkContext.CurrentCustomer.Email; + model.GiftCard.SenderName = customer.GetFullName(); + model.GiftCard.SenderEmail = customer.Email; } #endregion @@ -1030,7 +1104,8 @@ public IEnumerable PrepareProductOverviewModels( bool prepareColorAttributes = false, bool prepareManufacturers = false, bool isCompact = false, - bool prepareFullDescription = false) + bool prepareFullDescription = false, + bool isCompareList = false) { if (products == null) throw new ArgumentNullException("products"); @@ -1043,10 +1118,11 @@ public IEnumerable PrepareProductOverviewModels( var enableShoppingCart = _services.Permissions.Authorize(StandardPermissionProvider.EnableShoppingCart); var enableWishlist = _services.Permissions.Authorize(StandardPermissionProvider.EnableWishlist); var taxDisplayType = _services.WorkContext.GetTaxDisplayTypeFor(currentCustomer, currentStore.Id); - string taxInfo = T(taxDisplayType == TaxDisplayType.IncludingTax ? "Tax.InclVAT" : "Tax.ExclVAT"); - string shippingInfoLink = _urlHelper.RouteUrl("Topic", new { SystemName = "shippinginfo" }); var cachedManufacturerModels = new Dictionary(); + string taxInfo = T(taxDisplayType == TaxDisplayType.IncludingTax ? "Tax.InclVAT" : "Tax.ExclVAT"); + var legalInfo = ""; + var res = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "Products.CallForPrice", T("Products.CallForPrice") }, @@ -1054,10 +1130,22 @@ public IEnumerable PrepareProductOverviewModels( { "Media.Product.ImageLinkTitleFormat", T("Media.Product.ImageLinkTitleFormat") }, { "Media.Product.ImageAlternateTextFormat", T("Media.Product.ImageAlternateTextFormat") }, { "Products.DimensionsValue", T("Products.DimensionsValue") }, - { "Tax.LegalInfoFooter", T("Tax.LegalInfoFooter") }, { "Common.AdditionalShippingSurcharge", T("Common.AdditionalShippingSurcharge") } }; + if (_taxSettings.ShowLegalHintsInProductList) + { + if (_topicService.Value.GetTopicBySystemName("ShippingInfo", currentStore.Id) == null) + { + legalInfo = T("Tax.LegalInfoFooter2").Text.FormatInvariant(taxInfo); + } + else + { + var shippingInfoLink = _urlHelper.RouteUrl("Topic", new { SystemName = "shippinginfo" }); + legalInfo = T("Tax.LegalInfoFooter").Text.FormatInvariant(taxInfo, shippingInfoLink); + } + } + var cargoData = _priceCalculationService.CreatePriceCalculationContext(products); var models = new List(); @@ -1065,6 +1153,7 @@ public IEnumerable PrepareProductOverviewModels( foreach (var product in products) { var contextProduct = product; + var finalPrice = decimal.Zero; var model = new ProductOverviewModel { @@ -1100,6 +1189,7 @@ public IEnumerable PrepareProductOverviewModels( var searchContext = new ProductSearchContext { + OrderBy = ProductSortingEnum.Position, StoreId = currentStore.Id, ParentGroupedProductId = product.Id, PageSize = int.MaxValue, @@ -1144,7 +1234,7 @@ public IEnumerable PrepareProductOverviewModels( decimal taxRate = decimal.Zero; decimal oldPriceBase = _taxService.GetProductPrice(contextProduct, contextProduct.OldPrice, out taxRate); decimal finalPriceBase = _taxService.GetProductPrice(contextProduct, displayPrice.Value, out taxRate); - decimal finalPrice = _currencyService.ConvertFromPrimaryStoreCurrency(finalPriceBase, workingCurrency); + finalPrice = _currencyService.ConvertFromPrimaryStoreCurrency(finalPriceBase, workingCurrency); priceModel.OldPrice = null; @@ -1212,7 +1302,7 @@ public IEnumerable PrepareProductOverviewModels( decimal finalPriceBase = _taxService.GetProductPrice(product, displayPrice, out taxRate); decimal oldPrice = _currencyService.ConvertFromPrimaryStoreCurrency(oldPriceBase, workingCurrency); - decimal finalPrice = _currencyService.ConvertFromPrimaryStoreCurrency(finalPriceBase, workingCurrency); + finalPrice = _currencyService.ConvertFromPrimaryStoreCurrency(finalPriceBase, workingCurrency); priceModel.HasDiscount = (finalPriceBase != oldPriceBase && oldPriceBase != decimal.Zero); @@ -1292,8 +1382,8 @@ from a in colorAttribute.ProductVariantAttributeValues.Take(50) var picture = product.GetDefaultProductPicture(_pictureService); var pictureModel = new PictureModel { - ImageUrl = _pictureService.GetPictureUrl(picture, pictureSize), - FullSizeImageUrl = _pictureService.GetPictureUrl(picture), + ImageUrl = _pictureService.GetPictureUrl(picture, pictureSize, !_catalogSettings.HideProductDefaultPictures), + FullSizeImageUrl = _pictureService.GetPictureUrl(picture, 0, !_catalogSettings.HideProductDefaultPictures), Title = string.Format(res["Media.Product.ImageLinkTitleFormat"], model.Name), AlternateText = string.Format(res["Media.Product.ImageAlternateTextFormat"], model.Name) }; @@ -1322,7 +1412,7 @@ from a in colorAttribute.ProductVariantAttributeValues.Take(50) model.DimensionMeasureUnit = _measureService.GetMeasureDimensionById(_measureSettings.BaseDimensionId).Name; model.ThumbDimension = _mediaSettings.ProductThumbPictureSize; model.ShowLegalInfo = _taxSettings.ShowLegalHintsInProductList; - model.LegalInfo = res["Tax.LegalInfoFooter"].Text.FormatWith(taxInfo, shippingInfoLink); + model.LegalInfo = legalInfo; model.RatingSum = product.ApprovedRatingSum; model.TotalReviews = product.ApprovedTotalReviews; model.ShowReviews = _catalogSettings.ShowProductReviewsInProductLists; @@ -1349,19 +1439,22 @@ from a in colorAttribute.ProductVariantAttributeValues.Take(50) if (prepareManufacturers) { - model.Manufacturers = PrepareManufacturersOverviewModel(_manufacturerService.GetProductManufacturersByProductId(product.Id), cachedManufacturerModels); + model.Manufacturers = PrepareManufacturersOverviewModel(_manufacturerService.GetProductManufacturersByProductId(product.Id), cachedManufacturerModels, false); } - if (_catalogSettings.ShowBasePriceInProductLists) + if (finalPrice != decimal.Zero && (_catalogSettings.ShowBasePriceInProductLists || isCompareList)) { - model.BasePriceInfo = contextProduct.GetBasePriceInfo(_localizationService, _priceFormatter, _currencyService, _taxService, _priceCalculationService, workingCurrency); + model.BasePriceInfo = contextProduct.GetBasePriceInfo(finalPrice, _localizationService, _priceFormatter, workingCurrency); } - var addShippingPrice = _currencyService.ConvertCurrency(contextProduct.AdditionalShippingCharge, currentStore.PrimaryStoreCurrency, workingCurrency); - - if (addShippingPrice > 0 && displayPrices) + if (displayPrices) { - model.TransportSurcharge = res["Common.AdditionalShippingSurcharge"].Text.FormatWith(_priceFormatter.FormatPrice(addShippingPrice, true, false)); + var addShippingPrice = _currencyService.ConvertCurrency(contextProduct.AdditionalShippingCharge, currentStore.PrimaryStoreCurrency, workingCurrency); + + if (addShippingPrice > 0) + { + model.TransportSurcharge = res["Common.AdditionalShippingSurcharge"].Text.FormatCurrent(_priceFormatter.FormatPrice(addShippingPrice, true, false)); + } } if (contextProduct.Weight > 0) @@ -1372,7 +1465,7 @@ from a in colorAttribute.ProductVariantAttributeValues.Take(50) // IsNew if (_catalogSettings.LabelAsNewForMaxDays.HasValue) { - model.IsNew = (DateTime.UtcNow - product.CreatedOnUtc).Days <= _catalogSettings.LabelAsNewForMaxDays.Value; + model.IsNew = ((DateTime.UtcNow - product.CreatedOnUtc).Days <= _catalogSettings.LabelAsNewForMaxDays.Value); } models.Add(model); @@ -1680,7 +1773,8 @@ public void PreparePagingFilteringModel(PagingFilteringModel model, PagingFilter public List PrepareManufacturersOverviewModel( ICollection manufacturers, - IDictionary cachedModels = null) + IDictionary cachedModels = null, + bool forProductDetailPage = false) { var model = new List(); @@ -1705,7 +1799,10 @@ public List PrepareManufacturersOverviewModel( }; - item.PictureModel = PrepareManufacturerPictureModel(manufacturer, manufacturer.GetLocalized(x => x.Name)); + if (_catalogSettings.ShowManufacturerPicturesInProductDetail) + { + item.PictureModel = PrepareManufacturerPictureModel(manufacturer, manufacturer.GetLocalized(x => x.Name)); + } cachedModels.Add(item.Id, item); } @@ -1720,11 +1817,11 @@ public PictureModel PrepareManufacturerPictureModel(Manufacturer manufacturer, s { var model = new PictureModel(); - int pictureSize = _mediaSettings.ManufacturerThumbPictureSize; + var pictureSize = _mediaSettings.ManufacturerThumbPictureSize; var manufacturerPictureCacheKey = string.Format(ModelCacheEventConsumer.MANUFACTURER_PICTURE_MODEL_KEY, manufacturer.Id, pictureSize, - true, + !_catalogSettings.HideManufacturerDefaultPictures, _services.WorkContext.WorkingLanguage.Id, _services.WebHelper.IsCurrentConnectionSecured(), _services.StoreContext.CurrentStore.Id); @@ -1735,7 +1832,7 @@ public PictureModel PrepareManufacturerPictureModel(Manufacturer manufacturer, s { PictureId = manufacturer.PictureId.GetValueOrDefault(), //FullSizeImageUrl = _pictureService.GetPictureUrl(manufacturer.PictureId.GetValueOrDefault()), - ImageUrl = _pictureService.GetPictureUrl(manufacturer.PictureId.GetValueOrDefault(), pictureSize), + ImageUrl = _pictureService.GetPictureUrl(manufacturer.PictureId.GetValueOrDefault(), pictureSize, !_catalogSettings.HideManufacturerDefaultPictures), Title = string.Format(T("Media.Manufacturer.ImageLinkTitleFormat"), localizedName), AlternateText = string.Format(T("Media.Manufacturer.ImageAlternateTextFormat"), localizedName) }; diff --git a/src/Presentation/SmartStore.Web/Controllers/CheckoutController.cs b/src/Presentation/SmartStore.Web/Controllers/CheckoutController.cs index ff8b371c7d..5047764d52 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CheckoutController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CheckoutController.cs @@ -23,6 +23,7 @@ using SmartStore.Services.Shipping; using SmartStore.Services.Tax; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Filters; using SmartStore.Web.Framework.Plugins; using SmartStore.Web.Framework.Security; using SmartStore.Web.Models.Checkout; @@ -359,6 +360,7 @@ protected CheckoutConfirmModel PrepareConfirmOrderModel(IList( - SystemCustomerAttributeNames.SelectedPaymentMethod, _genericAttributeService, _storeContext.CurrentStore.Id); + processPaymentRequest.StoreId = storeId; + processPaymentRequest.CustomerId = customer.Id; + processPaymentRequest.PaymentMethodSystemName = customer.GetAttribute(SystemCustomerAttributeNames.SelectedPaymentMethod, _genericAttributeService, storeId); var placeOrderExtraData = new Dictionary(); placeOrderExtraData["CustomerComment"] = form["customercommenthidden"]; + placeOrderExtraData["SubscribeToNewsLetter"] = form["SubscribeToNewsLetterHidden"]; + placeOrderExtraData["AcceptThirdPartyEmailHandOver"] = form["AcceptThirdPartyEmailHandOverHidden"]; - var placeOrderResult = _orderProcessingService.PlaceOrder(processPaymentRequest, placeOrderExtraData); - - if (placeOrderResult.Success) - { - var postProcessPaymentRequest = new PostProcessPaymentRequest - { - Order = placeOrderResult.PlacedOrder - }; - - _paymentService.PostProcessPayment(postProcessPaymentRequest); - - _httpContext.Session["PaymentData"] = null; - _httpContext.Session["OrderPaymentInfo"] = null; - _httpContext.RemoveCheckoutState(); - - if (postProcessPaymentRequest.RedirectUrl.HasValue()) - { - return Redirect(postProcessPaymentRequest.RedirectUrl); - } + placeOrderResult = _orderProcessingService.PlaceOrder(processPaymentRequest, placeOrderExtraData); - return RedirectToAction("Completed"); - } - else + if (!placeOrderResult.Success) { - model.Warnings.AddRange(placeOrderResult.Errors); + model.Warnings.AddRange(placeOrderResult.Errors.Select(x => HtmlUtils.ConvertPlainTextToHtml(x))); } } - catch (Exception exc) + catch (Exception exception) { - Logger.Warning(exc.Message, exc); - model.Warnings.Add(exc.Message); + Logger.Warning(exception.Message, exception); + + if (!model.Warnings.Any(x => x == exception.Message)) + { + model.Warnings.Add(exception.Message); + } } - return View(model); - } + if (placeOrderResult == null || !placeOrderResult.Success || model.Warnings.Any()) + { + return View(model); + } + + try + { + postProcessPaymentRequest = new PostProcessPaymentRequest + { + Order = placeOrderResult.PlacedOrder + }; + + _paymentService.PostProcessPayment(postProcessPaymentRequest); + } + catch (Exception exception) + { + NotifyError(exception); + } + finally + { + _httpContext.Session["PaymentData"] = null; + _httpContext.Session["OrderPaymentInfo"] = null; + _httpContext.RemoveCheckoutState(); + } + + if (postProcessPaymentRequest != null && postProcessPaymentRequest.RedirectUrl.HasValue()) + { + return Redirect(postProcessPaymentRequest.RedirectUrl); + } + + return RedirectToAction("Completed"); + } public ActionResult Completed() diff --git a/src/Presentation/SmartStore.Web/Controllers/CommonController.cs b/src/Presentation/SmartStore.Web/Controllers/CommonController.cs index 4c411f9238..05cdc3bdc6 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CommonController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CommonController.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; using System.Drawing; -using System.Globalization; using System.Linq; using System.Text; -using System.Web; using System.Web.Mvc; +using SmartStore.Core.Data; +using SmartStore.Core.Domain; using SmartStore.Core.Domain.Blogs; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Common; @@ -35,21 +35,24 @@ using SmartStore.Services.Orders; using SmartStore.Services.Security; using SmartStore.Services.Topics; +using SmartStore.Web.Framework; using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Framework.Localization; using SmartStore.Web.Framework.Pdf; -using SmartStore.Web.Framework.Themes; +using SmartStore.Web.Framework.Theming; using SmartStore.Web.Framework.UI; using SmartStore.Web.Infrastructure.Cache; using SmartStore.Web.Models.Common; +using SmartStore.Services.Seo; namespace SmartStore.Web.Controllers { - public partial class CommonController : PublicControllerBase + public partial class CommonController : PublicControllerBase { - #region Fields + private readonly static string[] s_hints = new string[] { "Shopsystem", "Onlineshop Software", "Shopsoftware", "E-Commerce Solution" }; - private readonly ITopicService _topicService; + private readonly ICommonServices _services; + private readonly ITopicService _topicService; private readonly Lazy _languageService; private readonly Lazy _currencyService; private readonly IThemeContext _themeContext; @@ -57,10 +60,11 @@ public partial class CommonController : PublicControllerBase private readonly Lazy _forumservice; private readonly Lazy _genericAttributeService; private readonly Lazy _mobileDeviceHelper; + private readonly Lazy _compareProductsService; + private readonly Lazy _urlRecordService; - private readonly static string[] s_hints = new string[] { "Shopsystem", "Onlineshop Software", "Shopsoftware", "E-Commerce Solution" }; - - private readonly CustomerSettings _customerSettings; + private readonly StoreInformationSettings _storeInfoSettings; + private readonly CustomerSettings _customerSettings; private readonly TaxSettings _taxSettings; private readonly CatalogSettings _catalogSettings; private readonly ThemeSettings _themeSettings; @@ -70,18 +74,19 @@ public partial class CommonController : PublicControllerBase private readonly ForumSettings _forumSettings; private readonly LocalizationSettings _localizationSettings; private readonly Lazy _securitySettings; - - private readonly IOrderTotalCalculationService _orderTotalCalculationService; + private readonly Lazy _socialSettings; + private readonly Lazy _mediaSettings; + private readonly IOrderTotalCalculationService _orderTotalCalculationService; + private readonly IPriceFormatter _priceFormatter; private readonly IPageAssetsBuilder _pageAssetsBuilder; private readonly Lazy _pictureService; - private readonly ICommonServices _services; - - #endregion - - #region Constructors + private readonly Lazy _manufacturerService; + private readonly Lazy _categoryService; + private readonly Lazy _productService; public CommonController( + ICommonServices services, ITopicService topicService, Lazy languageService, Lazy currencyService, @@ -90,7 +95,10 @@ public CommonController( Lazy forumService, Lazy genericAttributeService, Lazy mobileDeviceHelper, - CustomerSettings customerSettings, + Lazy compareProductsService, + Lazy urlRecordService, + StoreInformationSettings storeInfoSettings, + CustomerSettings customerSettings, TaxSettings taxSettings, CatalogSettings catalogSettings, EmailAccountSettings emailAccountSettings, @@ -100,14 +108,19 @@ public CommonController( ForumSettings forumSettings, LocalizationSettings localizationSettings, Lazy securitySettings, - IOrderTotalCalculationService orderTotalCalculationService, + Lazy socialSettings, + Lazy mediaSettings, + IOrderTotalCalculationService orderTotalCalculationService, IPriceFormatter priceFormatter, ThemeSettings themeSettings, IPageAssetsBuilder pageAssetsBuilder, Lazy pictureService, - ICommonServices services) + Lazy manufacturerService, + Lazy categoryService, + Lazy productService) { - this._topicService = topicService; + this._services = services; + this._topicService = topicService; this._languageService = languageService; this._currencyService = currencyService; this._themeContext = themeContext; @@ -115,8 +128,11 @@ public CommonController( this._forumservice = forumService; this._genericAttributeService = genericAttributeService; this._mobileDeviceHelper = mobileDeviceHelper; - - this._customerSettings = customerSettings; + this._compareProductsService = compareProductsService; + this._urlRecordService = urlRecordService; + + this._storeInfoSettings = storeInfoSettings; + this._customerSettings = customerSettings; this._taxSettings = taxSettings; this._catalogSettings = catalogSettings; this._commonSettings = commonSettings; @@ -125,6 +141,8 @@ public CommonController( this._forumSettings = forumSettings; this._localizationSettings = localizationSettings; this._securitySettings = securitySettings; + this._socialSettings = socialSettings; + this._mediaSettings = mediaSettings; this._orderTotalCalculationService = orderTotalCalculationService; this._priceFormatter = priceFormatter; @@ -132,11 +150,11 @@ public CommonController( this._themeSettings = themeSettings; this._pageAssetsBuilder = pageAssetsBuilder; this._pictureService = pictureService; - this._services = services; + this._manufacturerService = manufacturerService; + this._categoryService = categoryService; + this._productService = productService; } - #endregion - #region Utilities [NonAction] @@ -150,7 +168,7 @@ protected LanguageSelectorModel PrepareLanguageSelectorModel() { Id = x.Id, Name = x.Name, - NativeName = GetLanguageNativeName(x.LanguageCulture) ?? x.Name, + NativeName = LocalizationHelper.GetLanguageNativeName(x.LanguageCulture) ?? x.Name, ISOCode = x.LanguageCulture, SeoCode = x.UniqueSeoCode, FlagImageFileName = x.FlagImageFileName @@ -161,7 +179,7 @@ protected LanguageSelectorModel PrepareLanguageSelectorModel() var workingLanguage = _services.WorkContext.WorkingLanguage; - var model = new LanguageSelectorModel() + var model = new LanguageSelectorModel { CurrentLanguageId = workingLanguage.Id, AvailableLanguages = availableLanguages, @@ -172,9 +190,10 @@ protected LanguageSelectorModel PrepareLanguageSelectorModel() foreach (var lang in model.AvailableLanguages) { - var helper = new LocalizedUrlHelper(HttpContext.Request, true); + //var helper = new LocalizedUrlHelper(HttpContext.Request, true); + var helper = CreateUrlHelperForLanguageSelector(lang, workingLanguage.Id); - if (_localizationSettings.SeoFriendlyUrlsForLanguagesEnabled) + if (_localizationSettings.SeoFriendlyUrlsForLanguagesEnabled) { if (lang.SeoCode == defaultSeoCode && (int)(_localizationSettings.DefaultLanguageRedirectBehaviour) > 0) { @@ -192,27 +211,39 @@ protected LanguageSelectorModel PrepareLanguageSelectorModel() return model; } - // TODO: (MC) zentral auslagern - private string GetLanguageNativeName(string locale) - { - try - { - if (!string.IsNullOrEmpty(locale)) - { - var info = CultureInfo.GetCultureInfoByIetfLanguageTag(locale); - if (info == null) - { - return null; - } - return info.NativeName; - } - return null; - } - catch - { - return null; - } - } + private LocalizedUrlHelper CreateUrlHelperForLanguageSelector(LanguageModel model, int currentLanguageId) + { + if (currentLanguageId != model.Id) + { + var routeValues = this.Request.RequestContext.RouteData.Values; + var controller = routeValues["controller"].ToString(); + + object val; + if (!routeValues.TryGetValue(controller + "id", out val)) + { + controller = routeValues["action"].ToString(); + routeValues.TryGetValue(controller + "id", out val); + } + + int entityId = 0; + if (val != null) + { + entityId = val.Convert(); + } + + if (entityId > 0) + { + var activeSlug = _urlRecordService.Value.GetActiveSlug(entityId, controller, model.Id); + if (activeSlug.HasValue()) + { + var helper = new LocalizedUrlHelper(Request.ApplicationPath, activeSlug, false); + return helper; + } + } + } + + return new LocalizedUrlHelper(HttpContext.Request, true); + } [NonAction] protected CurrencySelectorModel PrepareCurrencySelectorModel() @@ -226,7 +257,7 @@ protected CurrencySelectorModel PrepareCurrencySelectorModel() Id = x.Id, Name = x.GetLocalized(y => y.Name), ISOCode = x.CurrencyCode, - Symbol = GetCurrencySymbol(x.DisplayLocale) ?? x.CurrencyCode + Symbol = LocalizationHelper.GetCurrencySymbol(x.DisplayLocale) ?? x.CurrencyCode }) .ToList(); return result; @@ -240,28 +271,6 @@ protected CurrencySelectorModel PrepareCurrencySelectorModel() return model; } - // TODO: Zentral auslagern - private static string GetCurrencySymbol(string locale) - { - try - { - if (!string.IsNullOrEmpty(locale)) - { - var info = new RegionInfo(locale); - if (info == null) - { - return null; - } - return info.CurrencySymbol; - } - return null; - } - catch - { - return null; - } - } - [NonAction] protected TaxTypeSelectorModel PrepareTaxTypeSelectorModel() { @@ -359,18 +368,6 @@ public ActionResult SetLanguage(int langid, string returnUrl = "") { _services.WorkContext.WorkingLanguage = language; } - - // url referrer - if (String.IsNullOrEmpty(returnUrl)) - { - returnUrl = _services.WebHelper.GetUrlReferrer(); - } - - // home page - if (String.IsNullOrEmpty(returnUrl)) - { - returnUrl = Url.RouteUrl("HomePage"); - } if (_localizationSettings.SeoFriendlyUrlsForLanguagesEnabled) { @@ -379,10 +376,9 @@ public ActionResult SetLanguage(int langid, string returnUrl = "") returnUrl = helper.GetAbsolutePath(); } - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } - //currency [ChildActionOnly] public ActionResult CurrencySelector() { @@ -393,40 +389,31 @@ public ActionResult CurrencySelector() return PartialView(model); } + public ActionResult CurrencySelected(int customerCurrency, string returnUrl = "") { var currency = _currencyService.Value.GetCurrencyById(customerCurrency); if (currency != null) + { _services.WorkContext.WorkingCurrency = currency; + } - //url referrer - if (String.IsNullOrEmpty(returnUrl)) - returnUrl = _services.WebHelper.GetUrlReferrer(); - //home page - if (String.IsNullOrEmpty(returnUrl)) - returnUrl = Url.RouteUrl("HomePage"); - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } - //tax type [ChildActionOnly] public ActionResult TaxTypeSelector() { var model = PrepareTaxTypeSelectorModel(); return PartialView(model); } + public ActionResult TaxTypeSelected(int customerTaxType, string returnUrl = "") { var taxDisplayType = (TaxDisplayType)Enum.ToObject(typeof(TaxDisplayType), customerTaxType); _services.WorkContext.TaxDisplayType = taxDisplayType; - //url referrer - if (String.IsNullOrEmpty(returnUrl)) - returnUrl = _services.WebHelper.GetUrlReferrer(); - //home page - if (String.IsNullOrEmpty(returnUrl)) - returnUrl = Url.RouteUrl("HomePage"); - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } //Configuration page (used on mobile devices) @@ -506,7 +493,18 @@ public ActionResult ShopBar() { var customer = _services.WorkContext.CurrentCustomer; - var unreadMessageCount = GetUnreadPrivateMessages(); + var isAdmin = customer.IsAdmin(); + var isRegistered = isAdmin || customer.IsRegistered(); + + if (_storeInfoSettings.StoreClosed) + { + if (!isAdmin || !_storeInfoSettings.StoreClosedAllowForAdmins) + { + return Content(""); + } + } + + var unreadMessageCount = GetUnreadPrivateMessages(); var unreadMessage = string.Empty; var alertMessage = string.Empty; if (unreadMessageCount > 0) @@ -542,8 +540,8 @@ public ActionResult ShopBar() } var model = new ShopBarModel { - IsAuthenticated = customer.IsRegistered(), - CustomerEmailUsername = customer.IsRegistered() ? (_customerSettings.UsernamesEnabled ? customer.Username : customer.Email) : "", + IsAuthenticated = isRegistered, + CustomerEmailUsername = isRegistered ? (_customerSettings.UsernamesEnabled ? customer.Username : customer.Email) : "", IsCustomerImpersonated = _services.WorkContext.OriginalCustomerIfImpersonated != null, DisplayAdminLink = _services.Permissions.Authorize(StandardPermissionProvider.AccessAdminPanel), ShoppingCartEnabled = _services.Permissions.Authorize(StandardPermissionProvider.EnableShoppingCart), @@ -555,7 +553,7 @@ public ActionResult ShopBar() CompareProductsEnabled = _catalogSettings.CompareProductsEnabled }; - if (model.ShoppingCartEnabled || model.WishlistEnabled) + if (model.ShoppingCartEnabled || model.WishlistEnabled) { if (model.ShoppingCartEnabled) model.ShoppingCartItems = cart.GetTotalProducts(); @@ -566,7 +564,7 @@ public ActionResult ShopBar() if (_catalogSettings.CompareProductsEnabled) { - model.CompareItems = EngineContext.Current.Resolve().GetComparedProductsCount(); + model.CompareItems = _compareProductsService.Value.GetComparedProductsCount(); } return PartialView(model); @@ -575,12 +573,11 @@ public ActionResult ShopBar() [ChildActionOnly] public ActionResult Footer() { - string taxInfo = (_services.WorkContext.GetTaxDisplayTypeFor(_services.WorkContext.CurrentCustomer, _services.StoreContext.CurrentStore.Id) == TaxDisplayType.IncludingTax) - ? T("Tax.InclVAT") - : T("Tax.ExclVAT"); - - string shippingInfoLink = Url.RouteUrl("Topic", new { SystemName = "shippinginfo" }); var store = _services.StoreContext.CurrentStore; + var allTopics = _topicService.GetAllTopics(store.Id); + var taxDisplayType = _services.WorkContext.GetTaxDisplayTypeFor(_services.WorkContext.CurrentCustomer, store.Id); + + var taxInfo = T(taxDisplayType == TaxDisplayType.IncludingTax ? "Tax.InclVAT" : "Tax.ExclVAT"); var availableStoreThemes = !_themeSettings.AllowCustomerToSelectTheme ? new List() : _themeRegistry.Value.GetThemeManifests() .Where(x => !x.MobileTheme) @@ -597,7 +594,6 @@ public ActionResult Footer() var model = new FooterModel { StoreName = store.Name, - LegalInfo = T("Tax.LegalInfoFooter", taxInfo, shippingInfoLink), ShowLegalInfo = _taxSettings.ShowLegalHintsInFooter, ShowThemeSelector = availableStoreThemes.Count > 1, BlogEnabled = _blogSettings.Enabled, @@ -605,6 +601,26 @@ public ActionResult Footer() HideNewsletterBlock = _customerSettings.HideNewsletterBlock, }; + model.TopicPageUrls = allTopics + .Where(x => !x.RenderAsWidget) + .GroupBy(x => x.SystemName) + .ToDictionary(x => x.Key.EmptyNull().ToLower(), x => + { + if (x.Key.IsCaseInsensitiveEqual("contactus")) + return Url.RouteUrl("ContactUs"); + + return Url.RouteUrl("Topic", new { SystemName = x.Key }); + }); + + if (model.TopicPageUrls.ContainsKey("shippinginfo")) + { + model.LegalInfo = T("Tax.LegalInfoFooter", taxInfo, model.TopicPageUrls["shippinginfo"]); + } + else + { + model.LegalInfo = T("Tax.LegalInfoFooter2", taxInfo); + } + var hint = _services.Settings.GetSettingByKey("Rnd_SmCopyrightHint", string.Empty, store.Id); if (hint.IsEmpty()) { @@ -612,30 +628,15 @@ public ActionResult Footer() _services.Settings.SetSetting("Rnd_SmCopyrightHint", hint, store.Id); } - var topics = new string[] { "paymentinfo", "imprint", "disclaimer" }; - foreach (var t in topics) - { - //load by store - var topic = _topicService.GetTopicBySystemName(t, store.Id); - if (topic == null) - //not found. let's find topic assigned to all stores - topic = _topicService.GetTopicBySystemName(t, 0); - - if (topic != null) - { - model.Topics.Add(t, topic.Title); - } - } - - var socialSettings = EngineContext.Current.Resolve(); + model.ShowSocialLinks = _socialSettings.Value.ShowSocialLinksInFooter; + model.FacebookLink = _socialSettings.Value.FacebookLink; + model.GooglePlusLink = _socialSettings.Value.GooglePlusLink; + model.TwitterLink = _socialSettings.Value.TwitterLink; + model.PinterestLink = _socialSettings.Value.PinterestLink; + model.YoutubeLink = _socialSettings.Value.YoutubeLink; - model.ShowSocialLinks = socialSettings.ShowSocialLinksInFooter; - model.FacebookLink = socialSettings.FacebookLink; - model.GooglePlusLink = socialSettings.GooglePlusLink; - model.TwitterLink = socialSettings.TwitterLink; - model.PinterestLink = socialSettings.PinterestLink; - model.YoutubeLink = socialSettings.YoutubeLink; - model.SmartStoreHint = "{0} by SmartStore AG © {1}".FormatCurrent(hint, DateTime.Now.Year); + model.SmartStoreHint = "{0} by SmartStore AG © {1}" + .FormatCurrent(hint, DateTime.Now.Year); return PartialView(model); } @@ -643,6 +644,7 @@ public ActionResult Footer() [ChildActionOnly] public ActionResult Menu() { + var store = _services.StoreContext.CurrentStore; var customer = _services.WorkContext.CurrentCustomer; var model = new MenuModel @@ -657,7 +659,10 @@ public ActionResult Menu() IsCustomerImpersonated = _services.WorkContext.OriginalCustomerIfImpersonated != null, IsAuthenticated = customer.IsRegistered(), DisplayAdminLink = _services.Permissions.Authorize(StandardPermissionProvider.AccessAdminPanel), - }; + HasContactUsPage = (_topicService.GetTopicBySystemName("ContactUs", store.Id) != null) + }; + + model.DisplayLoginLink = _storeInfoSettings.StoreClosed && !model.DisplayAdminLink; return PartialView(model); } @@ -666,7 +671,10 @@ public ActionResult Menu() [ChildActionOnly] public ActionResult InfoBlock() { - var model = new InfoBlockModel + var store = _services.StoreContext.CurrentStore; + var allTopics = _topicService.GetAllTopics(store.Id); + + var model = new InfoBlockModel { RecentlyAddedProductsEnabled = _catalogSettings.RecentlyAddedProductsEnabled, RecentlyViewedProductsEnabled = _catalogSettings.RecentlyViewedProductsEnabled, @@ -677,7 +685,18 @@ public ActionResult InfoBlock() AllowPrivateMessages = _forumSettings.AllowPrivateMessages, }; - return PartialView(model); + model.TopicPageUrls = allTopics + .Where(x => !x.RenderAsWidget) + .GroupBy(x => x.SystemName) + .ToDictionary(x => x.Key.EmptyNull().ToLower(), x => + { + if (x.Key.IsCaseInsensitiveEqual("contactus")) + return Url.RouteUrl("ContactUs"); + + return Url.RouteUrl("Topic", new { SystemName = x.Key }); + }); + + return PartialView(model); } [ChildActionOnly] @@ -722,12 +741,7 @@ public ActionResult ChangeTheme(string themeName, string returnUrl = null) return Json(new { Success = true }); } - if (returnUrl.IsEmpty()) - { - return RedirectToRoute("HomePage"); - } - - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } [ChildActionOnly] @@ -773,13 +787,13 @@ public ActionResult Favicon() [HttpPost] public ActionResult ChangeDevice(bool dontUseMobileVersion) { - _genericAttributeService.Value.SaveAttribute(_services.WorkContext.CurrentCustomer, - SystemCustomerAttributeNames.DontUseMobileVersion, dontUseMobileVersion, _services.StoreContext.CurrentStore.Id); + _genericAttributeService.Value.SaveAttribute( + _services.WorkContext.CurrentCustomer, + SystemCustomerAttributeNames.DontUseMobileVersion, + dontUseMobileVersion, + _services.StoreContext.CurrentStore.Id); - string returnurl = _services.WebHelper.GetUrlReferrer(); - if (String.IsNullOrEmpty(returnurl)) - returnurl = Url.RouteUrl("HomePage"); - return Redirect(returnurl); + return RedirectToReferrer(); } [ChildActionOnly] @@ -849,7 +863,9 @@ public ActionResult RobotsTextFile() "/Product/EmailAFriend", "/Search", "/Config", - "/Settings" + "/Settings", + "/Login", + "/Register" }; @@ -1008,6 +1024,153 @@ protected PdfReceiptHeaderFooterModel PreparePdfReceiptHeaderFooterModel(int sto }, 1 /* 1 min. (just for the duration of pdf processing) */); } - #endregion - } + #endregion + + #region Entity Picker + + public ActionResult EntityPicker(EntityPickerModel model) + { + model.PageSize = 48; // _commonSettings.EntityPickerPageSize; + model.AllString = T("Admin.Common.All"); + + if (model.Entity.IsCaseInsensitiveEqual("product")) + { + var allCategories = _categoryService.Value.GetAllCategories(showHidden: true); + var mappedCategories = allCategories.ToDictionary(x => x.Id); + + model.AvailableCategories = allCategories + .Select(x => new SelectListItem { Text = x.GetCategoryNameWithPrefix(_categoryService.Value, mappedCategories), Value = x.Id.ToString() }) + .ToList(); + + model.AvailableManufacturers = _manufacturerService.Value.GetAllManufacturers(true) + .Select(x => new SelectListItem { Text = x.Name, Value = x.Id.ToString() }) + .ToList(); + + model.AvailableStores = _services.StoreService.GetAllStores() + .Select(x => new SelectListItem { Text = x.Name, Value = x.Id.ToString() }) + .ToList(); + + model.AvailableProductTypes = ProductType.SimpleProduct.ToSelectList(false).ToList(); + } + + return PartialView(model); + } + + [HttpPost] + public ActionResult EntityPicker(EntityPickerModel model, FormCollection form) + { + model.PageSize = 48; // _commonSettings.EntityPickerPageSize; + model.PublishedString = T("Common.Published"); + model.UnpublishedString = T("Common.Unpublished"); + + try + { + var disableIf = model.DisableIf.SplitSafe(",").Select(x => x.ToLower().Trim()).ToList(); + var disableIds = model.DisableIds.SplitSafe(",").Select(x => x.ToInt()).ToList(); + + using (var scope = new DbContextScope(_services.DbContext, autoDetectChanges: false, proxyCreation: true, validateOnSave: false, forceNoTracking: true)) + { + if (model.Entity.IsCaseInsensitiveEqual("product")) + { + #region Product + + model.SearchTerm = model.ProductName.TrimSafe(); + + var hasPermission = _services.Permissions.Authorize(StandardPermissionProvider.ManageCatalog); + var storeLocation = _services.WebHelper.GetStoreLocation(false); + var disableIfNotSimpleProduct = disableIf.Contains("notsimpleproduct"); + var labelTextGrouped = T("Admin.Catalog.Products.ProductType.GroupedProduct.Label").Text; + var labelTextBundled = T("Admin.Catalog.Products.ProductType.BundledProduct.Label").Text; + var sku = T("Products.Sku").Text; + + var searchContext = new ProductSearchContext + { + CategoryIds = (model.CategoryId == 0 ? null : new List { model.CategoryId }), + ManufacturerId = model.ManufacturerId, + StoreId = model.StoreId, + Keywords = model.SearchTerm, + ProductType = model.ProductTypeId > 0 ? (ProductType?)model.ProductTypeId : null, + SearchSku = !_catalogSettings.SuppressSkuSearch, + ShowHidden = hasPermission + }; + + var query = _productService.Value.PrepareProductSearchQuery(searchContext, x => new { x.Id, x.Sku, x.Name, x.Published, x.ProductTypeId }); + + query = from x in query + group x by x.Id into grp + orderby grp.Key + select grp.FirstOrDefault(); + + var products = query + .OrderBy(x => x.Name) + .Skip(model.PageIndex * model.PageSize) + .Take(model.PageSize) + .ToList(); + + var productIds = products.Select(x => x.Id).ToArray(); + var pictures = _productService.Value.GetProductPicturesByProductIds(productIds, true); + + model.SearchResult = products + .Select(x => + { + var item = new EntityPickerModel.SearchResultModel + { + Id = x.Id, + ReturnValue = (model.ReturnField.IsCaseInsensitiveEqual("sku") ? x.Sku : x.Id.ToString()), + Title = x.Name, + Summary = x.Sku, + SummaryTitle = "{0}: {1}".FormatInvariant(sku, x.Sku.NaIfEmpty()), + Published = (hasPermission ? x.Published : (bool?)null) + }; + + if (disableIfNotSimpleProduct) + { + item.Disable = (x.ProductTypeId != (int)ProductType.SimpleProduct); + } + + if (!item.Disable && disableIds.Contains(x.Id)) + { + item.Disable = true; + } + + if (x.ProductTypeId == (int)ProductType.GroupedProduct) + { + item.LabelText = labelTextGrouped; + item.LabelClassName = "label-success"; + } + else if (x.ProductTypeId == (int)ProductType.BundledProduct) + { + item.LabelText = labelTextBundled; + item.LabelClassName = "label-info"; + } + + var productPicture = pictures.FirstOrDefault(y => y.Key == x.Id); + if (productPicture.Value != null) + { + var picture = productPicture.Value.FirstOrDefault(); + if (picture != null) + { + item.ImageUrl = _pictureService.Value.GetPictureUrl(picture.Picture, _mediaSettings.Value.ProductThumbPictureSizeOnProductDetailsPage, + !_catalogSettings.HideProductDefaultPictures, storeLocation); + } + } + + return item; + }) + .ToList(); + + #endregion + } + } + } + catch (Exception exception) + { + NotifyError(exception.ToAllMessages()); + } + + return PartialView("EntityPickerList", model); + } + + #endregion + } } diff --git a/src/Presentation/SmartStore.Web/Controllers/CustomerController.cs b/src/Presentation/SmartStore.Web/Controllers/CustomerController.cs index a643b42759..a72b4fc872 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CustomerController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CustomerController.cs @@ -30,6 +30,7 @@ using SmartStore.Services.Tax; using SmartStore.Utilities; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Filters; using SmartStore.Web.Framework.Plugins; using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.UI.Captcha; @@ -38,7 +39,7 @@ namespace SmartStore.Web.Controllers { - public partial class CustomerController : PublicControllerBase + public partial class CustomerController : PublicControllerBase { #region Fields @@ -351,37 +352,46 @@ protected void PrepareCustomerInfoModel(CustomerInfoModel model, Customer custom } [NonAction] - protected CustomerOrderListModel PrepareCustomerOrderListModel(Customer customer) + protected CustomerOrderListModel PrepareCustomerOrderListModel(Customer customer, int pageIndex) { if (customer == null) throw new ArgumentNullException("customer"); - var model = new CustomerOrderListModel(); + var storeScope = (_orderSettings.DisplayOrdersOfAllStores ? 0 : _storeContext.CurrentStore.Id); + + var model = new CustomerOrderListModel(); model.NavigationModel = GetCustomerNavigationModel(customer); model.NavigationModel.SelectedTab = CustomerNavigationEnum.Orders; - var orders = _orderService.SearchOrders(_storeContext.CurrentStore.Id, customer.Id, - null, null, null, null, null, null, null, null, 0, int.MaxValue); - foreach (var order in orders) - { - var orderModel = new CustomerOrderListModel.OrderDetailsModel() - { - Id = order.Id, - OrderNumber = order.GetOrderNumber(), - CreatedOn = _dateTimeHelper.ConvertToUserTime(order.CreatedOnUtc, DateTimeKind.Utc), - OrderStatus = order.OrderStatus.GetLocalizedEnum(_localizationService, _workContext), - IsReturnRequestAllowed = _orderProcessingService.IsReturnRequestAllowed(order) - }; - var orderTotalInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderTotal, order.CurrencyRate); - orderModel.OrderTotal = _priceFormatter.FormatPrice(orderTotalInCustomerCurrency, true, order.CustomerCurrencyCode, false, _workContext.WorkingLanguage); - model.Orders.Add(orderModel); - } + var orders = _orderService.SearchOrders(storeScope, customer.Id, null, null, null, null, null, null, null, null, pageIndex, _orderSettings.OrderListPageSize); + + var orderModels = orders + .Select(x => + { + var orderModel = new CustomerOrderListModel.OrderDetailsModel + { + Id = x.Id, + OrderNumber = x.GetOrderNumber(), + CreatedOn = _dateTimeHelper.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc), + OrderStatus = x.OrderStatus.GetLocalizedEnum(_localizationService, _workContext), + IsReturnRequestAllowed = _orderProcessingService.IsReturnRequestAllowed(x) + }; + + var orderTotalInCustomerCurrency = _currencyService.ConvertCurrency(x.OrderTotal, x.CurrencyRate); + orderModel.OrderTotal = _priceFormatter.FormatPrice(orderTotalInCustomerCurrency, true, x.CustomerCurrencyCode, false, _workContext.WorkingLanguage); + + return orderModel; + }) + .ToList(); + + model.Orders = new PagedList(orderModels, orders.PageIndex, orders.PageSize, orders.TotalCount); + var recurringPayments = _orderService.SearchRecurringPayments(_storeContext.CurrentStore.Id, customer.Id, 0, null); foreach (var recurringPayment in recurringPayments) { - var recurringPaymentModel = new CustomerOrderListModel.RecurringOrderModel() + var recurringPaymentModel = new CustomerOrderListModel.RecurringOrderModel { Id = recurringPayment.Id, StartDate = _dateTimeHelper.ConvertToUserTime(recurringPayment.StartDateUtc, DateTimeKind.Utc).ToString(), @@ -457,10 +467,7 @@ public ActionResult Login(LoginModel model, string returnUrl, bool captchaValid) //activity log _customerActivityService.InsertActivity("PublicStore.Login", _localizationService.GetResource("ActivityLog.PublicStore.Login"), customer); - if (!String.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl)) - return Redirect(returnUrl); - else - return RedirectToRoute("HomePage"); + return RedirectToReferrer(returnUrl); } else { @@ -486,6 +493,7 @@ public ActionResult Register() foreach (var tzi in _dateTimeHelper.GetSystemTimeZones()) model.AvailableTimeZones.Add(new SelectListItem() { Text = tzi.DisplayName, Value = tzi.Id, Selected = (tzi.Id == _dateTimeHelper.DefaultStoreTimeZone.Id) }); model.DisplayVatNumber = _taxSettings.EuVatEnabled; + model.VatRequired = _taxSettings.VatRequired; //form fields model.GenderEnabled = _customerSettings.GenderEnabled; model.DateOfBirthEnabled = _customerSettings.DateOfBirthEnabled; @@ -573,12 +581,12 @@ public ActionResult Register(RegisterModel model, string returnUrl, bool captcha var registrationResult = _customerRegistrationService.RegisterCustomer(registrationRequest); if (registrationResult.Success) { - //properties + // properties if (_dateTimeSettings.AllowCustomersToSetTimeZone) { _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.TimeZoneId, model.TimeZoneId); } - //VAT number + // VAT number if (_taxSettings.EuVatEnabled) { _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.VatNumber, model.VatNumber); @@ -589,12 +597,12 @@ public ActionResult Register(RegisterModel model, string returnUrl, bool captcha _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.VatNumberStatusId, (int)vatNumberStatus); - //send VAT number admin notification + // send VAT number admin notification if (!String.IsNullOrEmpty(model.VatNumber) && _taxSettings.EuVatEmailAdminWhenNewVatSubmitted) _workflowMessageService.SendNewVatSubmittedStoreOwnerNotification(customer, model.VatNumber, vatAddress, _localizationSettings.DefaultAdminLanguageId); } - //form fields + // form fields if (_customerSettings.GenderEnabled) _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.Gender, model.Gender); _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.FirstName, model.FirstName); @@ -630,10 +638,10 @@ public ActionResult Register(RegisterModel model, string returnUrl, bool captcha if (_customerSettings.CustomerNumberMethod == CustomerNumberMethod.AutomaticallySet && String.IsNullOrEmpty(customer.GetAttribute(SystemCustomerAttributeNames.CustomerNumber))) _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.CustomerNumber, customer.Id); - //newsletter + // newsletter if (_customerSettings.NewsletterEnabled) { - //save newsletter value + // save newsletter value var newsletter = _newsLetterSubscriptionService.GetNewsLetterSubscriptionByEmail(model.Email, _storeContext.CurrentStore.Id); if (newsletter != null) { @@ -712,11 +720,10 @@ public ActionResult Register(RegisterModel model, string returnUrl, bool captcha { case UserRegistrationType.EmailValidation: { - //email validation message + // email validation message _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.AccountActivationToken, Guid.NewGuid().ToString()); _workflowMessageService.SendCustomerEmailValidationMessage(customer, _workContext.WorkingLanguage.Id); - //result return RedirectToRoute("RegisterResult", new { resultId = (int)UserRegistrationType.EmailValidation }); } case UserRegistrationType.AdminApproval: @@ -725,8 +732,8 @@ public ActionResult Register(RegisterModel model, string returnUrl, bool captcha } case UserRegistrationType.Standard: { - //send customer welcome message - _workflowMessageService.SendCustomerWelcomeMessage(customer, _workContext.WorkingLanguage.Id); + // send customer welcome message + _workflowMessageService.SendCustomerWelcomeMessage(customer, _workContext.WorkingLanguage.Id); var redirectUrl = Url.RouteUrl("RegisterResult", new { resultId = (int)UserRegistrationType.Standard }); if (!String.IsNullOrEmpty(returnUrl)) @@ -751,6 +758,7 @@ public ActionResult Register(RegisterModel model, string returnUrl, bool captcha foreach (var tzi in _dateTimeHelper.GetSystemTimeZones()) model.AvailableTimeZones.Add(new SelectListItem() { Text = tzi.DisplayName, Value = tzi.Id, Selected = (tzi.Id == _dateTimeHelper.DefaultStoreTimeZone.Id) }); model.DisplayVatNumber = _taxSettings.EuVatEnabled; + model.VatRequired = _taxSettings.VatRequired; //form fields model.GenderEnabled = _customerSettings.GenderEnabled; model.DateOfBirthEnabled = _customerSettings.DateOfBirthEnabled; @@ -821,10 +829,8 @@ public ActionResult RegisterResult(int resultId) default: break; } - var model = new RegisterResultModel - { - Result = resultText - }; + + var model = new RegisterResultModel { Result = resultText }; return View(model); } @@ -909,8 +915,8 @@ public ActionResult AccountActivation(string token, string email) _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.AccountActivationToken, ""); //send welcome message _workflowMessageService.SendCustomerWelcomeMessage(customer, _workContext.WorkingLanguage.Id); - - var model = new AccountActivationModel(); + + var model = new AccountActivationModel(); model.Result = _localizationService.GetResource("Account.AccountActivation.Activated"); return View(model); } @@ -972,8 +978,7 @@ public ActionResult Info(CustomerInfoModel model) if (ModelState.IsValid) { //username - if (_customerSettings.UsernamesEnabled && - this._customerSettings.AllowUsersToChangeUsernames) + if (_customerSettings.UsernamesEnabled && _customerSettings.AllowUsersToChangeUsernames) { if (!customer.Username.Equals(model.Username.Trim(), StringComparison.InvariantCultureIgnoreCase)) { @@ -1033,7 +1038,7 @@ public ActionResult Info(CustomerInfoModel model) if (model.CustomerNumber != currentCustomerNumber && customerNumbers.Where(x => x.Value == model.CustomerNumber).Any()) { - this.NotifyError("Common.CustomerNumberAlreadyExists"); + NotifyError("Common.CustomerNumberAlreadyExists"); } else { @@ -1076,38 +1081,13 @@ public ActionResult Info(CustomerInfoModel model) //newsletter if (_customerSettings.NewsletterEnabled) { - //save newsletter value - var newsletter = _newsLetterSubscriptionService.GetNewsLetterSubscriptionByEmail(customer.Email, _storeContext.CurrentStore.Id); - if (newsletter != null) - { - if (model.Newsletter) - { - newsletter.Active = true; - _newsLetterSubscriptionService.UpdateNewsLetterSubscription(newsletter); - } - else - { - _newsLetterSubscriptionService.DeleteNewsLetterSubscription(newsletter); - } - } - else - { - if (model.Newsletter) - { - _newsLetterSubscriptionService.InsertNewsLetterSubscription(new NewsLetterSubscription() - { - NewsLetterSubscriptionGuid = Guid.NewGuid(), - Email = customer.Email, - Active = true, - CreatedOnUtc = DateTime.UtcNow, - StoreId = _storeContext.CurrentStore.Id - }); - } - } + _newsLetterSubscriptionService.AddNewsLetterSubscriptionFor(model.Newsletter, customer.Email, _storeContext.CurrentStore.Id); } - if (_forumSettings.ForumsEnabled && _forumSettings.SignaturesEnabled) - _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.Signature, model.Signature); + if (_forumSettings.ForumsEnabled && _forumSettings.SignaturesEnabled) + { + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.Signature, model.Signature); + } return RedirectToAction("Info"); } @@ -1281,13 +1261,13 @@ public ActionResult AddressEdit(CustomerAddressEditModel model, int id) #region Orders [RequireHttpsByConfigAttribute(SslRequirement.Yes)] - public ActionResult Orders() + public ActionResult Orders(int? page) { if (!IsCurrentUserRegistered()) return new HttpUnauthorizedResult(); - var customer = _workContext.CurrentCustomer; - var model = PrepareCustomerOrderListModel(customer); + var model = PrepareCustomerOrderListModel(_workContext.CurrentCustomer, Math.Max((page ?? 0) - 1, 0)); + return View(model); } @@ -1300,9 +1280,13 @@ public ActionResult CancelRecurringPayment(FormCollection form) //get recurring payment identifier int recurringPaymentId = 0; - foreach (var formValue in form.AllKeys) - if (formValue.StartsWith("cancelRecurringPayment", StringComparison.InvariantCultureIgnoreCase)) - recurringPaymentId = Convert.ToInt32(formValue.Substring("cancelRecurringPayment".Length)); + foreach (var formValue in form.AllKeys) + { + if (formValue.StartsWith("cancelRecurringPayment", StringComparison.InvariantCultureIgnoreCase)) + { + recurringPaymentId = Convert.ToInt32(formValue.Substring("cancelRecurringPayment".Length)); + } + } var recurringPayment = _orderService.GetRecurringPaymentById(recurringPaymentId); if (recurringPayment == null) @@ -1315,16 +1299,14 @@ public ActionResult CancelRecurringPayment(FormCollection form) { var errors = _orderProcessingService.CancelRecurringPayment(recurringPayment); - var model = PrepareCustomerOrderListModel(customer); + var model = PrepareCustomerOrderListModel(customer, 0); model.CancelRecurringPaymentErrors = errors; return View(model); } - else - { - return RedirectToAction("Orders"); - } - } + + return RedirectToAction("Orders"); + } #endregion @@ -1586,30 +1568,34 @@ public ActionResult UploadAvatar(CustomerAvatarModel model, HttpPostedFileBase u try { var customerAvatar = _pictureService.GetPictureById(customer.GetAttribute(SystemCustomerAttributeNames.AvatarPictureId)); - if ((uploadedFile != null) && (!String.IsNullOrEmpty(uploadedFile.FileName))) - { - int avatarMaxSize = _customerSettings.AvatarMaximumSizeBytes; - if (uploadedFile.ContentLength > avatarMaxSize) - throw new SmartException(string.Format(_localizationService.GetResource("Account.Avatar.MaximumUploadedFileSize"), Prettifier.BytesToString(avatarMaxSize))); - byte[] customerPictureBinary = uploadedFile.InputStream.ToByteArray(); - if (customerAvatar != null) - customerAvatar = _pictureService.UpdatePicture(customerAvatar.Id, customerPictureBinary, uploadedFile.ContentType, null, true); - else - customerAvatar = _pictureService.InsertPicture(customerPictureBinary, uploadedFile.ContentType, null, true, false); - } + if ((uploadedFile != null) && (!String.IsNullOrEmpty(uploadedFile.FileName))) + { + var avatarMaxSize = _customerSettings.AvatarMaximumSizeBytes; + + if (uploadedFile.ContentLength > avatarMaxSize) + throw new SmartException(T("Account.Avatar.MaximumUploadedFileSize", Prettifier.BytesToString(avatarMaxSize))); - int customerAvatarId = 0; - if (customerAvatar != null) - customerAvatarId = customerAvatar.Id; + byte[] customerPictureBinary = uploadedFile.InputStream.ToByteArray(); + + if (customerAvatar != null) + customerAvatar = _pictureService.UpdatePicture(customerAvatar.Id, customerPictureBinary, uploadedFile.ContentType, null, true); + else + customerAvatar = _pictureService.InsertPicture(customerPictureBinary, uploadedFile.ContentType, null, true, false); + } + else if (customerAvatar != null) + { + _pictureService.DeletePicture(customerAvatar); + customerAvatar = null; + } + + var customerAvatarId = (customerAvatar != null ? customerAvatar.Id : 0); _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.AvatarPictureId, customerAvatarId); - model.AvatarUrl = _pictureService.GetPictureUrl( - customerAvatarId, - _mediaSettings.AvatarPictureSize, - false); - return View(model); + model.AvatarUrl = _pictureService.GetPictureUrl(customerAvatarId, _mediaSettings.AvatarPictureSize, false); + + return View(model); } catch (Exception exc) { @@ -1617,12 +1603,8 @@ public ActionResult UploadAvatar(CustomerAvatarModel model, HttpPostedFileBase u } } - //If we got this far, something failed, redisplay form - model.AvatarUrl = _pictureService.GetPictureUrl( - customer.GetAttribute(SystemCustomerAttributeNames.AvatarPictureId), - _mediaSettings.AvatarPictureSize, - false); + model.AvatarUrl = _pictureService.GetPictureUrl(customer.GetAttribute(SystemCustomerAttributeNames.AvatarPictureId), _mediaSettings.AvatarPictureSize, false); return View(model); } @@ -1645,6 +1627,7 @@ public ActionResult RemoveAvatar(CustomerAvatarModel model, HttpPostedFileBase u var customerAvatar = _pictureService.GetPictureById(customer.GetAttribute(SystemCustomerAttributeNames.AvatarPictureId)); if (customerAvatar != null) _pictureService.DeletePicture(customerAvatar); + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.AvatarPictureId, 0); return RedirectToAction("Avatar"); diff --git a/src/Presentation/SmartStore.Web/Controllers/DownloadController.cs b/src/Presentation/SmartStore.Web/Controllers/DownloadController.cs index 99cc481d18..857b877aa3 100644 --- a/src/Presentation/SmartStore.Web/Controllers/DownloadController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/DownloadController.cs @@ -1,7 +1,11 @@ using System; +using System.Web; using System.Web.Mvc; using SmartStore.Core; +using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.Html; using SmartStore.Services.Catalog; using SmartStore.Services.Media; using SmartStore.Services.Orders; @@ -9,7 +13,7 @@ namespace SmartStore.Web.Controllers { - public partial class DownloadController : PublicControllerBase + public partial class DownloadController : PublicControllerBase { private readonly IDownloadService _downloadService; private readonly IProductService _productService; @@ -18,8 +22,12 @@ public partial class DownloadController : PublicControllerBase private readonly CustomerSettings _customerSettings; - public DownloadController(IDownloadService downloadService, IProductService productService, - IOrderService orderService, IWorkContext workContext, CustomerSettings customerSettings) + public DownloadController( + IDownloadService downloadService, + IProductService productService, + IOrderService orderService, + IWorkContext workContext, + CustomerSettings customerSettings) { this._downloadService = downloadService; this._productService = productService; @@ -28,32 +36,40 @@ public DownloadController(IDownloadService downloadService, IProductService prod this._customerSettings = customerSettings; } + private ActionResult GetFileContentResultFor(Download download, Product product) + { + if (download.DownloadBinary == null) + { + return Content(T("Common.Download.NoDataAvailable")); + } + + var id = (product != null ? product.Id : download.Id); + var fileName = !String.IsNullOrWhiteSpace(download.Filename) ? download.Filename : id.ToString(); + var contentType = !String.IsNullOrWhiteSpace(download.ContentType) ? download.ContentType : "application/octet-stream"; + + return new FileContentResult(download.DownloadBinary, contentType) + { + FileDownloadName = fileName + download.Extension + }; + } + public ActionResult Sample(int id /* productId */) { var product = _productService.GetProductById(id); if (product == null) return HttpNotFound(); - if (!product.HasSampleDownload) - return Content("Product variant doesn't have a sample download."); + if (!product.HasSampleDownload) + return Content(T("Common.Download.HasNoSample")); var download = _downloadService.GetDownloadById(product.SampleDownloadId.GetValueOrDefault()); if (download == null) - return Content("Sample download is not available any more."); + return Content(T("Common.Download.SampleNotAvailable")); if (download.UseDownloadUrl) - { return new RedirectResult(download.DownloadUrl); - } - else - { - if (download.DownloadBinary == null) - return Content("Download data is not available any more."); - string fileName = !String.IsNullOrWhiteSpace(download.Filename) ? download.Filename : product.Id.ToString(); - string contentType = !String.IsNullOrWhiteSpace(download.ContentType) ? download.ContentType : "application/octet-stream"; - return new FileContentResult(download.DownloadBinary, contentType) { FileDownloadName = fileName + download.Extension }; - } + return GetFileContentResultFor(download, product); } public ActionResult GetDownload(Guid id /* orderItemId */, bool agree = false) @@ -68,7 +84,7 @@ public ActionResult GetDownload(Guid id /* orderItemId */, bool agree = false) var order = orderItem.Order; var product = orderItem.Product; if (!_downloadService.IsDownloadAllowed(orderItem)) - return Content("Downloads are not allowed"); + return Content(T("Common.Download.NotAllowed")); if (_customerSettings.DownloadableProductsValidateUser) { @@ -76,46 +92,35 @@ public ActionResult GetDownload(Guid id /* orderItemId */, bool agree = false) return new HttpUnauthorizedResult(); if (order.CustomerId != _workContext.CurrentCustomer.Id) - return Content("This is not your order"); + return Content(T("Account.CustomerOrders.NotYourOrder")); } var download = _downloadService.GetDownloadById(product.DownloadId); if (download == null) - return Content("Download is not available any more."); - - if (product.HasUserAgreement) - { - if (!agree) - return RedirectToAction("UserAgreement", "Customer", new { id = id }); - } + return Content(T("Common.Download.NoDataAvailable")); + if (product.HasUserAgreement && !agree) + return RedirectToAction("UserAgreement", "Customer", new { id = id }); if (!product.UnlimitedDownloads && orderItem.DownloadCount >= product.MaxNumberOfDownloads) - return Content(string.Format("You have reached maximum number of downloads {0}", product.MaxNumberOfDownloads)); + return Content(T("Common.Download.MaxNumberReached", product.MaxNumberOfDownloads)); - if (download.UseDownloadUrl) { - //increase download orderItem.DownloadCount++; _orderService.UpdateOrder(order); - //return result return new RedirectResult(download.DownloadUrl); } else { if (download.DownloadBinary == null) - return Content("Download data is not available any more."); + return Content(T("Common.Download.NoDataAvailable")); - //increase download orderItem.DownloadCount++; _orderService.UpdateOrder(order); - //return result - string fileName = !String.IsNullOrWhiteSpace(download.Filename) ? download.Filename : product.Id.ToString(); - string contentType = !String.IsNullOrWhiteSpace(download.ContentType) ? download.ContentType : "application/octet-stream"; - return new FileContentResult(download.DownloadBinary, contentType) { FileDownloadName = fileName + download.Extension }; + return GetFileContentResultFor(download, product); } } @@ -130,8 +135,9 @@ public ActionResult GetLicense(Guid id /* orderItemId */) var order = orderItem.Order; var product = orderItem.Product; + if (!_downloadService.IsLicenseDownloadAllowed(orderItem)) - return Content("Downloads are not allowed"); + return Content(T("Common.Download.NotAllowed")); if (_customerSettings.DownloadableProductsValidateUser) { @@ -139,52 +145,49 @@ public ActionResult GetLicense(Guid id /* orderItemId */) return new HttpUnauthorizedResult(); if (order.CustomerId != _workContext.CurrentCustomer.Id) - return Content("This is not your order"); + return Content(T("Account.CustomerOrders.NotYourOrder")); } var download = _downloadService.GetDownloadById(orderItem.LicenseDownloadId.HasValue ? orderItem.LicenseDownloadId.Value : 0); if (download == null) - return Content("Download is not available any more."); + return Content(T("Common.Download.NotAvailable")); if (download.UseDownloadUrl) - { - //return result return new RedirectResult(download.DownloadUrl); - } - else - { - if (download.DownloadBinary == null) - return Content("Download data is not available any more."); - - //return result - string fileName = !String.IsNullOrWhiteSpace(download.Filename) ? download.Filename : product.Id.ToString(); - string contentType = !String.IsNullOrWhiteSpace(download.ContentType) ? download.ContentType : "application/octet-stream"; - return new FileContentResult(download.DownloadBinary, contentType) { FileDownloadName = fileName + download.Extension }; - } - } + + return GetFileContentResultFor(download, product); + } public ActionResult GetFileUpload(Guid downloadId) { var download = _downloadService.GetDownloadByGuid(downloadId); if (download == null) - return Content("Download is not available any more."); + return Content(T("Common.Download.NotAvailable")); if (download.UseDownloadUrl) - { - //return result return new RedirectResult(download.DownloadUrl); - } - else - { - if (download.DownloadBinary == null) - return Content("Download data is not available any more."); - //return result - string fileName = !String.IsNullOrWhiteSpace(download.Filename) ? download.Filename : downloadId.ToString(); - string contentType = !String.IsNullOrWhiteSpace(download.ContentType) ? download.ContentType : "application/octet-stream"; - return new FileContentResult(download.DownloadBinary, contentType) { FileDownloadName = fileName + download.Extension }; - } - } + return GetFileContentResultFor(download, null); + } + + public ActionResult GetUserAgreement(int productId, bool? asPlainText) + { + var product = _productService.GetProductById(productId); + if (product == null) + return Content(T("Products.NotFound", productId)); + + if (!product.IsDownload || !product.HasUserAgreement || product.UserAgreementText.IsEmpty()) + return Content(T("DownloadableProducts.HasNoUserAgreement")); + + if (asPlainText ?? false) + { + var agreement = HtmlUtils.ConvertHtmlToPlainText(product.UserAgreementText); + agreement = HtmlUtils.StripTags(HttpUtility.HtmlDecode(agreement)); + + return Content(agreement); + } - } + return Content(product.UserAgreementText); + } + } } diff --git a/src/Presentation/SmartStore.Web/Controllers/ExternalAuthenticationController.cs b/src/Presentation/SmartStore.Web/Controllers/ExternalAuthenticationController.cs index f95f0ef4d2..3fc35cd8db 100644 --- a/src/Presentation/SmartStore.Web/Controllers/ExternalAuthenticationController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/ExternalAuthenticationController.cs @@ -31,10 +31,10 @@ public ExternalAuthenticationController(IOpenAuthenticationService openAuthentic #region Methods - public RedirectResult RemoveParameterAssociation(string returnUrl) + public ActionResult RemoveParameterAssociation(string returnUrl) { ExternalAuthorizerHelper.RemoveParameters(); - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } [ChildActionOnly] diff --git a/src/Presentation/SmartStore.Web/Controllers/FilterController.cs b/src/Presentation/SmartStore.Web/Controllers/FilterController.cs index 4b069a06f3..e122492826 100644 --- a/src/Presentation/SmartStore.Web/Controllers/FilterController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/FilterController.cs @@ -20,7 +20,7 @@ public FilterController(IFilterService filterService, CatalogSettings catalogSet private bool IsShowAllText(ICollection criteriaGroup) { - if (criteriaGroup.Any(c => c.Entity == FilterService.ShortcutPrice)) + if (criteriaGroup.Any(c => c.Entity.IsCaseInsensitiveEqual(FilterService.ShortcutPrice))) return false; return (criteriaGroup.Count >= _catalogSettings.MaxFilterItemsToDisplay || criteriaGroup.Any(c => !c.IsInactive)); diff --git a/src/Presentation/SmartStore.Web/Controllers/HomeController.cs b/src/Presentation/SmartStore.Web/Controllers/HomeController.cs index 6b65cfaf2d..58e6eb4ace 100644 --- a/src/Presentation/SmartStore.Web/Controllers/HomeController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/HomeController.cs @@ -6,6 +6,7 @@ using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Cms; using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Messages; using SmartStore.Core.Infrastructure; using SmartStore.Core.Localization; @@ -41,6 +42,7 @@ public partial class HomeController : PublicControllerBase private readonly Lazy _sitemapGenerator; private readonly Lazy _captchaSettings; private readonly Lazy _commonSettings; + private readonly Lazy _customerSettings; #endregion @@ -56,7 +58,8 @@ public HomeController( Lazy emailAccountService, Lazy sitemapGenerator, Lazy captchaSettings, - Lazy commonSettings) + Lazy commonSettings, + Lazy customerSettings) { this._services = services; this._categoryService = categoryService; @@ -68,6 +71,7 @@ public HomeController( this._sitemapGenerator = sitemapGenerator; this._captchaSettings = captchaSettings; this._commonSettings = commonSettings; + this._customerSettings = customerSettings; } #endregion @@ -86,7 +90,7 @@ public ActionResult ContentSlider() var settings = _services.Settings.LoadSetting(); settings.BackgroundPictureUrl = pictureService.GetPictureUrl(settings.BackgroundPictureId, 0, false); - + var slides = settings.Slides .Where(s => s.LanguageCulture == _services.WorkContext.WorkingLanguage.LanguageCulture && @@ -119,7 +123,9 @@ public ActionResult ContactUs() { Email = _services.WorkContext.CurrentCustomer.Email, FullName = _services.WorkContext.CurrentCustomer.GetFullName(), - DisplayCaptcha = _captchaSettings.Value.Enabled && _captchaSettings.Value.ShowOnContactUsPage + DisplayCaptcha = _captchaSettings.Value.Enabled && _captchaSettings.Value.ShowOnContactUsPage, + DisplayPrivacyAgreement = _customerSettings.Value.DisplayPrivacyAgreementOnContactUs + }; return View(model); diff --git a/src/Presentation/SmartStore.Web/Controllers/InstallController.cs b/src/Presentation/SmartStore.Web/Controllers/InstallController.cs index e0bef97946..7c5a919eb0 100644 --- a/src/Presentation/SmartStore.Web/Controllers/InstallController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/InstallController.cs @@ -1,33 +1,28 @@ -using Autofac; -using System; +using System; using System.Collections.Generic; +using System.Data.Entity; using System.Data.SqlClient; using System.Linq; using System.Security.Principal; using System.Threading; using System.Threading.Tasks; -using System.Web; -using System.Web.SessionState; -using System.Web.Caching; using System.Web.Hosting; using System.Web.Mvc; -using System.ComponentModel.Composition; +using System.Web.SessionState; +using Autofac; using SmartStore.Core; -using SmartStore.Core.Caching; +using SmartStore.Core.Async; using SmartStore.Core.Data; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Infrastructure; using SmartStore.Core.Plugins; +using SmartStore.Data; +using SmartStore.Data.Setup; using SmartStore.Services.Security; +using SmartStore.Utilities; using SmartStore.Web.Framework.Security; using SmartStore.Web.Infrastructure.Installation; using SmartStore.Web.Models.Install; -using SmartStore.Core.Async; -using System.Data.Entity; -using SmartStore.Data; -using SmartStore.Data.Setup; -using System.Configuration; -using SmartStore.Utilities; namespace SmartStore.Web.Controllers { @@ -182,8 +177,8 @@ public ActionResult Index() if (DataSettings.DatabaseIsInstalled()) return RedirectToRoute("HomePage"); - //set page timeout to 5 minutes - this.Server.ScriptTimeout = 300; + // set page timeout to 10 minutes + this.Server.ScriptTimeout = 600; var model = new InstallModel { @@ -480,7 +475,7 @@ protected virtual InstallationResult InstallCore(ILifetimeScope scope, InstallMo { return UpdateResult(x => { - x.Errors.Add(string.Format("The install language '{0}' is not registered", model.PrimaryLanguage)); + x.Errors.Add(_locService.GetResource("Install.LanguageNotRegistered").FormatInvariant(model.PrimaryLanguage)); x.Completed = true; x.Success = false; x.RedirectUrl = null; diff --git a/src/Presentation/SmartStore.Web/Controllers/NewsController.cs b/src/Presentation/SmartStore.Web/Controllers/NewsController.cs index 2234247d80..b285065191 100644 --- a/src/Presentation/SmartStore.Web/Controllers/NewsController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/NewsController.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.ServiceModel.Syndication; using System.Web.Mvc; using SmartStore.Core; using SmartStore.Core.Caching; @@ -17,8 +19,10 @@ using SmartStore.Services.News; using SmartStore.Services.Seo; using SmartStore.Services.Stores; +using SmartStore.Utilities; using SmartStore.Web.Framework.Controllers; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Filters; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.UI.Captcha; using SmartStore.Web.Infrastructure.Cache; @@ -43,6 +47,7 @@ public partial class NewsController : PublicControllerBase private readonly ICacheManager _cacheManager; private readonly ICustomerActivityService _customerActivityService; private readonly IStoreMappingService _storeMappingService; + private readonly ILanguageService _languageService; private readonly MediaSettings _mediaSettings; private readonly NewsSettings _newsSettings; @@ -61,6 +66,7 @@ public NewsController(INewsService newsService, IWorkflowMessageService workflowMessageService, IWebHelper webHelper, ICacheManager cacheManager, ICustomerActivityService customerActivityService, IStoreMappingService storeMappingService, + ILanguageService languageService, MediaSettings mediaSettings, NewsSettings newsSettings, LocalizationSettings localizationSettings, CustomerSettings customerSettings, CaptchaSettings captchaSettings) @@ -77,6 +83,7 @@ public NewsController(INewsService newsService, this._cacheManager = cacheManager; this._customerActivityService = customerActivityService; this._storeMappingService = storeMappingService; + this._languageService = languageService; this._mediaSettings = mediaSettings; this._newsSettings = newsSettings; @@ -208,7 +215,38 @@ public ActionResult List(NewsPagingFilteringModel command) [ActionName("rss"), Compress] public ActionResult ListRss(int languageId) { - var feed = _newsService.CreateRssFeed(Url, languageId); + DateTime? maxAge = null; + var protocol = _webHelper.IsCurrentConnectionSecured() ? "https" : "http"; + var selfLink = Url.Action("rss", "News", new { languageId = languageId }, protocol); + var newsLink = Url.RouteUrl("NewsArchive", null, protocol); + + var title = "{0} - News".FormatInvariant(_storeContext.CurrentStore.Name); + + if (_newsSettings.MaxAgeInDays > 0) + maxAge = DateTime.UtcNow.Subtract(new TimeSpan(_newsSettings.MaxAgeInDays, 0, 0, 0)); + + var language = _languageService.GetLanguageById(languageId); + var feed = new SmartSyndicationFeed(new Uri(newsLink), title); + + feed.AddNamespaces(true); + feed.Init(selfLink, language); + + if (!_newsSettings.Enabled) + return new RssActionResult { Feed = feed }; + + var items = new List(); + var newsItems = _newsService.GetAllNews(languageId, _storeContext.CurrentStore.Id, 0, int.MaxValue, false, maxAge); + + foreach (var news in newsItems) + { + var newsUrl = Url.RouteUrl("NewsItem", new { SeName = news.GetSeName(news.LanguageId, ensureTwoPublishedLanguages: false) }, "http"); + + var item = feed.CreateItem(news.Title, news.Short, newsUrl, news.CreatedOnUtc, news.Full); + + items.Add(item); + } + + feed.Items = items; return new RssActionResult { Feed = feed }; } @@ -281,13 +319,11 @@ public ActionResult NewsCommentAdd(int newsItemId, NewsItemModel model, bool cap //activity log _customerActivityService.InsertActivity("PublicStore.AddNewsComment", _localizationService.GetResource("ActivityLog.PublicStore.AddNewsComment")); - //The text boxes should be cleared after a comment has been posted - //That' why we reload the page - TempData["sm.news.addcomment.result"] = _localizationService.GetResource("News.Comments.SuccessfullyAdded"); + NotifySuccess(T("News.Comments.SuccessfullyAdded")); + return RedirectToRoute("NewsItem", new { SeName = newsItem.GetSeName(newsItem.LanguageId, ensureTwoPublishedLanguages: false) }); } - //If we got this far, something failed, redisplay form PrepareNewsItemModel(model, newsItem, true); return View(model); diff --git a/src/Presentation/SmartStore.Web/Controllers/NewsletterController.cs b/src/Presentation/SmartStore.Web/Controllers/NewsletterController.cs index ad148aeb33..d41e2a40b0 100644 --- a/src/Presentation/SmartStore.Web/Controllers/NewsletterController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/NewsletterController.cs @@ -3,16 +3,14 @@ using SmartStore.Core; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Messages; -using SmartStore.Services.Localization; using SmartStore.Services.Messages; -using SmartStore.Web.Models.Newsletter; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Models.Newsletter; namespace SmartStore.Web.Controllers { - public partial class NewsletterController : PublicControllerBase + public partial class NewsletterController : PublicControllerBase { - private readonly ILocalizationService _localizationService; private readonly IWorkContext _workContext; private readonly INewsLetterSubscriptionService _newsLetterSubscriptionService; private readonly IWorkflowMessageService _workflowMessageService; @@ -20,12 +18,13 @@ public partial class NewsletterController : PublicControllerBase private readonly CustomerSettings _customerSettings; - public NewsletterController(ILocalizationService localizationService, - IWorkContext workContext, INewsLetterSubscriptionService newsLetterSubscriptionService, - IWorkflowMessageService workflowMessageService, CustomerSettings customerSettings, + public NewsletterController( + IWorkContext workContext, + INewsLetterSubscriptionService newsLetterSubscriptionService, + IWorkflowMessageService workflowMessageService, + CustomerSettings customerSettings, IStoreContext storeContext) { - this._localizationService = localizationService; this._workContext = workContext; this._newsLetterSubscriptionService = newsLetterSubscriptionService; this._workflowMessageService = workflowMessageService; @@ -47,56 +46,59 @@ public ActionResult NewsletterBox() public ActionResult Subscribe(bool subscribe, string email) { string result; - bool success = false; + var success = false; if (!email.IsEmail()) - result = _localizationService.GetResource("Newsletter.Email.Wrong"); - else - { - //subscribe/unsubscribe - email = email.Trim(); - - var subscription = _newsLetterSubscriptionService.GetNewsLetterSubscriptionByEmail(email, _storeContext.CurrentStore.Id); - if (subscription != null) - { - if (subscribe) - { - if (!subscription.Active) - { - _workflowMessageService.SendNewsLetterSubscriptionActivationMessage(subscription, _workContext.WorkingLanguage.Id); - } - result = _localizationService.GetResource("Newsletter.SubscribeEmailSent"); - } - else - { - if (subscription.Active) - { - _workflowMessageService.SendNewsLetterSubscriptionDeactivationMessage(subscription, _workContext.WorkingLanguage.Id); - } - result = _localizationService.GetResource("Newsletter.UnsubscribeEmailSent"); - } - } - else if (subscribe) - { - subscription = new NewsLetterSubscription() - { - NewsLetterSubscriptionGuid = Guid.NewGuid(), - Email = email, - Active = false, - CreatedOnUtc = DateTime.UtcNow, + { + result = T("Newsletter.Email.Wrong"); + } + else + { + //subscribe/unsubscribe + email = email.Trim(); + + var subscription = _newsLetterSubscriptionService.GetNewsLetterSubscriptionByEmail(email, _storeContext.CurrentStore.Id); + if (subscription != null) + { + if (subscribe) + { + if (!subscription.Active) + { + _workflowMessageService.SendNewsLetterSubscriptionActivationMessage(subscription, _workContext.WorkingLanguage.Id); + } + result = T("Newsletter.SubscribeEmailSent"); + } + else + { + if (subscription.Active) + { + _workflowMessageService.SendNewsLetterSubscriptionDeactivationMessage(subscription, _workContext.WorkingLanguage.Id); + } + result = T("Newsletter.UnsubscribeEmailSent"); + } + } + else if (subscribe) + { + subscription = new NewsLetterSubscription + { + NewsLetterSubscriptionGuid = Guid.NewGuid(), + Email = email, + Active = false, + CreatedOnUtc = DateTime.UtcNow, StoreId = _storeContext.CurrentStore.Id - }; - _newsLetterSubscriptionService.InsertNewsLetterSubscription(subscription); - _workflowMessageService.SendNewsLetterSubscriptionActivationMessage(subscription, _workContext.WorkingLanguage.Id); - - result = _localizationService.GetResource("Newsletter.SubscribeEmailSent"); - } - else - { - result = _localizationService.GetResource("Newsletter.UnsubscribeEmailSent"); - } - success = true; - } + }; + + _newsLetterSubscriptionService.InsertNewsLetterSubscription(subscription); + _workflowMessageService.SendNewsLetterSubscriptionActivationMessage(subscription, _workContext.WorkingLanguage.Id); + + result = T("Newsletter.SubscribeEmailSent"); + } + else + { + result = T("Newsletter.UnsubscribeEmailSent"); + } + success = true; + } return Json(new { @@ -108,25 +110,26 @@ public ActionResult Subscribe(bool subscribe, string email) public ActionResult SubscriptionActivation(Guid token, bool active) { var subscription = _newsLetterSubscriptionService.GetNewsLetterSubscriptionByGuid(token); - if (subscription == null) + if (subscription == null) + { return HttpNotFound(); + } var model = new SubscriptionActivationModel(); - if (active) - { - subscription.Active = active; - _newsLetterSubscriptionService.UpdateNewsLetterSubscription(subscription); - } - else - _newsLetterSubscriptionService.DeleteNewsLetterSubscription(subscription); - - if (active) - model.Result = _localizationService.GetResource("Newsletter.ResultActivated"); - else - model.Result = _localizationService.GetResource("Newsletter.ResultDeactivated"); - - return View(model); + if (active) + { + subscription.Active = active; + _newsLetterSubscriptionService.UpdateNewsLetterSubscription(subscription); + } + else + { + _newsLetterSubscriptionService.DeleteNewsLetterSubscription(subscription); + } + + model.Result = T(active ? "Newsletter.ResultActivated" : "Newsletter.ResultDeactivated"); + + return View(model); } } } diff --git a/src/Presentation/SmartStore.Web/Controllers/OrderController.cs b/src/Presentation/SmartStore.Web/Controllers/OrderController.cs index b4b1976c25..448a1983d3 100644 --- a/src/Presentation/SmartStore.Web/Controllers/OrderController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/OrderController.cs @@ -24,6 +24,7 @@ using SmartStore.Services.Shipping; using SmartStore.Services.Stores; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Filters; using SmartStore.Web.Framework.Pdf; using SmartStore.Web.Framework.Plugins; using SmartStore.Web.Framework.Security; @@ -110,6 +111,7 @@ protected OrderDetailsModel PrepareOrderDetailsModel(Order order) throw new ArgumentNullException("order"); var store = _storeService.GetStoreById(order.StoreId) ?? _services.StoreContext.CurrentStore; + var language = _services.WorkContext.WorkingLanguage; var orderSettings = _services.Settings.LoadSetting(store.Id); var catalogSettings = _services.Settings.LoadSetting(store.Id); @@ -183,56 +185,62 @@ protected OrderDetailsModel PrepareOrderDetailsModel(Order order) case TaxDisplayType.ExcludingTax: { //order subtotal - var orderSubtotalExclTaxInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderSubtotalExclTax, order.CurrencyRate); - model.OrderSubtotal = _priceFormatter.FormatPrice(orderSubtotalExclTaxInCustomerCurrency, true, order.CustomerCurrencyCode, _services.WorkContext.WorkingLanguage, false, false); + var orderSubtotalExclTax = _currencyService.ConvertCurrency(order.OrderSubtotalExclTax, order.CurrencyRate); + model.OrderSubtotal = _priceFormatter.FormatPrice(orderSubtotalExclTax, true, order.CustomerCurrencyCode, language, false, false); //discount (applied to order subtotal) - var orderSubTotalDiscountExclTaxInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderSubTotalDiscountExclTax, order.CurrencyRate); - if (orderSubTotalDiscountExclTaxInCustomerCurrency > decimal.Zero) + var orderSubTotalDiscountExclTax = _currencyService.ConvertCurrency(order.OrderSubTotalDiscountExclTax, order.CurrencyRate); + if (orderSubTotalDiscountExclTax > decimal.Zero) { - model.OrderSubTotalDiscount = _priceFormatter.FormatPrice(-orderSubTotalDiscountExclTaxInCustomerCurrency, true, - order.CustomerCurrencyCode, _services.WorkContext.WorkingLanguage, false, false); + model.OrderSubTotalDiscount = _priceFormatter.FormatPrice(-orderSubTotalDiscountExclTax, true, order.CustomerCurrencyCode, language, false, false); } //order shipping - var orderShippingExclTaxInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderShippingExclTax, order.CurrencyRate); - model.OrderShipping = _priceFormatter.FormatShippingPrice(orderShippingExclTaxInCustomerCurrency, true, order.CustomerCurrencyCode, _services.WorkContext.WorkingLanguage, false, false); + var orderShippingExclTax = _currencyService.ConvertCurrency(order.OrderShippingExclTax, order.CurrencyRate); + model.OrderShipping = _priceFormatter.FormatShippingPrice(orderShippingExclTax, true, order.CustomerCurrencyCode, language, false, false); + //payment method additional fee - var paymentMethodAdditionalFeeExclTaxInCustomerCurrency = _currencyService.ConvertCurrency(order.PaymentMethodAdditionalFeeExclTax, order.CurrencyRate); - if (paymentMethodAdditionalFeeExclTaxInCustomerCurrency != decimal.Zero) - model.PaymentMethodAdditionalFee = _priceFormatter.FormatPaymentMethodAdditionalFee(paymentMethodAdditionalFeeExclTaxInCustomerCurrency, true, order.CustomerCurrencyCode, _services.WorkContext.WorkingLanguage, false, false); + var paymentMethodAdditionalFeeExclTax = _currencyService.ConvertCurrency(order.PaymentMethodAdditionalFeeExclTax, order.CurrencyRate); + if (paymentMethodAdditionalFeeExclTax != decimal.Zero) + { + model.PaymentMethodAdditionalFee = _priceFormatter.FormatPaymentMethodAdditionalFee(paymentMethodAdditionalFeeExclTax, true, order.CustomerCurrencyCode, + language, false, false); + } } break; case TaxDisplayType.IncludingTax: { //order subtotal - var orderSubtotalInclTaxInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderSubtotalInclTax, order.CurrencyRate); - model.OrderSubtotal = _priceFormatter.FormatPrice(orderSubtotalInclTaxInCustomerCurrency, true, order.CustomerCurrencyCode, _services.WorkContext.WorkingLanguage, true, false); + var orderSubtotalInclTax = _currencyService.ConvertCurrency(order.OrderSubtotalInclTax, order.CurrencyRate); + model.OrderSubtotal = _priceFormatter.FormatPrice(orderSubtotalInclTax, true, order.CustomerCurrencyCode, language, true, false); //discount (applied to order subtotal) - var orderSubTotalDiscountInclTaxInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderSubTotalDiscountInclTax, order.CurrencyRate); - if (orderSubTotalDiscountInclTaxInCustomerCurrency > decimal.Zero) + var orderSubTotalDiscountInclTax = _currencyService.ConvertCurrency(order.OrderSubTotalDiscountInclTax, order.CurrencyRate); + if (orderSubTotalDiscountInclTax > decimal.Zero) { - model.OrderSubTotalDiscount = _priceFormatter.FormatPrice(-orderSubTotalDiscountInclTaxInCustomerCurrency, true, - order.CustomerCurrencyCode, _services.WorkContext.WorkingLanguage, true, false); + model.OrderSubTotalDiscount = _priceFormatter.FormatPrice(-orderSubTotalDiscountInclTax, true, order.CustomerCurrencyCode, language, true, false); } //order shipping - var orderShippingInclTaxInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderShippingInclTax, order.CurrencyRate); - model.OrderShipping = _priceFormatter.FormatShippingPrice(orderShippingInclTaxInCustomerCurrency, true, order.CustomerCurrencyCode, _services.WorkContext.WorkingLanguage, true, false); + var orderShippingInclTax = _currencyService.ConvertCurrency(order.OrderShippingInclTax, order.CurrencyRate); + model.OrderShipping = _priceFormatter.FormatShippingPrice(orderShippingInclTax, true, order.CustomerCurrencyCode, language, true, false); //payment method additional fee - var paymentMethodAdditionalFeeInclTaxInCustomerCurrency = _currencyService.ConvertCurrency(order.PaymentMethodAdditionalFeeInclTax, order.CurrencyRate); - if (paymentMethodAdditionalFeeInclTaxInCustomerCurrency != decimal.Zero) - model.PaymentMethodAdditionalFee = _priceFormatter.FormatPaymentMethodAdditionalFee(paymentMethodAdditionalFeeInclTaxInCustomerCurrency, true, order.CustomerCurrencyCode, _services.WorkContext.WorkingLanguage, true, false); + var paymentMethodAdditionalFeeInclTax = _currencyService.ConvertCurrency(order.PaymentMethodAdditionalFeeInclTax, order.CurrencyRate); + if (paymentMethodAdditionalFeeInclTax != decimal.Zero) + { + model.PaymentMethodAdditionalFee = _priceFormatter.FormatPaymentMethodAdditionalFee(paymentMethodAdditionalFeeInclTax, true, order.CustomerCurrencyCode, + language, true, false); + } } break; } //tax - bool displayTax = true; - bool displayTaxRates = true; + var displayTax = true; + var displayTaxRates = true; + if (taxSettings.HideTaxInOrderSummary && order.CustomerTaxDisplayType == TaxDisplayType.IncludingTax) { displayTax = false; @@ -251,52 +259,62 @@ protected OrderDetailsModel PrepareOrderDetailsModel(Order order) displayTax = !displayTaxRates; var orderTaxInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderTax, order.CurrencyRate); - //TODO pass languageId to _priceFormatter.FormatPrice - model.Tax = _priceFormatter.FormatPrice(orderTaxInCustomerCurrency, true, order.CustomerCurrencyCode, false, _services.WorkContext.WorkingLanguage); + + model.Tax = _priceFormatter.FormatPrice(orderTaxInCustomerCurrency, true, order.CustomerCurrencyCode, false, language); foreach (var tr in order.TaxRatesDictionary) { var rate = _priceFormatter.FormatTaxRate(tr.Key); - var labelKey = "ShoppingCart.Totals.TaxRateLine" + (_services.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax ? "Incl" : "Excl"); - model.TaxRates.Add(new OrderDetailsModel.TaxRate() + //var labelKey = "ShoppingCart.Totals.TaxRateLine" + (_services.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax ? "Incl" : "Excl"); + var labelKey = (_services.WorkContext.TaxDisplayType == TaxDisplayType.IncludingTax ? "ShoppingCart.Totals.TaxRateLineIncl" : "ShoppingCart.Totals.TaxRateLineExcl"); + + model.TaxRates.Add(new OrderDetailsModel.TaxRate { Rate = rate, Label = T(labelKey).Text.FormatCurrent(rate), - //TODO pass languageId to _priceFormatter.FormatPrice - Value = _priceFormatter.FormatPrice(_currencyService.ConvertCurrency(tr.Value, order.CurrencyRate), true, order.CustomerCurrencyCode, false, _services.WorkContext.WorkingLanguage), + Value = _priceFormatter.FormatPrice(_currencyService.ConvertCurrency(tr.Value, order.CurrencyRate), true, order.CustomerCurrencyCode, false, language), }); } } } + model.DisplayTaxRates = displayTaxRates; model.DisplayTax = displayTax; //discount (applied to order total) var orderDiscountInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderDiscount, order.CurrencyRate); - if (orderDiscountInCustomerCurrency > decimal.Zero) - model.OrderTotalDiscount = _priceFormatter.FormatPrice(-orderDiscountInCustomerCurrency, true, order.CustomerCurrencyCode, false, _services.WorkContext.WorkingLanguage); - + if (orderDiscountInCustomerCurrency > decimal.Zero) + { + model.OrderTotalDiscount = _priceFormatter.FormatPrice(-orderDiscountInCustomerCurrency, true, order.CustomerCurrencyCode, false, language); + } //gift cards foreach (var gcuh in order.GiftCardUsageHistory) { - model.GiftCards.Add(new OrderDetailsModel.GiftCard - { - CouponCode = gcuh.GiftCard.GiftCardCouponCode, - Amount = _priceFormatter.FormatPrice(-(_currencyService.ConvertCurrency(gcuh.UsedValue, order.CurrencyRate)), true, order.CustomerCurrencyCode, false, _services.WorkContext.WorkingLanguage), - }); - } + var remainingAmountBase = gcuh.GiftCard.GetGiftCardRemainingAmount(); + var remainingAmount = _currencyService.ConvertCurrency(remainingAmountBase, order.CurrencyRate); + + var gcModel = new OrderDetailsModel.GiftCard + { + CouponCode = gcuh.GiftCard.GiftCardCouponCode, + Amount = _priceFormatter.FormatPrice(-(_currencyService.ConvertCurrency(gcuh.UsedValue, order.CurrencyRate)), true, order.CustomerCurrencyCode, false, language), + Remaining = _priceFormatter.FormatPrice(remainingAmount, true, false) + }; + + model.GiftCards.Add(gcModel); + } //reward points if (order.RedeemedRewardPointsEntry != null) { model.RedeemedRewardPoints = -order.RedeemedRewardPointsEntry.Points; - model.RedeemedRewardPointsAmount = _priceFormatter.FormatPrice(-(_currencyService.ConvertCurrency(order.RedeemedRewardPointsEntry.UsedAmount, order.CurrencyRate)), true, order.CustomerCurrencyCode, false, _services.WorkContext.WorkingLanguage); + model.RedeemedRewardPointsAmount = _priceFormatter.FormatPrice(-(_currencyService.ConvertCurrency(order.RedeemedRewardPointsEntry.UsedAmount, order.CurrencyRate)), + true, order.CustomerCurrencyCode, false, language); } //total var orderTotalInCustomerCurrency = _currencyService.ConvertCurrency(order.OrderTotal, order.CurrencyRate); - model.OrderTotal = _priceFormatter.FormatPrice(orderTotalInCustomerCurrency, true, order.CustomerCurrencyCode, false, _services.WorkContext.WorkingLanguage); + model.OrderTotal = _priceFormatter.FormatPrice(orderTotalInCustomerCurrency, true, order.CustomerCurrencyCode, false, language); //checkout attributes model.CheckoutAttributeInfo = HtmlUtils.ConvertPlainTextToTable(HtmlUtils.ConvertHtmlToPlainText(order.CheckoutAttributeDescription)); @@ -337,7 +355,7 @@ protected ShipmentDetailsModel PrepareShipmentDetailsModel(Shipment shipment) var order = shipment.Order; if (order == null) - throw new Exception("order cannot be loaded"); + throw new SmartException(T("Order.NotFound", shipment.OrderId)); var store = _storeService.GetStoreById(order.StoreId) ?? _services.StoreContext.CurrentStore; var catalogSettings = _services.Settings.LoadSetting(store.Id); @@ -568,28 +586,36 @@ public ActionResult PrintMany(string ids = null, bool pdf = false) if (!_services.Permissions.Authorize(StandardPermissionProvider.ManageOrders)) return new HttpUnauthorizedResult(); - IList orders; + const int maxOrders = 500; + IList orders = null; + int totalCount = 0; using (var scope = new DbContextScope(_services.DbContext, autoDetectChanges: false, forceNoTracking: true)) { if (ids != null) { - int[] intIds = ids.ToIntArray(); - orders = _orderService.GetOrdersByIds(intIds); + orders = _orderService.GetOrdersByIds(ids.ToIntArray()); + totalCount = orders.Count; } else { - orders = _orderService.SearchOrders(0, 0, null, null, null, null, null, null, null, null, 0, int.MaxValue); + var pagedOrders = _orderService.SearchOrders(0, 0, null, null, null, null, null, null, null, null, 0, 1); + totalCount = pagedOrders.TotalCount; + + if (totalCount > 0 && totalCount <= maxOrders) + { + orders = _orderService.SearchOrders(0, 0, null, null, null, null, null, null, null, null, 0, int.MaxValue); + } } } - if (orders.Count == 0) + if (totalCount == 0) { NotifyInfo(T("Admin.Common.ExportNoData")); return RedirectToReferrer(); } - if (orders.Count > 500) + if (totalCount > maxOrders) { NotifyWarning(T("Admin.Common.ExportToPdf.TooManyItems")); return RedirectToReferrer(); @@ -654,21 +680,29 @@ public ActionResult RePostPayment(int id) if (IsUnauthorizedOrder(order)) return new HttpUnauthorizedResult(); - if (_paymentService.CanRePostProcessPayment(order)) + try { - var postProcessPaymentRequest = new PostProcessPaymentRequest + if (_paymentService.CanRePostProcessPayment(order)) { - Order = order, - IsRePostProcessPayment = true - }; + var postProcessPaymentRequest = new PostProcessPaymentRequest + { + Order = order, + IsRePostProcessPayment = true + }; - _paymentService.PostProcessPayment(postProcessPaymentRequest); + _paymentService.PostProcessPayment(postProcessPaymentRequest); - if (postProcessPaymentRequest.RedirectUrl.HasValue()) - { - return Redirect(postProcessPaymentRequest.RedirectUrl); + if (postProcessPaymentRequest.RedirectUrl.HasValue()) + { + return Redirect(postProcessPaymentRequest.RedirectUrl); + } } } + catch (Exception exception) + { + NotifyError(exception); + } + return RedirectToAction("Details", "Order", new { id = order.Id }); } diff --git a/src/Presentation/SmartStore.Web/Controllers/PollController.cs b/src/Presentation/SmartStore.Web/Controllers/PollController.cs index 6b51058139..1919207834 100644 --- a/src/Presentation/SmartStore.Web/Controllers/PollController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/PollController.cs @@ -6,7 +6,6 @@ using SmartStore.Core.Caching; using SmartStore.Core.Domain.Polls; using SmartStore.Services.Customers; -using SmartStore.Services.Localization; using SmartStore.Services.Polls; using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Infrastructure.Cache; @@ -14,11 +13,10 @@ namespace SmartStore.Web.Controllers { - public partial class PollController : PublicControllerBase + public partial class PollController : PublicControllerBase { #region Fields - private readonly ILocalizationService _localizationService; private readonly IWorkContext _workContext; private readonly IPollService _pollService; private readonly IWebHelper _webHelper; @@ -29,12 +27,13 @@ public partial class PollController : PublicControllerBase #region Constructors - public PollController(ILocalizationService localizationService, - IWorkContext workContext, IPollService pollService, - IWebHelper webHelper, ICacheManager cacheManager, + public PollController( + IWorkContext workContext, + IPollService pollService, + IWebHelper webHelper, + ICacheManager cacheManager, IStoreContext storeContext) { - this._localizationService = localizationService; this._workContext = workContext; this._pollService = pollService; this._webHelper = webHelper; @@ -49,18 +48,23 @@ public PollController(ILocalizationService localizationService, [NonAction] protected PollModel PreparePollModel(Poll poll, bool setAlreadyVotedProperty) { - var model = new PollModel() + var model = new PollModel { Id = poll.Id, AlreadyVoted = setAlreadyVotedProperty && _pollService.AlreadyVoted(poll.Id, _workContext.CurrentCustomer.Id), Name = poll.Name }; + var answers = poll.PollAnswers.OrderBy(x => x.DisplayOrder); - foreach (var answer in answers) - model.TotalVotes += answer.NumberOfVotes; + + foreach (var answer in answers) + { + model.TotalVotes += answer.NumberOfVotes; + } + foreach (var pa in answers) { - model.Answers.Add(new PollAnswerModel() + model.Answers.Add(new PollAnswerModel { Id = pa.Id, Name = pa.Name, @@ -88,10 +92,11 @@ public ActionResult PollBlock(string systemKeyword) var poll = _pollService.GetPollBySystemKeyword(systemKeyword, _workContext.WorkingLanguage.Id); if (poll == null) - return new PollModel() { Id = 0 }; //we do not cache nulls. that's why let's return an empty record (ID = 0) + return new PollModel { Id = 0 }; //we do not cache nulls. that's why let's return an empty record (ID = 0) return PreparePollModel(poll, false); }); + if (cachedModel == null || cachedModel.Id == 0) return Content(""); @@ -108,30 +113,29 @@ public ActionResult PollBlock(string systemKeyword) public ActionResult Vote(int pollAnswerId) { var pollAnswer = _pollService.GetPollAnswerById(pollAnswerId); - if (pollAnswer == null) - return Json(new - { - error = "No poll answer found with the specified id", - }); + + if (pollAnswer == null) + { + return Json(new { error = T("Polls.AnswerNotFound", pollAnswerId).Text }); + } var poll = pollAnswer.Poll; - if (!poll.Published) - return Json(new - { - error = "Poll is not available", - }); - if (_workContext.CurrentCustomer.IsGuest() && !poll.AllowGuestsToVote) - return Json(new - { - error = _localizationService.GetResource("Polls.OnlyRegisteredUsersVote"), - }); + if (!poll.Published) + { + return Json(new { error = T("Polls.NotAvailable").Text }); + } + + if (_workContext.CurrentCustomer.IsGuest() && !poll.AllowGuestsToVote) + { + return Json(new { error = T("Polls.OnlyRegisteredUsersVote").Text }); + } bool alreadyVoted = _pollService.AlreadyVoted(poll.Id, _workContext.CurrentCustomer.Id); if (!alreadyVoted) { //vote - pollAnswer.PollVotingRecords.Add(new PollVotingRecord() + pollAnswer.PollVotingRecords.Add(new PollVotingRecord { PollAnswerId = pollAnswer.Id, CustomerId = _workContext.CurrentCustomer.Id, @@ -140,8 +144,10 @@ public ActionResult Vote(int pollAnswerId) CreatedOnUtc = DateTime.UtcNow, UpdatedOnUtc = DateTime.UtcNow, }); + //update totals pollAnswer.NumberOfVotes = pollAnswer.PollVotingRecords.Count; + _pollService.UpdatePoll(poll); } @@ -161,12 +167,14 @@ public ActionResult HomePagePolls() .Select(x => PreparePollModel(x, false)) .ToList(); }); + //"AlreadyVoted" property of "PollModel" object depends on the current customer. Let's update it. //But first we need to clone the cached model (the updated one should not be cached) var model = new List(); + foreach (var p in cachedModel) { - var pollModel = (PollModel) p.Clone(); + var pollModel = (PollModel)p.Clone(); pollModel.AlreadyVoted = _pollService.AlreadyVoted(pollModel.Id, _workContext.CurrentCustomer.Id); model.Add(pollModel); } @@ -178,6 +186,5 @@ public ActionResult HomePagePolls() } #endregion - } } diff --git a/src/Presentation/SmartStore.Web/Controllers/PrivateMessagesController.cs b/src/Presentation/SmartStore.Web/Controllers/PrivateMessagesController.cs index 6d1f6ddcf2..8171bf6d4f 100644 --- a/src/Presentation/SmartStore.Web/Controllers/PrivateMessagesController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/PrivateMessagesController.cs @@ -4,19 +4,19 @@ using SmartStore.Core; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Forums; +using SmartStore.Core.Logging; using SmartStore.Services.Customers; using SmartStore.Services.Forums; using SmartStore.Services.Helpers; -using SmartStore.Services.Localization; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Filters; using SmartStore.Web.Framework.Security; using SmartStore.Web.Models.Common; using SmartStore.Web.Models.PrivateMessages; -using SmartStore.Core.Logging; namespace SmartStore.Web.Controllers { - [RequireHttpsByConfigAttribute(SslRequirement.Yes)] + [RequireHttpsByConfigAttribute(SslRequirement.Yes)] public partial class PrivateMessagesController : PublicControllerBase { #region Fields @@ -24,7 +24,6 @@ public partial class PrivateMessagesController : PublicControllerBase private readonly IForumService _forumService; private readonly ICustomerService _customerService; private readonly ICustomerActivityService _customerActivityService; - private readonly ILocalizationService _localizationService; private readonly IWorkContext _workContext; private readonly IStoreContext _storeContext; private readonly IDateTimeHelper _dateTimeHelper; @@ -36,16 +35,17 @@ public partial class PrivateMessagesController : PublicControllerBase #region Constructors public PrivateMessagesController(IForumService forumService, - ICustomerService customerService, ICustomerActivityService customerActivityService, - ILocalizationService localizationService, - IWorkContext workContext, IStoreContext storeContext, + ICustomerService customerService, + ICustomerActivityService customerActivityService, + IWorkContext workContext, + IStoreContext storeContext, IDateTimeHelper dateTimeHelper, - ForumSettings forumSettings, CustomerSettings customerSettings) + ForumSettings forumSettings, + CustomerSettings customerSettings) { this._forumService = forumService; this._customerService = customerService; this._customerActivityService = customerActivityService; - this._localizationService = localizationService; this._workContext = workContext; this._storeContext = storeContext; this._dateTimeHelper = dateTimeHelper; @@ -130,7 +130,7 @@ public ActionResult Inbox(int page, string tab) foreach (var pm in list) { - inbox.Add(new PrivateMessageModel() + inbox.Add(new PrivateMessageModel { Id = pm.Id, FromCustomerId = pm.FromCustomer.Id, @@ -174,7 +174,7 @@ public ActionResult SentItems(int page, string tab) foreach (var pm in list) { - sentItems.Add(new PrivateMessageModel() + sentItems.Add(new PrivateMessageModel { Id = pm.Id, FromCustomerId = pm.FromCustomer.Id, @@ -226,8 +226,8 @@ public ActionResult DeleteInboxPM(FormCollection formCollection) } } } - return RedirectToRoute("PrivateMessages"); - } + return RedirectToAction("Index"); + } [HttpPost, FormValueRequired("mark-unread"), ActionName("InboxUpdate")] public ActionResult MarkUnread(FormCollection formCollection) @@ -255,7 +255,7 @@ public ActionResult MarkUnread(FormCollection formCollection) } } } - return RedirectToRoute("PrivateMessages"); + return RedirectToAction("Index"); } //updates sent items (deletes PrivateMessages) @@ -305,8 +305,8 @@ public ActionResult Send(int id /* toCustomerId */, int? replyToMessageId) if (customerTo == null || customerTo.IsGuest()) { - return RedirectToRoute("PrivateMessages"); - } + return RedirectToAction("Index"); + } var model = new SendPrivateMessageModel(); model.ToCustomerId = customerTo.Id; @@ -318,8 +318,8 @@ public ActionResult Send(int id /* toCustomerId */, int? replyToMessageId) var replyToPM = _forumService.GetPrivateMessageById(replyToMessageId.Value); if (replyToPM == null) { - return RedirectToRoute("PrivateMessages"); - } + return RedirectToAction("Index"); + } if (replyToPM.ToCustomerId == _workContext.CurrentCustomer.Id || replyToPM.FromCustomerId == _workContext.CurrentCustomer.Id) { @@ -328,10 +328,10 @@ public ActionResult Send(int id /* toCustomerId */, int? replyToMessageId) } else { - return RedirectToRoute("PrivateMessages"); - } + return RedirectToAction("Index"); + } } - return View(model); + return View(model); } [HttpPost] @@ -357,8 +357,8 @@ public ActionResult Send(SendPrivateMessageModel model) } else { - return RedirectToRoute("PrivateMessages"); - } + return RedirectToAction("Index"); + } } else { @@ -367,8 +367,8 @@ public ActionResult Send(SendPrivateMessageModel model) if (toCustomer == null || toCustomer.IsGuest()) { - return RedirectToRoute("PrivateMessages"); - } + return RedirectToAction("Index"); + } model.ToCustomerId = toCustomer.Id; model.CustomerToName = toCustomer.FormatUserName(); model.AllowViewingToProfile = _customerSettings.AllowViewingProfiles && !toCustomer.IsGuest(); @@ -407,9 +407,9 @@ public ActionResult Send(SendPrivateMessageModel model) _forumService.InsertPrivateMessage(privateMessage); //activity log - _customerActivityService.InsertActivity("PublicStore.SendPM", _localizationService.GetResource("ActivityLog.PublicStore.SendPM"), toCustomer.Email); + _customerActivityService.InsertActivity("PublicStore.SendPM", T("ActivityLog.PublicStore.SendPM", toCustomer.Email)); - return RedirectToRoute("PrivateMessages", new { tab = "sent" }); + return RedirectToAction("Index", new { tab = "sent" }); } catch (Exception ex) { @@ -437,8 +437,8 @@ public ActionResult View(int id /* privateMessageId */) { if (pm.ToCustomerId != _workContext.CurrentCustomer.Id && pm.FromCustomerId != _workContext.CurrentCustomer.Id) { - return RedirectToRoute("PrivateMessages"); - } + return RedirectToAction("Index"); + } if (!pm.IsRead && pm.ToCustomerId == _workContext.CurrentCustomer.Id) { @@ -448,10 +448,10 @@ public ActionResult View(int id /* privateMessageId */) } else { - return RedirectToRoute("PrivateMessages"); - } + return RedirectToAction("Index"); + } - var model = new PrivateMessageModel() + var model = new PrivateMessageModel { Id = pm.Id, FromCustomerId = pm.FromCustomer.Id, @@ -496,8 +496,8 @@ public ActionResult Delete(int id /* privateMessageId */) _forumService.UpdatePrivateMessage(pm); } } - return RedirectToRoute("PrivateMessages"); - } + return RedirectToAction("Index"); + } #endregion } diff --git a/src/Presentation/SmartStore.Web/Controllers/ProductController.cs b/src/Presentation/SmartStore.Web/Controllers/ProductController.cs index 74c3d06c58..79497418a9 100644 --- a/src/Presentation/SmartStore.Web/Controllers/ProductController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/ProductController.cs @@ -8,7 +8,6 @@ using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Media; using SmartStore.Core.Domain.Orders; -using SmartStore.Core.Localization; using SmartStore.Services; using SmartStore.Services.Catalog; using SmartStore.Services.Common; @@ -23,6 +22,7 @@ using SmartStore.Services.Stores; using SmartStore.Services.Tax; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Filters; using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.UI.Captcha; using SmartStore.Web.Infrastructure.Cache; @@ -30,7 +30,7 @@ namespace SmartStore.Web.Controllers { - public partial class ProductController : PublicControllerBase + public partial class ProductController : PublicControllerBase { #region Fields @@ -167,14 +167,16 @@ public ActionResult ProductDetails(int productId, string attributes) } } - //prepare the model var selectedAttributes = new NameValueCollection(); - selectedAttributes.ConvertAttributeQueryData( + // get selected attributes from query string + selectedAttributes.GetSelectedAttributes( + Request.QueryString, _productAttributeParser.DeserializeQueryData(attributes), product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing ? 0 : product.Id); - var model = _helper.PrepareProductDetailsPageModel(product, selectedAttributes: selectedAttributes); + // prepare the view model + var model = _helper.PrepareProductDetailsPageModel(product, selectedAttributes: selectedAttributes, queryData: Request.QueryString); //save as recently viewed _recentlyViewedProductsService.AddProductToRecentlyViewedList(product.Id); @@ -319,7 +321,12 @@ public ActionResult AddProductToCart(int productId, FormCollection form) [ChildActionOnly] public ActionResult ProductManufacturers(int productId, bool preparePictureModel = false) { - string cacheKey = string.Format(ModelCacheEventConsumer.PRODUCT_MANUFACTURERS_MODEL_KEY, productId, _services.WorkContext.WorkingLanguage.Id, _services.StoreContext.CurrentStore.Id); + var cacheKey = string.Format(ModelCacheEventConsumer.PRODUCT_MANUFACTURERS_MODEL_KEY, + productId, + !_catalogSettings.HideManufacturerDefaultPictures, + _services.WorkContext.WorkingLanguage.Id, + _services.StoreContext.CurrentStore.Id); + var cacheModel = _services.Cache.Get(cacheKey, () => { var model = _manufacturerService.GetProductManufacturersByProductId(productId) @@ -328,7 +335,8 @@ public ActionResult ProductManufacturers(int productId, bool preparePictureModel var m = x.Manufacturer.ToModel(); if (preparePictureModel) { - m.PictureModel.ImageUrl = _pictureService.GetPictureUrl(x.Manufacturer.PictureId.GetValueOrDefault()); + m.PictureModel.ImageUrl = _pictureService.GetPictureUrl(x.Manufacturer.PictureId.GetValueOrDefault(), 0, !_catalogSettings.HideManufacturerDefaultPictures); + var picture = _pictureService.GetPictureUrl(x.Manufacturer.PictureId.GetValueOrDefault()); if (picture != null) { @@ -354,7 +362,7 @@ public ActionResult ReviewOverview(int id) { var product = _productService.GetProductById(id); if (product == null) - throw new ArgumentException("No product found with the specified id"); + throw new ArgumentException(T("Products.NotFound", id)); var model = new ProductReviewOverviewModel() { @@ -371,7 +379,7 @@ public ActionResult ProductSpecifications(int productId) { var product = _productService.GetProductById(productId); if (product == null) - throw new ArgumentException("No product found with the specified id"); + throw new ArgumentException(T("Products.NotFound", productId)); var model = _helper.PrepareProductSpecificationModel(product); @@ -402,7 +410,7 @@ public ActionResult ProductTierPrices(int productId) var product = _productService.GetProductById(productId); if (product == null) - throw new ArgumentException("No product found with the specified id"); + throw new ArgumentException(T("Products.NotFound", productId)); if (!product.HasTierPrices) return Content(""); //no tier prices @@ -521,7 +529,7 @@ public ActionResult BackInStockSubscribePopup(int id /* productId */) { var product = _productService.GetProductById(id); if (product == null || product.Deleted) - throw new ArgumentException("No product found with the specified id"); + throw new ArgumentException(T("Products.NotFound", id)); var model = new BackInStockSubscribeModel(); model.ProductId = product.Id; @@ -550,7 +558,7 @@ public ActionResult BackInStockSubscribePopupPOST(int id /* productId */) { var product = _productService.GetProductById(id); if (product == null || product.Deleted) - throw new ArgumentException("No product found with the specified id"); + throw new ArgumentException(T("Products.NotFound", id)); if (!_services.WorkContext.CurrentCustomer.IsRegistered()) return Content(T("BackInStockSubscriptions.OnlyRegistered")); @@ -761,7 +769,7 @@ public ActionResult ProductTags(int productId) { var product = _productService.GetProductById(productId); if (product == null) - throw new ArgumentException("No product found with the specified id"); + throw new ArgumentException(T("Products.NotFound", productId)); var cacheKey = string.Format(ModelCacheEventConsumer.PRODUCTTAG_BY_PRODUCT_MODEL_KEY, product.Id, _services.WorkContext.WorkingLanguage.Id, _services.StoreContext.CurrentStore.Id); var cacheModel = _services.Cache.Get(cacheKey, () => @@ -892,7 +900,7 @@ public ActionResult SetReviewHelpfulness(int productReviewId, bool washelpful) { var productReview = _customerContentService.GetCustomerContentById(productReviewId) as ProductReview; if (productReview == null) - throw new ArgumentException("No product review found with the specified id"); + throw new ArgumentException(T("Reviews.NotFound", productReviewId)); if (_services.WorkContext.CurrentCustomer.IsGuest() && !_catalogSettings.AllowAnonymousUsersToReviewProduct) { @@ -1031,7 +1039,7 @@ public ActionResult AskQuestionSend(ProductAskQuestionModel model, bool captchaV } else { - ModelState.AddModelError("", "Fehler beim Versenden der Email. Bitte versuchen Sie es später erneut."); + ModelState.AddModelError("", T("Common.Error.SendMail")); } } diff --git a/src/Presentation/SmartStore.Web/Controllers/ShoppingCartController.cs b/src/Presentation/SmartStore.Web/Controllers/ShoppingCartController.cs index 98ea0b9fcc..e4ff2cb6a7 100644 --- a/src/Presentation/SmartStore.Web/Controllers/ShoppingCartController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/ShoppingCartController.cs @@ -32,7 +32,9 @@ using SmartStore.Services.Seo; using SmartStore.Services.Shipping; using SmartStore.Services.Tax; +using SmartStore.Services.Topics; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Filters; using SmartStore.Web.Framework.Plugins; using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.UI.Captcha; @@ -90,12 +92,15 @@ public partial class ShoppingCartController : PublicControllerBase private readonly AddressSettings _addressSettings; private readonly PluginMediator _pluginMediator; private readonly IQuantityUnitService _quantityUnitService; + private readonly Lazy _topicService; + private readonly IMeasureService _measureService; + private readonly MeasureSettings _measureSettings; - #endregion + #endregion - #region Constructors + #region Constructors - public ShoppingCartController(IProductService productService, + public ShoppingCartController(IProductService productService, IWorkContext workContext, IStoreContext storeContext, IShoppingCartService shoppingCartService, IPictureService pictureService, ILocalizationService localizationService, @@ -120,7 +125,9 @@ public ShoppingCartController(IProductService productService, ShippingSettings shippingSettings, TaxSettings taxSettings, CaptchaSettings captchaSettings, AddressSettings addressSettings, HttpContextBase httpContext, PluginMediator pluginMediator, - IQuantityUnitService quantityUnitService) + IQuantityUnitService quantityUnitService, + Lazy topicService, + IMeasureService measureService, MeasureSettings measureSettings) { this._productService = productService; this._workContext = workContext; @@ -166,6 +173,9 @@ public ShoppingCartController(IProductService productService, this._addressSettings = addressSettings; this._pluginMediator = pluginMediator; this._quantityUnitService = quantityUnitService; + this._topicService = topicService; + this._measureService = measureService; + this._measureSettings = measureSettings; } #endregion @@ -173,7 +183,7 @@ public ShoppingCartController(IProductService productService, #region Utilities [NonAction] - protected PictureModel PrepareCartItemPictureModel(Product product, int pictureSize, bool showDefaultPicture, string productName, string attributesXml) + protected PictureModel PrepareCartItemPictureModel(Product product, int pictureSize, string productName, string attributesXml) { if (product == null) throw new ArgumentNullException("product"); @@ -204,10 +214,10 @@ protected PictureModel PrepareCartItemPictureModel(Product product, int pictureS picture = _pictureService.GetPicturesByProductId(product.ParentGroupedProductId, 1).FirstOrDefault(); } - return new PictureModel() + return new PictureModel { PictureId = picture != null ? picture.Id : 0, - ImageUrl = _pictureService.GetPictureUrl(picture, pictureSize, showDefaultPicture), + ImageUrl = _pictureService.GetPictureUrl(picture, pictureSize, !_catalogSettings.HideProductDefaultPictures), Title = string.Format(_localizationService.GetResource("Media.Product.ImageLinkTitleFormat"), productName), AlternateText = string.Format(_localizationService.GetResource("Media.Product.ImageAlternateTextFormat"), productName), }; @@ -235,7 +245,10 @@ private ShoppingCartModel.ShoppingCartItemModel PrepareShoppingCartItemModel(Org ShortDesc = product.GetLocalized(x => x.ShortDescription), ProductType = product.ProductType, BasePrice = product.GetBasePriceInfo(_localizationService, _priceFormatter, _currencyService, _taxService, _priceCalculationService, _workContext.WorkingCurrency), - Weight = product.Weight + Weight = product.Weight, + IsDownload = product.IsDownload, + HasUserAgreement = product.HasUserAgreement, + IsEsd = product.IsEsd }; model.ProductUrl = GetProductUrlWithAttributes(sci, model.ProductSeName); @@ -246,7 +259,7 @@ private ShoppingCartModel.ShoppingCartItemModel PrepareShoppingCartItemModel(Org model.BundlePerItemShoppingCart = item.BundleItem.BundleProduct.BundlePerItemShoppingCart; model.AttributeInfo = _productAttributeFormatter.FormatAttributes(product, item.AttributesXml, _workContext.CurrentCustomer, - renderPrices: false, renderGiftCardAttributes: true, allowHyperlinks: false); + renderPrices: false, renderGiftCardAttributes: true, allowHyperlinks: true); string bundleItemName = item.BundleItem.GetLocalized(x => x.Name); if (bundleItemName.HasValue()) @@ -384,17 +397,17 @@ private ShoppingCartModel.ShoppingCartItemModel PrepareShoppingCartItemModel(Org if (item.BundleItem != null) { if (_shoppingCartSettings.ShowProductBundleImagesOnShoppingCart) - model.Picture = PrepareCartItemPictureModel(product, _mediaSettings.CartThumbBundleItemPictureSize, true, model.ProductName, item.AttributesXml); + model.Picture = PrepareCartItemPictureModel(product, _mediaSettings.CartThumbBundleItemPictureSize, model.ProductName, item.AttributesXml); } else { if (_shoppingCartSettings.ShowProductImagesOnShoppingCart) - model.Picture = PrepareCartItemPictureModel(product, _mediaSettings.CartThumbPictureSize, true, model.ProductName, item.AttributesXml); + model.Picture = PrepareCartItemPictureModel(product, _mediaSettings.CartThumbPictureSize, model.ProductName, item.AttributesXml); } //item warnings var itemWarnings = _shoppingCartService.GetShoppingCartItemWarnings(_workContext.CurrentCustomer, item.ShoppingCartType, product, item.StoreId, - item.AttributesXml, item.CustomerEnteredPrice, item.Quantity, false, childItems: sci.ChildItems); + item.AttributesXml, item.CustomerEnteredPrice, item.Quantity, false, bundleItem: item.BundleItem, childItems: sci.ChildItems); foreach (var warning in itemWarnings) { @@ -532,12 +545,12 @@ private WishlistModel.ShoppingCartItemModel PrepareWishlistCartItemModel(Organiz if (item.BundleItem != null) { if (_shoppingCartSettings.ShowProductBundleImagesOnShoppingCart) - model.Picture = PrepareCartItemPictureModel(product, _mediaSettings.CartThumbBundleItemPictureSize, true, model.ProductName, item.AttributesXml); + model.Picture = PrepareCartItemPictureModel(product, _mediaSettings.CartThumbBundleItemPictureSize, model.ProductName, item.AttributesXml); } else { if (_shoppingCartSettings.ShowProductImagesOnShoppingCart) - model.Picture = PrepareCartItemPictureModel(product, _mediaSettings.CartThumbPictureSize, true, model.ProductName, item.AttributesXml); + model.Picture = PrepareCartItemPictureModel(product, _mediaSettings.CartThumbPictureSize, model.ProductName, item.AttributesXml); } //item warnings @@ -630,12 +643,21 @@ protected void PrepareShoppingCartModel(ShoppingCartModel model, model.ShowProductImages = _shoppingCartSettings.ShowProductImagesOnShoppingCart; model.ShowProductBundleImages = _shoppingCartSettings.ShowProductBundleImagesOnShoppingCart; model.ShowSku = _catalogSettings.ShowProductSku; + + var measure = _measureService.GetMeasureWeightById(_measureSettings.BaseWeightId); + if(measure != null) + { + model.MeasureUnitName = measure.Name; + } + + var checkoutAttributesXml = _workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.CheckoutAttributes, _genericAttributeService); model.CheckoutAttributeInfo = HtmlUtils.ConvertPlainTextToTable(HtmlUtils.ConvertHtmlToPlainText( _checkoutAttributeFormatter.FormatAttributes(checkoutAttributesXml, _workContext.CurrentCustomer) )); //model.CheckoutAttributeInfo = _checkoutAttributeFormatter.FormatAttributes(_workContext.CurrentCustomer.CheckoutAttributes, _workContext.CurrentCustomer); //model.CheckoutAttributeInfo = _checkoutAttributeFormatter.FormatAttributes(_workContext.CurrentCustomer.CheckoutAttributes, _workContext.CurrentCustomer, "", false); + bool minOrderSubtotalAmountOk = _orderProcessingService.ValidateMinOrderSubtotalAmount(cart); if (!minOrderSubtotalAmountOk) { @@ -648,23 +670,37 @@ protected void PrepareShoppingCartModel(ShoppingCartModel model, model.DiscountBox.Display = _shoppingCartSettings.ShowDiscountBox; var discountCouponCode = _workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.DiscountCouponCode); var discount = _discountService.GetDiscountByCouponCode(discountCouponCode); - if (discount != null && - discount.RequiresCouponCode && - _discountService.IsDiscountValid(discount, _workContext.CurrentCustomer)) + if (discount != null && discount.RequiresCouponCode && _discountService.IsDiscountValid(discount, _workContext.CurrentCustomer)) + { model.DiscountBox.CurrentCode = discount.CouponCode; + } + model.GiftCardBox.Display = _shoppingCartSettings.ShowGiftCardBox; model.DisplayCommentBox = _shoppingCartSettings.ShowCommentBox; + model.NewsLetterSubscription = _shoppingCartSettings.NewsLetterSubscription; + model.ThirdPartyEmailHandOver = _shoppingCartSettings.ThirdPartyEmailHandOver; + model.DisplayEsdRevocationWaiverBox = _shoppingCartSettings.ShowEsdRevocationWaiverBox; + + if (_shoppingCartSettings.ThirdPartyEmailHandOver != CheckoutThirdPartyEmailHandOver.None) + { + model.ThirdPartyEmailHandOverLabel = _shoppingCartSettings.GetLocalized(x => x.ThirdPartyEmailHandOverLabel, _workContext.WorkingLanguage.Id, true, false); + + if (model.ThirdPartyEmailHandOverLabel.IsEmpty()) + model.ThirdPartyEmailHandOverLabel = T("Admin.Configuration.Settings.ShoppingCart.ThirdPartyEmailHandOverLabel.Default"); + } //cart warnings var cartWarnings = _shoppingCartService.GetShoppingCartWarnings(cart, checkoutAttributesXml, validateCheckoutAttributes); foreach (var warning in cartWarnings) + { model.Warnings.Add(warning); + } #endregion #region Checkout attributes - var checkoutAttributes = _checkoutAttributeService.GetAllCheckoutAttributes(); + var checkoutAttributes = _checkoutAttributeService.GetAllCheckoutAttributes(_storeContext.CurrentStore.Id); if (!cart.RequiresShipping()) { //remove attributes which require shippable products @@ -949,7 +985,7 @@ protected MiniShoppingCartModel PrepareMiniShoppingCartModel() //1. "terms of services" are enabled (OBSOLETE now) //2. we have at least one checkout attribute //3. min order sub-total is OK - var checkoutAttributes = _checkoutAttributeService.GetAllCheckoutAttributes(); + var checkoutAttributes = _checkoutAttributeService.GetAllCheckoutAttributes(_storeContext.CurrentStore.Id); if (!cart.RequiresShipping()) { //remove attributes which require shippable products @@ -960,7 +996,6 @@ protected MiniShoppingCartModel PrepareMiniShoppingCartModel() //products. sort descending (recently added products) foreach (var sci in cart - .OrderByDescending(x => x.Item.Id) .Take(_shoppingCartSettings.MiniShoppingCartProductNumber) .ToList()) { @@ -1026,7 +1061,7 @@ protected MiniShoppingCartModel PrepareMiniShoppingCartModel() //picture if (_shoppingCartSettings.ShowProductImagesInMiniShoppingCart) { - cartItemModel.Picture = PrepareCartItemPictureModel(product, _mediaSettings.MiniCartThumbPictureSize, true, cartItemModel.ProductName, item.AttributesXml); + cartItemModel.Picture = PrepareCartItemPictureModel(product, _mediaSettings.MiniCartThumbPictureSize, cartItemModel.ProductName, item.AttributesXml); } model.Items.Add(cartItemModel); @@ -1040,15 +1075,18 @@ protected MiniShoppingCartModel PrepareMiniShoppingCartModel() protected void ParseAndSaveCheckoutAttributes(List cart, FormCollection form) { string selectedAttributes = ""; - var checkoutAttributes = _checkoutAttributeService.GetAllCheckoutAttributes(); + var checkoutAttributes = _checkoutAttributeService.GetAllCheckoutAttributes(_storeContext.CurrentStore.Id); + if (!cart.RequiresShipping()) { //remove attributes which require shippable products checkoutAttributes = checkoutAttributes.RemoveShippableAttributes(); } + foreach (var attribute in checkoutAttributes) { string controlId = string.Format("checkout_attribute_{0}", attribute.Id); + switch (attribute.AttributeControlType) { case AttributeControlType.DropdownList: @@ -1058,13 +1096,13 @@ protected void ParseAndSaveCheckoutAttributes(List ca var rblAttributes = form[controlId]; if (!String.IsNullOrEmpty(rblAttributes)) { - int selectedAttributeId = int.Parse(rblAttributes); + var selectedAttributeId = rblAttributes.SplitSafe(",").SafeGet(0).ToInt(); if (selectedAttributeId > 0) - selectedAttributes = _checkoutAttributeParser.AddCheckoutAttribute(selectedAttributes, - attribute, selectedAttributeId.ToString()); + selectedAttributes = _checkoutAttributeParser.AddCheckoutAttribute(selectedAttributes, attribute, selectedAttributeId.ToString()); } } break; + case AttributeControlType.Checkboxes: { var cblAttributes = form[controlId]; @@ -1072,14 +1110,14 @@ protected void ParseAndSaveCheckoutAttributes(List ca { foreach (var item in cblAttributes.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { - int selectedAttributeId = int.Parse(item); - if (selectedAttributeId > 0) - selectedAttributes = _checkoutAttributeParser.AddCheckoutAttribute(selectedAttributes, - attribute, selectedAttributeId.ToString()); + var selectedAttributeId = item.SplitSafe(",").SafeGet(0).ToInt(); + if (selectedAttributeId > 0) + selectedAttributes = _checkoutAttributeParser.AddCheckoutAttribute(selectedAttributes, attribute, selectedAttributeId.ToString()); } } } break; + case AttributeControlType.TextBox: case AttributeControlType.MultilineTextbox: { @@ -1087,29 +1125,31 @@ protected void ParseAndSaveCheckoutAttributes(List ca if (!String.IsNullOrEmpty(txtAttribute)) { string enteredText = txtAttribute.Trim(); - selectedAttributes = _checkoutAttributeParser.AddCheckoutAttribute(selectedAttributes, - attribute, enteredText); + selectedAttributes = _checkoutAttributeParser.AddCheckoutAttribute(selectedAttributes, attribute, enteredText); } } break; + case AttributeControlType.Datepicker: { var date = form[controlId + "_day"]; var month = form[controlId + "_month"]; var year = form[controlId + "_year"]; DateTime? selectedDate = null; + try { selectedDate = new DateTime(Int32.Parse(year), Int32.Parse(month), Int32.Parse(date)); } catch { } + if (selectedDate.HasValue) { - selectedAttributes = _checkoutAttributeParser.AddCheckoutAttribute(selectedAttributes, - attribute, selectedDate.Value.ToString("D")); + selectedAttributes = _checkoutAttributeParser.AddCheckoutAttribute(selectedAttributes, attribute, selectedDate.Value.ToString("D")); } } break; + case AttributeControlType.FileUpload: { var postedFile = this.Request.Files[controlId].ToPostedFileResult(); @@ -1142,6 +1182,7 @@ protected void ParseAndSaveCheckoutAttributes(List ca } } break; + default: break; } @@ -1190,7 +1231,7 @@ public ActionResult AddProductSimple(int productId, bool forceredirection = fals return Json(new { success = false, - message = "No product found with the specified ID" + message = T("Products.NotFound", productId) }); } @@ -1438,7 +1479,7 @@ public ActionResult UploadFileProductAttribute(int productId, int productAttribu var postedFile = Request.ToPostedFileResult(); if (postedFile == null) { - throw new ArgumentException("No file uploaded"); + throw new ArgumentException(T("Common.NoFileUploaded")); } int fileMaxSize = _catalogSettings.FileUploadMaximumSizeBytes; @@ -1707,14 +1748,7 @@ public ActionResult DeleteCartItem(int cartItemId, bool? wishlistItem) public ActionResult ContinueShopping() { string returnUrl = _workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.LastContinueShoppingPage, _storeContext.CurrentStore.Id); - if (!String.IsNullOrEmpty(returnUrl)) - { - return Redirect(returnUrl); - } - else - { - return RedirectToRoute("HomePage"); - } + return RedirectToReferrer(returnUrl); } [ValidateInput(false)] @@ -1832,7 +1866,8 @@ public ActionResult ApplyGiftCard(string giftcardcouponcode, FormCollection form [FormValueRequired("estimateshipping")] public ActionResult GetEstimateShipping(EstimateShippingModel shippingModel, FormCollection form) { - var cart = _workContext.CurrentCustomer.GetCartItems(ShoppingCartType.ShoppingCart, _storeContext.CurrentStore.Id); + var store = _storeContext.CurrentStore; + var cart = _workContext.CurrentCustomer.GetCartItems(ShoppingCartType.ShoppingCart, store.Id); //parse and save checkout attributes ParseAndSaveCheckoutAttributes(cart, form); @@ -1841,11 +1876,17 @@ public ActionResult GetEstimateShipping(EstimateShippingModel shippingModel, For model.EstimateShipping.CountryId = shippingModel.CountryId; model.EstimateShipping.StateProvinceId = shippingModel.StateProvinceId; model.EstimateShipping.ZipPostalCode = shippingModel.ZipPostalCode; + PrepareShoppingCartModel(model, cart, setEstimateShippingDefaultAddress: false); if (cart.RequiresShipping()) { - var address = new Address() + if (_topicService.Value.GetTopicBySystemName("ShippingInfo", store.Id) != null) + { + model.EstimateShipping.ShippingInfoUrl = Url.RouteUrl("Topic", new { SystemName = "shippinginfo" }); + } + + var address = new Address { CountryId = shippingModel.CountryId, Country = shippingModel.CountryId.HasValue ? _countryService.GetCountryById(shippingModel.CountryId.Value) : null, @@ -1853,12 +1894,15 @@ public ActionResult GetEstimateShipping(EstimateShippingModel shippingModel, For StateProvince = shippingModel.StateProvinceId.HasValue ? _stateProvinceService.GetStateProvinceById(shippingModel.StateProvinceId.Value) : null, ZipPostalCode = shippingModel.ZipPostalCode, }; - GetShippingOptionResponse getShippingOptionResponse = _shippingService - .GetShippingOptions(cart, address, "", _storeContext.CurrentStore.Id); + + var getShippingOptionResponse = _shippingService.GetShippingOptions(cart, address, "", store.Id); + if (!getShippingOptionResponse.Success) { - foreach (var error in getShippingOptionResponse.Errors) - model.EstimateShipping.Warnings.Add(error); + foreach (var error in getShippingOptionResponse.Errors) + { + model.EstimateShipping.Warnings.Add(error); + } } else { @@ -1868,13 +1912,13 @@ public ActionResult GetEstimateShipping(EstimateShippingModel shippingModel, For foreach (var shippingOption in getShippingOptionResponse.ShippingOptions) { - var soModel = new EstimateShippingModel.ShippingOptionModel() + var soModel = new EstimateShippingModel.ShippingOptionModel { ShippingMethodId = shippingOption.ShippingMethodId, Name = shippingOption.Name, - Description = shippingOption.Description, - + Description = shippingOption.Description }; + //calculate discounted and taxed rate Discount appliedDiscount = null; decimal shippingTotal = _orderTotalCalculationService.AdjustShippingRate( @@ -1883,12 +1927,13 @@ public ActionResult GetEstimateShipping(EstimateShippingModel shippingModel, For decimal rateBase = _taxService.GetShippingPrice(shippingTotal, _workContext.CurrentCustomer); decimal rate = _currencyService.ConvertFromPrimaryStoreCurrency(rateBase, _workContext.WorkingCurrency); soModel.Price = _priceFormatter.FormatShippingPrice(rate, false /*true*/); + model.EstimateShipping.ShippingOptions.Add(soModel); } } else { - model.EstimateShipping.Warnings.Add(_localizationService.GetResource("Checkout.ShippingIsNotAllowed")); + model.EstimateShipping.Warnings.Add(T("Checkout.ShippingIsNotAllowed")); } } } @@ -2587,7 +2632,7 @@ public ActionResult FlyoutWishlist() { Customer customer = _workContext.CurrentCustomer; - var cart = customer.GetCartItems(ShoppingCartType.Wishlist, _storeContext.CurrentStore.Id, true); + var cart = customer.GetCartItems(ShoppingCartType.Wishlist, _storeContext.CurrentStore.Id); var model = new WishlistModel(); PrepareWishlistModel(model, cart, true); diff --git a/src/Presentation/SmartStore.Web/Controllers/TaskSchedulerController.cs b/src/Presentation/SmartStore.Web/Controllers/TaskSchedulerController.cs index b52e1c033b..68c94b3fe7 100644 --- a/src/Presentation/SmartStore.Web/Controllers/TaskSchedulerController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/TaskSchedulerController.cs @@ -9,7 +9,7 @@ using SmartStore.Web.Framework.Controllers; using SmartStore.Services.Security; using SmartStore.Services; - +using SmartStore.Collections; namespace SmartStore.Web.Controllers { @@ -44,14 +44,14 @@ public ActionResult Sweep() var prevTaskStart = DateTime.UtcNow; var count = 0; - foreach (var task in pendingTasks) + for (var i = 0; i < pendingTasks.Count; i++) { - var elapsedSincePrevTask = DateTime.UtcNow - prevTaskStart; - if (elapsedSincePrevTask >= TimeSpan.FromMinutes(_taskScheduler.SweepIntervalMinutes)) + var task = pendingTasks[i]; + + if (i > 0) { - // the previous Task execution in this loop took longer - // than the scheduler's sweep interval. Most likely a subsequent - // Sweep call executed it already in another HTTP request. + // Maybe a subsequent Sweep call or another machine in a webfarm executed + // successive tasks already. // To be able to determine this, we need to reload the entity from the database. // The TaskExecutor will exit when the task should be in running state then. _services.DbContext.ReloadEntity(task); @@ -65,7 +65,7 @@ public ActionResult Sweep() } } - return Content("{0} tasks executed".FormatInvariant(count)); + return Content("{0} of {1} pending tasks executed".FormatInvariant(count, pendingTasks.Count)); } [HttpPost] @@ -77,11 +77,16 @@ public ActionResult Execute(int id /* taskId */) var task = _scheduledTaskService.GetTaskById(id); if (task == null) return HttpNotFound(); - - _taskExecutor.Execute(task); + + _taskExecutor.Execute(task, QueryString.Current.ToDictionary()); return Content("Task '{0}' executed".FormatCurrent(task.Name)); } - } + public ContentResult Noop() + { + return Content("noop"); + } + + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Controllers/ThemeController.cs b/src/Presentation/SmartStore.Web/Controllers/ThemeController.cs index 645c7f19ad..92e8cde656 100644 --- a/src/Presentation/SmartStore.Web/Controllers/ThemeController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/ThemeController.cs @@ -3,7 +3,8 @@ using SmartStore.Core.Themes; using SmartStore.Services.Themes; using SmartStore.Web.Framework.Controllers; -using SmartStore.Web.Framework.Themes; +using SmartStore.Web.Framework.Security; +using SmartStore.Web.Framework.Theming; namespace SmartStore.Web.Controllers { diff --git a/src/Presentation/SmartStore.Web/Controllers/TopicController.cs b/src/Presentation/SmartStore.Web/Controllers/TopicController.cs index 7e861fac2e..7c4be750a3 100644 --- a/src/Presentation/SmartStore.Web/Controllers/TopicController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/TopicController.cs @@ -66,7 +66,8 @@ protected TopicModel PrepareTopicModel(string systemName) MetaDescription = topic.GetLocalized(x => x.MetaDescription), MetaTitle = topic.GetLocalized(x => x.MetaTitle), TitleTag = titleTag, - }; + RenderAsWidget = topic.RenderAsWidget + }; return model; } @@ -79,8 +80,9 @@ public ActionResult TopicDetails(string systemName) var cacheKey = string.Format(ModelCacheEventConsumer.TOPIC_MODEL_KEY, systemName, _workContext.WorkingLanguage.Id, _storeContext.CurrentStore.Id); var cacheModel = _cacheManager.Get(cacheKey, () => PrepareTopicModel(systemName)); - if (cacheModel == null) + if (cacheModel == null || (cacheModel.RenderAsWidget && !cacheModel.IncludeInSitemap)) return HttpNotFound(); + return View("TopicDetails", cacheModel); } diff --git a/src/Presentation/SmartStore.Web/Controllers/WidgetController.cs b/src/Presentation/SmartStore.Web/Controllers/WidgetController.cs index 67f1ddf752..74461e92ef 100644 --- a/src/Presentation/SmartStore.Web/Controllers/WidgetController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/WidgetController.cs @@ -14,5 +14,11 @@ public ActionResult WidgetsByZone(WidgetZoneModel zoneModel) return PartialView(zoneModel); } + [ChildActionOnly] + public ActionResult TabWidgets(object model, string viewDataKey) + { + var widgets = this.ControllerContext.ParentActionViewContext.ViewData[viewDataKey]; + return PartialView(widgets); + } } } diff --git a/src/Presentation/SmartStore.Web/Global.asax.cs b/src/Presentation/SmartStore.Web/Global.asax.cs index 1ab1679bc0..4297e54186 100644 --- a/src/Presentation/SmartStore.Web/Global.asax.cs +++ b/src/Presentation/SmartStore.Web/Global.asax.cs @@ -1,33 +1,34 @@ -using System.Linq; +using System; +using System.Linq; using System.Web; using System.Web.Hosting; using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; +using System.Web.Security; using System.Web.WebPages; using FluentValidation.Mvc; using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Events; using SmartStore.Core.Infrastructure; +using SmartStore.Services.Customers; using SmartStore.Services.Tasks; -using SmartStore.Web.Framework.Controllers; -using SmartStore.Web.Framework.Mvc; -using SmartStore.Web.Framework.Mvc.Bundles; -using SmartStore.Web.Framework.Mvc.Routes; +using SmartStore.Web.Framework.Bundling; +using SmartStore.Web.Framework.Filters; +using SmartStore.Web.Framework.Localization; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Framework.Plugins; -using SmartStore.Web.Framework.Themes; +using SmartStore.Web.Framework.Routing; +using SmartStore.Web.Framework.Theming; using SmartStore.Web.Framework.Validators; - namespace SmartStore.Web { // Note: For instructions on enabling IIS6 or IIS7 classic mode, // visit http://go.microsoft.com/?LinkId=9394801 - public class MvcApplication : System.Web.HttpApplication { - public static void RegisterGlobalFilters(GlobalFilterCollection filters) { var eventPublisher = EngineContext.Current.Resolve(); @@ -115,7 +116,7 @@ protected void Application_Start() HostingEnvironment.RegisterVirtualPathProvider(new PluginDebugViewVirtualPathProvider()); } - // Install filter + // "throw-away" filter for task scheduler initialization (the filter removes itself when processed) GlobalFilters.Filters.Add(new InitializeSchedulerFilter()); } else @@ -160,6 +161,19 @@ public override string GetVaryByCustomString(HttpContext context, string custom) return base.GetVaryByCustomString(context, custom); } - } - + public void AnonymousIdentification_Creating(object sender, AnonymousIdentificationEventArgs args) + { + try + { + var customerService = EngineContext.Current.Resolve(); + var customer = customerService.FindGuestCustomerByClientIdent(maxAgeSeconds: 180); + if (customer != null) + { + // We found our anonymous visitor: don't let ASP.NET create a new id. + args.AnonymousID = customer.CustomerGuid.ToString(); + } + } + catch { } + } + } } diff --git a/src/Presentation/SmartStore.Web/Infrastructure/Cache/ModelCacheEventConsumer.cs b/src/Presentation/SmartStore.Web/Infrastructure/Cache/ModelCacheEventConsumer.cs index 652defa00a..c17e051277 100644 --- a/src/Presentation/SmartStore.Web/Infrastructure/Cache/ModelCacheEventConsumer.cs +++ b/src/Presentation/SmartStore.Web/Infrastructure/Cache/ModelCacheEventConsumer.cs @@ -122,15 +122,16 @@ public partial class ModelCacheEventConsumer: IConsumer>, IConsumer> { - /// - /// Key for ManufacturerNavigationModel caching - /// - /// - /// {0} : current manufacturer id - /// {1} : language id - /// {2} : current store ID - /// - public const string MANUFACTURER_NAVIGATION_MODEL_KEY = "sm.pres.manufacturer.navigation-{0}-{1}-{2}"; + /// + /// Key for ManufacturerNavigationModel caching + /// + /// + /// {0} : current manufacturer id + /// {1} : value indicating whether a default picture is displayed in case if no real picture exists + /// {2} : language id + /// {3} : current store ID + /// + public const string MANUFACTURER_NAVIGATION_MODEL_KEY = "sm.pres.manufacturer.navigation-{0}-{1}-{2}-{3}"; public const string MANUFACTURER_NAVIGATION_PATTERN_KEY = "sm.pres.manufacturer.navigation"; /// @@ -199,15 +200,16 @@ public partial class ModelCacheEventConsumer: public const string PRODUCTTAG_POPULAR_MODEL_KEY = "sm.pres.producttag.popular-{0}-{1}"; public const string PRODUCTTAG_POPULAR_PATTERN_KEY = "sm.pres.producttag.popular"; - /// - /// Key for ProductManufacturers model caching - /// - /// - /// {0} : product id - /// {1} : language id - /// {2} : current store ID - /// - public const string PRODUCT_MANUFACTURERS_MODEL_KEY = "sm.pres.product.manufacturers-{0}-{1}-{2}"; + /// + /// Key for ProductManufacturers model caching + /// + /// + /// {0} : product id + /// {1} : value indicating whether a default picture is displayed in case if no real picture exists + /// {2} : language id + /// {3} : current store ID + /// + public const string PRODUCT_MANUFACTURERS_MODEL_KEY = "sm.pres.product.manufacturers-{0}-{1}-{2}-{3}"; public const string PRODUCT_MANUFACTURERS_PATTERN_KEY = "sm.pres.product.manufacturers"; /// diff --git a/src/Presentation/SmartStore.Web/Infrastructure/DefaultBundles.cs b/src/Presentation/SmartStore.Web/Infrastructure/DefaultBundles.cs index a3ee64e969..d64c2a3d2f 100644 --- a/src/Presentation/SmartStore.Web/Infrastructure/DefaultBundles.cs +++ b/src/Presentation/SmartStore.Web/Infrastructure/DefaultBundles.cs @@ -4,7 +4,7 @@ using System.Web; using System.Web.Optimization; using BundleTransformer.Core.Bundles; -using SmartStore.Web.Framework.Mvc.Bundles; +using SmartStore.Web.Framework.Bundling; namespace SmartStore.Web.Infrastructure { diff --git a/src/Presentation/SmartStore.Web/Infrastructure/Installation/DeDESeedData.cs b/src/Presentation/SmartStore.Web/Infrastructure/Installation/DeDESeedData.cs index 2d75d6749c..3896e5c829 100644 --- a/src/Presentation/SmartStore.Web/Infrastructure/Installation/DeDESeedData.cs +++ b/src/Presentation/SmartStore.Web/Infrastructure/Installation/DeDESeedData.cs @@ -258,7 +258,7 @@ protected override void Alter(IList entities) .Alter("ReturnRequestStatusChanged.CustomerNotification", x => { x.Subject = "%Store.Name%. Rücksendung - Status-Änderung"; - x.Body = templateHeader + "

    %Store.Name% 

    Hallo %Customer.FullName%,

    der Status Ihrer Rücksendung #%ReturnRequest.ID% wurde aktualisiert.

    Mit freundlichen Grüßen,

    Ihr %Store.Name% - Team

    " + templateFooter; + x.Body = templateHeader + "

    %Store.Name% 

    Hallo %Customer.FullName%,

    der Status Ihrer Rücksendung #%ReturnRequest.ID% wurde aktualisiert: %ReturnRequest.Status%

    Mit freundlichen Grüßen,

    Ihr %Store.Name% - Team

    " + templateFooter; }) .Alter("Service.EmailAFriend", x => { diff --git a/src/Presentation/SmartStore.Web/Infrastructure/Installation/InstallationLocalizationService.cs b/src/Presentation/SmartStore.Web/Infrastructure/Installation/InstallationLocalizationService.cs index e9352a710e..04ba8b6e49 100644 --- a/src/Presentation/SmartStore.Web/Infrastructure/Installation/InstallationLocalizationService.cs +++ b/src/Presentation/SmartStore.Web/Infrastructure/Installation/InstallationLocalizationService.cs @@ -53,7 +53,7 @@ public virtual InstallationLanguage GetCurrentLanguage() if (cookie != null && !String.IsNullOrEmpty(cookie.Value)) cookieLanguageCode = cookie.Value; - // ensure it's available (it could be delete since the previous installation) + // ensure it's available (it could be deleted since the previous installation) var availableLanguages = GetAvailableLanguages(); var language = availableLanguages @@ -79,7 +79,7 @@ public virtual InstallationLanguage GetCurrentLanguage() private bool MatchLanguageByCurrentCulture(InstallationLanguage language) { - var curCulture = CultureInfo.GetCultureInfoByIetfLanguageTag("tr-TR"); // Thread.CurrentThread.CurrentCulture; + var curCulture = Thread.CurrentThread.CurrentUICulture; if (language.Code.IsCaseInsensitiveEqual(curCulture.IetfLanguageTag)) return true; diff --git a/src/Presentation/SmartStore.Web/Infrastructure/Routes/1_StoreRoutes.cs b/src/Presentation/SmartStore.Web/Infrastructure/Routes/1_StoreRoutes.cs index fb5b9838e8..d5d5d1413d 100644 --- a/src/Presentation/SmartStore.Web/Infrastructure/Routes/1_StoreRoutes.cs +++ b/src/Presentation/SmartStore.Web/Infrastructure/Routes/1_StoreRoutes.cs @@ -3,7 +3,7 @@ using System.Web.Mvc.Routing.Constraints; using System.Web.Routing; using SmartStore.Web.Framework.Localization; -using SmartStore.Web.Framework.Mvc.Routes; +using SmartStore.Web.Framework.Routing; namespace SmartStore.Web.Infrastructure { diff --git a/src/Presentation/SmartStore.Web/Infrastructure/Routes/2_DefaultRoutes.cs b/src/Presentation/SmartStore.Web/Infrastructure/Routes/2_DefaultRoutes.cs index 3a1c185382..352c05e1fb 100644 --- a/src/Presentation/SmartStore.Web/Infrastructure/Routes/2_DefaultRoutes.cs +++ b/src/Presentation/SmartStore.Web/Infrastructure/Routes/2_DefaultRoutes.cs @@ -6,9 +6,9 @@ using System.Linq; using SmartStore.Web.Controllers; using SmartStore.Web.Framework.Localization; -using SmartStore.Web.Framework.Mvc.Routes; using SmartStore.Web.Framework.Seo; using System.Collections.Generic; +using SmartStore.Web.Framework.Routing; namespace SmartStore.Web.Infrastructure { diff --git a/src/Presentation/SmartStore.Web/Infrastructure/Routes/3_GenericRoutes.cs b/src/Presentation/SmartStore.Web/Infrastructure/Routes/3_GenericRoutes.cs index 626f9f95a0..a3abdc0415 100644 --- a/src/Presentation/SmartStore.Web/Infrastructure/Routes/3_GenericRoutes.cs +++ b/src/Presentation/SmartStore.Web/Infrastructure/Routes/3_GenericRoutes.cs @@ -1,7 +1,7 @@ using System.Web.Mvc; using System.Web.Routing; using SmartStore.Web.Framework.Localization; -using SmartStore.Web.Framework.Mvc.Routes; +using SmartStore.Web.Framework.Routing; using SmartStore.Web.Framework.Seo; namespace SmartStore.Web.Infrastructure diff --git a/src/Presentation/SmartStore.Web/Media/Thumbs/placeholder.txt b/src/Presentation/SmartStore.Web/Media/Thumbs/placeholder.txt index 5f282702bb..0eaf54c3d7 100644 --- a/src/Presentation/SmartStore.Web/Media/Thumbs/placeholder.txt +++ b/src/Presentation/SmartStore.Web/Media/Thumbs/placeholder.txt @@ -1 +1 @@ - \ No newline at end of file +placeholder file \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Media/Uploaded/placeholder.txt b/src/Presentation/SmartStore.Web/Media/Uploaded/placeholder.txt index 5f282702bb..0eaf54c3d7 100644 --- a/src/Presentation/SmartStore.Web/Media/Uploaded/placeholder.txt +++ b/src/Presentation/SmartStore.Web/Media/Uploaded/placeholder.txt @@ -1 +1 @@ - \ No newline at end of file +placeholder file \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Models/Blogs/AddBlogCommentModel.cs b/src/Presentation/SmartStore.Web/Models/Blogs/AddBlogCommentModel.cs index 642bb76372..e3208d7ab0 100644 --- a/src/Presentation/SmartStore.Web/Models/Blogs/AddBlogCommentModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Blogs/AddBlogCommentModel.cs @@ -1,6 +1,6 @@ using System.Web.Mvc; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Blogs { diff --git a/src/Presentation/SmartStore.Web/Models/Blogs/BlogCommentModel.cs b/src/Presentation/SmartStore.Web/Models/Blogs/BlogCommentModel.cs index d7144cb6e0..9de84ec882 100644 --- a/src/Presentation/SmartStore.Web/Models/Blogs/BlogCommentModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Blogs/BlogCommentModel.cs @@ -1,5 +1,5 @@ using System; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Blogs { diff --git a/src/Presentation/SmartStore.Web/Models/Blogs/BlogPostListModel.cs b/src/Presentation/SmartStore.Web/Models/Blogs/BlogPostListModel.cs index de22f36422..7487110110 100644 --- a/src/Presentation/SmartStore.Web/Models/Blogs/BlogPostListModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Blogs/BlogPostListModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Blogs { diff --git a/src/Presentation/SmartStore.Web/Models/Blogs/BlogPostModel.cs b/src/Presentation/SmartStore.Web/Models/Blogs/BlogPostModel.cs index 67b8ee3b2c..7c8df3e2d6 100644 --- a/src/Presentation/SmartStore.Web/Models/Blogs/BlogPostModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Blogs/BlogPostModel.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using FluentValidation.Attributes; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Validators.Blogs; namespace SmartStore.Web.Models.Blogs diff --git a/src/Presentation/SmartStore.Web/Models/Blogs/BlogPostTagListModel.cs b/src/Presentation/SmartStore.Web/Models/Blogs/BlogPostTagListModel.cs index 613df18752..7db54f132a 100644 --- a/src/Presentation/SmartStore.Web/Models/Blogs/BlogPostTagListModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Blogs/BlogPostTagListModel.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Blogs { diff --git a/src/Presentation/SmartStore.Web/Models/Blogs/BlogPostTagModel.cs b/src/Presentation/SmartStore.Web/Models/Blogs/BlogPostTagModel.cs index f774e346f7..ddab728568 100644 --- a/src/Presentation/SmartStore.Web/Models/Blogs/BlogPostTagModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Blogs/BlogPostTagModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Blogs { diff --git a/src/Presentation/SmartStore.Web/Models/Blogs/BlogPostYearMonthModel.cs b/src/Presentation/SmartStore.Web/Models/Blogs/BlogPostYearMonthModel.cs index 54b7ed1cfe..89ea5f646d 100644 --- a/src/Presentation/SmartStore.Web/Models/Blogs/BlogPostYearMonthModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Blogs/BlogPostYearMonthModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Blogs { diff --git a/src/Presentation/SmartStore.Web/Models/Boards/TopicMoveModel.cs b/src/Presentation/SmartStore.Web/Models/Boards/TopicMoveModel.cs index 034fddec45..c88dc4c6b4 100644 --- a/src/Presentation/SmartStore.Web/Models/Boards/TopicMoveModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Boards/TopicMoveModel.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Web.Mvc; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Boards { diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/AddToCompareListModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/AddToCompareListModel.cs index 1bffb79e6e..d493b61c51 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/AddToCompareListModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/AddToCompareListModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Catalog { diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/BackInStockSubscribeModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/BackInStockSubscribeModel.cs index 7bcac2f0ba..d403abbf0d 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/BackInStockSubscribeModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/BackInStockSubscribeModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Catalog { diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/CatalogPagingFilteringModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/CatalogPagingFilteringModel.cs index 9e8da38ee9..39e3c6be9d 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/CatalogPagingFilteringModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/CatalogPagingFilteringModel.cs @@ -2,13 +2,11 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Web.Mvc; using SmartStore.Core; using SmartStore.Core.Domain.Catalog; using SmartStore.Services.Catalog; using SmartStore.Services.Localization; -using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Catalog { diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/CategoryModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/CategoryModel.cs index 52be36ea9b..a09f815652 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/CategoryModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/CategoryModel.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using SmartStore.Core.Domain.Catalog; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Framework.UI; using SmartStore.Web.Models.Media; diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/CompareProductsModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/CompareProductsModel.cs index f7661c0956..1b58526ad5 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/CompareProductsModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/CompareProductsModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Catalog { diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/HomePageBestsellersModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/HomePageBestsellersModel.cs index 3178fc0182..1f6bb34c0e 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/HomePageBestsellersModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/HomePageBestsellersModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Catalog { diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/HomePageProductsModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/HomePageProductsModel.cs index 3e206bdae4..e4a7303d2a 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/HomePageProductsModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/HomePageProductsModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Catalog { diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/ManufacturerModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/ManufacturerModel.cs index 4c9521ba7f..7f8452ad4d 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/ManufacturerModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/ManufacturerModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Models.Media; namespace SmartStore.Web.Models.Catalog diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/ManufacturerNavigationModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/ManufacturerNavigationModel.cs index 1c03322e45..a27b37ba7f 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/ManufacturerNavigationModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/ManufacturerNavigationModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Catalog { diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/ManufacturerOverviewModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/ManufacturerOverviewModel.cs index 3715363b32..523526d25e 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/ManufacturerOverviewModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/ManufacturerOverviewModel.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Models.Media; namespace SmartStore.Web.Models.Catalog diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/NavigationModelBuiltEvent.cs b/src/Presentation/SmartStore.Web/Models/Catalog/NavigationModelBuiltEvent.cs index 4ba7c59c8b..633a453b1f 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/NavigationModelBuiltEvent.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/NavigationModelBuiltEvent.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using SmartStore.Collections; +using SmartStore.Collections; using SmartStore.Web.Framework.UI; namespace SmartStore.Web.Models.Catalog diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/PagingFilteringModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/PagingFilteringModel.cs index 4fd52cee47..b5a339879c 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/PagingFilteringModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/PagingFilteringModel.cs @@ -2,7 +2,7 @@ using System.Web.Mvc; using SmartStore.Core; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Catalog { diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/PopularProductTagsModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/PopularProductTagsModel.cs index 0efd6badc8..65d46aa59e 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/PopularProductTagsModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/PopularProductTagsModel.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Catalog { diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/ProductAskQuestionModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/ProductAskQuestionModel.cs index 1d6a96f40f..c8e8facd98 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/ProductAskQuestionModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/ProductAskQuestionModel.cs @@ -1,7 +1,7 @@ using System.Web.Mvc; using FluentValidation.Attributes; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Validators.Catalog; namespace SmartStore.Web.Models.Catalog diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/ProductDetailsModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/ProductDetailsModel.cs index 5aa0d0a7f5..ab13510f40 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/ProductDetailsModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/ProductDetailsModel.cs @@ -2,9 +2,8 @@ using System.Collections.Generic; using System.Web.Mvc; using SmartStore.Core.Domain.Catalog; -using SmartStore.Core.Domain.Directory; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Framework.UI; using SmartStore.Web.Models.Media; diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/ProductEmailAFriendModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/ProductEmailAFriendModel.cs index fe12f24f7c..77054abb8e 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/ProductEmailAFriendModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/ProductEmailAFriendModel.cs @@ -1,7 +1,7 @@ using System.Web.Mvc; using FluentValidation.Attributes; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Validators.Catalog; namespace SmartStore.Web.Models.Catalog diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/ProductOverviewModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/ProductOverviewModel.cs index 8081c404d1..74d7baecc8 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/ProductOverviewModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/ProductOverviewModel.cs @@ -1,8 +1,6 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; using SmartStore.Web.Models.Media; -using SmartStore.Core; -using SmartStore.Core.Domain.Directory; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Catalog { diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/ProductReviewModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/ProductReviewModel.cs index 17dd5e7a27..0118abf688 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/ProductReviewModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/ProductReviewModel.cs @@ -2,7 +2,7 @@ using System.Web.Mvc; using FluentValidation.Attributes; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Validators.Catalog; namespace SmartStore.Web.Models.Catalog diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/ProductSpecificationModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/ProductSpecificationModel.cs index 1b4b0c98fb..c6436eb6af 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/ProductSpecificationModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/ProductSpecificationModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Catalog { diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/ProductTagModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/ProductTagModel.cs index 155f168818..926a87fa0e 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/ProductTagModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/ProductTagModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Catalog { diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/ProductsByTagModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/ProductsByTagModel.cs index a5c5afc51e..4c312cc1d8 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/ProductsByTagModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/ProductsByTagModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Catalog { diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/RecentlyAddedProductsModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/RecentlyAddedProductsModel.cs index 5ac18739f5..7c3ef212be 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/RecentlyAddedProductsModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/RecentlyAddedProductsModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Catalog { diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/SearchBoxModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/SearchBoxModel.cs index 6bcc77a21d..4d784901ee 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/SearchBoxModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/SearchBoxModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Catalog { diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/SearchModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/SearchModel.cs index 4c3d9e96cb..91904e47fc 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/SearchModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/SearchModel.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Web.Mvc; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Catalog { diff --git a/src/Presentation/SmartStore.Web/Models/Catalog/SearchPagingFilteringModel.cs b/src/Presentation/SmartStore.Web/Models/Catalog/SearchPagingFilteringModel.cs index 99ab88bebf..086054f61f 100644 --- a/src/Presentation/SmartStore.Web/Models/Catalog/SearchPagingFilteringModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Catalog/SearchPagingFilteringModel.cs @@ -1,10 +1,4 @@ -using System.Collections.Generic; -using System.Web.Mvc; -using SmartStore.Core; -using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; - -namespace SmartStore.Web.Models.Catalog +namespace SmartStore.Web.Models.Catalog { public partial class SearchPagingFilteringModel : PagingFilteringModel { diff --git a/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutBillingAddressModel.cs b/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutBillingAddressModel.cs index d5e592dca5..bde0e52a42 100644 --- a/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutBillingAddressModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutBillingAddressModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Models.Common; namespace SmartStore.Web.Models.Checkout diff --git a/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutCompletedModel.cs b/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutCompletedModel.cs index 3920514ded..370be66071 100644 --- a/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutCompletedModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutCompletedModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Checkout { diff --git a/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutConfirmModel.cs b/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutConfirmModel.cs index b553051c80..2239a2e298 100644 --- a/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutConfirmModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutConfirmModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Checkout { @@ -17,6 +17,7 @@ public CheckoutConfirmModel() public IList Warnings { get; set; } public bool ShowConfirmOrderLegalHint { get; set; } + public bool ShowEsdRevocationWaiverBox { get; set; } public bool BypassPaymentMethodInfo { get; set; } } diff --git a/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutPaymentInfoModel.cs b/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutPaymentInfoModel.cs index e08675e71a..2c23a55607 100644 --- a/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutPaymentInfoModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutPaymentInfoModel.cs @@ -1,5 +1,5 @@ using System.Web.Routing; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Checkout { diff --git a/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutPaymentMethodModel.cs b/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutPaymentMethodModel.cs index 8c54334466..b3eba49ab3 100644 --- a/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutPaymentMethodModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutPaymentMethodModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Checkout { diff --git a/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutProgressModel.cs b/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutProgressModel.cs index 8974f5773d..5ef7e38f32 100644 --- a/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutProgressModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutProgressModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Checkout { diff --git a/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutShippingAddressModel.cs b/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutShippingAddressModel.cs index 5a4cac101f..eae1a17753 100644 --- a/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutShippingAddressModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutShippingAddressModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Models.Common; namespace SmartStore.Web.Models.Checkout diff --git a/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutShippingMethodModel.cs b/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutShippingMethodModel.cs index ab1ab0f69a..a1104b942f 100644 --- a/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutShippingMethodModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Checkout/CheckoutShippingMethodModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Checkout { diff --git a/src/Presentation/SmartStore.Web/Models/CheckoutBillingAddressModel.cs b/src/Presentation/SmartStore.Web/Models/CheckoutBillingAddressModel.cs new file mode 100644 index 0000000000..d07e120dff --- /dev/null +++ b/src/Presentation/SmartStore.Web/Models/CheckoutBillingAddressModel.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using SmartStore.Web.Framework.Modelling; +using SmartStore.Web.Models.Common; + +namespace SmartStore.Web.Models.Checkout +{ + public partial class CheckoutBillingAddressModel2 : ModelBase + { + public CheckoutBillingAddressModel2() + { + ExistingAddresses = new List(); + NewAddress = new AddressModel(); + } + + public IList ExistingAddresses { get; set; } + + public AddressModel NewAddress { get; set; } + + /// + /// Used on one-page checkout page + /// + public bool NewAddressPreselected { get; set; } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Models/Common/AccountDropdownModel.cs b/src/Presentation/SmartStore.Web/Models/Common/AccountDropdownModel.cs index 445ff6fc39..46686d6cf9 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/AccountDropdownModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/AccountDropdownModel.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.Web.Mvc; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Common { diff --git a/src/Presentation/SmartStore.Web/Models/Common/AddressModel.cs b/src/Presentation/SmartStore.Web/Models/Common/AddressModel.cs index 5afdc02f9b..a393eb0873 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/AddressModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/AddressModel.cs @@ -1,13 +1,14 @@ using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Web.Mvc; using FluentValidation.Attributes; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Validators.Common; namespace SmartStore.Web.Models.Common { - [Validator(typeof(AddressValidator))] + [Validator(typeof(AddressValidator))] public partial class AddressModel : EntityModelBase { public AddressModel() @@ -26,9 +27,12 @@ public AddressModel() [SmartResourceDisplayName("Address.Fields.Email")] [AllowHtml] - public string Email { get; set; } + [DataType(DataType.EmailAddress)] + public string Email { get; set; } + [SmartResourceDisplayName("Address.Fields.EmailMatch")] - public string EmailMatch { get; set; } + [DataType(DataType.EmailAddress)] + public string EmailMatch { get; set; } public bool ValidateEmailAddress { get; set; } [SmartResourceDisplayName("Address.Fields.Company")] @@ -79,13 +83,15 @@ public AddressModel() [SmartResourceDisplayName("Address.Fields.PhoneNumber")] [AllowHtml] - public string PhoneNumber { get; set; } + [DataType(DataType.PhoneNumber)] + public string PhoneNumber { get; set; } public bool PhoneEnabled { get; set; } public bool PhoneRequired { get; set; } [SmartResourceDisplayName("Address.Fields.FaxNumber")] [AllowHtml] - public string FaxNumber { get; set; } + [DataType(DataType.PhoneNumber)] + public string FaxNumber { get; set; } public bool FaxEnabled { get; set; } public bool FaxRequired { get; set; } diff --git a/src/Presentation/SmartStore.Web/Models/Common/CompareDropdownModel.cs b/src/Presentation/SmartStore.Web/Models/Common/CompareDropdownModel.cs index 8e4c279077..d8d32376fc 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/CompareDropdownModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/CompareDropdownModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Common { diff --git a/src/Presentation/SmartStore.Web/Models/Common/ContactUsModel.cs b/src/Presentation/SmartStore.Web/Models/Common/ContactUsModel.cs index 4965525cea..db9ab838bc 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/ContactUsModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/ContactUsModel.cs @@ -1,17 +1,24 @@ -using System.Web.Mvc; +using System.ComponentModel.DataAnnotations; +using System.Web.Mvc; using FluentValidation.Attributes; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Validators.Common; namespace SmartStore.Web.Models.Common { - [Validator(typeof(ContactUsValidator))] + [Validator(typeof(ContactUsValidator))] public partial class ContactUsModel : ModelBase { + [SmartResourceDisplayName("ContactUs.PrivacyAgreement")] + public bool PrivacyAgreement { get; set; } + + public bool DisplayPrivacyAgreement { get; set; } + [AllowHtml] [SmartResourceDisplayName("ContactUs.Email")] - public string Email { get; set; } + [DataType(DataType.EmailAddress)] + public string Email { get; set; } [AllowHtml] [SmartResourceDisplayName("ContactUs.Enquiry")] diff --git a/src/Presentation/SmartStore.Web/Models/Common/CurrencyModel.cs b/src/Presentation/SmartStore.Web/Models/Common/CurrencyModel.cs index a9a910f39e..f9048e5d37 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/CurrencyModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/CurrencyModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Common { diff --git a/src/Presentation/SmartStore.Web/Models/Common/CurrencySelectorModel.cs b/src/Presentation/SmartStore.Web/Models/Common/CurrencySelectorModel.cs index 7c5b6e0bab..ec2ff8e876 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/CurrencySelectorModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/CurrencySelectorModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Common { diff --git a/src/Presentation/SmartStore.Web/Models/Common/EntityPickerModel.cs b/src/Presentation/SmartStore.Web/Models/Common/EntityPickerModel.cs new file mode 100644 index 0000000000..c83422b7f2 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Models/Common/EntityPickerModel.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Web.Mvc; +using SmartStore.Web.Framework; +using SmartStore.Web.Framework.Modelling; + +namespace SmartStore.Web.Models.Common +{ + public class EntityPickerModel : ModelBase + { + public string AllString { get; set; } + public string PublishedString { get; set; } + public string UnpublishedString { get; set; } + + public string Entity { get; set; } + public bool HighligtSearchTerm { get; set; } + public string DisableIf { get; set; } + public string DisableIds { get; set; } + public string SearchTerm { get; set; } + public string ReturnField { get; set; } + public int MaxReturnValues { get; set; } + public int PageIndex { get; set; } + public int PageSize { get; set; } + + public List SearchResult { get; set; } + + #region Products + + [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchProductName")] + public string ProductName { get; set; } + + [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchCategory")] + public int CategoryId { get; set; } + + [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchManufacturer")] + public int ManufacturerId { get; set; } + + [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchStore")] + public int StoreId { get; set; } + + [SmartResourceDisplayName("Admin.Catalog.Products.List.SearchProductType")] + public int ProductTypeId { get; set; } + + public IList AvailableCategories { get; set; } + public IList AvailableManufacturers { get; set; } + public IList AvailableStores { get; set; } + public IList AvailableProductTypes { get; set; } + + #endregion + + public class SearchResultModel : EntityModelBase + { + public string ReturnValue { get; set; } + public string Title { get; set; } + public string Summary { get; set; } + public string SummaryTitle { get; set; } + public bool? Published { get; set; } + public bool Disable { get; set; } + public string ImageUrl { get; set; } + public string LabelText { get; set; } + public string LabelClassName { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Models/Common/FooterModel.cs b/src/Presentation/SmartStore.Web/Models/Common/FooterModel.cs index 67aea71c20..a6911f9e67 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/FooterModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/FooterModel.cs @@ -1,15 +1,10 @@ -using SmartStore.Web.Framework.Mvc; -using System.Collections.Generic; +using System.Collections.Generic; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Common { public partial class FooterModel : ModelBase { - public FooterModel() - { - Topics = new Dictionary(); - } - public string StoreName { get; set; } public string LegalInfo { get; set; } @@ -20,7 +15,6 @@ public FooterModel() public bool HideNewsletterBlock { get; set; } public bool BlogEnabled { get; set; } public bool ForumEnabled { get; set; } - public Dictionary Topics { get; set; } public bool ShowSocialLinks { get; set; } public string FacebookLink { get; set; } @@ -28,5 +22,7 @@ public FooterModel() public string TwitterLink { get; set; } public string PinterestLink { get; set; } public string YoutubeLink { get; set; } - } + + public Dictionary TopicPageUrls { get; set; } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Models/Common/HeaderLinksModel.cs b/src/Presentation/SmartStore.Web/Models/Common/HeaderLinksModel.cs index f1eb44279c..a3a794535d 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/HeaderLinksModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/HeaderLinksModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Common { diff --git a/src/Presentation/SmartStore.Web/Models/Common/InfoBlockModel.cs b/src/Presentation/SmartStore.Web/Models/Common/InfoBlockModel.cs index 872c97d1d6..5678d3938b 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/InfoBlockModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/InfoBlockModel.cs @@ -1,4 +1,5 @@ -using SmartStore.Web.Framework.Mvc; +using System.Collections.Generic; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Common { @@ -11,5 +12,7 @@ public partial class InfoBlockModel : ModelBase public bool SitemapEnabled { get; set; } public bool ForumEnabled { get; set; } public bool AllowPrivateMessages { get; set; } - } + + public Dictionary TopicPageUrls { get; set; } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Models/Common/LanguageModel.cs b/src/Presentation/SmartStore.Web/Models/Common/LanguageModel.cs index f78b449f5b..fa5d5a0590 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/LanguageModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/LanguageModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Common { diff --git a/src/Presentation/SmartStore.Web/Models/Common/LanguageSelectorModel.cs b/src/Presentation/SmartStore.Web/Models/Common/LanguageSelectorModel.cs index 0b26742a2c..a869b91166 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/LanguageSelectorModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/LanguageSelectorModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Common { diff --git a/src/Presentation/SmartStore.Web/Models/Common/MenuModel.cs b/src/Presentation/SmartStore.Web/Models/Common/MenuModel.cs index d461318081..c906dc18ea 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/MenuModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/MenuModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Common { @@ -13,8 +13,11 @@ public partial class MenuModel : ModelBase public int UnreadPrivateMessages { get; set; } public bool IsAuthenticated { get; set; } - public bool DisplayAdminLink { get; set; } + public bool DisplayLoginLink { get; set; } + public bool DisplayAdminLink { get; set; } public bool IsCustomerImpersonated { get; set; } public string CustomerEmailUsername { get; set; } - } + + public bool HasContactUsPage { get; set; } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Models/Common/PagerModel.cs b/src/Presentation/SmartStore.Web/Models/Common/PagerModel.cs index d42a70eaf7..a55ba04714 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/PagerModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/PagerModel.cs @@ -1,7 +1,4 @@ -using SmartStore.Core.Infrastructure; -using SmartStore.Services.Localization; - -namespace SmartStore.Web.Models.Common +namespace SmartStore.Web.Models.Common { #region Classes diff --git a/src/Presentation/SmartStore.Web/Models/Common/PaymentInfoModel.cs b/src/Presentation/SmartStore.Web/Models/Common/PaymentInfoModel.cs index 3a8460e97f..f550571863 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/PaymentInfoModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/PaymentInfoModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Models.Catalog; using SmartStore.Web.Models.Topics; diff --git a/src/Presentation/SmartStore.Web/Models/Common/ShipmentInfoModel.cs b/src/Presentation/SmartStore.Web/Models/Common/ShipmentInfoModel.cs index b36a289de5..0567d5606a 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/ShipmentInfoModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/ShipmentInfoModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Models.Catalog; using SmartStore.Web.Models.Topics; diff --git a/src/Presentation/SmartStore.Web/Models/Common/ShopBarModel.cs b/src/Presentation/SmartStore.Web/Models/Common/ShopBarModel.cs index 85a41134de..e517dc9c6c 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/ShopBarModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/ShopBarModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Common { diff --git a/src/Presentation/SmartStore.Web/Models/Common/ShopHeaderModel.cs b/src/Presentation/SmartStore.Web/Models/Common/ShopHeaderModel.cs index 4aa61f8404..f52360b057 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/ShopHeaderModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/ShopHeaderModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Common { diff --git a/src/Presentation/SmartStore.Web/Models/Common/ShopLogoModel.cs b/src/Presentation/SmartStore.Web/Models/Common/ShopLogoModel.cs index 34daebea13..10e4f6e8a4 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/ShopLogoModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/ShopLogoModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Common { diff --git a/src/Presentation/SmartStore.Web/Models/Common/SitemapModel.cs b/src/Presentation/SmartStore.Web/Models/Common/SitemapModel.cs index e32142a5ab..0f24e7c67b 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/SitemapModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/SitemapModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Models.Catalog; using SmartStore.Web.Models.Topics; diff --git a/src/Presentation/SmartStore.Web/Models/Common/StoreThemeModel.cs b/src/Presentation/SmartStore.Web/Models/Common/StoreThemeModel.cs index d7472f77f8..e2d777357f 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/StoreThemeModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/StoreThemeModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Common { diff --git a/src/Presentation/SmartStore.Web/Models/Common/StoreThemeSelectorModel.cs b/src/Presentation/SmartStore.Web/Models/Common/StoreThemeSelectorModel.cs index f842b79886..51f428e3b9 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/StoreThemeSelectorModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/StoreThemeSelectorModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Common { diff --git a/src/Presentation/SmartStore.Web/Models/Common/TaxTypeSelectorModel.cs b/src/Presentation/SmartStore.Web/Models/Common/TaxTypeSelectorModel.cs index 12e6fb41ea..a28efcbc24 100644 --- a/src/Presentation/SmartStore.Web/Models/Common/TaxTypeSelectorModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Common/TaxTypeSelectorModel.cs @@ -1,5 +1,5 @@ using SmartStore.Core.Domain.Tax; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Common { diff --git a/src/Presentation/SmartStore.Web/Models/Customer/AccountActivationModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/AccountActivationModel.cs index 1405143edc..16596efe64 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/AccountActivationModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/AccountActivationModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Customer { diff --git a/src/Presentation/SmartStore.Web/Models/Customer/BackInStockSubscriptionModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/BackInStockSubscriptionModel.cs index 7709196b2a..2248c1942a 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/BackInStockSubscriptionModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/BackInStockSubscriptionModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Customer { diff --git a/src/Presentation/SmartStore.Web/Models/Customer/ChangePasswordModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/ChangePasswordModel.cs index 4c7eacc63f..cb580e2105 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/ChangePasswordModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/ChangePasswordModel.cs @@ -2,7 +2,7 @@ using System.Web.Mvc; using FluentValidation.Attributes; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Validators.Customer; namespace SmartStore.Web.Models.Customer diff --git a/src/Presentation/SmartStore.Web/Models/Customer/CustomerAddressEditModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/CustomerAddressEditModel.cs index e5805e1d0d..370286d602 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/CustomerAddressEditModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/CustomerAddressEditModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Models.Common; namespace SmartStore.Web.Models.Customer diff --git a/src/Presentation/SmartStore.Web/Models/Customer/CustomerAddressListModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/CustomerAddressListModel.cs index 6c405db530..761ad41556 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/CustomerAddressListModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/CustomerAddressListModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Models.Common; namespace SmartStore.Web.Models.Customer diff --git a/src/Presentation/SmartStore.Web/Models/Customer/CustomerAvatarModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/CustomerAvatarModel.cs index 09e79682b4..2aeedc3e16 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/CustomerAvatarModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/CustomerAvatarModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Customer { diff --git a/src/Presentation/SmartStore.Web/Models/Customer/CustomerBackInStockSubscriptionsModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/CustomerBackInStockSubscriptionsModel.cs index c85eada465..1087058c29 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/CustomerBackInStockSubscriptionsModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/CustomerBackInStockSubscriptionsModel.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using SmartStore.Core; -using SmartStore.Web.Models.Common; namespace SmartStore.Web.Models.Customer { diff --git a/src/Presentation/SmartStore.Web/Models/Customer/CustomerDownloadableProductsModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/CustomerDownloadableProductsModel.cs index 3d5faaeede..77fffc7a39 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/CustomerDownloadableProductsModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/CustomerDownloadableProductsModel.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Customer { diff --git a/src/Presentation/SmartStore.Web/Models/Customer/CustomerForumSubscriptionsModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/CustomerForumSubscriptionsModel.cs index d1d741cb24..bab1809345 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/CustomerForumSubscriptionsModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/CustomerForumSubscriptionsModel.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using SmartStore.Core; -using SmartStore.Web.Models.Common; namespace SmartStore.Web.Models.Customer { diff --git a/src/Presentation/SmartStore.Web/Models/Customer/CustomerInfoModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/CustomerInfoModel.cs index 8b02cc2e31..80e9ddafef 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/CustomerInfoModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/CustomerInfoModel.cs @@ -1,13 +1,14 @@ using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Web.Mvc; using FluentValidation.Attributes; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Validators.Customer; namespace SmartStore.Web.Models.Customer { - [Validator(typeof(CustomerInfoValidator))] + [Validator(typeof(CustomerInfoValidator))] public partial class CustomerInfoModel : ModelBase { public CustomerInfoModel() @@ -20,7 +21,8 @@ public CustomerInfoModel() [SmartResourceDisplayName("Account.Fields.Email")] [AllowHtml] - public string Email { get; set; } + [DataType(DataType.EmailAddress)] + public string Email { get; set; } [SmartResourceDisplayName("Account.Fields.CustomerNumber")] [AllowHtml] @@ -100,13 +102,15 @@ public CustomerInfoModel() public bool PhoneRequired { get; set; } [SmartResourceDisplayName("Account.Fields.Phone")] [AllowHtml] - public string Phone { get; set; } + [DataType(DataType.PhoneNumber)] + public string Phone { get; set; } public bool FaxEnabled { get; set; } public bool FaxRequired { get; set; } [SmartResourceDisplayName("Account.Fields.Fax")] [AllowHtml] - public string Fax { get; set; } + [DataType(DataType.PhoneNumber)] + public string Fax { get; set; } public bool NewsletterEnabled { get; set; } [SmartResourceDisplayName("Account.Fields.Newsletter")] diff --git a/src/Presentation/SmartStore.Web/Models/Customer/CustomerNavigationModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/CustomerNavigationModel.cs index 533025a128..aea5297338 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/CustomerNavigationModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/CustomerNavigationModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Customer { diff --git a/src/Presentation/SmartStore.Web/Models/Customer/CustomerOrderListModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/CustomerOrderListModel.cs index c3e278616e..eebb536d65 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/CustomerOrderListModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/CustomerOrderListModel.cs @@ -1,19 +1,19 @@ using System; using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Core; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Customer { - public partial class CustomerOrderListModel : ModelBase + public partial class CustomerOrderListModel : ModelBase { public CustomerOrderListModel() { - Orders = new List(); - RecurringOrders = new List(); - CancelRecurringPaymentErrors = new List(); + RecurringOrders = new List(); + CancelRecurringPaymentErrors = new List(); } - public IList Orders { get; set; } + public PagedList Orders { get; set; } public IList RecurringOrders { get; set; } public IList CancelRecurringPaymentErrors { get; set; } @@ -21,6 +21,7 @@ public CustomerOrderListModel() #region Nested classes + public partial class OrderDetailsModel : EntityModelBase { public string OrderNumber { get; set; } @@ -29,6 +30,7 @@ public partial class OrderDetailsModel : EntityModelBase public string OrderStatus { get; set; } public DateTime CreatedOn { get; set; } } + public partial class RecurringOrderModel : EntityModelBase { public string StartDate { get; set; } @@ -39,6 +41,7 @@ public partial class RecurringOrderModel : EntityModelBase public int InitialOrderId { get; set; } public bool CanCancel { get; set; } } + #endregion } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Models/Customer/CustomerReturnRequestsModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/CustomerReturnRequestsModel.cs index 227ac02b46..0e302ec53e 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/CustomerReturnRequestsModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/CustomerReturnRequestsModel.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Customer { diff --git a/src/Presentation/SmartStore.Web/Models/Customer/CustomerRewardPointsModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/CustomerRewardPointsModel.cs index 22b9ccc117..c37089ddae 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/CustomerRewardPointsModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/CustomerRewardPointsModel.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Customer { diff --git a/src/Presentation/SmartStore.Web/Models/Customer/ExternalAuthenticationMethodModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/ExternalAuthenticationMethodModel.cs index d85169b975..c9a81bc26b 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/ExternalAuthenticationMethodModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/ExternalAuthenticationMethodModel.cs @@ -1,5 +1,5 @@ using System.Web.Routing; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Customer { diff --git a/src/Presentation/SmartStore.Web/Models/Customer/ForumSubscriptionModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/ForumSubscriptionModel.cs index 499092c518..a899d2cb22 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/ForumSubscriptionModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/ForumSubscriptionModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Customer { diff --git a/src/Presentation/SmartStore.Web/Models/Customer/LoginModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/LoginModel.cs index 12f55b6a38..c99aaf03e2 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/LoginModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/LoginModel.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.Web.Mvc; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Customer { diff --git a/src/Presentation/SmartStore.Web/Models/Customer/PasswordRecoveryConfirmModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/PasswordRecoveryConfirmModel.cs index ed22787350..a31e5d72c1 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/PasswordRecoveryConfirmModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/PasswordRecoveryConfirmModel.cs @@ -2,7 +2,7 @@ using System.Web.Mvc; using FluentValidation.Attributes; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Validators.Customer; namespace SmartStore.Web.Models.Customer diff --git a/src/Presentation/SmartStore.Web/Models/Customer/PaswordRecoveryModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/PaswordRecoveryModel.cs index cea324b284..b8581117d4 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/PaswordRecoveryModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/PaswordRecoveryModel.cs @@ -1,17 +1,19 @@ -using System.Web.Mvc; +using System.ComponentModel.DataAnnotations; +using System.Web.Mvc; using FluentValidation.Attributes; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Validators.Customer; namespace SmartStore.Web.Models.Customer { - [Validator(typeof(PasswordRecoveryValidator))] + [Validator(typeof(PasswordRecoveryValidator))] public partial class PasswordRecoveryModel : ModelBase { [AllowHtml] [SmartResourceDisplayName("Account.PasswordRecovery.Email")] - public string Email { get; set; } + [DataType(DataType.EmailAddress)] + public string Email { get; set; } public string Result { get; set; } } diff --git a/src/Presentation/SmartStore.Web/Models/Customer/RegisterModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/RegisterModel.cs index 2a9e1b44a4..4d1a445e2d 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/RegisterModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/RegisterModel.cs @@ -3,7 +3,7 @@ using System.Web.Mvc; using FluentValidation.Attributes; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Validators.Customer; namespace SmartStore.Web.Models.Customer @@ -20,7 +20,8 @@ public RegisterModel() [SmartResourceDisplayName("Account.Fields.Email")] [AllowHtml] - public string Email { get; set; } + [DataType(DataType.EmailAddress)] + public string Email { get; set; } public bool UsernamesEnabled { get; set; } [SmartResourceDisplayName("Account.Fields.Username")] @@ -104,13 +105,15 @@ public RegisterModel() public bool PhoneRequired { get; set; } [SmartResourceDisplayName("Account.Fields.Phone")] [AllowHtml] - public string Phone { get; set; } + [DataType(DataType.PhoneNumber)] + public string Phone { get; set; } public bool FaxEnabled { get; set; } public bool FaxRequired { get; set; } [SmartResourceDisplayName("Account.Fields.Fax")] [AllowHtml] - public string Fax { get; set; } + [DataType(DataType.PhoneNumber)] + public string Fax { get; set; } public bool NewsletterEnabled { get; set; } [SmartResourceDisplayName("Account.Fields.Newsletter")] @@ -127,6 +130,7 @@ public RegisterModel() public string VatNumber { get; set; } public string VatNumberStatusNote { get; set; } public bool DisplayVatNumber { get; set; } + public bool VatRequired { get; set; } public bool DisplayCaptcha { get; set; } } diff --git a/src/Presentation/SmartStore.Web/Models/Customer/RegisterResultModel.cs b/src/Presentation/SmartStore.Web/Models/Customer/RegisterResultModel.cs index 6942031829..9330b13339 100644 --- a/src/Presentation/SmartStore.Web/Models/Customer/RegisterResultModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Customer/RegisterResultModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Customer { diff --git a/src/Presentation/SmartStore.Web/Models/Filter/ProductFilterModel.cs b/src/Presentation/SmartStore.Web/Models/Filter/ProductFilterModel.cs index ba8a02bac5..a53bd91670 100644 --- a/src/Presentation/SmartStore.Web/Models/Filter/ProductFilterModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Filter/ProductFilterModel.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; -using SmartStore.Web.Framework.Mvc; -using SmartStore.Services.Filter; +using SmartStore.Services.Filter; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Filter { diff --git a/src/Presentation/SmartStore.Web/Models/Install/InstallModel.cs b/src/Presentation/SmartStore.Web/Models/Install/InstallModel.cs index 8da7d84e94..c50cac8d06 100644 --- a/src/Presentation/SmartStore.Web/Models/Install/InstallModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Install/InstallModel.cs @@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations; using System.Web.Mvc; using FluentValidation.Attributes; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Validators.Install; namespace SmartStore.Web.Models.Install diff --git a/src/Presentation/SmartStore.Web/Models/Media/PictureModel.cs b/src/Presentation/SmartStore.Web/Models/Media/PictureModel.cs index c07f995020..352676256c 100644 --- a/src/Presentation/SmartStore.Web/Models/Media/PictureModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Media/PictureModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Media { diff --git a/src/Presentation/SmartStore.Web/Models/News/AddNewsCommentModel.cs b/src/Presentation/SmartStore.Web/Models/News/AddNewsCommentModel.cs index a990d4ba86..8fd2495661 100644 --- a/src/Presentation/SmartStore.Web/Models/News/AddNewsCommentModel.cs +++ b/src/Presentation/SmartStore.Web/Models/News/AddNewsCommentModel.cs @@ -1,6 +1,6 @@ using System.Web.Mvc; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.News { diff --git a/src/Presentation/SmartStore.Web/Models/News/HomePageNewsItemsModel.cs b/src/Presentation/SmartStore.Web/Models/News/HomePageNewsItemsModel.cs index 4439f524f3..2462025a1d 100644 --- a/src/Presentation/SmartStore.Web/Models/News/HomePageNewsItemsModel.cs +++ b/src/Presentation/SmartStore.Web/Models/News/HomePageNewsItemsModel.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.News { diff --git a/src/Presentation/SmartStore.Web/Models/News/NewsCommentModel.cs b/src/Presentation/SmartStore.Web/Models/News/NewsCommentModel.cs index d6ca5c0f54..0be2e50419 100644 --- a/src/Presentation/SmartStore.Web/Models/News/NewsCommentModel.cs +++ b/src/Presentation/SmartStore.Web/Models/News/NewsCommentModel.cs @@ -1,5 +1,5 @@ using System; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.News { diff --git a/src/Presentation/SmartStore.Web/Models/News/NewsItemListModel.cs b/src/Presentation/SmartStore.Web/Models/News/NewsItemListModel.cs index ca2afa0f8e..ff5ba23165 100644 --- a/src/Presentation/SmartStore.Web/Models/News/NewsItemListModel.cs +++ b/src/Presentation/SmartStore.Web/Models/News/NewsItemListModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.News { diff --git a/src/Presentation/SmartStore.Web/Models/News/NewsItemModel.cs b/src/Presentation/SmartStore.Web/Models/News/NewsItemModel.cs index fadd949b5d..3870d06898 100644 --- a/src/Presentation/SmartStore.Web/Models/News/NewsItemModel.cs +++ b/src/Presentation/SmartStore.Web/Models/News/NewsItemModel.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using FluentValidation.Attributes; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Validators.News; namespace SmartStore.Web.Models.News diff --git a/src/Presentation/SmartStore.Web/Models/Newsletter/NewsletterBoxModel.cs b/src/Presentation/SmartStore.Web/Models/Newsletter/NewsletterBoxModel.cs index d74ee82698..d0a54a2571 100644 --- a/src/Presentation/SmartStore.Web/Models/Newsletter/NewsletterBoxModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Newsletter/NewsletterBoxModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Newsletter { diff --git a/src/Presentation/SmartStore.Web/Models/Newsletter/SubscriptionActivationModel.cs b/src/Presentation/SmartStore.Web/Models/Newsletter/SubscriptionActivationModel.cs index 3494a8dbdf..582c127a29 100644 --- a/src/Presentation/SmartStore.Web/Models/Newsletter/SubscriptionActivationModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Newsletter/SubscriptionActivationModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Newsletter { diff --git a/src/Presentation/SmartStore.Web/Models/Order/OrderDetailsModel.cs b/src/Presentation/SmartStore.Web/Models/Order/OrderDetailsModel.cs index ec3b9eed33..c4b4773f3a 100644 --- a/src/Presentation/SmartStore.Web/Models/Order/OrderDetailsModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Order/OrderDetailsModel.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Common; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Models.Common; namespace SmartStore.Web.Models.Order @@ -126,7 +126,8 @@ public partial class GiftCard : ModelBase { public string CouponCode { get; set; } public string Amount { get; set; } - } + public string Remaining { get; set; } + } public partial class OrderNote : ModelBase { diff --git a/src/Presentation/SmartStore.Web/Models/Order/ShipmentDetailsModel.cs b/src/Presentation/SmartStore.Web/Models/Order/ShipmentDetailsModel.cs index c96764835b..c935a596f1 100644 --- a/src/Presentation/SmartStore.Web/Models/Order/ShipmentDetailsModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Order/ShipmentDetailsModel.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Order { diff --git a/src/Presentation/SmartStore.Web/Models/Order/SubmitReturnRequestModel.cs b/src/Presentation/SmartStore.Web/Models/Order/SubmitReturnRequestModel.cs index 1c8ff2f007..5e5e837a55 100644 --- a/src/Presentation/SmartStore.Web/Models/Order/SubmitReturnRequestModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Order/SubmitReturnRequestModel.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Web.Mvc; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Order { diff --git a/src/Presentation/SmartStore.Web/Models/Polls/PollModel.cs b/src/Presentation/SmartStore.Web/Models/Polls/PollModel.cs index 0475127251..f4fa173939 100644 --- a/src/Presentation/SmartStore.Web/Models/Polls/PollModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Polls/PollModel.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Polls { diff --git a/src/Presentation/SmartStore.Web/Models/PrivateMessages/PrivateMessageListModel.cs b/src/Presentation/SmartStore.Web/Models/PrivateMessages/PrivateMessageListModel.cs index 07e7275191..ebe3930685 100644 --- a/src/Presentation/SmartStore.Web/Models/PrivateMessages/PrivateMessageListModel.cs +++ b/src/Presentation/SmartStore.Web/Models/PrivateMessages/PrivateMessageListModel.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using SmartStore.Core; -using SmartStore.Web.Models.Common; namespace SmartStore.Web.Models.PrivateMessages { diff --git a/src/Presentation/SmartStore.Web/Models/PrivateMessages/PrivateMessageModel.cs b/src/Presentation/SmartStore.Web/Models/PrivateMessages/PrivateMessageModel.cs index a383d89518..f30fc6ea1f 100644 --- a/src/Presentation/SmartStore.Web/Models/PrivateMessages/PrivateMessageModel.cs +++ b/src/Presentation/SmartStore.Web/Models/PrivateMessages/PrivateMessageModel.cs @@ -1,6 +1,6 @@ using System; using FluentValidation.Attributes; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Validators.PrivateMessages; namespace SmartStore.Web.Models.PrivateMessages diff --git a/src/Presentation/SmartStore.Web/Models/PrivateMessages/SendPrivateMessageModel.cs b/src/Presentation/SmartStore.Web/Models/PrivateMessages/SendPrivateMessageModel.cs index da8fdf3241..0736872b33 100644 --- a/src/Presentation/SmartStore.Web/Models/PrivateMessages/SendPrivateMessageModel.cs +++ b/src/Presentation/SmartStore.Web/Models/PrivateMessages/SendPrivateMessageModel.cs @@ -1,6 +1,6 @@ using System.Web.Mvc; using FluentValidation.Attributes; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Validators.PrivateMessages; namespace SmartStore.Web.Models.PrivateMessages diff --git a/src/Presentation/SmartStore.Web/Models/Profile/ProfilePostsModel.cs b/src/Presentation/SmartStore.Web/Models/Profile/ProfilePostsModel.cs index 6c8b5fb70e..be4da001b8 100644 --- a/src/Presentation/SmartStore.Web/Models/Profile/ProfilePostsModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Profile/ProfilePostsModel.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using SmartStore.Core; -using SmartStore.Web.Models.Common; namespace SmartStore.Web.Models.Profile { diff --git a/src/Presentation/SmartStore.Web/Models/ShoppingCart/ButtonPaymentMethodModel.cs b/src/Presentation/SmartStore.Web/Models/ShoppingCart/ButtonPaymentMethodModel.cs index 8949d991a4..1282a50501 100644 --- a/src/Presentation/SmartStore.Web/Models/ShoppingCart/ButtonPaymentMethodModel.cs +++ b/src/Presentation/SmartStore.Web/Models/ShoppingCart/ButtonPaymentMethodModel.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Web.Routing; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.ShoppingCart { diff --git a/src/Presentation/SmartStore.Web/Models/ShoppingCart/EstimateShippingModel.cs b/src/Presentation/SmartStore.Web/Models/ShoppingCart/EstimateShippingModel.cs index c540bf5709..d0f241a1e1 100644 --- a/src/Presentation/SmartStore.Web/Models/ShoppingCart/EstimateShippingModel.cs +++ b/src/Presentation/SmartStore.Web/Models/ShoppingCart/EstimateShippingModel.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Web.Mvc; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.ShoppingCart { @@ -18,6 +18,8 @@ public EstimateShippingModel() public bool Enabled { get; set; } + public string ShippingInfoUrl { get; set; } + public IList ShippingOptions { get; set; } public IList Warnings { get; set; } diff --git a/src/Presentation/SmartStore.Web/Models/ShoppingCart/MiniShoppingCartModel.cs b/src/Presentation/SmartStore.Web/Models/ShoppingCart/MiniShoppingCartModel.cs index 8a2aac28bc..6f69774664 100644 --- a/src/Presentation/SmartStore.Web/Models/ShoppingCart/MiniShoppingCartModel.cs +++ b/src/Presentation/SmartStore.Web/Models/ShoppingCart/MiniShoppingCartModel.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; -using SmartStore.Core.Domain.Catalog; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Models.Media; namespace SmartStore.Web.Models.ShoppingCart diff --git a/src/Presentation/SmartStore.Web/Models/ShoppingCart/OrderTotalsModel.cs b/src/Presentation/SmartStore.Web/Models/ShoppingCart/OrderTotalsModel.cs index c5fefda507..b1f0826ee6 100644 --- a/src/Presentation/SmartStore.Web/Models/ShoppingCart/OrderTotalsModel.cs +++ b/src/Presentation/SmartStore.Web/Models/ShoppingCart/OrderTotalsModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.ShoppingCart { diff --git a/src/Presentation/SmartStore.Web/Models/ShoppingCart/ShoppingCartModel.cs b/src/Presentation/SmartStore.Web/Models/ShoppingCart/ShoppingCartModel.cs index 35e9ab459e..e8c08f9810 100644 --- a/src/Presentation/SmartStore.Web/Models/ShoppingCart/ShoppingCartModel.cs +++ b/src/Presentation/SmartStore.Web/Models/ShoppingCart/ShoppingCartModel.cs @@ -1,7 +1,8 @@ using System.Collections.Generic; using System.Web.Mvc; using SmartStore.Core.Domain.Catalog; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Core.Domain.Orders; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Models.Common; using SmartStore.Web.Models.Media; @@ -50,10 +51,20 @@ public ShoppingCartModel() public bool DisplayCommentBox { get; set; } public string CustomerComment { get; set; } + public string MeasureUnitName { get; set; } + + public CheckoutNewsLetterSubscription NewsLetterSubscription { get; set; } + public bool? SubscribeToNewsLetter { get; set; } + + public CheckoutThirdPartyEmailHandOver ThirdPartyEmailHandOver { get; set; } + public string ThirdPartyEmailHandOverLabel { get; set; } + public bool? AcceptThirdPartyEmailHandOver { get; set; } + + public bool DisplayEsdRevocationWaiverBox { get; set; } #region Nested Classes - public partial class ShoppingCartItemModel : EntityModelBase + public partial class ShoppingCartItemModel : EntityModelBase { public ShoppingCartItemModel() { @@ -108,6 +119,11 @@ public ShoppingCartItemModel() public string BasePrice { get; set; } + public bool IsDownload { get; set; } + public bool HasUserAgreement { get; set; } + + public bool IsEsd { get; set; } + public bool BundlePerItemPricing { get; set; } public bool BundlePerItemShoppingCart { get; set; } public BundleItemModel BundleItem { get; set; } diff --git a/src/Presentation/SmartStore.Web/Models/ShoppingCart/WishlistEmailAFriendModel.cs b/src/Presentation/SmartStore.Web/Models/ShoppingCart/WishlistEmailAFriendModel.cs index aa515a6d0b..4c87a4991b 100644 --- a/src/Presentation/SmartStore.Web/Models/ShoppingCart/WishlistEmailAFriendModel.cs +++ b/src/Presentation/SmartStore.Web/Models/ShoppingCart/WishlistEmailAFriendModel.cs @@ -1,7 +1,7 @@ using System.Web.Mvc; using FluentValidation.Attributes; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Validators.ShoppingCart; namespace SmartStore.Web.Models.ShoppingCart diff --git a/src/Presentation/SmartStore.Web/Models/ShoppingCart/WishlistModel.cs b/src/Presentation/SmartStore.Web/Models/ShoppingCart/WishlistModel.cs index dec0395d63..2ceba4c312 100644 --- a/src/Presentation/SmartStore.Web/Models/ShoppingCart/WishlistModel.cs +++ b/src/Presentation/SmartStore.Web/Models/ShoppingCart/WishlistModel.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Web.Mvc; using SmartStore.Core.Domain.Catalog; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Models.Media; namespace SmartStore.Web.Models.ShoppingCart diff --git a/src/Presentation/SmartStore.Web/Models/Topics/TopicModel.cs b/src/Presentation/SmartStore.Web/Models/Topics/TopicModel.cs index 0b226b4318..96548a4559 100644 --- a/src/Presentation/SmartStore.Web/Models/Topics/TopicModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Topics/TopicModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Topics { @@ -21,5 +21,7 @@ public partial class TopicModel : EntityModelBase public string MetaTitle { get; set; } public string TitleTag { get; set; } - } + + public bool RenderAsWidget { get; set; } + } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Models/Topics/TopicWidgetModel.cs b/src/Presentation/SmartStore.Web/Models/Topics/TopicWidgetModel.cs index a0567bcef5..07d465763c 100644 --- a/src/Presentation/SmartStore.Web/Models/Topics/TopicWidgetModel.cs +++ b/src/Presentation/SmartStore.Web/Models/Topics/TopicWidgetModel.cs @@ -1,5 +1,4 @@ -using System.Web.Routing; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.Web.Models.Topics { diff --git a/src/Presentation/SmartStore.Web/Scripts/_references.js b/src/Presentation/SmartStore.Web/Scripts/_references.js index ea01b5303a..54be21233f 100644 Binary files a/src/Presentation/SmartStore.Web/Scripts/_references.js and b/src/Presentation/SmartStore.Web/Scripts/_references.js differ diff --git a/src/Presentation/SmartStore.Web/Scripts/public.product-detail.js b/src/Presentation/SmartStore.Web/Scripts/public.product-detail.js index d36f75d8f4..90e9d734aa 100644 --- a/src/Presentation/SmartStore.Web/Scripts/public.product-detail.js +++ b/src/Presentation/SmartStore.Web/Scripts/public.product-detail.js @@ -25,15 +25,25 @@ // update product data and gallery $(el).find(':input').change(function () { + var inputType = $(this).attr('type'); + if (inputType && (inputType === 'file' || inputType === 'submit')) + return this; + var context = $(this).closest('.update-container'); - if (context[0]) { // associated or bundled item + if (context[0]) { } else { context = el; } - context.doAjax({ + var url = context.attr('data-url'); + if (!url) { + return this; + } + + $({}).doAjax({ + url: url, data: context.find(':input').serialize(), callbackSuccess: function (response) { self.updateDetailData(response, context); diff --git a/src/Presentation/SmartStore.Web/Scripts/public.product-detail.mobile.js b/src/Presentation/SmartStore.Web/Scripts/public.product-detail.mobile.js index e15764dc15..3f4d6c1aa3 100644 --- a/src/Presentation/SmartStore.Web/Scripts/public.product-detail.mobile.js +++ b/src/Presentation/SmartStore.Web/Scripts/public.product-detail.mobile.js @@ -16,17 +16,27 @@ var opts = this.options; - // update product data and gallery - $(el).find(':input').change(function () { + // update product data. grouped product not supported because associated product has no mobile view. + $(el).find(':input').change(function () { + var inputType = $(this).attr('type'); + if (inputType && (inputType === 'file' || inputType === 'submit')) + return this; + var context = $(this).closest('.update-container'); - if (context[0]) { // associated or bundled item + if (context[0]) { } else { context = el; } - context.doAjax({ + var url = context.attr('data-url'); + if (!url) { + return this; + } + + $({}).doAjax({ + url: url, data: context.find(':input').serialize(), callbackSuccess: function (response) { self.updateDetailData(response, context); diff --git a/src/Presentation/SmartStore.Web/Scripts/public.product-list-scroller.js b/src/Presentation/SmartStore.Web/Scripts/public.product-list-scroller.js index c0a489774d..0878cdc0b8 100644 --- a/src/Presentation/SmartStore.Web/Scripts/public.product-list-scroller.js +++ b/src/Presentation/SmartStore.Web/Scripts/public.product-list-scroller.js @@ -16,7 +16,7 @@ list.evenIfHidden(function(el) { var visibleElemnts = parseInt(list.outerWidth() / list.find(".item-box:first").outerWidth()); - visibleElemnts = (visibleElemnts == 0) ? 1 : (visibleElemnts - 1); + visibleElemnts = (visibleElemnts >= 1) ? 1 : (visibleElemnts - 1); list.find('.pl-row').wrap('
    ') ; diff --git a/src/Presentation/SmartStore.Web/Scripts/public.shopbar.js b/src/Presentation/SmartStore.Web/Scripts/public.shopbar.js index 1ae9feb17b..f28bf1c9a5 100644 --- a/src/Presentation/SmartStore.Web/Scripts/public.shopbar.js +++ b/src/Presentation/SmartStore.Web/Scripts/public.shopbar.js @@ -117,8 +117,8 @@ var ShopBar = (function($) { var tool = tools[data.type]; var items = $(data.src).closest(".items"); if (items.length) { - // deletion occuured within the dropdown itself, so reload html! - items.throbber({ white: true, small: true }); + // deletion occured within the dropdown itself, so reload html! + items.throbber({ white: true, small: true, message: '' }); ShopBar.loadHtml(tool, true, function () { //items.data("throbber").hide(); }); @@ -179,19 +179,28 @@ var ShopBar = (function($) { var tool = _.isString(type) ? tools[type] : type; if (!tool) return; + var cnt = tool.find('.shopbar-flyout'); + var spinner = cnt.find('.spinner-container'); + if (spinner.length === 0) { + spinner = $('
    ').appendTo(cnt).append(createCircularSpinner(32)); + } + if (!keepOpen) { tool.removeClass("loaded").addClass("loading"); + spinner.addClass("active"); } - var cnt = tool.find('.shopbar-flyout'); + $.ajax({ cache: false, type: "POST", url: cnt.data("href"), success: function (data) { - cnt.empty().html(data); + cnt.find('.shopbar-flyout-inner').remove(); + cnt.append(data); }, complete: function (jqXHR, textStatus) { tool.removeClass("loading").addClass("loaded"); + spinner.removeClass("active"); if (_.isFunction(fn)) { fn.apply(this); } diff --git a/src/Presentation/SmartStore.Web/Scripts/smartstore.common.js b/src/Presentation/SmartStore.Web/Scripts/smartstore.common.js index 84b7ffab8d..ff2f5be51f 100644 --- a/src/Presentation/SmartStore.Web/Scripts/smartstore.common.js +++ b/src/Presentation/SmartStore.Web/Scripts/smartstore.common.js @@ -52,6 +52,54 @@ } } + window.createCircularSpinner = function (size, active, strokeWidth, boxed, white) { + var spinner = $('
    '); + if (active) spinner.addClass('active'); + if (boxed) spinner.addClass('spinner-boxed').css('font-size', size + 'px'); + if (white) spinner.addClass('white'); + + if (!_.isNumber(strokeWidth)) { + strokeWidth = 6; + } + + var svg = ''.format(size, 32 - strokeWidth, strokeWidth); + spinner.append($(svg)); + + return spinner; + } + + window.copyTextToClipboard = function (text) { + var result = false; + + if (window.clipboardData && window.clipboardData.setData) { + result = clipboardData.setData('Text', text); + } + else if (document.queryCommandSupported && document.queryCommandSupported('copy')) { + var textarea = document.createElement('textarea'), + focusElement = document.activeElement; + + textarea.textContent = text; + textarea.style.position = 'fixed'; + document.body.appendChild(textarea); + textarea.focus(); + textarea.setSelectionRange(0, textarea.value.length); + + try { + result = document.execCommand('copy'); + } + catch (e) { + } + finally { + document.body.removeChild(textarea); + if (focusElement) { + focusElement.focus(); + } + } + } + return result; + } + + // on document ready $(function () { @@ -219,6 +267,10 @@ if ($({}).moreLess) { $('.more-less').moreLess(); } + + // fixes bootstrap 2 bug: non functional links on mobile devices + // https://github.com/twbs/bootstrap/issues/4550 + $('body').on('touchstart.dropdown', '.dropdown-menu a', function (e) { e.stopPropagation(); }); }); })( jQuery, this, document ); diff --git a/src/Presentation/SmartStore.Web/Scripts/smartstore.doAjax.js b/src/Presentation/SmartStore.Web/Scripts/smartstore.doAjax.js index 0aba22bf44..7521d4c677 100644 --- a/src/Presentation/SmartStore.Web/Scripts/smartstore.doAjax.js +++ b/src/Presentation/SmartStore.Web/Scripts/smartstore.doAjax.js @@ -22,6 +22,38 @@ /* [...] */ }; + $.fn.doPostData = function (options) { + function createAndSubmitForm() { + var id = 'DynamicForm_' + Math.random().toString().substring(2), + form = '
    '; + + if (!_.isUndefined(options.data)) { + $.each(options.data, function (key, val) { + form += ''; + }); + } + + form += ''; + + $('body').append(form); + $('#' + id).submit(); + } + + normalizeOptions(this, options); + + if (_.isEmpty(options.url)) { + console.log('doPostData can\'t find the url!'); + } + else if (_.isEmpty(options.ask)) { + createAndSubmitForm(); + } + else if (confirm(options.ask)) { + createAndSubmitForm(); + } + + return this.each(function () { }); + } + function normalizeOptions(element, opt) { opt.ask = (_.isUndefined(opt.ask) ? $(element).attr('data-ask') : opt.ask); @@ -65,10 +97,10 @@ $.throbber.show(opt.curtainTitle); } else if (opt.throbber) { - $(opt.throbber).removeData('throbber').throbber({ white: true, small: true }); + $(opt.throbber).removeData('throbber').throbber({ white: true, small: true, message: '' }); } else if (opt.smallIcon) { - $(opt.smallIcon).append(''); + $(opt.smallIcon).append(window.createCircularSpinner(16, true)); } } @@ -77,8 +109,9 @@ $.throbber.hide(true); if (opt.throbber) $(opt.throbber).data('throbber').hide(true); - if (opt.smallIcon) - $(opt.smallIcon).find('span.ajax-loader-small').remove(); + if (opt.smallIcon) { + $(opt.smallIcon).find('.spinner').remove(); + } } function doRequest(opt) { diff --git a/src/Presentation/SmartStore.Web/Scripts/smartstore.entityPicker.js b/src/Presentation/SmartStore.Web/Scripts/smartstore.entityPicker.js new file mode 100644 index 0000000000..2bfc9fc193 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Scripts/smartstore.entityPicker.js @@ -0,0 +1,345 @@ +/* +* Project: SmartStore entity picker +* Author: Marcus Gesing, SmartStore AG +*/ + +; (function ($, window, document, undefined) { + + var methods = { + loadDialog: function (options) { + options = normalizeOptions(options, this); + + return this.each(function () { + loadDialog(options); + }); + }, + + initDialog: function () { + return this.each(function () { + initDialog(this); + }); + }, + + fillList: function (options) { + return this.each(function () { + fillList(this, options); + }); + }, + + itemClick: function () { + return this.each(function () { + itemClick(this); + }); + } + }; + + $.fn.entityPicker = function (method) { + return main.apply(this, arguments); + }; + + $.entityPicker = function () { + return main.apply($('.entity-picker:first'), arguments); + }; + + + function main(method) { + if (methods[method]) + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + + if (typeof method === 'object' || !method) + return methods.init.apply(this, arguments); + + EventBroker.publish("message", { title: 'Method "' + method + '" does not exist on jQuery.entityPicker', type: "error" }); + return null; + } + + function normalizeOptions(options, context) { + var self = $(context), + selector = self.selector; + + var defaults = { + url: '', + entity: 'product', + caption: ' ', + disableIf: '', + disableIds: '', + thumbZoomer: false, + highligtSearchTerm: true, + returnField: 'id', + returnValueDelimiter: ',', + returnSelector: '', + maxReturnValues: 0, + onLoadDialogBefore: null, + onLoadDialogComplete: null, + onOkClicked: null + }; + + options = $.extend({}, defaults, options); + + if (_.isEmpty(options.url)) { + options.url = self.attr('data-url'); + } + + if (_.isEmpty(options.url)) { + console.log('entityPicker cannot find the url for entity picker!'); + } + + if (_.isString(selector) && !_.isEmpty(selector) && $(selector).is('input')) { + options.returnSelector = selector; + } + + return options; + } + + function ajaxErrorHandler(objXml) { + try { + if (objXml != null && objXml.responseText != null && objXml.responseText !== '') { + EventBroker.publish("message", { title: objXml.responseText, type: "error" }); + } + } + catch (e) { } + } + + function showStatus(dialog, noteClass, condition) { + var footerNote = $(dialog).find('.footer-note'); + footerNote.find('span').hide(); + footerNote.find('.' + (noteClass || 'default')).show(); + } + + function loadDialog(opt) { + var dialog = $('#entity-picker-' + opt.entity + '-dialog'); + + function showAndFocusDialog() { + dialog = $('#entity-picker-' + opt.entity + '-dialog'); + dialog.find('.caption').html(opt.caption || ' '); + dialog.data('entitypicker', opt); + dialog.modal('show'); + + fillList(dialog, { append: false }); + + setTimeout(function () { + dialog.find('.modal-body :input:visible:enabled:first').focus(); + }, 800); + } + + if (dialog.length) { + showAndFocusDialog(); + } + else { + $.ajax({ + cache: false, + type: 'GET', + data: { + "Entity": opt.entity, + "HighligtSearchTerm": opt.highligtSearchTerm, + "ReturnField": opt.returnField, + "MaxReturnValues": opt.maxReturnValues, + "DisableIf": opt.disableIf, + "DisableIds": opt.disableIds + }, + url: opt.url, + beforeSend: function () { + if (_.isFunction(opt.onLoadDialogBefore)) { + return opt.onLoadDialogBefore(); + } + }, + success: function (response) { + $('body').append(response); + showAndFocusDialog(); + }, + complete: function () { + if (_.isFunction(opt.onLoadDialogComplete)) { + opt.onLoadDialogComplete(); + } + }, + error: ajaxErrorHandler + }); + } + } + + function initDialog(context) { + var dialog = $(context), + keyUpTimer = null, + currentValue = ''; + + // search entities + dialog.find('button[name=SearchEntities]').click(function (e) { + e.preventDefault(); + fillList(this, { append: false }); + return false; + }); + + // toggle filters + dialog.find('button[name=FilterEntities]').click(function () { + dialog.find('.entity-picker-filter').slideToggle(); + }); + + // hit enter or key up starts searching + dialog.find('input.entity-picker-searchterm').keydown(function (e) { + if (e.keyCode == 13) { + e.preventDefault(); + return false; + } + }).bind('keyup change paste', function (e) { + try { + var val = $(this).val(); + + if (val !== currentValue) { + if (keyUpTimer) { + keyUpTimer = clearTimeout(keyUpTimer); + } + + keyUpTimer = setTimeout(function () { + fillList(dialog, { + append: false, + onSuccess: function () { + currentValue = val; + } + }); + }, 500); + } + } + catch (err) { } + }); + + // filter change starts searching + dialog.find('.entity-picker-filter .item').change(function () { + fillList(this, { append: false }); + }); + + // lazy loading + dialog.find('.modal-body').on('scroll', function (e) { + if ($('.load-more:not(.loading)').visible(true, false, 'vertical')) { + fillList(this, { append: true }); + } + }); + + // item select and item hover + dialog.find('.entity-picker-list').on('click', '.item', function (e) { + var item = $(this); + + if (item.hasClass('disable')) + return false; + + var dialog = item.closest('.entity-picker'), + list = item.closest('.entity-picker-list'), + data = dialog.data('entitypicker'); + + if (data.maxReturnValues === 1) { + list.find('.item').removeClass('selected'); + item.addClass('selected'); + } + else if (item.hasClass('selected')) { + item.removeClass('selected'); + } + else if (data.maxReturnValues === 0 || list.find('.selected').length < data.maxReturnValues) { + item.addClass('selected'); + } + + dialog.find('.modal-footer .btn-primary').prop('disabled', list.find('.selected').length <= 0); + }).on({ + mouseenter: function () { + if ($(this).hasClass('disable')) + showStatus($(this).closest('.entity-picker'), 'not-selectable'); + }, + mouseleave: function () { + if ($(this).hasClass('disable')) + showStatus($(this).closest('.entity-picker')); + } + }, '.item'); + + // return value(s) + dialog.find('.modal-footer .btn-primary').click(function () { + var dialog = $(this).closest('.entity-picker'), + items = dialog.find('.entity-picker-list .selected'), + data = dialog.data('entitypicker'), + result = ''; + + items.each(function (index, elem) { + var val = $(elem).attr('data-returnvalue'); + if (!_.isEmpty(val)) { + result = (_.isEmpty(result) ? val : (result + data.returnValueDelimiter + val)); + } + }); + + if (!_.isEmpty(data.returnSelector)) { + $(data.returnSelector).val(result).focus().blur(); + } + + if (_.isFunction(data.onOkClicked)) { + if (data.onOkClicked(result)) { + dialog.modal('hide'); + } + } + else { + dialog.modal('hide'); + } + }); + + // cancel + dialog.find('button[class=btn][data-dismiss=modal]').click(function () { + dialog.find('.entity-picker-list').empty(); + dialog.find('.footer-note span').hide(); + dialog.find('.modal-footer .btn-primary').prop('disabled', true); + }); + } + + function fillList(context, opt) { + var dialog = $(context).closest('.entity-picker'); + + if (_.isTrue(opt.append)) { + var pageElement = dialog.find('input[name=PageIndex]'), + pageIndex = parseInt(pageElement.val()); + + pageElement.val(pageIndex + 1); + } + else { + dialog.find('input[name=PageIndex]').val('0'); + } + + $.ajax({ + cache: false, + type: 'POST', + data: dialog.find('form:first').serialize(), + url: dialog.find('form:first').attr('action'), + beforeSend: function () { + if (_.isTrue(opt.append)) { + dialog.find('.load-more').addClass('loading'); + } + else { + dialog.find('.entity-picker-list').empty(); + dialog.find('.modal-footer .btn-primary').prop('disabled', true); + } + + dialog.find('button[name=SearchEntities]').button('loading').prop('disabled', true); + dialog.find('.load-more').append(createCircularSpinner(20, true)); + }, + success: function (response) { + var list = dialog.find('.entity-picker-list'), + data = dialog.data('entitypicker'); + + list.stop().append(response); + + if (_.isFalse(opt.append)) { + dialog.find('.entity-picker-filter').slideUp(); + showStatus(dialog); + } + + if (list.thumbZoomer && _.isTrue(data.thumbZoomer)) { + list.find('.thumb img:not(.zoomable-thumb)').addClass('zoomable-thumb'); + list.thumbZoomer(); + } + + if (_.isFunction(opt.onSuccess)) { + opt.onSuccess(); + } + }, + complete: function () { + dialog.find('button[name=SearchEntities]').prop('disabled', false).button('reset'); + dialog.find('.load-more.loading').parent().remove(); + }, + error: ajaxErrorHandler + }); + } + +})(jQuery, window, document); \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Scripts/smartstore.instantsearch.js b/src/Presentation/SmartStore.Web/Scripts/smartstore.instantsearch.js index 517d1ac8af..957e35f1a9 100644 --- a/src/Presentation/SmartStore.Web/Scripts/smartstore.instantsearch.js +++ b/src/Presentation/SmartStore.Web/Scripts/smartstore.instantsearch.js @@ -21,7 +21,12 @@ menu: ''.format(showThumbs ? " rich" : ""), autoSelectFirstItem: false, source: function (query, process) { - $('#instantsearch-progress').removeClass('hide'); + var spinner = $('#instantsearch-progress'); + if (spinner.length === 0) { + spinner = createCircularSpinner(20).attr('id', 'instantsearch-progress').appendTo(searchBox.parent()); + } + spinner.addClass('active'); + return $.ajax({ dataType: "json", url: url, @@ -38,7 +43,7 @@ searchResult = null; }, complete: function () { - $('#instantsearch-progress').addClass('hide'); + spinner.removeClass('active'); } }); }, diff --git a/src/Presentation/SmartStore.Web/Scripts/smartstore.smartgallery.js b/src/Presentation/SmartStore.Web/Scripts/smartstore.smartgallery.js index f7c684fcb2..56fbff1fc5 100644 --- a/src/Presentation/SmartStore.Web/Scripts/smartstore.smartgallery.js +++ b/src/Presentation/SmartStore.Web/Scripts/smartstore.smartgallery.js @@ -88,6 +88,10 @@ this.imageWrapperWidth = this.imageWrapper.width(); this.imageWrapperHeight = opts.height || 300; + + if (this.imageWrapperHeight > 300) { + this.imageWrapper.css({ height: this.imageWrapperHeight + 'px' }) + } this.navDisplayWidth = this.nav.width(); this.currentIndex = 0; @@ -128,7 +132,9 @@ }; }; - this.loading(true); + if (this.images[startAt]) { + this.loading(true); + } this.showImage(startAt); @@ -180,9 +186,8 @@ this.nav = el.find('.sg-nav').css("opacity", "0"); this.thumbsWrapper = this.nav.find('.sg-thumbs'); this.preloads = $('
    '); - this.loader = $('
    '); + this.loader = $('
    ').append(createCircularSpinner(24, false, null, true)); this.imageWrapper.append(this.loader); - this.loader.hide(); $(document.body).append(this.preloads); }, @@ -220,10 +225,10 @@ loading: function(value) { if (value) { - this.loader.show(); + this.loader.addClass('active'); } else { - this.loader.hide(); + this.loader.removeClass('active'); }; }, diff --git a/src/Presentation/SmartStore.Web/Scripts/smartstore.throbber.js b/src/Presentation/SmartStore.Web/Scripts/smartstore.throbber.js index a72c2f60da..0929bab9bd 100644 --- a/src/Presentation/SmartStore.Web/Scripts/smartstore.throbber.js +++ b/src/Presentation/SmartStore.Web/Scripts/smartstore.throbber.js @@ -17,7 +17,7 @@ opts = this.options = options, throbber = this.throbber = null, throbberContent = this.throbberContent = null; - + this.visible = false; this.init = function () { @@ -29,31 +29,21 @@ this.initialized = false; this.init(); - + } Throbber.prototype = { - - _reposition: function() { - var self = this, - size = { - left: (self.el.width() - self.throbberContent.outerWidth()) / 2, - top: (self.el.height() - self.throbberContent.outerHeight()) / 2 - } - self.throbberContent.css(size); - }, show: function (o) { - if (this.visible) return; - var self = this, - opts = $.extend( { }, this.options, o); - + var self = this, + opts = $.extend({}, this.options, o); + // create throbber if not avail if (!self.throbber) { - self.throbber = $('
    ') + self.throbber = $('
    ') .addClass(opts.cssClass) .addClass(opts.small ? "small" : "large") .appendTo(opts._global ? 'body' : self.el); @@ -63,44 +53,51 @@ if (opts._global) { self.throbber.addClass("global"); } - + else { + if (/static/.test(self.el.css("position"))) { + self.el.css("position", "relative"); + } + } + self.throbberContent = self.throbber.find(".throbber-content"); + var spinner = window.createCircularSpinner(opts.small ? 50 : 100, true, 3); + spinner.insertAfter(self.throbberContent); self.initialized = true; } - // set text and reposition + // set text self.throbber.css({ visibility: 'hidden', display: 'block' }); self.throbberContent.html(opts.message); - self._reposition(); + self.throbberContent.toggleClass('hide', !(_.isString(opts.message) && opts.message.trim().length > 0)); self.throbber.css({ visibility: 'visible', opacity: 0 }); - var show = function() { - if (_.isFunction(opts.callback)) { - opts.callback.apply(this); - } - if (!self.visible) { + var show = function () { + if (_.isFunction(opts.callback)) { + opts.callback.apply(this); + } + if (!self.visible) { // could have been set to false in 'hide'. // this can happen in very short running processes. self.hide(); - } + } } - + self.visible = true; - self.throbber.delay(opts.delay).transition({opacity: 1}, opts.speed || 0, "linear", show); + self.throbber.delay(opts.delay).transition({ opacity: 1 }, opts.speed || 0, "linear", show); - if (opts.timeout) { - var hideFn = _.bind(self.hide, this); - window.setTimeout(hideFn, opts.timeout + opts.delay); + if (opts.timeout) { + var hideFn = _.bind(self.hide, this); + window.setTimeout(hideFn, opts.timeout + opts.delay); } }, - hide: function(immediately) { + hide: function (immediately) { var self = this, opts = this.options; if (self.throbber && self.visible) { - var hide = function() { - self.throbber.css('display', 'none'); + var hide = function () { + self.throbber.css('display', 'none'); } self.visible = false; @@ -116,10 +113,9 @@ // A really lightweight plugin wrapper around the constructor, // preventing against multiple instantiations $.fn[pluginName] = function (options) { - return this.each(function () { if (!$.data(this, pluginName)) { - options = $.extend( {}, $[pluginName].defaults, options ); + options = $.extend({}, $[pluginName].defaults, options); $.data(this, pluginName, new Throbber(this, options)); } }); @@ -142,40 +138,30 @@ // internal _global: false }; - - // global resize event - $(window).on('resize.throbber', function() { - // resize all active/visible throbbers - $.each(throbbers, function(i, throbber) { - if (throbber.initialized && throbber.visible) { - throbber._reposition(); - } - }) - }); $[pluginName] = { - + // the global, default plugin options defaults: defaults, // options: a message string || options object - show: function(options) { - var opts = $.extend( defaults, _.isString(options) ? { message: options } : options, { show: false, _global: true } ); + show: function (options) { + var opts = $.extend(defaults, _.isString(options) ? { message: options } : options, { show: false, _global: true }); if (!globalThrobber) { globalThrobber = $(window).throbber(opts).data("throbber"); } globalThrobber.show(opts); - }, - hide: function() { + hide: function (immediately) { if (globalThrobber) { - globalThrobber.hide(); + globalThrobber.hide(immediately); } } } // $.throbber })(jQuery, window, document); + diff --git a/src/Presentation/SmartStore.Web/SmartStore.Web.csproj b/src/Presentation/SmartStore.Web/SmartStore.Web.csproj index 591df9c3e2..8340a62bfb 100644 --- a/src/Presentation/SmartStore.Web/SmartStore.Web.csproj +++ b/src/Presentation/SmartStore.Web/SmartStore.Web.csproj @@ -48,6 +48,7 @@ 4 false AnyCPU + AllFilesInProjectFolder pdbonly @@ -57,27 +58,27 @@ prompt 4 false - AllFilesInTheProject + AllFilesInProjectFolder false false AnyCPU - - False - ..\..\packages\AjaxMin.5.8.5172.27710\lib\net40\AjaxMin.dll + + ..\..\packages\AjaxMin.5.14.5506.26202\lib\net40\AjaxMin.dll + True False ..\..\packages\Antlr.3.5.0.2\lib\Antlr3.Runtime.dll - - False - ..\..\packages\Autofac.3.4.1\lib\net40\Autofac.dll + + ..\..\packages\Autofac.3.5.2\lib\net40\Autofac.dll + True - False - ..\..\packages\Autofac.Mvc5.3.3.1\lib\net45\Autofac.Integration.Mvc.dll + ..\..\packages\Autofac.Mvc5.3.3.4\lib\net45\Autofac.Integration.Mvc.dll + True False @@ -106,20 +107,21 @@ False ..\..\packages\EntityFramework.SqlServerCompact.6.1.3\lib\net45\EntityFramework.SqlServerCompact.dll - - False - ..\..\packages\FluentValidation.5.0.0.1\lib\Net40\FluentValidation.dll + + ..\..\packages\FluentValidation.5.6.2.0\lib\Net45\FluentValidation.dll + True - - False - ..\..\packages\FluentValidation.MVC4.5.0.0.1\lib\Net40\FluentValidation.Mvc.dll + + ..\..\packages\FluentValidation.MVC5.5.6.2.0\lib\Net45\FluentValidation.Mvc.dll + True - - False - ..\..\packages\JavaScriptEngineSwitcher.Core.1.1.3\lib\net40\JavaScriptEngineSwitcher.Core.dll + + ..\..\packages\JavaScriptEngineSwitcher.Core.1.2.4\lib\net40\JavaScriptEngineSwitcher.Core.dll + True - - ..\..\packages\JavaScriptEngineSwitcher.Msie.1.1.4\lib\net40\JavaScriptEngineSwitcher.Msie.dll + + ..\..\packages\JavaScriptEngineSwitcher.Msie.1.2.11\lib\net40\JavaScriptEngineSwitcher.Msie.dll + True False @@ -129,15 +131,13 @@ True ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - ..\..\packages\MsieJavaScriptEngine.1.4.2\lib\net40\MsieJavaScriptEngine.dll - - - False - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\MsieJavaScriptEngine.1.5.6\lib\net40\MsieJavaScriptEngine.dll + True - - ..\..\packages\recaptcha.1.0.5.0\lib\.NetFramework 4.0\Recaptcha.dll + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True @@ -337,6 +337,7 @@ + @@ -346,6 +347,7 @@ + @@ -643,291 +645,19 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ResXFileCodeGenerator MvcLocalization.de.Designer.cs Designer + Designer - - - - - - - - - - - @@ -941,11 +671,11 @@ Designer - - - - - + + + + + @@ -1049,6 +779,7 @@ + @@ -1060,7 +791,6 @@ - @@ -1077,6 +807,8 @@ + + @@ -1194,6 +926,7 @@ + @@ -1217,7 +950,6 @@ - @@ -1519,14 +1251,6 @@ - - - - - - - - @@ -1550,12 +1274,6 @@ - - - - - - @@ -1564,7 +1282,6 @@ - @@ -1587,11 +1304,7 @@ - - - - @@ -1865,6 +1578,9 @@ + + + @@ -2142,25 +1858,12 @@ - - if not exist "$(TargetDir)x86" md "$(TargetDir)x86" - xcopy /s /y "$(SolutionDir)..\lib\sqlce\x86\*.*" "$(TargetDir)x86" - xcopy /s /y "$(SolutionDir)..\lib\ClearScript.V8\Native\x86\*.*" "$(TargetDir)x86" -if not exist "$(TargetDir)amd64" md "$(TargetDir)amd64" - xcopy /s /y "$(SolutionDir)..\lib\sqlce\amd64\*.*" "$(TargetDir)amd64" - xcopy /s /y "$(SolutionDir)..\lib\ClearScript.V8\Native\amd64\*.*" "$(TargetDir)amd64" - -if not exist "$(TargetDir)ClearScript.V8" md "$(TargetDir)ClearScript.V8" - xcopy /s /y "$(SolutionDir)..\lib\ClearScript.V8\lib\*.*" "$(TargetDir)ClearScript.V8" + + @@ -2171,9 +1874,80 @@ if not exist "$(TargetDir)ClearScript.V8" md "$(TargetDir)ClearScript.V8" - + + + + false + + + + + **\*.cs; + **\*.orig; + **\*.bak; + **\*.log; + **\*.csproj; + **\*.csproj.user; + **\*.DotSettings.user; + **\packages.config; + **\*.Debug.config; + **\*.Release.config; + **\obj\**; + **\bin\*.xml; + **\bin\HostRestart\**; + Administration\bin\**; + App_Data\_Backup\**; + App_Data\_temp\**; + App_Data\ExportProfiles\**; + App_Data\ImportProfiles\**; + App_Data\Migrations\**; + App_Data\InstalledPlugins.txt; + App_Data\Licenses.lic; + App_Data\Settings.txt; + App_Data\SmartStore.Db.sdf; + Exchange\**; + Content\Files\ExportImport\**; + Content\Images\Thumbs\**; + Media\**\*.png; + Media\**\*.jpg; + Media\**\*.jpeg; + Media\**\*.gif; + Media\**\*.pdf; + Media\**\*.docx; + Properties\**; + Plugins\*\bin\**; + + + + + + + + + + + + + + + + + + + + + + - - + $(ProjectDir)Media\Thumbs\placeholder.txt + + + + + + + + + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/bootstrap.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/bootstrap.less index 1398710a14..d4c3a5acd4 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/bootstrap.less +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/bootstrap.less @@ -63,6 +63,7 @@ @import "~/Content/bootstrap/carousel.less"; // (MC) extra 3rd party or own components +@import "~/Content/bootstrap/custom/spinner.less"; @import "~/Content/bootstrap/custom/throbber.less"; @import "~/Content/bootstrap/custom/select2.less"; @import "~/Content/bootstrap/custom/datetimepicker.less"; diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/cart.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/cart.less index 7fb08a94db..1db6a5e082 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/cart.less +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/cart.less @@ -2,7 +2,6 @@ // ShoppingCart, OrderSummary & WishList Styles // -------------------------------------------------- - .order-summary-content .delivery-time { text-align: left; } diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/checkout.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/checkout.less index 067b66c9b2..60e01fb235 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/checkout.less +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/checkout.less @@ -240,24 +240,14 @@ /* Terms of service ================================================ */ - -#terms-of-service-modal { - width: 650px; -} -#terms-of-service-modal .modal-body { - min-height: 300px; - overflow: hidden; -} -#iframe-terms-of-service { - min-height: 400px; - width: 100%; -} - .terms-of-service.alert { - padding-bottom: 4px !important; - a.read { - font-weight: bold; - } + padding-bottom: 4px !important; + a.read { + font-weight: bold; + &:hover { + cursor: pointer; + } + } } diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/common.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/common.less index 5de3a398a9..327e7c7ea5 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/common.less +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/common.less @@ -130,16 +130,6 @@ nav ul, nav li { /* Misc ================================================ */ -.ajax-loader-small { - display: inline-block; - position: relative; - width: 16px; - height: 16px; - padding: 0; - margin: 0; - background: transparent url('images/ajax_loader_small.gif') 50% 50% no-repeat; -} - #popup-content { margin: 20px; } diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/datalist.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/datalist.less index 43fd4b4ccb..6cd5cfe191 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/datalist.less +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/datalist.less @@ -89,7 +89,7 @@ .item-box figure.picture { position: relative; z-index: 0; - padding: 4px; + padding: 12px 12px 4px 12px; text-align: center; margin: 0; min-height: 100px; @@ -138,7 +138,8 @@ } .item-box .data { - padding: 8px; + padding: 12px; + padding-top: 8px; } .item-box.details .data { @@ -257,6 +258,9 @@ text-decoration: none; } } + .fa { + font-size: 13px; + } } .item-box:hover .quicklinks { .opacity(100); diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/header.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/header.less index c783048936..012a75ee71 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/header.less +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/header.less @@ -7,6 +7,10 @@ padding: 46px 0 0 0; } +.store-closed #header { + padding-top: 0; +} + #logobar { position: relative; padding: 10px 0; diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/images/ajax_loader_large.gif b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/images/ajax_loader_large.gif deleted file mode 100644 index fb2c7a3ddb..0000000000 Binary files a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/images/ajax_loader_large.gif and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/images/gradient.png b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/images/gradient.png deleted file mode 100644 index 7c0307f62c..0000000000 Binary files a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/images/gradient.png and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/images/ico-delete.gif b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/images/ico-delete.gif deleted file mode 100644 index 2ca8b23523..0000000000 Binary files a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/images/ico-delete.gif and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/images/ico-edit.gif b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/images/ico-edit.gif deleted file mode 100644 index 137a605679..0000000000 Binary files a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/images/ico-edit.gif and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/images/ico-move-topic.gif b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/images/ico-move-topic.gif deleted file mode 100644 index a5ee14c907..0000000000 Binary files a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/images/ico-move-topic.gif and /dev/null differ diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/layout.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/layout.less index ae6bc7a7fd..b6cb2e661c 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/layout.less +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/layout.less @@ -11,6 +11,7 @@ #content-body { margin-top: 10px; padding-bottom: 10px; + min-height: 400px; } /* one column */ diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/pages.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/pages.less index 11a472e78c..4df8f7751d 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/pages.less +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/pages.less @@ -67,6 +67,7 @@ } .compare-products-table tr.product-name { background-color: #e5e5e5; + font-weight: bold; } .compare-products-table tr.product-name a { color: #888; diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/product.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/product.less index 9edce6806f..0a0fde971a 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/product.less +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/product.less @@ -138,6 +138,10 @@ border: 0; } +.manufacturer-pics-container { + min-height: 20px; +} + #product-detail-tabs { margin-top: 50px; } @@ -200,7 +204,7 @@ .product-details-page .add-to-cart { padding-top: 10px; } -.product-details-page #details-cnt .add-to-cart .fa-plus-circle { +.product-details-page #details-cnt .add-to-cart .fa-cart-plus { position: absolute; right: 6px; top: 50%; @@ -391,15 +395,15 @@ border-top: none !important; } +.attributes .image-has-value { + display: table-cell; + padding-top: 10px; +} + .attribute-value-image { margin-right: 5px; vertical-align: middle; } - -.attributes .image-has-value { - margin-left: 28px; -} - .mb-bundle-pictures .fa-plus { padding: 4px 10px 0 10px; } @@ -409,6 +413,10 @@ .attributes .attribute-value-image-checkbox { float: left; } +.attributes .attribute-value-image-checkbox { + margin-right: 10px; + padding-top: 10px; +} .mb-bundle-pictures img { max-width: 24px; diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/productfilter.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/productfilter.less index 13866fddad..eafccc84d9 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/productfilter.less +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/productfilter.less @@ -14,7 +14,7 @@ position: relative; min-height: 32px; - .ajax-loader-small { + .spinner { position: absolute; left: 50%; top: 50%; diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/shopbar.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/shopbar.less index 74d2919a7b..474543e015 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/shopbar.less +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/shopbar.less @@ -27,18 +27,9 @@ @bg: top, @shopBarBackgroundColor, @shopBarBackgroundColor 40%, darken(@shopBarBackgroundColor, 7%) 100%; background-color: @shopBarBackgroundColor; background-image: -webkit-linear-gradient(@bg); - background-image: -o-linear-gradient(@bg); background-image: linear-gradient(@bg); } -.no-cssgradients #shopbar { - // fix the ie9 "gradient dropdown transparent" bug - background-color: @shopBarBackgroundColor; - background-image: url('images/shopbar-bg.png'); - background-position: 0 100%; - background-repeat: repeat-x; -} - #shopbar.sticky { .opacity(75); &:hover { @@ -66,7 +57,7 @@ position: absolute; right: 45px; top: 50%; - margin-top: -8px; + margin-top: -10px; z-index: 10; } @@ -139,7 +130,7 @@ width: 1px; background-color: rgba(0,0,0, .15); margin: 8px; - padding: none; + padding: 0; border-right: 1px solid @shopBarBackgroundColor; .no-rgba & { @@ -172,8 +163,6 @@ position: relative; display: inline-block; margin: 2px 4px 0 0; - width: 24px; - height: 24px; span.label { position: absolute; @@ -185,19 +174,18 @@ } } -.shopbar-tool [class^="sm-icon-"] { +.shopbar-button-icon .fa { display: inline-block; - width: 24px; - height: 24px; - font-size: 24px; + font-size: 24px !important; font-style: normal; - margin-top: 5px; + line-height: 28px; + vertical-align: middle; color: @shopBarIconColor; .transition(color .1s linear); } -.shopbar-button:hover [class^="sm-icon-"] { +.shopbar-button:hover .shopbar-button-icon .fa { color: darken(@shopBarIconColor, 22%); } @@ -272,10 +260,18 @@ .box-shadow(0 2px 8px rgba(0,0,0,.2)); .shopbar-tool.loading & { - background-image: url('images/loading.gif'); + //background-image: url('images/loading.gif'); } - .shopbar-tool.loading & > * { + .spinner-container { + height: 100%; + } + + .shopbar-tool:not(.loading) & > .spinner-container { + display: none; + } + + .shopbar-tool.loading & > .shopbar-flyout-inner { display: none; } diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/theme.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/theme.less index 346d8148ab..a1c1b682af 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/theme.less +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/theme.less @@ -7,7 +7,6 @@ // Fonts @import "typo.less"; -@import "~/Content/fonts/fontastic.less"; // Bootstrap @import "bootstrap.less"; diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Views/Shared/ConfigureTheme.cshtml b/src/Presentation/SmartStore.Web/Themes/Alpha/Views/Shared/ConfigureTheme.cshtml index f611bc9347..7b282093c4 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Views/Shared/ConfigureTheme.cshtml +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Views/Shared/ConfigureTheme.cshtml @@ -1,9 +1,6 @@ @model IDictionary -@using System.Dynamic; -@using SmartStore.Core.Themes; -@using SmartStore.Web.Framework.Themes; -@using SmartStore.Utilities; +@using SmartStore.Web.Framework.Theming; @using SmartStore.Web.Framework.UI; @{ diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Views/Shared/Head.cshtml b/src/Presentation/SmartStore.Web/Themes/Alpha/Views/Shared/Head.cshtml index 2442d88fa6..5ac574bc9a 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Views/Shared/Head.cshtml +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Views/Shared/Head.cshtml @@ -1,6 +1,4 @@ -@using SmartStore.Core; -@using SmartStore.Core.Infrastructure; -@using SmartStore.Web.Framework.Themes; +@using SmartStore.Web.Framework.Theming; @{ Html.AppendCssFileParts(false, "~/Themes/Alpha/Content/theme.less"); diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Views/Web.config b/src/Presentation/SmartStore.Web/Themes/Alpha/Views/Web.config index 3f3148484e..bd9d09444f 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Views/Web.config +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Views/Web.config @@ -10,7 +10,7 @@ - + diff --git a/src/Presentation/SmartStore.Web/Themes/AlphaBlack/Views/Web.config b/src/Presentation/SmartStore.Web/Themes/AlphaBlack/Views/Web.config index 3f3148484e..bd9d09444f 100644 --- a/src/Presentation/SmartStore.Web/Themes/AlphaBlack/Views/Web.config +++ b/src/Presentation/SmartStore.Web/Themes/AlphaBlack/Views/Web.config @@ -10,7 +10,7 @@ - + diff --git a/src/Presentation/SmartStore.Web/Themes/AlphaBlue/Views/Web.config b/src/Presentation/SmartStore.Web/Themes/AlphaBlue/Views/Web.config index 3f3148484e..bd9d09444f 100644 --- a/src/Presentation/SmartStore.Web/Themes/AlphaBlue/Views/Web.config +++ b/src/Presentation/SmartStore.Web/Themes/AlphaBlue/Views/Web.config @@ -10,7 +10,7 @@ - + diff --git a/src/Presentation/SmartStore.Web/Themes/Mobile/Views/Web.config b/src/Presentation/SmartStore.Web/Themes/Mobile/Views/Web.config index 3f3148484e..bd9d09444f 100644 --- a/src/Presentation/SmartStore.Web/Themes/Mobile/Views/Web.config +++ b/src/Presentation/SmartStore.Web/Themes/Mobile/Views/Web.config @@ -10,7 +10,7 @@ - + diff --git a/src/Presentation/SmartStore.Web/Themes/MobileLight/Views/Web.config b/src/Presentation/SmartStore.Web/Themes/MobileLight/Views/Web.config index 3f3148484e..bd9d09444f 100644 --- a/src/Presentation/SmartStore.Web/Themes/MobileLight/Views/Web.config +++ b/src/Presentation/SmartStore.Web/Themes/MobileLight/Views/Web.config @@ -10,7 +10,7 @@ - + diff --git a/src/Presentation/SmartStore.Web/Validators/Common/ContactUsValidator.cs b/src/Presentation/SmartStore.Web/Validators/Common/ContactUsValidator.cs index 7e9b037037..9e42f7ed02 100644 --- a/src/Presentation/SmartStore.Web/Validators/Common/ContactUsValidator.cs +++ b/src/Presentation/SmartStore.Web/Validators/Common/ContactUsValidator.cs @@ -8,6 +8,11 @@ public class ContactUsValidator : AbstractValidator { public ContactUsValidator(ILocalizationService localizationService) { + RuleFor(x => x.PrivacyAgreement) + .Must(x => x == true) + .WithMessage(localizationService.GetResource("ContactUs.PrivacyAgreement.MustBeAccepted")) + .When(x => x.DisplayPrivacyAgreement == true); + RuleFor(x => x.Email).NotEmpty().WithMessage(localizationService.GetResource("ContactUs.Email.Required")); RuleFor(x => x.Email).EmailAddress().WithMessage(localizationService.GetResource("Common.WrongEmail")); RuleFor(x => x.FullName).NotEmpty().WithMessage(localizationService.GetResource("ContactUs.FullName.Required")); diff --git a/src/Presentation/SmartStore.Web/Validators/Customer/RegisterValidator.cs b/src/Presentation/SmartStore.Web/Validators/Customer/RegisterValidator.cs index b04dd7468b..3c9516c656 100644 --- a/src/Presentation/SmartStore.Web/Validators/Customer/RegisterValidator.cs +++ b/src/Presentation/SmartStore.Web/Validators/Customer/RegisterValidator.cs @@ -1,5 +1,6 @@ using FluentValidation; using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Tax; using SmartStore.Services.Localization; using SmartStore.Web.Models.Customer; @@ -7,7 +8,7 @@ namespace SmartStore.Web.Validators.Customer { public class RegisterValidator : AbstractValidator { - public RegisterValidator(ILocalizationService localizationService, CustomerSettings customerSettings) + public RegisterValidator(ILocalizationService localizationService, CustomerSettings customerSettings, TaxSettings taxSettings) { RuleFor(x => x.Email).NotEmpty().WithMessage(localizationService.GetResource("Account.Fields.Email.Required")); RuleFor(x => x.Email).EmailAddress().WithMessage(localizationService.GetResource("Common.WrongEmail")); @@ -50,6 +51,11 @@ public RegisterValidator(ILocalizationService localizationService, CustomerSetti { RuleFor(x => x.Fax).NotEmpty().WithMessage(localizationService.GetResource("Account.Fields.Fax.Required")); } + + if (taxSettings.EuVatEnabled && taxSettings.VatRequired) + { + RuleFor(x => x.VatNumber).NotEmpty().WithMessage(localizationService.GetResource("Account.Fields.Vat.Required")); + } } } } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Views/Blog/BlogPost.Mobile.cshtml b/src/Presentation/SmartStore.Web/Views/Blog/BlogPost.Mobile.cshtml index 15f3a51e38..3c49bafc7a 100644 --- a/src/Presentation/SmartStore.Web/Views/Blog/BlogPost.Mobile.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Blog/BlogPost.Mobile.cshtml @@ -50,13 +50,6 @@ @Html.TextAreaFor(model => model.AddNewComment.CommentText) @Html.ValidationMessageFor(model => model.AddNewComment.CommentText)
    - string result = TempData["sm.blog.addcomment.result"] as string; - if (!String.IsNullOrEmpty(result)) - { -
    - @result -
    - }
    @Html.ValidationSummary(true)
    diff --git a/src/Presentation/SmartStore.Web/Views/Blog/BlogPost.cshtml b/src/Presentation/SmartStore.Web/Views/Blog/BlogPost.cshtml index 7e3cc38336..02b64785ba 100644 --- a/src/Presentation/SmartStore.Web/Views/Blog/BlogPost.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Blog/BlogPost.cshtml @@ -56,27 +56,13 @@ @using (Html.BeginForm()) {
    - @if (!ViewData.ModelState.IsValid) - { - @Html.ValidationSummary(true) - } - @{ - string result = TempData["sm.blog.addcomment.result"] as string; - } - @if (!String.IsNullOrEmpty(result)) - { -
    - - @result -
    - } + @Html.ValidationSummary()
    @Html.LabelFor(model => model.AddNewComment.CommentText)
    @Html.TextAreaFor(model => model.AddNewComment.CommentText, new { @class = "comment-text" })
    - @Html.ValidationMessageFor(model => model.AddNewComment.CommentText)
    @if (Model.AddNewComment.DisplayCaptcha) { diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/CategoryTemplate.ProductsInGridOrLines.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/CategoryTemplate.ProductsInGridOrLines.cshtml index f96a1c71c6..cbe8111e78 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/CategoryTemplate.ProductsInGridOrLines.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/CategoryTemplate.ProductsInGridOrLines.cshtml @@ -44,9 +44,12 @@ @item.Name
    - - @item.PictureModel.AlternateText - + @if (item.PictureModel.ImageUrl.HasValue()) + { + + @item.PictureModel.AlternateText + + }
    diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/CompareProducts.Mobile.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/CompareProducts.Mobile.cshtml index 77cb8c034f..69605ebe6e 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/CompareProducts.Mobile.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/CompareProducts.Mobile.cshtml @@ -1,13 +1,12 @@ -@model CompareProductsModel +@using SmartStore.Core; +@using SmartStore.Core.Infrastructure; +@using SmartStore.Web.Models.Catalog; +@model CompareProductsModel @{ Layout = "~/Views/Shared/_Root.cshtml"; - //title Html.AddTitleParts(T("PageTitle.CompareProducts").Text); } -@using SmartStore.Core; -@using SmartStore.Core.Infrastructure; -@using SmartStore.Web.Models.Catalog; @{ string columnWidth = ""; if (Model.Products.Count > 0) @@ -15,7 +14,6 @@ columnWidth = Math.Round((decimal)(90M / Model.Products.Count), 0).ToString() + "%"; } - var specificationAttributes = new List(); foreach (var product in Model.Products) { @@ -51,7 +49,15 @@ @product.Name
    - @T("Products.Compare.Price"): @product.ProductPrice.Price +
    + @T("Products.Compare.Price"): @product.ProductPrice.Price +
    + @if (product.BasePriceInfo.HasValue()) + { +
    + @product.BasePriceInfo +
    + }
    @foreach (var specificationAttribute in specificationAttributes) diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/CompareProducts.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/CompareProducts.cshtml index c1c9f6bdf8..8202ddd22c 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/CompareProducts.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/CompareProducts.cshtml @@ -1,13 +1,12 @@ -@model CompareProductsModel +@using SmartStore.Core; +@using SmartStore.Core.Infrastructure; +@using SmartStore.Web.Models.Catalog; +@model CompareProductsModel @{ Layout = "~/Views/Shared/_ColumnsTwo.cshtml"; - //title Html.AddTitleParts(T("PageTitle.CompareProducts").Text); } -@using SmartStore.Core; -@using SmartStore.Core.Infrastructure; -@using SmartStore.Web.Models.Catalog; @{ string columnWidth = ""; if (Model.Products.Count > 0) @@ -15,7 +14,6 @@ columnWidth = Math.Round((decimal)(90M / Model.Products.Count), 0).ToString() + "%"; } - var specificationAttributes = new List(); foreach (var product in Model.Products) { @@ -52,9 +50,14 @@ {
    } diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/CompareProductsButton.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/CompareProductsButton.cshtml index 0254f49751..0141d7c45b 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/CompareProductsButton.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/CompareProductsButton.cshtml @@ -7,7 +7,7 @@ data-href="@Url.Action("AddProductToCompare", new { id = Model.ProductId })" data-type="compare" data-action="add"> - + @T("Products.Compare.AddToCompareList") \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/FlyoutCompare.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/FlyoutCompare.cshtml index 42d9889f80..f10c3ec1e2 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/FlyoutCompare.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/FlyoutCompare.cshtml @@ -19,9 +19,12 @@
  • - - @item.DefaultPictureModel.AlternateText - + @if (item.DefaultPictureModel.ImageUrl.HasValue()) + { + + @item.DefaultPictureModel.AlternateText + + }
    @item.Name diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/HomepageCategories.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/HomepageCategories.cshtml index 66d489bb87..92a35f55c8 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/HomepageCategories.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/HomepageCategories.cshtml @@ -8,14 +8,16 @@ @ )) diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/ManufacturerAll.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/ManufacturerAll.cshtml index 9962eceb67..9e207b9861 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/ManufacturerAll.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/ManufacturerAll.cshtml @@ -1,13 +1,12 @@ -@model IList +@using SmartStore.Core; +@using SmartStore.Core.Infrastructure; +@using SmartStore.Web.Models.Catalog; +@model IList @{ Layout = "~/Views/Shared/_ColumnsThree.cshtml"; - //title Html.AddTitleParts(T("PageTitle.Manufacturers").Text); } -@using SmartStore.Core; -@using SmartStore.Core.Infrastructure; -@using SmartStore.Web.Models.Catalog;

    @T("Manufacturers.List")

    @@ -21,12 +20,14 @@ @(Html.DataList(Model, 3, @

    - - @item.Name + @item.Name

    - - @item.PictureModel.AlternateText + @if (item.PictureModel.ImageUrl.HasValue()) + { + + @item.PictureModel.AlternateText + }
    )) diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/ManufacturerNavigation.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/ManufacturerNavigation.cshtml index e5d653f039..e94c1df66b 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/ManufacturerNavigation.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/ManufacturerNavigation.cshtml @@ -1,8 +1,8 @@ -@model ManufacturerNavigationModel -@using SmartStore.Core.Domain.Catalog; +@using SmartStore.Core.Domain.Catalog; @using SmartStore.Core.Infrastructure; @using SmartStore.Services.Catalog; @using SmartStore.Web.Models.Catalog; +@model ManufacturerNavigationModel @if (Model.Manufacturers.Count > 0) {
    @@ -15,7 +15,7 @@ {
  • - @if (Model.DisplayImages) + @if (Model.DisplayImages && manufacturer.PictureUrl.HasValue()) { @manufacturer.Name diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/RecentlyViewedProductsBlock.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/RecentlyViewedProductsBlock.cshtml index 978bb9c0c9..d374c9044b 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/RecentlyViewedProductsBlock.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/RecentlyViewedProductsBlock.cshtml @@ -26,8 +26,11 @@
  • - - @product.DefaultPictureModel.AlternateText + @if (product.DefaultPictureModel.ImageUrl.HasValue()) + { + + @product.DefaultPictureModel.AlternateText + }
    @product.Name diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/Search.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/Search.cshtml index 156c24a9d0..8809946c25 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/Search.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/Search.cshtml @@ -1,15 +1,11 @@ -@model SearchModel +@using SmartStore.Web.Models.Catalog; +@using SmartStore.Web.Framework.UI; +@model SearchModel @{ Layout = "~/Views/Shared/_ColumnsThree.cshtml"; - //title Html.AddTitleParts(T("PageTitle.Search").Text); } -@using SmartStore.Core; -@using SmartStore.Core.Infrastructure; -@using SmartStore.Web; -@using SmartStore.Web.Models.Catalog; -@using SmartStore.Web.Framework.UI; @functions{ private bool ShowListOptions() { @@ -32,7 +28,9 @@ diff --git a/src/Presentation/SmartStore.Web/Views/Checkout/Confirm.cshtml b/src/Presentation/SmartStore.Web/Views/Checkout/Confirm.cshtml index fed6c42b8a..10c8f7d30f 100644 --- a/src/Presentation/SmartStore.Web/Views/Checkout/Confirm.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Checkout/Confirm.cshtml @@ -8,8 +8,8 @@ //title Html.AddTitleParts(T("PageTitle.Checkout").Text); - string termsLink = ""; - string disclaimerLink = ""; + string termsLink = ""; + string disclaimerLink = ""; string terms = string.Format(T("Checkout.TermsOfService.IAccept"), termsLink, "", disclaimerLink); } @section orderProgress{ @@ -27,18 +27,22 @@ @Html.Raw(@T("Checkout.ConfirmHint")) - + + + - if (Model.TermsOfServiceEnabled) - { + if (Model.TermsOfServiceEnabled) + {
    @@ -48,8 +52,8 @@ - @if (Model.ShowConfirmOrderLegalHint) - { - - } + { + + }
    @@ -109,26 +113,59 @@ $(function () { var checkoutButton = $(".confirm-order-next-step-button"); checkoutButton.click(function () { + var termOfServiceOk = true, + userAgreementsOk = true, + esdRevocationWaiverOk = true; - $("#customercommenthidden").val($("#CustomerComment").val()); + $("#customercommenthidden").val($("#CustomerComment").val()); + $('#SubscribeToNewsLetterHidden').val($('input[name=SubscribeToNewsLetter]').is(':checked')); + $('#AcceptThirdPartyEmailHandOverHidden').val($('input[name=AcceptThirdPartyEmailHandOver]').is(':checked')); - //terms of services - var termOfServiceOk = true; + // terms of services @if (Model.TermsOfServiceEnabled) { - + if (!$('#termsofservice').is(':checked')) { - displayNotification('@T("Checkout.TermsOfService.PleaseAccept").ToString().EncodeJsString()', "error"); + displayNotification('@T("Checkout.TermsOfService.PleaseAccept").ToString().EncodeJsString('"', false)', "error"); termOfServiceOk = false; $.scrollTo($('#termsofservice'), 800, { offset: -70 }); } else { termOfServiceOk = true; } - + } + + // agree user agreement for downloadable products + $('table.table-order-products').find('input[name^=AgreeUserAgreement]').each(function () { + if (!$(this).is(':checked')) { + userAgreementsOk = false; + displayNotification('@T("Checkout.DownloadUserAgreement.PleaseAgree").ToString().EncodeJsString('"', false)', 'error'); + if (termOfServiceOk) { + $.scrollTo($('table.table-order-products'), 800, { offset: -20 }); + } + return false; + } + }); + + // agree esd revocation waiver + @if(Model.ShowEsdRevocationWaiverBox) + { + + $('table.table-order-products').find('input[name^=AgreeEsdRevocationWaiver]').each(function () { + if (!$(this).is(':checked')) { + esdRevocationWaiverOk = false; + displayNotification('@T("Checkout.EsdRevocationWaiverConfirmation.PleaseAgree").ToString().EncodeJsString('"', false)', 'error'); + if (termOfServiceOk) { + $.scrollTo($('table.table-order-products'), 800, { offset: -20 }); + } + return false; + } + }); + + } - if (termOfServiceOk) { + if (termOfServiceOk && userAgreementsOk && esdRevocationWaiverOk) { var submitOrderEvent = jQuery.Event('submitOrder'); submitOrderEvent.isOrderValid = true; submitOrderEvent.isMobile = false; diff --git a/src/Presentation/SmartStore.Web/Views/Checkout/PaymentMethod.Mobile.cshtml b/src/Presentation/SmartStore.Web/Views/Checkout/PaymentMethod.Mobile.cshtml index df97ab4bae..cab3cdbc8c 100644 --- a/src/Presentation/SmartStore.Web/Views/Checkout/PaymentMethod.Mobile.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Checkout/PaymentMethod.Mobile.cshtml @@ -70,6 +70,7 @@ @Html.Action(infoRoute.Action, infoRoute.Controller, infoRoute.RouteValues) }
    +
    }
    } diff --git a/src/Presentation/SmartStore.Web/Views/Checkout/ShippingMethod.Mobile.cshtml b/src/Presentation/SmartStore.Web/Views/Checkout/ShippingMethod.Mobile.cshtml index 6674bafc8e..07fc9cbf98 100644 --- a/src/Presentation/SmartStore.Web/Views/Checkout/ShippingMethod.Mobile.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Checkout/ShippingMethod.Mobile.cshtml @@ -27,7 +27,7 @@
    - +
    @if (!String.IsNullOrEmpty(shippingMethod.Description)) { diff --git a/src/Presentation/SmartStore.Web/Views/Checkout/ShippingMethod.cshtml b/src/Presentation/SmartStore.Web/Views/Checkout/ShippingMethod.cshtml index 54c1a51dce..68d6091639 100644 --- a/src/Presentation/SmartStore.Web/Views/Checkout/ShippingMethod.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Checkout/ShippingMethod.cshtml @@ -42,7 +42,7 @@ - @shippingMethod.Name + @Html.Raw(HttpUtility.HtmlDecode(shippingMethod.Name))
    diff --git a/src/Presentation/SmartStore.Web/Views/Common/AccountDropdown.cshtml b/src/Presentation/SmartStore.Web/Views/Common/AccountDropdown.cshtml index cacb881673..96b1274211 100644 --- a/src/Presentation/SmartStore.Web/Views/Common/AccountDropdown.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Common/AccountDropdown.cshtml @@ -20,7 +20,7 @@ @Html.Widget("account_dropdown_before")
  • - + @T("Account.MyAccount")
  • @@ -28,7 +28,7 @@ {
  • - + @T("Account.Administration")
  • @@ -38,7 +38,7 @@ {
  • - + @T("PrivateMessages.Inbox") @if (Model.UnreadPrivateMessages != "") { @@ -63,23 +63,26 @@ {
  • - - @T("Wishlist") @Model.WishlistItems + + @T("Wishlist") @Model.WishlistItems +
  • } @if (Model.ShoppingCartEnabled) { }
  • - - @T("Account.Logout") + + @T("Account.Logout") +
  • @Html.Widget("account_dropdown_after") diff --git a/src/Presentation/SmartStore.Web/Views/Common/EntityPicker.cshtml b/src/Presentation/SmartStore.Web/Views/Common/EntityPicker.cshtml new file mode 100644 index 0000000000..f2ce8112b9 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Views/Common/EntityPicker.cshtml @@ -0,0 +1,96 @@ +@model EntityPickerModel +@using SmartStore.Web.Models.Common; + + + +@helper FormButtons() +{ +
    + @Html.LabelFor(model => model.ProductName, new { @class = "control-label", @for = "ProductName" }) +
    + @Html.TextBoxFor(model => model.ProductName, new { @class = "entity-picker-searchterm" }) + + +
    +
    +} + +@helper ProductSearchForm() +{ +
    + @FormButtons() + +
    +
    + @Html.LabelFor(model => model.CategoryId, new { @class = "control-label", @for = "CategoryId" }) +
    + @Html.DropDownListFor(model => model.CategoryId, Model.AvailableCategories, Model.AllString, new { @class = "item" }) +
    +
    + +
    + @Html.LabelFor(model => model.ManufacturerId, new { @class = "control-label", @for = "ManufacturerId" }) +
    + @Html.DropDownListFor(model => model.ManufacturerId, Model.AvailableManufacturers, Model.AllString, new { @class = "item" }) +
    +
    + + @if (Model.AvailableStores.Count > 1) + { +
    + @Html.LabelFor(model => model.StoreId, new { @class = "control-label", @for = "StoreId" }) +
    + @Html.DropDownListFor(model => model.StoreId, Model.AvailableStores, Model.AllString, new { @class = "item" }) +
    +
    + } + +
    + @Html.LabelFor(model => model.ProductTypeId, new { @class = "control-label", @for = "ProductTypeId" }) +
    + @Html.DropDownListFor(model => model.ProductTypeId, Model.AvailableProductTypes, Model.AllString, new { @class = "item" }) +
    +
    +
    +
    +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Views/Common/EntityPickerList.cshtml b/src/Presentation/SmartStore.Web/Views/Common/EntityPickerList.cshtml new file mode 100644 index 0000000000..7537bfba4f --- /dev/null +++ b/src/Presentation/SmartStore.Web/Views/Common/EntityPickerList.cshtml @@ -0,0 +1,56 @@ +@using SmartStore.Web.Models.Common; +@model EntityPickerModel +@helper HighlightSearchTermInTitle(EntityPickerModel.SearchResultModel item) +{ + if (Model.HighligtSearchTerm && Model.SearchTerm.HasValue() && item.Title.HasValue()) + { + @Html.Raw(item.Title.Replace(Model.SearchTerm, "{0}".FormatInvariant(Model.SearchTerm), StringComparison.OrdinalIgnoreCase)) + } + else + { + @item.Title + } +} +@foreach (var item in Model.SearchResult ?? Enumerable.Empty()) +{ +
    +
    +
    + @if (item.ImageUrl.HasValue()) + { + + } +
    +
    +
    + @if (item.LabelText.HasValue()) + { + @item.LabelText + } + @HighlightSearchTermInTitle(item) +
    +
    + @if (item.Published.HasValue) + { + + } + @item.Summary +
    +
    +
    +
    +} + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Views/Common/Footer.cshtml b/src/Presentation/SmartStore.Web/Views/Common/Footer.cshtml index 77b6c316da..cb9d4ab796 100644 --- a/src/Presentation/SmartStore.Web/Views/Common/Footer.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Common/Footer.cshtml @@ -52,28 +52,45 @@
    - } - } + } + } @if (Model.RedeemedRewardPoints > 0) { diff --git a/src/Presentation/SmartStore.Web/Views/Product/ProductTemplate.Grouped.cshtml b/src/Presentation/SmartStore.Web/Views/Product/ProductTemplate.Grouped.cshtml index f86e9dcef7..850a58a87e 100644 --- a/src/Presentation/SmartStore.Web/Views/Product/ProductTemplate.Grouped.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Product/ProductTemplate.Grouped.cshtml @@ -1,27 +1,29 @@ @model ProductDetailsModel @using SmartStore.Core; -@using SmartStore.Core.Domain.Common; +@using SmartStore.Core.Domain.Media; @using SmartStore.Core.Domain.Seo; @using SmartStore.Core.Infrastructure; @using SmartStore.Web; @using SmartStore.Web.Models.Catalog; @using SmartStore.Web.Framework.UI; @{ - Layout = "~/Views/Shared/_ColumnsTwo.cshtml"; - - //title, meta - Html.AddTitleParts(!String.IsNullOrEmpty(Model.MetaTitle) ? Model.MetaTitle : Model.Name); - Html.AddMetaDescriptionParts(Model.MetaDescription); - Html.AddMetaKeywordParts(Model.MetaKeywords); - - Html.AddScriptParts("~/Scripts/public.product-detail.js"); - - var canonicalUrlsEnabled = EngineContext.Current.Resolve().CanonicalUrlsEnabled; - if (canonicalUrlsEnabled) - { - var productUrl = Url.RouteUrl("Product", new { SeName = Model.SeName }, this.Request.Url.Scheme); - Html.AddCanonicalUrlParts(productUrl); - } + Layout = "~/Views/Shared/_ColumnsTwo.cshtml"; + + //title, meta + Html.AddTitleParts(!String.IsNullOrEmpty(Model.MetaTitle) ? Model.MetaTitle : Model.Name); + Html.AddMetaDescriptionParts(Model.MetaDescription); + Html.AddMetaKeywordParts(Model.MetaKeywords); + + Html.AddScriptParts("~/Scripts/public.product-detail.js"); + + int pictureSize = EngineContext.Current.Resolve().ProductDetailsPictureSize; + + var canonicalUrlsEnabled = EngineContext.Current.Resolve().CanonicalUrlsEnabled; + if (canonicalUrlsEnabled) + { + var productUrl = Url.RouteUrl("Product", new { SeName = Model.SeName }, this.Request.Url.Scheme); + Html.AddCanonicalUrlParts(productUrl); + } }
    @@ -70,23 +72,21 @@ @Html.Action("ReviewOverview", new { id = Model.Id }) - @if (Model.Manufacturers != null && Model.Manufacturers.Count > 0) - { -
    - - @foreach (var manufacturer in Model.Manufacturers) - { -
    -
    - - @manufacturer.PictureModel.AlternateText - -
    -
    - } - -
    - } + @if (Model.Manufacturers != null && Model.Manufacturers.Any(x => x.PictureModel != null && x.PictureModel.ImageUrl.HasValue())) + { +
    + @foreach (var manufacturer in Model.Manufacturers.Where(x => x.PictureModel != null && x.PictureModel.ImageUrl.HasValue())) + { +
    +
    + + @manufacturer.PictureModel.AlternateText + +
    +
    + } +
    + } +@Html.Widget("mobile_headerlogo_after") \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Views/Shared/_CreateOrUpdateAddress.cshtml b/src/Presentation/SmartStore.Web/Views/Shared/_CreateOrUpdateAddress.cshtml index 23bde2bdda..ad1a8d84cb 100644 --- a/src/Presentation/SmartStore.Web/Views/Shared/_CreateOrUpdateAddress.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Shared/_CreateOrUpdateAddress.cshtml @@ -159,18 +159,18 @@
    @Html.LabelFor(model => model.PhoneNumber, new { @class="control-label" + (Model.PhoneRequired ? " required" : "") , @for="PhoneNumber" } )
    - @Html.TextBoxFor(model => model.PhoneNumber, (Model.PhoneRequired ? null : new { placeholder = "Optional" } ) ) + @Html.TextBoxFor(model => model.PhoneNumber, new { placeholder = Model.PhoneRequired ? null : T("Common.Optional"), @type = "tel" }) @Html.ValidationMessageFor(model => model.PhoneNumber)
    } @if (Model.FaxEnabled) - { + {
    @Html.LabelFor(model => model.FaxNumber, new { @class="control-label" + (Model.FaxRequired ? " required" : ""), @for="FaxNumber" } )
    - @Html.TextBoxFor(model => model.FaxNumber, (Model.FaxRequired ? null : new { placeholder = "Optional" } ) ) + @Html.TextBoxFor(model => model.FaxNumber, new { placeholder = Model.FaxRequired ? null : T("Common.Optional"), @type = "tel" }) @Html.ValidationMessageFor(model => model.FaxNumber)
    diff --git a/src/Presentation/SmartStore.Web/Views/Shared/_ProductBox.Mobile.cshtml b/src/Presentation/SmartStore.Web/Views/Shared/_ProductBox.Mobile.cshtml index 03504b527d..5a787d4d30 100644 --- a/src/Presentation/SmartStore.Web/Views/Shared/_ProductBox.Mobile.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Shared/_ProductBox.Mobile.cshtml @@ -1,7 +1,10 @@ @model ProductOverviewModel @using SmartStore.Web.Models.Catalog; - @Model.DefaultPictureModel.AlternateText + @if (Model.DefaultPictureModel.ImageUrl.HasValue()) + { + @Model.DefaultPictureModel.AlternateText + }
    @Model.Name
    @if (!String.IsNullOrEmpty(Model.ShortDescription)) { diff --git a/src/Presentation/SmartStore.Web/Views/Shared/_ProductBox.cshtml b/src/Presentation/SmartStore.Web/Views/Shared/_ProductBox.cshtml index 63254eb14e..37bb9be1c0 100644 --- a/src/Presentation/SmartStore.Web/Views/Shared/_ProductBox.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Shared/_ProductBox.cshtml @@ -30,7 +30,7 @@ data-form-selector="#product-details-form" data-type="wishlist" data-action="add"> - + @T("Categories.Wishlist")
    } @@ -42,7 +42,7 @@ rel="nofollow" data-type="compare" data-action="add"> - + @T("Categories.Compare") } @@ -50,9 +50,12 @@ }
    - - @Model.DefaultPictureModel.AlternateText - + @if (Model.DefaultPictureModel.ImageUrl.HasValue()) + { + + @Model.DefaultPictureModel.AlternateText + + } @if (Model.ColorAttributes.Any()) {
    @@ -66,7 +69,8 @@
    - @if (Model.ShowReviews) { + @if (Model.ShowReviews) + { int ratingPercent = 0; if (Model.TotalReviews != 0) { @@ -103,13 +107,15 @@ @{ - string desc = null, descTitle = null; - if (Model.ShortDescription.HasValue()) { - desc = Model.ShortDescription.Truncate(50, "..."); - if (Model.ShortDescription.Length > desc.Length) { - descTitle = Model.ShortDescription; - } - } + string desc = null, descTitle = null; + if (Model.ShortDescription.HasValue()) + { + desc = Model.ShortDescription.Truncate(50, "..."); + if (Model.ShortDescription.Length > desc.Length) + { + descTitle = Model.ShortDescription; + } + } }

    @Html.Raw(desc) @@ -176,7 +182,7 @@ rel="tooltip nofollow" data-type="cart" data-action="add"> - + } else diff --git a/src/Presentation/SmartStore.Web/Views/Shared/_ProductBoxCompact.cshtml b/src/Presentation/SmartStore.Web/Views/Shared/_ProductBoxCompact.cshtml index ed3cffd7f8..443fd03826 100644 --- a/src/Presentation/SmartStore.Web/Views/Shared/_ProductBoxCompact.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Shared/_ProductBoxCompact.cshtml @@ -3,8 +3,11 @@ @using SmartStore.Web.Models.Catalog;

    - - @Model.DefaultPictureModel.AlternateText + @if (Model.DefaultPictureModel.ImageUrl.HasValue()) + { + + @Model.DefaultPictureModel.AlternateText + }
    @{ diff --git a/src/Presentation/SmartStore.Web/Views/Shared/_Root.Head.Mobile.cshtml b/src/Presentation/SmartStore.Web/Views/Shared/_Root.Head.Mobile.cshtml index 0b77f51229..44325f9fed 100644 --- a/src/Presentation/SmartStore.Web/Views/Shared/_Root.Head.Mobile.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Shared/_Root.Head.Mobile.cshtml @@ -41,7 +41,7 @@ }); - + @RenderBody() diff --git a/src/Presentation/SmartStore.Web/Views/Shared/_Root.Head.cshtml b/src/Presentation/SmartStore.Web/Views/Shared/_Root.Head.cshtml index 5dfc536e54..902712cb95 100644 --- a/src/Presentation/SmartStore.Web/Views/Shared/_Root.Head.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Shared/_Root.Head.cshtml @@ -5,83 +5,85 @@ @using SmartStore.Core.Domain.Localization; @{ - var currentUICulture = System.Threading.Thread.CurrentThread.CurrentUICulture; - - var localizationService = EngineContext.Current.Resolve(); - - // add css assets - Html.AppendCssFileParts( + var currentUICulture = System.Threading.Thread.CurrentThread.CurrentUICulture; + + var localizationService = EngineContext.Current.Resolve(); + + // add css assets + Html.AppendCssFileParts( "~/Content/font-awesome.css", - "~/Content/jquery.pnotify.default.css", - "~/Content/jquery.pnotify.default.icons.css"); + "~/Content/jquery.pnotify.default.css", + "~/Content/jquery.pnotify.default.icons.css", + "~/Content/smartstore.entitypicker.css"); - Html.AppendScriptParts(ResourceLocation.Head, - "~/Scripts/modernizr-2.7.2.js", - "~/Scripts/jquery-2.1.1.js", + Html.AppendScriptParts(ResourceLocation.Head, + "~/Scripts/modernizr-2.7.2.js", + "~/Scripts/jquery-2.1.1.js", "~/Scripts/jquery-migrate-1.2.1.js", - "~/Scripts/globalize/globalize.js"); + "~/Scripts/globalize/globalize.js"); - Html.AddScriptParts(ResourceLocation.Head, true, - "~/Scripts/globalize/cultures/globalize.culture.{0}.js".FormatInvariant(currentUICulture.ToString())); + Html.AddScriptParts(ResourceLocation.Head, true, + "~/Scripts/globalize/cultures/globalize.culture.{0}.js".FormatInvariant(currentUICulture.ToString())); - var bootstrapJsRoot = "~/Content/bootstrap/js/"; - Html.AppendScriptParts(ResourceLocation.Foot, + var bootstrapJsRoot = "~/Content/bootstrap/js/"; + Html.AppendScriptParts(ResourceLocation.Foot, // jQuery UI Core "~/Scripts/jquery-ui/effect.js", "~/Scripts/jquery-ui/effect-transfer.js", "~/Scripts/jquery-ui/effect-shake.js", "~/Scripts/jquery-ui/position.js", - // jQuery Validation - "~/Scripts/jquery.unobtrusive-ajax.js", - "~/Scripts/jquery.validate.js", - "~/Scripts/jquery.validate.unobtrusive.js", - // SmartStore system - "~/Scripts/smartstore.system.js", - "~/Scripts/underscore.js", - "~/Scripts/underscore.string.js", - "~/Scripts/underscore.mixins.js", - "~/Scripts/smartstore.jquery.utils.js", - "~/Scripts/jquery.ba-outside-events.js", - "~/Scripts/jquery.preload.js", - "~/Scripts/jquery.transit.js", - "~/Scripts/jquery.menu-aim.js", - "~/Scripts/smartstore.doAjax.js", - "~/Scripts/jquery.addeasing.js", - "~/Scripts/smartstore.eventbroker.js", - "~/Scripts/smartstore.hacks.js", + // jQuery Validation + "~/Scripts/jquery.unobtrusive-ajax.js", + "~/Scripts/jquery.validate.js", + "~/Scripts/jquery.validate.unobtrusive.js", + // SmartStore system + "~/Scripts/smartstore.system.js", + "~/Scripts/underscore.js", + "~/Scripts/underscore.string.js", + "~/Scripts/underscore.mixins.js", + "~/Scripts/smartstore.jquery.utils.js", + "~/Scripts/jquery.ba-outside-events.js", + "~/Scripts/jquery.preload.js", + "~/Scripts/jquery.transit.js", + "~/Scripts/jquery.menu-aim.js", + "~/Scripts/smartstore.doAjax.js", + "~/Scripts/smartstore.entityPicker.js", + "~/Scripts/jquery.addeasing.js", + "~/Scripts/smartstore.eventbroker.js", + "~/Scripts/smartstore.hacks.js", "~/Scripts/smartstore.common.js", - // Bootstrap - bootstrapJsRoot + "bootstrap-transition.js", - bootstrapJsRoot + "bootstrap-alert.js", - bootstrapJsRoot + "bootstrap-button.js", - bootstrapJsRoot + "bootstrap-collapse.js", - bootstrapJsRoot + "bootstrap-dropdown.js", - bootstrapJsRoot + "bootstrap-modal.js", - bootstrapJsRoot + "bootstrap-tooltip.js", - bootstrapJsRoot + "bootstrap-popover.js", - bootstrapJsRoot + "bootstrap-tab.js", + // Bootstrap + bootstrapJsRoot + "bootstrap-transition.js", + bootstrapJsRoot + "bootstrap-alert.js", + bootstrapJsRoot + "bootstrap-button.js", + bootstrapJsRoot + "bootstrap-collapse.js", + bootstrapJsRoot + "bootstrap-dropdown.js", + bootstrapJsRoot + "bootstrap-modal.js", + bootstrapJsRoot + "bootstrap-tooltip.js", + bootstrapJsRoot + "bootstrap-popover.js", + bootstrapJsRoot + "bootstrap-tab.js", bootstrapJsRoot + "bootstrap-typeahead.js", - // Bootstrap custom - bootstrapJsRoot + "custom/bootstrap-datetimepicker.js", - // Shared UI - "~/Scripts/smartstore.placeholder.js", - "~/Scripts/select2.js", - "~/Scripts/smartstore.selectwrapper.js", - "~/Scripts/smartstore.throbber.js", - "~/Scripts/smartstore.navbar.js", - "~/Scripts/smartstore.thumbzoomer.js", - "~/Scripts/smartstore.column-equalizer.js", - "~/Scripts/smartstore.shrinkmenu.js", - "~/Scripts/smartstore.scrollbutton.js", - "~/Scripts/jquery.pnotify.js", - "~/Scripts/jquery.scrollTo.js", - "~/Scripts/jquery.serialScroll.js", + // Bootstrap custom + bootstrapJsRoot + "custom/bootstrap-datetimepicker.js", + // Shared UI + "~/Scripts/smartstore.placeholder.js", + "~/Scripts/select2.js", + "~/Scripts/smartstore.selectwrapper.js", + "~/Scripts/smartstore.throbber.js", + "~/Scripts/smartstore.navbar.js", + "~/Scripts/smartstore.thumbzoomer.js", + "~/Scripts/smartstore.column-equalizer.js", + "~/Scripts/smartstore.shrinkmenu.js", + "~/Scripts/smartstore.scrollbutton.js", + "~/Scripts/jquery.pnotify.js", + "~/Scripts/jquery.scrollTo.js", + "~/Scripts/jquery.serialScroll.js", "~/Scripts/smartstore.globalize.adapter.js", - // Shop - "~/Scripts/public.common.js", - "~/Scripts/public.ajaxcart.js", - "~/Scripts/public.shopbar.js", - "~/Scripts/public.product-list-scroller.js", + // Shop + "~/Scripts/public.common.js", + "~/Scripts/public.ajaxcart.js", + "~/Scripts/public.shopbar.js", + "~/Scripts/public.product-list-scroller.js", "~/Scripts/public.product-filter.js"); } @@ -97,6 +99,7 @@ @*This is used so that themes can inject content into the header*@ @Html.Partial("Head") + @Html.SmartMetaRobots() @Html.MetaAcceptLanguage() @Html.Partial("_ClientRes") diff --git a/src/Presentation/SmartStore.Web/Views/ShoppingCart/EstimateShipping.cshtml b/src/Presentation/SmartStore.Web/Views/ShoppingCart/EstimateShipping.cshtml index c424331ec1..d59e914960 100644 --- a/src/Presentation/SmartStore.Web/Views/ShoppingCart/EstimateShipping.cshtml +++ b/src/Presentation/SmartStore.Web/Views/ShoppingCart/EstimateShipping.cshtml @@ -12,7 +12,7 @@ var throbber = estimateBox.data("throbber"); if (!throbber) { - throbber = estimateBox.throbber({ white: true, small: true, show: false }).data("throbber"); + throbber = estimateBox.throbber({ white: true, small: true, message: '', show: false }).data("throbber"); } throbber.show(); @@ -80,7 +80,7 @@
    -
    +
    @if (Model.ShippingOptions.Any()) {
    @@ -88,7 +88,8 @@ {
    - @shippingOption.Name (@shippingOption.Price) + @Html.Raw(HttpUtility.HtmlDecode(shippingOption.Name)) + (@shippingOption.Price)
    @Html.Raw(shippingOption.Description) @@ -107,10 +108,13 @@
    }
    -
    -
    - @Html.Raw(String.Format(T("ShoppingCart.ShippingInfoLink"), Url.RouteUrl("Topic", new { SystemName = "shippinginfo" }))) + @if (Model.ShippingInfoUrl.HasValue()) + { +
    + @Html.Raw(T("ShoppingCart.ShippingInfoLink", Model.ShippingInfoUrl)) +
    + }
    } \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Views/ShoppingCart/FlyoutShoppingCart.cshtml b/src/Presentation/SmartStore.Web/Views/ShoppingCart/FlyoutShoppingCart.cshtml index 4af27a5a06..0ed8e7bcb3 100644 --- a/src/Presentation/SmartStore.Web/Views/ShoppingCart/FlyoutShoppingCart.cshtml +++ b/src/Presentation/SmartStore.Web/Views/ShoppingCart/FlyoutShoppingCart.cshtml @@ -32,9 +32,11 @@ @if (Model.ShowProductImages) {
    - - @item.Picture.AlternateText - + @if(item.Picture.ImageUrl.HasValue()) + { + + @item.Picture.AlternateText + }
    }
    @@ -68,14 +70,17 @@ {
    @for (int j = 0, bundlelength = item.BundleItems.Count; j < bundlelength; j++) - { - var bundleItem = item.BundleItems[j]; + { + var bundleItem = item.BundleItems[j]; - - @bundleItem.ProductName - - if (bundlelength != (j + 1)) - { + if (bundleItem.PictureUrl.HasValue()) + { + + @bundleItem.ProductName + } + + if (bundlelength != (j + 1)) + { } } diff --git a/src/Presentation/SmartStore.Web/Views/ShoppingCart/FlyoutWishlist.cshtml b/src/Presentation/SmartStore.Web/Views/ShoppingCart/FlyoutWishlist.cshtml index a2a157d2d9..1985c87658 100644 --- a/src/Presentation/SmartStore.Web/Views/ShoppingCart/FlyoutWishlist.cshtml +++ b/src/Presentation/SmartStore.Web/Views/ShoppingCart/FlyoutWishlist.cshtml @@ -28,9 +28,11 @@ @if (Model.ShowProductImages) {
    - - @item.Picture.AlternateText - + @if (item.Picture.ImageUrl.HasValue()) + { + + @item.Picture.AlternateText + }
    }
    @@ -56,7 +58,7 @@ data-name="@item.ProductName" data-type="cart" data-action="addfromwishlist"> - +
    } diff --git a/src/Presentation/SmartStore.Web/Views/ShoppingCart/OrderSummary.Mobile.cshtml b/src/Presentation/SmartStore.Web/Views/ShoppingCart/OrderSummary.Mobile.cshtml index a543fb732d..64fdbd9632 100644 --- a/src/Presentation/SmartStore.Web/Views/ShoppingCart/OrderSummary.Mobile.cshtml +++ b/src/Presentation/SmartStore.Web/Views/ShoppingCart/OrderSummary.Mobile.cshtml @@ -75,7 +75,7 @@ using (Html.BeginRouteForm("ShoppingCart", FormMethod.Post, new Dictionary { { "data-ajax", "false" }, { "enctype", "multipart/form-data" } })) {
    -
      +
        @foreach (var item in Model.Items) {
      • @@ -155,6 +155,24 @@
    }
    + @if (!Model.IsEditable && item.IsDownload && item.HasUserAgreement) + { +
    + +
    + } + @if (!Model.IsEditable && Model.DisplayEsdRevocationWaiverBox && item.IsEsd) + { +
    + +
    + } @if (Model.IsEditable) {
    @@ -274,5 +292,41 @@ { @T("ShoppingCart.CartIsEmpty") } + + @if (!Model.IsEditable) + { +
    + @if (Model.NewsLetterSubscription != CheckoutNewsLetterSubscription.None) + { + + } + @if (Model.ThirdPartyEmailHandOver != CheckoutThirdPartyEmailHandOver.None) + { + + } +
    + } + @Html.Widget("mobile_order_summary_content_after")
    + +@if (!Model.IsEditable && Model.Items.Any(x => x.IsDownload && x.HasUserAgreement)) +{ + +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Views/ShoppingCart/OrderSummary.cshtml b/src/Presentation/SmartStore.Web/Views/ShoppingCart/OrderSummary.cshtml index a89ac3e95b..0ea8605123 100644 --- a/src/Presentation/SmartStore.Web/Views/ShoppingCart/OrderSummary.cshtml +++ b/src/Presentation/SmartStore.Web/Views/ShoppingCart/OrderSummary.cshtml @@ -21,7 +21,10 @@ @if (Model.ShowProductImages) {
    } } + + @if (Model.DisplayWeight) + { + + } + }
    - @Html.SmartLabelFor(model => model.SlugsPerEntity) - - @Model.SlugsPerEntity -
    @Html.SmartLabelFor(model => model.IsActive) @@ -37,24 +30,25 @@ @Html.EditorFor(model => model.Slug) @Html.ValidationMessageFor(model => model.Slug) + @Html.Action("NamesPerEntity", "UrlRecord", new { entityName = Model.EntityName, entityId = @Model.EntityId })
    - @Html.SmartLabelFor(model => model.EntityId) + @Html.SmartLabelFor(model => model.EntityName) - @Html.EditorFor(model => model.EntityId) - @Html.ValidationMessageFor(model => model.EntityId) + @Html.EditorFor(model => model.EntityName) + @Html.ValidationMessageFor(model => model.EntityName)
    - @Html.SmartLabelFor(model => model.EntityName) + @Html.SmartLabelFor(model => model.EntityId) - @Html.EditorFor(model => model.EntityName) - @Html.ValidationMessageFor(model => model.EntityName) + @Html.EditorFor(model => model.EntityId) + @Html.ValidationMessageFor(model => model.EntityId)
    -

    - @product.DefaultPictureModel.AlternateText -

    + @if (product.DefaultPictureModel.ImageUrl.HasValue()) + { +

    + + @product.DefaultPictureModel.AlternateText + +

    + }

    @T("Common.Remove") @@ -84,7 +87,15 @@ @foreach (var product in Model.Products) {

    - @product.ProductPrice.Price +
    + @product.ProductPrice.Price +
    + @if (product.BasePriceInfo.HasValue()) + { +
    + @product.BasePriceInfo +
    + }
    - - @string.Format(T("Order.GiftCardInfo").Text, gc.CouponCode): + @string.Format(T("Order.GiftCardInfo").Text, gc.CouponCode): +
    + @string.Format(T("ShoppingCart.Totals.GiftCardInfo.Remaining").Text, gc.Remaining)
    diff --git a/src/Presentation/SmartStore.Web/Views/Order/Details.cshtml b/src/Presentation/SmartStore.Web/Views/Order/Details.cshtml index 8e56cfc5a6..5a4fbfb5c6 100644 --- a/src/Presentation/SmartStore.Web/Views/Order/Details.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Order/Details.cshtml @@ -532,8 +532,9 @@ {
    - - @string.Format(T("Order.GiftCardInfo").Text, gc.CouponCode): + @string.Format(T("Order.GiftCardInfo").Text, gc.CouponCode): +
    + @string.Format(T("ShoppingCart.Totals.GiftCardInfo.Remaining").Text, gc.Remaining)
    @@ -541,8 +542,8 @@
    - @item.Picture.AlternateText + @if (item.Picture.ImageUrl.HasValue()) + { + @item.Picture.AlternateText + } @@ -41,15 +44,12 @@ } - @if (Model.DisplayDeliveryTime) + @if (Model.DisplayDeliveryTime && item.IsShipEnabled && (!String.IsNullOrEmpty(item.DeliveryTimeName) && !String.IsNullOrEmpty(item.DeliveryTimeHexValue))) {
    - @if ((!String.IsNullOrEmpty(item.DeliveryTimeName) && !String.IsNullOrEmpty(item.DeliveryTimeHexValue)) && item.IsShipEnabled) - { - @T("Products.DeliveryTime") - - @item.DeliveryTimeName - } + @T("Products.DeliveryTime") + + @item.DeliveryTimeName
    } @@ -65,6 +65,28 @@ @Html.Raw(item.RecurringInfo) } + @if (!Model.IsEditable && item.IsDownload && item.HasUserAgreement) + { +
    +
    + +
    +
    + } + @if (!Model.IsEditable && Model.DisplayEsdRevocationWaiverBox && item.IsEsd) + { +
    +
    + +
    +
    + } @if (item.Warnings.Count > 0) {
    @@ -79,7 +101,7 @@ @if (Model.DisplayWeight) {
    - @((item.Weight * item.Quantity).ToString("N2")) kg + @((item.Weight * item.Quantity).ToString("N2")) @Model.MeasureUnitName @@ -123,8 +145,7 @@ @helper BundleProducts(ShoppingCartModel.ShoppingCartItemModel parentItem) { if (parentItem.ChildItems != null) - { - + {
      @@ -152,7 +173,7 @@ @if (Model.ShowProductBundleImages) {
    - @if (!item.BundleItem.HideThumbnail) + @if (item.Picture.ImageUrl.HasValue() && !item.BundleItem.HideThumbnail) { @item.Picture.AlternateText } @@ -205,6 +226,14 @@
    + @((item.Weight * item.Quantity).ToString("N2")) @Model.MeasureUnitName + @if (!String.IsNullOrWhiteSpace(item.BundleItem.PriceWithDiscount)) { @@ -237,6 +266,35 @@ @Html.Partial("_CommentBox", Model) } + @if (!Model.IsEditable && (Model.NewsLetterSubscription != CheckoutNewsLetterSubscription.None || Model.ThirdPartyEmailHandOver != CheckoutThirdPartyEmailHandOver.None)) + { + + } + @if (!String.IsNullOrEmpty(Model.CheckoutAttributeInfo)) {
    @@ -410,3 +468,33 @@
    } + +@if (!Model.IsEditable && Model.Items.Any(x => x.IsDownload && x.HasUserAgreement)) +{ + + +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Views/ShoppingCart/Wishlist.cshtml b/src/Presentation/SmartStore.Web/Views/ShoppingCart/Wishlist.cshtml index 57e67c51ad..9af4cc4676 100644 --- a/src/Presentation/SmartStore.Web/Views/ShoppingCart/Wishlist.cshtml +++ b/src/Presentation/SmartStore.Web/Views/ShoppingCart/Wishlist.cshtml @@ -33,7 +33,10 @@ @if (Model.ShowProductImages) {
    - @item.Picture.AlternateText + @if (item.Picture.ImageUrl.HasValue()) + { + @item.Picture.AlternateText + } @@ -133,7 +136,7 @@ @if (Model.ShowProductBundleImages) {
    - @if (!item.BundleItem.HideThumbnail) + @if (item.Picture.ImageUrl.HasValue() && !item.BundleItem.HideThumbnail) { @item.Picture.AlternateText } @@ -313,7 +316,7 @@ @if (Model.DisplayAddToCart) { } diff --git a/src/Presentation/SmartStore.Web/Views/Web.config b/src/Presentation/SmartStore.Web/Views/Web.config index 3f3148484e..bd9d09444f 100644 --- a/src/Presentation/SmartStore.Web/Views/Web.config +++ b/src/Presentation/SmartStore.Web/Views/Web.config @@ -10,7 +10,7 @@ - + diff --git a/src/Presentation/SmartStore.Web/Views/Widget/TabWidgets.cshtml b/src/Presentation/SmartStore.Web/Views/Widget/TabWidgets.cshtml new file mode 100644 index 0000000000..f8e7be55a9 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Views/Widget/TabWidgets.cshtml @@ -0,0 +1,5 @@ +@model IEnumerable + +@foreach (var widget in Model) { + @Html.Action(widget.Action, widget.Controller, widget.RouteValues) +} \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Web.Release.config b/src/Presentation/SmartStore.Web/Web.Release.config index 49f9b30567..f9afce5a4c 100644 --- a/src/Presentation/SmartStore.Web/Web.Release.config +++ b/src/Presentation/SmartStore.Web/Web.Release.config @@ -5,7 +5,7 @@ - + diff --git a/src/Presentation/SmartStore.Web/Web.config b/src/Presentation/SmartStore.Web/Web.config index f45ffa195f..eeab4dda13 100644 --- a/src/Presentation/SmartStore.Web/Web.config +++ b/src/Presentation/SmartStore.Web/Web.config @@ -75,14 +75,17 @@ + - + + + @@ -154,7 +167,7 @@ - + @@ -164,12 +177,14 @@ + + @@ -228,7 +243,7 @@ - + @@ -256,11 +271,11 @@ - + - + @@ -302,6 +317,14 @@ + + + + + + + + diff --git a/src/Presentation/SmartStore.Web/packages.config b/src/Presentation/SmartStore.Web/packages.config index 777a775b2c..23a46f19b7 100644 --- a/src/Presentation/SmartStore.Web/packages.config +++ b/src/Presentation/SmartStore.Web/packages.config @@ -1,18 +1,18 @@  - + - - + + - - - - + + + + @@ -26,9 +26,8 @@ - - - + + \ No newline at end of file diff --git a/src/SmartStoreNET.Minimal.sln b/src/SmartStoreNET.Minimal.sln new file mode 100644 index 0000000000..feeb737168 --- /dev/null +++ b/src/SmartStoreNET.Minimal.sln @@ -0,0 +1,270 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.23107.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{7881B112-7843-4542-B1F7-F99553FB9BB7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartStore.Services", "Libraries\SmartStore.Services\SmartStore.Services.csproj", "{210541AD-F659-47DA-8763-16F36C5CD2F4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartStore.Data", "Libraries\SmartStore.Data\SmartStore.Data.csproj", "{CCD7F2C9-6A2C-4CF0-8E89-076B8FC0F144}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartStore.Core", "Libraries\SmartStore.Core\SmartStore.Core.csproj", "{6BDA8332-939F-45B7-A25E-7A797260AE59}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartStore.Web", "Presentation\SmartStore.Web\SmartStore.Web.csproj", "{4F1F649C-1020-45BE-A487-F416D9297FF3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartStore.Admin", "Presentation\SmartStore.Web\Administration\SmartStore.Admin.csproj", "{152C761A-DD2E-4C1F-AF89-DFB2547A3BCA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartStore.Web.Framework", "Presentation\SmartStore.Web.Framework\SmartStore.Web.Framework.csproj", "{75FD4163-333C-4DD5-854D-2EF294E45D94}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E11A41CA-FA9F-4BB7-A35E-F80EE1084232}" + ProjectSection(SolutionItems) = preProject + AssemblySharedInfo.cs = AssemblySharedInfo.cs + AssemblyVersionInfo.cs = AssemblyVersionInfo.cs + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{C0A5A9AE-1509-4795-82CD-0BE7D6A17C69}" + ProjectSection(SolutionItems) = preProject + .nuget\NuGet.Config = .nuget\NuGet.Config + .nuget\NuGet.exe = .nuget\NuGet.exe + .nuget\NuGet.targets = .nuget\NuGet.targets + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartStore.OfflinePayment", "Plugins\SmartStore.OfflinePayment\SmartStore.OfflinePayment.csproj", "{692E9D31-1393-47BF-B372-63F671052F89}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartStore.Tax", "Plugins\SmartStore.Tax\SmartStore.Tax.csproj", "{94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartStore.Shipping", "Plugins\SmartStore.Shipping\SmartStore.Shipping.csproj", "{ACC1E122-B2C8-4441-BDED-D4A77763331A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartStore.DevTools", "Plugins\SmartStore.DevTools\SmartStore.DevTools.csproj", "{542B9C12-E2A1-49BB-9134-0E3484F9D669}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|Mixed Platforms = Debug|Mixed Platforms + Debug|x86 = Debug|x86 + EFMigrations|Any CPU = EFMigrations|Any CPU + EFMigrations|Mixed Platforms = EFMigrations|Mixed Platforms + EFMigrations|x86 = EFMigrations|x86 + PluginDev|Any CPU = PluginDev|Any CPU + PluginDev|Mixed Platforms = PluginDev|Mixed Platforms + PluginDev|x86 = PluginDev|x86 + Release|Any CPU = Release|Any CPU + Release|Mixed Platforms = Release|Mixed Platforms + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {210541AD-F659-47DA-8763-16F36C5CD2F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {210541AD-F659-47DA-8763-16F36C5CD2F4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {210541AD-F659-47DA-8763-16F36C5CD2F4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {210541AD-F659-47DA-8763-16F36C5CD2F4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {210541AD-F659-47DA-8763-16F36C5CD2F4}.Debug|x86.ActiveCfg = Debug|Any CPU + {210541AD-F659-47DA-8763-16F36C5CD2F4}.EFMigrations|Any CPU.ActiveCfg = EFMigrations|Any CPU + {210541AD-F659-47DA-8763-16F36C5CD2F4}.EFMigrations|Any CPU.Build.0 = EFMigrations|Any CPU + {210541AD-F659-47DA-8763-16F36C5CD2F4}.EFMigrations|Mixed Platforms.ActiveCfg = EFMigrations|Any CPU + {210541AD-F659-47DA-8763-16F36C5CD2F4}.EFMigrations|Mixed Platforms.Build.0 = EFMigrations|Any CPU + {210541AD-F659-47DA-8763-16F36C5CD2F4}.EFMigrations|x86.ActiveCfg = EFMigrations|Any CPU + {210541AD-F659-47DA-8763-16F36C5CD2F4}.PluginDev|Any CPU.ActiveCfg = PluginDev|Any CPU + {210541AD-F659-47DA-8763-16F36C5CD2F4}.PluginDev|Any CPU.Build.0 = PluginDev|Any CPU + {210541AD-F659-47DA-8763-16F36C5CD2F4}.PluginDev|Mixed Platforms.ActiveCfg = PluginDev|Any CPU + {210541AD-F659-47DA-8763-16F36C5CD2F4}.PluginDev|Mixed Platforms.Build.0 = PluginDev|Any CPU + {210541AD-F659-47DA-8763-16F36C5CD2F4}.PluginDev|x86.ActiveCfg = PluginDev|Any CPU + {210541AD-F659-47DA-8763-16F36C5CD2F4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {210541AD-F659-47DA-8763-16F36C5CD2F4}.Release|Any CPU.Build.0 = Release|Any CPU + {210541AD-F659-47DA-8763-16F36C5CD2F4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {210541AD-F659-47DA-8763-16F36C5CD2F4}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {210541AD-F659-47DA-8763-16F36C5CD2F4}.Release|x86.ActiveCfg = Release|Any CPU + {CCD7F2C9-6A2C-4CF0-8E89-076B8FC0F144}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CCD7F2C9-6A2C-4CF0-8E89-076B8FC0F144}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CCD7F2C9-6A2C-4CF0-8E89-076B8FC0F144}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {CCD7F2C9-6A2C-4CF0-8E89-076B8FC0F144}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {CCD7F2C9-6A2C-4CF0-8E89-076B8FC0F144}.Debug|x86.ActiveCfg = Debug|Any CPU + {CCD7F2C9-6A2C-4CF0-8E89-076B8FC0F144}.EFMigrations|Any CPU.ActiveCfg = EFMigrations|Any CPU + {CCD7F2C9-6A2C-4CF0-8E89-076B8FC0F144}.EFMigrations|Any CPU.Build.0 = EFMigrations|Any CPU + {CCD7F2C9-6A2C-4CF0-8E89-076B8FC0F144}.EFMigrations|Mixed Platforms.ActiveCfg = EFMigrations|Any CPU + {CCD7F2C9-6A2C-4CF0-8E89-076B8FC0F144}.EFMigrations|Mixed Platforms.Build.0 = EFMigrations|Any CPU + {CCD7F2C9-6A2C-4CF0-8E89-076B8FC0F144}.EFMigrations|x86.ActiveCfg = EFMigrations|Any CPU + {CCD7F2C9-6A2C-4CF0-8E89-076B8FC0F144}.PluginDev|Any CPU.ActiveCfg = PluginDev|Any CPU + {CCD7F2C9-6A2C-4CF0-8E89-076B8FC0F144}.PluginDev|Any CPU.Build.0 = PluginDev|Any CPU + {CCD7F2C9-6A2C-4CF0-8E89-076B8FC0F144}.PluginDev|Mixed Platforms.ActiveCfg = PluginDev|Any CPU + {CCD7F2C9-6A2C-4CF0-8E89-076B8FC0F144}.PluginDev|Mixed Platforms.Build.0 = PluginDev|Any CPU + {CCD7F2C9-6A2C-4CF0-8E89-076B8FC0F144}.PluginDev|x86.ActiveCfg = PluginDev|Any CPU + {CCD7F2C9-6A2C-4CF0-8E89-076B8FC0F144}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CCD7F2C9-6A2C-4CF0-8E89-076B8FC0F144}.Release|Any CPU.Build.0 = Release|Any CPU + {CCD7F2C9-6A2C-4CF0-8E89-076B8FC0F144}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {CCD7F2C9-6A2C-4CF0-8E89-076B8FC0F144}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {CCD7F2C9-6A2C-4CF0-8E89-076B8FC0F144}.Release|x86.ActiveCfg = Release|Any CPU + {6BDA8332-939F-45B7-A25E-7A797260AE59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6BDA8332-939F-45B7-A25E-7A797260AE59}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6BDA8332-939F-45B7-A25E-7A797260AE59}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {6BDA8332-939F-45B7-A25E-7A797260AE59}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {6BDA8332-939F-45B7-A25E-7A797260AE59}.Debug|x86.ActiveCfg = Debug|Any CPU + {6BDA8332-939F-45B7-A25E-7A797260AE59}.EFMigrations|Any CPU.ActiveCfg = EFMigrations|Any CPU + {6BDA8332-939F-45B7-A25E-7A797260AE59}.EFMigrations|Any CPU.Build.0 = EFMigrations|Any CPU + {6BDA8332-939F-45B7-A25E-7A797260AE59}.EFMigrations|Mixed Platforms.ActiveCfg = EFMigrations|Any CPU + {6BDA8332-939F-45B7-A25E-7A797260AE59}.EFMigrations|Mixed Platforms.Build.0 = EFMigrations|Any CPU + {6BDA8332-939F-45B7-A25E-7A797260AE59}.EFMigrations|x86.ActiveCfg = EFMigrations|Any CPU + {6BDA8332-939F-45B7-A25E-7A797260AE59}.PluginDev|Any CPU.ActiveCfg = PluginDev|Any CPU + {6BDA8332-939F-45B7-A25E-7A797260AE59}.PluginDev|Any CPU.Build.0 = PluginDev|Any CPU + {6BDA8332-939F-45B7-A25E-7A797260AE59}.PluginDev|Mixed Platforms.ActiveCfg = PluginDev|Any CPU + {6BDA8332-939F-45B7-A25E-7A797260AE59}.PluginDev|Mixed Platforms.Build.0 = PluginDev|Any CPU + {6BDA8332-939F-45B7-A25E-7A797260AE59}.PluginDev|x86.ActiveCfg = PluginDev|Any CPU + {6BDA8332-939F-45B7-A25E-7A797260AE59}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6BDA8332-939F-45B7-A25E-7A797260AE59}.Release|Any CPU.Build.0 = Release|Any CPU + {6BDA8332-939F-45B7-A25E-7A797260AE59}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {6BDA8332-939F-45B7-A25E-7A797260AE59}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {6BDA8332-939F-45B7-A25E-7A797260AE59}.Release|x86.ActiveCfg = Release|Any CPU + {4F1F649C-1020-45BE-A487-F416D9297FF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F1F649C-1020-45BE-A487-F416D9297FF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F1F649C-1020-45BE-A487-F416D9297FF3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {4F1F649C-1020-45BE-A487-F416D9297FF3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {4F1F649C-1020-45BE-A487-F416D9297FF3}.Debug|x86.ActiveCfg = Debug|Any CPU + {4F1F649C-1020-45BE-A487-F416D9297FF3}.EFMigrations|Any CPU.ActiveCfg = EFMigrations|Any CPU + {4F1F649C-1020-45BE-A487-F416D9297FF3}.EFMigrations|Any CPU.Build.0 = EFMigrations|Any CPU + {4F1F649C-1020-45BE-A487-F416D9297FF3}.EFMigrations|Mixed Platforms.ActiveCfg = EFMigrations|Any CPU + {4F1F649C-1020-45BE-A487-F416D9297FF3}.EFMigrations|Mixed Platforms.Build.0 = EFMigrations|Any CPU + {4F1F649C-1020-45BE-A487-F416D9297FF3}.EFMigrations|x86.ActiveCfg = EFMigrations|Any CPU + {4F1F649C-1020-45BE-A487-F416D9297FF3}.PluginDev|Any CPU.ActiveCfg = PluginDev|Any CPU + {4F1F649C-1020-45BE-A487-F416D9297FF3}.PluginDev|Any CPU.Build.0 = PluginDev|Any CPU + {4F1F649C-1020-45BE-A487-F416D9297FF3}.PluginDev|Mixed Platforms.ActiveCfg = PluginDev|Any CPU + {4F1F649C-1020-45BE-A487-F416D9297FF3}.PluginDev|Mixed Platforms.Build.0 = PluginDev|Any CPU + {4F1F649C-1020-45BE-A487-F416D9297FF3}.PluginDev|x86.ActiveCfg = PluginDev|Any CPU + {4F1F649C-1020-45BE-A487-F416D9297FF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F1F649C-1020-45BE-A487-F416D9297FF3}.Release|Any CPU.Build.0 = Release|Any CPU + {4F1F649C-1020-45BE-A487-F416D9297FF3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {4F1F649C-1020-45BE-A487-F416D9297FF3}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {4F1F649C-1020-45BE-A487-F416D9297FF3}.Release|x86.ActiveCfg = Release|Any CPU + {152C761A-DD2E-4C1F-AF89-DFB2547A3BCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {152C761A-DD2E-4C1F-AF89-DFB2547A3BCA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {152C761A-DD2E-4C1F-AF89-DFB2547A3BCA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {152C761A-DD2E-4C1F-AF89-DFB2547A3BCA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {152C761A-DD2E-4C1F-AF89-DFB2547A3BCA}.Debug|x86.ActiveCfg = Debug|Any CPU + {152C761A-DD2E-4C1F-AF89-DFB2547A3BCA}.EFMigrations|Any CPU.ActiveCfg = EFMigrations|Any CPU + {152C761A-DD2E-4C1F-AF89-DFB2547A3BCA}.EFMigrations|Any CPU.Build.0 = EFMigrations|Any CPU + {152C761A-DD2E-4C1F-AF89-DFB2547A3BCA}.EFMigrations|Mixed Platforms.ActiveCfg = EFMigrations|Any CPU + {152C761A-DD2E-4C1F-AF89-DFB2547A3BCA}.EFMigrations|Mixed Platforms.Build.0 = EFMigrations|Any CPU + {152C761A-DD2E-4C1F-AF89-DFB2547A3BCA}.EFMigrations|x86.ActiveCfg = EFMigrations|Any CPU + {152C761A-DD2E-4C1F-AF89-DFB2547A3BCA}.PluginDev|Any CPU.ActiveCfg = PluginDev|Any CPU + {152C761A-DD2E-4C1F-AF89-DFB2547A3BCA}.PluginDev|Any CPU.Build.0 = PluginDev|Any CPU + {152C761A-DD2E-4C1F-AF89-DFB2547A3BCA}.PluginDev|Mixed Platforms.ActiveCfg = PluginDev|Any CPU + {152C761A-DD2E-4C1F-AF89-DFB2547A3BCA}.PluginDev|Mixed Platforms.Build.0 = PluginDev|Any CPU + {152C761A-DD2E-4C1F-AF89-DFB2547A3BCA}.PluginDev|x86.ActiveCfg = PluginDev|Any CPU + {152C761A-DD2E-4C1F-AF89-DFB2547A3BCA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {152C761A-DD2E-4C1F-AF89-DFB2547A3BCA}.Release|Any CPU.Build.0 = Release|Any CPU + {152C761A-DD2E-4C1F-AF89-DFB2547A3BCA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {152C761A-DD2E-4C1F-AF89-DFB2547A3BCA}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {152C761A-DD2E-4C1F-AF89-DFB2547A3BCA}.Release|x86.ActiveCfg = Release|Any CPU + {75FD4163-333C-4DD5-854D-2EF294E45D94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {75FD4163-333C-4DD5-854D-2EF294E45D94}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75FD4163-333C-4DD5-854D-2EF294E45D94}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {75FD4163-333C-4DD5-854D-2EF294E45D94}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {75FD4163-333C-4DD5-854D-2EF294E45D94}.Debug|x86.ActiveCfg = Debug|Any CPU + {75FD4163-333C-4DD5-854D-2EF294E45D94}.EFMigrations|Any CPU.ActiveCfg = EFMigrations|Any CPU + {75FD4163-333C-4DD5-854D-2EF294E45D94}.EFMigrations|Any CPU.Build.0 = EFMigrations|Any CPU + {75FD4163-333C-4DD5-854D-2EF294E45D94}.EFMigrations|Mixed Platforms.ActiveCfg = EFMigrations|Any CPU + {75FD4163-333C-4DD5-854D-2EF294E45D94}.EFMigrations|Mixed Platforms.Build.0 = EFMigrations|Any CPU + {75FD4163-333C-4DD5-854D-2EF294E45D94}.EFMigrations|x86.ActiveCfg = EFMigrations|Any CPU + {75FD4163-333C-4DD5-854D-2EF294E45D94}.PluginDev|Any CPU.ActiveCfg = PluginDev|Any CPU + {75FD4163-333C-4DD5-854D-2EF294E45D94}.PluginDev|Any CPU.Build.0 = PluginDev|Any CPU + {75FD4163-333C-4DD5-854D-2EF294E45D94}.PluginDev|Mixed Platforms.ActiveCfg = PluginDev|Any CPU + {75FD4163-333C-4DD5-854D-2EF294E45D94}.PluginDev|Mixed Platforms.Build.0 = PluginDev|Any CPU + {75FD4163-333C-4DD5-854D-2EF294E45D94}.PluginDev|x86.ActiveCfg = PluginDev|Any CPU + {75FD4163-333C-4DD5-854D-2EF294E45D94}.Release|Any CPU.ActiveCfg = Release|Any CPU + {75FD4163-333C-4DD5-854D-2EF294E45D94}.Release|Any CPU.Build.0 = Release|Any CPU + {75FD4163-333C-4DD5-854D-2EF294E45D94}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {75FD4163-333C-4DD5-854D-2EF294E45D94}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {75FD4163-333C-4DD5-854D-2EF294E45D94}.Release|x86.ActiveCfg = Release|Any CPU + {692E9D31-1393-47BF-B372-63F671052F89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {692E9D31-1393-47BF-B372-63F671052F89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {692E9D31-1393-47BF-B372-63F671052F89}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {692E9D31-1393-47BF-B372-63F671052F89}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {692E9D31-1393-47BF-B372-63F671052F89}.Debug|x86.ActiveCfg = Debug|Any CPU + {692E9D31-1393-47BF-B372-63F671052F89}.EFMigrations|Any CPU.ActiveCfg = EFMigrations|Any CPU + {692E9D31-1393-47BF-B372-63F671052F89}.EFMigrations|Any CPU.Build.0 = EFMigrations|Any CPU + {692E9D31-1393-47BF-B372-63F671052F89}.EFMigrations|Mixed Platforms.ActiveCfg = EFMigrations|Any CPU + {692E9D31-1393-47BF-B372-63F671052F89}.EFMigrations|Mixed Platforms.Build.0 = EFMigrations|Any CPU + {692E9D31-1393-47BF-B372-63F671052F89}.EFMigrations|x86.ActiveCfg = EFMigrations|Any CPU + {692E9D31-1393-47BF-B372-63F671052F89}.PluginDev|Any CPU.ActiveCfg = PluginDev|Any CPU + {692E9D31-1393-47BF-B372-63F671052F89}.PluginDev|Any CPU.Build.0 = PluginDev|Any CPU + {692E9D31-1393-47BF-B372-63F671052F89}.PluginDev|Mixed Platforms.ActiveCfg = PluginDev|Any CPU + {692E9D31-1393-47BF-B372-63F671052F89}.PluginDev|Mixed Platforms.Build.0 = PluginDev|Any CPU + {692E9D31-1393-47BF-B372-63F671052F89}.PluginDev|x86.ActiveCfg = PluginDev|Any CPU + {692E9D31-1393-47BF-B372-63F671052F89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {692E9D31-1393-47BF-B372-63F671052F89}.Release|Any CPU.Build.0 = Release|Any CPU + {692E9D31-1393-47BF-B372-63F671052F89}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {692E9D31-1393-47BF-B372-63F671052F89}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {692E9D31-1393-47BF-B372-63F671052F89}.Release|x86.ActiveCfg = Release|Any CPU + {94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2}.Debug|x86.ActiveCfg = Debug|Any CPU + {94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2}.EFMigrations|Any CPU.ActiveCfg = EFMigrations|Any CPU + {94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2}.EFMigrations|Any CPU.Build.0 = EFMigrations|Any CPU + {94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2}.EFMigrations|Mixed Platforms.ActiveCfg = EFMigrations|Any CPU + {94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2}.EFMigrations|Mixed Platforms.Build.0 = EFMigrations|Any CPU + {94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2}.EFMigrations|x86.ActiveCfg = EFMigrations|Any CPU + {94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2}.PluginDev|Any CPU.ActiveCfg = PluginDev|Any CPU + {94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2}.PluginDev|Any CPU.Build.0 = PluginDev|Any CPU + {94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2}.PluginDev|Mixed Platforms.ActiveCfg = PluginDev|Any CPU + {94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2}.PluginDev|Mixed Platforms.Build.0 = PluginDev|Any CPU + {94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2}.PluginDev|x86.ActiveCfg = PluginDev|Any CPU + {94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2}.Release|Any CPU.Build.0 = Release|Any CPU + {94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2}.Release|x86.ActiveCfg = Release|Any CPU + {ACC1E122-B2C8-4441-BDED-D4A77763331A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ACC1E122-B2C8-4441-BDED-D4A77763331A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ACC1E122-B2C8-4441-BDED-D4A77763331A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {ACC1E122-B2C8-4441-BDED-D4A77763331A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {ACC1E122-B2C8-4441-BDED-D4A77763331A}.Debug|x86.ActiveCfg = Debug|Any CPU + {ACC1E122-B2C8-4441-BDED-D4A77763331A}.EFMigrations|Any CPU.ActiveCfg = EFMigrations|Any CPU + {ACC1E122-B2C8-4441-BDED-D4A77763331A}.EFMigrations|Any CPU.Build.0 = EFMigrations|Any CPU + {ACC1E122-B2C8-4441-BDED-D4A77763331A}.EFMigrations|Mixed Platforms.ActiveCfg = EFMigrations|Any CPU + {ACC1E122-B2C8-4441-BDED-D4A77763331A}.EFMigrations|Mixed Platforms.Build.0 = EFMigrations|Any CPU + {ACC1E122-B2C8-4441-BDED-D4A77763331A}.EFMigrations|x86.ActiveCfg = EFMigrations|Any CPU + {ACC1E122-B2C8-4441-BDED-D4A77763331A}.PluginDev|Any CPU.ActiveCfg = PluginDev|Any CPU + {ACC1E122-B2C8-4441-BDED-D4A77763331A}.PluginDev|Any CPU.Build.0 = PluginDev|Any CPU + {ACC1E122-B2C8-4441-BDED-D4A77763331A}.PluginDev|Mixed Platforms.ActiveCfg = PluginDev|Any CPU + {ACC1E122-B2C8-4441-BDED-D4A77763331A}.PluginDev|Mixed Platforms.Build.0 = PluginDev|Any CPU + {ACC1E122-B2C8-4441-BDED-D4A77763331A}.PluginDev|x86.ActiveCfg = PluginDev|Any CPU + {ACC1E122-B2C8-4441-BDED-D4A77763331A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ACC1E122-B2C8-4441-BDED-D4A77763331A}.Release|Any CPU.Build.0 = Release|Any CPU + {ACC1E122-B2C8-4441-BDED-D4A77763331A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {ACC1E122-B2C8-4441-BDED-D4A77763331A}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {ACC1E122-B2C8-4441-BDED-D4A77763331A}.Release|x86.ActiveCfg = Release|Any CPU + {542B9C12-E2A1-49BB-9134-0E3484F9D669}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {542B9C12-E2A1-49BB-9134-0E3484F9D669}.Debug|Any CPU.Build.0 = Debug|Any CPU + {542B9C12-E2A1-49BB-9134-0E3484F9D669}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {542B9C12-E2A1-49BB-9134-0E3484F9D669}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {542B9C12-E2A1-49BB-9134-0E3484F9D669}.Debug|x86.ActiveCfg = Debug|Any CPU + {542B9C12-E2A1-49BB-9134-0E3484F9D669}.EFMigrations|Any CPU.ActiveCfg = EFMigrations|Any CPU + {542B9C12-E2A1-49BB-9134-0E3484F9D669}.EFMigrations|Any CPU.Build.0 = EFMigrations|Any CPU + {542B9C12-E2A1-49BB-9134-0E3484F9D669}.EFMigrations|Mixed Platforms.ActiveCfg = EFMigrations|Any CPU + {542B9C12-E2A1-49BB-9134-0E3484F9D669}.EFMigrations|Mixed Platforms.Build.0 = EFMigrations|Any CPU + {542B9C12-E2A1-49BB-9134-0E3484F9D669}.EFMigrations|x86.ActiveCfg = EFMigrations|Any CPU + {542B9C12-E2A1-49BB-9134-0E3484F9D669}.PluginDev|Any CPU.ActiveCfg = PluginDev|Any CPU + {542B9C12-E2A1-49BB-9134-0E3484F9D669}.PluginDev|Any CPU.Build.0 = PluginDev|Any CPU + {542B9C12-E2A1-49BB-9134-0E3484F9D669}.PluginDev|Mixed Platforms.ActiveCfg = PluginDev|Any CPU + {542B9C12-E2A1-49BB-9134-0E3484F9D669}.PluginDev|Mixed Platforms.Build.0 = PluginDev|Any CPU + {542B9C12-E2A1-49BB-9134-0E3484F9D669}.PluginDev|x86.ActiveCfg = PluginDev|Any CPU + {542B9C12-E2A1-49BB-9134-0E3484F9D669}.Release|Any CPU.ActiveCfg = Release|Any CPU + {542B9C12-E2A1-49BB-9134-0E3484F9D669}.Release|Any CPU.Build.0 = Release|Any CPU + {542B9C12-E2A1-49BB-9134-0E3484F9D669}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {542B9C12-E2A1-49BB-9134-0E3484F9D669}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {542B9C12-E2A1-49BB-9134-0E3484F9D669}.Release|x86.ActiveCfg = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {692E9D31-1393-47BF-B372-63F671052F89} = {7881B112-7843-4542-B1F7-F99553FB9BB7} + {94D1BEEB-64A3-4EB6-9017-D66AFAF4F2B2} = {7881B112-7843-4542-B1F7-F99553FB9BB7} + {ACC1E122-B2C8-4441-BDED-D4A77763331A} = {7881B112-7843-4542-B1F7-F99553FB9BB7} + {542B9C12-E2A1-49BB-9134-0E3484F9D669} = {7881B112-7843-4542-B1F7-F99553FB9BB7} + EndGlobalSection + GlobalSection(NDepend) = preSolution + Project = ".\SmartStoreNET.ndproj" + EndGlobalSection +EndGlobal diff --git a/src/SmartStoreNET.sln b/src/SmartStoreNET.sln index 15bd2323aa..6eab37e315 100644 --- a/src/SmartStoreNET.sln +++ b/src/SmartStoreNET.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.31101.0 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{A340B280-C12B-47E7-90B7-89C650F8AAFC}" EndProject diff --git a/src/Tests/SmartStore.Core.Tests/App.config b/src/Tests/SmartStore.Core.Tests/App.config index dd4106ff0d..2e47caa6e0 100644 --- a/src/Tests/SmartStore.Core.Tests/App.config +++ b/src/Tests/SmartStore.Core.Tests/App.config @@ -12,7 +12,7 @@ - + diff --git a/src/Tests/SmartStore.Core.Tests/ComponentModel/ExpandoTests.cs b/src/Tests/SmartStore.Core.Tests/ComponentModel/ExpandoTests.cs deleted file mode 100644 index 2e6d0c3436..0000000000 --- a/src/Tests/SmartStore.Core.Tests/ComponentModel/ExpandoTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; -using SmartStore.Core.ComponentModel; -using SmartStore.Tests; -using NUnit.Framework; -using SmartStore.Utilities; - -namespace SmartStore.Core.Tests.ComponentModel -{ - [TestFixture] - public class ExpandoTests - { - [SetUp] - public void SetUp() - { - } - - [Test] - public void Can_get_int_list_type_converter() - { - var converter = CommonHelper.GetTypeConverter(typeof(List)); - converter.GetType().ShouldEqual(typeof(GenericListTypeConverter)); - } - - [Test] - public void Can_get_string_list_type_converter() - { - var converter = CommonHelper.GetTypeConverter(typeof(List)); - converter.GetType().ShouldEqual(typeof(GenericListTypeConverter)); - } - - [Test] - public void Can_get_int_list_from_string() - { - var items = "10,20,30,40,50"; - var converter = CommonHelper.GetTypeConverter(typeof(List)); - var result = converter.ConvertFrom(items) as IList; - result.ShouldNotBeNull(); - result.Count.ShouldEqual(5); - } - - [Test] - public void Can_get_string_list_from_string() - { - var items = "foo, bar, day"; - var converter = CommonHelper.GetTypeConverter(typeof(List)); - var result = converter.ConvertFrom(items) as List; - result.ShouldNotBeNull(); - result.Count.ShouldEqual(3); - } - - [Test] - public void Can_convert_int_list_to_string() - { - var items = new List { 10, 20, 30, 40, 50 }; - var converter = CommonHelper.GetTypeConverter(items.GetType()); - var result = converter.ConvertTo(items, typeof(string)) as string; - - result.ShouldNotBeNull(); - result.ShouldEqual("10,20,30,40,50"); - } - - [Test] - public void Can_convert_string_list_to_string() - { - var items = new List { "foo", "bar", "day" }; - var converter = CommonHelper.GetTypeConverter(items.GetType()); - var result = converter.ConvertTo(items, typeof(string)) as string; - result.ShouldNotBeNull(); - result.ShouldEqual("foo,bar,day"); - } - } -} diff --git a/src/Tests/SmartStore.Core.Tests/ComponentModel/GenericListTypeConverterTests.cs b/src/Tests/SmartStore.Core.Tests/ComponentModel/GenericListTypeConverterTests.cs deleted file mode 100644 index 696471b509..0000000000 --- a/src/Tests/SmartStore.Core.Tests/ComponentModel/GenericListTypeConverterTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; -using SmartStore.Core.ComponentModel; -using SmartStore.Tests; -using NUnit.Framework; -using SmartStore.Utilities; - -namespace SmartStore.Core.Tests.ComponentModel -{ - [TestFixture] - public class GenericListTypeConverter - { - [SetUp] - public void SetUp() - { - } - - [Test] - public void Can_get_int_list_type_converter() - { - var converter = CommonHelper.GetTypeConverter(typeof(List)); - converter.GetType().ShouldEqual(typeof(GenericListTypeConverter)); - } - - [Test] - public void Can_get_string_list_type_converter() - { - var converter = CommonHelper.GetTypeConverter(typeof(List)); - converter.GetType().ShouldEqual(typeof(GenericListTypeConverter)); - } - - [Test] - public void Can_get_int_list_from_string() - { - var items = "10,20,30,40,50"; - var converter = CommonHelper.GetTypeConverter(typeof(List)); - var result = converter.ConvertFrom(items) as IList; - result.ShouldNotBeNull(); - result.Count.ShouldEqual(5); - } - - [Test] - public void Can_get_string_list_from_string() - { - var items = "foo, bar, day"; - var converter = CommonHelper.GetTypeConverter(typeof(List)); - var result = converter.ConvertFrom(items) as List; - result.ShouldNotBeNull(); - result.Count.ShouldEqual(3); - } - - [Test] - public void Can_convert_int_list_to_string() - { - var items = new List { 10, 20, 30, 40, 50 }; - var converter = CommonHelper.GetTypeConverter(items.GetType()); - var result = converter.ConvertTo(items, typeof(string)) as string; - - result.ShouldNotBeNull(); - result.ShouldEqual("10,20,30,40,50"); - } - - [Test] - public void Can_convert_string_list_to_string() - { - var items = new List { "foo", "bar", "day" }; - var converter = CommonHelper.GetTypeConverter(items.GetType()); - var result = converter.ConvertTo(items, typeof(string)) as string; - result.ShouldNotBeNull(); - result.ShouldEqual("foo,bar,day"); - } - } -} diff --git a/src/Tests/SmartStore.Core.Tests/ConversionTests.cs b/src/Tests/SmartStore.Core.Tests/ConversionTests.cs index 8899b041ee..f9a3e61f96 100644 --- a/src/Tests/SmartStore.Core.Tests/ConversionTests.cs +++ b/src/Tests/SmartStore.Core.Tests/ConversionTests.cs @@ -1,37 +1,186 @@ using SmartStore.Tests; using NUnit.Framework; using System.Collections.Generic; +using System.Linq; using System; using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Infrastructure; +using System.Reflection; +using System.Collections.ObjectModel; +using System.Collections.Concurrent; +using System.Globalization; namespace SmartStore.Core.Tests { [TestFixture] public class ConversionTests { - [Test] - public void Can_get_typed_value() - { + public void CanConvertNullables() + { + var r1 = ((double)3).Convert(); + r1.ShouldBe(); + Assert.AreEqual(r1.Value, 3); + + var r2 = ((double?)3).Convert(); + r2.ShouldBe(); + Assert.AreEqual(r2, 3); + + var r3 = (true).Convert(); + r3.ShouldBe(); + Assert.AreEqual(r3.Value, true); + + var r4 = ("1000").Convert(); + r4.ShouldBe(); + Assert.AreEqual(r4.Value, 1000); + + var r5 = ((int?)5).Convert(); + r5.ShouldBe(); + Assert.AreEqual(r5, 5); + + var r6 = ((short)5).Convert(typeof(int)); + r6.ShouldBe(); + Assert.AreEqual(r6, 5); + } + + [Test] + public void CanConvertEnums() + { + var e1 = ("CreateInstance").Convert(); + e1.ShouldBe(); + Assert.AreEqual(e1, BindingFlags.CreateInstance); + + var e2 = ("CreateInstance").Convert(); + e2.ShouldBe(); + Assert.AreEqual(e2.Value, BindingFlags.CreateInstance); + + BindingFlags flags = BindingFlags.CreateInstance | BindingFlags.GetProperty | BindingFlags.IgnoreCase; + + var e3 = (flags).Convert(); + e3.ShouldBe(); + Assert.AreEqual(e3, "IgnoreCase, CreateInstance, GetProperty"); + + var e5 = (e3).Convert(); + e5.ShouldBe(); + Assert.AreEqual(e5.Value, flags); + + var e4 = (flags).Convert(); + e4.ShouldBe(); + Assert.AreEqual(e4, 4609); + + var enu = SmartStore.Core.Domain.Catalog.AttributeControlType.FileUpload; + Assert.AreEqual((int)enu, enu.Convert()); + Assert.AreEqual("FileUpload", enu.Convert()); + } + + [Test] + public void CanConvertBoolean() + { + var b = ("yes").Convert(); + Assert.AreEqual(b, true); + + b = ("off").Convert(); + Assert.AreEqual(b, false); + + b = (1).Convert(); + Assert.AreEqual(b, true); + + b = (0).Convert(); + Assert.AreEqual(b, false); + + var s = (true).Convert(); + Assert.AreEqual(s, "True"); + + var bn = ("true").Convert(); + Assert.AreEqual(bn.Value, true); + + bn = ("wahr").Convert(); + Assert.AreEqual(bn.Value, true); + + bn = ("").Convert(); + Assert.AreEqual(bn.HasValue, false); + } + + [Test] + public void CanConvertNumerics() + { "1000".Convert().ShouldBe(); "1000".Convert().ShouldEqual(1000); - var intList = "1,2,3,4,5".Convert>(); - intList.ShouldBe>(); - Assert.AreEqual(5, intList.Count); - Assert.AreEqual(3, intList[2]); + var i = ((short?)null).Convert(); + Assert.AreEqual(i.HasValue, false); - var strList = "one,two,three".Convert>(); - strList.ShouldBe>(); - Assert.AreEqual(3, strList.Count); - Assert.AreEqual("two", strList[1]); + var sh = ((decimal?)10).Convert(); + sh.ShouldBe(); + Assert.AreEqual(sh, 10); - double dbl = 3; - var r1 = dbl.Convert(); - r1.ShouldBe(); - Assert.AreEqual(r1.Value, 3); + var dec = ((double)10).Convert(); + dec.ShouldBe(); + Assert.AreEqual(dec.Value, 10); + + var dbl = ((decimal)10).Convert(); + dbl.ShouldBe(); + Assert.AreEqual(dbl.Value, 10); + + var f = (20f).Convert(); + f.ShouldBe(); + Assert.AreEqual(f.Value, 20); + + var f2 = ((float?)20f).Convert(); + f2.ShouldBe(); + Assert.AreEqual(f2, 20); + + var culture = CultureInfo.GetCultureInfoByIetfLanguageTag("de-DE"); + ("123567896,54").Convert(culture).ShouldBe(); + } + + [Test] + public void CanConvertDateTime() + { + var dt = ((double)40248.3926).Convert(); + dt.ShouldBe(); + dt.Year.ShouldEqual(2010); + dt.Month.ShouldEqual(3); + dt.Day.ShouldEqual(11); + } + + [Test] + public void CanConvertEnumerables() + { + var list = "1,2,3,4,5".Convert>(); + list.ShouldBe>(); + Assert.AreEqual(5, list.Count); + Assert.AreEqual(3, list[2]); + + var list2 = "1,0,off,wahr,false,y,n".Convert>(); + list2.ShouldBe>(); + Assert.AreEqual(7, list2.Count); + Assert.AreEqual(true, list2.ElementAt(3)); + + "1,2,3,4,5".Convert>().ShouldBe>(); + "1,2,3,4,5".Convert>().ShouldBe>(); + "1,2,3,4,5".Convert>().ShouldBe>(); + "1,2,3,4,5".Convert>().ShouldBe>(); + "1,2,3,4,5".Convert>().ShouldBe>(); + "1,2,3,4,5".Convert>().ShouldBe>(); + "1,2,3,4,5".Convert>().ShouldBe>(); + "1,2,3,4,5".Convert>().ShouldBe>(); + "1,2,3,4,5".Convert>().ShouldBe>(); + + var list3 = new List(new int[] { 1,2,3,4,5 }); + var str = list3.Convert(); + Assert.AreEqual("1,2,3,4,5", str); + + var list4 = ((double)5).Convert>(); + list4.ShouldBe>(); + Assert.AreEqual(1, list4.Count); + Assert.AreEqual(5, list4[0]); + } + + [Test] + public void CanConvertShippingOptions() + { var shippingOption = new ShippingOption { ShippingMethodId = 2, @@ -52,7 +201,7 @@ public void Can_get_typed_value() Assert.AreEqual(shippingOption.ShippingRateComputationMethodSystemName, "SystemName"); var shippingOptions = new List - { + { new ShippingOption { ShippingMethodId = 1, Name = "Name1", Description = "Desc1" }, new ShippingOption { ShippingMethodId = 2, Name = "Name2", Description = "Desc2" } }; @@ -67,11 +216,7 @@ public void Can_get_typed_value() var shippingOptions2 = soStr.Convert>(); Assert.AreEqual(shippingOptions2.Count, 2); Assert.AreEqual(shippingOptions[1].ShippingMethodId, 2); - Assert.AreEqual(shippingOptions2[1].Description, "Desc2"); - - var enu = SmartStore.Core.Domain.Catalog.AttributeControlType.FileUpload; - Assert.AreEqual((int)enu, enu.Convert()); - Assert.AreEqual("FileUpload", enu.Convert()); - } + Assert.AreEqual(shippingOptions2.First().Description, "Desc1"); + } } } diff --git a/src/Tests/SmartStore.Core.Tests/Domain/Shipping/ShippingOptionListTypeConverterTests.cs b/src/Tests/SmartStore.Core.Tests/Domain/Shipping/ShippingOptionListTypeConverterTests.cs deleted file mode 100644 index 653b851dda..0000000000 --- a/src/Tests/SmartStore.Core.Tests/Domain/Shipping/ShippingOptionListTypeConverterTests.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; -using SmartStore.Core.Domain.Shipping; -using SmartStore.Tests; -using NUnit.Framework; -using SmartStore.Utilities; - -namespace SmartStore.Core.Tests.Domain.Shipping -{ - [TestFixture] - public class ShippingOptionListTypeConverterTests - { - [SetUp] - public void SetUp() - { - } - - [Test] - public void Can_get_type_converter() - { - var converter = CommonHelper.GetTypeConverter(typeof(List)); - converter.GetType().ShouldEqual(typeof(ShippingOptionListTypeConverter)); - } - - [Test] - public void Can_convert_shippingOptionList_to_string_and_back() - { - var shippingOptionsInput = new List(); - shippingOptionsInput.Add(new ShippingOption() - { - ShippingMethodId = 1, - Name = "a1", - Description = "a2", - Rate = 3.57M, - ShippingRateComputationMethodSystemName = "a4" - }); - shippingOptionsInput.Add(new ShippingOption() - { - ShippingMethodId = 2, - Name = "b1", - Description = "b2", - Rate = 7.00M, - ShippingRateComputationMethodSystemName = "b4" - }); - - var converter = CommonHelper.GetTypeConverter(shippingOptionsInput.GetType()); - var result = converter.ConvertTo(shippingOptionsInput, typeof(string)) as string; - - var shippingOptionsOutput = converter.ConvertFrom(result) as List; - shippingOptionsOutput.ShouldNotBeNull(); - shippingOptionsOutput.Count.ShouldEqual(2); - shippingOptionsOutput[0].ShippingMethodId.ShouldEqual(1); - shippingOptionsOutput[0].Name.ShouldEqual("a1"); - shippingOptionsOutput[0].Description.ShouldEqual("a2"); - shippingOptionsOutput[0].Rate.ShouldEqual(3.57M); - shippingOptionsOutput[0].ShippingRateComputationMethodSystemName.ShouldEqual("a4"); - - shippingOptionsOutput[1].ShippingMethodId.ShouldEqual(2); - shippingOptionsOutput[1].Name.ShouldEqual("b1"); - shippingOptionsOutput[1].Description.ShouldEqual("b2"); - shippingOptionsOutput[1].Rate.ShouldEqual(7.00M); - shippingOptionsOutput[1].ShippingRateComputationMethodSystemName.ShouldEqual("b4"); - } - } -} diff --git a/src/Tests/SmartStore.Core.Tests/Domain/Shipping/ShippingOptionTypeConverterTests.cs b/src/Tests/SmartStore.Core.Tests/Domain/Shipping/ShippingOptionTypeConverterTests.cs deleted file mode 100644 index 5f40a3b0aa..0000000000 --- a/src/Tests/SmartStore.Core.Tests/Domain/Shipping/ShippingOptionTypeConverterTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.ComponentModel; -using SmartStore.Core.Domain.Shipping; -using SmartStore.Tests; -using NUnit.Framework; -using SmartStore.Utilities; - -namespace SmartStore.Core.Tests.Domain.Shipping -{ - [TestFixture] - public class ShippingOptionTypeConverterTests - { - [SetUp] - public void SetUp() - { - } - - [Test] - public void Can_get_type_converter() - { - var converter = CommonHelper.GetTypeConverter(typeof(ShippingOption)); - converter.GetType().ShouldEqual(typeof(ShippingOptionTypeConverter)); - } - - [Test] - public void Can_convert_shippingOption_to_string_and_back() - { - var shippingOptionInput = new ShippingOption() - { - ShippingMethodId = 1, - Name = "1", - Description = "2", - Rate = 3.57M, - ShippingRateComputationMethodSystemName = "4" - }; - var converter = CommonHelper.GetTypeConverter(shippingOptionInput.GetType()); - var result = converter.ConvertTo(shippingOptionInput, typeof(string)) as string; - - var shippingOptionOutput = converter.ConvertFrom(result) as ShippingOption; - shippingOptionOutput.ShouldNotBeNull(); - shippingOptionInput.ShippingMethodId.ShouldEqual(1); - shippingOptionOutput.Name.ShouldEqual("1"); - shippingOptionOutput.Description.ShouldEqual("2"); - shippingOptionOutput.Rate.ShouldEqual(3.57M); - shippingOptionOutput.ShippingRateComputationMethodSystemName.ShouldEqual("4"); - } - } -} diff --git a/src/Tests/SmartStore.Core.Tests/FastPropertyTests.cs b/src/Tests/SmartStore.Core.Tests/FastPropertyTests.cs new file mode 100644 index 0000000000..458d462a32 --- /dev/null +++ b/src/Tests/SmartStore.Core.Tests/FastPropertyTests.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using SmartStore.ComponentModel; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Tests; + +namespace SmartStore.Core.Tests +{ + [TestFixture] + public class FastPropertyTests + { + [Test] + public void CanCreateFastPropertyByLambda() + { + var fastProp = FastProperty.GetProperty(x => x.Name, PropertyCachingStrategy.Cached); + fastProp.ShouldNotBeNull(); + + Assert.AreEqual(fastProp.Property.Name, "Name"); + + // from cache + var fastProp2 = FastProperty.GetProperty(x => x.Name); + Assert.AreSame(fastProp, fastProp2); + } + + [Test] + public void CanCreateFastPropertyByPropInfo() + { + var pi = typeof(Product).GetProperty("Name"); + + var fastProp = FastProperty.GetProperty(pi, PropertyCachingStrategy.Cached); + fastProp.ShouldNotBeNull(); + + Assert.AreEqual(fastProp.Property.Name, "Name"); + + // from cache + var fastProp2 = FastProperty.GetProperty(x => x.Name); + Assert.AreSame(fastProp, fastProp2); + Assert.AreSame(fastProp.Property, pi); + } + + [Test] + public void CanCreateFastPropertyByName() + { + var fastProp = FastProperty.GetProperty(typeof(Product), "Name", PropertyCachingStrategy.Cached); + fastProp.ShouldNotBeNull(); + + Assert.AreEqual(fastProp.Property.Name, "Name"); + + // from cache + var fastProp2 = FastProperty.GetProperty(x => x.Name); + Assert.AreSame(fastProp, fastProp2); + + var product = new Product { Name = "MyName" }; + var name = fastProp.GetValue(product); + + Assert.AreEqual("MyName", name); + } + } + + public class TestClass + { + public TestClass() + { + } + public TestClass(IEnumerable param1) + { + } + public TestClass(int param1) + { + } + public TestClass(IEnumerable param1, int param2) + { + } + public TestClass(IEnumerable param1, int param2, string param3) + { + } + public TestClass(DateTime param1) + { + } + public TestClass(double param1) + { + } + public TestClass(decimal param1) + { + } + public TestClass(long param1) + { + } + } + +} + + + diff --git a/src/Tests/SmartStore.Core.Tests/FileSystemStorageProviderTests.cs b/src/Tests/SmartStore.Core.Tests/FileSystemStorageProviderTests.cs index edf78fe893..f996e40ead 100644 --- a/src/Tests/SmartStore.Core.Tests/FileSystemStorageProviderTests.cs +++ b/src/Tests/SmartStore.Core.Tests/FileSystemStorageProviderTests.cs @@ -89,9 +89,9 @@ public void AnySlashInGetFileBecomesEnvironmentAppropriate() { [Test] public void ListFoldersReturnsItemsWithShortPathAndEnvironmentSlashes() { - var folders = _storageProvider.ListFolders(@"Subfolder1"); + var folders = _storageProvider.ListFolders(@"Subfolder1").ToArray(); Assert.That(folders, Is.Not.Null); - Assert.That(folders.Count(), Is.EqualTo(1)); + Assert.That(folders.Length, Is.EqualTo(1)); Assert.That(folders.Single().GetName(), Is.EqualTo("SubSubfolder1")); Assert.That(folders.Single().GetPath(), Is.EqualTo(Path.Combine("Subfolder1", "SubSubfolder1"))); } diff --git a/src/Tests/SmartStore.Core.Tests/PerformanceTests.cs b/src/Tests/SmartStore.Core.Tests/PerformanceTests.cs new file mode 100644 index 0000000000..955b0b8a37 --- /dev/null +++ b/src/Tests/SmartStore.Core.Tests/PerformanceTests.cs @@ -0,0 +1,70 @@ +//using System; +//using System.Collections.Generic; +//using NUnit.Framework; +//using SmartStore.Core.Domain.Catalog; +//using SmartStore.Tests; + +//namespace SmartStore.Core.Tests +//{ +// [TestFixture] +// public class PerformanceTests +// { +// [Test] +// public void InstantiatePerfTest() +// { +// int cycles = 500000; + +// Chronometer.Measure(cycles, "Create Product NATIVE", i => new Product()); +// Chronometer.Measure(cycles, "Create Product Reflection", i => Activator.CreateInstance(typeof(Product))); +// Chronometer.Measure(cycles, "Create Product FASTACTIVATOR", i => FastActivator.CreateInstance(typeof(Product))); + +// var list = new List(); + +// Chronometer.Measure(cycles, "Create List NATIVE", i => new TestClass(list)); +// Chronometer.Measure(cycles, "Create List Reflection", i => Activator.CreateInstance(typeof(TestClass), list)); +// Chronometer.Measure(cycles, "Create List FASTACTIVATOR.CreateInstance()", i => FastActivator.CreateInstance(typeof(TestClass), list) ); + +// var ctor = typeof(TestClass).GetConstructor(new Type[] { typeof(List) }); +// //var activator = new FastActivator(ctor); +// var activator = FastActivator.FindMatchingActivator(typeof(TestClass), list); +// Chronometer.Measure(cycles, "Create List FASTACTIVATOR.Activate()", i => activator.Activate(list)); + +// Chronometer.Measure(cycles, "Create List CTOR.Invoke()", i => ctor.Invoke(new object[] { list })); +// } +// } + +// public class TestClass +// { +// public TestClass() +// { +// } +// public TestClass(IEnumerable param1) +// { +// } +// public TestClass(int param1) +// { +// } +// public TestClass(IEnumerable param1, int param2) +// { +// } +// public TestClass(IEnumerable param1, int param2, string param3) +// { +// } +// public TestClass(DateTime param1) +// { +// } +// public TestClass(double param1) +// { +// } +// public TestClass(decimal param1) +// { +// } +// public TestClass(long param1) +// { +// } +// } + +//} + + + diff --git a/src/Tests/SmartStore.Core.Tests/SmartStore.Core.Tests.csproj b/src/Tests/SmartStore.Core.Tests/SmartStore.Core.Tests.csproj index 3c9e2534bb..398459e843 100644 --- a/src/Tests/SmartStore.Core.Tests/SmartStore.Core.Tests.csproj +++ b/src/Tests/SmartStore.Core.Tests/SmartStore.Core.Tests.csproj @@ -62,8 +62,9 @@ MinimumRecommendedRules.ruleset - - ..\..\packages\Autofac.3.4.1\lib\net40\Autofac.dll + + ..\..\packages\Autofac.3.5.2\lib\net40\Autofac.dll + True False @@ -83,15 +84,14 @@ - + + - - @@ -99,7 +99,6 @@ - diff --git a/src/Tests/SmartStore.Core.Tests/WebHelperTests.cs b/src/Tests/SmartStore.Core.Tests/WebHelperTests.cs index 82bfefacd8..a564edd259 100644 --- a/src/Tests/SmartStore.Core.Tests/WebHelperTests.cs +++ b/src/Tests/SmartStore.Core.Tests/WebHelperTests.cs @@ -72,18 +72,18 @@ public void Can_get_storeLocation_in_virtual_directory() serverVariables.Add("HTTP_HOST", "www.example.com"); _httpContext = new FakeHttpContext("~/SmartStoreNETpath", "GET", null, null, null, null, null, serverVariables); _webHelper = new WebHelper(_httpContext); - _webHelper.GetStoreLocation(false).ShouldEqual("http://www.example.com/smartstorenetpath/"); + _webHelper.GetStoreLocation(false).ShouldEqual("http://www.example.com/SmartStoreNETpath/"); } - [Test] - public void Get_storeLocation_should_return_lowerCased_result() - { - var serverVariables = new NameValueCollection(); - serverVariables.Add("HTTP_HOST", "www.Example.com"); - _httpContext = new FakeHttpContext("~/", "GET", null, null, null, null, null, serverVariables); - _webHelper = new WebHelper(_httpContext); - _webHelper.GetStoreLocation(false).ShouldEqual("http://www.example.com/"); - } + //[Test] + //public void Get_storeLocation_should_return_lowerCased_result() + //{ + // var serverVariables = new NameValueCollection(); + // serverVariables.Add("HTTP_HOST", "www.Example.com"); + // _httpContext = new FakeHttpContext("~/", "GET", null, null, null, null, null, serverVariables); + // _webHelper = new WebHelper(_httpContext); + // _webHelper.GetStoreLocation(false).ShouldEqual("http://www.Example.com/"); + //} [Test] public void Can_get_queryString() @@ -114,14 +114,14 @@ public void Can_remove_queryString() .ShouldEqual("http://www.example.com/?param1=value1¶m2=value2"); } - [Test] - public void Can_remove_queryString_should_return_lowerCased_result() - { - _httpContext = new FakeHttpContext("~/", "GET", null, null, null, null, null, null); - _webHelper = new WebHelper(_httpContext); - _webHelper.RemoveQueryString("htTp://www.eXAmple.com/?param1=value1&parAm2=value2", "paRAm1") - .ShouldEqual("http://www.example.com/?param2=value2"); - } + //[Test] + //public void Can_remove_queryString_should_return_lowerCased_result() + //{ + // _httpContext = new FakeHttpContext("~/", "GET", null, null, null, null, null, null); + // _webHelper = new WebHelper(_httpContext); + // _webHelper.RemoveQueryString("htTp://www.eXAmple.com/?param1=value1&parAm2=value2", "paRAm1") + // .ShouldEqual("http://www.example.com/?param2=value2"); + //} [Test] public void Can_remove_queryString_should_ignore_input_parameter_case() @@ -129,7 +129,7 @@ public void Can_remove_queryString_should_ignore_input_parameter_case() _httpContext = new FakeHttpContext("~/", "GET", null, null, null, null, null, null); _webHelper = new WebHelper(_httpContext); _webHelper.RemoveQueryString("http://www.example.com/?param1=value1&parAm2=value2", "paRAm1") - .ShouldEqual("http://www.example.com/?param2=value2"); + .ShouldEqual("http://www.example.com/?parAm2=value2"); } [Test] @@ -154,7 +154,7 @@ public void Can_modify_queryString_with_anchor() _httpContext = new FakeHttpContext("~/", "GET", null, null, null, null, null, null); _webHelper = new WebHelper(_httpContext); _webHelper.ModifyQueryString("http://www.example.com/?param1=value1¶m2=value2", "param1=value3", "Test") - .ShouldEqual("http://www.example.com/?param1=value3¶m2=value2#test"); + .ShouldEqual("http://www.example.com/?param1=value3¶m2=value2#Test"); } [Test] @@ -163,7 +163,7 @@ public void Can_modify_queryString_new_anchor_should_remove_previous_one() _httpContext = new FakeHttpContext("~/", "GET", null, null, null, null, null, null); _webHelper = new WebHelper(_httpContext); _webHelper.ModifyQueryString("http://www.example.com/?param1=value1¶m2=value2#test1", "param1=value3", "Test2") - .ShouldEqual("http://www.example.com/?param1=value3¶m2=value2#test2"); + .ShouldEqual("http://www.example.com/?param1=value3¶m2=value2#Test2"); } } } diff --git a/src/Tests/SmartStore.Core.Tests/packages.config b/src/Tests/SmartStore.Core.Tests/packages.config index a7b933ccff..aef6cdc357 100644 --- a/src/Tests/SmartStore.Core.Tests/packages.config +++ b/src/Tests/SmartStore.Core.Tests/packages.config @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/src/Tests/SmartStore.Data.Tests/App.config b/src/Tests/SmartStore.Data.Tests/App.config index f876dcfe11..e4c1c58af1 100644 --- a/src/Tests/SmartStore.Data.Tests/App.config +++ b/src/Tests/SmartStore.Data.Tests/App.config @@ -29,6 +29,10 @@ + + + + diff --git a/src/Tests/SmartStore.Services.Tests/App.config b/src/Tests/SmartStore.Services.Tests/App.config index 28d743eb22..520c0c7293 100644 --- a/src/Tests/SmartStore.Services.Tests/App.config +++ b/src/Tests/SmartStore.Services.Tests/App.config @@ -2,6 +2,8 @@
    + +
    @@ -11,14 +13,15 @@ - - + + + @@ -29,6 +32,16 @@ + + + + - + + + + + + + \ No newline at end of file diff --git a/src/Tests/SmartStore.Services.Tests/Customers/CustomerRegistrationServiceTests.cs b/src/Tests/SmartStore.Services.Tests/Customers/CustomerRegistrationServiceTests.cs index c589a427c4..81be79200c 100644 --- a/src/Tests/SmartStore.Services.Tests/Customers/CustomerRegistrationServiceTests.cs +++ b/src/Tests/SmartStore.Services.Tests/Customers/CustomerRegistrationServiceTests.cs @@ -1,24 +1,24 @@ using System.Collections.Generic; using System.Linq; +using NUnit.Framework; +using Rhino.Mocks; +using SmartStore.Core; using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Security; using SmartStore.Core.Events; +using SmartStore.Core.Fakes; using SmartStore.Services.Common; using SmartStore.Services.Customers; -using SmartStore.Services.Localization; using SmartStore.Services.Messages; using SmartStore.Services.Security; using SmartStore.Tests; -using NUnit.Framework; -using Rhino.Mocks; -using SmartStore.Core; namespace SmartStore.Services.Tests.Customers { - [TestFixture] + [TestFixture] public class CustomerRegistrationServiceTests : ServiceTest { IRepository _customerRepo; @@ -29,16 +29,16 @@ public class CustomerRegistrationServiceTests : ServiceTest IEncryptionService _encryptionService; ICustomerService _customerService; ICustomerRegistrationService _customerRegistrationService; - ILocalizationService _localizationService; CustomerSettings _customerSettings; INewsLetterSubscriptionService _newsLetterSubscriptionService; IEventPublisher _eventPublisher; RewardPointsSettings _rewardPointsSettings; SecuritySettings _securitySettings; IStoreContext _storeContext; - IWebHelper _webHelper; + ICommonServices _services; + IUserAgent _userAgent; - [SetUp] + [SetUp] public new void SetUp() { _customerSettings = new CustomerSettings(); @@ -109,25 +109,27 @@ public class CustomerRegistrationServiceTests : ServiceTest _eventPublisher = MockRepository.GenerateMock(); _eventPublisher.Expect(x => x.Publish(Arg.Is.Anything)); - _customerRepo.Expect(x => x.Table).Return(new List() { customer1, customer2, customer3, customer4, customer5 }.AsQueryable()); + _customerRepo.Expect(x => x.Table).Return(new List() { customer1, customer2, customer3, customer4, customer5 }.AsQueryable()); _customerRoleRepo = MockRepository.GenerateMock>(); _genericAttributeRepo = MockRepository.GenerateMock>(); _rewardPointsHistoryRepo = MockRepository.GenerateMock>(); + _userAgent = MockRepository.GenerateMock(); ; - _genericAttributeService = MockRepository.GenerateMock(); + _genericAttributeService = MockRepository.GenerateMock(); _newsLetterSubscriptionService = MockRepository.GenerateMock(); - _localizationService = MockRepository.GenerateMock(); _storeContext = MockRepository.GenerateMock(); - _webHelper = MockRepository.GenerateMock(); - _customerService = new CustomerService(new NullCache(), _customerRepo, _customerRoleRepo, - _genericAttributeRepo, _rewardPointsHistoryRepo, _genericAttributeService, _eventPublisher, _rewardPointsSettings, _webHelper); + _services = MockRepository.GenerateMock(); + _services.Expect(x => x.StoreContext).Return(_storeContext); + _services.Expect(x => x.Cache).Return(NullCache.Instance); + + _customerService = new CustomerService(_customerRepo, _customerRoleRepo, + _genericAttributeRepo, _rewardPointsHistoryRepo, _genericAttributeService, _services, NullCache.Instance, _rewardPointsSettings, new FakeHttpContext("~/"), _userAgent); _customerRegistrationService = new CustomerRegistrationService(_customerService, - _encryptionService, _newsLetterSubscriptionService, _localizationService, - _rewardPointsSettings, _customerSettings, _storeContext, _eventPublisher); + _encryptionService, _newsLetterSubscriptionService, _rewardPointsSettings, _customerSettings, _storeContext, _eventPublisher); } //[Test] diff --git a/src/Tests/SmartStore.Services.Tests/DataExchange/CsvWriterTests.cs b/src/Tests/SmartStore.Services.Tests/DataExchange/CsvWriterTests.cs new file mode 100644 index 0000000000..aef4d32beb --- /dev/null +++ b/src/Tests/SmartStore.Services.Tests/DataExchange/CsvWriterTests.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using System.Text; +using NUnit.Framework; +using SmartStore.Services.DataExchange.Csv; + +namespace SmartStore.Services.Tests.DataExchange +{ + [TestFixture] + public class CsvWriterTests + { + [Test] + public void CanWriteValidCsv() + { + var sb = new StringBuilder(); + var cfg = new CsvConfiguration + { + QuoteAllFields = false, + Quote = '"', + Delimiter = ';', + Escape = '"', + TrimValues = true, + SupportsMultiline = false + }; + + using (var writer = new CsvWriter(new StringWriter(sb), cfg)) + { + writer.WriteFields(new string[] + { + "1234", + " abc ", + " def", + "ghi ", + "j;klmn\"o", + "pqrs" + }); + + writer.NextRow(); + } + + string expected = "1234;abc;def;ghi;\"j;klmn\"\"o\";pqrs" + "\r\n"; + + Assert.AreEqual(expected, sb.ToString()); + } + + [Test] + public void CanSpanMultipleLines() + { + var sb = new StringBuilder(); + var cfg = new CsvConfiguration + { + QuoteAllFields = false, + Quote = '"', + Delimiter = ';', + Escape = '"', + TrimValues = true, + SupportsMultiline = true + }; + + using (var writer = new CsvWriter(new StringWriter(sb), cfg)) + { + writer.WriteFields(new string[] + { + "1234", + " abc ", + " def", + "ghi ", + "jk\rl\nmno", + "pqrs" + }); + + writer.NextRow(); + } + + string expected = "1234;abc;def;ghi;\"jk\rl\nmno\";pqrs" + "\r\n"; + + Assert.AreEqual(expected, sb.ToString()); + } + + [Test] + public void CanQuoteAllFields() + { + var sb = new StringBuilder(); + var cfg = new CsvConfiguration + { + QuoteAllFields = true, + Quote = '\'', + Delimiter = ';', + Escape = '"', + TrimValues = true, + SupportsMultiline = true + }; + + using (var writer = new CsvWriter(new StringWriter(sb), cfg)) + { + writer.WriteFields(new string[] + { + "1234", + " abc ", + " def", + "ghi ", + "jk\rl\nmno", + "pqrs" + }); + + writer.NextRow(); + } + + string expected = "'1234';'abc';'def';'ghi';'jk\rl\nmno';'pqrs'" + "\r\n"; + + Assert.AreEqual(expected, sb.ToString()); + } + + [Test] + public void CanEscapeQuotes() + { + var sb = new StringBuilder(); + var cfg = new CsvConfiguration + { + QuoteAllFields = true, + Quote = '\'', + Delimiter = ';', + Escape = '\\', + TrimValues = true, + SupportsMultiline = true + }; + + using (var writer = new CsvWriter(new StringWriter(sb), cfg)) + { + writer.WriteFields(new string[] + { + "1234", + " abc ", + " def", + "ghi ", + "jk'lmo", + "pqrs" + }); + + writer.NextRow(); + } + + string expected = "'1234';'abc';'def';'ghi';'jk\\'lmo';'pqrs'" + "\r\n"; + + Assert.AreEqual(expected, sb.ToString()); + } + } + +} + + + diff --git a/src/Tests/SmartStore.Services.Tests/DataExchange/DataReaderTests.cs b/src/Tests/SmartStore.Services.Tests/DataExchange/DataReaderTests.cs new file mode 100644 index 0000000000..dc20e56e89 --- /dev/null +++ b/src/Tests/SmartStore.Services.Tests/DataExchange/DataReaderTests.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using NUnit.Framework; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Utilities; +using SmartStore.Services.DataExchange.Csv; +using SmartStore.Services.DataExchange.Excel; +using SmartStore.Services.DataExchange.Import; +using SmartStore.Tests; +using SmartStore.Utilities; + +namespace SmartStore.Services.Tests.DataExchange +{ + [TestFixture] + public class DataReaderTests + { + private Stream GetFileStream(string fileName) + { + return typeof(DataReaderTests).Assembly.GetManifestResourceStream("SmartStore.Services.Tests.DataExchange.Files.{0}".FormatInvariant(fileName)); + } + + [Test] + public void CsvReaderThrowsIfNotInitialized() + { + using (var csv = new CsvDataReader(new StreamReader(GetFileStream("testdata.csv")))) + { + ExceptionAssert.Throws(() => { var value = csv["Name"]; }); + } + } + + [Test] + public void ExcelReaderThrowsIfNotInitialized() + { + using (var excel = new ExcelDataReader(GetFileStream("testdata.xlsx"), true)) + { + ExceptionAssert.Throws(() => { var value = excel["Name"]; }); + } + } + + [Test] + public void CanReadExcel() + { + IDataTable table; + + using (var excel = new ExcelDataReader(GetFileStream("testdata.xlsx"), true)) + { + table = LightweightDataTable.FromDataReader(excel); + } + + VerifyDataTable(table); + } + + [Test] + public void CanReadCsv() + { + IDataTable table; + + using (var csv = new CsvDataReader(new StreamReader(GetFileStream("testdata.csv"), Encoding.UTF8))) + { + table = LightweightDataTable.FromDataReader(csv); + } + + VerifyDataTable(table); + } + + private void VerifyDataTable(IDataTable table) + { + Assert.AreEqual(12, table.Columns.Count, "Columns Count"); + Assert.AreEqual(10, table.Rows.Count, "Rows Count"); + + var cols = table.Columns; + + Assert.AreEqual("Id", cols[0].Name); + Assert.AreEqual("Sku", cols[1].Name); + Assert.AreEqual("Name", cols[2].Name); + Assert.AreEqual("Description", cols[3].Name); + Assert.AreEqual("Bool", cols[4].Name); + Assert.AreEqual("Date", cols[5].Name); + Assert.AreEqual("OADate", cols[6].Name); + Assert.AreEqual("UnixDate", cols[7].Name); + Assert.AreEqual("Int", cols[8].Name); + Assert.AreEqual("Double", cols[9].Name); + Assert.AreEqual("Guid", cols[10].Name); + Assert.AreEqual("IntList", cols[11].Name); + + var rows = table.Rows; + + rows[3]["Sku"].ShouldEqual("SKU 4"); + rows[1]["Name"].ShouldEqual("äöü"); + rows[7]["Description"].ShouldEqual("Description 8"); + rows[0]["Bool"].Convert().ShouldBeTrue(); + rows[5]["Bool"].Convert().ShouldBeTrue(); + rows[6]["Bool"].Convert().ShouldBeFalse(); + rows[3]["Double"].Convert().ShouldEqual(9999.765); + rows[0]["OADate"].Convert().ShouldEqual(DateTime.FromOADate(rows[0]["OADate"].Convert())); + rows[3]["Guid"].Convert().ShouldEqual(Guid.Parse("77866957-eec3-4b35-950f-10d1699ac46d")); + + rows[0]["IntList"].Convert>().ShouldSequenceEqual(new List { 1, 2, 3, 4 }); + rows[1]["IntList"].Convert>().ShouldSequenceEqual(new List { 1, 2, 3, 4 }); + rows[5]["IntList"].Convert>().ShouldSequenceEqual(new List { 1, 2, 3, 4 }); + } + } + +} + + + diff --git a/src/Tests/SmartStore.Services.Tests/DataExchange/Files/testdata.csv b/src/Tests/SmartStore.Services.Tests/DataExchange/Files/testdata.csv new file mode 100644 index 0000000000..2c25db4247 --- /dev/null +++ b/src/Tests/SmartStore.Services.Tests/DataExchange/Files/testdata.csv @@ -0,0 +1,11 @@ +Id;Sku;Name;Description;Bool;Date;OADate;UnixDate;Int;Double;Guid;IntList +1;SKU 1;Name 1;;on;10/10/2015;40248.71645;1448028000;5;2.567;8dda6cf7-d851-481e-8945-1a0418f338f2;1,2,3,4 +2;SKU 2;äöü;Description 2;1;01/01/2015;40141.32856;1448028000;10;1.456;5bfdaaf7-e1ce-4eb6-a7e8-11eed69f8630;"1;2;3;4" +3;;;Description 3;true;;40596.95014;1448028000;;;d178cf65-5bfd-4556-b842-2c8a4c1d02fa;1,2,3,4 +4;SKU 4;Name 4;Description 4;yes;;40141.32856;;50;9999.765;77866957-eec3-4b35-950f-10d1699ac46d;1,2,3,4 +5;;;;yes;08/03/2010;40141.32856;1448028000;;;77866957-eec3-4b35-950f-10d1699ac46d;1|2|3|4 +6;SKU 6;Name 6;;WAHR;;40141.32856;;2222;9999.765;;1,2,3,4 +7;SKU 7;;Description 7;off;10/02/2011;40141.32856;;;;9024de6a-fb02-432b-8694-12c892deec0b;1,2,3,4 +8;SKU 8;Name 8;Description 8;0;;40141.32856;1448028000;;9999.765;;1|2|3|4 +9;SKU 9;;;false;;40141,32856;;986746548;;;1,2,3,4 +10;SKU 10;Name 10;;;;40141.32856;1448028000;;;2d168af3-9766-4acd-a3c3-162da81d468d;"1;2;3;4" diff --git a/src/Tests/SmartStore.Services.Tests/DataExchange/Files/testdata.xlsx b/src/Tests/SmartStore.Services.Tests/DataExchange/Files/testdata.xlsx new file mode 100644 index 0000000000..b5a323eb76 Binary files /dev/null and b/src/Tests/SmartStore.Services.Tests/DataExchange/Files/testdata.xlsx differ diff --git a/src/Tests/SmartStore.Services.Tests/DataExchange/ImportDataSegmenterTests.cs b/src/Tests/SmartStore.Services.Tests/DataExchange/ImportDataSegmenterTests.cs new file mode 100644 index 0000000000..aac1e9af4e --- /dev/null +++ b/src/Tests/SmartStore.Services.Tests/DataExchange/ImportDataSegmenterTests.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using NUnit.Framework; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Services.DataExchange.Import; +using SmartStore.Tests; + +namespace SmartStore.Services.Tests.DataExchange +{ + [TestFixture] + public class ImportDataSegmenterTests + { + [Test] + public void CanResolveColumnIndexNames() + { + var columns = new List + { + new LightweightDataColumn("Attr[Color]", typeof(string)), + new LightweightDataColumn("Attr[Size]", typeof(string)), + new LightweightDataColumn("material", typeof(string)), + new LightweightDataColumn("Name[en]", typeof(string)), + new LightweightDataColumn("Name[de]", typeof(string)), + new LightweightDataColumn("Name[fr]", typeof(string)), + new LightweightDataColumn("name_it", typeof(string)), + }; + + var table = new LightweightDataTable(columns, new List()); + var segmenter = new ImportDataSegmenter(table, new ColumnMap()); + segmenter.ColumnMap.AddMapping("Attr[Material]", "material"); + segmenter.ColumnMap.AddMapping("Name[it]", "name_it"); + + var attrs = segmenter.GetColumnIndexes("Attr"); + attrs.ShouldSequenceEqual(new string[] {"Material", "Color", "Size"}); + + var langs = segmenter.GetColumnIndexes("Name"); + langs.ShouldSequenceEqual(new string[] { "it", "en", "de", "fr" }); + } + } +} diff --git a/src/Tests/SmartStore.Services.Tests/DataExchange/SyncMappingServiceTests.cs b/src/Tests/SmartStore.Services.Tests/DataExchange/SyncMappingServiceTests.cs index f0b76f57d3..7989e217a3 100644 --- a/src/Tests/SmartStore.Services.Tests/DataExchange/SyncMappingServiceTests.cs +++ b/src/Tests/SmartStore.Services.Tests/DataExchange/SyncMappingServiceTests.cs @@ -10,6 +10,8 @@ using SmartStore.Core.Domain.Orders; using SmartStore.Services.DataExchange; using SmartStore.Tests; +using Rhino.Mocks; +using SmartStore.Core; namespace SmartStore.Services.Tests.DataExchange { @@ -48,6 +50,13 @@ public class SyncMappingServiceTests : ServiceTest new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, new string[] { "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "e10"} ); + + int i = 1; + foreach (var m in _rs.Table) + { + m.Id = i; + i++; + } } [Test] @@ -114,6 +123,8 @@ public void Can_load_mapping_by_source() [Test] public void Can_delete_mappings() { + _rs.Table.Count().ShouldEqual(40); + _service.DeleteSyncMappings("App1"); _rs.Table.Count().ShouldEqual(20); diff --git a/src/Tests/SmartStore.Services.Tests/Logging/CustomerActivityServiceTests.cs b/src/Tests/SmartStore.Services.Tests/Logging/CustomerActivityServiceTests.cs index 77bdd50b8d..d125fdc122 100644 --- a/src/Tests/SmartStore.Services.Tests/Logging/CustomerActivityServiceTests.cs +++ b/src/Tests/SmartStore.Services.Tests/Logging/CustomerActivityServiceTests.cs @@ -1,29 +1,29 @@ using System.Collections.Generic; using System.Linq; +using NUnit.Framework; +using Rhino.Mocks; using SmartStore.Core; -using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Logging; using SmartStore.Core.Logging; -using SmartStore.Tests; -using NUnit.Framework; -using Rhino.Mocks; using SmartStore.Services.Logging; +using SmartStore.Tests; namespace SmartStore.Services.Tests.Logging { - [TestFixture] + [TestFixture] public class CustomerActivityServiceTests : ServiceTest { - ICacheManager _cacheManager; IRepository _activityLogRepository; IRepository _activityLogTypeRepository; - IWorkContext _workContext; + IRepository _customerRepository; + IWorkContext _workContext; ICustomerActivityService _customerActivityService; ActivityLogType _activityType1, _activityType2; ActivityLog _activity1, _activity2; Customer _customer1, _customer2; + [SetUp] public new void SetUp() { @@ -69,13 +69,15 @@ public class CustomerActivityServiceTests : ServiceTest CustomerId = _customer2.Id, Customer = _customer2 }; - _cacheManager = new NullCache(); + _workContext = MockRepository.GenerateMock(); _activityLogRepository = MockRepository.GenerateMock>(); _activityLogTypeRepository = MockRepository.GenerateMock>(); + _customerRepository = MockRepository.GenerateMock>(); _activityLogTypeRepository.Expect(x => x.Table).Return(new List() { _activityType1, _activityType2 }.AsQueryable()); _activityLogRepository.Expect(x => x.Table).Return(new List() { _activity1, _activity2 }.AsQueryable()); - _customerActivityService = new CustomerActivityService(_cacheManager, _activityLogRepository, _activityLogTypeRepository, _workContext, null, null, null); + + _customerActivityService = new CustomerActivityService(_activityLogRepository, _activityLogTypeRepository, _customerRepository, _workContext, null); } [Test] diff --git a/src/Tests/SmartStore.Services.Tests/Messages/NewsLetterSubscriptionServiceTests.cs b/src/Tests/SmartStore.Services.Tests/Messages/NewsLetterSubscriptionServiceTests.cs index 843e2c60ca..bce6703f67 100644 --- a/src/Tests/SmartStore.Services.Tests/Messages/NewsLetterSubscriptionServiceTests.cs +++ b/src/Tests/SmartStore.Services.Tests/Messages/NewsLetterSubscriptionServiceTests.cs @@ -1,20 +1,18 @@ -using SmartStore.Core.Data; +using NUnit.Framework; +using Rhino.Mocks; +using SmartStore.Core.Data; using SmartStore.Core.Domain.Messages; using SmartStore.Core.Events; using SmartStore.Services.Messages; -using NUnit.Framework; -using Rhino.Mocks; -using SmartStore.Services.Stores; namespace SmartStore.Services.Tests.Messages { - [TestFixture] + [TestFixture] public class NewsLetterSubscriptionServiceTests : ServiceTest { IEventPublisher _eventPublisher; IRepository _subscriptionRepository; IDbContext _dbContext; - IStoreService _storeService; NewsLetterSubscriptionService _newsLetterSubscriptionService; [SetUp] @@ -23,9 +21,8 @@ public class NewsLetterSubscriptionServiceTests : ServiceTest _eventPublisher = MockRepository.GenerateStub(); _subscriptionRepository = MockRepository.GenerateStub>(); _dbContext = MockRepository.GenerateStub(); - _storeService = MockRepository.GenerateStub(); - _newsLetterSubscriptionService = new NewsLetterSubscriptionService(_dbContext, _subscriptionRepository, _eventPublisher, _storeService); + _newsLetterSubscriptionService = new NewsLetterSubscriptionService(_dbContext, _subscriptionRepository, _eventPublisher); } /// diff --git a/src/Tests/SmartStore.Services.Tests/Orders/CheckoutAttributeParserAndFormatterTests.cs b/src/Tests/SmartStore.Services.Tests/Orders/CheckoutAttributeParserAndFormatterTests.cs index 11a8780744..c744c9874a 100644 --- a/src/Tests/SmartStore.Services.Tests/Orders/CheckoutAttributeParserAndFormatterTests.cs +++ b/src/Tests/SmartStore.Services.Tests/Orders/CheckoutAttributeParserAndFormatterTests.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; using System.Linq; +using NUnit.Framework; +using Rhino.Mocks; using SmartStore.Core; using SmartStore.Core.Caching; using SmartStore.Core.Data; @@ -7,24 +9,24 @@ using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Events; using SmartStore.Services.Catalog; using SmartStore.Services.Directory; -using SmartStore.Core.Events; using SmartStore.Services.Media; using SmartStore.Services.Orders; using SmartStore.Services.Tax; using SmartStore.Tests; -using NUnit.Framework; -using Rhino.Mocks; namespace SmartStore.Services.Tests.Orders { - [TestFixture] + [TestFixture] public class CheckoutAttributeParserAndFormatterTests : ServiceTest { IRepository _checkoutAttributeRepo; IRepository _checkoutAttributeValueRepo; - IEventPublisher _eventPublisher; + IRepository _storeMappingRepo; + IEventPublisher _eventPublisher; ICheckoutAttributeService _checkoutAttributeService; ICheckoutAttributeParser _checkoutAttributeParser; @@ -129,6 +131,8 @@ public class CheckoutAttributeParserAndFormatterTests : ServiceTest _checkoutAttributeValueRepo.Expect(x => x.GetById(cav2_1.Id)).Return(cav2_1); _checkoutAttributeValueRepo.Expect(x => x.GetById(cav2_2.Id)).Return(cav2_2); + _storeMappingRepo = MockRepository.GenerateMock>(); + var cacheManager = new NullCache(); _eventPublisher = MockRepository.GenerateMock(); @@ -137,6 +141,7 @@ public class CheckoutAttributeParserAndFormatterTests : ServiceTest _checkoutAttributeService = new CheckoutAttributeService(cacheManager, _checkoutAttributeRepo, _checkoutAttributeValueRepo, + _storeMappingRepo, _eventPublisher); _checkoutAttributeParser = new CheckoutAttributeParser(_checkoutAttributeService); diff --git a/src/Tests/SmartStore.Services.Tests/Orders/OrderProcessingServiceTests.cs b/src/Tests/SmartStore.Services.Tests/Orders/OrderProcessingServiceTests.cs index afab03b6fc..f52dc057b4 100644 --- a/src/Tests/SmartStore.Services.Tests/Orders/OrderProcessingServiceTests.cs +++ b/src/Tests/SmartStore.Services.Tests/Orders/OrderProcessingServiceTests.cs @@ -4,7 +4,6 @@ using NUnit.Framework; using Rhino.Mocks; using SmartStore.Core; -using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Common; @@ -39,7 +38,7 @@ namespace SmartStore.Services.Tests.Orders { - [TestFixture] + [TestFixture] public class OrderProcessingServiceTests : ServiceTest { IWorkContext _workContext; @@ -83,12 +82,13 @@ public class OrderProcessingServiceTests : ServiceTest LocalizationSettings _localizationSettings; ShoppingCartSettings _shoppingCartSettings; CatalogSettings _catalogSettings; - IOrderProcessingService _orderProcessingService; + IOrderProcessingService _orderProcessingService; IEventPublisher _eventPublisher; CurrencySettings _currencySettings; IAffiliateService _affiliateService; ISettingService _settingService; IDownloadService _downloadService; + INewsLetterSubscriptionService _newsLetterSubscriptionService; ICommonServices _services; HttpRequestBase _httpRequestBase; IGeoCountryLookup _geoCountryLookup; @@ -105,7 +105,6 @@ public class OrderProcessingServiceTests : ServiceTest _storeContext.Expect(x => x.CurrentStore).Return(_store); var pluginFinder = new PluginFinder(); - var cacheManager = new NullCache(); _shoppingCartSettings = new ShoppingCartSettings(); _catalogSettings = new CatalogSettings(); @@ -130,16 +129,16 @@ public class OrderProcessingServiceTests : ServiceTest _shippingMethodRepository = MockRepository.GenerateMock>(); _logger = new NullLogger(); - _shippingService = new ShippingService(cacheManager, + _shippingService = new ShippingService( _shippingMethodRepository, _logger, _productAttributeParser, _productService, _checkoutAttributeParser, _genericAttributeService, - _localizationService, - _shippingSettings, pluginFinder, - _eventPublisher, _shoppingCartSettings, + _shippingSettings, + _eventPublisher, + _shoppingCartSettings, _settingService, this.ProviderManager, _typeFinder); @@ -190,8 +189,9 @@ public class OrderProcessingServiceTests : ServiceTest _customerActivityService = MockRepository.GenerateMock(); _currencyService = MockRepository.GenerateMock(); _affiliateService = MockRepository.GenerateMock(); + _newsLetterSubscriptionService = MockRepository.GenerateMock(); - _paymentSettings = new PaymentSettings() + _paymentSettings = new PaymentSettings() { ActivePaymentMethodSystemNames = new List() { @@ -218,6 +218,7 @@ public class OrderProcessingServiceTests : ServiceTest _encryptionService, _workContext, _storeContext, _workflowMessageService, _customerActivityService, _currencyService, _affiliateService, _eventPublisher, _genericAttributeService, + _newsLetterSubscriptionService, _paymentSettings, _rewardPointsSettings, _orderSettings, _taxSettings, _localizationSettings, _currencySettings, _shoppingCartSettings); diff --git a/src/Tests/SmartStore.Services.Tests/Orders/OrderTotalCalculationServiceTests.cs b/src/Tests/SmartStore.Services.Tests/Orders/OrderTotalCalculationServiceTests.cs index 77eb79f431..1f5e26956f 100644 --- a/src/Tests/SmartStore.Services.Tests/Orders/OrderTotalCalculationServiceTests.cs +++ b/src/Tests/SmartStore.Services.Tests/Orders/OrderTotalCalculationServiceTests.cs @@ -4,7 +4,6 @@ using NUnit.Framework; using Rhino.Mocks; using SmartStore.Core; -using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Common; @@ -23,17 +22,15 @@ using SmartStore.Services.Configuration; using SmartStore.Services.Directory; using SmartStore.Services.Discounts; -using SmartStore.Services.Localization; using SmartStore.Services.Media; using SmartStore.Services.Orders; -using SmartStore.Services.Payments; using SmartStore.Services.Shipping; using SmartStore.Services.Tax; using SmartStore.Tests; namespace SmartStore.Services.Tests.Orders { - [TestFixture] + [TestFixture] public class OrderTotalCalculationServiceTests : ServiceTest { IWorkContext _workContext; @@ -55,7 +52,6 @@ public class OrderTotalCalculationServiceTests : ServiceTest IOrderTotalCalculationService _orderTotalCalcService; IAddressService _addressService; ShippingSettings _shippingSettings; - ILocalizationService _localizationService; ILogger _logger; IRepository _shippingMethodRepository; ShoppingCartSettings _shoppingCartSettings; @@ -79,7 +75,6 @@ public class OrderTotalCalculationServiceTests : ServiceTest _storeContext.Expect(x => x.CurrentStore).Return(_store); var pluginFinder = new PluginFinder(); - var cacheManager = new NullCache(); _shoppingCartSettings = new ShoppingCartSettings(); _catalogSettings = new CatalogSettings(); @@ -94,7 +89,6 @@ public class OrderTotalCalculationServiceTests : ServiceTest _eventPublisher = MockRepository.GenerateMock(); _eventPublisher.Expect(x => x.Publish(Arg.Is.Anything)); - _localizationService = MockRepository.GenerateMock(); _settingService = MockRepository.GenerateMock(); _typeFinder = MockRepository.GenerateMock(); @@ -105,16 +99,16 @@ public class OrderTotalCalculationServiceTests : ServiceTest _shippingMethodRepository = MockRepository.GenerateMock>(); _logger = new NullLogger(); - _shippingService = new ShippingService(cacheManager, + _shippingService = new ShippingService( _shippingMethodRepository, _logger, _productAttributeParser, _productService, _checkoutAttributeParser, _genericAttributeService, - _localizationService, - _shippingSettings, pluginFinder, - _eventPublisher, _shoppingCartSettings, + _shippingSettings, + _eventPublisher, + _shoppingCartSettings, _settingService, this.ProviderManager, _typeFinder); diff --git a/src/Tests/SmartStore.Services.Tests/Payments/PaymentServiceTests.cs b/src/Tests/SmartStore.Services.Tests/Payments/PaymentServiceTests.cs index b7148173fb..843a0b0deb 100644 --- a/src/Tests/SmartStore.Services.Tests/Payments/PaymentServiceTests.cs +++ b/src/Tests/SmartStore.Services.Tests/Payments/PaymentServiceTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using Rhino.Mocks; @@ -6,25 +7,20 @@ using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; using SmartStore.Core.Infrastructure; -using SmartStore.Core.Plugins; -using SmartStore.Services.Directory; using SmartStore.Services.Localization; -using SmartStore.Services.Orders; using SmartStore.Services.Payments; using SmartStore.Tests; namespace SmartStore.Services.Tests.Payments { - [TestFixture] + [TestFixture] public class PaymentServiceTests : ServiceTest { IRepository _paymentMethodRepository; PaymentSettings _paymentSettings; ShoppingCartSettings _shoppingCartSettings; IPaymentService _paymentService; - ICurrencyService _currencyService; ICommonServices _services; - IOrderTotalCalculationService _orderTotalCalculationService; ITypeFinder _typeFinder; [SetUp] @@ -34,20 +30,18 @@ public class PaymentServiceTests : ServiceTest _paymentSettings.ActivePaymentMethodSystemNames = new List(); _paymentSettings.ActivePaymentMethodSystemNames.Add("Payments.TestMethod"); - var pluginFinder = new PluginFinder(); - _shoppingCartSettings = new ShoppingCartSettings(); _paymentMethodRepository = MockRepository.GenerateMock>(); - _currencyService = MockRepository.GenerateMock(); _services = MockRepository.GenerateMock(); - _orderTotalCalculationService = MockRepository.GenerateMock(); + _typeFinder = MockRepository.GenerateMock(); + _typeFinder.Expect(x => x.FindClassesOfType((Type)null, null, true)).IgnoreArguments().Return(Enumerable.Empty()).Repeat.Any(); var localizationService = MockRepository.GenerateMock(); localizationService.Expect(ls => ls.GetResource(null)).IgnoreArguments().Return("NotSupported").Repeat.Any(); - _paymentService = new PaymentService(_paymentMethodRepository, _paymentSettings, pluginFinder, _shoppingCartSettings, - this.ProviderManager, _currencyService, _services, _orderTotalCalculationService, _typeFinder); + _paymentService = new PaymentService(_paymentMethodRepository, _paymentSettings, _shoppingCartSettings, + this.ProviderManager, _services, _typeFinder); } [Test] diff --git a/src/Tests/SmartStore.Services.Tests/Seo/SeoExtensionsTests.cs b/src/Tests/SmartStore.Services.Tests/Seo/SeoExtensionsTests.cs index 08d600fd58..3d57edf05a 100644 --- a/src/Tests/SmartStore.Services.Tests/Seo/SeoExtensionsTests.cs +++ b/src/Tests/SmartStore.Services.Tests/Seo/SeoExtensionsTests.cs @@ -1,11 +1,11 @@ -using SmartStore.Services.Seo; -using SmartStore.Tests; +using System; using NUnit.Framework; +using SmartStore.Tests; using SmartStore.Utilities; namespace SmartStore.Services.Tests.Seo { - [TestFixture] + [TestFixture] public class SeoExtensionsTests { [Test] @@ -38,17 +38,23 @@ public void Can_convert_non_western_chars() { //german letters with diacritics SeoHelper.GetSeName("testäöü", true, false).ShouldEqual("testaou"); - SeoHelper.GetSeName("testäöü", false, false).ShouldEqual("testaeoeue"); - } + SeoHelper.GetSeName("testäöü", false, false).ShouldEqual("test"); + + var charConversions = string.Join(Environment.NewLine, new string[] { "ä;ae", "ö;oe", "ü;ue" }); + + SeoHelper.GetSeName("testäöü", false, false, charConversions).ShouldEqual("testaeoeue"); + + SeoHelper.ResetUserSeoCharacterTable(); + } - //[Test] - //public void Can_allow_unicode_chars() - //{ - // //russian letters - //SeoHelper.GetSeName("testтест", false, true).ShouldEqual("testтест"); - //SeoHelper.GetSeName("testтест", false, false).ShouldEqual("test"); - //} - } + [Test] + public void Can_allow_unicode_chars() + { + //russian letters + SeoHelper.GetSeName("testтест", false, true).ShouldEqual("testтест"); + SeoHelper.GetSeName("testтест", false, false).ShouldEqual("test"); + } + } } diff --git a/src/Tests/SmartStore.Services.Tests/Shipping/ShippingServiceTests.cs b/src/Tests/SmartStore.Services.Tests/Shipping/ShippingServiceTests.cs index 6f817238ec..96d8595160 100644 --- a/src/Tests/SmartStore.Services.Tests/Shipping/ShippingServiceTests.cs +++ b/src/Tests/SmartStore.Services.Tests/Shipping/ShippingServiceTests.cs @@ -1,28 +1,24 @@ using System.Collections.Generic; using System.Linq; -using SmartStore.Core.Caching; +using NUnit.Framework; +using Rhino.Mocks; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Shipping; -using SmartStore.Core.Plugins; -using SmartStore.Services.Catalog; using SmartStore.Core.Events; -using SmartStore.Services.Localization; +using SmartStore.Core.Infrastructure; using SmartStore.Core.Logging; -using SmartStore.Services.Logging; +using SmartStore.Services.Catalog; +using SmartStore.Services.Common; +using SmartStore.Services.Configuration; using SmartStore.Services.Orders; using SmartStore.Services.Shipping; using SmartStore.Tests; -using NUnit.Framework; -using Rhino.Mocks; -using SmartStore.Services.Common; -using SmartStore.Services.Configuration; -using SmartStore.Core.Infrastructure; namespace SmartStore.Services.Tests.Shipping { - [TestFixture] + [TestFixture] public class ShippingServiceTests : ServiceTest { IRepository _shippingMethodRepository; @@ -32,7 +28,6 @@ public class ShippingServiceTests : ServiceTest ICheckoutAttributeParser _checkoutAttributeParser; ShippingSettings _shippingSettings; IEventPublisher _eventPublisher; - ILocalizationService _localizationService; IGenericAttributeService _genericAttributeService; IShippingService _shippingService; ShoppingCartSettings _shoppingCartSettings; @@ -52,28 +47,23 @@ public class ShippingServiceTests : ServiceTest _productService = MockRepository.GenerateMock(); _checkoutAttributeParser = MockRepository.GenerateMock(); - var cacheManager = new NullCache(); - - var pluginFinder = new PluginFinder(); - _eventPublisher = MockRepository.GenerateMock(); _eventPublisher.Expect(x => x.Publish(Arg.Is.Anything)); - _localizationService = MockRepository.GenerateMock(); _genericAttributeService = MockRepository.GenerateMock(); _settingService = MockRepository.GenerateMock(); _typeFinder = MockRepository.GenerateMock(); _shoppingCartSettings = new ShoppingCartSettings(); - _shippingService = new ShippingService(cacheManager, + _shippingService = new ShippingService( _shippingMethodRepository, _logger, _productAttributeParser, _productService, _checkoutAttributeParser, _genericAttributeService, - _localizationService, - _shippingSettings, pluginFinder, _eventPublisher, + _shippingSettings, + _eventPublisher, _shoppingCartSettings, _settingService, this.ProviderManager, diff --git a/src/Tests/SmartStore.Services.Tests/SmartStore.Services.Tests.csproj b/src/Tests/SmartStore.Services.Tests/SmartStore.Services.Tests.csproj index b57ac1295b..a507b19eb5 100644 --- a/src/Tests/SmartStore.Services.Tests/SmartStore.Services.Tests.csproj +++ b/src/Tests/SmartStore.Services.Tests/SmartStore.Services.Tests.csproj @@ -62,6 +62,14 @@ MinimumRecommendedRules.ruleset + + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll + True + + + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll + True + True ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll @@ -75,6 +83,7 @@ + @@ -95,6 +104,9 @@ + + + @@ -146,6 +158,8 @@ + + diff --git a/src/Tests/SmartStore.Services.Tests/packages.config b/src/Tests/SmartStore.Services.Tests/packages.config index d7230e8b4a..92d78f69c0 100644 --- a/src/Tests/SmartStore.Services.Tests/packages.config +++ b/src/Tests/SmartStore.Services.Tests/packages.config @@ -1,5 +1,6 @@  + diff --git a/src/Tests/SmartStore.Tests/App.config b/src/Tests/SmartStore.Tests/App.config index d11112e2c5..14da58aef9 100644 --- a/src/Tests/SmartStore.Tests/App.config +++ b/src/Tests/SmartStore.Tests/App.config @@ -9,6 +9,10 @@ + + + + \ No newline at end of file diff --git a/src/Tests/SmartStore.Tests/Chronometer.cs b/src/Tests/SmartStore.Tests/Chronometer.cs new file mode 100644 index 0000000000..3484cffc27 --- /dev/null +++ b/src/Tests/SmartStore.Tests/Chronometer.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Diagnostics; +using System.Threading; + +namespace SmartStore.Tests +{ + public static class Chronometer + { + public static void Measure(int cycles, string text, Action action) + { + var watch = new Stopwatch(); + watch.Start(); + for (int i = 0; i < cycles; i++) + { + action(i); + } + watch.Stop(); + + Console.WriteLine("{0}: {1} ms.".FormatCurrent(text, watch.ElapsedMilliseconds.ToString())); + } + } +} diff --git a/src/Tests/SmartStore.Tests/MemoryRepository.cs b/src/Tests/SmartStore.Tests/MemoryRepository.cs index 449867f374..8b92a16b26 100644 --- a/src/Tests/SmartStore.Tests/MemoryRepository.cs +++ b/src/Tests/SmartStore.Tests/MemoryRepository.cs @@ -113,6 +113,8 @@ public IDbContext Context ctx.Stub(x => x.ExecuteStoredProcedureList(Arg.Is.Anything, Arg.Is.Anything)).Return(new List()); ctx.Stub(x => x.GetModifiedProperties(Arg.Is.Anything)).Return(new Dictionary()); ctx.Stub(x => x.BeginTransaction(Arg.Is.Anything)).Return(MockRepository.GenerateMock()); + ctx.Stub(x => x.ChangeState(Arg.Is.TypeOf, Arg.Is.Anything)) + .WhenCalled(ChangeState); _dbContext = ctx; } @@ -121,6 +123,27 @@ public IDbContext Context } } + private void ChangeState(MethodInvocation invocation) + { + var entity = (T)invocation.Arguments[0]; + var state = (System.Data.Entity.EntityState)invocation.Arguments[1]; + + if (state == System.Data.Entity.EntityState.Deleted) + { + if (_dbSet.Contains(entity)) + { + _dbSet.Remove(entity); + } + } + else if (state == System.Data.Entity.EntityState.Added) + { + if (!_dbSet.Contains(entity)) + { + _dbSet.Add(entity); + } + } + } + public bool? AutoCommitEnabled { get; set; } } } diff --git a/src/Tests/SmartStore.Tests/SmartStore.Tests.csproj b/src/Tests/SmartStore.Tests/SmartStore.Tests.csproj index a3613f888f..83b35356e7 100644 --- a/src/Tests/SmartStore.Tests/SmartStore.Tests.csproj +++ b/src/Tests/SmartStore.Tests/SmartStore.Tests.csproj @@ -86,6 +86,7 @@ + diff --git a/src/Tests/SmartStore.Tests/TestDbSet.cs b/src/Tests/SmartStore.Tests/TestDbSet.cs index 194a1c09a1..8284461b5e 100644 --- a/src/Tests/SmartStore.Tests/TestDbSet.cs +++ b/src/Tests/SmartStore.Tests/TestDbSet.cs @@ -16,12 +16,12 @@ namespace SmartStore.Tests public class TestDbSet : DbSet, IQueryable, IEnumerable, IDbAsyncEnumerable where TEntity : BaseEntity, new() { - ObservableCollection _data; + HashSet _data; IQueryable _query; public TestDbSet() { - _data = new ObservableCollection(); + _data = new HashSet(); _query = _data.AsQueryable(); } @@ -65,7 +65,7 @@ public override Task FindAsync(CancellationToken cancellationToken, par public override TEntity Create() { - return Activator.CreateInstance(); + return new TEntity(); } public override TDerivedEntity Create() @@ -75,7 +75,7 @@ public override TDerivedEntity Create() public override ObservableCollection Local { - get { return _data; } + get { return new ObservableCollection(_data); } } Type IQueryable.ElementType diff --git a/src/Tests/SmartStore.Tests/TestExtensions.cs b/src/Tests/SmartStore.Tests/TestExtensions.cs index 11be85f1b2..2ec1e3de6e 100644 --- a/src/Tests/SmartStore.Tests/TestExtensions.cs +++ b/src/Tests/SmartStore.Tests/TestExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using NUnit.Framework; namespace SmartStore.Tests @@ -35,14 +37,20 @@ public static T ShouldEqual(this T actual, object expected) return actual; } - /// - /// Asserts that two objects are equal. - /// - /// - /// - /// - /// - public static void ShouldEqual(this object actual, object expected, string message) + public static IEnumerable ShouldSequenceEqual(this IEnumerable actual, IEnumerable expected) + { + Assert.IsTrue(actual.SequenceEqual(expected)); + return actual; + } + + /// + /// Asserts that two objects are equal. + /// + /// + /// + /// + /// + public static void ShouldEqual(this object actual, object expected, string message) { Assert.AreEqual(expected, actual); } diff --git a/src/Tests/SmartStore.Web.MVC.Tests/App.config b/src/Tests/SmartStore.Web.MVC.Tests/App.config index 10b4b64ed8..c359ffa2b2 100644 --- a/src/Tests/SmartStore.Web.MVC.Tests/App.config +++ b/src/Tests/SmartStore.Web.MVC.Tests/App.config @@ -15,7 +15,7 @@ - + @@ -23,7 +23,7 @@ - + @@ -43,7 +43,7 @@ - + @@ -69,6 +69,14 @@ + + + + + + + + diff --git a/src/Tests/SmartStore.Web.MVC.Tests/Framework/Controllers/AdminAuthorizeAttributeTests.cs b/src/Tests/SmartStore.Web.MVC.Tests/Framework/Controllers/AdminAuthorizeAttributeTests.cs index 3976d88649..07999fcd7f 100644 --- a/src/Tests/SmartStore.Web.MVC.Tests/Framework/Controllers/AdminAuthorizeAttributeTests.cs +++ b/src/Tests/SmartStore.Web.MVC.Tests/Framework/Controllers/AdminAuthorizeAttributeTests.cs @@ -4,6 +4,7 @@ using SmartStore.Web.Framework.Controllers; using NUnit.Framework; using Rhino.Mocks; +using SmartStore.Web.Framework.Security; namespace SmartStore.Web.MVC.Tests.Framework.Controllers { diff --git a/src/Tests/SmartStore.Web.MVC.Tests/Framework/Controllers/FormValueRequiredTests.cs b/src/Tests/SmartStore.Web.MVC.Tests/Framework/Controllers/FormValueRequiredTests.cs index 23618f2705..02b6e8e31a 100644 --- a/src/Tests/SmartStore.Web.MVC.Tests/Framework/Controllers/FormValueRequiredTests.cs +++ b/src/Tests/SmartStore.Web.MVC.Tests/Framework/Controllers/FormValueRequiredTests.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using NUnit.Framework; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Filters; namespace SmartStore.Web.MVC.Tests.Framework.Controllers { diff --git a/src/Tests/SmartStore.Web.MVC.Tests/Public/Infrastructure/RoutesTests.cs b/src/Tests/SmartStore.Web.MVC.Tests/Public/Infrastructure/RoutesTests.cs index b275d3afec..380578d32f 100644 --- a/src/Tests/SmartStore.Web.MVC.Tests/Public/Infrastructure/RoutesTests.cs +++ b/src/Tests/SmartStore.Web.MVC.Tests/Public/Infrastructure/RoutesTests.cs @@ -96,7 +96,7 @@ public void Customer_routes() "~/customer/myaccount/".ShouldMapTo(c => c.MyAccount()); "~/customer/info/".ShouldMapTo(c => c.Info()); "~/customer/addresses/".ShouldMapTo(c => c.Addresses()); - "~/customer/orders/".ShouldMapTo(c => c.Orders()); + "~/customer/orders/".ShouldMapTo(c => c.Orders(null)); "~/customer/returnrequests/".ShouldMapTo(c => c.ReturnRequests()); "~/customer/downloadableproducts/".ShouldMapTo(c => c.DownloadableProducts()); "~/customer/backinstocksubscriptions/".ShouldMapTo(c => c.BackInStockSubscriptions(null)); diff --git a/src/Tests/SmartStore.Web.MVC.Tests/Public/Validators/Customer/RegisterValidatorTests.cs b/src/Tests/SmartStore.Web.MVC.Tests/Public/Validators/Customer/RegisterValidatorTests.cs index 81cac040bd..2db579d23c 100644 --- a/src/Tests/SmartStore.Web.MVC.Tests/Public/Validators/Customer/RegisterValidatorTests.cs +++ b/src/Tests/SmartStore.Web.MVC.Tests/Public/Validators/Customer/RegisterValidatorTests.cs @@ -3,6 +3,7 @@ using SmartStore.Web.Models.Customer; using SmartStore.Web.Validators.Customer; using NUnit.Framework; +using SmartStore.Core.Domain.Tax; namespace SmartStore.Web.MVC.Tests.Public.Validators.Customer { @@ -11,12 +12,14 @@ public class RegisterValidatorTests : BaseValidatorTests { private RegisterValidator _validator; private CustomerSettings _customerSettings; + private TaxSettings _taxSettings = new TaxSettings(); [SetUp] public new void Setup() { _customerSettings = new CustomerSettings(); - _validator = new RegisterValidator(_localizationService, _customerSettings); + _taxSettings = new TaxSettings(); + _validator = new RegisterValidator(_localizationService, _customerSettings, _taxSettings); } [Test] @@ -147,7 +150,7 @@ public void Should_not_have_error_when_password_equals_confirmationPassword() public void Should_validate_password_is_length() { _customerSettings.PasswordMinLength = 5; - _validator = new RegisterValidator(_localizationService, _customerSettings); + _validator = new RegisterValidator(_localizationService, _customerSettings, _taxSettings); var model = new RegisterModel(); model.Password = "1234"; diff --git a/src/Tests/SmartStore.Web.MVC.Tests/SmartStore.Web.MVC.Tests.csproj b/src/Tests/SmartStore.Web.MVC.Tests/SmartStore.Web.MVC.Tests.csproj index 802605a430..14ea432893 100644 --- a/src/Tests/SmartStore.Web.MVC.Tests/SmartStore.Web.MVC.Tests.csproj +++ b/src/Tests/SmartStore.Web.MVC.Tests/SmartStore.Web.MVC.Tests.csproj @@ -62,17 +62,13 @@ MinimumRecommendedRules.ruleset - - False - ..\..\packages\AutoMapper.3.2.1\lib\net40\AutoMapper.dll - - - False - ..\..\packages\AutoMapper.3.2.1\lib\net40\AutoMapper.Net4.dll + + ..\..\packages\AutoMapper.4.1.1\lib\net45\AutoMapper.dll + True - - False - ..\..\packages\FluentValidation.5.0.0.1\lib\Net40\FluentValidation.dll + + ..\..\packages\FluentValidation.5.6.2.0\lib\Net45\FluentValidation.dll + True True @@ -190,7 +186,6 @@ -