diff --git a/.gitignore b/.gitignore index c95b6dbc19..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 @@ -207,6 +303,7 @@ Kopie von* # Import/Export /src/Presentation/SmartStore.Web/Content/files/[Ee]xport[Ii]mport/*.* +/src/Presentation/SmartStore.Web/[Ee]xchange/* # Media /src/Presentation/SmartStore.Web/Media/[Tt]humbs diff --git a/README.md b/README.md index 39784f16b0..a7a83aee3a 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,10 @@ 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) ## Highlights @@ -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 980f2060e3..f184eaa7eb 100644 --- a/SmartStoreNET.Tasks.Targets +++ b/SmartStoreNET.Tasks.Targets @@ -2,7 +2,7 @@ - + @@ -44,7 +44,7 @@ x86 $(BUILD_NUMBER) - 2.2.1 + 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 2d8240fc1c..0dac88a9fd 100644 --- a/changelog.md +++ b/changelog.md @@ -1,27 +1,247 @@ -# Release Notes +# 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 +* 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 +* #747 Restrict payment methods by old versus new customer (plugin) +* #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) #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 +* New scheduled task: Clear uploadeded transient media files +* #704 Make primary store currency suitable for multi-stores +* #727 Web-API: Option to deactivate TimestampOlderThanLastRequest validation +* #731 Web-API: Allow deletion and inserting of product category and manufacturer assignments +* #733 Option to set a display order for homepage products +* #607 HTML capable full description for payment methods displayed in checkout +* #732 Product list: Option to display the pre-selected price instead of the lowest price +* New payment provider for Offline Payment Plugin: Purchase Order Number +* #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%. +* (Perf) Significantly reduced number of database reads during product list rendering. Increases request speed by up to 10%. +* (Perf) Implemented 2nd level cache for infrequently changed entities. Increases request speed by up to 10%. +* TaskScheduler: Rewritten from scratch to be suitable for Web Farms +* TaskScheduler: Supports cron expressions to define task execution frequency +* TaskScheduler: Editing tasks does not require app restart anymore +* TaskScheduler: Enhanced UI +* #721 Message Queue: implemented "Delete all" +* #725 Prevent LowestProductPrice being 0 +* #709 News feed produced invalid RSS feed. Added content:encoded. Added maximum news age setting for feed export. +* #735 Include SKUs of attribute combinations when filtering the backend product list +* Filter for homepage and published\unpublished products in backend product list +* Reduce database round trips initiated by price calculation +* Google Analytics: added support for mobile devices +* (Dev) TaskScheduler: Tasks can propagate progress info (percentage & message) +* (Dev) TaskScheduler: Cancellation request is sent to tasks on app shutdown +* ShippingByWeight & ShippingByTotal: Support for multiple zip ranges (comma separated) +* Two more options to handle customer numbers: display customer number in frontend & let customers enter their customer number if it's still empty +* #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 +* Preselected price was wrong for product attributes with multiple preselected values (same on product detail page) +* #749 Visual Studio 2015 compilation error: CS0009: Metadata file. SmartStore.Licensing.dll could not be opened -- Illegal tables in compressed metadata stream +* PayPal Express: fixed capture method +* #770 Resizing browser with product details page causes product image to disappear +* GMC feed: Availability value "available for order" deprecated +* Mobile: Shopping cart warnings weren't displayed to customers +* Tax provider and payment method were automatically activated when there were no active provider\method +* #784 Biz-Importer: Name of delivery time must not be imported empty +* #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 ### New Features +* SmartStore.NET User Guide * #210 Implement multi-store support for import/export * Added zip code to shipping by weight computation method +* Skrill payment plugin (distributed via Marketplace) +* (Dev) DevTool plugin: added option to display all widget zones in public store +* New options for manufacturer display on the homepage +* Added optional customer number field ### Improvements +* (Perf) several minor optimizations for faster app startup and page rendering +* UI: optimized image gallery widget (white background & nicer animations) + enhanced modal dialog fade animations * (Soft) deletion of SEO slug supporting entities now also deletes the corresponding url records * License checker now supports IDN mapping for domain names * #716 Supporting of paged google-product data query for SQL-Server Compact Edition -* #648 Add hint for * at mandatory form fields at address creation +* #648 Add hint for * at mandatory form fields at address creation +* Added link to imprint and disclaimer to footer in mobile theme +* #521 Display bonus points in order export +* Updated GMC taxonomy files +* MsieJsEngine now is the default LESS script engine ### Bugfixes * #694 Product variant attribute in product page should not be preselected implicitly * Fixed: If currencies are limited to one for a multi-store, this currency should dominate the setting for the primary store currency -* #563 Scheduled Tasks: ensure, that 'LastEndUtc' is ALWAYS set +* #563 Scheduled Tasks: ensure that 'LastEndUtc' is ALWAYS set * Topics grid: fixed 'maxJsonLength exceeded' error * Debitoor: Fixed "The property named 'lines.0.productOrService' should be defined" * Send currency code of primary store currency (not of working currency) to payment gateway * #691 Product quantity not added to cart on mobile theme * #186 Mobile: variant images do not refresh * #671 Bundle products: display base price according to applied discount +* #619 Display base price according to applied tier price +* #726 PAngV: basket displays wrong base price when attribute price adjustment has been set +* Weight adjustment of attributes weren't applied in shopping cart overview +* Shipping by weight calculates wrong surcharge if attribute combination prices are set +* Don't let database hooks call other hooks. +* There was no payment redirect if only one payment method is available in checkout ## SmartStore.NET 2.2.1 diff --git a/lib/SmartStore.Licensing/SmartStore.Licensing.dll b/lib/SmartStore.Licensing/SmartStore.Licensing.dll index 4c4579257f..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 8cf3480af0..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.1.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 0186e041c1..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 { @@ -14,11 +13,18 @@ public static class AsyncRunner { private static readonly BackgroundWorkHost _host = new BackgroundWorkHost(); + /// + /// Gets the global cancellation token which signals the application shutdown + /// + public static CancellationToken AppShutdownCancellationToken + { + get { return _host.ShutdownCancellationTokenSource.Token; } + } /// - /// Execute's 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; @@ -46,10 +52,10 @@ public static void RunSync(Func func) } /// - /// Execute's 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) { @@ -113,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); } @@ -136,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); } @@ -185,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); } @@ -208,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); } @@ -222,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) { @@ -234,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) @@ -268,7 +275,7 @@ public void BeginMessageLoop() } else { - workItemsWaiting.WaitOne(); + _workItemsWaiting.WaitOne(); } } } @@ -283,14 +290,19 @@ 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() { HostingEnvironment.RegisterObject(this); } + public CancellationTokenSource ShutdownCancellationTokenSource + { + get { return _shutdownCancellationTokenSource; } + } + public void Stop(bool immediate) { int num; diff --git a/src/Libraries/SmartStore.Core/Async/AsyncState.cs b/src/Libraries/SmartStore.Core/Async/AsyncState.cs index 2a4fb4a1d9..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,8 +8,8 @@ namespace SmartStore.Core.Async public class AsyncState { - private readonly static AsyncState s_instance = new AsyncState(); - private readonly MemoryCache _cache = MemoryCache.Default; + private static readonly AsyncState s_instance = new AsyncState(); + private readonly MemoryCache _cache = new MemoryCache("SmartStore.AsyncState"); private AsyncState() { @@ -35,6 +32,22 @@ public T Get(string name = null) return Get(out cancelTokenSource, name); } + public IEnumerable GetAll() + { + var keyPrefix = BuildKey(null); + foreach (var kvp in _cache) + { + if (kvp.Key.StartsWith(keyPrefix)) + { + var value = kvp.Value as StateInfo; + if (value != null && value.Progress != null) + { + yield return (T)value.Progress; + } + } + } + } + public CancellationTokenSource GetCancelTokenSource(string name = null) { CancellationTokenSource cancelTokenSource; @@ -46,13 +59,13 @@ private T Get(out CancellationTokenSource cancelTokenSource, string name = nu { cancelTokenSource = null; var key = BuildKey(name); - + var value = _cache.Get(key) as StateInfo; if (value != null) { cancelTokenSource = value.CancellationTokenSource; - return (T)(value.Progress); + return (T)value.Progress; } return default(T); @@ -60,14 +73,32 @@ private T Get(out CancellationTokenSource cancelTokenSource, string name = nu public void Set(T state, string name = null, bool neverExpires = false) { - this.Set(state, null, name, neverExpires); + Guard.ArgumentNotNull(() => state); + Set(state, null, name, neverExpires); + } + + public void Update(Action update, string name = null) + { + Guard.ArgumentNotNull(() => update); + + var key = BuildKey(typeof(T), name); + + var value = _cache.Get(key) as StateInfo; + if (value != null) + { + var state = (T)value.Progress; + if (state != null) + { + update(state); + } + } } public void SetCancelTokenSource(CancellationTokenSource cancelTokenSource, string name = null) { 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) @@ -79,7 +110,10 @@ private void Set(T state, CancellationTokenSource cancelTokenSource, string n if (value != null) { // exists already, so update - value.Progress = state; + if (state != null) + { + value.Progress = state; + } if (cancelTokenSource != null && value.CancellationTokenSource == null) { value.CancellationTokenSource = cancelTokenSource; @@ -88,7 +122,10 @@ private void Set(T state, CancellationTokenSource cancelTokenSource, string n var policy = new CacheItemPolicy { SlidingExpiration = neverExpires ? TimeSpan.Zero : TimeSpan.FromMinutes(15) }; - _cache.Set(key, value ?? new StateInfo { Progress = state, CancellationTokenSource = cancelTokenSource }, policy); + _cache.Set( + key, + value ?? new StateInfo { Progress = state, CancellationTokenSource = cancelTokenSource }, + policy); } public bool Remove(string name = null) diff --git a/src/Libraries/SmartStore.Core/BaseEntity.cs b/src/Libraries/SmartStore.Core/BaseEntity.cs index 9ff517f329..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.")) @@ -36,9 +37,9 @@ public Type GetUnproxiedType() /// Transient objects are not associated with an item already in storage. For instance, /// a Product entity is transient if its Id is 0. /// - public virtual bool IsTransient + public virtual bool IsTransientRecord() { - get { return Id == 0; } + return Id == 0; } public override bool Equals(object obj) @@ -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.IsTransient) + 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(); } } @@ -95,7 +97,7 @@ public override int GetHashCode() private bool HasSameNonDefaultIds(BaseEntity other) { - return !this.IsTransient && !other.IsTransient && this.Id == other.Id; + return !this.IsTransientRecord() && !other.IsTransientRecord() && this.Id == other.Id; } } } diff --git a/src/Libraries/SmartStore.Core/Caching/AspNetCache.cs b/src/Libraries/SmartStore.Core/Caching/AspNetCache.cs index e50f69ed08..4af855729a 100644 --- a/src/Libraries/SmartStore.Core/Caching/AspNetCache.cs +++ b/src/Libraries/SmartStore.Core/Caching/AspNetCache.cs @@ -4,20 +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$$"; - - // AspNetCache object does not have a ContainsKey() method: - // Therefore we 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 RegionName = "$$SmartStoreNET$$"; public IEnumerable> Entries { @@ -28,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); } } @@ -40,12 +33,7 @@ public object Get(string key) if (HttpRuntime.Cache == null) return null; - var value = HttpRuntime.Cache.Get(BuildKey(key)); - - if (value.Equals(FAKE_NULL)) - return null; - - return value; + return HttpRuntime.Cache.Get(BuildKey(key)); } public void Set(string key, object value, int? cacheTime) @@ -62,7 +50,7 @@ public void Set(string key, object value, int? cacheTime) absoluteExpiration = DateTime.UtcNow + TimeSpan.FromMinutes(cacheTime.Value); } - HttpRuntime.Cache.Insert(key, value ?? FAKE_NULL, null, absoluteExpiration, Cache.NoSlidingExpiration); + HttpRuntime.Cache.Insert(key, value, null, absoluteExpiration, Cache.NoSlidingExpiration); } public bool Contains(string key) @@ -83,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 bdf35da722..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)); } } @@ -21,7 +21,12 @@ public partial class CacheManager : ICacheManager where TCache : ICache private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); private readonly ICache _cache; - public CacheManager(Func fn) + // 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 FakeNull = "__[NULL]__"; + + public CacheManager(Func fn) { this._cache = fn(typeof(TCache)); } @@ -32,35 +37,40 @@ public T Get(string key, Func acquirer, int? cacheTime = null) if (_cache.Contains(key)) { - return (T)_cache.Get(key); + return GetExisting(key); } - else + + using (EnterReadLock()) { - using (EnterReadLock()) + if (!_cache.Contains(key)) { - if (!_cache.Contains(key)) - { - var value = acquirer(); - this.Set(key, value, cacheTime); + var value = acquirer(); + this.Set(key, value, cacheTime); - return value; - } + return value; } - - return (T)_cache.Get(key); } - } + + return GetExisting(key); + } + + private T GetExisting(string key) + { + var value = _cache.Get(key); + + if (value.Equals(FakeNull)) + return default(T); + + return (T)_cache.Get(key); + } public void Set(string key, object value, int? cacheTime = null) { Guard.ArgumentNotEmpty(() => key); - - if (value == null) - return; using (EnterWriteLock()) { - _cache.Set(key, value, 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 a293c6cd4f..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); } } } @@ -64,13 +64,10 @@ public void Set(string key, object value, int? cacheTime) key = BuildKey(key); - if (value != null) - { - if (items.Contains(key)) - items[key] = value; - else - items.Add(key, value); - } + if (items.Contains(key)) + items[key] = value; + else + items.Add(key, value); } public bool Contains(string key) @@ -93,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 new file mode 100644 index 0000000000..789c363da0 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Collections/LazyMultimap.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace SmartStore.Collections +{ + /// + /// Manages data keys and offers a combination of eager and lazy data loading + /// + public class LazyMultimap : Multimap + { + private readonly Func> _load; + private readonly List _loaded; // to avoid database round trips with empty results + private List _collect; + //private int _roundTripCount; + + /// + /// Constructor + /// + /// 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(); + + _collect = collect == null ? new List() : new List(collect); + } + + protected virtual void Load(IEnumerable keys) + { + if (keys != null) + { + var loadKeys = (_collect.Count == 0 ? keys : _collect.Concat(keys)) + .Distinct() + .Except(_loaded) + .ToArray(); + + _collect.Clear(); // invalidate, do not load again + + if (loadKeys.Any()) + { + //++_roundTripCount; + //Debug.WriteLine("Round trip {0} of {1}: {2}", _roundTripCount, typeof(T).Name, string.Join(",", loadKeys.OrderBy(x => x))); + + var items = _load(loadKeys); + + _loaded.AddRange(loadKeys); + + if (items != null) + { + foreach (var range in items) + { + base.AddRange(range.Key, range.Value); + } + } + } + } + } + + /// + /// Get data. Load it if not already loaded yet. + /// + /// Data key + /// Collection of data + public virtual ICollection Load(int key) + { + if (key == 0) + { + return new List(); + } + + if (!_loaded.Contains(key)) + { + Load(new int[] { key }); + } + + // better not override indexer cause of stack overflow risk + var result = base[key]; + + Debug.Assert(_loaded.Contains(key), "Possible missing multimap result for key {0} and type {1}.".FormatInvariant(key, typeof(T).Name), ""); + + return result; + } + + /// + /// Collect keys for combined loading + /// + /// Data keys + [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] + public virtual void Collect(IEnumerable keys) + { + if (keys != null && keys.Any()) + { + _collect = _collect.Union(keys).ToList(); + } + } + + /// + /// Collect single key for combined loading + /// + /// Data key + public virtual void Collect(int key) + { + if (key != 0 && !_collect.Contains(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/DbContextScope.cs b/src/Libraries/SmartStore.Core/Data/DbContextScope.cs index 50ca2cc29b..93a7552c50 100644 --- a/src/Libraries/SmartStore.Core/Data/DbContextScope.cs +++ b/src/Libraries/SmartStore.Core/Data/DbContextScope.cs @@ -1,23 +1,31 @@ using System; using SmartStore.Core.Infrastructure; +using System.Linq.Expressions; +using System.Collections.Generic; +using System.Linq; namespace SmartStore.Core.Data { public class DbContextScope : IDisposable { - private readonly bool _autoDetectChangesEnabled; + private readonly IDbContext _ctx; + private readonly bool _autoDetectChangesEnabled; private readonly bool _proxyCreationEnabled; private readonly bool _validateOnSaveEnabled; private readonly bool _forceNoTracking; private readonly bool _hooksEnabled; - private readonly IDbContext _ctx; + private readonly bool _autoCommit; + private readonly bool _lazyLoading; + public DbContextScope(IDbContext ctx = null, bool? autoDetectChanges = null, bool? proxyCreation = null, bool? validateOnSave = null, bool? forceNoTracking = null, - bool? hooksEnabled = null) + bool? hooksEnabled = null, + bool? autoCommit = null, + bool? lazyLoading = null) { _ctx = ctx ?? EngineContext.Current.Resolve(); _autoDetectChangesEnabled = _ctx.AutoDetectChangesEnabled; @@ -25,6 +33,8 @@ public DbContextScope(IDbContext ctx = null, _validateOnSaveEnabled = _ctx.ValidateOnSaveEnabled; _forceNoTracking = _ctx.ForceNoTracking; _hooksEnabled = _ctx.HooksEnabled; + _autoCommit = _ctx.AutoCommitEnabled; + _lazyLoading = _ctx.LazyLoadingEnabled; if (autoDetectChanges.HasValue) _ctx.AutoDetectChangesEnabled = autoDetectChanges.Value; @@ -40,7 +50,39 @@ public DbContextScope(IDbContext ctx = null, if (hooksEnabled.HasValue) _ctx.HooksEnabled = hooksEnabled.Value; - } + + if (autoCommit.HasValue) + _ctx.AutoCommitEnabled = autoCommit.Value; + + if (lazyLoading.HasValue) + _ctx.LazyLoadingEnabled = lazyLoading.Value; + } + + public IDbContext DbContext + { + get { return _ctx; } + } + + public void LoadCollection( + TEntity entity, + Expression>> navigationProperty, + bool force = false, + Func, IQueryable> queryAction = null) + where TEntity : BaseEntity + where TCollection : BaseEntity + { + _ctx.LoadCollection(entity, navigationProperty, force, queryAction); + } + + public void LoadReference( + TEntity entity, + Expression> navigationProperty, + bool force = false) + where TEntity : BaseEntity + where TProperty : BaseEntity + { + _ctx.LoadReference(entity, navigationProperty, force); + } public int Commit() { @@ -54,6 +96,8 @@ public void Dispose() _ctx.ValidateOnSaveEnabled = _validateOnSaveEnabled; _ctx.ForceNoTracking = _forceNoTracking; _ctx.HooksEnabled = _hooksEnabled; + _ctx.AutoCommitEnabled = _autoCommit; + _ctx.LazyLoadingEnabled = _lazyLoading; } } 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 b0a2e1506d..cb276a03ac 100644 --- a/src/Libraries/SmartStore.Core/Data/IDbContext.cs +++ b/src/Libraries/SmartStore.Core/Data/IDbContext.cs @@ -1,9 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Data.Entity; +using System.Linq; +using System.Linq.Expressions; using System.Threading.Tasks; -using SmartStore.Core; namespace SmartStore.Core.Data { @@ -39,12 +41,12 @@ IList ExecuteStoredProcedureList(string commandText, params ob /// Executes sql by using SQL-Server Management Objects which supports GO statements. int ExecuteSqlThroughSmo(string sql); - // codehint: sm-add (required for UoW implementation) string Alias { get; } // increasing performance on bulk operations bool ProxyCreationEnabled { get; set; } - bool AutoDetectChangesEnabled { get; set; } + bool LazyLoadingEnabled { get; set; } + bool AutoDetectChangesEnabled { get; set; } bool ValidateOnSaveEnabled { get; set; } bool HooksEnabled { get; set; } bool HasChanges { get; } @@ -59,6 +61,12 @@ IList ExecuteStoredProcedureList(string commandText, params ob /// bool ForceNoTracking { get; set; } + /// + /// Gets or sets a value indicating whether database write operations + /// originating from repositories should be committed immediately. + /// + bool AutoCommitEnabled { get; set; } + /// /// Gets a list of modified properties for the specified entity /// @@ -77,26 +85,29 @@ IList ExecuteStoredProcedureList(string commandText, params ob /// Type of entity /// The entity instance to attach /// true when the entity is attched already, false otherwise - bool IsAttached(TEntity entity) where TEntity : BaseEntity, new(); + 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, new(); + /// + /// 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 + /// Detaches an entity from the current object context if it's attached /// + /// Type of entity /// The entity instance to detach - void Detach(object entity); + void DetachEntity(TEntity entity) where TEntity : BaseEntity; /// - /// Detaches all entities from the current object context + /// Detaches all entities of type TEntity from the current object context /// + /// When true, only entities in unchanged state get detached. /// The count of detached entities - int DetachAll(); + int DetachEntities(bool unchangedEntitiesOnly = true) where TEntity : class; /// /// Change the state of an entity object @@ -104,15 +115,15 @@ IList ExecuteStoredProcedureList(string commandText, params ob /// Type of entity /// The entity instance /// The new state - void ChangeState(TEntity entity, System.Data.Entity.EntityState newState); + void ChangeState(TEntity entity, System.Data.Entity.EntityState newState) where TEntity : BaseEntity; /// - /// Changes the object state to unchanged + /// Reloads the entity from the database overwriting any property values with values from the database. + /// The entity will be in the Unchanged state after calling this method. /// /// Type of entity /// The entity instance - /// true on success, false on failure - bool SetToUnchanged(TEntity entity); + void ReloadEntity(TEntity entity) where TEntity : BaseEntity; /// /// Begins a transaction on the underlying store connection using the specified isolation level @@ -127,4 +138,5 @@ IList ExecuteStoredProcedureList(string commandText, params ob /// the external transaction void UseTransaction(DbTransaction transaction); } + } diff --git a/src/Libraries/SmartStore.Core/Data/IDbContextExtensions.cs b/src/Libraries/SmartStore.Core/Data/IDbContextExtensions.cs new file mode 100644 index 0000000000..f3afa01d07 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Data/IDbContextExtensions.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using SmartStore.Core; +using SmartStore.Core.Data; +using System.Linq; +using System.Linq.Expressions; +using System.Data.Entity; + +namespace SmartStore +{ + public static class IDbContextExtensions + { + + /// + /// Detaches all entities from the current object context + /// + /// When true, only entities in unchanged state get detached. + /// The count of detached entities + public static int DetachAll(this IDbContext ctx, bool unchangedEntitiesOnly = true) + { + return ctx.DetachEntities(unchangedEntitiesOnly); + } + + public static void DetachEntities(this IDbContext ctx, IEnumerable entities) where TEntity : BaseEntity + { + Guard.ArgumentNotNull(() => ctx); + + entities.Each(x => ctx.DetachEntity(x)); + } + + /// + /// Changes the object state to unchanged + /// + /// Type of entity + /// + /// The entity instance + /// true on success, false on failure + public static bool SetToUnchanged(this IDbContext ctx, TEntity entity) where TEntity : BaseEntity + { + try + { + ctx.ChangeState(entity, System.Data.Entity.EntityState.Unchanged); + return true; + } + catch (Exception ex) + { + ex.Dump(); + return false; + } + } + + public static IQueryable QueryForCollection( + this IDbContext ctx, + TEntity entity, + Expression>> navigationProperty) + where TEntity : BaseEntity + where TCollection : BaseEntity + { + Guard.ArgumentNotNull(() => entity); + Guard.ArgumentNotNull(() => navigationProperty); + + var dbContext = ctx as DbContext; + if (dbContext == null) + { + throw new NotSupportedException("The IDbContext instance does not inherit from DbContext (EF)"); + } + + return dbContext.Entry(entity).Collection(navigationProperty).Query(); + } + + public static IQueryable QueryForReference( + this IDbContext ctx, + TEntity entity, + Expression> navigationProperty) + where TEntity : BaseEntity + where TProperty : BaseEntity + { + Guard.ArgumentNotNull(() => entity); + Guard.ArgumentNotNull(() => navigationProperty); + + var dbContext = ctx as DbContext; + if (dbContext == null) + { + throw new NotSupportedException("The IDbContext instance does not inherit from DbContext (EF)"); + } + + return dbContext.Entry(entity).Reference(navigationProperty).Query(); + } + + public static void LoadCollection( + this IDbContext ctx, + TEntity entity, + Expression>> navigationProperty, + bool force = false, + Func, IQueryable> queryAction = null) + where TEntity : BaseEntity + where TCollection : BaseEntity + { + Guard.ArgumentNotNull(() => entity); + Guard.ArgumentNotNull(() => navigationProperty); + + var dbContext = ctx as DbContext; + if (dbContext == null) + { + throw new NotSupportedException("The IDbContext instance does not inherit from DbContext (EF)"); + } + + var entry = dbContext.Entry(entity); + var collection = entry.Collection(navigationProperty); + + if (force) + { + collection.IsLoaded = false; + } + + if (!collection.IsLoaded) + { + if (queryAction != null || ctx.ForceNoTracking) + { + var query = !ctx.ForceNoTracking + ? collection.Query() + : collection.Query().AsNoTracking(); + + var myQuery = queryAction != null + ? queryAction(query) + : query; + + collection.CurrentValue = myQuery.ToList(); + } + else + { + collection.Load(); + } + + collection.IsLoaded = true; + } + } + + public static void LoadReference( + this IDbContext ctx, + TEntity entity, + Expression> navigationProperty, + bool force = false) + where TEntity : BaseEntity + where TProperty : BaseEntity + { + Guard.ArgumentNotNull(() => entity); + Guard.ArgumentNotNull(() => navigationProperty); + + var dbContext = ctx as DbContext; + if (dbContext == null) + { + throw new NotSupportedException("The IDbContext instance does not inherit from DbContext (EF)"); + } + + var entry = dbContext.Entry(entity); + var reference = entry.Reference(navigationProperty); + + if (force) + { + reference.IsLoaded = false; + } + + if (!reference.IsLoaded) + { + reference.Load(); + reference.IsLoaded = true; + } + } + + 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/IQueryableExtensions.cs b/src/Libraries/SmartStore.Core/Data/IQueryableExtensions.cs index 6349fec29c..2289488b95 100644 --- a/src/Libraries/SmartStore.Core/Data/IQueryableExtensions.cs +++ b/src/Libraries/SmartStore.Core/Data/IQueryableExtensions.cs @@ -3,8 +3,9 @@ using System.Linq; using System.Data.Entity; using System.Linq.Expressions; +using SmartStore.Core; -namespace SmartStore.Core.Data +namespace SmartStore { public static class IQueryableExtensions { diff --git a/src/Libraries/SmartStore.Core/Data/IRepository.cs b/src/Libraries/SmartStore.Core/Data/IRepository.cs index 905705c289..cae856f654 100644 --- a/src/Libraries/SmartStore.Core/Data/IRepository.cs +++ b/src/Libraries/SmartStore.Core/Data/IRepository.cs @@ -39,6 +39,13 @@ public partial interface IRepository where T : BaseEntity /// The resolved entity T GetById(object id); + /// + /// Attaches an entity to the context + /// + /// The entity to attach + /// The entity + T Attach(T entity); + /// /// Marks the entity instance to be saved to the store. /// @@ -63,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); @@ -119,6 +126,10 @@ public partial interface IRepository where T : BaseEntity /// Gets or sets a value indicating whether database write operations /// such as insert, delete or update should be committed immediately. /// - bool AutoCommitEnabled { get; set; } + /// + /// Set this to true or false to supersede the global AutoCommitEnabled + /// on level for this repository instance only. + /// + bool? AutoCommitEnabled { get; set; } } } 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 2e6e8ac414..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); @@ -42,15 +27,77 @@ public static IEnumerable GetMany(this IRepository rs, IEnumerable } } - public static void Delete(this IRepository rs, object id) where T : BaseEntity + public static void Delete(this IRepository rs, int id) where T : BaseEntity { - T entityToDelete = rs.GetById(id); - if (entityToDelete != null) - { - rs.Delete(entityToDelete); - } + Guard.ArgumentNotZero(id, "id"); + + // Perf: work with stub entity + var entity = rs.Create(); + entity.Id = id; + + rs.Attach(entity); + + // must downcast 'cause of Rhino mocks stub + rs.Context.ChangeState((BaseEntity)entity, System.Data.Entity.EntityState.Deleted); } + public static void DeleteRange(this IRepository rs, IEnumerable ids) where T : BaseEntity + { + Guard.ArgumentNotNull(() => ids); + + ids.Each(id => rs.Delete(id)); + } + + /// + /// Truncates the table + /// + /// Entity type + /// The repository + /// An optional filter + /// + /// false: does not make any attempts to determine dependant entities, just deletes ONLY them (faster). + /// true: loads all entities into the context first and deletes them, along with their dependencies (slower). + /// + /// The total number of affected entities + /// + /// 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; + + using (var scope = new DbContextScope(ctx: rs.Context, autoDetectChanges: false, validateOnSave: false, hooksEnabled: false, autoCommit: false)) + { + var query = rs.Table; + if (predicate != null) + { + query = query.Where(predicate); + } + + if (cascade) + { + var records = query.ToList(); + foreach (var chunk in records.Chunk(500)) + { + rs.DeleteRange(chunk.ToList()); + count += rs.Context.SaveChanges(); + } + } + else + { + var ids = query.Select(x => new { Id = x.Id }).ToList(); + foreach (var chunk in ids.Chunk(500)) + { + rs.DeleteRange(chunk.Select(x => x.Id)); + count += rs.Context.SaveChanges(); + } + } + } + + return count; + } + public static IQueryable Get( this IRepository rs, Expression> predicate = null, 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/Blogs/BlogSettings.cs b/src/Libraries/SmartStore.Core/Domain/Blogs/BlogSettings.cs index 8a1b5ca08e..e0c7492934 100644 --- a/src/Libraries/SmartStore.Core/Domain/Blogs/BlogSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Blogs/BlogSettings.cs @@ -11,6 +11,7 @@ public BlogSettings() PostsPageSize = 10; AllowNotRegisteredUsersToLeaveComments = true; NumberOfTags = 15; + MaxAgeInDays = 180; } /// @@ -38,6 +39,11 @@ public BlogSettings() /// public int NumberOfTags { get; set; } + /// + /// The maximum age of blog items (in days) for RSS feed + /// + public int MaxAgeInDays { get; set; } + /// /// Enable the blog RSS feed link in customers browser address bar /// diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs index dd7d67edea..12f9fa8587 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/CatalogSettings.cs @@ -16,6 +16,7 @@ public CatalogSettings() { FileUploadAllowedExtensions = new List(); AllowProductSorting = true; + DefaultSortOrder = ProductSortingEnum.Position; AllowProductViewModeChanging = true; DefaultViewMode = "grid"; CategoryBreadcrumbEnabled = true; @@ -32,12 +33,16 @@ public CatalogSettings() CompareProductsEnabled = true; FilterEnabled = true; MaxFilterItemsToDisplay = 4; + SortFilterResultsByMatches = true; SubCategoryDisplayType = SubCategoryDisplayType.AboveProductList; ProductSearchAutoCompleteEnabled = true; ShowProductImagesInSearchAutoComplete = true; ProductSearchAutoCompleteNumberOfProducts = 10; ProductSearchTermMinimumLength = 3; NumberOfBestsellersOnHomepage = 6; + ShowManufacturersOnHomepage = true; + ShowManufacturerPictures = false; + ShowManufacturerPicturesInProductDetail = true; SearchPageProductsPerPage = 6; ProductsAlsoPurchasedEnabled = true; ProductsAlsoPurchasedNumber = 6; @@ -123,13 +128,18 @@ public CatalogSettings() /// public bool AllowProductSorting { get; set; } + /// + /// Gets or sets the default sort order in product lists + /// + public ProductSortingEnum DefaultSortOrder { get; set; } + /// /// Gets or sets a value indicating whether customers are allowed to change product view mode /// public bool AllowProductViewModeChanging { get; set; } /// - /// Gets or sets a value indicating whether customers are allowed to change product view mode + /// Gets or sets the default view mode for product lists /// public string DefaultViewMode { get; set; } @@ -167,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 /// @@ -284,9 +299,39 @@ public CatalogSettings() public int NumberOfBestsellersOnHomepage { get; set; } /// - /// Gets or sets a number of products per page on search products page + /// Gets or sets a value indicating whether to show manufacturers on home page /// - public int SearchPageProductsPerPage { get; set; } + public bool ShowManufacturersOnHomepage { get; set; } + + /// + /// Gets or sets a value indicating whether to show manufacturer pictures or names on home page + /// + public bool ShowManufacturerPictures { 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 @@ -348,10 +393,20 @@ 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 + /// + public PriceDisplayType PriceDisplayType { get; set; } /// /// Gets or sets a value indicating whether to include "Short description" in compare products diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/PriceDisplayType.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/PriceDisplayType.cs new file mode 100644 index 0000000000..aeb4c24250 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/PriceDisplayType.cs @@ -0,0 +1,29 @@ + +namespace SmartStore.Core.Domain.Catalog +{ + /// + /// Represents types of product prices to display + /// + public enum PriceDisplayType + { + /// + /// The lowest possible price of a product (default) + /// + LowestPrice = 0, + + /// + /// The product price initially displayed on the product detail page + /// + PreSelectedPrice = 10, + + /// + /// The product price without associated data like discounts, tier prices, attributes or attribute combinations + /// + PriceWithoutDiscountsAndAttributes = 20, + + /// + /// Do not display a product price + /// + Hide = 30 + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/Product.cs index 61f332eb11..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; @@ -104,6 +104,12 @@ public partial class Product : BaseEntity, ISoftDeletable, ILocalizedEntity, ISl [DataMember] public bool ShowOnHomePage { get; set; } + /// + /// Gets or sets the display order for homepage products + /// + [DataMember] + public int HomePageDisplayOrder { get; set; } + /// /// Gets or sets the meta keywords /// @@ -185,6 +191,7 @@ public string Sku /// Gets or sets the manufacturer part number /// [DataMember] + [Index] public string ManufacturerPartNumber { [DebuggerStepThrough] @@ -202,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] @@ -246,7 +254,7 @@ public string Gtin public bool AutomaticallyAddRequiredProducts { get; set; } /// - /// Gets or sets a value indicating whether the product is download + /// Gets or sets a value indicating whether the product is a download /// [DataMember] public bool IsDownload { get; set; } @@ -387,7 +395,7 @@ public int StockQuantity [DebuggerStepThrough] get { - return this.GetMergedDataValue("StockQuantity", _stockQuantity); + return this.GetMergedDataValue("StockQuantity", _stockQuantity); } set { @@ -559,7 +567,7 @@ public decimal Price /// Gets or sets a value indicating whether this product has tier prices configured /// The same as if we run this.TierPrices.Count > 0 /// We use this property for performance optimization: - /// if this property is set to false, then we do not need to load tier prices navifation property + /// if this property is set to false, then we do not need to load tier prices navigation property /// /// [DataMember] @@ -1040,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/Catalog/ProductSortingEnum.cs b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductSortingEnum.cs index 17a29dbab3..8db56d81b4 100644 --- a/src/Libraries/SmartStore.Core/Domain/Catalog/ProductSortingEnum.cs +++ b/src/Libraries/SmartStore.Core/Domain/Catalog/ProductSortingEnum.cs @@ -5,10 +5,14 @@ /// public enum ProductSortingEnum { + /// + /// Initial state + /// + Initial = 0, /// /// Position (display order) /// - Position = 0, + Position = 1, /// /// Name: A to Z /// @@ -32,6 +36,6 @@ public enum ProductSortingEnum /// /// Product creation date /// - CreatedOnAsc = 16 // codehint: sm-add + CreatedOnAsc = 16 } } \ No newline at end of file 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/PdfSettings.cs b/src/Libraries/SmartStore.Core/Domain/Common/PdfSettings.cs index 45a7c50906..bddfb93963 100644 --- a/src/Libraries/SmartStore.Core/Domain/Common/PdfSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Common/PdfSettings.cs @@ -30,5 +30,15 @@ public PdfSettings() /// Gets or sets a value indicating whether to render order notes in PDf reports /// public bool RenderOrderNotes { get; set; } + + /// + /// Gets or sets a value indicating whether to attach the order PDF to 'Order Placed (customer)' email + /// + public bool AttachOrderPdfToOrderPlacedEmail { get; set; } + + /// + /// Gets or sets a value indicating whether to attach the order PDF to 'Order Completed (customer)' email + /// + public bool AttachOrderPdfToOrderCompletedEmail { get; set; } } } \ No newline at end of file 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 new file mode 100644 index 0000000000..f333486399 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerNumberMethod.cs @@ -0,0 +1,24 @@ +namespace SmartStore.Core.Domain.Customers +{ + /// + /// Represents the customer number method + /// + public enum CustomerNumberMethod + { + /// + /// no customer number will be saved + /// + Disabled = 10, + + /// + /// customer numbers can be saved + /// + Enabled = 20, + + /// + /// customer numbers will automatically be set when new customers are created + /// + AutomaticallySet = 30, + + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerNumberVisibility.cs b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerNumberVisibility.cs new file mode 100644 index 0000000000..d05e588e97 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerNumberVisibility.cs @@ -0,0 +1,28 @@ +namespace SmartStore.Core.Domain.Customers +{ + /// + /// Represents the customer visibility in the frontend + /// + public enum CustomerNumberVisibility + { + /// + /// Customer number won't be displayed in the frontend + /// + None = 10, + + /// + /// Customer number will be displayed in the frontend + /// + Display = 20, + + /// + /// A customer can enter his own number if customer number wasn't saved yet + /// + EditableIfEmpty = 30, + + /// + /// A customer can enter his own number and alter it + /// + Editable = 40, + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs index f37d102711..e6d7f56f56 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/CustomerSettings.cs @@ -8,6 +8,8 @@ public class CustomerSettings : ISettings public CustomerSettings() { UsernamesEnabled = true; + CustomerNumberMethod = Customers.CustomerNumberMethod.Disabled; + CustomerNumberVisibility = Customers.CustomerNumberVisibility.None; DefaultPasswordFormat = PasswordFormat.Hashed; HashedPasswordFormat = "SHA1"; PasswordMinLength = 6; @@ -22,6 +24,7 @@ public CustomerSettings() NewsletterEnabled = true; OnlineCustomerMinutes = 20; StoreLastVisitedPage = true; + DisplayPrivacyAgreementOnContactUs = false; } /// @@ -29,6 +32,16 @@ public CustomerSettings() /// public bool UsernamesEnabled { get; set; } + /// + /// Gets or sets the customer number method + /// + public CustomerNumberMethod CustomerNumberMethod { get; set; } + + /// + /// Gets or sets the customer number visibility + /// + public CustomerNumberVisibility CustomerNumberVisibility { get; set; } + /// /// Gets or sets a value indicating whether users can check the availability of usernames (when registering or changing in 'My Account') /// @@ -139,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 /// @@ -234,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/SystemCustomerAttributeNames.cs b/src/Libraries/SmartStore.Core/Domain/Customers/SystemCustomerAttributeNames.cs index ea0bb24d01..33569b4855 100644 --- a/src/Libraries/SmartStore.Core/Domain/Customers/SystemCustomerAttributeNames.cs +++ b/src/Libraries/SmartStore.Core/Domain/Customers/SystemCustomerAttributeNames.cs @@ -20,6 +20,7 @@ public static partial class SystemCustomerAttributeNames public static string VatNumber { get { return "VatNumber"; } } public static string VatNumberStatusId { get { return "VatNumberStatusId"; } } public static string TimeZoneId { get { return "TimeZoneId"; } } + public static string CustomerNumber { get { return "CustomerNumber"; } } //Other attributes public static string DiscountCouponCode { get { return "DiscountCouponCode"; } } @@ -31,7 +32,8 @@ public static partial class SystemCustomerAttributeNames public static string PasswordRecoveryToken { get { return "PasswordRecoveryToken"; } } public static string AccountActivationToken { get { return "AccountActivationToken"; } } public static string LastVisitedPage { get { return "LastVisitedPage"; } } - public static string ImpersonatedCustomerId { get { return "ImpersonatedCustomerId"; } } + public static string LastUserAgent { get { return "LastUserAgent"; } } + public static string ImpersonatedCustomerId { get { return "ImpersonatedCustomerId"; } } public static string AdminAreaStoreScopeConfiguration { get { return "AdminAreaStoreScopeConfiguration"; } } public static string MostRecentlyUsedCategories { get { return "MostRecentlyUsedCategories"; } } public static string MostRecentlyUsedManufacturers { get { return "MostRecentlyUsedManufacturers"; } } 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 new file mode 100644 index 0000000000..4b0bc6d497 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/DataExchangeSettings.cs @@ -0,0 +1,28 @@ +using SmartStore.Core.Configuration; + +namespace SmartStore.Core.Domain.DataExchange +{ + 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 new file mode 100644 index 0000000000..b9e323c670 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportDeployment.cs @@ -0,0 +1,141 @@ +using System; +using SmartStore.Core.Domain.DataExchange; + +namespace SmartStore.Core.Domain +{ + public class ExportDeployment : BaseEntity, ICloneable + { + /// + /// The profile identifier + /// + public int ProfileId { get; set; } + + /// + /// Name of the deployment + /// + public string Name { get; set; } + + /// + /// Whether the deployment is enabled + /// + public bool Enabled { get; set; } + + /// + /// XML with information about the last deployment result + /// + public string ResultInfo { get; set; } + + /// + /// The deployment type identifier + /// + public int DeploymentTypeId { get; set; } + + /// + /// The deployment type + /// + public ExportDeploymentType DeploymentType + { + get + { + return (ExportDeploymentType)DeploymentTypeId; + } + set + { + DeploymentTypeId = (int)value; + } + } + + public string Username { get; set; } + + public string Password { get; set; } + + /// + /// Deployment URL + /// + public string Url { get; set; } + + /// + /// The type identifier of how to transmit via HTTP + /// + public int HttpTransmissionTypeId { get; set; } + + /// + /// The type of how to transmit via HTTP + /// + public ExportHttpTransmissionType HttpTransmissionType + { + get + { + return (ExportHttpTransmissionType)HttpTransmissionTypeId; + } + set + { + HttpTransmissionTypeId = (int)value; + } + } + + /// + /// The file system path + /// + public string FileSystemPath { get; set; } + + /// + /// Path of a subfolder + /// + public string SubFolder { get; set; } + + /// + /// Multiple email addresses can be separated by commas + /// + public string EmailAddresses { get; set; } + + /// + /// Subject of the email + /// + public string EmailSubject { get; set; } + + /// + /// Identifier of the email account + /// + public int EmailAccountId { get; set; } + + /// + /// Whether to use FTP active or passive mode + /// + public bool PassiveMode { get; set; } + + /// + /// Whether to use SSL + /// + public bool UseSsl { get; set; } + + public virtual ExportProfile Profile { get; set; } + + public ExportDeployment Clone() + { + var deployment = new ExportDeployment + { + Name = this.Name, + Enabled = this.Enabled, + 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, + PassiveMode = this.PassiveMode, + UseSsl = this.UseSsl + }; + return deployment; + } + + object ICloneable.Clone() + { + return this.Clone(); + } + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs new file mode 100644 index 0000000000..6ea68ec73c --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportEnums.cs @@ -0,0 +1,146 @@ +using System; + +namespace SmartStore.Core.Domain.DataExchange +{ + /// + /// Supported entity types + /// + public enum ExportEntityType + { + Product = 0, + Category, + Manufacturer, + Customer, + Order, + NewsLetterSubscription + } + + /// + /// Supported deployment types + /// + public enum ExportDeploymentType + { + FileSystem = 0, + Email, + Http, + Ftp, + PublicFolder + } + + /// + /// Supported HTTP transmission types + /// + public enum ExportHttpTransmissionType + { + SimplePost = 0, + MultipartFormDataPost + } + + /// + /// Controls the merging of various data as product description + /// + public enum ExportDescriptionMerging + { + None = 0, + ShortDescriptionOrNameIfEmpty, + ShortDescription, + Description, + NameAndShortDescription, + NameAndDescription, + ManufacturerAndNameAndShortDescription, + ManufacturerAndNameAndDescription + } + + /// + /// Controls the merging of various data while exporting attribute combinations as products + /// + public enum ExportAttributeValueMerging + { + None = 0, + AppendAllValuesToName + } + + /// + /// Controls data processing and projection items supported by an export provider + /// + [Flags] + public enum ExportFeatures + { + None = 0, + + /// + /// Whether to automatically create a file based public deployment when an export profile is created + /// + CreatesInitialPublicDeployment = 1, + + /// + /// Whether to offer option to include\exclude grouped products + /// + CanOmitGroupedProducts = 1 << 2, + + /// + /// Whether to offer option to export attribute combinations as products + /// + CanProjectAttributeCombinations = 1 << 3, + + /// + /// Whether to offer further options to manipulate the product description + /// + CanProjectDescription = 1 << 4, + + /// + /// Whether to offer option to enter a brand fallback + /// + OffersBrandFallback = 1 << 5, + + /// + /// Whether to offer option to set a picture size and to get the URL of the main image + /// + CanIncludeMainPicture = 1 << 6, + + /// + /// Whether to use SKU as manufacturer part number if MPN is empty + /// + UsesSkuAsMpnFallback = 1 << 7, + + /// + /// Whether to offer option to enter a shipping time fallback + /// + OffersShippingTimeFallback = 1 << 8, + + /// + /// Whether to offer option to enter a shipping costs fallback and a free shipping threshold + /// + OffersShippingCostsFallback = 1 << 9, + + /// + /// Whether to get the calculated old product price + /// + 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 provide additional data of attribute combinations + /// + UsesAttributeCombination = 1 << 13 + } + + /// + /// Possible order status change after order exporting + /// + public enum ExportOrderStatusChange + { + None = 0, + Processing, + Complete + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportFilter.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportFilter.cs new file mode 100644 index 0000000000..23d08fd03c --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportFilter.cs @@ -0,0 +1,175 @@ +using System; +using SmartStore.Core.Domain.Catalog; + +namespace SmartStore.Core.Domain.DataExchange +{ + [Serializable] + public class ExportFilter + { + /// + /// Store identifier; 0 to load all records + /// + public int StoreId { get; set; } + + /// + /// Entity created from + /// + public DateTime? CreatedFrom { get; set; } + + /// + /// Entity created to + /// + public DateTime? CreatedTo { get; set; } + + #region Product + + /// + /// Minimum product identifier + /// + public int? IdMinimum { get; set; } + + /// + /// Maximum product identifier + /// + public int? IdMaximum { get; set; } + + /// + /// Minimum price + /// + public decimal? PriceMinimum { get; set; } + + /// + /// Maximum price + /// + public decimal? PriceMaximum { get; set; } + + /// + /// Minimum product availability + /// + public int? AvailabilityMinimum { get; set; } + + /// + /// Maximum product availability + /// + public int? AvailabilityMaximum { get; set; } + + /// + /// A value indicating whether to load only published or non published products + /// + public bool? IsPublished { get; set; } + + /// + /// Category identifiers + /// + public int[] CategoryIds { get; set; } + + /// + /// A value indicating whether to load products without any catgory mapping + /// + public bool? WithoutCategories { get; set; } + + /// + /// Manufacturer identifier + /// + public int? ManufacturerId { get; set; } + + /// + /// A value indicating whether to load products without any manufacturer mapping + /// + public bool? WithoutManufacturers { get; set; } + + /// + /// Identifiers of product tag + /// + public int? ProductTagId { get; set; } + + /// + /// A value indicating whether to load products that are marked as featured (relates only to categories and manufacturers) + /// + public bool? FeaturedProducts { get; set; } + + /// + /// Filter by product type + /// + public ProductType? ProductType { get; set; } + + #endregion + + #region Order + + /// + /// Filter by order status + /// + public int[] OrderStatusIds { get; set; } + + /// + /// Filter by payment status + /// + public int[] PaymentStatusIds { get; set; } + + /// + /// Filter by shipping status + /// + 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 new file mode 100644 index 0000000000..3161d1ea94 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProfile.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Domain.Tasks; + +namespace SmartStore.Core.Domain +{ + public class ExportProfile : BaseEntity, ICloneable + { + private ICollection _deployments; + + public ExportProfile() + { + Enabled = true; + PerStore = true; + Cleanup = true; + EmailAccountId = 0; + } + + /// + /// The name of the profile + /// + public string Name { get; set; } + + /// + /// The root path of the export folder + /// + public string FolderName { get; set; } + + /// + /// The pattern for file names + /// + 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 + /// + public bool Enabled { get; set; } + + /// + /// The scheduling task identifier + /// + public int SchedulingTaskId { get; set; } + + /// + /// XML with filtering information + /// + public string Filtering { get; set; } + + /// + /// XML with projection information + /// + public string Projection { get; set; } + + /// + /// XML with provider specific configuration data + /// + public string ProviderConfigData { get; set; } + + /// + /// XML with information about the last export + /// + public string ResultInfo { get; set; } + + /// + /// The number of records to be skipped + /// + public int Offset { get; set; } + + /// + /// How many records to be loaded per database round-trip + /// + public int Limit { get; set; } + + /// + /// The maximum number of records of one processed batch + /// + public int BatchSize { get; set; } + + /// + /// Whether to start a separate run-through for each store + /// + public bool PerStore { get; set; } + + /// + /// Email Account identifier used to send a notification message an export completes + /// + public int EmailAccountId { get; set; } + + /// + /// Email addresses where to send the notification message + /// + public string CompletedEmailAddresses { get; set; } + + /// + /// Whether to combine and compress the export files in a ZIP archive + /// + public bool CreateZipArchive { get; set; } + + /// + /// Whether to delete unneeded files after deployment + /// + public bool Cleanup { get; set; } + + + /// + /// The scheduling task + /// + public virtual ScheduleTask ScheduleTask { get; set; } + + /// + /// Gets or sets export deployments + /// + public virtual ICollection Deployments + { + get { return _deployments ?? (_deployments = new HashSet()); } + set { _deployments = value; } + } + + public ExportProfile Clone() + { + var profile = new ExportProfile + { + Name = this.Name, + FolderName = null, + FileNamePattern = this.FileNamePattern, + ProviderSystemName = this.ProviderSystemName, + Enabled = this.Enabled, + SchedulingTaskId = 0, + Filtering = this.Filtering, + Projection = this.Projection, + ProviderConfigData = this.ProviderConfigData, + ResultInfo = null, + Offset = this.Offset, + Limit = this.Limit, + BatchSize = this.BatchSize, + PerStore = this.PerStore, + EmailAccountId = this.EmailAccountId, + CompletedEmailAddresses = this.CompletedEmailAddresses, + CreateZipArchive = this.CreateZipArchive, + Cleanup = this.Cleanup + }; + return profile; + } + + object ICloneable.Clone() + { + return this.Clone(); + } + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProjection.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProjection.cs new file mode 100644 index 0000000000..493acda473 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/ExportProjection.cs @@ -0,0 +1,181 @@ +using System; +using System.Xml.Serialization; +using SmartStore.Core.Domain.Catalog; + +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 + /// + [Serializable] + public class ExportProjection + { + #region All entity types + + /// + /// Store identifier + /// + public int? StoreId { get; set; } + + /// + /// The language to be applied to the export + /// + public int? LanguageId { get; set; } + + /// + /// The currency to be applied to the export + /// + public int? CurrencyId { get; set; } + + /// + /// Customer identifier + /// + public int? CustomerId { get; set; } + + #endregion + + #region Product + + /// + /// Description merging identifier + /// + public int DescriptionMergingId { get; set; } + + /// + /// Decription merging + /// + [XmlIgnore] + public ExportDescriptionMerging DescriptionMerging + { + get + { + return (ExportDescriptionMerging)DescriptionMergingId; + } + set + { + DescriptionMergingId = (int)value; + } + } + + /// + /// Convert HTML decription to plain text + /// + public bool DescriptionToPlainText { get; set; } + + /// + /// Comma separated text to append to the decription + /// + public string AppendDescriptionText { get; set; } + + /// + /// Remove critical characters from the description + /// + public bool RemoveCriticalCharacters { get; set; } + + /// + /// Comma separated list of critical characters + /// + public string CriticalCharacters { get; set; } + + /// + /// The price type for calculating the product price + /// + public PriceDisplayType? PriceType { get; set; } + + /// + /// Convert net to gross prices + /// + public bool ConvertNetToGrossPrices { get; set; } + + /// + /// Fallback for product brand + /// + public string Brand { get; set; } + + /// + /// Number of images per object to be exported + /// + public int? NumberOfPictures { get; set; } + + /// + /// Picture size + /// + public int PictureSize { get; set; } + + /// + /// Fallback for shipping time + /// + public string ShippingTime { get; set; } + + /// + /// Fallback for shipping costs + /// + public decimal? ShippingCosts { get; set; } + + /// + /// Free shipping threshold + /// + public decimal? FreeShippingThreshold { get; set; } + + /// + /// Whether to export attribute combinations as products + /// + public bool AttributeCombinationAsProduct { get; set; } + + /// + /// Identifier for merging attribute values of attribute combinations + /// + public int AttributeCombinationValueMergingId { get; set; } + + /// + /// Merging attribute values of attribute combinations + /// + [XmlIgnore] + public ExportAttributeValueMerging AttributeCombinationValueMerging + { + get + { + return (ExportAttributeValueMerging)AttributeCombinationValueMergingId; + } + set + { + AttributeCombinationValueMergingId = (int)value; + } + } + + /// + /// Whether to export grouped products + /// + public bool NoGroupedProducts { get; set; } + + #endregion + + #region Order + + /// + /// Identifier of the new state for orders + /// + public int OrderStatusChangeId { get; set; } + + /// + /// New state for orders + /// + [XmlIgnore] + public ExportOrderStatusChange OrderStatusChange + { + get + { + return (ExportOrderStatusChange)OrderStatusChangeId; + } + set + { + OrderStatusChangeId = (int)value; + } + } + + #endregion + } +} 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/DataExchange/SyncMapping.cs b/src/Libraries/SmartStore.Core/Domain/DataExchange/SyncMapping.cs new file mode 100644 index 0000000000..a297bc8f85 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/DataExchange/SyncMapping.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace SmartStore.Core.Domain.DataExchange +{ + /// + /// Holds info about a synchronization operation with an external system + /// + [DataContract] + public partial class SyncMapping : BaseEntity + { + public SyncMapping() + { + this.SyncedOnUtc = DateTime.UtcNow; + } + + /// + /// Gets or sets the entity identifier in SmartStore + /// + [Index("IX_SyncMapping_ByEntity", 0, IsUnique = true)] + [DataMember] + public int EntityId { get; set; } + + /// + /// Gets or sets the entity's key in the external application + /// + [Index("IX_SyncMapping_BySource", 0, IsUnique = true)] + [DataMember] + public string SourceKey { get; set; } + + /// + /// Gets or sets a name representing the entity type + /// + [Index("IX_SyncMapping_ByEntity", 1, IsUnique = true)] + [Index("IX_SyncMapping_BySource", 1, IsUnique = true)] + [DataMember] + public string EntityName { get; set; } + + /// + /// Gets or sets a name for the external application + /// + [Index("IX_SyncMapping_ByEntity", 2, IsUnique = true)] + [Index("IX_SyncMapping_BySource", 2, IsUnique = true)] + [DataMember] + public string ContextName { get; set; } + + /// + /// Gets or sets an optional content hash reflecting the source model at the time of last sync + /// + [DataMember] + public string SourceHash { get; set; } + + /// + /// Gets or sets a custom integer value + /// + [DataMember] + public int? CustomInt { get; set; } + + /// + /// Gets or sets a custom string value + /// + [DataMember] + public string CustomString { get; set; } + + /// + /// Gets or sets a custom bool value + /// + [DataMember] + public bool? CustomBool { get; set; } + + /// + /// Gets or sets the date of the last sync operation + /// + [DataMember] + public DateTime SyncedOnUtc { get; set; } + + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/Directory/CurrencySettings.cs b/src/Libraries/SmartStore.Core/Domain/Directory/CurrencySettings.cs index d1c807b4a6..371d202799 100644 --- a/src/Libraries/SmartStore.Core/Domain/Directory/CurrencySettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Directory/CurrencySettings.cs @@ -1,12 +1,9 @@ - -using SmartStore.Core.Configuration; +using SmartStore.Core.Configuration; namespace SmartStore.Core.Domain.Directory { public class CurrencySettings : ISettings { - public int PrimaryStoreCurrencyId { get; set; } - public int PrimaryExchangeRateCurrencyId { get; set; } public string ActiveExchangeRateProviderSystemName { get; set; } public bool AutoUpdateEnabled { get; set; } public long LastUpdateTime { get; set; } diff --git a/src/Libraries/SmartStore.Core/Domain/Localization/LocalizationSettings.cs b/src/Libraries/SmartStore.Core/Domain/Localization/LocalizationSettings.cs index 4cd7889fee..b02a4ad2a2 100644 --- a/src/Libraries/SmartStore.Core/Domain/Localization/LocalizationSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Localization/LocalizationSettings.cs @@ -9,6 +9,7 @@ public LocalizationSettings() UseImagesForLanguageSelection = true; DefaultLanguageRedirectBehaviour = DefaultLanguageRedirectBehaviour.StripSeoCode; InvalidLanguageRedirectBehaviour = InvalidLanguageRedirectBehaviour.ReturnHttp404; + LoadAllLocalizedPropertiesOnStartup = true; } /// @@ -26,6 +27,11 @@ public LocalizationSettings() /// public bool LoadAllLocaleRecordsOnStartup { get; set; } + /// + /// A value indicating whether to load all localized entity properties on application startup + /// + public bool LoadAllLocalizedPropertiesOnStartup { get; set; } + /// /// A value indicating whether the browser user lannguage should be detected /// 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/Download.cs b/src/Libraries/SmartStore.Core/Domain/Media/Download.cs index 933790f578..0082cbfc67 100644 --- a/src/Libraries/SmartStore.Core/Domain/Media/Download.cs +++ b/src/Libraries/SmartStore.Core/Domain/Media/Download.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel.DataAnnotations.Schema; using System.Runtime.Serialization; namespace SmartStore.Core.Domain.Media @@ -7,12 +8,18 @@ namespace SmartStore.Core.Domain.Media /// Represents a download /// [DataContract] - public partial class Download : BaseEntity + public partial class Download : BaseEntity, ITransient { - /// + public Download() + { + this.UpdatedOnUtc = DateTime.UtcNow; + } + + /// /// Gets or sets a GUID /// [DataMember] + [Index] public Guid DownloadGuid { get; set; } /// @@ -55,5 +62,19 @@ public partial class Download : BaseEntity /// [DataMember] public bool IsNew { get; set; } + + /// + /// Gets or sets a value indicating whether the entity transient/preliminary + /// + [DataMember] + [Index("IX_UpdatedOn_IsTransient", 1)] + public bool IsTransient { get; set; } + + /// + /// Gets or sets the date and time of instance update + /// + [DataMember] + [Index("IX_UpdatedOn_IsTransient", 0)] + public DateTime UpdatedOnUtc { 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/Picture.cs b/src/Libraries/SmartStore.Core/Domain/Media/Picture.cs index 45d8e80069..f527b7d693 100644 --- a/src/Libraries/SmartStore.Core/Domain/Media/Picture.cs +++ b/src/Libraries/SmartStore.Core/Domain/Media/Picture.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using SmartStore.Core.Domain.Catalog; using System.Runtime.Serialization; +using System; +using System.ComponentModel.DataAnnotations.Schema; namespace SmartStore.Core.Domain.Media { @@ -8,9 +10,14 @@ namespace SmartStore.Core.Domain.Media /// Represents a picture /// [DataContract] - public partial class Picture : BaseEntity + public partial class Picture : BaseEntity, ITransient { - private ICollection _productPictures; + public Picture() + { + this.UpdatedOnUtc = DateTime.UtcNow; + } + + private ICollection _productPictures; /// /// Gets or sets the picture binary /// @@ -34,6 +41,20 @@ public partial class Picture : BaseEntity [DataMember] public bool IsNew { get; set; } + /// + /// Gets or sets a value indicating whether the entity transient/preliminary + /// + [DataMember] + [Index("IX_UpdatedOn_IsTransient", 1)] + public bool IsTransient { get; set; } + + /// + /// Gets or sets the date and time of instance update + /// + [DataMember] + [Index("IX_UpdatedOn_IsTransient", 0)] + public DateTime UpdatedOnUtc { get; set; } + /// /// Gets or sets the product pictures /// 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/EmailAccount.cs b/src/Libraries/SmartStore.Core/Domain/Messages/EmailAccount.cs index decde80336..859a7bd74f 100644 --- a/src/Libraries/SmartStore.Core/Domain/Messages/EmailAccount.cs +++ b/src/Libraries/SmartStore.Core/Domain/Messages/EmailAccount.cs @@ -54,9 +54,10 @@ public string FriendlyName { get { - if (!String.IsNullOrWhiteSpace(this.DisplayName)) - return this.Email + " (" + this.DisplayName + ")"; - return this.Email; + if (DisplayName.IsEmpty()) + return Email; + + return "{0} ({1})".FormatInvariant(DisplayName, Email); } } diff --git a/src/Libraries/SmartStore.Core/Domain/Messages/EmailAttachmentStorageLocation.cs b/src/Libraries/SmartStore.Core/Domain/Messages/EmailAttachmentStorageLocation.cs new file mode 100644 index 0000000000..3dada46a59 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/Messages/EmailAttachmentStorageLocation.cs @@ -0,0 +1,22 @@ +using SmartStore.Core.Domain.Media; + +namespace SmartStore.Core.Domain.Messages +{ + public enum EmailAttachmentStorageLocation + { + /// + /// Attachment is embedded as Blob + /// + Blob, + + /// + /// Attachment is a reference to + /// + FileReference, + + /// + /// Attachment is located on disk (physical or virtual path) + /// + Path + } +} diff --git a/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplate.cs b/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplate.cs index 282fc0407d..75f66564ae 100644 --- a/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplate.cs +++ b/src/Libraries/SmartStore.Core/Domain/Messages/MessageTemplate.cs @@ -1,4 +1,5 @@ using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Media; using SmartStore.Core.Domain.Stores; namespace SmartStore.Core.Domain.Messages @@ -47,5 +48,20 @@ public partial class MessageTemplate : BaseEntity, ILocalizedEntity, IStoreMappi /// Gets or sets a value indicating whether emails derived from the template are only send manually /// public bool SendManually { get; set; } + + /// + /// Gets or sets the attachment 1 file identifier + /// + public int? Attachment1FileId { get; set; } + + /// + /// Gets or sets the attachment 2 file identifier + /// + public int? Attachment2FileId { get; set; } + + /// + /// Gets or sets the attachment 3 file identifier + /// + public int? Attachment3FileId { get; set; } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Messages/QueuedEmail.cs b/src/Libraries/SmartStore.Core/Domain/Messages/QueuedEmail.cs index 573231b382..d3f7e63b93 100644 --- a/src/Libraries/SmartStore.Core/Domain/Messages/QueuedEmail.cs +++ b/src/Libraries/SmartStore.Core/Domain/Messages/QueuedEmail.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace SmartStore.Core.Domain.Messages { @@ -7,6 +8,8 @@ namespace SmartStore.Core.Domain.Messages /// public partial class QueuedEmail : BaseEntity { + private ICollection _attachments; + /// /// Gets or sets the priority /// @@ -91,5 +94,14 @@ public partial class QueuedEmail : BaseEntity /// Gets the email account /// public virtual EmailAccount EmailAccount { get; set; } + + /// + /// Gets or sets the collection of attachments + /// + public virtual ICollection Attachments + { + get { return _attachments ?? (_attachments = new HashSet()); } + protected set { _attachments = value; } + } } } diff --git a/src/Libraries/SmartStore.Core/Domain/Messages/QueuedEmailAttachment.cs b/src/Libraries/SmartStore.Core/Domain/Messages/QueuedEmailAttachment.cs new file mode 100644 index 0000000000..cd6d436139 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/Messages/QueuedEmailAttachment.cs @@ -0,0 +1,61 @@ +using SmartStore.Core.Domain.Media; + +namespace SmartStore.Core.Domain.Messages +{ + /// + /// Reperesents an e-mail attachment + /// + public partial class QueuedEmailAttachment : BaseEntity + { + + /// + /// Gets or sets the queued email identifier + /// + public int QueuedEmailId { get; set; } + + /// + /// Gets or sets the queued email entity instance + /// + public virtual QueuedEmail QueuedEmail { get; set; } + + /// + /// Gets or sets the storage location + /// + public EmailAttachmentStorageLocation StorageLocation { get; set; } + + /// + /// A physical or virtual path to the file (only applicable if location is Path) + /// + public string Path { get; set; } + + /// + /// The id of a record (only applicable if location is FileReference) + /// + public int? FileId { get; set; } + + /// + /// Gets the file object + /// + /// + /// This property is not named Download on purpose, because we're going to rename Download to File in a future release. + /// + public virtual Download File { get; set; } + + /// + /// The attachment's binary data (only applicable if location is Blob) + /// + public byte[] Data { get; set; } + + /// + /// The attachment file name (without path) + /// + public string Name { get; set; } + + /// + /// The attachment file's mime type, e.g. application/pdf + /// + public string MimeType { get; set; } + + } + +} diff --git a/src/Libraries/SmartStore.Core/Domain/News/NewsSettings.cs b/src/Libraries/SmartStore.Core/Domain/News/NewsSettings.cs index 479b3c5c9b..ffceb3edab 100644 --- a/src/Libraries/SmartStore.Core/Domain/News/NewsSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/News/NewsSettings.cs @@ -1,5 +1,4 @@ - -using SmartStore.Core.Configuration; +using SmartStore.Core.Configuration; namespace SmartStore.Core.Domain.News { @@ -12,6 +11,7 @@ public NewsSettings() ShowNewsOnMainPage = true; MainPageNewsCount = 3; NewsArchivePageSize = 10; + MaxAgeInDays = 180; } /// @@ -44,6 +44,11 @@ public NewsSettings() /// public int NewsArchivePageSize { get; set; } + /// + /// The maximum age of news (in days) for RSS feed + /// + public int MaxAgeInDays { get; set; } + /// /// Enable the news RSS feed link in customers browser address bar /// 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 new file mode 100644 index 0000000000..e078434cc6 --- /dev/null +++ b/src/Libraries/SmartStore.Core/Domain/Payments/PaymentMethod.cs @@ -0,0 +1,24 @@ +using System.Runtime.Serialization; +using SmartStore.Core.Domain.Localization; + +namespace SmartStore.Core.Domain.Payments +{ + /// + /// Represents a payment method + /// + [DataContract] + public partial class PaymentMethod : BaseEntity, ILocalizedEntity + { + /// + /// Gets or sets the payment method system name + /// + [DataMember] + public string PaymentMethodSystemName { get; set; } + + /// + /// Gets or sets the full description + /// + [DataMember] + public string FullDescription { get; set; } + } +} 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 6dacb605a6..1f06b5aba6 100644 --- a/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs +++ b/src/Libraries/SmartStore.Core/Domain/Seo/SeoSettings.cs @@ -1,34 +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(); + 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; } /// @@ -37,5 +83,10 @@ public SeoSettings() public List ReservedUrlRecordSlugs { get; set; } public List ExtraRobotsDisallows { get; set; } + + /// + /// A value indicating whether to load all URL records and active slugs on application startup + /// + public bool LoadAllUrlAliasesOnStartup { get; set; } } } \ No newline at end of file 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 527dbbb655..078b12a20c 100644 --- a/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingMethod.cs +++ b/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingMethod.cs @@ -5,10 +5,10 @@ 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; diff --git a/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingOption.cs b/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingOption.cs index d059f630b7..5edb06f415 100644 --- a/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingOption.cs +++ b/src/Libraries/SmartStore.Core/Domain/Shipping/ShippingOption.cs @@ -13,6 +13,11 @@ namespace SmartStore.Core.Domain.Shipping /// public partial class ShippingOption { + /// + /// Shipping method identifier + /// + public int ShippingMethodId { get; set; } + /// /// Gets or sets the system name of shipping rate computation method /// @@ -33,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/Store.cs b/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs index 0b5c7e30b2..0d9fe6be96 100644 --- a/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs +++ b/src/Libraries/SmartStore.Core/Domain/Stores/Store.cs @@ -1,4 +1,5 @@ using System.Runtime.Serialization; +using SmartStore.Core.Domain.Directory; namespace SmartStore.Core.Domain.Stores { @@ -59,8 +60,34 @@ public partial class Store : BaseEntity /// /// Gets or sets the CDN host name, if static media content should be served through a CDN. /// + [DataMember] public string ContentDeliveryNetwork { get; set; } + /// + /// Gets or sets the primary store currency identifier + /// + [DataMember] + public int PrimaryStoreCurrencyId { get; set; } + + /// + /// Gets or sets the primary exchange rate currency identifier + /// + [DataMember] + public int PrimaryExchangeRateCurrencyId { get; set; } + + /// + /// Gets or sets the primary store currency + /// + [DataMember] + public virtual Currency PrimaryStoreCurrency { get; set; } + + /// + /// Gets or sets the primary exchange rate currency + /// + [DataMember] + public virtual Currency PrimaryExchangeRateCurrency { get; set; } + + /// /// Gets the security mode for the store /// 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/Tasks/ScheduleTask.cs b/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs index c04458e8de..5806d83c4f 100644 --- a/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs +++ b/src/Libraries/SmartStore.Core/Domain/Tasks/ScheduleTask.cs @@ -1,9 +1,13 @@  using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Diagnostics; namespace SmartStore.Core.Domain.Tasks { - public class ScheduleTask : BaseEntity + [DebuggerDisplay("{Name} (Type: {Type})")] + public class ScheduleTask : BaseEntity, ICloneable { /// /// Gets or sets the name @@ -11,18 +15,25 @@ public class ScheduleTask : BaseEntity public string Name { get; set; } /// - /// Gets or sets the run period (in seconds) + /// Gets or sets the task alias (an optional key for advanced customization) /// - public int Seconds { get; set; } + public string Alias { get; set; } + + /// + /// Gets or sets the CRON expression used to calculate future schedules + /// + public string CronExpression { get; set; } /// /// Gets or sets the type of appropriate ITask class /// + [Index("IX_Type")] public string Type { get; set; } /// /// Gets or sets the value indicating whether a task is enabled /// + [Index("IX_NextRun_Enabled", 1)] public bool Enabled { get; set; } /// @@ -30,25 +41,69 @@ public class ScheduleTask : BaseEntity /// public bool StopOnError { get; set; } + [Index("IX_NextRun_Enabled", 0)] + public DateTime? NextRunUtc { get; set; } + + [Index("IX_LastStart_LastEnd", 0)] public DateTime? LastStartUtc { get; set; } + [Index("IX_LastStart_LastEnd", 1)] public DateTime? LastEndUtc { get; set; } public DateTime? LastSuccessUtc { get; set; } public string LastError { get; set; } + public bool IsHidden { get; set; } + + /// + /// Gets or sets a value indicating the current percentual progress for a running task + /// + public int? ProgressPercent { get; set; } + + /// + /// Gets or sets the current progress message for a running task + /// + public string ProgressMessage { get; set; } + + /// + /// Concurrency Token + /// + [Timestamp] + public byte[] RowVersion { get; set; } + /// - /// Gets the value indicating whether a task is running + /// Gets a value indicating whether a task is running /// public bool IsRunning { get { - var result = (LastStartUtc.HasValue && LastStartUtc.Value > LastEndUtc.GetValueOrDefault()); + var result = LastStartUtc.HasValue && LastStartUtc.Value > LastEndUtc.GetValueOrDefault(); + return result; + } + } + /// + /// Gets a value indicating whether a task is scheduled for execution (Enabled = true and NextRunUtc <= UtcNow ) + /// + public bool IsPending + { + get + { + var result = Enabled && NextRunUtc.HasValue && NextRunUtc <= DateTime.UtcNow; return result; } } - } + + public ScheduleTask Clone() + { + return (ScheduleTask)this.MemberwiseClone(); + } + + object ICloneable.Clone() + { + return this.MemberwiseClone(); + } + } } 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/Attachment.cs b/src/Libraries/SmartStore.Core/Email/Attachment.cs deleted file mode 100644 index 437848f0ac..0000000000 --- a/src/Libraries/SmartStore.Core/Email/Attachment.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System; -using System.Text; -using System.IO; -using System.Net.Mime; -using net = System.Net.Mail; - -namespace SmartStore.Core.Email -{ - /// - /// attachment-multipart - /// - public class Attachment - { - //private readonly net.Attachment _attachment; - private net.Attachment _attachment; - - public Attachment() - { - MemoryStream stream = new MemoryStream(Encoding.ASCII.GetBytes("")); - this.Stream = stream; - - _attachment = new net.Attachment(this.Stream, ""); - } - - public Attachment(string filePath) - { - _attachment = new net.Attachment(filePath); - this.GetContentFromFile(filePath); - } - - public Attachment(string filePath, string mediaType) - { - _attachment = new net.Attachment(filePath, mediaType); - } - - public ContentType ContentType - { - get - { - return _attachment.ContentType; - } - set - { - _attachment.ContentType = value; - } - } - public string Name - { - get - { - return _attachment.Name; - } - set - { - _attachment.Name = value; - } - } - public string MediaType - { - get - { - return _attachment.ContentType.MediaType; - } - set - { - _attachment.ContentType.MediaType = value; - } - } - public ContentDisposition ContentDisposition - { - get - { - return _attachment.ContentDisposition; - } - } - public TransferEncoding ContentTransferEncoding - { - get - { - return _attachment.TransferEncoding; - } - set - { - _attachment.TransferEncoding = value; - } - } - public string ContentDescription { get; set; } - public Stream Stream { get; set; } - //public string FileName { get; set; } - - public void GetContentFromFile(string location) - { - byte[] buffer; - FileStream fileStream = new FileStream(location, FileMode.Open, FileAccess.Read); - - int length = (int)fileStream.Length; - buffer = new byte[length]; - int count; - int sum = 0; - - while ((count = fileStream.Read(buffer, sum, length - sum)) > 0) - sum += count; - - this.Stream = new MemoryStream(buffer); - - string extension = Path.GetExtension(location); - string filename = Path.GetFileName(location); - - switch(extension) - { - case ".txt": - this.MediaType = "text/plain"; - break; - case ".jpg": - this.MediaType = "image/jpeg"; - break; - default: - this.MediaType = "application/octet-stream"; - break; - } - - this.Name = filename; - this.ContentTransferEncoding = TransferEncoding.Base64; - this.ContentDisposition.FileName = filename; - this.ContentDisposition.DispositionType = "attachment"; - } - - public void GetContentFromString(string content) - { - this.Stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); - //this.Stream = new MemoryStream(Convert.FromBase64String(content)); - } - - public void GetContentFromBase64String(string content) - { - this.Stream = new MemoryStream(Convert.FromBase64String(content)); - } - - //NEW METHODS TO WRAP ATTACHMENT - //TODO: Testen - public net.Attachment Instance - { - get - { - return _attachment; - } - set - { - _attachment = value; - } - } - - public Attachment CreateAttachmentFromString(string content, ContentType contentType) - { - this.Instance = net.Attachment.CreateAttachmentFromString(content, contentType); - return InitAttachment(_attachment); - } - - public Attachment CreateAttachmentFromString(string content, string name) - { - this.Instance = net.Attachment.CreateAttachmentFromString(content, name); - return InitAttachment(_attachment); - } - - public Attachment CreateAttachmentFromString(string content, string name, Encoding contentEncoding, string mediaType) - { - this.Instance = net.Attachment.CreateAttachmentFromString(content, name, contentEncoding, mediaType); - return InitAttachment(_attachment); - } - - public Attachment InitAttachment(net.Attachment tempAttachment) - { - this.Stream = tempAttachment.ContentStream; - this.Name = tempAttachment.Name; - this.ContentTransferEncoding = tempAttachment.TransferEncoding; - this.ContentType = tempAttachment.ContentType; - this.MediaType = tempAttachment.ContentType.MediaType; - - return this; - } - - } -} \ No newline at end of file diff --git a/src/Libraries/SmartStore.Core/Email/DefaultEmailSender.cs b/src/Libraries/SmartStore.Core/Email/DefaultEmailSender.cs index 9e5ebd2543..b9862e0c52 100644 --- a/src/Libraries/SmartStore.Core/Email/DefaultEmailSender.cs +++ b/src/Libraries/SmartStore.Core/Email/DefaultEmailSender.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using net = System.Net.Mail; +using System.Net.Mail; using System.Net.Mime; using System.Net; using System.IO; @@ -14,18 +14,14 @@ namespace SmartStore.Core.Email public class DefaultEmailSender : IEmailSender { - public DefaultEmailSender() - { - } - /// /// Builds System.Net.Mail.Message /// /// SmartStore.Email.Message /// System.Net.Mail.Message - protected virtual net.MailMessage BuildMailMessage(EmailMessage original) + protected virtual MailMessage BuildMailMessage(EmailMessage original) { - net.MailMessage msg = new net.MailMessage(); + MailMessage msg = new MailMessage(); if (String.IsNullOrEmpty(original.Subject)) { @@ -37,15 +33,15 @@ protected virtual net.MailMessage BuildMailMessage(EmailMessage original) if (original.AltText.HasValue()) { - msg.AlternateViews.Add(net.AlternateView.CreateAlternateViewFromString(original.AltText, new ContentType("text/html"))); - msg.AlternateViews.Add(net.AlternateView.CreateAlternateViewFromString(original.Body, new ContentType("text/plain"))); + msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(original.AltText, new ContentType("text/html"))); + msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(original.Body, new ContentType("text/plain"))); } else { msg.Body = original.Body; } - msg.DeliveryNotificationOptions = net.DeliveryNotificationOptions.None; + msg.DeliveryNotificationOptions = DeliveryNotificationOptions.None; msg.From = original.From.ToMailAddress(); @@ -54,31 +50,7 @@ protected virtual net.MailMessage BuildMailMessage(EmailMessage original) msg.Bcc.AddRange(original.Bcc.Where(x => x.Address.HasValue()).Select(x => x.ToMailAddress())); msg.ReplyToList.AddRange(original.ReplyTo.Where(x => x.Address.HasValue()).Select(x => x.ToMailAddress())); - foreach (Attachment attachment in original.Attachments) - { - byte[] byteData; - - if (attachment.ContentTransferEncoding == TransferEncoding.Base64) - { - using (var sr = new StreamReader(attachment.Stream)) - { - byteData = Convert.FromBase64String(sr.ReadToEnd()); - } - } - else - { - byteData = attachment.Stream.ToByteArray(); - } - - MemoryStream s = new MemoryStream(byteData); - net.Attachment att = new net.Attachment(s, attachment.Name, attachment.ContentType.MediaType); - - att.ContentType.MediaType = attachment.MediaType; - att.TransferEncoding = attachment.ContentTransferEncoding; - att.ContentDisposition.DispositionType = attachment.ContentDisposition.DispositionType; - - msg.Attachments.Add(att); - } + msg.Attachments.AddRange(original.Attachments); if (original.Headers != null) msg.Headers.AddRange(original.Headers); @@ -96,11 +68,12 @@ public void SendEmail(SmtpContext context, EmailMessage message) Guard.ArgumentNotNull(() => context); Guard.ArgumentNotNull(() => message); - var msg = this.BuildMailMessage(message); - - using (var client = context.ToSmtpClient()) + using (var msg = this.BuildMailMessage(message)) { - client.Send(msg); + using (var client = context.ToSmtpClient()) + { + client.Send(msg); + } } } @@ -109,12 +82,14 @@ public Task SendEmailAsync(SmtpContext context, EmailMessage message) Guard.ArgumentNotNull(() => context); Guard.ArgumentNotNull(() => message); + var client = context.ToSmtpClient(); var msg = this.BuildMailMessage(message); - using (var client = context.ToSmtpClient()) + return client.SendMailAsync(msg).ContinueWith(t => { - return client.SendMailAsync(msg); - } + client.Dispose(); + msg.Dispose(); + }); } #endregion diff --git a/src/Libraries/SmartStore.Core/Email/EmailMessage.cs b/src/Libraries/SmartStore.Core/Email/EmailMessage.cs index 58a8508c07..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 { @@ -78,18 +72,13 @@ public EmailMessage(EmailAddress to, string subject, string body, EmailAddress f public NameValueCollection Headers { get; private set; } - public void AddAttachment(Attachment attachment) - { - this.Attachments.Add(attachment); - } - 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/Events/CommonMessages/AppInitScheduledTasksEvent.cs b/src/Libraries/SmartStore.Core/Events/CommonMessages/AppInitScheduledTasksEvent.cs index fb13ce342b..348b5fac89 100644 --- a/src/Libraries/SmartStore.Core/Events/CommonMessages/AppInitScheduledTasksEvent.cs +++ b/src/Libraries/SmartStore.Core/Events/CommonMessages/AppInitScheduledTasksEvent.cs @@ -9,7 +9,6 @@ namespace SmartStore.Core.Events /// /// to initialize scheduled tasks in Application_Start /// - /// codehint: sm-add public class AppInitScheduledTasksEvent { public IList ScheduledTasks { get; set; } diff --git a/src/Libraries/SmartStore.Core/Events/CommonMessages/AppRegisterGlobalFiltersEvent.cs b/src/Libraries/SmartStore.Core/Events/CommonMessages/AppRegisterGlobalFiltersEvent.cs index f7c0c63e4e..d825cc079d 100644 --- a/src/Libraries/SmartStore.Core/Events/CommonMessages/AppRegisterGlobalFiltersEvent.cs +++ b/src/Libraries/SmartStore.Core/Events/CommonMessages/AppRegisterGlobalFiltersEvent.cs @@ -7,7 +7,6 @@ namespace SmartStore.Core.Events /// /// to register global filters in Application_Start /// - /// codehint: sm-add public class AppRegisterGlobalFiltersEvent { public GlobalFilterCollection Filters { get; set; } 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 20ff82f9a2..a97d4862bf 100644 --- a/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/ConversionExtensions.cs @@ -1,209 +1,83 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.IO; -using System.Drawing; -using System.ComponentModel; -using System.Drawing.Imaging; +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 @@ -219,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; + Guard.ArgumentIsEnumType(typeof(T), "T"); - if (!string.IsNullOrEmpty(value)) - { - try - { - convertedValue = (T)Enum.Parse(typeof(T), value.Trim(), true); - } - catch (ArgumentException) - { - } - } - - return convertedValue; - } - - 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) @@ -452,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) { @@ -486,67 +218,49 @@ public static Version ToVersion(this string value, Version defaultVersion = null #endregion - #region DateTime - - // [...] - - #endregion - #region Stream public static byte[] ToByteArray(this Stream stream) { Guard.ArgumentNotNull(stream, "stream"); - byte[] buffer; - - if (stream is MemoryStream && stream.CanRead && stream.CanSeek) - { - int len = System.Convert.ToInt32(stream.Length); - buffer = new byte[len]; - stream.Read(buffer, 0, len); - return buffer; - } - - MemoryStream memStream = null; - try - { - buffer = new byte[1024]; - memStream = new MemoryStream(); - int bytesRead = stream.Read(buffer, 0, buffer.Length); - if (bytesRead > 0) - { - memStream.Write(buffer, 0, bytesRead); - bytesRead = stream.Read(buffer, 0, buffer.Length); - } - } - finally - { - if (memStream != null) - memStream.Close(); - } - - if (memStream != null) - { - return memStream.ToArray(); - } - - return null; + if (stream is MemoryStream) + { + return ((MemoryStream)stream).ToArray(); + } + else + { + using (var ms = new MemoryStream()) + { + stream.CopyTo(ms); + return ms.ToArray(); + } + } } - public static string AsString(this Stream stream) + public static string AsString(this Stream stream) + { + return stream.AsString(Encoding.UTF8); + } + + public static string AsString(this Stream stream, Encoding encoding) { - // convert memory stream to string + Guard.ArgumentNotNull(() => encoding); + + // convert stream to string string result; - stream.Position = 0; - using (StreamReader sr = new StreamReader(stream)) + if (stream.CanSeek) + { + stream.Position = 0; + } + + using (StreamReader sr = new StreamReader(stream, encoding)) { result = sr.ReadToEnd(); } return result; - } #endregion @@ -569,32 +283,18 @@ public static object ToObject(this byte[] bytes) } } - public static Image ToImage(this byte[] bytes) - { - using (var stream = new MemoryStream(bytes)) - { - return Image.FromStream(stream); - } - } - 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"); @@ -623,45 +323,6 @@ public static string Hash(this byte[] value, bool toBase64 = false) #endregion - #region Image/Bitmap - - public static byte[] ToByteArray(this Image image) - { - Guard.ArgumentNotNull(() => image); - - byte[] bytes; - - ImageConverter converter = new ImageConverter(); - bytes = (byte[])converter.ConvertTo(image, typeof(byte[])); - return bytes; - } - - internal static byte[] ToByteArray(this Image image, ImageFormat format) - { - Guard.ArgumentNotNull(() => image); - Guard.ArgumentNotNull(() => format); - - using (var stream = new MemoryStream()) - { - image.Save(stream, format); - return stream.ToByteArray(); - } - } - - internal static Image ConvertTo(this Image image, ImageFormat format) - { - Guard.ArgumentNotNull(() => image); - Guard.ArgumentNotNull(() => format); - - using (var stream = new MemoryStream()) - { - image.Save(stream, format); - return Image.FromStream(stream); - } - } - - #endregion - #region Enumerable: Collections/List/Dictionary... public static T ToObject(this IDictionary values) where T : class 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 cd3fe2ffd1..948cfddc6a 100644 --- a/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs @@ -1,33 +1,35 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Collections; -using System.Linq; -using System.Linq.Expressions; using System.Collections.Specialized; +using System.Linq; +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; } } } @@ -74,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) { @@ -97,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; @@ -107,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()) @@ -132,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 @@ -177,6 +230,7 @@ public static Multimap ToMultimap( public static void AddRange(this NameValueCollection initial, NameValueCollection other) { Guard.ArgumentNotNull(initial, "initial"); + if (other == null) return; @@ -186,63 +240,45 @@ public static void AddRange(this NameValueCollection initial, NameValueCollectio } } - #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(); - } + /// + /// Builds an URL query string + /// + /// Name value collection + /// Encoding type. Can be null. + /// Whether to encode keys and values + /// The query string without leading a question mark + public static string BuildQueryString(this NameValueCollection nvc, Encoding encoding, bool encode = true) + { + var sb = new StringBuilder(); - public IEnumerator GetEnumerator() - { - return this._collection.GetEnumerator(); - } + if (nvc != null) + { + foreach (string str in nvc) + { + if (sb.Length > 0) + sb.Append('&'); + + if (!encode) + sb.Append(str); + else if (encoding == null) + sb.Append(HttpUtility.UrlEncode(str)); + else + sb.Append(HttpUtility.UrlEncode(str, encoding)); + + sb.Append('='); + + if (!encode) + sb.Append(nvc[str]); + else if (encoding == null) + sb.Append(HttpUtility.UrlEncode(nvc[str])); + else + sb.Append(HttpUtility.UrlEncode(nvc[str], encoding)); + } + } - IEnumerator IEnumerable.GetEnumerator() - { - return ((IEnumerable)this._collection).GetEnumerator(); - } - } + return sb.ToString(); + } #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/ListExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/ListExtensions.cs deleted file mode 100644 index f966ee5456..0000000000 --- a/src/Libraries/SmartStore.Core/Extensions/ListExtensions.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace SmartStore -{ - - public static class ListExtensions - { - - public static string ToSeparatedString(this IList value) - { - return ToSeparatedString(value, ","); - } - - public static string ToSeparatedString(this IList value, string separator) - { - if (value.Count == 0) - { - return String.Empty; - } - if (value.Count == 1) - { - if (value[0] != null) - { - return value[0].ToString(); - } - return string.Empty; - } - - StringBuilder builder = new StringBuilder(); - bool flag = true; - bool flag2 = false; - foreach (object obj2 in value) - { - if (!flag) - { - builder.Append(separator); - } - if (obj2 != null) - { - builder.Append(obj2.ToString().TrimEnd(new char[0])); - flag2 = true; - } - flag = false; - } - if (!flag2) - { - return string.Empty; - } - return builder.ToString(); - } - - /// - /// Makes a slice of the specified list in between the start and end indexes. - /// - /// The list. - /// The start index. - /// The end index. - /// A slice of the list. - public static IList Slice(this IList list, int? start, int? end) - { - return list.Slice(start, end, null); - } - - /// - /// Makes a slice of the specified list in between the start and end indexes, - /// getting every so many items based upon the step. - /// - /// The list. - /// The start index. - /// The end index. - /// The step. - /// A slice of the list. - public static IList Slice(this IList list, int? start, int? end, int? step) - { - if (list == null) - throw new ArgumentNullException("list"); - - if (step == 0) - throw Error.Argument("step", "Step cannot be zero."); - - List slicedList = new List(); - - // nothing to slice - if (list.Count == 0) - return slicedList; - - // set defaults for null arguments - int s = step ?? 1; - int startIndex = start ?? 0; - int endIndex = end ?? list.Count; - - // start from the end of the list if start is negative - startIndex = (startIndex < 0) ? list.Count + startIndex : startIndex; - - // end from the start of the list if end is negative - endIndex = (endIndex < 0) ? list.Count + endIndex : endIndex; - - // ensure indexes keep within collection bounds - startIndex = Math.Max(startIndex, 0); - endIndex = Math.Min(endIndex, list.Count - 1); - - // loop between start and end indexes, incrementing by the step - for (int i = startIndex; i < endIndex; i += s) - { - slicedList.Add(list[i]); - } - - return slicedList; - } - - } - -} diff --git a/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs index da7f1483e3..033c5d4230 100644 --- a/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs +++ b/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs @@ -11,25 +11,27 @@ 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 { } } - public static string ToAllMessages(this Exception exc) + public static string ToAllMessages(this Exception exception) { var sb = new StringBuilder(); - - while (exc != null) + + while (exception != null) { - if (!sb.ToString().EmptyNull().Contains(exc.Message)) - sb.Grow(exc.Message, " "); - exc = exc.InnerException; + if (!sb.ToString().EmptyNull().Contains(exception.Message)) + { + sb.Grow(exception.Message, " * "); + } + exception = exception.InnerException; } return sb.ToString(); } @@ -44,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()); @@ -115,29 +71,17 @@ public static string ToHexString(this byte[] bytes, int length = 0) public static T GetMergedDataValue(this IMergedData mergedData, string key, T defaultValue) { - try + if (mergedData.MergedDataValues != null && !mergedData.MergedDataIgnore) { - if (mergedData.MergedDataValues != null && !mergedData.MergedDataIgnore) - { - object value; + object value; - if (mergedData.MergedDataValues.TryGetValue(key, out value)) - return (T)value; - } + if (mergedData.MergedDataValues.TryGetValue(key, out value)) + return (T)value; } - catch (Exception) { } 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 448f5ffe96..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); @@ -868,46 +778,7 @@ public static string Prettify(this string value, bool allowSpace = false, char[] public static string SanitizeHtmlId(this string value) { - if (string.IsNullOrWhiteSpace(value)) - { - return null; - } - StringBuilder builder = new StringBuilder(value.Length); - int index = value.IndexOf("#"); - int num2 = value.LastIndexOf("#"); - if (num2 > index) - { - ReplaceInvalidHtmlIdCharacters(value.Substring(0, index), builder); - builder.Append(value.Substring(index, (num2 - index) + 1)); - ReplaceInvalidHtmlIdCharacters(value.Substring(num2 + 1), builder); - } - else - { - ReplaceInvalidHtmlIdCharacters(value, builder); - } - return builder.ToString(); - } - - private static bool IsValidHtmlIdCharacter(char c) - { - bool invalid = (c == '?' || c == '!' || c == '#' || c == '.' || c == ' ' || c == ';' || c == ':'); - return !invalid; - } - - private static void ReplaceInvalidHtmlIdCharacters(string part, StringBuilder builder) - { - for (int i = 0; i < part.Length; i++) - { - char c = part[i]; - if (IsValidHtmlIdCharacter(c)) - { - builder.Append(c); - } - else - { - builder.Append('_'); - } - } + return System.Web.Mvc.TagBuilder.CreateSanitizedId(value); } public static string Sha(this string value, Encoding encoding) @@ -919,7 +790,6 @@ public static string Sha(this string value, Encoding encoding) byte[] data = encoding.GetBytes(value); return sha1.ComputeHash(data).ToHexString(); - //return BitConverter.ToString(sha1.ComputeHash(data)).Replace("-", ""); } } return ""; @@ -1003,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 @@ -1080,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 fd92e44f83..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/IPageable.cs b/src/Libraries/SmartStore.Core/IPageable.cs index 4751bc51c0..60d6b3f63f 100644 --- a/src/Libraries/SmartStore.Core/IPageable.cs +++ b/src/Libraries/SmartStore.Core/IPageable.cs @@ -3,8 +3,6 @@ using System.Collections; using System.Collections.Generic; -// codehint: sm-add (new file) - namespace SmartStore.Core { diff --git a/src/Libraries/SmartStore.Core/IStoreContext.cs b/src/Libraries/SmartStore.Core/IStoreContext.cs index 0986f24bb8..2ab8bfede9 100644 --- a/src/Libraries/SmartStore.Core/IStoreContext.cs +++ b/src/Libraries/SmartStore.Core/IStoreContext.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Domain.Stores; namespace SmartStore.Core { @@ -44,7 +39,6 @@ public interface IStoreContext /// /// IsSingleStoreMode ? 0 : CurrentStore.Id /// - /// codehint: sm-add int CurrentStoreIdIfMultiStoreMode { get; } } } diff --git a/src/Libraries/SmartStore.Core/ITransient.cs b/src/Libraries/SmartStore.Core/ITransient.cs new file mode 100644 index 0000000000..761dcee833 --- /dev/null +++ b/src/Libraries/SmartStore.Core/ITransient.cs @@ -0,0 +1,9 @@ +using System; + +namespace SmartStore +{ + public interface ITransient + { + bool IsTransient { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Core/IWebHelper.cs b/src/Libraries/SmartStore.Core/IWebHelper.cs index d1fda9b4e8..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 @@ -118,16 +128,5 @@ public partial interface IWebHelper /// Redirect URL; empty string if you want to redirect to the current page URL /// Usually true after a new plugin was installed (nukes the MVC cache) void RestartAppDomain(bool makeRedirect = false, string redirectUrl = "", bool aggressive = false); - - /// - /// Gets a value that indicates whether the client is being redirected to a new location - /// - bool IsRequestBeingRedirected { get; } - - /// - /// Gets or sets a value that indicates whether the client is being redirected to a new location using POST - /// - bool IsPostBeingDone { get; set; } - } } 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 7bac9acb62..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) { @@ -21,9 +25,9 @@ public IContainer Container get { return _container; } } - public T Resolve(string key = "", ILifetimeScope scope = null) where T : class + public T Resolve(object key = null, ILifetimeScope scope = null) where T : class { - if (string.IsNullOrEmpty(key)) + if (key == null) { return (scope ?? Scope()).Resolve(); } @@ -45,9 +49,9 @@ public object ResolveNamed(string name, Type type, ILifetimeScope scope = null) return (scope ?? Scope()).ResolveNamed(name, type); } - public T[] ResolveAll(string key = "", ILifetimeScope scope = null) + public T[] ResolveAll(object key = null, ILifetimeScope scope = null) { - if (string.IsNullOrEmpty(key)) + if (key == null) { return (scope ?? Scope()).Resolve>().ToArray(); } @@ -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/RegularExpressions.cs b/src/Libraries/SmartStore.Core/Infrastructure/RegularExpressions.cs index cc621f6629..a9143bed56 100644 --- a/src/Libraries/SmartStore.Core/Infrastructure/RegularExpressions.cs +++ b/src/Libraries/SmartStore.Core/Infrastructure/RegularExpressions.cs @@ -30,7 +30,7 @@ public static class RegularExpressions public static readonly Regex IsGuid = new Regex(@"\{?[a-fA-F0-9]{8}(?:-(?:[a-fA-F0-9]){4}){3}-[a-fA-F0-9]{12}\}?", RegexOptions.Compiled); public static readonly Regex IsBase64Guid = new Regex(@"[a-zA-Z0-9+/=]{22,24}", RegexOptions.Compiled); - public static readonly Regex IsCultureCode = new Regex(@"^([a-z]{2})|([a-z]{2}-[A-Z]{2})$", RegexOptions.Singleline | RegexOptions.Compiled); + public static readonly Regex IsCultureCode = new Regex(@"^[a-z]{2}(-[A-Z]{2})?$", RegexOptions.Singleline | RegexOptions.Compiled); public static readonly Regex IsYearRange = new Regex(@"^(\d{4})-(\d{4})$", RegexOptions.Compiled); 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 new file mode 100644 index 0000000000..ed9beed2ad --- /dev/null +++ b/src/Libraries/SmartStore.Core/Localization/LocalizationHelper.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace SmartStore.Core.Localization +{ + public static class LocalizationHelper + { + private readonly static HashSet _cultureCodes = + new HashSet( + CultureInfo.GetCultures(CultureTypes.NeutralCultures | CultureTypes.SpecificCultures | CultureTypes.UserCustomCulture) + .Select(x => x.IetfLanguageTag) + .Where(x => !string.IsNullOrWhiteSpace(x))); + + 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 922ba78a78..fb8a10be35 100644 --- a/src/Libraries/SmartStore.Core/Logging/LoggingExtensions.cs +++ b/src/Libraries/SmartStore.Core/Logging/LoggingExtensions.cs @@ -9,43 +9,61 @@ 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.Message, 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 + // don't log thread abort exception if ((exception != null) && (exception is System.Threading.ThreadAbortException)) return; if (logger.IsEnabled(level)) { - string fullMessage = exception == null ? string.Empty : exception.ToString(); + 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 8bf32d705b..593a39ddb0 100644 --- a/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs +++ b/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs @@ -1,18 +1,20 @@ 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 { public class TraceLogger : DisposableObject, ILogger { private readonly TraceSource _traceSource; + private readonly StreamWriter _streamWriter; - public TraceLogger() : this("SmartStore.log") + public TraceLogger() : this(CommonHelper.MapPath("~/App_Data/SmartStore.log")) { } @@ -23,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; } @@ -85,12 +101,17 @@ public IList GetLogByIds(int[] logIds) public void InsertLog(LogContext context) { var type = LogLevelToEventType(context.LogLevel); - _traceSource.TraceEvent(type, (int)type, "{0}: {1}".FormatCurrent(type.ToString().ToUpper(), context.ShortMessage)); + var msg = context.ShortMessage.Grow(context.FullMessage, Environment.NewLine); + + if (msg.HasValue()) + { + _traceSource.TraceEvent(type, (int)type, "{0}: {1}".FormatCurrent(type.ToString().ToUpper(), msg)); + } } public void InsertLog(LogLevel logLevel, string shortMessage, string fullMessage = "", Customer customer = null) { - var context = new LogContext() + var context = new LogContext { LogLevel = logLevel, ShortMessage = shortMessage, @@ -120,12 +141,19 @@ private TraceEventType LogLevelToEventType(LogLevel level) public void Flush() { + _traceSource.Flush(); } 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 568eb0de70..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"); @@ -78,7 +77,10 @@ public int TotalPages { get { - var total = (this.PageSize == 0 ? 0 : this.TotalCount / this.PageSize); + if (this.PageSize == 0) + return 0; + + var total = this.TotalCount / this.PageSize; if (this.TotalCount % this.PageSize > 0) total++; @@ -144,7 +146,8 @@ public virtual IEnumerator GetEnumerator() public class PagedList : PagedListBase { - public PagedList(int pageIndex, int pageSize, int totalItemsCount) : base(pageIndex, pageSize, totalItemsCount) + public PagedList(int pageIndex, int pageSize, int totalItemsCount) + : base(pageIndex, pageSize, totalItemsCount) { } } diff --git a/src/Libraries/SmartStore.Core/PagedList`T.cs b/src/Libraries/SmartStore.Core/PagedList`T.cs index 63db21a70d..af5039d810 100644 --- a/src/Libraries/SmartStore.Core/PagedList`T.cs +++ b/src/Libraries/SmartStore.Core/PagedList`T.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Data.Entity; +using System.Data.Entity.Infrastructure; namespace SmartStore.Core { @@ -19,7 +21,25 @@ public class PagedList : List, IPagedList public PagedList(IQueryable source, int pageIndex, int pageSize) { Guard.ArgumentNotNull(source, "source"); - Init(source.Skip(pageIndex * pageSize).Take(pageSize), pageIndex, pageSize, source.Count()); + + if (pageIndex == 0 && pageSize == int.MaxValue) + { + // avoid unnecessary SQL + Init(source, pageIndex, pageSize, source.Count()); + } + else + { + if (source.Provider is IDbAsyncQueryProvider) + { + // the Lambda overloads for Skip() and Take() let EF use cached query plans, thus slightly increasing performance. + var skip = pageIndex * pageSize; + Init(source.Skip(() => skip).Take(() => pageSize), pageIndex, pageSize, source.Count()); + } + else + { + Init(source.Skip(pageIndex * pageSize).Take(pageSize), pageIndex, pageSize, source.Count()); + } + } } /// @@ -31,6 +51,7 @@ public PagedList(IQueryable source, int pageIndex, int pageSize) public PagedList(IList source, int pageIndex, int pageSize) { Guard.ArgumentNotNull(source, "source"); + Init(source.Skip(pageIndex * pageSize).Take(pageSize), pageIndex, pageSize, source.Count); } @@ -43,12 +64,10 @@ public PagedList(IList source, int pageIndex, int pageSize) /// Total count public PagedList(IEnumerable source, int pageIndex, int pageSize, int totalCount) { - // codehint: sm-edit Guard.ArgumentNotNull(source, "source"); Init(source, pageIndex, pageSize, totalCount); } - // codehint: sm-add private void Init(IEnumerable source, int pageIndex, int pageSize, int totalCount) { Guard.PagingArgsValid(pageIndex, pageSize,"pageIndex", "pageSize"); 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/ILicensable.cs b/src/Libraries/SmartStore.Core/Plugins/ILicensable.cs deleted file mode 100644 index b2c4b60eb8..0000000000 --- a/src/Libraries/SmartStore.Core/Plugins/ILicensable.cs +++ /dev/null @@ -1,11 +0,0 @@ - -namespace SmartStore.Core.Plugins -{ - /// - /// Marks a plugin as a licensed piece of code where the user has to enter a license key that has to be activated. - /// Note that a license key is only valid for the IP address that activated the key. - /// - public interface ILicensable - { - } -} 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/IsHiddenAttribute.cs b/src/Libraries/SmartStore.Core/Plugins/Providers/IsHiddenAttribute.cs new file mode 100644 index 0000000000..6ba32b9d5a --- /dev/null +++ b/src/Libraries/SmartStore.Core/Plugins/Providers/IsHiddenAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace SmartStore.Core.Plugins +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public sealed class IsHiddenAttribute : Attribute + { + public IsHiddenAttribute(bool isHidden) + { + IsHidden = isHidden; + } + + public bool IsHidden { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Core/Plugins/Providers/ProviderMetadata.cs b/src/Libraries/SmartStore.Core/Plugins/Providers/ProviderMetadata.cs index c07d51555c..d07a628170 100644 --- a/src/Libraries/SmartStore.Core/Plugins/Providers/ProviderMetadata.cs +++ b/src/Libraries/SmartStore.Core/Plugins/Providers/ProviderMetadata.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using SmartStore.Core.Domain.DataExchange; namespace SmartStore.Core.Plugins { @@ -67,6 +68,16 @@ public class ProviderMetadata /// public bool IsEditable { get; set; } + /// + /// Gets or sets a value indicating whether the provider is hidden (by decorating with ) + /// + public bool IsHidden { get; set; } + + /// + /// Gets or sets flags that reflects what features of export data processing is supported by a provider + /// + 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/Security/SmartStorePrincipal.cs b/src/Libraries/SmartStore.Core/Security/SmartStorePrincipal.cs new file mode 100644 index 0000000000..991551ca7c --- /dev/null +++ b/src/Libraries/SmartStore.Core/Security/SmartStorePrincipal.cs @@ -0,0 +1,39 @@ +using System.Security; +using System.Security.Claims; +using System.Security.Principal; +using System.Web.Security; +using SmartStore.Core.Domain.Customers; + +namespace SmartStore.Core +{ + + public class SmartStoreIdentity : ClaimsIdentity + { + [SecuritySafeCritical] + public SmartStoreIdentity(int customerId, string name, string type) + : base(new GenericIdentity(name, type)) + { + CustomerId = customerId; + } + + public int CustomerId { get; private set; } + + public override bool IsAuthenticated { get { return CustomerId != 0; } } + } + + + public class SmartStorePrincipal : IPrincipal + { + public SmartStorePrincipal(Customer customer, string type) + { + this.Identity = new SmartStoreIdentity(customer.Id, customer.Username, type); + } + + public bool IsInRole(string role) + { + return (Identity != null && Identity.IsAuthenticated && role.HasValue() && Roles.IsUserInRole(Identity.Name, role)); + } + + public IIdentity Identity { get; private set; } + } +} diff --git a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj index 367040dc86..a14afb2ba4 100644 --- a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj +++ b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj @@ -62,24 +62,21 @@ 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 - ..\..\packages\EntityFramework.6.1.0\lib\net45\EntityFramework.dll + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll False - ..\..\packages\EntityFramework.6.1.0\lib\net45\EntityFramework.SqlServer.dll - - - False - ..\..\packages\fasterflect.2.1.3\lib\net40\Fasterflect.dll + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll True @@ -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 @@ -101,7 +98,6 @@ - ..\..\packages\System.Linq.Dynamic.1.0.0\lib\net40\System.Linq.Dynamic.dll @@ -153,24 +149,58 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + @@ -197,10 +227,6 @@ - - - - @@ -222,7 +248,6 @@ - @@ -286,7 +311,7 @@ - + @@ -330,15 +355,12 @@ - - - @@ -352,11 +374,11 @@ + - @@ -418,23 +440,17 @@ - - - - - - @@ -549,17 +565,19 @@ - - + - + + + + @@ -580,6 +598,7 @@ + @@ -588,6 +607,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/201506261018157_Merge.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201506261018157_Merge.Designer.cs new file mode 100644 index 0000000000..8d36b523ab --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201506261018157_Merge.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.0-30225")] + public sealed partial class Merge : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(Merge)); + + string IMigrationMetadata.Id + { + get { return "201506261018157_Merge"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201506261018157_Merge.cs b/src/Libraries/SmartStore.Data/Migrations/201506261018157_Merge.cs new file mode 100644 index 0000000000..d5201b31f3 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201506261018157_Merge.cs @@ -0,0 +1,16 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class Merge : DbMigration + { + public override void Up() + { + } + + public override void Down() + { + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201506261018157_Merge.resx b/src/Libraries/SmartStore.Data/Migrations/201506261018157_Merge.resx new file mode 100644 index 0000000000..01a895415e --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201506261018157_Merge.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/201506261756463_PrimaryStoreCurrencyMultiStore.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201506261756463_PrimaryStoreCurrencyMultiStore.Designer.cs new file mode 100644 index 0000000000..11aadff013 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201506261756463_PrimaryStoreCurrencyMultiStore.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.0-30225")] + public sealed partial class PrimaryStoreCurrencyMultiStore : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(PrimaryStoreCurrencyMultiStore)); + + string IMigrationMetadata.Id + { + get { return "201506261756463_PrimaryStoreCurrencyMultiStore"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201506261756463_PrimaryStoreCurrencyMultiStore.cs b/src/Libraries/SmartStore.Data/Migrations/201506261756463_PrimaryStoreCurrencyMultiStore.cs new file mode 100644 index 0000000000..b54d2c2f84 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201506261756463_PrimaryStoreCurrencyMultiStore.cs @@ -0,0 +1,160 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using System.Linq; + using System.Web.Hosting; + using SmartStore.Core.Data; + using SmartStore.Core.Domain.Configuration; + using SmartStore.Core.Domain.Directory; + using SmartStore.Core.Domain.Stores; + using SmartStore.Data.Setup; + + public partial class PrimaryStoreCurrencyMultiStore : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.Store", "PrimaryStoreCurrencyId", c => c.Int(nullable: false, defaultValue: 1)); + AddColumn("dbo.Store", "PrimaryExchangeRateCurrencyId", c => c.Int(nullable: false, defaultValue: 1)); + + // avoid conflicts with foreign key constraint + if (HostingEnvironment.IsHosted && DataSettings.Current.IsSqlServer) + { + // what sql-server compact does not support here: + // - Update Set with a select sub-query + // - Select in variable via declare + // - Alter table to check/nocheck a constraint + // so the the foreign key check constraint fails here (and therefore this migration) if there's no currency with id 1. + + Sql("Update dbo.Store Set PrimaryStoreCurrencyId = (Select Min(Id) From dbo.Currency)"); + Sql("Update dbo.Store Set PrimaryExchangeRateCurrencyId = (Select Min(Id) From dbo.Currency)"); + } + + CreateIndex("dbo.Store", "PrimaryStoreCurrencyId"); + CreateIndex("dbo.Store", "PrimaryExchangeRateCurrencyId"); + + AddForeignKey("dbo.Store", "PrimaryExchangeRateCurrencyId", "dbo.Currency", "Id"); + AddForeignKey("dbo.Store", "PrimaryStoreCurrencyId", "dbo.Currency", "Id"); + } + + public override void Down() + { + DropForeignKey("dbo.Store", "PrimaryStoreCurrencyId", "dbo.Currency"); + DropForeignKey("dbo.Store", "PrimaryExchangeRateCurrencyId", "dbo.Currency"); + + DropIndex("dbo.Store", new[] { "PrimaryExchangeRateCurrencyId" }); + DropIndex("dbo.Store", new[] { "PrimaryStoreCurrencyId" }); + + DropColumn("dbo.Store", "PrimaryExchangeRateCurrencyId"); + DropColumn("dbo.Store", "PrimaryStoreCurrencyId"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + + var settings = context.Set(); + var primaryStoreCurrencySetting = settings.FirstOrDefault(x => x.Name == "CurrencySettings.PrimaryStoreCurrencyId"); + var primaryExchangeRateCurrencySetting = settings.FirstOrDefault(x => x.Name == "CurrencySettings.PrimaryExchangeRateCurrencyId"); + + int primaryStoreCurrencyId = primaryStoreCurrencySetting.Value.ToInt(); + int primaryExchangeRateCurrencyId = primaryExchangeRateCurrencySetting.Value.ToInt(); + + if (primaryStoreCurrencyId == 0) + primaryStoreCurrencyId = context.Set().First().Id; + + if (primaryExchangeRateCurrencyId == 0) + primaryExchangeRateCurrencyId = primaryStoreCurrencyId; + + var stores = context.Set().ToList(); + + stores.ForEach(x => + { + x.PrimaryStoreCurrencyId = primaryStoreCurrencyId; + x.PrimaryExchangeRateCurrencyId = primaryExchangeRateCurrencyId; + }); + + settings.Remove(primaryStoreCurrencySetting); + settings.Remove(primaryExchangeRateCurrencySetting); + + context.SaveChanges(); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.Configuration.Currencies.DeleteOrPublishStoreConflict", + "The currency cannot be deleted or deactivated because it is attached to the store \"{0}\" as primary or exchange rate currency.", + "Die Whrung kann nicht gelscht oder deaktiviert werden, weil sie dem Shop \"{0}\" als Leit- oder Umrechnungswhrung zugeordnet ist."); + + //builder.AddOrUpdate("Admin.Configuration.Currencies.StoreLimitationConflict", + // "The store limitations must include store \"{0}\" because the currency is attached to it as primary or exchange rate currency.", + // "Die Shop-Eingrenzungen mssen den Shop \"{0}\" enthalten, da ihm die Whrung als Leit- oder Umrechnungswhrung zugeordnet ist."); + + builder.AddOrUpdate("Admin.Configuration.Stores.Fields.PrimaryStoreCurrencyId", + "Primary store currency", + "Leitwhrung", + "Specifies the the primary store currency.", + "Legt die Leitwhrung des Shops fest."); + + builder.AddOrUpdate("Admin.Configuration.Stores.Fields.PrimaryExchangeRateCurrencyId", + "Exchange rate currency", + "Umrechnungswhrung", + "Specifies the primary exchange rate currency for this store.", + "Legt die Umrechnungswhrung fr diesen Shop fest."); + + builder.AddOrUpdate("Admin.Configuration.Currencies.Fields.IsPrimaryStoreCurrency", + "Primary currency", + "Leitwhrung"); + + builder.AddOrUpdate("Admin.Configuration.Currencies.Fields.IsPrimaryExchangeRateCurrency", + "Exchange rate currency", + "Umrechnungswhrung"); + + builder.AddOrUpdate("Admin.Configuration.Currencies.Fields.PrimaryStoreCurrencyStores", + "Is primary store currency for", + "Ist Leitwhrung fr", + "A list of stores where the currency is primary store currency.", + "Eine Liste mit Shops, in denen die Whrung Leitwhrung ist."); + + builder.AddOrUpdate("Admin.Configuration.Currencies.Fields.PrimaryExchangeRateCurrencyStores", + "Is exchange rate currency for", + "Ist Umrechnungswhrung fr", + "A list of stores where the currency is primary exchange rate currency.", + "Eine Liste mit Shops, in denen die Whrung Umrechnungswhrung ist."); + + builder.AddOrUpdate("Admin.Configuration.Stores.Fields.SslEnabled", + "SSL", + "SSL", + "Specifies whether the store should be SSL secured.", + "Legt fest, ob der Shop SSL gesichert werden soll."); + + builder.AddOrUpdate("Admin.Configuration.Settings.News.MaxAgeInDays", + "Maximum age (in days)", + "Maximales Alter (in Tagen)", + "Specifies the maximum news age in days. Older news are not exported in the RSS feed.", + "Legt das maximale News-Alter in Tagen fest. ltere News werden im RSS-Feed nicht exportiert."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Blog.MaxAgeInDays", + "Maximum age (in days)", + "Maximales Alter (in Tagen)", + "Specifies the maximum news age in days. Older blog posts are not exported in the RSS feed.", + "Legt das maximale Blog-Alter in Tagen fest. ltere Blog-Eintrge werden im RSS-Feed nicht exportiert."); + + + builder.AddOrUpdate("Admin.Common.Deleted", + "Deleted", + "Gelscht"); + + + builder.Delete("Admin.Configuration.Currencies.CantDeletePrimary"); + builder.Delete("Admin.Configuration.Currencies.CantDeleteExchange"); + builder.Delete("Admin.Configuration.Currencies.Fields.MarkAsPrimaryStoreCurrency"); + builder.Delete("Admin.Configuration.Currencies.Fields.MarkAsPrimaryExchangeRateCurrency"); + builder.Delete("Forum.ForumFeedTitle"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201506261756463_PrimaryStoreCurrencyMultiStore.resx b/src/Libraries/SmartStore.Data/Migrations/201506261756463_PrimaryStoreCurrencyMultiStore.resx new file mode 100644 index 0000000000..7e45cc22c4 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201506261756463_PrimaryStoreCurrencyMultiStore.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/201507072138058_MessageTemplateAttachments.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201507072138058_MessageTemplateAttachments.Designer.cs new file mode 100644 index 0000000000..cb29f9e05f --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507072138058_MessageTemplateAttachments.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.0-30225")] + public sealed partial class MessageTemplateAttachments : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(MessageTemplateAttachments)); + + string IMigrationMetadata.Id + { + get { return "201507072138058_MessageTemplateAttachments"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201507072138058_MessageTemplateAttachments.cs b/src/Libraries/SmartStore.Data/Migrations/201507072138058_MessageTemplateAttachments.cs new file mode 100644 index 0000000000..d83d2520df --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507072138058_MessageTemplateAttachments.cs @@ -0,0 +1,40 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + using SmartStore.Data.Setup; + + public partial class MessageTemplateAttachments : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.MessageTemplate", "Attachment1FileId", c => c.Int()); + AddColumn("dbo.MessageTemplate", "Attachment2FileId", c => c.Int()); + AddColumn("dbo.MessageTemplate", "Attachment3FileId", c => c.Int()); + } + + public override void Down() + { + DropColumn("dbo.MessageTemplate", "Attachment3FileId"); + DropColumn("dbo.MessageTemplate", "Attachment2FileId"); + DropColumn("dbo.MessageTemplate", "Attachment1FileId"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Common.Replace", + "Replace", + "Ersetzen"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201507072138058_MessageTemplateAttachments.resx b/src/Libraries/SmartStore.Data/Migrations/201507072138058_MessageTemplateAttachments.resx new file mode 100644 index 0000000000..3970382b24 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507072138058_MessageTemplateAttachments.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/201507092146153_DownloadGuidIndex.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201507092146153_DownloadGuidIndex.Designer.cs new file mode 100644 index 0000000000..0693d8660f --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507092146153_DownloadGuidIndex.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.0-30225")] + public sealed partial class DownloadGuidIndex : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(DownloadGuidIndex)); + + string IMigrationMetadata.Id + { + get { return "201507092146153_DownloadGuidIndex"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201507092146153_DownloadGuidIndex.cs b/src/Libraries/SmartStore.Data/Migrations/201507092146153_DownloadGuidIndex.cs new file mode 100644 index 0000000000..803e4557f6 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507092146153_DownloadGuidIndex.cs @@ -0,0 +1,18 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class DownloadGuidIndex : DbMigration + { + public override void Up() + { + CreateIndex("dbo.Download", "DownloadGuid"); + } + + public override void Down() + { + DropIndex("dbo.Download", new[] { "DownloadGuid" }); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201507092146153_DownloadGuidIndex.resx b/src/Libraries/SmartStore.Data/Migrations/201507092146153_DownloadGuidIndex.resx new file mode 100644 index 0000000000..457d21844b --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507092146153_DownloadGuidIndex.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/201507102159496_TransientMedia.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201507102159496_TransientMedia.Designer.cs new file mode 100644 index 0000000000..d738e053cf --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507102159496_TransientMedia.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.0-30225")] + public sealed partial class TransientMedia : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(TransientMedia)); + + string IMigrationMetadata.Id + { + get { return "201507102159496_TransientMedia"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201507102159496_TransientMedia.cs b/src/Libraries/SmartStore.Data/Migrations/201507102159496_TransientMedia.cs new file mode 100644 index 0000000000..f70f8ba8a7 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507102159496_TransientMedia.cs @@ -0,0 +1,51 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + using SmartStore.Core.Domain.Tasks; + using SmartStore.Data.Setup; + + public partial class TransientMedia : DbMigration, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.Download", "IsTransient", c => c.Boolean(nullable: false)); + AddColumn("dbo.Download", "UpdatedOnUtc", c => c.DateTime(nullable: false)); + AddColumn("dbo.Picture", "IsTransient", c => c.Boolean(nullable: false)); + AddColumn("dbo.Picture", "UpdatedOnUtc", c => c.DateTime(nullable: false)); + CreateIndex("dbo.Download", new[] { "UpdatedOnUtc", "IsTransient" }, name: "IX_UpdatedOn_IsTransient"); + CreateIndex("dbo.Picture", new[] { "UpdatedOnUtc", "IsTransient" }, name: "IX_UpdatedOn_IsTransient"); + } + + public override void Down() + { + DropIndex("dbo.Picture", "IX_UpdatedOn_IsTransient"); + DropIndex("dbo.Download", "IX_UpdatedOn_IsTransient"); + DropColumn("dbo.Picture", "UpdatedOnUtc"); + DropColumn("dbo.Picture", "IsTransient"); + DropColumn("dbo.Download", "UpdatedOnUtc"); + DropColumn("dbo.Download", "IsTransient"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.Set().AddOrUpdate(x => x.Type, + new ScheduleTask + { + Name = "Clear transient uploads", + CronExpression = "30 1,13 * * *", + Type = "SmartStore.Services.Media.TransientMediaClearTask, SmartStore.Services", + Enabled = true, + StopOnError = false, + } + ); + + context.SaveChanges(); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201507102159496_TransientMedia.resx b/src/Libraries/SmartStore.Data/Migrations/201507102159496_TransientMedia.resx new file mode 100644 index 0000000000..11d76add9f --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507102159496_TransientMedia.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/201507132241575_QueuedEmailAttachments.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201507132241575_QueuedEmailAttachments.Designer.cs new file mode 100644 index 0000000000..c4e3a3c11d --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507132241575_QueuedEmailAttachments.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.0-30225")] + public sealed partial class QueuedEmailAttachments : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(QueuedEmailAttachments)); + + string IMigrationMetadata.Id + { + get { return "201507132241575_QueuedEmailAttachments"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201507132241575_QueuedEmailAttachments.cs b/src/Libraries/SmartStore.Data/Migrations/201507132241575_QueuedEmailAttachments.cs new file mode 100644 index 0000000000..e174e25c2c --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507132241575_QueuedEmailAttachments.cs @@ -0,0 +1,107 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using SmartStore.Core.Domain.Tasks; + using SmartStore.Data.Setup; + + public partial class QueuedEmailAttachments : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + CreateTable( + "dbo.QueuedEmailAttachment", + c => new + { + Id = c.Int(nullable: false, identity: true), + QueuedEmailId = c.Int(nullable: false), + StorageLocation = c.Int(nullable: false), + Path = c.String(maxLength: 1000), + FileId = c.Int(), + Data = c.Binary(), + Name = c.String(nullable: false, maxLength: 200), + MimeType = c.String(nullable: false, maxLength: 200), + }) + .PrimaryKey(t => t.Id) + .ForeignKey("dbo.Download", t => t.FileId, cascadeDelete: true) + .ForeignKey("dbo.QueuedEmail", t => t.QueuedEmailId, cascadeDelete: true) + .Index(t => t.QueuedEmailId) + .Index(t => t.FileId); + + } + + public override void Down() + { + DropForeignKey("dbo.QueuedEmailAttachment", "QueuedEmailId", "dbo.QueuedEmail"); + DropForeignKey("dbo.QueuedEmailAttachment", "FileId", "dbo.Download"); + DropIndex("dbo.QueuedEmailAttachment", new[] { "FileId" }); + DropIndex("dbo.QueuedEmailAttachment", new[] { "QueuedEmailId" }); + DropTable("dbo.QueuedEmailAttachment"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + + context.Set().AddOrUpdate(x => x.Type, + new ScheduleTask + { + Name = "Clear email queue", + CronExpression = "0 2 * * *", + Type = "SmartStore.Services.Messages.QueuedMessagesClearTask, SmartStore.Services", + Enabled = true, + StopOnError = false, + } + ); + + context.SaveChanges(); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.AttachOrderPdfToOrderPlacedEmail", + "Attach order PDF to 'Order Placed' email", + "Bei Bestelleingang PDF mitsenden", + "Dynamically creates and attaches the order PDF to the 'Order Placed' customer notification email.", + "Erstellt bei Bestelleingang das Auftrags-PDF-Dokument und hngt es der Kunden-Benachrichtigungs-Email an"); + + builder.AddOrUpdate("Admin.Configuration.Settings.GeneralCommon.AttachOrderPdfToOrderCompletedEmail", + "Attach order PDF to 'Order Completed' email", + "Bei Abschluss einer Bestellung PDF mitsenden", + "Dynamically creates and attaches the order PDF to the 'Order Completed' customer notification email.", + "Erstellt bei Abschluss einer Bestellung das Auftrags-PDF-Dokument und hngt es der Kunden-Benachrichtigungs-Email an"); + + builder.AddOrUpdate("Admin.System.QueuedEmails.Fields.Attachments", + "Attachments", + "Anhnge"); + + builder.AddOrUpdate("Admin.System.QueuedEmails.CouldNotDownloadAttachment", + "Could not download e-mail attachment: no data.", + "E-Mail Anhang konnte nicht herunterladen: Daten nicht verfgbar."); + + builder.AddOrUpdate("Admin.System.QueuedEmails.ErrorCreatingAttachment", + "An error occured while creating e-mail attachment", + "Whrend der Erstellung des E-Mail-Anhangs ist ein Fehler aufgetreten"); + + builder.AddOrUpdate("Admin.System.QueuedEmails.ErrorEmptyAttachmentResult", + "The e-mail attachment data could not be downloaded from path '{0}'", + "Daten fr den E-Mail Anhang konnten nicht heruntergeladen werden. Pfad: {0}"); + + builder.AddOrUpdate("Admin.System.QueuedEmails.ErrorNoPdfAttachment", + "The content type of the e-mail attachment must be 'application/pdf'", + "Der Inhaltstyp des E-Mail Anhangs muss 'application/pdf' sein"); + + builder.AddOrUpdate("Admin.System.QueuedEmails.List.AttachmentsCount", + "Number of attachments", + "Anzahl Anhnge"); + + builder.AddOrUpdate("Order.PdfInvoiceFileName", + "order-{0}.pdf", + "bestellung-{0}.pdf"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201507132241575_QueuedEmailAttachments.resx b/src/Libraries/SmartStore.Data/Migrations/201507132241575_QueuedEmailAttachments.resx new file mode 100644 index 0000000000..4bcdc56bda --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507132241575_QueuedEmailAttachments.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/201507141647299_CustomerTablePerf.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201507141647299_CustomerTablePerf.Designer.cs new file mode 100644 index 0000000000..c12151b6d9 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507141647299_CustomerTablePerf.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.0-30225")] + public sealed partial class CustomerTablePerf : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(CustomerTablePerf)); + + string IMigrationMetadata.Id + { + get { return "201507141647299_CustomerTablePerf"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201507141647299_CustomerTablePerf.cs b/src/Libraries/SmartStore.Data/Migrations/201507141647299_CustomerTablePerf.cs new file mode 100644 index 0000000000..982225727d --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507141647299_CustomerTablePerf.cs @@ -0,0 +1,35 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class CustomerTablePerf : DbMigration + { + public override void Up() + { + // without dropping the indexes we cannot adjust column lengths + DropIndex("dbo.Customer", "IX_Customer_Email"); + DropIndex("dbo.Customer", "IX_Customer_Username"); + + AlterColumn("dbo.Customer", "Username", c => c.String(maxLength: 500)); + AlterColumn("dbo.Customer", "Email", c => c.String(maxLength: 500)); + AlterColumn("dbo.Customer", "Password", c => c.String(maxLength: 500)); + AlterColumn("dbo.Customer", "PasswordSalt", c => c.String(maxLength: 500)); + AlterColumn("dbo.Customer", "LastIpAddress", c => c.String(maxLength: 100)); + + // recreate previously dropped indexes + CreateIndex("dbo.Customer", "Email", name: "IX_Customer_Email"); + CreateIndex("dbo.Customer", "Username", name: "IX_Customer_Username"); + } + + public override void Down() + { + //// INFO: (mc) Unnecessary + //AlterColumn("dbo.Customer", "LastIpAddress", c => c.String()); + //AlterColumn("dbo.Customer", "PasswordSalt", c => c.String()); + //AlterColumn("dbo.Customer", "Password", c => c.String()); + //AlterColumn("dbo.Customer", "Email", c => c.String(maxLength: 1000)); + //AlterColumn("dbo.Customer", "Username", c => c.String(maxLength: 1000)); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201507141647299_CustomerTablePerf.resx b/src/Libraries/SmartStore.Data/Migrations/201507141647299_CustomerTablePerf.resx new file mode 100644 index 0000000000..5e18dcfce7 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507141647299_CustomerTablePerf.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/201507200832223_SortFilterHomepageProducts.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201507200832223_SortFilterHomepageProducts.Designer.cs new file mode 100644 index 0000000000..2f16779b2a --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507200832223_SortFilterHomepageProducts.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 SortFilterHomepageProducts : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(SortFilterHomepageProducts)); + + string IMigrationMetadata.Id + { + get { return "201507200832223_SortFilterHomepageProducts"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201507200832223_SortFilterHomepageProducts.cs b/src/Libraries/SmartStore.Data/Migrations/201507200832223_SortFilterHomepageProducts.cs new file mode 100644 index 0000000000..c9e3918b69 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507200832223_SortFilterHomepageProducts.cs @@ -0,0 +1,61 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + using System.Web.Hosting; + using SmartStore.Core.Data; + using SmartStore.Data.Setup; + + public partial class SortFilterHomepageProducts : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.Product", "HomePageDisplayOrder", c => c.Int(nullable: false)); + + if (HostingEnvironment.IsHosted && DataSettings.Current.IsSqlServer) + { + this.SqlFileOrResource("LatestProductLoadAllPaged.sql"); + } + } + + public override void Down() + { + DropColumn("dbo.Product", "HomePageDisplayOrder"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Common.Unspecified", + "Unspecified", + "Nicht spezifiziert"); + + builder.AddOrUpdate("Admin.Catalog.Products.List.SearchIsPublished", + "Published", + "Verffentlicht", + "Filters for published or unpublished products.", + "Filtert nach verffentlichten bzw. unverffentlichten Produkten."); + + builder.AddOrUpdate("Admin.Catalog.Products.List.SearchHomePageProducts", + "Showed on home page", + "Auf Homepage angezeigt", + "Filters for products displayed or not displayed on homepage.", + "Filtert nach Produkten, die auf der Homepage angezeigt oder nicht angezeigt werden."); + + builder.AddOrUpdate("Admin.Catalog.Products.Fields.HomePageDisplayOrder", + "Homepage display order", + "Homepage Reihenfolge", + "Specifies the display order for products displayed on homepage. 1 represents the first element in the list.", + "Legt die Anzeige-Reihenfolge der Produkte auf der Homepage fest (1 steht bspw. fr das erste Element in der Liste)."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201507200832223_SortFilterHomepageProducts.resx b/src/Libraries/SmartStore.Data/Migrations/201507200832223_SortFilterHomepageProducts.resx new file mode 100644 index 0000000000..cff0d41d5c --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507200832223_SortFilterHomepageProducts.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/201507210952098_PaymentMethodDescription.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201507210952098_PaymentMethodDescription.Designer.cs new file mode 100644 index 0000000000..4412803c98 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507210952098_PaymentMethodDescription.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 PaymentMethodDescription : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(PaymentMethodDescription)); + + string IMigrationMetadata.Id + { + get { return "201507210952098_PaymentMethodDescription"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201507210952098_PaymentMethodDescription.cs b/src/Libraries/SmartStore.Data/Migrations/201507210952098_PaymentMethodDescription.cs new file mode 100644 index 0000000000..b71dd98ac0 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507210952098_PaymentMethodDescription.cs @@ -0,0 +1,66 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + using SmartStore.Data.Setup; + + public partial class PaymentMethodDescription : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.PaymentMethod", "FullDescription", c => c.String(maxLength: 4000)); + } + + public override void Down() + { + DropColumn("dbo.PaymentMethod", "FullDescription"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.Configuration.Payment.Methods.ShortDescription", + "Short description", + "Kurzbeschreibung", + "Specifies a short description of the payment method.", + "Legt eine Kurzbeschreibung der Zahlungsmethode fest."); + + builder.AddOrUpdate("Admin.Configuration.Payment.Methods.FullDescription", + "Full description", + "Langtext", + "Specifies a full description of the payment method. It appears in the payment list in checkout.", + "Legt eine vollstndige Beschreibung der Zahlungsmethode fest. Sie erscheint in der Zahlungsliste im Kassenbereich."); + + builder.AddOrUpdate("Admin.Configuration.Settings.Catalog.PriceDisplayType", + "Price display", + "Preisanzeige", + "Specifies whether or what type of price to be displayed in product lists.", + "Legt fest, ob bzw. welcher Typ von Preis in Produktlisten angezeigt werden soll."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.PriceDisplayType.LowestPrice", + "Minimum feasible price", + "Minimal realisierbarer Preis"); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.PriceDisplayType.PreSelectedPrice", + "Price preselected on detail page", + "Auf der Detailseite vorgewhlter Preis"); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.PriceDisplayType.PriceWithoutDiscountsAndAttributes", + "Price without discounts and attributes", + "Preis ohne Rabatte und Attribute"); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.Catalog.PriceDisplayType.Hide", + "No price indication", + "Keine Preisanzeige"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201507210952098_PaymentMethodDescription.resx b/src/Libraries/SmartStore.Data/Migrations/201507210952098_PaymentMethodDescription.resx new file mode 100644 index 0000000000..04f8e677a2 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507210952098_PaymentMethodDescription.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/201507242008201_WebScheduler.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201507242008201_WebScheduler.Designer.cs new file mode 100644 index 0000000000..d9e3e0bca9 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507242008201_WebScheduler.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 WebScheduler : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(WebScheduler)); + + string IMigrationMetadata.Id + { + get { return "201507242008201_WebScheduler"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201507242008201_WebScheduler.cs b/src/Libraries/SmartStore.Data/Migrations/201507242008201_WebScheduler.cs new file mode 100644 index 0000000000..d9a437cb3c --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507242008201_WebScheduler.cs @@ -0,0 +1,97 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + using SmartStore.Data.Setup; + + public partial class WebScheduler : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.ScheduleTask", "Alias", c => c.String(maxLength: 500)); + AddColumn("dbo.ScheduleTask", "NextRunUtc", c => c.DateTime()); + AddColumn("dbo.ScheduleTask", "IsHidden", c => c.Boolean(nullable: false)); + AddColumn("dbo.ScheduleTask", "ProgressPercent", c => c.Int()); + AddColumn("dbo.ScheduleTask", "ProgressMessage", c => c.String(maxLength: 1000)); + AlterColumn("dbo.ScheduleTask", "Name", c => c.String(nullable: false, maxLength: 500)); + AlterColumn("dbo.ScheduleTask", "Type", c => c.String(nullable: false, maxLength: 800)); + CreateIndex("dbo.ScheduleTask", "Type"); + CreateIndex("dbo.ScheduleTask", new[] { "NextRunUtc", "Enabled" }, name: "IX_NextRun_Enabled"); + CreateIndex("dbo.ScheduleTask", new[] { "LastStartUtc", "LastEndUtc" }, name: "IX_LastStart_LastEnd"); + } + + public override void Down() + { + DropIndex("dbo.ScheduleTask", "IX_LastStart_LastEnd"); + DropIndex("dbo.ScheduleTask", "IX_NextRun_Enabled"); + DropIndex("dbo.ScheduleTask", new[] { "Type" }); + //AlterColumn("dbo.ScheduleTask", "Type", c => c.String(nullable: false)); + //AlterColumn("dbo.ScheduleTask", "Name", c => c.String(nullable: false)); + DropColumn("dbo.ScheduleTask", "ProgressMessage"); + DropColumn("dbo.ScheduleTask", "ProgressPercent"); + DropColumn("dbo.ScheduleTask", "IsHidden"); + DropColumn("dbo.ScheduleTask", "NextRunUtc"); + DropColumn("dbo.ScheduleTask", "Alias"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.Delete("Admin.System.ScheduleTasks.RestartApplication"); + + builder.AddOrUpdate("Admin.System.ScheduleTasks.NextRun", + "Next Run in", + "Nchste Ausfhrung in"); + builder.AddOrUpdate("Admin.System.ScheduleTasks.LastStart", + "Last Run", + "Letzte Ausfhrung"); + builder.AddOrUpdate("Admin.System.ScheduleTasks.AbnormalAbort", + "Abnormally aborted due to application shutdown", + "Abbruch erzwungen durch Herunterfahren der Anwendung"); + builder.AddOrUpdate("Admin.System.ScheduleTasks.UpdateLocked", + "The task can not be edited while it is running.", + "Die Aufgabe kann nicht bearbeitet werden, whrend sie ausgefhrt wird."); + builder.AddOrUpdate("Admin.System.ScheduleTasks.CancellationRequested", + "Cancellation request has been submitted.", + "Abbruchanforderung wurde bermittelt."); + + builder.AddOrUpdate("Common.Waiting", + "Waiting", + "Wartend"); + + builder.AddOrUpdate("Time.Year", "Year", "Jahr"); + builder.AddOrUpdate("Time.Years", "Years", "Jahre"); + builder.AddOrUpdate("Time.Month", "Month", "Monat"); + builder.AddOrUpdate("Time.Months", "Months", "Monate"); + builder.AddOrUpdate("Time.Week", "Week", "Woche"); + builder.AddOrUpdate("Time.Weeks", "Weeks", "Wochen"); + builder.AddOrUpdate("Time.Day", "Day", "Tag"); + builder.AddOrUpdate("Time.Days", "Days", "Tage"); + builder.AddOrUpdate("Time.Hour", "Hour", "Stunde"); + builder.AddOrUpdate("Time.Hours", "Hours", "Stunden"); + builder.AddOrUpdate("Time.Minute", "Minute", "Minute"); + builder.AddOrUpdate("Time.Minutes", "Minutes", "Minuten"); + builder.AddOrUpdate("Time.Second", "Second", "Sekunde"); + builder.AddOrUpdate("Time.Seconds", "Seconds", "Sekunden"); + + builder.AddOrUpdate("Time.DayAbbr", "d", "Tg."); + builder.AddOrUpdate("Time.DaysAbbr", "d", "Tg."); + builder.AddOrUpdate("Time.HourAbbr", "h", "Std."); + builder.AddOrUpdate("Time.HoursAbbr", "h", "Std."); + builder.AddOrUpdate("Time.MinuteAbbr", "min", "Min."); + builder.AddOrUpdate("Time.MinutesAbbr", "min", "Min."); + builder.AddOrUpdate("Time.SecondAbbr", "sec", "Sek."); + builder.AddOrUpdate("Time.SecondsAbbr", "sec", "Sek."); + + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201507242008201_WebScheduler.resx b/src/Libraries/SmartStore.Data/Migrations/201507242008201_WebScheduler.resx new file mode 100644 index 0000000000..7795c02827 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507242008201_WebScheduler.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/201507250039446_Merge2.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201507250039446_Merge2.Designer.cs new file mode 100644 index 0000000000..8830d1f8ac --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507250039446_Merge2.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 Merge2 : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(Merge2)); + + string IMigrationMetadata.Id + { + get { return "201507250039446_Merge2"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201507250039446_Merge2.cs b/src/Libraries/SmartStore.Data/Migrations/201507250039446_Merge2.cs new file mode 100644 index 0000000000..a26a4dfe70 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507250039446_Merge2.cs @@ -0,0 +1,16 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class Merge2 : DbMigration + { + public override void Up() + { + } + + public override void Down() + { + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201507250039446_Merge2.resx b/src/Libraries/SmartStore.Data/Migrations/201507250039446_Merge2.resx new file mode 100644 index 0000000000..483dbdf042 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201507250039446_Merge2.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/201508042207146_RemoveKeepAlive.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201508042207146_RemoveKeepAlive.Designer.cs new file mode 100644 index 0000000000..fbd14a355a --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201508042207146_RemoveKeepAlive.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 RemoveKeepAlive : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(RemoveKeepAlive)); + + string IMigrationMetadata.Id + { + get { return "201508042207146_RemoveKeepAlive"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201508042207146_RemoveKeepAlive.cs b/src/Libraries/SmartStore.Data/Migrations/201508042207146_RemoveKeepAlive.cs new file mode 100644 index 0000000000..8c0dc62ad6 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201508042207146_RemoveKeepAlive.cs @@ -0,0 +1,40 @@ +namespace SmartStore.Data.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() + { + if (HostingEnvironment.IsHosted && DataSettings.Current.IsSqlServer) + { + Sql("DELETE FROM [dbo].[ScheduleTask] WHERE [Type] = 'SmartStore.Services.Common.KeepAliveTask, SmartStore.Services'"); + } + } + + public override void Down() + { + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.System.ScheduleTasks", + "Scheduled Tasks", + "Geplante Aufgaben"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201508042207146_RemoveKeepAlive.resx b/src/Libraries/SmartStore.Data/Migrations/201508042207146_RemoveKeepAlive.resx new file mode 100644 index 0000000000..483dbdf042 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201508042207146_RemoveKeepAlive.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/201508091512101_ExportFramework.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201508091512101_ExportFramework.Designer.cs new file mode 100644 index 0000000000..532c012d7e --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201508091512101_ExportFramework.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 ExportFramework : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(ExportFramework)); + + string IMigrationMetadata.Id + { + get { return "201508091512101_ExportFramework"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201508091512101_ExportFramework.cs b/src/Libraries/SmartStore.Data/Migrations/201508091512101_ExportFramework.cs new file mode 100644 index 0000000000..5203e70451 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201508091512101_ExportFramework.cs @@ -0,0 +1,485 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using System.Web.Hosting; + using SmartStore.Core.Data; + using SmartStore.Core.Domain.Customers; + using SmartStore.Core.Domain.Security; + using SmartStore.Data.Setup; + + public partial class ExportFramework : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + CreateTable( + "dbo.ExportDeployment", + c => new + { + Id = c.Int(nullable: false, identity: true), + ProfileId = c.Int(nullable: false), + Name = c.String(nullable: false, maxLength: 100), + Enabled = c.Boolean(nullable: false), + IsPublic = c.Boolean(nullable: false), + DeploymentTypeId = c.Int(nullable: false), + Username = c.String(maxLength: 400), + Password = c.String(maxLength: 400), + Url = c.String(maxLength: 4000), + FileSystemPath = c.String(maxLength: 400), + EmailAddresses = c.String(maxLength: 4000), + EmailSubject = c.String(maxLength: 400), + EmailAccountId = c.Int(nullable: false), + }) + .PrimaryKey(t => t.Id) + .ForeignKey("dbo.ExportProfile", t => t.ProfileId, cascadeDelete: true) + .Index(t => t.ProfileId); + + CreateTable( + "dbo.ExportProfile", + c => new + { + Id = c.Int(nullable: false, identity: true), + Name = c.String(nullable: false, maxLength: 100), + FolderName = c.String(nullable: false, maxLength: 100), + ProviderSystemName = c.String(nullable: false, maxLength: 4000), + Enabled = c.Boolean(nullable: false), + SchedulingTaskId = c.Int(nullable: false), + Filtering = c.String(), + Projection = c.String(), + ProviderConfigData = c.String(), + Offset = c.Int(nullable: false), + Limit = c.Int(nullable: false), + BatchSize = c.Int(nullable: false), + PerStore = c.Boolean(nullable: false), + CompletedEmailAddresses = c.String(maxLength: 400), + CreateZipArchive = c.Boolean(nullable: false), + Cleanup = c.Boolean(nullable: false), + }) + .PrimaryKey(t => t.Id) + .ForeignKey("dbo.ScheduleTask", t => t.SchedulingTaskId) + .Index(t => t.SchedulingTaskId); + + if (HostingEnvironment.IsHosted && DataSettings.Current.IsSqlServer) + { + this.SqlFileOrResource("LatestProductLoadAllPaged.sql"); + } + } + + public override void Down() + { + DropForeignKey("dbo.ExportDeployment", "ProfileId", "dbo.ExportProfile"); + DropForeignKey("dbo.ExportProfile", "SchedulingTaskId", "dbo.ScheduleTask"); + DropIndex("dbo.ExportProfile", new[] { "SchedulingTaskId" }); + DropIndex("dbo.ExportDeployment", new[] { "ProfileId" }); + DropTable("dbo.ExportProfile"); + DropTable("dbo.ExportDeployment"); + } + + 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 Exports", + SystemName = "ManageExports", + Category = "Configuration" + }, new string[] { SystemCustomerRoleNames.Administrators }); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Common.Enabled", "Enabled", "Aktiviert"); + builder.AddOrUpdate("Common.Provider", "Provider", "Provider"); + builder.AddOrUpdate("Common.Profile", "Profile", "Profil"); + builder.AddOrUpdate("Common.Partition", "Partition", "Aufteilung"); + builder.AddOrUpdate("Common.Image", "Image", "Bild"); + builder.AddOrUpdate("Common.Filter", "Filter", "Filter"); + builder.AddOrUpdate("Common.Projection", "Projection", "Projektion"); + builder.AddOrUpdate("Common.Deployment", "Deployment", "Bereitstellung"); + builder.AddOrUpdate("Common.Website", "Website", "Web-Seite"); + builder.AddOrUpdate("Common.DetailDescription", "Detail description", "Detailbeschreibung"); + + builder.AddOrUpdate("Admin.Validation.UsernamePassword", "Please enter username and password", "Bitte geben Sie Benutzername und Passwort ein"); + builder.AddOrUpdate("Admin.Validation.Url", "Please enter a valid URL", "Bitte geben Sie eine gltige URL ein"); + builder.AddOrUpdate("Admin.Validation.Name", "Please enter a name", "Bitte geben Sie einen Namen ein"); + builder.AddOrUpdate("Admin.Validation.EmailAddress", "Please enter a valid email address", "Bitte geben Sie eine gltige E-Mail Adresse ein"); + + + builder.AddOrUpdate("Admin.DataExchange.Export.NoProfiles", + "There were no export profiles found.", + "Es wurden keine Exportprofile gefunden."); + + + builder.AddOrUpdate("Admin.DataExchange.Export.ProviderSystemName", + "Provider", + "Provider", + "Specifies the export provider. It is responsible for the individual formatting of the export data.", + "Legt den Export-Provider fest. Er ist fr die individuelle Formatierung der zu exportierenden Daten zustndig."); + + builder.AddOrUpdate("Admin.DataExchange.Export.EntityType", + "Object", + "Objekt", + "The object type the provider processes.", + "Der Objekttyp, den der Provider verarbeitet."); + + + builder.AddOrUpdate("Admin.DataExchange.Export.Name", + "Name of profile", + "Name des Profils", + "Specifies the name of the export profile.", + "Legt den Namen des Exportprofils fest."); + + builder.AddOrUpdate("Admin.DataExchange.Export.FileExtension", + "File type", + "Dateityp", + "The file type of the exported data.", + "Der Dateityp der exportierten Daten."); + + + + builder.AddOrUpdate("Admin.DataExchange.Export.BatchSize", + "Batch size", + "Stapelgre", + "Specifies the maximum number of records per export file. 0 is the default and means that all the records are exported in one file.", + "Legt die maximale Anzahl der Datenstze pro Exportdatei fest. 0 ist der Standard und bedeutet, dass alle Datenstze in eine Datei exportiert werden."); + + builder.AddOrUpdate("Admin.DataExchange.Export.PerStore", + "Per store", + "Per Shop", + "Specifies whether to start a separate run-through for each store. For each shop a new file will be created.", + "Legt fest, ob fr jeden Shop ein separater Verarbeitungsdurchlauf erfolgen soll. Fr jeden Shop wird eine neue Datei erzeugt."); + + builder.AddOrUpdate("Admin.DataExchange.Export.CreateZipArchive", + "Create ZIP archive", + "ZIP-Archiv erstellen", + "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.", + "Legt fest, ob die Exportdateien in einem ZIP-Archiv zusammengefasst werden sollen. Das Archiv verbleibt im temporren Ordner des Exportprofils ohne weitere Vearbeitung."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Cleanup", + "Clean up at the end", + "Zum Schluss aufrumen", + "Specifies whether to delete unneeded files after deployment.", + "Legt fest, ob nicht mehr bentigte Dateien nach der Bereitstellung gelscht werden sollen."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportEntityType.Product", "Product", "Produkt"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportEntityType.Category", "Category", "Warengruppe"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportEntityType.Manufacturer", "Manufacturer", "Hersteller"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportEntityType.Customer", "Customer", "Kunde"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportEntityType.Order", "Order", "Auftrag"); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.StoreId", + "Store", + "Shop", + "Filter by store.", + "Nach Shop filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.CreatedFrom", + "Created from", + "Erstellt von", + "Filter by created date.", + "Nach dem Erstellungsdatum filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.CreatedTo", + "Created to", + "Erstellt bis", + "Filter by created date.", + "Nach dem Erstellungsdatum filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.IdMinimum", + "Product Id from", + "Produkt-ID von", + "Filter by product identifier.", + "Nach der Produkt-ID filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.IdMaximum", + "Product Id to", + "Produkt-ID bis", + "Filter by product identifier.", + "Nach der Produkt-ID filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.PriceMinimum", + "Price from", + "Preis von", + "Filter by price.", + "Nach dem Preis filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.PriceMaximum", + "Price to", + "Preis bis", + "Filter by price.", + "Nach dem Preis filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.AvailabilityMinimum", + "Availability from", + "Verfgbar von", + "Filter by availability quantity.", + "Nach der Verfgbarkeitsmenge filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.AvailabilityMaximum", + "Availability to", + "Verfgbar bis", + "Filter by availability quantity.", + "Nach der Verfgbarkeitsmenge filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.IsPublished", + "Published", + "Verffentlicht", + "Filter by publishing.", + "Nach Verffentlichung filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.CategoryIds", + "Categories", + "Warengruppen", + "Filter by categtories.", + "Nach Warengruppen filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.WithoutCategories", + "Without category mapping", + "Ohne Warengruppenzuordnung", + "Filter by missing category mapping.", + "Nach fehlender Warengruppenzuordnung filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.ManufacturerId", + "Manufacturer", + "Hersteller", + "Filter by manufacturer.", + "Nach Hersteller filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.WithoutManufacturers", + "Without manufacturer mapping", + "Ohne Herstellerzuordnung", + "Filter by missing manufacturer mapping.", + "Nach fehlender Herstellerzuordnung filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.ProductTagId", + "Product tag", + "Produkt-Tag", + "Filter by product tag.", + "Nach Produkt-Tag filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.FeaturedProducts", + "Only featured products", + "Nur empfohlene Produkte", + "Filter by featured products. Is only applied when the filtering by categories and manufacturers.", + "Nach empfohlenen Produkten filtern. Wird nur bei der Filterung nach Warengruppen und Hersteller angewendet."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.ProductType", + "Product type", + "Produkttyp", + "Filter by product type.", + "Nach Produkttyp filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.OrderStatusIds", + "Order status", + "Auftragsstatus", + "Filter by order status.", + "Nach Auftragsstaus filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.PaymentStatusIds", + "Payment status", + "Zahlungsstatus", + "Filter by payment status.", + "Nach Zahlungsstatus filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.ShippingStatusIds", + "Shipping status", + "Versandstatus", + "Filter by shipping status.", + "Nach Versandstatus filtern."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.CustomerRoleIds", + "Customer roles", + "Kundengruppen", + "Filter by customer roles.", + "Nach Kundengruppen filtern."); + + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.StoreId", + "Store", + "Shop", + "Specifies the store to be applied to the export.", + "Legt den auf den Export anzuwendenden Shop fest."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.LanguageId", + "Language", + "Sprache", + "Specifies the language to be applied to the export.", + "Legt die auf den Export anzuwendende Sprache fest."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.CurrencyId", + "Currency", + "Whrung", + "Specifies the currency to be applied to the export.", + "Legt die auf den Export anzuwendende Whrung fest."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.CustomerId", + "Customer ID", + "Kunden-ID", + "Specifies the ID of the customer to be applied to the export. Is taken into account for price calculations for example.", + "Legt die ID des Kunden fest, auf den sich der Export beziehen soll. Wird z.B. bei Preisberechnungen bercksichtigt."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.DescriptionMerging", + "Product description", + "Artikelbeschreibung", + "Specifies what information to use for the description of the product.", + "Legt fest, welche Informationen zur Beschreibung des Artikel wie verwendet werden sollen."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.DescriptionToPlainText", + "Remove HTML from description", + "HTML aus der Beschreibung entfernen", + "Specifies whether to remove all HTML from the product description for the export.", + "Legt fest, ob fr den Export alle HTML-Auszeichnungen aus der Artikelbeschreibung entfernt werden sollen."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.AppendDescriptionText", + "Text to be appended", + "Anzuhngender Text", + "Specifies the text to be attached to the product description. If there are multiple texts then one of it is selected randomly.", + "Legt den an die Artikelbeschreibung anzuhngenden Text fest. Bei mehreren Texten wird einer per Zufall ausgewhlt."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.RemoveCriticalCharacters", + "Remove critical characters", + "Kritische Zeichen entfernen", + "Specifies whether to remove critical characters (like ) from the detail description.", + "Legt fest, ob kritische Zeichen (wie z.B. ) aus der Detailsbeschreibung entfernt werden sollen."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.CriticalCharacters", + "Critical characters", + "Kritische Zeichen", + "List with characters to be removed from the detail description.", + "Liste mit Zeichen, die aus der Detailbeschreibung entfernt werden sollen."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.PriceType", + "Product price", + "Produktpreis", + "Specifies the product price to be exported.", + "Legt den zu exportierenden Produktpreis fest."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.ConvertNetToGrossPrices", + "Convert net into gross prices", + "Netto- in Bruttopreise umrechnen", + "Specifies to convert net into gross prices.", + "Legt fest, dass Netto- in Bruttopreise umgerechnet werden sollen."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.Brand", + "Manufacturer\\Brand", + "Hersteller\\Marke", + "Specifies the manufacturer or brand to be exported, if a product has no manufacturer assigned.", + "Legt den zu exportierenden Hersteller bzw. die Marke fest, wenn fr ein Produkt kein Hersteller zugeordnet ist."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.PictureSize", + "Product picture size", + "Produktbildgre", + "Specifies the size (in pixel) of the product image.", + "Legt die Gre (in Pixel) des Produktbildes fest."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.ShippingTime", + "Shipping time", + "Lieferzeit", + "Specifies the shipping time for products where it is unspecified.", + "Legt die Lieferzeit fr Produkte fest, wo diese nicht angegeben ist."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.ShippingCosts", + "Shipping costs", + "Versandkosten", + "The shipping costs to be exported.", + "Die zu exportierenden Versandkosten."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.FreeShippingThreshold", + "Free shipping threshold", + "Kostenloser Versand ab", + "Specifies the amount as from shipping is free.", + "Legt den Betrag fest, ab dem keine Versandkosten anfallen."); + + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportDeploymentType.FileSystem", "File system", "Dateisystem"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportDeploymentType.Email", "Email", "E-Mail"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportDeploymentType.Http", "HTTP", "HTTP"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportDeploymentType.Ftp", "FTP", "FTP"); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportDescriptionMerging.None", + "Not specified", "Nicht spezifiziert"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportDescriptionMerging.ShortDescriptionOrNameIfEmpty", + "Short description or name if empty", "Kurzbeschreibung oder Name falls leer"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportDescriptionMerging.ShortDescription", + "Short description", "Kurzbeschreibung"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportDescriptionMerging.Description", + "Description", "Detailbeschreibung"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportDescriptionMerging.NameAndShortDescription", + "Product name + short description", "Produktname + Kurzbeschreibung"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportDescriptionMerging.NameAndDescription", + "Product name + long description", "Produktname + Detailbeschreibung"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportDescriptionMerging.ManufacturerAndNameAndShortDescription", + "Manufacturer + Product name + short description", "Hersteller + Produktname + Kurzbeschreibung"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportDescriptionMerging.ManufacturerAndNameAndDescription", + "Manufacturer + Product name + long description", "Hersteller + Produktname + Detailbeschreibung"); + + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.Name", + "Name", + "Name", + "Specifies the name of the deployment.", + "Legt den Namen der Bereitstellung fest."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.IsPublic", + "Copy to public folder", + "In ffentlichen Ordner kopieren", + "Specifies whether to copy the exported data into a folder that is accessible through the internet.", + "Legt fest, ob die exportierten Daten in einen bers Internet zugnglichen Ordner kopiert werden sollen."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.FileSystemPath", + "Directory path", + "Ordnerpfad", + "Specifies the path (relative or absolute) where to deploy the data.", + "Legt den Pfad (relativ oder absolut) zu einem Ordner fest, in den die Daten bereitgestellt werden sollen."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.DeploymentType", + "Type of deployment", + "Art der Bereitstellung", + "Specifies the deployment type.", + "Legt die Art Bereitstellung fest."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.Username", + "User name", + "Benutzername", + "Specifies the user name.", + "Legt den Benutzernamen fest."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.Password", + "Password", + "Passwort", + "Specifies the password.", + "Legt das Passwort fest."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.Url", + "URL\\Host", + "URL\\Host", + "Specifies the URL or host name where to send the data.", + "Legt die URL bzw. den Host-Namen fest, an die die Daten bermittelt werden sollen."); + + 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.DataExchange.Export.Deployment.EmailSubject", + "Email subject", + "E-Mail Betreff", + "Specifies the subject of the email.", + "Legt den Betreff der verschickten Daten fest."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.EmailAccountId", + "Email account", + "E-Mail Konto", + "Specifies the email account used to sent the data.", + "Legt das E-Mail Konto fest, ber welches die Daten verschickt werden sollen."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201508091512101_ExportFramework.resx b/src/Libraries/SmartStore.Data/Migrations/201508091512101_ExportFramework.resx new file mode 100644 index 0000000000..3f0be8609e --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201508091512101_ExportFramework.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/201508121735397_AddSyncMapping.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201508121735397_AddSyncMapping.Designer.cs new file mode 100644 index 0000000000..da67ef8766 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201508121735397_AddSyncMapping.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 AddSyncMapping : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AddSyncMapping)); + + string IMigrationMetadata.Id + { + get { return "201508121735397_AddSyncMapping"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201508121735397_AddSyncMapping.cs b/src/Libraries/SmartStore.Data/Migrations/201508121735397_AddSyncMapping.cs new file mode 100644 index 0000000000..ce062a376e --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201508121735397_AddSyncMapping.cs @@ -0,0 +1,113 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + using SmartStore.Data.Setup; + + public partial class AddSyncMapping : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + CreateTable( + "dbo.SyncMapping", + c => new + { + Id = c.Int(nullable: false, identity: true), + EntityId = c.Int(nullable: false), + SourceKey = c.String(nullable: false, maxLength: 150), + EntityName = c.String(nullable: false, maxLength: 100), + ContextName = c.String(nullable: false, maxLength: 100), + SourceHash = c.String(maxLength: 40), + CustomInt = c.Int(), + CustomString = c.String(), + CustomBool = c.Boolean(), + SyncedOnUtc = c.DateTime(nullable: false), + }) + .PrimaryKey(t => t.Id) + .Index(t => new { t.EntityId, t.EntityName, t.ContextName }, unique: true, name: "IX_SyncMapping_ByEntity") + .Index(t => new { t.SourceKey, t.EntityName, t.ContextName }, unique: true, name: "IX_SyncMapping_BySource"); + + } + + public override void Down() + { + DropIndex("dbo.SyncMapping", "IX_SyncMapping_BySource"); + DropIndex("dbo.SyncMapping", "IX_SyncMapping_ByEntity"); + DropTable("dbo.SyncMapping"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + string attachHint = "A file that is to be appended to each sent email (eg Terms, Conditions etc.)"; + string attachHintDe = "Eine Datei, die jedem gesendeten E-Mail angehangen werden soll (z.B. AGB, Widerrufsbelehrung etc.)"; + + builder.AddOrUpdate("Admin.ContentManagement.MessageTemplates.Fields.Attachment1FileId", + "Attachment 1", + "Anhang 1", + attachHint, + attachHintDe); + builder.AddOrUpdate("Admin.ContentManagement.MessageTemplates.Fields.Attachment2FileId", + "Attachment 2", + "Anhang 2", + attachHint, + attachHintDe); + builder.AddOrUpdate("Admin.ContentManagement.MessageTemplates.Fields.Attachment3FileId", + "Attachment 3", + "Anhang 3", + attachHint, + attachHintDe); + + builder.AddOrUpdate("Common.FileUploader.EnterUrl", + "Enter URL", + "URL eingeben"); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CustomerNumberEnabled", + "Save customer number", + "Kundennummer speichern", + "Specifies whether customer numbers can be saved.", + "Bestimmt ob Kundennummern hinterlegt werden knnen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.DisplayCustomerNumber", + "Display customer numbers in frontend", + "Kundennummern im Frontend anzeigen", + "Specifies whether customer numbers will be displayed to customers in their account area.", + "Bestimmt ob Kunden ihre Kundennummer in Ihrem Account-Bereich einsehen knnen."); + + builder.AddOrUpdate("Admin.Configuration.Settings.CustomerUser.CustomerCanEditNumberIfEmpty", + "Customers can enter a customer number", + "Kunden knnen Kundennummer hinterlegen", + "Specifies whether customers can enter a customer number if the customer number doesn't contain a value yet.", + "Bestimmt ob Kunden eine Kundennummer angeben knnen, wenn fr diese noch kein Wert hinterlegt wurde."); + + builder.AddOrUpdate("Common.FreeShipping", + "Free shipping", + "Versandkostenfrei"); + + 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 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", + "Default product sort order", + "Standardsortierreihenfolge fr Produkte", + "Specifies the default product sort order.", + "Legt die Standardsortierreihenfolge fr Produkte fest."); + + builder.AddOrUpdate("Common.CustomerNumberAlreadyExists", + "Customer number already exists, please choose another.", + "Die von Ihnen gewhlte Kundennummer existiert bereits. Bitte geben Sie eine andere Kundennummer an."); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201508121735397_AddSyncMapping.resx b/src/Libraries/SmartStore.Data/Migrations/201508121735397_AddSyncMapping.resx new file mode 100644 index 0000000000..3299ad9347 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201508121735397_AddSyncMapping.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/201508142203054_CronExpressions.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201508142203054_CronExpressions.Designer.cs new file mode 100644 index 0000000000..85ac9e4af4 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201508142203054_CronExpressions.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 CronExpressions : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(CronExpressions)); + + string IMigrationMetadata.Id + { + get { return "201508142203054_CronExpressions"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201508142203054_CronExpressions.cs b/src/Libraries/SmartStore.Data/Migrations/201508142203054_CronExpressions.cs new file mode 100644 index 0000000000..9f12663ade --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201508142203054_CronExpressions.cs @@ -0,0 +1,174 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Linq; + using System.Collections.Generic; + using System.Data.Entity; + using System.Data.Entity.Migrations; + using SmartStore.Core.Domain.Tasks; + using SmartStore.Data.Setup; + + public partial class CronExpressions : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.ScheduleTask", "CronExpression", c => c.String(maxLength: 1000, defaultValue: "0 */1 * * *" /* Every hour */)); + AddColumn("dbo.ScheduleTask", "RowVersion", c => c.Binary(nullable: false, fixedLength: true, timestamp: true, storeType: "rowversion")); + DropColumn("dbo.ScheduleTask", "Seconds"); + } + + public override void Down() + { + AddColumn("dbo.ScheduleTask", "Seconds", c => c.Int(nullable: false)); + DropColumn("dbo.ScheduleTask", "RowVersion"); + DropColumn("dbo.ScheduleTask", "CronExpression"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + + // Seconds > CronExpressions + var table = context.Set(); + var tasks = table.ToList(); + + foreach (var task in tasks) + { + if (task.Type.Contains(".QueuedMessagesSendTask")) + { + task.CronExpression = "* * * * *"; // every Minute + } + else if (task.Type.Contains(".DeleteGuestsTask")) + { + task.CronExpression = "*/10 * * * *"; // every 10 Minutes + } + else if (task.Type.Contains(".ClearCacheTask")) + { + task.CronExpression = "0 */4 * * *"; // every 4 hrs + } + else if (task.Type.Contains(".UpdateExchangeRateTask")) + { + task.CronExpression = "0/15 * * * *"; // every 15 Minutes + } + else if (task.Type.Contains(".DeleteLogsTask")) + { + task.CronExpression = "0 1 * * *"; // At 01:00 + } + else if (task.Type.Contains(".TransientMediaClearTask")) + { + task.CronExpression = "30 1,13 * * *"; // At 01:30 and 13:30 + } + else if (task.Type.Contains(".QueuedMessagesClearTask")) + { + task.CronExpression = "0 2 * * *"; // At 02:00 + } + else if (task.Type.Contains(".UpdateRatingWidgetStateTask")) + { + task.CronExpression = "0 3 * * *"; // At 03:00 + } + else if (task.Type.Contains(".MailChimpSynchronizationTask")) + { + task.CronExpression = "0 */1 * * *"; // Every hour + } + else if (task.Type.Contains(".AmazonPay.DataPollingTask")) + { + task.CronExpression = "*/30 * * * *"; // Every 30 minutes + } + else if (task.Type.Contains(".NewsImportTask")) + { + task.CronExpression = "30 */12 * * *"; // At 30 minutes past the hour, every 12 hours + } + else if (task.Type.Contains(".TempFileCleanupTask")) + { + task.CronExpression = "30 3 * * *"; // At 03:30 + } + else if (task.Type.Contains(".BMEcat.FileImportTask")) + { + task.CronExpression = "30 2 * * *"; // At 02:30 + } + else if (task.Type.Contains(".StaticFileGenerationTask")) + { + task.CronExpression = "0 */6 * * *"; // Every 06 hours + } + else + { + task.CronExpression = "0 */1 * * *"; // Every hour + } + } + + context.SaveChanges(); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.Delete( + "Admin.System.ScheduleTasks.Seconds", + "Admin.System.ScheduleTasks.Seconds.Positive", + "Admin.System.ScheduleTasks.RunNow.Completed"); + + builder.AddOrUpdate("Common.Rule", + "Rule", + "Regel"); + builder.AddOrUpdate("Common.Scheduled", + "Scheduled", + "Geplant"); + builder.AddOrUpdate("Common.Unscheduled", + "Unscheduled", + "Ungeplant"); + + builder.AddOrUpdate("Admin.System.ScheduleTasks.CronExpression", + "Cron Expression", + "Cron Ausdruck", + "An expression that defines the schedule for the automatic execution of the task.", + "Ein Ausdruck, der den Zeitplan fr die automatische Ausfhrung der Aufgabe festlegt."); + builder.AddOrUpdate("Admin.System.ScheduleTasks.Enabled.Hint", + "Enables the scheduled execution of the task in accordance with the cron expression", + "Aktiviert die geplante Ausfhrung der Aufgabe gem Cron Ausdruck"); + builder.AddOrUpdate("Admin.System.ScheduleTasks.StopOnError", + "Disable on error", + "Bei Fehler deaktivieren", + "Check the box if the task should be disabled automatically when an error occurs during execution", + "Aktivieren Sie das Kstchen, wenn die Aufgabe bei Auftreten eines Fehlers whrend der Ausfhrung deaktiviert werden soll"); + builder.AddOrUpdate("Admin.System.ScheduleTasks.LastStart.Hint", + "Start date of the last execution", + "Startdatum der letzten Ausfhrung"); + builder.AddOrUpdate("Admin.System.ScheduleTasks.LastSuccess.Hint", + "Start date of the last successful execution", + "Startdatum der letzten erfolgreichen Ausfhrung"); + builder.AddOrUpdate("Admin.System.ScheduleTasks.Duration.Hint", + "Duration of the latest execution ([h]:[min]:[sec])", + "Dauer der letzten Ausfhrung ([Std.]:[Min.]:[Sek.])"); + builder.AddOrUpdate("Admin.System.ScheduleTasks.NextRun.Hint", + "Date of the next scheduled execution", + "Datum der nchsten geplanten Ausfhrung"); + + builder.AddOrUpdate("Admin.System.ScheduleTasks.CronHelp", + "Cron Expressions help", + "Hilfe zu Cron-Ausdrcken"); + + builder.AddOrUpdate("Admin.System.ScheduleTasks.FutureSchedules", + "Future schedules", + "Zuknftige Zeitplne"); + builder.AddOrUpdate("Admin.System.ScheduleTasks.EditTask", + "Edit task", + "Aufgabe bearbeiten"); + builder.AddOrUpdate("Admin.System.ScheduleTasks.ScheduleExecution", + "Schedule execution", + "Ausfhrung planen"); + builder.AddOrUpdate("Admin.System.ScheduleTasks.InvalidCronExpression", + "The cron expression is invalid", + "Der Cron-Ausdruck ist ungltig"); + builder.AddOrUpdate("Admin.System.ScheduleTasks.RunNow.Success", + "The task has been executed successfully", + "Aufgabe wurde erfolgreich ausgefhrt"); + builder.AddOrUpdate("Admin.System.ScheduleTasks.UpdateSuccess", + "The task has been updated successfully", + "Die Aufgabe wurde erfolgreich bearbeitet"); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201508142203054_CronExpressions.resx b/src/Libraries/SmartStore.Data/Migrations/201508142203054_CronExpressions.resx new file mode 100644 index 0000000000..62a0f6a2fb --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201508142203054_CronExpressions.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 + + + H4sIAAAAAAAEAOy92XIcObIo+D5m8w8yPd0ZO1cqqbvM+rZVzTWSIqW0o4VNUtI554UWjAQzoxkZkRULlx67XzYP80nzCwMgNux7RGRm5YuUDDgcgMPhcHc4HP/f//P//vY/nzfpq0dQlEme/f763ZtfXr8CWZwvk2z1++u6uv/vf3v9P/+v//1/++18uXl+9aOD+wuCgzWz8vfX66ra/v3t2zJeg01UvtkkcZGX+X31Js43b6Nl/vb9L7/8j7fv3r0FEMVriOvVq9+u6qxKNgD/Af88y7MYbKs6Sr/kS5CW7XdYco2xvvoabUC5jWLw++vrTVRU11VegDcfoip6/eokTSLYjWuQ3r9+FWVZXkUV7OTfv5fguirybHW9hR+i9OZlCyDcfZSWoO383wdw03H88h6N4+1QsUMV12WVbywRvvtLS5i3bHUn8r7uCQdJdw5JXL2gUWPyQQq9ZPGXaLuFU/v6Fdve38/SAsFSBD7DVIZgSYaJff4cr6NsBd4QqP7tFVPh33o2ef/m/Ztf3vzyb6/O6rSqC/B7BuqqiNJ/e3VZ36VJ/O/g5SZ/ANnvWZ2mZN9h72EZ9QF+uizyLSiqlytw345osXz96i1d7y1bsa9G1GkGusiqv7x//eorbDy6S0HPGgRR8Kg+ggwUUQWWl1FVgSJDOAAmLtc601YzBfoW1Viu87qIARxXhwayNZ7DL9HzZ5CtqjVcuL/CBXmRPINl96VF/T1L4GqGlaqiBtZNNwNAv1Vt/zJK21AoVOC5mqfxhuafonKtaPuvZk1rhokXIuQMhklManV9kvYQ/gzWxdM8T7um0G8QZVoqQjEBlt+y71Xc1YRiBNwkG92M/PZ2EF9KofaPOsKf4IAqF6mWFCCGX1/ekIiOMk3elmY9jiOHPoAyLpJtszH6tq1pKim3afTyOY+jNMBAjRr7VixB4bdJLMoP4D6CPMkt0VDrDNRgeQ4XTXpSVVG83oDMZcF9AWUZrUD5RojwuPDkbREE89YoYE/gJCAep5YUVKHfMDPCgVo2BYep2kLh9h1iHV0kKeCIoll62Hho10qSRcWL2c7pJR3fj6OtfIEbKmpzgpa/Ro/JCrOCYApev7oCKS4s18m2ZSjhQr9twC+KfHOVpzL5gqFuG10Mji3Xgt5ExQpU5p0mUFn0naqlGwLx1XAkZA3RgIxl9of8KUvzaOkkppdJ9KZDcBTLCjnS0uhjnfStNr8tF/H3EnS4vhe8pm2rMfGoRrMPurY6MRpaqGJLMKs0Ei7MWJAYydRCPExD589wTKVaoQ3T0qL8Cp58+WlR3hQR7C/ILDVMAaNvl2iljWwWEiI6iJJ6lIHyti6LJC8wnI9WinZSpcE1iu6EWtXatCF01Jt8+sHd5JMM7Qps05cg4zNqZ5IxnZ2N3sRpHI/exnV9908QV+ObYKf5UuWgDuSJLIDr1iEgDdIpigSUnsY0RCPpkEYDwHp/HOd1Vnlb9CBbfomyOkrTF+v9WWoeDUZJOYV51Bk75uZRZ1CZDogkuW5EtzSwcCAUjMq8owG9rDq6W+66DYnnqNxolqnKs/Lrr6O4vBvXsM6hZNi4uq1PeanaIUYa4WVesIde9iZzoTHWxup7VJZPebGcvuXzDAFfl94eAuRvaI4M4K6K1wKsNdLpwWX0gmT3F1Ctcxd3VFu/fEMhOkotFYMShLp+KSugs3L+aqoH2jLsc5zWS7BsTpFBgfbGxbIcXfPt20UbXPGibvJ9GCW4a/MaahQoOKYh/xSjbUeJO4BWQBs94atXfkmyZFNv8AHlyaZROVotF8TJJoJS6LKAv9rwsb+9fnWNzk0hN2kxR88jYW6wXYES0jquwtHiAoKbHUQbLyU3YVrkyzquTutsCVcSXNhQOS6Su7oCF0laoYNka/l6FlVRmq/e6FAfJa68rYFovozWEz0Yoh9RWntjW5RwQUIrCcSQPOGMzIFuQotMx5K3ZP3BSLOoxtltNnW9TDmuoZBL97hW5W21xPJdEQ2xAyHrAtD8sECzMQ65mXb4LgFcIlkVrYCv3aFXQ0O4PdfQuDTbrIOdG0IW7AWFpSElMMmTJbhZ15u7jPA+uCL7ASf+LvWeOSwcyrWD8B8t7C2oQ9jjYFLjRSV3D7ErdYJtjnWvumyRdpt624LZgG+ZOopB0qD6/ZuBt40SshuG0QDMu67qtK2i4a9eHJUKhXhsSISa89UFLqMCtvmxyOst7EgY/aLdBBbZMnlMlm4nReNs5bZnXZPv7caGf5j2TpabJDvLNxsi2Ga0xjquBRu4JfubnHBynr5ln/INuAygK3Z4wikMX0AVQaGBfPcqp1wYLRQ1NiXjoPZukkp5byHMyE7SNH/qPbrgMQFP3irvyXZb5I9geQWlcba6rjd+M/01rwJj7NDdwA0jZQbt3cdwSNuQj5v8JPa2HD4nm6RCncN7o/cUXz/U4y+6KKvvoxipIAXcR6uv0IgapMZozX6ELDZ6I4vyY3JfnUWFtwnW4QmhrVyBP+qkAN+qNaR4s514c0qLc1B+VNI6UAjTSY104yqJkW50slwyffAe06IcIvLDRHf7ztz3LG0WeIfQe4xwVpoF9+2ew+no0WrRnD9vkwIr6h+iFxanGYqTuEoeMYoQbP8pKq8jqDWBULNKY7O8zwR7g8IhTlYFAKTi6BEeMCC7Ac/ja6KL8grEdUFeaXaXHi2is5c4BU2nfGUcifESFEnuvfp6nHjvx4g918qiRCfPTYiINzsuygs4/d1Rtrd2t1wmaOVFaYfxbI1cGuaOavsbC9Hz+TO0a7zXwqI8L73JCXtzBg2sVV54Z4SAKg40ihbZI1yaEF0XZeCpNlZ5/BDsEAKZaxjjyWOUwLpJSmD19BsLe+q8YSVZwJF/zp+aUaOdJkDuD2ggJPcv2CFwkRddH08BtL48j66i+CFH5jTKuON9zoysQYRx0dASWiC90eutUWCjv41DCTNJDcYm/iQMRkwAsGyRESHl4ymrcC2gDp7WL6d1VQ3OBY+1haB/JuU6TcoqDNJ28acAMi+U65T/xhXnGdTOMbok9j/RIpEE34G+pctxG2gNkzMifDd4G9dbiCdKLQdijvO6gjZ6d8rmcIuCxHWeLT0wde6s8wyd0gXhr1ZsUZjByEzRCrZJ24TGx00CmjXpLfDhHgrKqj+MPMs3d0nWnssFZELY5y64oTzZbtPEX2P+CZLVerylSNsxwdH/TJYjYv80Lm36ncZXnvSI/IRJuBOLcFEXIAUukXu7G3LBDzB5BMULqmzpPSGzf1lWPY1K0O4+QczuHt8XEJV1AZrMZlJ1Mkgeqr7NsJHYPVr0g0ZtRlocBoFPtDQeqDDu9TZMBBQo8iKU44NCiqgRGuf1Om/8KVFh7+mQRy01W2K/RQqjX7rS2xb6Jh985UP4ixyKi0lSgNqGIJHSQBW6c0sDclE7VLksYIcGcgwwGqJ+DGPE3EOmtLFgkhAryxG1Di9sEMsH1LnFFKFTLIhsABycY8fJQ0Rl30lARf9FYLIxCGEdx3GZYDzKIbQwit4zELKOs2COfe4PweVdbkAUPaYBZB1moBz7iy3Q+6RJkEdGAMv7L66iGI+6gmx8mlqO472JVsrB3VJwvDwliqXilIRxlKY/oiKBSp3IjlR2X1FPMUEGtWSzZFLVcapY1FYDtxit7RCdx0Wn+1XwIA3IMyFZLuVCCsiWDdmzYUVnWVC+uzSEtMMMmG2XSR+OoLt9sYgzuEKOF3gI0eybZzfsb7o4ZH1uldr+1zGuWd6WJsp3pJym3cyEiNT4XiJbKdD9JfmNqOBOra4pW6vc+uS3dZm5JDFqvGQONdvoqvIsr7fojvbSe2Z4TB4PBpjNDI6PxKvMl0cHTIh4tnEgOjP+JteYYrzhTVZQ2fIDHLctKYFtNyeBi2EMj4R+DJzzwtgj0aJqOV+eV6tvU1xDMA4RoHwoQmivC0ad3e1xw6hDcdyK5W1ptuKRLtyg+y+BWtY58Ke7K3GaV5BPJ73Wgx6qGn1n6pZRqAs9xwszQUbW3OgLFQDYetosT6rQTarr5F/Ar23q6k95kzcZQVjczhE5LZ5vdPCYh91hEAF0hR41G5+7A9+NCx+/sMM3h3btFPwgUxhwpMLpAX8k4OmL2qhylMahDx8dTBb9AaTAvjH2wjZiWtj74aSqAxp6zJZxmjwH4JcdoOuB4zMhbf2j4q4QXw2Jxnohw+ABIMPHCm13DJAbvJjxl0CXHw/mHYvQJ7e8CBFDaE9uGXnjJk4onP45R47iZap8ZjKzYjrdyGkrdV4NkmM02aKxXMFhYy50fQ2RJkjgFwxyukbgO65i5boJ8hiEgOxXdQqMskCH8atcb8Hop1anSZqiqMcuvbPn7cfuUmoYdF32YWxAUImvrTw2A5pewqvcFGEmj2iUTCI+ZcPdbOi9QKFbbv0VlvN0/gwFFfmi6AQ28hD/MN4JktQ6Vp43ue09VBSux0PeJKLjbiNvS7MRjPOQ91me5sUn8IwTUk/e+l687W1rZwU4emUwHVeNvK1Allaog6BFeQEiNBGdgjCjX1u6Uw0canAHYACWXwLof0iMIh5wBBPO5daCrr9BrLhL/hqBv2wgsR3lw+jygST3n0JG0NxqeN+GrqS+dEP9IVmH8gojyA+fm0Mm/Q8iSwIJkaP02OX4rkkjbghOOIYo7VaI0jGoaA+Dio5xNvsXZ+N0ykVv7vwZl6icUxSEQCGsjeYqrb+d0eAJpiO8Qpkn2vZx1E0rYvCTgFn1egpdXie9Q7mwEeEmSdTapFP3I8onkG7v6/Q/QYmTnwZB9jV3weWjw9veQ5fo7ZLb6pYdbbC0tMhAWcK1qku9wNW5ZdazZETSGuqr9vJqXmcH3ap2ET+dztH/Oton8rY6Gn2sk77V5rdDdmlN0FigV4A1r7eHacXgOfCwDV3kxSbyj/dpsV1HqTrFVJDs+lO+ahMw+fPJ/X2SJgHMVJwT2NuWCaRXL8omHOQkpp6LdL45YBJbEoaPPkdltdieLJfQ2pngOlVIqwF1/XO+SjLHK9OofpdZWorC/apwQ1KJwtCJ/lsCjIhY50r5mHUexFbRaUN++rlXdZOFFfSVBpF3mIGz7TVrc6i63cLcDhoN328WhlO5pIC29xfIyCUNuRlQAbUpCDmxaTBbWp8/I4UmSk/qao1Umib10RWIG9+gYASqGsJpMKrAzYlZLdsJgmpAvbnMS8kNmb5YOBC+lOu1AMSpizf5NokVfcTl8k7SxeJeMjC23cQOJnEPcZGwc3QJ1y+m2LZLVwDaGBmKsAKyCaZAhF0UQ3BdlYDZd/kpKpaXeZJV5aekrGSH/gI4Se/lcIIxKIBtR9JFIBrtNRywQPwxMHIByAJa550i0oHKc0myUELyS4E42sshvQz7nqD2dj00NPLsTYvgaNPL27pIirKaJCocqa+TNKQz+sO0AllsG2UvE7QjDknXWGSQO1D61scki23P9M6IB1FGfOUWL813UzX0fvSG/ivZIl0pSjUpqAI93LvOM6B93DLQC8jdq37jM7uXvS23ZJo1JNwKu422hxk2QKaI27TZcuu9mlykys4xkHwXKQBpR2kov5RPHbk8YvVbHMftWd6WZsM09XK5xGOUrePD24OKkYXKp3/zlH8GiIiLMtcI2vcBBNLNugDAtMG/BGgQClpQJDHTmOdT0D8ibxdvuMiQYCEdvrEvCjuWvYnW34ET2lIkzO1QuRFvTDoSPbTAutJWsbZvyY1AMixqrxDsjkIAvvNCKC+zkKaHww7UIWCm7rgPKcTSwYemhpNLi1UGad28b+sdkXf+HKf1Eq516hqx6uQrzDlbu1Jx82gJ4NOEZ/szZwMhS8jJKcQrqxxbSGQ3cUXr7h76MoXpKK20zOv9/O48yvfdXQEek0gj9cIccO+CWudktjuqJ9ziVyoxXuFo/YFvgKi0FtdxzSvWfEsq7/tyBsElYe4JLFDiTsh6B/UoozaaIHwAhOwsTRop4bSsT9N81cevWS9pVLt8Q+DYjRj3tjOTxI2jwSMHeTgVssMoZCmC1rcD4MBOonKOlYRA3mzUdMWDhxCC414gb+tzlK3qaOUdsxnq7oZtbqt8GeIAzukSXsPs3mZrt7O1+M4Ej7/aKuN5FR5p80jdyIJvngd2jtdid/GC5UgHnMQ+JfaiOO2GrC9XuWWadrWTzdJuYqwDFN1HqlC4XdMQXnv10AnrvRonmUr+hYHfdHiOW7a8rXl8Ld3MtFRXmnxjtA9r/1GDazDFUeZFGq0WGzhYlLJbe1cjxEXR6nCfkRjB29XkpWsm4wqUWLSJhbkIUigxlYCceFdDex3ZiVB7S1UBzqOE1cu6AMltMa01ImSkh0G71nUpJEcykhYlWl2Xab1KMk6IaC8l5nUsEj5BVKdAQoFVqcxEiJNQuJTcDQ+VRoJAuRveNqp3vsvwZ1S2AwwX+HJJ3/6fMnGAMjeCPt+AEwN+BU+lu0sX1X5DoNgNHus8upNkHZnSfYwoje6WhHMfdxiFjE5M7O0AOLC1qJxjYiGQN8s2XXHnV1T/qCjJ29pz7/H1Oi9CLEfLZi+w6Jq61WAW3TxO2j+jz32nPKpHp3mQkbmZLP2WKDJTuELh7kpD2F7AIfZnsafFSQtg/SpKVcHNcsrT9EdeYZMsxomHrLUBhKJ8wyLaDR0W9eokK58cgnnkpk2PU2zXMIS4JeEJY0YOxlswClg/u5nomse8NyiOiqCaD0eOIR1JLWqua367h/w37LA79JxfjkSEZBm2S6SBoZceUSRcbmS5dRpFZrlKEif6ywkuRaKBTHGWE14S4igb5G2FMhLnkQ9NwrZW0x3/rnowI22dP33LPkFF4hJri572EjK+PuL0Ojc5kpS7c7VxHmt0tAuVSisAS2uRBUAVCOW9u+Y/yFi5nDfciETyXLRReV0mQKiD3CRAiI5yXd6W7lTz119HcZ4VAAS7IR8q9SlE1Eo0hIeZAo1oDZLstMtS2jC/59ZhkqLUdHaNpQwoNklZanIQslDybIpqSF4SqcH9lEsGt8s1bBDXBcTKEeAon5zl06j64jyycXgScPSWPVKhBlvEnJ5jtuadFrEqLamXtqFCfFzdCl4PdIlxmhR43Sw3o7tPJkgQ1jXZKiYTtvwN8TLmvmmaOoljUJbTNHiJLkBDo9VIzId+yVx5MXSk/MyskLXL6uwka/tkyg6CFdcth4TORxEqbwtngvZ+oDmQHA4UGWVrN13u5fsIc1w8t86RzkoORRp10w4OCcw1XSQBRZ0kMqHLu0kA+cuztjNeAg3jOEo0eVuYSjsj0dBsIQS+iNrkhKpYFMNbSPZno+2bCT7d/5GAJ08UKCG2U0IECZ5QU9zha+Su3RHCYWwIdg9SCGWt5NEKqz1B18EWSNg7/FPVtQbAfwvwlP5HwS9vCxPoY5HX25FPmUe6qzVlcOPX4ZkZL0IF2RyQBA1ihxziFvHnetN5WMNyaX5LAjHSnCgTS3MSwF+at53wEukYx1GuK4TMwUvjg1zjU0XLNGvJX1wIH0oTyRMncdHOrbWkaJ5aa/47Cgl5W5hAEz27gtvyftUZD3hX/BT0q53ezgr6VTbLB5UweVGm2TpYR8Jgu4xe0K2UoMiaNNpjniRJOOasLgqQxS+TPL/UNXYVDWGjH0CcbKL09avLAv7CUuz1u7+9fnWNMij8/vpvzmuBDsTynacfUTWlWLmu7yooEtNFFqc3CK0Rsf5qPSyqMZS2forGblBjcG7wK9pTjZBqdJqRtlJnmhG2jU06MtiQxVK2b4wSjnAXSZCKEKUXAIxNU3nLYxNY3vLY1G7xj5/aE/PQ6EzarfVxW8FiZbQmoOJdZ0uwPNmMOhLyFeCfoACQ7QKkV1+D+CGvq5MK8tFdXYEpTWmu8fI/NhO8btqqHqGuOZ3c3ydpAtdkMPtgOzoN8P0gZNSgl40LAOXXGeQtWndyZimIiYyqH28iUZcnUcM52oyXyCAqH8BSNiWjjvDs8XH8l2RRQ+fP26TAjoQveVatJ27zP0E0Pj3J9dU8cvQB3CXe11YIVCcx3jc/5elyAv7gG56IMYmGT6PsYRIDmGlzEhFDtrk4m7K5/n2h6Zpc3EUTKBftbooVwD4V3tjrvoYGQdGmcb0poqyMYvRzUA0mb3qSJSNr/AqUdTo+2c+iLXKkT0twvtGJRntd3/U6+rRDvqyLeB2VYEoH/mWUuF76Zl9THXte2uaQfQ4FzrZuTksm9xp/ACmoDurlLYUlfAXQ0Rtxt9jo2OJTVH4FT60X52uO7tnE1KuE/ked9CmN8MgTL6NbFnA49hSVc5ESQiDbmDxl3GDTgihkkC6RdM05ULDzDX0voZX+KSmrXPJgogjwtj3EHTorh+KOkxWgtm+CfEzusSGnHYQI8PZ7CZY/k2rNDUYPzQ3KoIrt4HAtlPNNwd84JRzXf6aI6yxb7tSzr02KKFnPULGkZ0ORuGdEuW3PrsASgA1YkkLsXPoWKAmlZQotMDcYfQ3r59dh3+VJB7tSnux0ifCheKLYpVdbI2nMQbLSjgGQCD0Wyit8TSwIHV5bbtCUQoF5DFiRt9Ufg3p6eBup5usmHuedreEkxnSHHWpoNtnuI7dW1NC2+kIrU8ZQEIw6Tksup5Uu1hZcA9FE2I7LXN5WRy/fFUrtoCGQUS/CBD9YHEmgdNQ013aHGhpFt/vIrUs1tK1AYRStKZR2owGJdTovgeMvZI6CRd5W5zcjpILouQ8z6RQiUG7kUIVF2XUWZ0uLAnijOoRneb2dyKl9BYmxTZCDaAqvXd/aNAlurkGG7NgpRtY0Nc2wvkBji8ifOlo7i3LgDuw+nNvfOpIDyn5LNnI7cRu46WjEklQ5nltZHX5EElDpriyD99qSiR563UA5voVkYoxDInnfDgmi5182j7/5ovlHHbWj9zI6sqS6LJJ49BDevqHRI3anGM0kIxk7wrjzbTQ64tgko1sbm3azhLlOG976IX/K0jxaBnjOalF2yIKp8Z/hAslK0OG1NIGQtP4JktXawnTR3KWrs2UKoJoVjR/I0Aj4M5wnLxR/SxWlk7LMYxSVvOyUFfGpR0AtSab56bQqf/+uxUGj8LhGcBBprJA2s6rpVg8l6lhbqOhaB+GvV37Fb0F46ZUIxVGvlLcVRBv8SrzZMV12xe4qZj4Ee+ykTauRBGYH+8LlJjj4d1psgoN1r6zTAnzHNajgu0BX7RuCh8BxGqVRFgNPs6wc/ZbZVA60kSSDMpBNFO0iCmtTwXFiQwkc/sQpcECQyWgCHjd1YTwuT2q0ETV9DNFR/snbCqKD3BRR/AApPlGgN76kG9a6wzwDXMPHP4A0eQTFi2P1yXUf4+A6dtFLYu9sQ/7kgaAkxO0gA/gOUgDSIEAayivVE4kyhFA6Ot31KxLTKUgknegQfRp/uXYxhFwHsvUqXi2O9gpkx+wK/IHejHR3EFBojutAsQ5CJBQLtghCmUthDp+uQFTm2UVeNNw0vRuk5V+A3d5BDgj8OqC+PGU2te0N2PHzxlxX0f3913yKFDUny02S6a/2/vWXIA+CULItTF65Hbpi52pREzSR2NIiCIHdKQTzNDZzrJydwU3IL56CxXTc2VTrP8DOdhkVrX5jeULYnOc5VCSnOESEZTAPZJiwkHlSLMElCQrIQygsYjSPZRilYx9E8cDcEuuCllK3JDxpY0jBBJaGHDboHWOuIdFeIgXS99vZI6s63+VaERzzymD0XQ5y6Nsiud7Ctdbdce9lgcuJVFRFab56o8R73B5Hl+diwn/b0olAHPcKlLzqIknhwAK82Q75+ulb1g77kjhack+C5ZyA32mVK1ldtOSNKvCPIRvVsvbNKvjEYbRqdKYkUGGxpIsSlZ/1oByqs+BUoT3KTVuBN/LjUeOG2YwhwcRkkrzrPvdSZ893AkgNP7ko1vKU7UrwEAqgfXVeN3TAMYLwCy72jgLvzyCaVNwrOS+efslxZ80ey9bHUruJVv5mGURyXFrOS+uvgR4p0+3Vyl35dpjKUrDdksWyfZSCCcGbP6IiibIhXftZvrlLsshTI9YjP/KyvK3rhzoAK2vu3leJ6hA0TCNfoqy+j2I0icUlnG1t1FmYZi1905rbTbgjobD9TJbhkH0Ke20qKgGmnG00siFa9INGbXSIc1KWySqDqyjBfLRYqs6g34U5Fu6iBJFv3/LMqTu5QJdgbW/ClfhKnrczbS+Pl6DIjB/CHPtgX+e3uvp2j5E2573WRJUnVSOYQ7XbKza/WxoHpwmYVpU51ozrhzw5MWhb7lg1qOUyWFcXK7mKXQdL47AaMVnVZdhU/RBnTWxj4VXDoz44ukBv0QRztN6A5wp+2mzHT4+PUvP8USeF/2V1Yr1kVZGnIeJCZjq9YpeQhXi1lanOgpTlOZfxCP1BpnVsR6h2thoMlUWM8zwq3QLCGrdSyaulgbK+zLFghySE5yHgZnLcRQx2kZM0iTS2UwBJPZOLe7TUL76aUpvmNbS6hNEeuV3eloRu/nE0e7SINNGReZoX13/UUQHKq9Xd6INqfEvLf8KVQAb3B48dba6bTtDQooSY4EYO4gBJlDx0SAYTFg0h1NrPSfaAAn3nzGFnq23tjp5lqHeaKWtOO9JNAorWFe68/fQ4jnuNSrKFif8MccuiC4pGTOfouvb1MgS9GKANPUcDFa76nnVvadBhXYshuJUrAQvpOx2aENjwXKGihyo73TxSCDGiy8159KVs/jvKC3lbE8UqMK1+L1SnOCM1el2m5xmC91aQrkEMeSXMIDTHqnmpvFMb6JDxc77K+0PN2RyQ7NirTXqaL1+meGs1zyq4XrrzqK+gesqLhymCA6BgesFL+KwuCpDFL/6edYzz/DleR9kK4MdCXVEr9g9pI+KAOzTCW2UtIrxOB8zHr2pr2G+Q/MzoR8aAS4ZEQanHQoN6baRDtxwedkNPa+fFy5sOyXFHlbel2VF/HWVv6yZG83DJr6NkkzDPFv431+3kc44Q+JPVxGC5yIsNnHTibtpY7X1ONglksZu80Va9g23QAivXu+QA2qFrz+wYsWw7z5ZwZoNrWHbmzZcI38z1tHJaLEfRLG+rIb+vhtVgmcdwcvDLmPvo1mADsNvvLnUxuXH98g2F5siN8rYwoUa3MQLxqYGfX9HK+zBm6pjcfxrFD4sMR2le13fEEa6zv1qC8bgkgk7waC7wUBl25shELWE9YdYTHSxnoWorhPRGyxoT+KY1oMbjCOK3Jq+b3IANVKe9gotE6I6CRN7WPNrZjwQ8wa6up2/Z1WIzdx/BaVjlxUsAXmZRHfn4yMeT8XEr3AOwMYPpyMVHLp6Mi/FUJLFX2HK+2eTZGxbTkY3lbYVxH8ERfSzyejs9W8KWZ1iFGtfASFHYY7oKTuL0CsQ5erXa3mOLQgcgujc9kuOKG3vFzemwVcbAWQf5LpbDIZj/7WFt2FrPo9KwNTEEZ2pLwLws7O+FzyrM3/T1jwvwkBfgdVqrznFHajVU/oTPUbaqo9VYO9l1vAbLOgU3UfngcuIDq5VvSCTHtSRvSxsYMgon6m4JmTarc3Ln2fnztgBlqb73FihgEeFXtPK3cWgZKny0yrffsvOiyL0fwP0Knqur2uXhls8RfmWkqBzrnmdL11brOIZ84touSbbxGGxRfkqWcGEHSICzQqviEkCFyDLdUFdX/2bquEEqJ/f3SZq4ecv6uuXw87hJyNs6WS7RlHtHYYMUBLgJ6KjESC2OdnRiY6Njj9seirAz2ELexOAg/M7vQFRC/uveC/WImaUwHVlf3pZGPzK9+Wu7Gb+UFdhAOjxhU3Lq5q8Q6caOnh3PEd0y9we4eWelW7gMt1J6ZMfFclwsh7RYLvKi3ngGl2Ec5RsO1XGtKLiWoNPHOulbbn7PFBaGZ9A77Vm+TeJ9DFDj+FcYmiaH4tQ/Bahn0sHkEdKiN8FcFyyN57haFas1RAwo4qFQK/UmD4UJ8uc/Qax8S3ac62EoPeL0Z7AoU2LkbYMuytaYPX05qau1v8+MQHgFNYdtQvhlXHGOJENJNpakkyHFyi0NT2aNkYIJksPIYW0je4elY9J7Elra9wFI13MC0msLwE4Q+Olz7nJFC9ZawfX2hsBylP7ytggyoSZ3Johf+wr4SDJ0JMHCkFly+t/D3HLwZAiAFEwQByCHDfreK9mQSKiIypW9DS5KGjKGECcI/ihS5G3N7LTQeGre79BRpcVtgM02SlYuDox2dy7fdDiOrOvMO2PFTmrNlJEaRtl+9m2LZY+ofVI6WLjd8SLyuMXQL0MG1XE1Oq/GkST5aRyfwylL22NNoArmMe2C7/oPFGQRaL1PE4rXTEIco1ej/NPZBk38cg2yJbohGqXpiy+uk6qK4jUydt5dJNZ5O4fa771q/0Vb21hWfgVP5WeAlrrn0UsvMsUYj5JT3paYYt5nMXhJqsThr6MkvAojT4KqHWNedzlbg/ghr70eyWgePnvDoTouGsWimSVefi9fUrpeJ9stgr9sLuWGwrsob6Ln82dAUMMVFUTUXXwP9hribrwbFUrfknv6WKmheMdIDHsrkGGDM9C0DvdekXFFrzeKxK0EFMLHB1x0WzVLMd/VNo9oP756MpIENJdc08ss9lDDWtg5yawrdM0Z8fVl9IJY4FNSohBPd6ElQXiUWvK2WJL5Ci08EzsabMaOVbjOJDx0y1ce1plpHW6dGVcMus7CLbDjylIw8UucgmZr9lwNCNElKJJ86R+lVkUpxlf6IcKXAtGSDOGXCOV8DXSfaZElVRKluyzJyC4aSbFbuoZcdFGAWnlFQ9sGZ0j3/6nFMmuyWctzJ7GM09Un/8Iv1mGyOEV6YBwY+A2H8Cie5W2FSVrhlnpBgAc/XTBf1qe+/bmanioDlFQYdTMplD7cyrodwAd5I4fiBKkC1FPVS/Fq6BLfOicupBEdBYm8rcsuk/Q7PxHQ43k/j5PW3NVY5GUJl0Xqz2UsqiOf7SqfWWg1PgHwx8B3zV65+gweQep9x2idF5U+Y8hIsXQXEDxc4xpjbtsnihg5IEp6i0CzHCEV1I9EhiEEZHJQFKCYoq0JH3TS3fADf9TNg3Z+Fzvwy4+folKdJ9iFdG5XF2RXFpRXFcJdUbgG7dNnDhmFs/tkVReNxdriOYp8eVvzxJMeYEJeIrbCKYvh8xsCw5FjnTl2X9Op4zQKLpyD6pXNf0e2Uax9fAMr0GUWnYc/TuslWGTXCWwy6v1t7hFgl1FZoptjsKAKEmjQIRydFjdJpXwzNUwrkwTxfwFV1N7gU6n74Rr7AIhQ8Qnam2auAt87uAIZFMkn5c9k2RyyeGFrsPxXno1Ph6apn0W0bfV/ru8m9aGp/UTNm9/IT3O0wQWQL0WSF1gqe50no4HdRKrc2S5zId+T6w2xIzeXbfrrIIhdoTGNXOvNilyUF2m0KvsW3S9zqFsKtqtDmsDJTV8gDclZoafgC9jcgaKTq2l+9/oV1tZ/f/0LN10ULLotg70A0CAGfaV36krN2zgtrOB2TTMh5MeTsszjBBOmxfGPGtRgydDwFnWHGdp5tnzVmKryKoNV28wcsoaF0JALIc2TLaQyZKXfX/+f3ECNmuvPhofmPuRPWZqjpDV0C7+8efPuNauIfcuasIRXKMYBBTOeRWUcLfmlBYm5pL+0k4UO2KEEKiHfJFnFK3pJFifbKDUeCoPBUFlEPezbYks+gC2S8lllPH0mneiud/Ed6dtjSKij2G9vCe40Ztpb8lafCctSFTQM68imdBMCJqX6zLSxU1wqHMm0PCqcL5MusNc95+ZUYrGRLGYn9kjCTCdsyVYF7KxYMTvFzeoRzSZ6BXNq0hei2mzs3Z5XXib4Ydvb9n8pT4vBRYxMQ9pwsKQNAdtKkM/PsuohTMCn6nky6UBbZWcYs42GMGUa9sHpMRiTeaSab2P3GZMewgyMSc+TEWMOb8vPwpjdIYVWVrKAImYczkzM2ZDDay4ZsQ3FYnZiJFknJmAhGV33Qap9SMpGDT7ZwklBbsB2NAkopYykqiRiqg7ehqmUbQgYzJBxvUjTrnQbwnRVxiNL34K52A9Ak+5He6seaZ0GZBHVsqFM0P3KtH8ilxAPp59EJ7FmRLoJZJwRiUz60dWfW4+7hXyTPILiBQcWaTQsClihwzkobzRqEa+R/Rxt+1T1ZjotTEhnI64i6s3NWb060G9JGhbgKig4zEU/kzZjsY3ujKUgHcV0XCqdL5MukAlfdoJLDQ1ZFn5kHt1jW1Y2hhkYdA+tWZSz8D7CRlGhtWhFwCLGJOFsuFKIfw7rVtWRCfhKRed9sHJbpqaGQfGERixJKyqkoCvPaZsU8J+qrZ2Ri9oRTScgtfNp0hWy3k5xtuGOLqozET/v8e6uGsdMDLyHuzxOzphkqy+gWufL2ysAsSUokPYM+QaUzkd9VRET07Vs+NegPZEdhQsDeiOvK6jUwWl7hIwCbjv00k6LoIV0IQGtyCJswZwS8y9o5QgmWMrKOTIyJZsqsy3i7lFtHTMycCI27G9tmjMgi9Wc9cKp5ZI+TMA8EpruE9vQokc3zfRqCctCNG4BIymlZHh2EvZnQqYS0tqkfarifAfi3aXX4XkMGQfwoMJD8f7qrcWhOI9ZwFhmTOsy9tMkTaHW0jWg7SYNH5wKDHpzUvCry4Ea7Y2O4Q61rr9sBRU9+usi9mThmhFtYhKyz69A6UYxgcTSzZfRfkhkcphFXjUpsZrLNNC8wD0fMk7JeEhZS8Svogo2TKtuUMC5Q9KsXeNco6FMwL5Gc2jSDzIx3Sw8fJrmq8u8rPR8y0GKeLUDsuFPHvFe8aS0+xPwoXRO9ob32sd4b3vGUXEJCyzjwP6FXzsm5NAL+FDG4LvBh7IRTMSKsvkxab6rM7dH/go8JuDpE0i393WaIZ2XKtA55zXVFX56rqaDs17Xutxv345u57jabmDTOfIN59nCp99UnI390bNuiwps9IoAByli6g7Ihol5xHulCEi7PwFXSudkLxQB1Ptu7+gZR8UlLLCMAx0UASF6AR/KGHw3+FA2golYUTY/Js13deZTBPI01UtBCkq4reep1a1xGuFeST5h16fYi0VzsBcSD/X8JCufUBRAziUKo5mCgJPxWQNiy20kYpFmKGDg3eA1Qccn4jbBXBipeBB+Vl77kVfYVRXnxfKW4BgVe0jqyHiQBLflRFlTEq4Us/tu8KZmKBPxqWbuTHm2qTUf54Jik5QoQ1M7ks7ljwYqPxtTVxPyL1PDin/VrSlOhhDACCeIZkQSgwc/P5yJGOfPKKtllJ7U1RrxYpMBjJkgKW2MaotIpapoQz6zDuzVoaPVkCaQklZzbNKf2Y8jL/Ki3mC3q5bBeVARN/dQNqwrQG3Fp0ECcuSdmICz5MTdHzbCaZAN+YiGlTJSm5jZkpMY5DOxkrgXU/GSmMD7w0y3+N/myTTlZBOAUjay5iASqYB9iL7t3J4p6/9UjCeYD5Omh1q7IMQarjEQMs2QxxBfDWYZ8+0o3wm6Pq3Ao+bDmO92QP0i+EWvJREDDq+CEchl3Cfk6x1hQckYJtXh+PkxaR5XmI0Vm3d+DSOJRcAiVmyf7DBnQyFi8wDiQPqbqhcTMJKKuibN0zVn5iitPUCDBeSi/fR5iPs+GdftpcXQ5Zn6jlL6dy9693nFZFyirKXKVEZWcMnnJm5QkXps97jUaCgTMK3RHJr0Y/ZUZcKRNJLPip+aZTwV9zatCVhXKLN3lG+pQczFtNS8mXQCV5h3c8dhOmoeZeCk27tt3B+Ld39YUNLzqXZ4fi72idl06U44yBEYbh8Tmkh7Pynb7WHyko/JfXUWoYAI2OV1VILlz6RaDxwkYxdNPRFbdlVsuFLXjEwsirg/3N12w15NwHuG02DEiUIMszMmpUT0LKTjF2EtFVO6ao3qBgXsKVsF88tQo6FMyNPKOTTpR1dnt3j4O7nE7BiZqjoZN9Ot7o8iaj6YuZhaOJ8mnaEqzqu2fs0rYGIjDXBSlRWBWKusBN79YU1Jz6dSVvm52H0b6Qo8weVzmUMEZbd+tL53VSURGwrgbRhS2dxeeelNRjIBt5rM31548EUDMVMEtDWNxZ7Q/rFpyG3BeCU9Qgkr8a05NZFoMFnOTNtrjwzW/dlexB2fYL2K52H3N5eu343N3LGKji8oaBXT2frixA2IcvlJuHp3WFA4hAk5UThHJu33COYNL+gy9ppFrDDQAYMNWMw+Se+8tGZZT6bSnWU0NuWp7Q5ErlyBqi4y9OoVMLkZIQYXqwMEpJ3mLGxiz3Rm1Rgm0ZZV87QXevLQaUO5J6sQ/NJeSAFopZnkuNmzqGj2stM6W6ZAeTajqCPWUmhwO01F3pT8/JAYw2ibhUHPJlFDtFNh0ouh1owqMjMS7bYhrTE6E+7n/qEdxhz8upe7CDcKXViFrMLonLqPsRa6QczBpnsYeUHnxtNxqBBamznROV3iXjKmcgQTcKVyjvaIJa+3IE7u29v/J1VVJHd1BUxZVF1bwbLiig4srOnBHrK02YimY3GzOd4HlheP5NsW/Xkr4Ujp7m6PS6hgKNBYKRsO3RH5mI2W5fxLxWO4U2gs7rxh5FkUYtjRjUTJ4G6yXU3e2bYcZbeM15p46c+/4vzHPvuOZcI37uuvwTP3Kuy25ZtopcgPx8Mq1o3DCqEwy1UwWBwu41mL80dUJFFW9dNylm/ukgwD3sIVkzyC4uUm2Sjy2trhURBOgcKBpsYdEt29JAc+mhfWsaPTyQXbOTXpGVlv7sWvGp+hQWeAYic4fo/tO4th7cbS2ENLz2BU/6hhCeSM71nitSpIPDuxNKgOCdYHNfA5NwNRR3eD40VzatIzst6u8b7rDuAg9gMw9CEJ+N2R6gckyruh6D11pgjmYXClO44b476x/BzeN9v5tlgEO+NsYwf3I0rrfoQct1qyphKZxSrBeAIsFXV/5MtGt2p3dvUYDXi+lWTEHxarikUx9+KyMg1M9X8XL9pOafIzq+sHoZPfXkebbQo+5E9ZmkfyzAZi8MDcxSAXuQ27fo7OW+K+TMhdYkobnQZQNWfjsJsEFHDUMaBeHJFymBhcxGE9pA2PSdBbPn+C+WzuPVo9lAlYVD1VJh0g6+0Ag+ocERzkCGy5j+4Fae8nZcL9dSEQ4fjNT0OPmKyeYj+WX3ow2Jml7e0TsxqOZbotXjeLJj2hKu4QO9sz8nQs7MK8IXVLeVdm4b1DEJ+9pX6RpBXK7q6/pmaBw4gnmepeLKroisu1th2UvfoRzrEW9CxgLpPnvS5X5QVSjJJNVLycP8frKFuBq6gCZ3UBm4hf5FHEuprCmGFUySo4WNuK0DBr+z6OdDbu0wScaTwLZtJaimY3GBT/YceZVJXwLEmjn5kXhZ2ZmgmFBLfgPqr+bGx3GsUPiwz2JX64ru/KuEiaWFvtbWJdRRELSurYMKW22b26Ymw6mgl423Q+bZxZO8fVOiNMU29int5Hn4LhWGZk6D207k7iVPBuu5SLxeAi5u0hbdhVgt7y4GB+VlWPYwIOVc/T3pwanNzfJyn8Am51yYE4SCFPdkBWPMlhnvwRP2kXpuAkGWFN2p47+xl+vdJO+ZRXkT5Q6ro5K5qa6aHvuTVFPfH3QkeE432EK+YLZP1oBW4vinyj5TtFHbFLlAS3c4DKG5qe7Qx6M4lzUkt8k16Q9XaF+W5yW9YbaozKeEQzs7Md35fpmY4nu0kfhlozmhBV8ggn53O+uiV+o8lU2BHSOmJjogexMyfkrYiUOKbzO2hWaMcziW2hnTsj3ZCuuhPsqxWVIuCRGHY/3Y6qEUzMm3upOp6tQfyQ12y4PfdZniXXEIEwa66wrlUOXdPWRUzNjXHnuNt2eBNwvO18G60CtvKM6cvjuoCkWF1GLzit/yJLEF7dQyqKWuJk5nQFu3zmqsbMXxIJotcadWYCrjSaAaN+EPV2hgu7N3M4tjHlERkCE950eqzKsHkBt+qWxvxC2XZ0M7C/br5NusTWnW01fM7jKE3+BZZdH28/R9mqRt4AGQPKq4g4noO24XVFUwLu7nu+c1ytH8cEfKyfN5NOdHVm5FgD005n0lmacq4m3E7ceprJetsTq+0c3yiFdSpYAxRdlN0mKqpvd/8EcYWKwDOc7xh3LMqyvMJY/v69BGdpgdig/P11VdS8yESor0HVIX3J4i8RToBevn7VlJKBd0OxgBVpVORNWBEu+jqxFhmowfJ8EyUpNBKieI12JDFWAaAWfXefUoRxuJZq3kdNz7Somv7HcV6LR0mWa5G1+/cXUK1z4QgpAD06TWC1sAVtQL1to0atmKJVINOzTlJKp6kr0yI5iyqwyosEiLC0hS/60SRxVRdCHG2RKUFUmCgIY/Kgp6OSAsiWrQBMj5rIhSjESaWhNBu4ciJoGIP5aOC/RFl9H2FyqRYHCaZFrcNphazbzoTM16sNGiRt6IWQcH20iq4jiAVkqwCX6YnePaYlF3c0hB4h3ErRJbxHqFQIe0YBGBMb79nixcCAaFGepvnqMi+FuLoyLZJOaRYhGawWHRKkv4PrCpmNV6DEGpUQoQBOi/wreCplor8r06/JPE1PsvJJshT7UiNEMhTGTIDUUBUHNNGGup6AYpOUJVT3mhA8Ya8YGL0G8gy35SxKT+pqjXTWJv20vAEVvLYxHIYj4+C+0AzNTb5NYikeXGqCKJJhMOvFxyKvt9Je4FItIuyDFOFoHbqGO+93dNrfOITEklUAqBezH5N7uBEWSy12AaA5dhVCM/rJBEZfaIbma14JR9cXatHwz5mL6SV89txgz5NpVsM7zoZIZBSjX5zWjpZ4L1Q8TOo5V23f6Ee5xP1jH08zU8/E+fUViprsZQfdGBR5/IXjUT7Y4NSYeTOmxMOJ/6WUwhn3zRAp8uMqGlCmOXZr2KI10yZMcFsjFabJs+h8e9SuaaxPxCLCTCTF0erQudiabG9yatUlfLVQYhj0tzVNOqFybhHlesKsoZGKaXonVuEoAL0KL77hI9ToZbe0LEzGG7DZppGYIUVwpg6MFxViFsZYxihwMiB6xQJkAHKsckWyMHrTt7v8IrR9h3tJGjTfCwWavlDP5vEaLOsU3ETlg5DNiXL90LrbGMKhDddbdLwHohLy00+QrNZCdYACMEX3IYH6SClZJiyMmd6uW3yC+xdaJiajbsU8TEc4a7mtDzkT8xsRCWiOCh8RqNE1MapaQbDZRslKSLyuzGB+MSmUUooGMXIefAYVNFJ1UyyG1A+cjWESUoAPObNFK93nZcF0Wk1dFDogs00koSGWbZjgNvM1EWfF4i4Lzva1nUUMtVQcDdAQ+iks8rKEFVMFShbGYPTC1W+y6uGvSqYCNUV67Sd6VnrKh3I9vaUOG7Gvhjgj1RwP3l4kSPMiKqjOCBto9jB3OF+XV+oP7/uRyc8rudNiowa643yiAeLEkj2SpylkR71b+mRRSTsa1mhgVBUN3YypRSMV0Io5TQ1JL2KmqPNdU6YjK9myBkmCMVmQbEdAXdWUORCXPl+87c8ZeYpKIOXDE1cQ0Y47BlUQTYJVQCkZugBU6nYNPZVEaUOU42HyhQSiEpMPhMcagkrdHqTiIg5GPgYWVEQTcltUkINDNS67dC7+25PtNk3A8iYn+8kTRQkvH5WqmohYROyEglZKrAK6yacgCOUG/jShWw9tM76uUkia9TjHXXJ9w+LADwXJhBUMRiiqF4BwQrQiRUw4zlACnn5EVi7eFY/NisSw+E1ZA47QIxORiA7UCUWbXoYOpoaUPjysdlhcFQWdDKW+FPPYUoxtWK8vcKDmo9JrDI7kmkZpIP3fKsVBCCcfjAhcRCImsktBHiHGSdROqmU61EzKUPI6WgaQVlWwmDkZtY0ISKrEHoiu+jUqBLcbqH6t+hFymiVLR/TdXoGyKiDTgyURUMhT0aCWfLD6yiKacsGJCnIatCDaNoYR+5OVDGy87QMwBZQUAiqGJoIX0ouJvVSRS4hzZAp1KcoUtGFB5CNgIEX0IIJsFaRgEU1EBCYOVk4KGlA/DnpqvclCoxMQR811Ln6I7o4R0VGBJ4KHUjgQOGChN2KI7FY5I3hcAqpIiexDkNMkTaGM6wPEFVRhQA2GQ9cIQB8G4UREasPAhxuFCipxsPpRsVVUdBqC1g3IxSEWySHZBDjQSxRUPlwVFhBNXUE+QGU9EfkkYfEKGqqbEBCSCN/3JmR3Z0BJPB5IPhoOVkQk4haDgjA8qgmIcZZv8EnIcJlCTA8OTj0OFtx75QmRCugjJbW76XIFHhPw9Amk2/s6zZB0pAoUVoyuptbg0CDwpqphMwrpJmvGgdzdRRfl2uSB5KPjYEX0Iq7eKAjFoxp5baIGO34fbgCJ6cHBqcfBgntzkRCpgD5SUruszTxNlYxCAyiWAAkndBY0t6FUq4hCMTJjDPe4bpv7WOKhkyDqnhOQsuH3t8o0RCBRidwiIko6kuBHXmH9ASdmJ++2ickhA1ePR1LLX+wqcEvoJpkCF+ox9+VumRt7AgKqayjGqawo5Db+wp+Kjmr8im2rHWs4a09LRAmkgWmmJZqlqTc5kVQXKJWmn1lF+ZCN6osoqrkiqqCyWZMjm4v9vVIldQVQ8nHxwCK6kbddFUQSIJuCIjjqUk8SBkwzDBpaSpQuFFRHFQbdFGS5JS/uSmhCwmhGQIBKqBHpyUAiEdCAuogckjnae9BKzmhgjOaxGUsYnmhwiYnBUdRLaJDXyVVig4AzWevEWEKIDgKdjEPEBHagTZNrU+/RFcLJByMCF9Gmuy6voIsQ1che3KZNlTxlIHTdV0lRYxpMJTtFuQVuh7RI8nAwcQX5sJT1VOFgbAIDg9gwcRuK2LDRKNlmgjAkoyCJsX58dA7jEQhI5y3WMrPrEsT+Fim9WBDN+hkgpatQ7y5jMU1DAkVYCQ9k0nlFCIk1IaYJF+kyiNxewh6voxIsfybVmsgIwpNGV0U+OE1NEdmI7CcKqukQy9gplHNRlNjldkjOIqehuIJ+oMJ6KvpZSCZ1GwJSSucoFCW/kxNqTE66luV4qcpjEpZuaHS5h/LzaEQ/AaKRUwOkVOK1yYJ0Eo/ANCYJBAmFlOqoEl4+JFU1EaUk+ZAURFO2MLIeK2pbu0L1lewG67hsLPG6zpVj3CY+EJOSkIGQj4sGlMVe6q9AMnjGXJhkQq3bIU2XnAg0oH4MFLyKJHoVTYxSFDsno7Kz6dzFwWo9CiygzgZm4L3saRbXyE4FKnuaRpoLIVUCQlRBLBXoHG9KqSNEOrLQHjqvZyEprMFZkQEjWZ4+TcxObOq8WzKFtkgeycFVIkRaSyybuJR/SvkkRy43Jql04uGpqFqXcmCLQapWpyf9plqjXMMK/4QU1mJcCm+FJ8Wm8V3QMWf6O0IMoCJMQAQfNnZuWhKJE1sakExTUTtYdX3FVStp0k49gTVtjnz9SpGp9FaWX1SwvB3QKNanPTahUFBnbVUJCIcOiBRqM6YIvk7U6WdtF48Smyt3qyk84TJTdsR4TiVM5ZE8gErSK50yCkxLAhLaO3MAiUsuo5r+h6KKImewabYFYxRaCphiUlBanTlZPwnGXZgus4OqS/r926S2F1X0O/mYczKpEqXqCP2Al9N0UCi8qEJimmliqC4IE6VRD6KNNUUOK8R9WTisBSc6z8v1XetK9dW4rvPglYrqGNRWKqYszIj0b54YlxRazIUajzWZlOgs5qhL6Ws/UeoeyCdNyyUeSqfhtmAp+00FvJneOZOovr2ONtsUDC9KyqnDQOqHRFfwphCDbuQMtP1LEtS9CxF9JJDyAYkriOhDvnWhoJAE4QRXRIaWFVs8D2QyFMU2bk2WSbdqws/f/NRrP9Iq2lUhq6lYbYojBqsWpiamFRldCDgm6WYiGvOoreYUzaa6xeDlWIxIzT/ma0N5ReOTncKhV3wgBySbqHg5f47XUbYCVxE01fp3ggSuZ20lhZ9XV1ec2CrXZUzW4xVuOcNLSWFJif8wpiENbThIqlIIqtEIxyaX5GEm5TGwto58pLqqwhwr0memVFlFdA2NfEQsa1+xRemq2A9WsV0FJeo0W1f/KJVW35ZAygcmriDM2kY8m6VK2yZGOIG+3b9vdasIHOKBFENhYYVkIZ7cUpGFwzVykBD39JX+krCtSJNXkl6GNF9xCuQjCzD6ga/biyLfqEinAlfpYtJaYsWPeZZMqebJUU9LupvcgnAEsPHYhjqBiUYgHplkxIttt9zrbSIBLwdXCWVpLbGoJ9+sUwp7OV6RfOMesAtKPhWzCeHMBqZiMSdKTRZQK3xi7lbwqp0gwNa0rnzIpiiEUWuyd/RU4bim7YnILngOMEDYN/1G3u0iS6okShU3UFQV5CNX1hPHgnMv/CnDwVXox70MJXzB8JZ/fVBPTGld84HLUJiQ2PTSlGGLAqprJ9U1SSv5HqM+Q6sYWj5keSVpblb2GUkFQRXYR05qp9uRDHci3Q6k3XnG2HF+e9sgQEHFUZKBoi/77S16sHgTtR9+ewtBYrCt6ij9ki9BWnYF3cveQ832y6vrbRQjcf7fr1+/et6kWfn763VVbf/+9m2JUZdvNklc5GV+X72J883baJm/ff/LL//j7bt3bzcNjrcxZdf9xvS2bwmuLTjbTClKt74EF0lRVh+iKrqLSkjxs+WGA7veREX17e6fIK5wdPUzI0t+66ncNdg66l6yuH/YnJ84VAepR10l9LutiBrEXrI3qGdvCEQsnoGeF3CISBzg0QLSPaeqDetfw5UTFd26afu+WEJa5Gm9yYa/WeaT125aZ3EMX80xNSvh38ELjYr4bNsr9FvUr+a7ObaWFXh0VIHtSD9F5Vo01Oa7Re/wal+gLZPq2/DZFleT9VqEriuxxXia56kIX/PdgnKQwcHyW/a9ihnSkQU8vt/eMkuHXaNvuUXKCE524RuJBTqKwUcuqGI1DASDuvo4koFfLrbr5AMgXv8mEVEFFviScptGL01qdwYjXWSNszUABCjbEnOMi/IDuI/qlFnPxOcd4m/hC7d+jC5A6cTxRnjGYX2icRYRU2Qh+hrNBnEovyC4QnO8l1HFbEPNF3MM6IVsdpjdN4uFBGefWUD4y5QC50uyAY2bjsQyfN2ZhTeEfvmsNVlwm8HyklcdZ0V17X2sEwYPXWKO8XvZB9d9LxjlhC2z7yeH0hPfaZJFxYsYZVdmqdBmFc/qVIGdBMi41Td8tVDcn2H7JSffiM82G+lX9O4FvYniTzY4booItgxY9ZoqsOC67TKqhBosXbIzkoZ6XD7Qxu63nU8lci6LJC9g+8zm2H+1WB5FvmGWBv5ih4Hf3oav5phuchoH+tumNt+L7ps5liuwTV/YjvQfrfHwXaIKLMTiGSMNz2xqn8bMisYfLBS8GvudGMWu+2jRj3zJ8GzzxYIOBZAIKbrEYmxoU2lc5dTohs92uEQegOGzxXaD7YMY50TlfFhMmVUPl+gR1ihNWWcWVbIzQp4cqp+UJzE5iHl19ZE8mHhT4afewQvASyKqwBzfJ/zGGYmo+WKxe+UFg6H5YqUgF7xSN3y1MTHL8ikvmFkavtr4daO7FFyX7HwNn+0MgMajAqXaEnJjEqUlN1gRyM6s2/ZYsHvM2GfhUqgcVq6m/kgqGtno9UtZAYGWJAWysUritF6CJRnfuFiWrI0iAXJop3k3WN4EUW6PnX7XWtqIAMzGwsQ9xLiQ3daelLDTrQCz8dpkyabeYEfryabJa0/7b/hyC+zRsxq7oNwce1Oje1pcTigVnIUZUaep1KnOFe6OmNPekfGSfBrsLsLQGuU48nHoAIuHLrHg124gHIOSBQ74cGSXFGlfauO5uSzANUih9QRYItJFu8vngRk7BCdP533BDbNIiM+2y0CCkSu0OeZpTlXZE57uq5X9EPNby/DVHtMlKGI4oTisS4STLJ/yiOV6Dc0Q6R7El1o5oaFoJcM9GWc0U2phjSVLcLOuN3cZZykyReY4fyRlcseeQvcfLdZJfZcm5ZoVccTnOc+yw3uS9s6B3l8aCyDK3QX4xGIbjUQiursiGyuvgCPD7+jhoE/hviCBsV6Qi2yZPCZLge9OCHAgotPTLpBqd8tNkrVvbjOaHVViz19gA+URr4UKiq3o+/Qt+wRt90tu22TLbFx5TR25cBVDWFiooIr+Hbw8NRc5KduUKrHDKOUHrtAO701Ssdse8dmCt9I0f+p9LTi9MTN6MYRFC9ttkT+C5VWE3sC+rpkzPEGxhTTIKw16MYR972/yKkrF9BFCOI1B3ogUyPpg7CY/iRntiy4xx/g52SQV6lCXz4BEyhVa9PShZjqIPtj4mrL6PoqrugAF3NGqr1CxZKWFDMa8lY+Qm2iczRcb23p4Zow2rIfvFv1p64iUBrbM5nD4jzopwLdqjdIidRf36VNiEYR1C4O6IcZPlVus3xrKrQg9zQ0VjZPlksHGrmUttFUAap8fjYlA7b/bRzCxM0t+t1D/s7RZnkTyM8oIEJRb+Xqb5fTtXoJfDGFPjfPnbVLgWM0P0UsppgwLY98KvqKKMYjWlhzKQruJSjalHqXa8MUW0pSqyfaeL7XqNTrHPFkVAPC6KV9qd2jaV7xB94e401Om2GZd9vcB2YVJFNjIr7bS2Uucgs8gW7GRwGII1xYuQZHk3DzKYBxawQoGRsNJYhGEDeXRYVhzyswJRarIBucF5IPukI1FSpfZWFzLBK3kKO1qn63RBUHW+pJBWUVGRs/nz9DS4iMjhwIbfOclR1v8ySI8LXo+g2bfKi+4G2lMkZVOBm2zRfYIVyys3J2GckqZGMgqvj9+EHuUmSJrhx6uf/IYJWl0l6QcejmUW0viQYghrE55Fdj5UgtrIH9qxt4mdGAnV1RuZSUl9y/Y33GRF13/TgG0TTlLSQ5ocdgRxQ85ciGgy7ncYQdbaGlvyzNtcYa3eVIueZvYFdIe34vnXQxh20JzhK9qgYWwpBtYtlW5EEhBsdWqQ/vOaf1yWlcVd0uOK7XG/DMp12lSVgr0LIgFZRpZkwLI7pfQDuM9Y2IIi+MHaA3hqknMXnSgSqyCwllU1ji+pUsBmuGrtTf0jIsWpAos9iD0LE6UCnpHl7hhvK6iAt2/B+ixEj5+Vwnp1uJ5tjRqj4Wz4LDWvXieoTgUEaOJAKxjmygsQMQ+SkDraCeD9lSAVpYYmd6aMcNUma/lOOF+DUrhCyGCcWiBrcbSna6XJ9ttmrC2ghDAHP9PkKzWzELvvllQR2Dn2Vt2P5Mli6T9ZEEvwXg+WY+n3yPU4kUB5tCWSrBIgeY8iA8aJgBSwMVU9R/3MzhAMdb+fS3Of8aU2QcOoXQM/AVxusxG8y9Bu50JHBV8qQPmLyAq6wI0L4sIsVMQDi2IQmy5Qge86IcSNwlgG1qGDy95bx9XaB2yBgoUcyd2EklAHNtANFA30UM4j2J4r1c5EhJsZ8J4+o3b78Z9F9FnH8gjrzpOJA/61y9epeux8OyBKbPysMtiG5miXQma7GqLxA9bZuMvbPUY3rKhSswxtvoKh4/8bn06Wp7l9RZdV1hyd3T5cgvdQYLVDVs3C/joHyv/Mn6lISwjDpoztualTi7igCzcGanXusyRW8pH7HWedwexJ6+6q2IPxcvxWIavNspn2PxXp3kFjXUpVkGxjZsziTjXJv5k465r5loW3ScqP4bJqXE1gbGyUzG+1AJzgoOQOJTDZ5tersB18i/u5mb31TEssLzJm2s+Yvw6WPv+fxMdh3CFlu7mK/TuEIuTLJg9xHVkx9f+BADuopvlsG91yKmH7+3/SMDTF05L5Ap3RuFqpafnbZEGicttEVnNcZSttjlRcjOmyObExDaHn1TsgFyc2IwqsIlwOeYj48vdrlOFWScULvfLVVOvmoBXY0Mqce4bzcw+vdYV4Z+8VoDQw9OnxDIOa3VNy9wfbtiIgVzVKZAlajEAtwoDEHvcqAIL2zlJU+SV7jKvMKYzV2qjk7chmGLUgmIbJ1yTMARps3ReGtYZJ4dzae1SeiVCBOHUAp36SNYKA+XSUjcBQttOAeY2S1gYqKanB7Bw7D5XRcTnVyY+745IJs5UPWUxgclFCCur76rvEdbNi0/gGedLYT3jVJH1tr4jWfPnVTsD+cNpbC5ucS2G3Vc9ZT5JN2/korwAEdJZu52Fu1HBFh8+t5JXRYMwLInQnWnVWHafccn+Cy6CUGVHBnZi4HCc68myc/CqvxoQ+uCQpILscE4GczygU+M6HqPt3zHa8fjoUI6P5oqraVdL+95KEOWsyazirpbJ6u++QiYQxdZiuBk9H8NLfrfAhjP2MJjabzYXJNLtfZ3+JyjxvXX2pgRTaI33ay5H25eFXzIa7l6U6Pe3+//GBZHRa+b/cAkmo1FMxetds5ylS3y3MBS2J8sl3FiYbYf4bGN0dMmZWGtj+P7nFOyWMvg0zVddOjt7vlTWHoknm+Z4gUcVWJyPwCFc5iWfXpf4PvssUTtdK+kytGQ8N00lqlH3z6Z9yS46FJrj/RmV7WholOT32efxK3gq3Vebsva4q41XVOiS+dYvIoooUTz5ffZ5v8zT9EeO1KgrEKOnYxzWrRbFSOsVtnuSlU+8FkCX7JyJ4hn532LxUNam1tL4J0bpEqurSoFeUArxRlW4V5i6Ohd5sYkEqZjZUnvM1xH7/DVdYuHICp6COHTiq5P7+yRNBB5VqsACH0pTxLro2m/Tu48WZRO/0z3qxlCMKbSImpEEEblFC32OoIYqtquYojmtIdSVz/kqyYTX9PhSO8xdeispcg5gZ/aqdnY8T+W7ObbfqqQ1x9mpLpKirHjWJz7bzTyPavg67R4FxfE2yl44jbb5aINHGEvnFp9XQX6H3x6TLOZkNFdo0Ucu1diZbWqxhu3esfta99Ua03shpvc2mP4r2SJzP0r5u7lMkYVWsM4zIMqFTRVYrJ8uiS+zfobPU0j5ufR6vAa8b/Q2K8lFrZfV3NXDdHy8WbYRvoKTz6HIFqc4rwdbZnEO8pR/BlUFDcky55cfX2qBeV0AoMItKLeYoxoaV0ksxMyWOdwa/BGJH9VuS2Y6ng1+rOp7KD2TPKKfGfWTSjQuB+GkQ7CrMip0wE947lysMkjqJs00e3JFF9nEtU/zLO8Yj9nOtdpIfdVzsZGoXNaauv5YjvhwhkEA1eLurgCPScSvW7pk37aomZi7O2v04+sOi+M5rrjqONz8OcpWNUo8z2Ahv08bRnOaLxmjtvliG3nYuGWFD3r1RRY422CGtu4Z74kUQ1jIguGZK3kjUiCLOYpWDFGaL1bujJ3OG/bnjAIeK6J079wTnejyk+EdFgcZLq+6q8p/1+OzOsVpCIRbQV9ocXyZJX/U4BoIrHKmyML1lkarxQb2ByXQ4IcuKLYIuqwY9zP+MP8a/FPrZM3d0OuqwBEXZV4XvnaHCKPLOjdCsw96W9d7fjnRJfYYBdeImSKbg+CLIt9cpvWKfX2SLrE6iM/rmFtXxOedWQVdFJUf53dYHGPexFX3gcNDWCb4fWxG4W0+WexedcpsMc2XefaCXdfkj5acqp3d0/j/nBbZXFf0+6hPz8tfPR7HYFhZ5XH2BdSiKAB2apupe8T3R16xa48pOmDdHBHen/sc+W4/NRF/zmsCFFupKYpd7Itm0ipGSRGMN/yPNSgrqJ2jlSXQB+jiOQ8rd12z2tOjfvIINsy1AoTJ42qBuPqu+vnk70W7vhYtCWR3CmOHldrlxmffZcumDmbv4szxfAsj0JuSscPP59rnQbFJShSS0Nx58tzzGWwu+78Wxa6uwbDXDvr0bbSd1n/dGQ46f67QpaX0pK7WsNEkxoEJIbhJhdmBs+zQjcNlYa/7hwir76iyWCKa3CesniQqt8feinhdIwIw87a+oYm9yR8A4zQgv1tiO4ljUJYynFSphUaO4pqg1imTF6LynVntF3lRb/wDaXo0DutYUXecRXuTb5OYRdF/nGvx89e7be91Ly7F2UIud+JG294lbsJ8idkiwNrAeFwXh6TyOKsDt8ii6D/OtjoQCUSvS1IF1jcGhJcFLJ2LjfRk/YrtV3NM6IUaBk37ycYBVlaizCzkd3tsspkUldtjx1m3hXibkqPMspNZkb+wcpVTk4qoj0Veb4Vyqi/Z52sZX/u9h5Us3ec5hBRamkL1iSo4CixDnjnm2xxJbcMiIIDahvG4ikNJ5V31d+3+xbJd4u4DO6/Bc+G3YJrptF8rknrjLBPcmCgjAFVgiY9PI0V8tjmKFLwH5PAGUFjLq73y3voQuJyPXKnN8XNzjCVBLSi2nBd07bDm8DJF9v0Vo+VLLVyIzdtVYsRcoTXe5oqz1D8pA7LnuLO6KEAWv/C3CMQQNi009a4iNp6ALrHvM31gKVszPJSFZR9VIolDfLbl6/quQimbF1mcwo6J2JuFcGwBXXBWt9BD2LeA0073zw2qxiKG9GxROTYxpG2LrUhQjI2FcGxBMRYWwrEFWJdfe2IIR/kE5XyCdMwovQBASDID8BBtC4lpAB6ibSGZDcCtgjdQFe5qaffVkj/EXOfCad1qE6Abimx7x2e9J7/bXJO5r7MlWIqeGWXLbLA+RcXyMk+yqvwJCgDnlg3sk4BY7HNrED/kdXVSVUVyV1dAauepIT1aLP9jk2qaakDsd29ZyKeo3CIUKXCezl7z3ko08q11gCWyC6DQhYYvFAdnkEVEKoYa0iZ4pVjyoV7DVztMvAo6fLXEJBiz2wi/ROUDWKqpKYOx6/PZ4+N7vsfNVztM58/bpMBBL1/yrFrzSDkAV/z/CSIBldlyNw7+kBQgrj6Au4S93SMDsnFB9dXavLOf8lTgjpJB+bQk4iA5lFNLp1H2wJtaQgBn/PxiFQK44V+cyVGjMiesXdZBGea+3An74i5i3aNsof2+gHUSYeZqMYTFSquhzlgk/8LL9KaIsjKK0U9uY1PA+bfGM6ka0r/FK1DWbF5xHayNdNyiDAsKeoohfFoQjUgOZRUJ0Wt5igEpwGwurxTxOiqB1AsrBLCxqxLx3QuqwN7j1+YIFPr7ujJ7rMjgQgmP66rZo5VeOuNKNgc+YfK+78OxDGlGXYFNlGTczQoJiHkbn6LyK3hqjfWvOYp4jQX53hRgO3Mc1Bnc30toO31Kysr/IXQBSpecwmZoxjk86j2Q7Johvls6KISHE5Zbxb7lffqY3GP7KSBzCVC6MJcZmnGYq2ubxUJ+t5CgJVj+TKq1kMm4Qju8glQxxOc/AeOG4VUP/pzsCn+rjhHcIng3TA5lz/2iMzi2zMI6EHhr7b20i7LrAb6OGHE6kxDAfuzQMN2KbCVRuY3mEyfbBKkanE7JFDngFNxYYsss9GKQIa2fV32J77bYBB2kCiw8hKAsuVvy/UcbbhrIjpU/npsEAAcsVXuJESD8yDFllqLuiGFIqEFJ6NBQNL0+eRnwue5/1BGmMo1q+GqheGRJdVkksfhkmC91wCw89+VLbSgp669rX+X9dO2j8MDZ4Vi5s4Ga7VU4aAmIaxtCMkhALFQG7TGp7/Go4ljU+Tj0Q/6UpXm0FOQvY4psNqquqlTtEQDYRMrGICtBh4ML1ueLLfoO5edPkKzW7CuExHeLEMg6W6YA7TJM8CPx3Vq+nqEbryIJ2xTs1vb8FSfI8t6eERrX7Vlcd8zt2X8r/cplP/rqlvDoJu+fyBVF0pPFB6wpkh7aMH4jHqOL28gIyzicGjbWuxkCI5Xab7ZYTmEJeu1FgKwvsvM7iax58vv0ttzerSB0itQkYvVZNR0Wx1e3xFV3W5LfFFH8AAcmOrRkyyywojBEkZ5CFVieLALxEShbZnVOmDyCQvxaMFf4J1g9/s4KEpPHKprSZdG3KTgH7747OEDEa9Paf2zvaJhNh6nqIrsCf+AMmJ7KC4HKSW1R1h+JjQJdcgvNQGEVqXB+rysQlXl2kRfNbLG+dqbQBi+edoBtd9bFwRU645VHcigB7edNnHyeL7Xh1Oj+/iufNJr8buH1WW6STBjiR5fYUJpYvuKrgxKQ/QztmW3Tz/GfZ1D4htj4aWxOm78OxW5L7suoaFUX/qYrWWLrnxNhpEtslJ2BxqIjaVH5bEZ7wPOaMXzT3aggC4MCLPFBhXjcNMQcu+1R3nV8c70FcR+ZOMy9XzphFWqX3MJ2+MYRiyGXn3gk37bCgGwNrOVtnIskhYuPC4ply6xE6NO3rCWD7H0BqtjaK74/z2+oJstToVBgdlEurNCNpGgI+2DG/5MnTToMThyDB4Nx3x6l0No3bmjlL35YOsDODvG4b+PCynPP9Lzz8iMqEqjE9ivhLN/cJVnkv2vo8bvPoxXSkXaQh5rZLNAHi4jcin24tPliccIYZfV9FKPbeQU0a4W3gWUwluFggjgwqzAckK3Yy+PdN3MsP5Mli6T9ZHF/SnAQ9sk+WCcqASaC6LiYK3TAi34ocZMAFlp4WSarDFq/CeaIBfsAo6jc/gwPpSjlri8xZfaGNwqSZLGyZTaBZ6KngIavh+MagRI0fhB7L5giS2vuW119u8co2sSOvFHHg+z67jfKlhdun9tHh0NbSWpkicotwhvAcwU/8Y+OEd/tLk78USeF6L5E991hOZ/lWVXkqcjLK4M5fIMg7JoLsNgmX2UnaRJxYhN/mtSgdMzJvFuCG98MHUV6N3dOg4lwCbpR5TjbB4kIFoHtGzfDunlx/UcdFaC8Wt0xByxsoaUZdLL8Z11W/PE6V2hh1GDjQ4aYL7XZyy4LcA1SEAtuVVBFU+w1Mox4OYg2RqrA5sZH9oCO94T6C1d4gMFfNwm09BE/+gnDHo2D5FPU3X11NXSGdPQUrOwQviub4xjaxq8z1zlC84KB17EB+ulySiCuN7erWBoBUDDuAvzBgunL9DyL7lJ2oyC/W2ADcV0Ark/EZwuHHf+o0CfrF4XyVd67tJgdgS6acyf8VG3S03z5wnaR/G6jB2UVZOzO7/YVVE958cCqQ2IYKyECF9wLXi1dknxeCothrFs5f47XUbYCOD2ZujEZ6M6ItrZTvnfLurE5PVovq7qrQk718oPbiw/8rXSHK+losX/OYSGXw5QqstUcLvJiE1UVFy3Dl9oox34PB0kXaH2XJuWa3T2Iz3MK1l0KuJOOOkfJAM+zJZxRZl6Yop0RYZhN2k8BlLQWzFVXk1YfR5o1zbM4hq+2mHjpSH4fz3qZy0hdgw3A7iaoW3oaqiQqF2NVXX8c9sGNMgcWzacpN1RBkj3rBHt7wnCnUfywyPBRJJll2I/1JEgdmNAY0zjsGOzaRUA/TNgLDHt3D5gM3LkBG6j4+B6UiTA6sKoZml21IdBz55cRG0Y0fD3gY9czOFGrvHgJw00sNhcLVIviyEU7x0VdIG0QJmKQecT1Hllof1joI8hAkcSB4j9YbC4pj7Uodt2I/Hfw0rwETmEavlph4pAcbQohH5/E6RWIc99c3T0aB85V1N11lg3r9xjntHVRLpasW7f7tjNs+L0IwoY9Ggc2VNT9c7HhdVoz7vrmyxyB6bJnK9XPVc7lTI7XYFmn4CYqHzydyQQmF2eysvquapMhYu/Oijw7f94WoCz5Z1uZMgtPJ/ekp+1znsLABJeohCrffsvOiyIvOM1iKLCYNfBcXdW8Q4n8brNacfacouLw0SV2GM+zpRBf992yf3UcQw4Q95Aos+wjPyPEZxvJ+SlZLkHGSs7uq5Ubc4VY/RIUsSDWkym0xytMDcoV7oxs7h8k9tRzOzQueq687jgi+WS5RJPB3dYYPlvdiwvyBp5IMZCrBXP5rUFU1gVoApc9HdYkKhdPtbr+ru7lzUOL0Ox+Qgo1vVPRRVaBLknORbqgTwfsZ2rn/0MCmyy9D/tYbO4MqUBx5MkD58mLvKg34U6gOXQOXGmAYxy2JNvk3yjiS+c6O8YEYlH1Hy1MoXybxCye/uMBn2VfFvjlllaz9T0yInE5nRipEYzE64EiLC6KfCPjbrbMhjNlOOkSq7X9TxBX3JJuPlr0DBrTTI/wFxuz8ApEguvckaUnuFXYT1+aJ95ZhFyxE+7+VT4pegLigCUGNmtgwefcM7SUQOR0vKKoPZLhOTQpTBfAF88WUiVKNO2QY3qfmRNhDsag2DHqxaRiDCPtaCOYBP6Giq2zeLZYrM02Slae2n+HxSn2SlZ1Z03QQBoFukpHI2m+zHmfxPdezmwOFqxDBwpPpZE5uVc0GHaVtU/jGD8V3XqXWQYQFO/jsgl3st1QI8aPsHIxAEzZ/Jfj0FvgKGg6SlM2/xpVYqEjVlUUr9EaeHeR8PEugmIX3O/VuN/74P6LGvdf5LhnEnVfwVP5GVQVKMI578Q4HQSfKaKR5J+wdd6hp4KzXPuCJT/eQdp0asCexD2erUH8kNehErhx6FzUWT2OXd38dzmBIX7PEdlObZi9GLccyqbfN9Hz+TPgyEAVWFA1eu6ul3AuTbrIavva6XSO4RSvXREsAZL6iXGGEDGTJvLjWud8eCKAKeXYMUGe3+qd7a3QuC7QuzuX0QtqNdSb5yKsbu+eG2IaZ92xzbPYROXm2EO9HL13LnSWbGG5LQCbTSbXX+IUiJ5HoAos8V2CIsmX3A5BF9mcDldRimszThGqwMbMgdMnfj+cKpnDtRQqVnORJVUSpcIFzpYd8DrHya+Sf+Gcq2ggvvsKi89lRzHAMc5aPw92acntao8UG85QJr5CypY5YJUgtMcluFpKFewM118BdP7Q5Rn23dtIXE47mxrBOLx+2aV8ecfdcui/O2B7L8H2/qB19LMiL0tojaRBOIrF5mILa1EcLlfNtpN6RkK5RUBNGPkEm/oMHkHKbWrEdxvXZV5UwotWdIlF6GWdpkKEVIGFirhtD3QZog2f54rsQk/lctmb+482xvM9KApQcLiogjlPXMIn8rwowB81zqvLxOb2n20i7HB+5k9RyRqHZMHOiCj4ASWk9by+3SBxubktq7mrJz9/olQpw5mLrwVInN64JPtU1d5VPtk7fRXfafGcZoTCZYLF9caZ2ibOlZ9g8ruNyyhO6yVYZNcJrB0xhjhfanXQEZUlCsaFBZX4uIMHsNEXmsqswtB9tXA1JhWbb6f9NGWk2hdQRW30MqOc0SV2GKXvrHGFdngFNCM+zx//dgUyKJ5Oyp/JcgUqVhGkyyyOBXGN/8ozZujkd1tsP4to26pWIqRUsS1uaHU8CWaKK7TFe5oj4c8uaLbM6hgXbtLsA0PDV8u1jB5c55cz/jr9dnVSlnmc4FfCuT0LbqU4ZvT2ZLtNE8TphMKi2KjU9djtqYOWKSFwj1zyu7+0gdvrvC6ET3QZ7XY9atG2h4jZ98WzmzdRgZa3Yzc7Yll287e3wvn2YYne/2bFEEMtGTtI3GhGZO6w7zQv9J3044QWzdSMgAPc4J9wW13ny9srUFZFgpSkMzRKnYQwqc3ZslSdBtBEWOjb8mQTpoEAzGLQZU/x0VBvaqbpXG+3xLUN1UNOPDT/YlMD0/kH9bTlsXpOf48wwMQLOuc30S2e2Sb6NElT+Ll33xrMNltFNuU2c03j9KQpgyzktDOo/RjTumNQkV8maD5fLcqvdZr+/vo+SktgOXpv5rkExSbBSRWbBKq3ZLJaJQ/panJ5Lxh4EtyAvdTNec4ehzwAn2k67LmnELhmkzfGrCKrIZM2lqwhRr+LO82hsMAVeIqK5WWeZFXZBsTefi/B8mdSrVtnsCqiRFuZjyHhqhjwhbYhzxmgcQXgE32Hd3GT0pEhnMDpVHUbDYerE0LFYZB68hGLLaTEYXHvIgPpx++v5rSGe/v/TbRS6zYicD6RFy4cYEy0GB6xr+ri5JIw7lwQfwnEFHyH6nytyCEeJRkoWJDemdt+6f8uuw9oQqMV+JIvQVoO9VAG9k2ER1Ruoxhb8EtwkRQlCg2P7qISNCCvX8G+PyZQ6nVHbq3D5Y/0LG1SZHUAX6IsuQdldZM/gOz31+9/eff+9SucIR3FDaT3r189b9Ks/HuM122UZXmFh/7763VVbf/+9m2JWyzfbJK4yMv8vnoT55u30TJ/C3H95e27d2/BcvOWrd6iNcLyy//osJTlMiVnnDg47Q8es7ilJs0av/074Bitm+ArcP9KJkZ+e8tW/E3AWqgHv79OEGHxYsTv5uDg0ghd684QFMB9ff0KSRt0J7SXOG+V6IcoaKIRlpx/X2RL8Pz76/8bV/r7q8V/3BKUuD19abD82yu8Ff791S//BsXe9yz5o4bAN0UNXv0v64410gHHKzc9yx6jIl5HhUP3Glzq7n2JnptbH7+/fvfrL450bI6evfvLkPMd31/ToQqq0kP9xX6o+CjuuRpnrO/dxyqo6j3Wpokm3osdKoH6rxTmquBjiTgiYjot0GojFp4LjusKXZ9iu/ffNtHz/+GG8DTP0w7dXWLdKzRHffxegwVF9FUJYhcl+cljRqUY/kcd4U9wsqsDkcPi1USwmINMomIbjFGbzHEb/9TcAhkHN2FOc4vDaPQod+d9VKeVhJf9GBDUYNmkPOqT5xwIJxJjs1cLqMrDhuuy/zdKKmIxkoFdOKF52FHBpHBnsGbTLlOSHX2aWjLCGC0PqGl3rcKx3CVZVLw4SXutyHnvsGF+gWK+eWjIC7HxSvyQP2VpHi0PZPF1w2myMzUN1VilSTDK+wQolSz7dfa9BF2j+FaAqayU951A46WNdPhOMYuH4fk2jEzEoU740HrOBOvICdn5c9W+TxEC26L8Cp585nNR3hQR7A/I6C3UQMz1lzhuCSSDUeLCptS1EFat9O6Sfody0Q0ORCgNQYjuezDKka9RFO03G4TUQHG23ttv8uBdvcnH6OgV2KYvdr21QDtGj8/OQmM8jePQKPs0rYF1xiYePoShTl27szKyBaNFG+IQTOe2vhESbXeMNkEmj6ydfk3X9jNAqCSxwU1IsqcHsk+0+UBViv+vv9qrlY0/QG+t0LhNeO1TXqoXuUt/L/NC5tQzVcgLkUrp37H+XlBgxE2a/+vSy3pAdkjjr4HCDXMgrDXCumvzTDVRrwey8KgxkZfglC5rBy3m/BlfhqNi2BbLMvT+2zfTRBDrWnjvsBV3TdBR0COMpR0Dbg/Zdu3pCcMR1l6WLNnUG7y7nWzwFtJtuyBONlGKTmDhrxIfpb77G2Q55KmFs27bfTj4iVpqsHdh6KEohXJEmPrB2SXB99pcyjQBAKd1toQrBK7HPj3oRZJWbNDN3gqeYXz2qhpZ10tRozKvunMKnV/WDxeTxjT8Jsay14Hw02WXAciWmfqKXpzU0NO5E0x1r650R5w+XNhdBxtLYve36JoX2XEaHncF0ERjsTbkUcohhfh39flCNieS8HsM+lOyBDfrenOXEaaTC6IfcD7vUq8JuKzv0qRc2wis8U5vg3o41M7joCL5sAQxasFvJ7yMCtgFnMSyz4Xoh7HldCh+k8dkaeme8Rc7Zr6jUeSOWpV1wnmy3CRZ/3ZkAIQd47RvgflNNUrP8C37BC3NS8/NpcMRSjzR+UjCblpcWpIA00IkJgnb2ZM0zZ96dwB4TMCT14Z4st0W+SNYohfps9V1vfGZpa95FRRfhwzn32YG69m/UCjbo4ub/CT20ie4vDPuqK4f6uBLJMrq+yiu6gIUcIepvkINaljQoVr5CFkmNM5F+TG5r86iwkvL6nD478/tm0HfqjUoLqksG249694g6rd6tXh0OUU7qZEhViUx2vpPlkumSa/uL8o+oChAJIzf1HzP0mYRdui8RgaJ3qySb/ccPifjskVy/rxNCmwRf4heZBhtwn3w2wEYoT93f4rK6whqIiDErNKYHK5MMPV9ogDhwNA50cmqAGDDBug4HDj1iNDrZ4HCj/qnPfzkSYuEepfDR96R+C77VzkCYKRe5fDxHaIzif7pbB9ZdgEntTvg8FLKls1lxijtsJ2t0f04F8eSaejZ8PicDwXOSy8SMk/WedgLUYaeh8ge4RKDyLrTJi9Nr8rjh0BuQmQTYXwnj1EC6yYpgdPDDSTso9PulWTBxvs5f2rGijYa/vKbvQ6f3L9gO/oiL7r+nQJoFvmgPY3iB5xFEN3S9DzYQCYawrdoaEg/pOsxKXj7ao8iQ0xMg685cAyBDw8cLFtURIBTMFUUsjkCPq1fTuuqGmx1xyWDoH8m5TpNysofYbuWUwD5Egpmyufhgu8M6tsYVRL7+ZlJBMG3jm/pctwGWjPjjIgdCt7G9RbiiVLngRidjRBt9O9/3SQbECCIj8R9ni0DYu5cTedZBYrSmxdb6UVhBSMzUCvhJm0T2gs3CWjWrpfMh5snKCvitd0Nuo2BtpJRmRX2vzttLNvklj6jaF4vHY3atJkSHP3PZDki9k/j0qbflULLnB5xWIET6sQgzOlq9yAigSLkZbBdOXUVjjx5BMULmlV71wtd28fxQl49d7keS9b26cdpVIJ2e/V2FPS4voCorAuAr9WrwyCduztuDGHfDPpBN+XgkWzieZrE6QK3mO9RQBsuBAoUwBXCTUMhRGQIie96nTeen2gIbw8Y0dbHKh1G/IQ2qsDlbnVHI3+/+PcShAnakoeABVccuqbcJYihW418CtlPd2g1kQCY2jOu8iyvtygyeuk1azwWmUfENSQQHx/jXdCPUQc8aOu28KsbCx7xixCHK3hcwplQ5JEtYiNuCRzlcppXUAMLHiOF0tWFXigd24UJlDqGI1HhhmHObC4THGDiEIzeVfRR8FG02nXyL+AzAio4q7zJmxsJLGYnH26L4xt9imCi6phFEUId/irKVhqfvVtIeKiYwrA+qR2M3NoLn8WhRZyLKIjvxf5IwNMXgc5mIIHNo9gb0XUgylA7mpAJhIzyW/3VIWIc5LJcQhTqv7iEHB4TAc2RCKg9KDywJTXvDb3xdLIR9xprJ1hr7R9QUsluZA6u+76m78yxpL2qUyBPluC0O1xvwegOqvZtpD43glcMWRfOFwLZ8ILcTU4niPDwwQ9IL6UR5U5TRSAms2eERt5RWGIoeWJvlXwvCp8/V0VEZhUdQ68kD+AORKRpHWIOGZvP8jQvPoFnnIIgNPLdS9psq0odmK92Zl1K4iUzaH+o6dWBRXkBTe56uLoTIOJ4ZA4kL54duTAEF5IUte8DXftPw40HyIajHFwFP7cg6H48uQl7cnM8bdH2bv9OW46HEft9GGEeRtE9Ydsk2D+QTakblYOK3Nf0U0q21Juf4db8ouwyT3gFEu0rmx4Yf6qfC7EPDtQnIHbJ6qpND+2A1CipsQfei7zYRJ4Zqzpc19HwGFWofgZP4BTo6vXJ/X2SJt7qMb6j66U7jb3jL8rmDKFL5e4TAaLI3Kzssh8LfY7KymyfcYlzC7lDoJ5+zldJFiiqFuHrboEboLQ5niHfdLbTHDgEPnaN6Ilpu+7wGMz7Y7wtd8x3GLsyfjI52JkiYtJgyIR7sONLWpttlAV6zkR4/GiiZXcVvdYIRI+y9j4mWezgf2Cq+/TkjEh44JnWEa+ndyGRvQ+C7L+S7WVeokQ5bpFsAu1qnWdAnADOLc1mlygrDGu773/mRk2zCg5EemodwS7vOGPfXNlurV56LUYU4rbezVP+GSBiLcpcexXnvS3f3awLAMzx/8UWP1wioEhiBrdXxsofkZf2vFPpq539nsaLnn645M+y9nfhECjYA9WrDFKvyaTm5Rmf6FWeAC/ZmHM3qVQdCHOPp+XOuK/e3RXgMYlEi8vXdTCjQDfm09M0XyF99kBY9HOUrWqUqNCaR4eaXkwqPFrmpLSZI8bksU2Lg93GxxokuXmL60yRn8JsSQ/5w0OhvIlWYYJUd/Me/THcIvzB/BQmbideDkTOjrIXdzQ6q1N84Uh9Ymx/LIjPFa/BCMbqRRqtFhvYdXQFTksb++jkq+qwbtSOq9M0AeLXFcppfQXKvC4ORgWfXb/p6Kllcpf8QB1y4e0BN9UH5RDPN5dpvUpkuWgND5XzOpazvcfR0VfwVB7QC4izc2hADRy/ghUGFcpAEwZTEAm8m8rtn8lO2Q399WhgdGiN79HkaXqSlU8HE4KHBuQQtI5rBffnuYnE7l2gH3nl93TJ2HewINEOhGtm3+bDcU8TtdbKvDAn+kG26GB5nvC2+rEGZQW1WLRGduGAcTc1kPGPPckzrgORBXpb8Ndf7RXmQE8/BQkGhkhavkc4JGxvpOF6hwN34bqYf0aK1ZVPnY/iBIpNUqJz1ysQI0l7uMzvsw+Nspj6vAN+iI3n+vwZUTVKT+pqjegaY2XgoOZ99ptV4SJhu9laEFduAqJtJWdg7N8Qb93kDyCMGYnRncQxKMtwSHH8B5xgryRNxovuIi/qzQGd5d/k2yS2X15tNb/MInMvbqNXTM30hct9uBgzxdVJvD4wdxzIAsEDsmfRttqeLxA0j6hlz/xxTeyu2nfokKrqayOJvZxRKF2uFwJ08Qb1wvMFgxaLZModseE8ap7G+n5KoEMSPh+LvN46SqC27rghoU6v1AQ+ofja7jpeazmEQEFrT6RUHcXKKB7O/RRPeFkeiIzaC/FwaOw2h1O9Id5hMC0eS8ALnBhf0Pwq6pS9+2E/0EkKfJMceHaGSVHgnePA7xI3ronuL9VsTxyHFQJXm+M6IKrm3mHg/Ol9FrG6KEAWvwS7JN4hvIqG41tNZva/Oa9K+rjLj9o/oiq0MINGewXXQrrI4hR21ZAcf3WTxF1j6NbgFI3doMb6NwMmGiHV6DQjbWXDNCNsG5t0ZLAhi8Vq3xglxqDgT9DeEKUXAIxNU3nLYxNY3vLY1G7xh7lahvlkdEbsXyQetRUsOkZrAmrqNdR2liM/SXIFnqJieZnDDa78CQoAWcszr+QaxA95XZ1UVZHc1RUIbTVyDZT/sQmUF6lVAiQRfraxZ2HS6PVa/zbIGHFEHDJj0NMxBYBy5AzOP62nOE07xEJGCPlNBOpSMMWUG59nHvHyASxlpPPu6dnjY5hsTQjZ+fM2KbDl9CXPqvUIeP8TRGHGTvLlhwQKt+oDwHzoE6/Zo2kzPH7K02WgueKRB2QEAvlplD0EM6UYvMGWGIl3cRYaZZcjKzDaxV0UaENqJTRWCkLmc0WBOXBB/Kt5RB29dxjF9Lvso6APxm6yBq5AWfPpdB2l0RZdmA5PHB5xwF5Du7PXicJ3/bJGdUsQ2p16GSWhgs2ZDFhBaNqiRLYKSnBZNy7DUXxdYycm3t0DDdJcuAKbKMmIIHWHqPBPEXrhtjVuv+bILR9TGYkCHpV0VuH3Eur2nxI4XQeTeHH2NzpxVfvW22p+AVkT5DD5mNxjZf8AWacbmv3kDTW95u97CZY/k2rtyEJMde+uUBkYgrtbpmTWA2HQTp0hZhnu5Q73diV4fA4MO1L7HxaN7OZblF1X8T2siNVeHIUGNM22AQ2GKzjYLXrTPZim1mMMd2fkGmRIrw7VwwZduO59AWVJ3Jr1fFBkmBGsm8325JBdPMwBJZXpxxM0jmVGde1y3jdL/1FH7US4y+rvWVLhh/fGPm7sGxr9dHGK0UwykrFPQzt7qdmvxyYZ3drYtBvtyC78Ud2H/ClL82jpnYxoUXaogihGnyGLZyXocEp1QqPdF0r9nyBZrZ3UQpMWTutsmYIPURWFcRC2T72ji5hheNRu2/+KEnwc0rY/ywb9lUiT4ncBs4uky/unG3dceSS9m4fl5Zk96rghqlfwKsZwGqURelfAR30rR4+qCWmEhT0MIP0fDk/KsfVHeFAOHeRsDucd4hkl+U0RxQ9Jtgp4Bohj3sZVSvBBHgh10vgBpMkjKEzeiNyVN8G6BXBAXox+SE73O5qaAc6neL/x6N4DC92jqovsCvyBstUdyrT7X5cKNHGz6z8h/E5XICrz7CIvGl4Jo6S3HAew/Wtg77sgNQ1BsLpnxCRL9n3x5P7+ax4qyl31sjebnNf+RQRSUIS4FbYr0SYWG2SOQ33OouKgNkl/aXkZFa3i4OV5avxCbsesZF2/56+HSfY/Xp1d/s987DHeZQm4OkEB2R8590ezp0Psnnsn5tqZv95CGnZhcf08HojMm3lZiGn7bUtH4xrbKkpsfusXXY+4SFJIbs+swE1y7ZZ8vvm1R84aryLogSwA8RBD8d7OZJqfhVEOhEX2ZhZaoXITrQ6Y8pZvWtsS70dUJFDX6XkYWpF3SRYdksR7qG1oahSGWSXqJ4xdXuyJsvo+itEloAKaVZLrfb6tOKvNRk864H6Nhf1nshwP+adx4x6iEmDKux8DWjWDftBNuTygUJbJKoMrMMFMqXsl/p2Da6k7LEG5BB2uklC1fWz/ztxD8W/2/aBr+/RjUfq/WnFwhj/cD+KHEAY5tmm+1dW3e4wSD3KMFHmSnfVAtlNf/ur+7+ly+yXCjreBAW9JvbHnxnfuXjAPO4fH4J1tHn4iHsnxDpj/o04KvyBCQvnLqiJP/d2fUsV/Aj55r50W25V8aEv4JE2i4I8UBLQavaKBfcV0cx/vMCZaMkRnKSiklawwpByfj2ONjibyNC+u/6ijApRXq7vQ/Wwsh+U/4TSRB7zBDz2aMKsJGlqUENM1SEHsGf0eYJsJwtL6LYfvO27If6f9nGQP6DRMpJTtTrDRTQKKxvVwUIJ1tmOkcGmh0eOG7ofUTe0QprdXFELAs2DzIwA0BwfCzgH8zoIj64Kz/71xXpfpeYbgvbaNaxDXBbDsn5EXkXw3JJiX7HO+ynsn3ChWmSWeT9UmPc2XL6FSX0GrEzJu5877CqqnvHgYwfO9iYoXvHS6FN8u8luExTNSB6M8f4ajzFYA55Py7Z8YmUU3LR5ZbrD/WSShwwtlqgz1NG77uNcxc9S34uJzjhBYEMU8musiLzZwyogIk0Dond9BESzPEC/MH/7LMx9ylJztPFvCyfTdAu10sNYteCACqBmj3z7f4BhFqbO2PMxNxDXYAGxwQ1QHMpl4TKEVGdt5NcFJJSGTvd9ljXU8ZjmN4odFho/xyNymB8I2IQLz53VQzB7+PsW9STJi5wZs4AZ/MCdEo+wd6JVd2DUuLbw34pHDDM8gTVd58XKc48Od41bsHaf4cKcY0zWJD+0wP4TBAIfQPEYbekoh4vD8J9JV3SINxlNQT+L0CsT5wSTF3XGzdIrTLMOj7cVycJYFDCr8XhwZako/R1qrfZIuSEOE97q8FWbuT4vXYFmn4CYqHw6Ezwwc+vYTqQ9H+tXeW3JW5Nn58xa9KiyIfPM+RUToeKQG0hHzx/+i2v+bA81E57YGrX8Fz9VVnd221b2iyCBvbb9l50WRe2UGbLsk9Cx4jMgheOJzhDNyFJVrX3oE+Nd55t0biCJ8X945UqaOY7iYAuSAwR0i2SbYqlyUn5IllG2eNz7+//aurDluHEn/lYl+3If2ds/um2cjZNlyO8Lu1khyO3ZfGHQVLHGbKlaQLMuaXz8keOHIBBIkwKPsl26rkEAe+JBMXIn72mtcs3wnHNsbceWnawlJJxhqG6d/ufJMvjmjH5f388Y99FqUqwaOcQp9zZTFxSlnbUa/8+hua4jxy5gAlL8gVmn2VMf8vlu/qREY+uBAqIWcFkOvK0d+KM5n1+cHjOaF0VWWnx7PcfdQUMnrIx6Lb+rxHpu2ZHCXHZPdYunrHLYj+NMAXRx2JsCcvq19lWeP42Eo1552jzMbL4ZYd9o1gNPn/2c7S8bHEQcW60uqfhbY6/up8aRI9F3RhrOvnpvnfT011j80NaW9OVwBD8Wrn95n53LETdCo5jJimqQ2sPGzKsQnvNcISb6qeB6wDBsqW6P7Xz2tqfo68/J4jJP7c4mJIeuP3DKGv7njGqsv0Sw+7IHlzrGn5x2m8Dyw/d6O24wZ4692O/5YZ7s2qGcMN3EgndIlRJFjlrZJ4J4rK1Jjwh30aPlcgwOwPDvs60OkcZo+T2nnoizj3UMdQ/xylWgHINyMPbT1q8e2/u7YFtmR/M6eivesHrhnuJIDK+d1TQd8B5i4jflUpJpwEW8waqf68g6e6Kb+2/2u23Qn4PUjCa1m+Dabx1RGlw9s91d2OrtcRkFO/6wySxd/p6qmb88q+2jzXXEXf3vzjQmajmmmaqQ7JD/t4zpjJrLgZ7fGD81zSkGlKTdimUVvwnuW8cku40eSJr8j0OEds90prx8puI6fa4Oc1zuqqnbuo0dvwcPDdqvfQFO1PhM4XD7vUian2R7VA3Uz1yxPMvzFcOI2WBmnvLVJT+3yY3+EFzNnO2Xt4QjVu0NSJnE68jlWufbqr8LyHCHJv3giOs77TMYbeEuBcpZVtUd0Wc0mshN+qHXK4X8/Av3XGIF4bhj8+pgfyf6urB6Mic96SUMJ+as3IUdfcHP4OqZ8fDQTyTMZq9dd5oJfpnyK+lZ+nX/WSZ8x5llRVLF5+qMH/fSgw4fuXE5hVJq8Z19ZOi0AvH3I8hK5LjBuJ/OqIqc2SFp8O7Y7Z773zUKeGyFd1qgsBOSwHGWkCqksz1nuq71F8pRRBLvK63eM63yFU6ZPTcLK3+LClgTB0y7XLWuS5J2H5wmyTb6JrADCOvn30pcrTATCz4Cfif2bQ2uk402kj+Vhl5727N3hNqmajfsZ3bi9peu4KOrDdFVBOXkZuWvMi553SamnNB3VkrdTLh9YGbeHD7VQZXSD055Sgdv0ZzuPR2luWBVl5RfFp2R/zybtZTYt/F928KNj09ynPD62UQMiG72t+kFlqQ/Ga/kqq13sxHGZJ1k+MYE/V6d+xtXd4mSvX31U+OmvKR/gro1I/xKA9B0vgN7+5ZCYuU0whKrTlo5F+R3nOEPVEGnHOw0nLIC4dmbLaqt9KYrvJsFQM0RP8mMt1eSm+rQ8ZPvLWtVxg1NuiDxEG4ajehXg6GZZrYGJ90gGVRwHa18zzBMBzXJHtw4zom+7Jqi92rIa1asSr3ELO1N7UhTfTYKhZoievGb5Y8LTEzW5yMTEaWO6VW2PPGgFvqP6GGTs6BX1JrxcQ+t1Gge9tnrIcTy1110H8+TeXsGIXn/vtl/59n91MD5mQKMxkom8YjZuFM8b0qACdPKPkqGpHLBHb9jXhD39xtLjl1N6GPkB9rD0RTn14qU7GoVHd0hXfZIon+KitXiAA8Ov0uy+uzR8rn3Z6kdKRUBap6tsdp0VIw5QDjVDDNL6Psj30pfeFg59g6PuhHclU3PLEK/zNDWDePAsTf/M6q2/RXP/zuC5K0UvDsXTmN10sW7wz+j59sCyD8b48w1NP3lzDXWKs+F5ulGvZTZxwP+ygp+W9tDU75ljSxjWL4oi2yW8Z1sO/zyxE9s3t6X727tRfXtXQf6bw/5v9Wykfvnt6ZBmQ5afW5Z++Xn48cMpLZNjmuwq/v/46T9//vmXn9Qh88ehOW/9t/rgdn1x5TIudvFeN0elxh4TA5RckgmhkAX8D40pPxtTb8/G6WV2KMo8rgyuj/rksEuOcaoaRSEkOoha175JteQ1O9a7XYfSqDmFcXcrW2fe81A6wGaMly8ETJGhFonX81GgSURix8oFcn+GRxuGsUDIwk0VHF0kdmqmhaWxJbgxVJVpHftdODRXGCzm04Sai8GvW0trHu2O2v+jmOvKxb7tf5sFa7LAsiBKURB0gRYKgyxFHwpH4fH1VcAJ3PPWOhPqxe8FTpCF1gOnYaq1CJz6oxke/RKP69W+G1rqD92ITQ0/bt2nwGeKsO5f2Jv0B0oujpXJ6+N3rfgJK6Lb7JTvDHO9tq481+t/nMW5aAe5IFkCY6tXeQZwmQ+uITzNh5sWh9ldnNfHNL24izOGmVOXLwEzwwHHxWDWfl435cugkE0rOxtP5hKerdGR9QizuLHlA/Hl8TVjKD4GXsYDK7Oiq/tHm9atXu9QT2qs03cJAoPiSOVn48NErVz82NKLCFHV88lXlj/f1bdn0b4ViaROlQqc5oAO3tAXLlBVw2DD6bsmyLY0JvooDrkbs67AXBEawtMZheWqRi5R+WpwtZkVywVBNf+apduq1cKLlnWe9C8xXznLZ1y4FNlKzckFW1/AlLTZwiJmC0cJFbgSUzs0rNNBRQLLg0DNuf/9eCBntmKFVWFvM1+4FYBt/i+du3tb+GunXAy9YVVrSZ1Bo7mpSdipkVuQelotmgV+8DVfXKyeIggEFRPMgETCNWeEMeVe80owadvWAXod6efvAoUuKFgMfvgF+blwV1bzlEqur5WqLAJFXxfGRHllbMklm8eUpI4DlhaDUnc73iOILPNHJfckb6n/besAABM6rLzrDaCdPoZXBwb3ITojJCThljsN193w7x9YtIXWXQ3ZPfQ/zvKRUXOqQKIE9jOdwjOgyphBBmFpzjKxELIsAbKDgzhbXLn08QKwwjMLzY2qV0maVhE9nFVpFKZsh3hd/N520UDiJRt/eTS0ySUjWImVfryUjJiSKH3Z2Xy8Oo1cPl6L4ap5ieS2zHnugYIHRFH3/AwKrp5A7NHhx1nABUkuywMSBIEZbLAwMAPVovAVHxVaBGtdgpVt4KuTVpJh+HHzOOpV2QR2uvwr28BOJ60kw/Dj5rHTq7IJ7NSJY7aBm1pSebOW/7B5vHA1NoOVJslQpAvt2lMBUdLIqEnQ/RxmP5/aix7Q0ipC4VaTL4cXNTusmMHTusKo1pb7UyucB12m3L8m+WTCMAhUTTIHGsm5kDF8kpIfrwCstl19DAimjv8OQeoMkKUBasm7PPd6p5MHXdUyl22AzAi+WRe8xgJudWDbkAdcI+jm8niTgbesp3vzrX4MLk4vTuVD3eKOFyqf5FV7PJMGcsY+I+HmPaBRPRdALobFqyw/PfJFWd/Aw/cXe55SS8Kvm8fFoMt2QMBfy5wbBc0TnRoM2p/PAwfAM6SrBkLE//s2z05HFAUCidZ57c+zfIg4Q12EQNDBDBMQPCRGg1xrcCGA3O49FhIvCzgdel/O63E4+QqCD0zsKV0XEkKzRy+O/Tpr/MLFWgxE/DGCAIfkTLGL9GI5b6T9Zetn4xo1KIxkgy/c+5uYNM8LmjnDXDpqFo9wu/RMH4v4nv2WVNLkzxGcV2qlabhEyUF5ZIKzScQlqUXhu3gmLhBrwFhx9xHfAcToPmUpfHEJl/3w8RNxa0ZUL6UuQcCjcLNhZ1BkS4BZfw6TZWAzY74SN+AsnaXkbfKlvIzzfXR9qh9YK9j+U1I+IDqM70bLtaROCqmx4cdwnoTcTxNB0etCwgTYFYtDRIp1YIXGdmkgXwNJDsozQ9DjhABPaHOOe7qK68LaR3EorDQaWhPUZouRRuNM6tFlw6bfs5KtP86updQlaH7dNoYGRdYfZ9+wpwrt11nVQNE5p02sTwKCS+KA5Ztfu4S02sRKJoQzrx9BSzi+HrjM5obGYkXqlmUT+/H3Udf8JeuE1JL1BXxXYjYA9Xqs/zPWicpXjGC5x/ZaYORoCw5yQbA8o+SO9QQi8rJEX2HZTdouNeV8iWx+bNP/pBh9wWilPOWH+jEdFuCscaB4WBBZCW2kkjOIgUV9NhH99hd45vcqP9Jjoe5lydg244JcxnkTsrw6HfYpM25PtNsqAiWw4yWWOsFElUgJSdTCkDtamC3CAEjTjcJzEHE9ENrEV2oNMJvzWzUKXYt/rjRsrX8Pfg3AmnFHfhSult6Yb/nfHtku+dLePrwoyzz5fCrZBjBmlB+SC6PcOvrM6m0BirDofxzrPyOKXqLrseOBBoRQrtGgK0HKjjDMStR4CE31oCZlSZMJsIGVOle6tqGB81365slgW9xhmzRYGvNd6HAX39tzmK0lihhkhmQRS88kWhBUcogQFkw/BoDLknHH3KdAZ34PEKN2+JIoq8gXB9qfcZ7Eh7L3rZfZ4+fkwAkj/Ml48T6MSCTdg5EKnJYF7bJB0DKSh7nihRooKNpMilKkEMVeM/42MzdfPVDn/+5OxejSU3UCPP95qkqqXvp4SHCMSkQiFuSCs3SPuIFWDT1R7LXhb7s+kQLJs/V+W3Z5HeTsS5IaIdDlcy9DrhaNsy47eoDlatYaVSX+jNNTD1KzhmFwMS9yuboUMVvCkBgehacwUG60dcCz2sDSsF48nKSEDNsODDcX/UW38eMxZa+zp0OaxfgV4Z5AWn/pf1w5GmD1lkeCbPzFsHCXsLzSb8ekPOzW81aciJp/nWMiyFesl16SRfj1LJKtD/pQmInSrQBW659SLgOiGaeNbvhZyURxOI7aHiReP5A0yd1OM28PWLpKFKZSd64IZl4BZg2BfqDEzHR1+OjncldJWtb5boNfb5jLTSmamYXUiM/n2oRVV7p3W/YaRZnldeCVPMb585tvu4f4cM9u4pJdnvKKxe7ZEN63BHJo3/1Id3JcBPk8XfNLoIAc0isMSBo9aB4M7YB1QIP/8QMTC2BCsvxyr8HHu7/eHSpZdn/dnj4Xuzxpzopv4tYVIrz8VjxGs/k7WJhmLgsEq8Pd+md46wPdjHH8FMwtHc1f7FLgwV2/y5yBMNeLLl+cH349izXOQZ/NrHFefPmSpNUvLJrpzZOeodzQ8OvWEx8MqlCYLZ1QhT/REzRwwqGg8dafQjqziEfXaROxTqXf1wrQHyqgxvcsusqzx/lQIjNX4ia5aPP4UBSicBQ7Yy0Auct+wGMl8Bi6YsGwtUy+ViZ/n91Hwr/rjsKDDYVOjhqVspki2J4rJk2o2MVkszBAE5UiRTGyiKuA2iYWgJZD1Zx+yxVOi7usywe2+ys7qSdotZ9xbGmUEsj00lnQBqtlFi3kGVmLPcOAEdGQhEu16oJ5MHenvNL4/jp+5rmY3x2Sul1PKZnxcEtlrCS0VAu3nWdZ04fEU+iJ1eCjy+xu1shXPwfLpAoqZRQtbMr4cQDxC0qXDPJq3cXw+T7bxWnyL7bvZIzex4f7kzarEXq/JxC7e/hxFgRqYsvC6KVBUAebKgzadJ0oTDsJF8SX7wlAuPPfaux/HjH/ZmL9dlfvhn1N2NNvLD1+OaWHOglx3SVD43zVs8dU9XtZh113WYTVt8KubQNEX1/mfCRTlwPYaQaICDdgJmGu02iWZTJKjzjxXhKHUoHtXEFLhfb5Yojye1QAMsYKwSRVXu6oSprdX2aPfJ5IdGpClbn9mMhaOpIi/n5G3go1tRO7mWFU//s6U195UHqRE6hd2Pw4z0EndyR58lKwedaBn062xXD0O3sqHN2RUGVudySyFpuUfj8jd4Sa2ondzDCq/228xNITqF0445WVEUjy5I5g86wDP51sy8XdWZr+mZXJ4b49XEmd9yn1Zp/vqfylqFwrPCMXZba8E88l8FX/cHEongwrUwKJ2qvdz/PcspuCMV8zPsRcK8LWIOI6Vg9GLV0tul5FX7DYtufytHoxO6DWf6tlJJy2d4PFHUPz3lt5w3ND1YOqqsHybpk/27OrJC/K13EZf44LfZ+vrnXLyu6O5/Nh9yHmTyn99LemULxzKZbe7h7YY/yPn/afs6rf48+pWl1zQTIrOS+YxksuhpjJFFZu7MT2bx7jJL0oy3j30MT9AFuQDuYPkloEGZJfabyHIojdUEpX1aygVS0rq0b53S47gcaUiyFmMoWFW7t7/oGVDxlkP6Uc4qeQ2BhaExjoMlirgGJZa7lKShGNKAuVOc7SyMg+YpICA9hQBI6YvtTC4TIu2T0/rqJxGIogDkOpzUTJrjzV99V1E3UloIm6QloPGLgoBIb+iFqiiOrIOzPXLxAnOUMcK0hl6jWJ0CaC9KKDzlsqBplKFDRrG1CjUZjs3VGRDd5W/BAfTl9i3lcGTyRTmeQQKcmyWISwc5cpbANVnffo41WlAIetSkRka+BnZmTl0N/o1Bj0JVD7faFNgXpAwf6tKwHF7wotzXfvSKNfZpUAjB0VGhvPsho2FXS/VvE7NOaVcpCjTGJhOOyiabyGIojNUGrhMJyq0zgMRRCHodTGoT5Wxm7LnC+0FM3TYAA3kAzkDFJapBg2ADTOQxHErS61e0dhtU53ikIh6AuFcgIfhAPeNtnVNEkCUHfTFJtcTkNh04Hlj0lRVFPJbi1R10cjAXXTqGwzh29VWHuI04tT+VBPd5uH9FApzOTgzMJYwyIdvzCMDHahDOLLi4uINOA57V12THYYn7bQwKiloHDCmBjbbyko7b/Ns9MRY9IWGji1FBZO7dUJjUn7O9R+W0QMYz/Wlzz78+toHCuTmQJZmdIixdvkSxU25nuLFDAZJAVMSZTCwNnMjdaNyEdAKEO7kzQx5ZS/ZyXkSYUylElTbGFyw54qha+z5FAWeIeBVBBjkJAQfyHTrqEIi7lIE6yOEOkxudjEidRvN6yaDhzqCSADXbBSDptRIrGqpzwfD6moksBqqlS0uRz2+i42q8PoTfM75Clk6kzP/Kixbi8jOWg7Y41R0pHlcpGI2qf8JVGsA3mhobd4OY2P8fk0jL+xkkEuY71x8tKFtKxbKc8Bua6nEASiSeKMFOSBHKpZWnKHXmtrWKQT0throghlEF+h2DqPz8AVy/Z3eN6eEdZEhzSjwCyqK4JnUF0pRXTDbplUjCpCReldVZ/xjvwMTg2VcrBXZBLbggeWjFBf/8AoweUQjNhhpfGOPR7TGBypMJlt5XGgJG4VGCTQSUxbB2TO3YcBZ6xRmL4vVLZv2YFVA9nkHHUScGqgUdnWRIc0hfqq6FAGrosOxRYmH3OciVAGMRGKbY6iqr0/pewuLv6CHIVUDDoKicJmtyGpn263oQy021BsG40sLqqB84kl9w9QiK6Ug+NPJqExfJ1Us4gCdkM6iYGtQEVZ27B4QIDGsNLh5PfUdFrAsJcJTKtSCql1BArpcIAxKJTCo1AgoLNqskuZ2DUUFpYNkdWdPx7j5B7q0qEIdt9dqRW33NamL5ZKAaNWISKssb9nZclyC3IxQmz9HaK1GVnPu6NbW6cBza6TuTLHgmuMkCQGLaBGs2QAKxwIJbzWgRA7ikOQgyYAbR9KTtmgsQZo0B0omcyqdj2C9vipFZUAVlmmsaEwz4qiajzFueokIPI0KqupId+N+myKr67+VSJTnq4EDGK6QttEJ/5mOFohlYKTHJHAxgrZezFsu9D2W7q1f4MiOolpEyG6OB7ThO3vspY+Yfat0K4yDjqNgiZDS26XQN7Ux48hIHT2cwI3rKhcMD/ATDgG0e6N4qctNArTFmtLROgIdXvUsstrJqfsv0Y1KXkBrD8PQ9t8pm9CSxnzXdfjKMuoxNXUaKBzlQLIsIAJA5AaZAKoCSdQ+huL+iKMWIqdQ+kJCOEjzkoqxQJFKiv9PpNuX40EO18hU7n0sK1bCX2pMRSO6ltOokdXST2WhAqm8+gNtXqnYLiUoZ4xr5jj58elmsZj8rwZ2qn3F7LqbmaJ5FPhRqPItLhi0El1ro/pBDpmGswgfs0g9LfEgwoSsZJXtVaCGPlMc9Sfbdbtg1DiSikHqbkayFls6DYUWF0p8q5+F8HZ1YcvTUHXynT5V6N+f2ra0O8ajf8eVwN8XhGdf7iraYr/o9v2vKWuOaWa4fuh3Lhovh/IbQqwJmgWvTCsee7i/J6BI4JSzVOfr8w83czNCTtqpZDIgXyPVhbSMC6oUSv5d6kLmQW4hFMYTQJWCIkT4LaR1Ijp+tDoj20k3xNCP7UynUEZ4N5So4XhPtJYXI1Xu/+ODq4JVV2nDeFIkStYovoe3aiqmj3W0kiDBVszmUC6HGYIuUA6/2EXdO2MVzZeJxvd88rVOJEFCgG8jme1IFSgDYHlQcxkHyQgebCBMrNJ5GXqqFunZu3itjl8p1fG1YYv5XGNzVftDK0oC/hAY9iVwSAGxEM1emXD1wlQFlFvLSYTLzRGfbuAeUDCAKaALmk2JjDevXRXvd2LMSmtkvhXV9lX4vWwK7rjVVRMhysqE3rvpRlV7/eT+i03g/PEiQ09rtzwbrocub0N1oQMoJWFMATuBHFiL/24BjO8StK0cqR9ywYjKKThTEDC0ASlu0xyQ9O41hptyCGgZGWQGsBSLbibAbpyPjxjA9jCXAFXS72Oz/XBrtrLNQ3X7JtWKNfm3U3TZRowmkMnCmECNVsCr4lmQnBXtUsUYFRVJwqhqprOgNccfpw+ucrS1KimTBBCRTHXQTNngnIZjFOtSbcQNS3CyokkE4XUasipIvp6SBKIESqqJ3PEIzGmMIZW0aAcktuhUdGSr8HYEnRMCGzVmIvCuynxQIhW0f6B0xQ2KbhKE4IHs0jBtCP6PEQTNqPPYxpCeD07mpYykSmnijEcpVUMgSZKnhnemlO+GHfT9VljjHYCqEIYRUtvw6sKv/pRlx+VtuurkAVTWDrsPWgMH+geqXIk5rpB9BVpLCJLGXUGkcFMOXptvaLfnm2bNHZrQzNZ0GV6k49EsVnTiBXoPIo+87jlaW8Iqyognf8lFSnZE68F53Iaq6jJNykUIdxSOPWg3FPRcL4CP8gBVzCcYJh8kAPKdCW1Ysxc5ck0rd2JdgFfQnfv11WYo8+mhdtAJfGruJYJbKjnaQFlkN+wJ60T+d+JDq9ql38tuq7mZw9xwfafkvJB4KArbqviTR2prpphjldFs8eNN4Q0bof2cTPAFTwpAtZERzwpdZ8n03wU+51sH7mWX7+wlHn6zH8WhyiQBHCIYtbCoR6YkNBdRSDHoDEUMtKHCIwMORV5I5QkiX7MYh0X9kp+0bGMabr0jfiYUCj8Kq0muLztTtF4Ot4rZqeMhnZxLWVCT4KDNbXvqzGT5tiJUHciyTrlUwm3NOeTcoJaHB5IGcbVAblO25FsSmE6YYGb0Nkorf/+Dn1sQk3JGokv20AjHCe3zgj093fEuQH+pI4y8uFktO3otySY9WAg08jAiUMMjsVNYZgworT+541zm8GYZJhwsNtS0b+BSFmUxdaImX1HIMiQxBjJwAyCa0QzBvzY7UIzCKFVOSG0oW0ksbNvtJqZu0LY2NrMplrpCNBTrhhPueDUwdyEnlhGbEEsDWIMwzkVnNqqFKINoMY6TGJIJE69ZUluwrDq7usCJiEJu2hRlzTqQUxs/4hTagcbolszp/wW7SibSk3gBoKezeWmMD2Hu1UTj4Dp/NikWCu8iYzhJLmuVXmj1rS4cZVm5AlcI5Qt2aTmdmYxCqVdKQmuqXE4se34QIjoKGf3hmETKES38eMxZUMKNlxxhdIQuoxO8hZW5f7pD+lcLKQyQmlfSBp/clZ70oRXx18rmaK+4dulE/n/WoVXVVvWbNdK7R9ttEqwj/bYlVkvZnEyyFmbQnnX3rIW71J9nrV5m0TmpjViDzf+M57RL3mM8+c333YP8eGe3cRVeN4/YgQsLlormVyw/KpS637hF5PkNTDxoadmsQt8x2miEfgfZO1l6m2pjTykZNy+sdYJsYtjeUiKN0R9FcqfmQxO2VbFv2te1kT9c0nWkA2hDBmyac9ANTvL6AtPI9TvXj2KDDviOpH/rXDt5aamJvoo08hrDWRvYaAO4SfQh5Zu+2sOnoEvv4cUXeXZo8keJvIQBoGffmqdivElp8mmuMscDCEQb90MwgNSkfaYFOQPcXKTSwMfu2odm/EFK6wdrA3PJjGBAqQLgYd51IbfeIqAF6iA41PUugbzYI9mNXayvYNlbktf/iO+aDXm0J38NlT07pCUSZwaDtSaKuBKjjtMC7+Adducv7O8ajXdGN2BYZ2V3S5o3UDqGluCTyLTniUbmQJIfPPLnv8HpsYVnJj8B3rbrGnC+lzZGHOYHXNwh6w6Yj8OuJ03ae/kRPV38TI7FGUeJ/VjKFUY1uvXJby6yyKsvmGlyT8vu7lHZM2CppzoO0XiHNT+5pDHbpIKnIwu1ySqjuu7avMJrzJRwSY95ASsXkxrMTxigXesmgUOwwNV0ww7JB4zm6unMwtPTmy2gNrC01vU3pde69INNLHF8HgCHivjbZheIZtm2CG7m9lcPZ1ZeHL2uAXUVl9VI38RtdfYgA+Ar7Zn+Moir9Q1nwfby3MezC4mpSNYUiA3K+WU/25Zk4gf4VGBmTUIGdfqzDEePdCZbGT7xrVCGGzT2qPqL180jdSGr3qZ5X3ZyxfNG4/tD9Wf1YS1mu99yPYsLfivL1/cnKraj6z56zUrkvuhiZdVmwfGX6MdGu1o3h2+ZN2sT5GoI+mKu0c3WBnv4zK+yMukfpqgKt5VUSd/vZSvz9TPF35m+3eHP07l8VRWKrPHz6k0p3z5wsz/5QtN5pfNfYPChwqVmEmlAvvj8OqUpPte7qs4LZR5NNbEZWX9t6z6venLamiW7P65b+n37EBsqDXfa3Zkh3015O7Y47F+xbv443Abf2VjZPtYsPfsPt49X9dJ0vlyE9aIvSNks798ncT3efxYtG0M9as/KwzvH7/9z78BYVFrdDNiCAA= + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/201508211346171_Merge1.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201508211346171_Merge1.Designer.cs new file mode 100644 index 0000000000..7884363732 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201508211346171_Merge1.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 Merge1 : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(Merge1)); + + string IMigrationMetadata.Id + { + get { return "201508211346171_Merge1"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201508211346171_Merge1.cs b/src/Libraries/SmartStore.Data/Migrations/201508211346171_Merge1.cs new file mode 100644 index 0000000000..146e495730 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201508211346171_Merge1.cs @@ -0,0 +1,16 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class Merge1 : DbMigration + { + public override void Up() + { + } + + public override void Down() + { + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201508211346171_Merge1.resx b/src/Libraries/SmartStore.Data/Migrations/201508211346171_Merge1.resx new file mode 100644 index 0000000000..3f0be8609e --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201508211346171_Merge1.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + H4sIAAAAAAAEAOy923IcObIg+L5m+w8yPc2snZFK6m6zPm1VO0ZSpEQ7EsUmKemcfqGBkSAzWpERWXHhpcfmy/ZhP2l/YQHEJXG/R0RmVj5UKRlwOACHw+HucDj+v//n//31fz6vslePsKzSIv/t9bs3v7x+BfOkWKT5w2+vm/r+f/z19f/8v//P/+PX08Xq+dX3Hu5PGA7VzKvfXi/rev23t2+rZAlXoHqzSpOyqIr7+k1SrN6CRfH2/S+//Pvbd+/eQoTiNcL16tWvV01epytI/kB/nhR5Atd1A7IvxQJmVfcdlVwTrK8uwApWa5DA315fr0BZX9dFCd98ADV4/eooSwHqxjXM7l+/Anle1KBGnfzbtwpe12WRP1yv0QeQ3bysIYK7B1kFu87/bQNuO45f3uNxvN1U7FElTVUXK0eE7/7UEeYtX92LvK8HwiHSnSIS1y941IR8v70+fV4XZf0BrrPiZQXz+vUrvtG/nWQlrsBQ+YSQGoGl+Rsexb+94gD/beCR92/ev/nlzS//9uqkyeqmhL/lsKlLkP3bq8vmLkuT/4AvN8VPmP+WN1lGdxx1HZUxH9Cny7JYw7J+uYL33XDOF69fvWXrveUrDtWoOu0Az/P6T+9fv7pAjYO7DA58QRGDjOojzGEJari4BHUNyxzjgISyQutcW+jP+zSD5ib1aPD/ewyIn9G6fP3qC3j+DPOHeolW7C9oJZ6lz3DRf+mwfstTtIxRpbpsoHOrpzkGHrp+XBQZBLkzmvOqnexQPBuOw3hCSYokQ5nryfpnS7IaWABU1VNRLkZv6FuZ6duI0cgZ4ubrl6qGK7QWlqOP6RRJnOxosShhVcFq/OGR9q6bu3/CpJ5ocElSoI3QmZ0vwGP6QGSUXOS8fnUFM1JeLdN1uzUKovt2AD4ri9VVkUl2iB7m9rpoygSvl8IAeAPKB1izPf717WYrstighn557k5d/cPWpG5rnj3lrMgWsJynbfTnY4pab+WXoQ/WAmWeTRXrxIsmQ72+AdXP0M0QSXXEPIQESoKgnzF2w7LAspUot+M3RaYbGRb36UNrJ4zc5Nf7+wrWYXPxOV2lgSiOQZ0sr9N/wTA0l2ipYBkTyqsnxWqdQSSjXHbzCLNxUkIkGf+Rro/KZJk+ho8D12rWzmiUW/Vm76zib9f9Lmzcrvt93bbXneiBWPBouj2gZ8D5jsugFJqGFDRI12C75qxq4GrVGxrJQd9Qt2XYb/8yznZLHEMRmjXsxjAp8kUVJm5xXU1P/7rd+khdrL/mp2VZlKGoLuBzfdXk3+rBY4D2bniTrsxbJ6jQ2kML0LPuab7wbbVJErSp+bZLk02uD8fg0vPqU7pAqzV0gtCfD3gLR/pBQlyIDNfb1f2C/gMPBiPAfdDWov/vDSCfEC4fH+iHtERabFG+vKERHcS/ZlWbxP8Ywu0DrJIyXRvMDcu2DU2l1ToDL5+LBGQRBmrV2NcSmTdhm8559QHeA8STbkLBYZ3Bplf86xokS89Dh05eVG+kCA8LT90WRbBQNwHuCZoEzOPMkiJKPzsjAqjrHqP3LkfaEc9kZzOGpUf5EY7THJQvdn6EIOn4fhzd7wvSTwxaZ6yWldbkmcpXLV3ot2ec+auGEqxIDajMitR1mkLl0HemlmkI1FfLkdA1gsziD8VTnhVg4SWmFyl40yM4iGWNHOlo9LFJh1bb3+4HmT0u6gTQ+5BVRDWat7RvqxejsYXqSZHX3Wnx6GPBYsRwnBynodNnNKZqCv/5eXUBn8IP/29KgPpL2Ym+qL6tF3ilfZV7BqKrq5GU1IMMVLd1WaZFSeCCDq/QTjq9YxG3GsmlaXALFtMP7qaYZGhXcJ29RBmfVTuTjOnkZPQmjpNk9DbMMTCRTLDjYvEy+lbWngR6bR0yZz/SKcoUBrr7MRpFh0YMGxI7sfgC8gZk2Yvz/qw0jzZGifxcM7J5xJ902htUtgOiSW4a0S0LLB0IA6Mz71jAsMAqplv+ug2N56DcGJapzrPyl7+M4vJuXcMmh5Jl4/q2PhWVbocYaYSXRRkYH2MR+ztW383BwCO13J7zXlfBHgLsb2iPDNCuStYCqjXS6cElIKEqX2C9LHzcUV396g2D6CC1dAxKEWreQMnnJGsWcHFCJh2WeG88X4wfyzG0ize48kXf5PtIYeddm9dIo1ijNlryTzHabpSkA3gFEJ/Zc7Be+SXN01WzIgeUR6tW5ei0XJikK4Ck0GWJfnU3zv76+tU1PjdF3GTEDJ5Hwtxiu4IVojWJUI1EizMEbncQ7XWJwV6YlsWiSerjJl+glYQWNlKOy/SuqWEb/ushX09ADbLi4Y0J9UHiqtvaEC2U0QaiR0P0HWRNMLbzCi1IZCXBpPaIM1PaZBu6SS0yE0ve0vU3RppDNcFuc6kbZMoJDcVcuoe1qm6rI1boimiJHQlZH4AWhgWZjUnMzbTH18UIUqF+3mGhRjU0httzSWLkbTbraOeGiAUHQeFoSElM8nQBb5bN6i6nvA++yL6jib/LgmeOCIdqGR5kHC/sLapDOOBg0uBFpXcPuSt1gm2Od6/6bJFum3rXgt2Ab7k6mkGyoOb9m4N3jRJyG4bVAOy7ruu0q6IRrl4clAqNeGxJhJsL1QUuQYna/FgWzRp1JI5+0W0C5/kifUwXfidF42zlrmddk+/t1oZ/nPaOFqs0PylWKyrYZszbtYRr4QptyeEmJ5qcp6/5p2IFLyPoij2eeArDF1gDJDSw7378i6u4sSkZB7d3k9baewtxRnaUZcXT4NGFjyl8ClZ5j9brsniEiyskjfOH62YVNtMXRR0ZY4/uBm0YGTfo4D7GQ9qFfNwUR0mw5UAusePOkb0xeIqvfzbjLzqQN/cgwSpIifbR+gIZURupMVqzHxGLjd7IefUxva9PQBlsgvV4YmgrV/D3Ji3h13qJKN5uJ8Gc0uHcKD86aR0phOmowbpxnSZYNzpaLLg+BI/pvNpE5MeJ7g5OnpVn7QLvEQaPEc1Ku+C+3gs4PT1aHZrT53VaEkX9A3jhcdqhOErq9JGgiMH2n0B1DXBGjFizymJzvM+EeoPDIY4eSghpxTEgPGCD7AY+j6+JnldXMGlKOn2Nv/ToEJ28JBlsOxUq42iMl7BMi+DVN+Akez9BHLhWzit88hwt398Zmv7+KDtYu1ssUrzyQNZjPFlil4a9o9r9xgJ4Pn1Gdk3wWjivTqtgcqLenCAD66HAsQiBRgzIkVF0nj+ipYnQ9VEGgWpjXSQ/ox1CYHONYDx6BCmqm2YU1kC/sbSn3htWmkcc+efiqR013mkQtuCEoUWd3r8Qh8BZUfZ9PIbI+go8ugLJzwKb0zhJb/A5M7YGMcbzlpbIAhmM3mCNghj9XRxKnElqMbbxJ3EwEgLARYcs1ebEiqSsorWAO3jcvBw3db1xLgSsLQz9I62WWVrVcZB2iz+DiHmRXGf8N97Zu5B2TtClSfiJFo0k+g70NVuM20BnmJxQ4bvR27heIzwgcxyIPU6S56c/ZfO4RUHjOs0XAZh6d9Zpjk/povBXJ7YYzHBkpugE26RtIuPjJoXtmgwW+GgPhVU9HEaeFKu7NO/O5SIyIepzH9xQHa3XWRquMf+A6cNyvKXI2jHR0f9IFyNi/zQubYadJlSeDIjChEm8E4t4UReQJO8M3n+3J+RCHGD6CMsXXNnRe0Jn/3Ksegwq2O0+UczuAd8XCKqmhG1mM6U6GSUP1dBm3EjsAS3+waK2Iy0JgyAnWgYPVBz3ehcmAksceRHL8cEgxdSIjfN6WbT+FFC6ezrUUUvtljhskdLol770toO+KTa+8k34ixpKiEnSgLqGINHSQBe6c8sCClE7TLkqYIcF8gww2kT9WMaI+YdMGWPBFCFWjiPqHF7EIFYPqHeLaUKneBDVAAQ4z47Th4javtOAmv7LwFRjkMJ6juMyJXi0Q+hgNL3nIFQd58E8+zwcgqu73IJoeswCqDrMQXn2l1ig92mbII+OAFb3X15FMx59BdX4DLU8x3sDHrSDu2XgRHlKFSvFKQ3jKU2/gzJFSp3MjtR2X1NPM0EWtVSzZFPVc6p41E4Ddxit6xC9x8Wm+9XwIAsoMiFdruRCBsiVDfmzYU1neVCxuyyEssMcmGuXaR+OpLtDsYwzhEKBF0QI2ezbZzccbrp4ZH3ulNrh1yGuWd2WIcp3pJym/czEiNT4VmFbKdL9JfWNqOhOrb4pV6vc+eS3c5n5JDFqvWQeNbvoquqkaNb4jvYieGZETAFvUtnNDImPJKsslEc3mDDxXONATGb8TWEwxUTDm66gs+U3cMK2pAV23ZwkLoYxPBLmMQjOC2uPRIeq43x1Xq2hTXkNyThkgOqhSKGDLhj1dnfADaMexWErVrdl2IpHunCD779EatnkwJ/ursRxUSM+nfRaj+EJozg7U7+MYl3oOVyYifR6ML7RFysAsPO0OZ5U4ZtU4a8JMld/qpuizQjC4/aOyOnwfGWDxwLsDosIoCuQP0zwjmHku3Hx4xe2+ObQtp2C72UKA4FUJD3g9xQ+fdEbVZ7SOPbho4fJYj6AlNg31l7YVkxLe785qeqBNj3mywRNXgAIyw7Q98DzmZCu/kFx14ivlkRjvZBh8QDQn8e5iw8Lixcz/hTp8uPevGMR++RWFCFyCOPJLSdv/MQJgzM858hBvEyVz0xlVkynG3ltpd6rQXGMplo0jis4bsyFqa8x0gRJ/IJRTtcofIdVrF03UR6DkJD9qsmgVRboOH6V6zUc/dTqOM0yHPXYp3cOvP3YX0qNg67PPkwMCCbxtZPHZoNmkPA6N0WcyaMapZOIT9lwPxtmL1Dsljt/heM8nT4jQUW/KDqBjbyJfxjvBElpHWvPm/z2HiYKN+AhbxrRYbdRt2XYCMZ5yPukyIryE3wmCaknb30n3vZ2tbMiHL1ymA6rRt1WJEsr1kHQeXUGAZ6IXkGY0a+t3Kk2HGpxB2ADrL4EMPxQGEUi4AgmnM+tBVN/o1hxl+I1gnDZQGM7yIfR5QNN7j+EjGC51fK+DVtJf+mG+UOxDtUVRpAfITeHbPofRZZEEiIH6bHN8V2TRtxQnHAIUdquEKVDUNEOBhUd4mx2L87G65SL3dzFMy5ZuaAoSIFiWBvtVdpwO6PFE01HeIUzT3Ttk6ibTsSQJwHz+vUUurxJesdyYWPCTZKotU2nHkaUTzBb3zfZf8GKJD+Nguyi8MEVosO73kNX6O2K2+qOHW2xdLTIYVWhtWpKvSDUueXWs2JEyhr6q/bqakFnB/2q9hE/vc4x/DrYJ+q2ehp9bNKh1fa3R3ZpQ9BYpFeADa+3x2nF4jnwuA2dFeUKhMf7dNiuQaZPMRUlu/6Ur9pETP58dH+fZmkEM5XkBA62ZSLp1edVGw5ylDDPRXrfHLCJLYnDR59BVZ+vjxYLZO1McJ0qptWAu/65eEhzzyvTuH6fWVqJwv+qcEtShcLQi/5bCoyKWBdKxZh1EcRV0elCfoa513WTh5X0lQVRd5iDc+01b3Pout3B3G40GrHfPIygcikBXe8v0JFLBnJzoBJqMxBqYrNgrrQ+fcYKDciOmnqJVZo29dEVTFrfoGQEuhrSabCqIMyJXS3XCUJqQLO6LCrFDZmhWDoQsVTotQTEq4s3xTpNNH0k5epOssXyXnIwrt0kDiZ5D0mRtHNsidAvrti1S1cQ2Rg5jrCCqglmQKRdlEMIXVWAuXf5CZSLyyLN6+pTWtWqQ38JnKL3ajjJGDTAriPpIxCt9hoBWCL+OBi1AOQBnfNOUelA1bkkeSgp+ZVAAu3VkEGG/UBQd7seGRpF/qZDcLDp1W2dpWVVTxIVjtXXSRoyGf1xWkEstgb5ywTtyEPSDRYZ4g6cvvUxzRPXM70T6kGUEV+5JUvz3VQNvR+9oX+ka6wrgcyQgirSw73LIofGxy0jvYDcv+o3PrMH2dtqS6ZdQ9KtsN9oB5jNBsgVCZs2X+68V9OLVNs5DlLsIgOg7CgLFZbyqSdXQKx+h+OwPavbMmyYtl4un3iMqnN8BHtQCbJY+fRvnorPEBPxvCoMgvZ9BIF0sywhtG3wTxEaRIIWlmnCNRb4FPR3EOzijRcZEi2kIzT2RWPH8jfRhjtwUluKhrndVG7FG5eOxAwtsa6MVZztW3ojUAyL2Ssku6MUQOy8FCrILGTp4bED9Qi4qTvsQxqxtPehqfHk0vlDjmjdvm8bHJF3+pxkzQKtdeYase7kK845W7dSSfN4CZDThGf3M2cLIUvJySnEK68cO0hkP3HF6u4B+jKD6SCtjMwb/PzuPMr33V0JH1NgkHpxDri3Qa3zMts91RNh8WuVmKBwtOHAN0JUWofrsOY1a74jVfB9OYvgkjj3BM5x4k7Eenv1KKMxmiB+AITqLE0ZKeG1rI+z4mGIX3Ne0rh29YbCsR0x7l1nJokbx4PHDvJ4KmSPUcpSFK1vN4AbdpKVC6wkBQpmo7YrATyEERz2AnVbn0H+0ICH4JjNWHc3XHNbFYsYB3Bel/BaZg82W/udrcN3Inn81VUZL+r4SNtH6kYWfPM8sHO4FruNFyxHOuCk9im5F8VrN+R9udot07arvWxWdpNg3UCxfWQKpds1CxG0V2864bxXkyRT6b8I8Jsez2HLVrc1j6+ln5mO6lqTb4z2Ue3fG3gNpzjKPMvAw/kKDRan7Dbe1YhxUbTe32ckRvB2tXnp2sm4ghURbXJhLoOUSkwtoCDe9dBBR3Yy1MFSVYLzIGHNsi5CcltCa4MIGelh0L51UwrJkYyk8wqvrsuseUhzQYgYLyUWTSITPlFUp0hCgVep7ESIl1C4VNwNj5VGgkK5Hd42pnehy/AHqLoBxgt8uWRv/0+ZOECbG8Gcb8CLAS/gU+Xv0sW131AotoPHeo/uJFlHpnQfY0rjuyXx3Mc9RimjUxN7uwHcsLWsXGBiKVAwy7Zd8edXXP+gKKnb2nHv8fWyKGMsR8dmz4jomrrVaBbdPE7aP6LPfas8qgeneZSR+Zksw5YoM1OEQunuykK4XsCh9me5p8VLC+D9KlpVwc9yKrLse1ETkywhiYectQGMonrDI9oOHRb36iivnjyCedSmzYBTbtdwhLil4SljRg0mWjAa2DC7mepawLy3KA6KoJ4PR44hHUktaq9rfr1H/LfZYbfoOb8CiwjFMuyWSAvDLj2qSLrc6HLnNIrcclUkTgyXE0KKRAuZ4i0ngiTEQTao24plJM4jH9qEbZ2mO/5d9WhG2rJ4+pp/QorEJdEWA+0lbHx9JOl1bgosKbfnauM81uhoFyq1VgCR1jILgCmQynt/zX8jY9Vy3nIjkslz2UYVdJkAo45ykwAjOsh1dVumU82//GUU51kJYbQb8rFSnyJEnUTDeLgpMIjWKMlO+yylLfMHbh02KUptZ9daysBylVaVIQchD6XOpqiHFCWRHjxMueRw+1zDhklTIqwCAQ7yyVs+jaovziMbN08Cjt5yQCrUaItY0HPs1rzXItalJQ3SNnSID6tbw+uRLjFOkwKvn+V2dPfpBAnC+iY7xWTClr9iXibcN01TR0kCq2qaBi/xBWhktFqJ+dgvmWsvho6Un5kXsm5Znb1k7ZBM2UOwkrrVJqHzQYSq2yKZoIMfaI4khyNFRrnaTZc7+T7CHBfPnXOk85JDk0bdtoObBOaGLtKAsk5SmdDV3aSAwuVZ15kggUZwHCSaui1Cpa2RaHi2MIJQRF1yQl0siuUtJPez0e7NhJDuf0/hUyAKnBDbKyGCAk+sKe7xtXLX7QhhPzYEtwcppLJW8WiF055g6mAHJO0d+anrWgsQvgUESv+D4Fe3RQj0sSya9cinzCPd1ZoyuPFi88xMEKGibA5YgkaxQ/Zxi/hjvem8WcNqaX5LA3HSnCqTS3MaIFyad50IEukEx0Gua4TM3kvjvVzjU0XLtGspXFxIH0qTyRMvcdHNrbOkaJ9aa/85CAl1W4RAEz27QtoKftWZDHhb/BTsq53Bzgr2VTbHB5UIeXGm2SZaR+JguwQv+FZKVGRtGu0xT5IUHHPSlCXMk5dJnl/qG7sCm7DRDzBJVyB7/eqyRL+IFHv97q+vX13jDAq/vf6r91pgA7FC5+k7qKcUK9fNXY1EYnaeJ9kNRmtFrD87D4tpDKetn6KxG9wYmhvyivZUI2QanWakndSZZoRdY5OODDXksJTdG2OEI9pFUqwigOwMwrFpqm55bAKrWx6b2h3+8VN7Eh4anUn7tT5uK0SsjNYEUrybfAEXR6tRR0K/AvwDlhCxXYT06kuY/Cya+qhGfHTX1HBKU1povPrP1QSvm3aqR6xrTkf392mWojUZzT5Yj04Dcj8IGzX4ZeMSIvl1gniL1Z28WQphoqPqx5tI3OVJ1HCBNuMlMgDVT7hQTcmoIzx5fBz/JVnc0OnzOi2JI+FLkdfLidv8LwjGpye9vtpHjj7AuzT42gqF6igh++anIltMwB9iwxMxJtXwMch/TmIAc21OImLoNs9PpmxueF9ouibP78AEykW3mxIFcEiFN/a6b5BBUHZpXG9KkFcgwT83qsHkTU+yZFSNX8GqycYn+wlYY0f6tAQXG51otNfN3aCjTzvky6ZMlqCCUzrwL0Hqe+mbf0117HnpmsP2ORI466Y9LZnca/wBZrDeq5e3NJbwFcRHb9TdYqtji0+guoBPnRfnosD3bBLmVcLwo072lEZ65EmW0S0PuDn2lJULkRJSINeYPG3cYNuCLGSQLVF0zTtQsPcNfauQlf4prepC8WCiDPC2O8TddFYNJRwna0Bd3wT5mN4TQ844CBng7bcKLn6k9VIYjBlaGJRFFdfBkVo455uGv0lKOKH/XJHQWb7cq2cXbYooVc9wsaJnmyJ5z6hy155dwQWEK7ighdip8i1QGsrIFEZgYTDmGs7Pr6O+q5MO9qUi2dkS6UPxVLFPr9ZW0liA5KUdB6AQejxUUPiaXBB6vLbcoqmkAvMQsKJuazgGDfTwtlIt1E08zjtbm5MY2x12U8OwyfYfhbWih3bVFzqZMoaCYNVxVnJ5rXS5tuAbiCbDdljm6rZ6eoWuUGYHjYGMeREm+sHiSAKlp6a9trupYVB0+4/CutRDuwoUTtGaQmm3GpBcpwsSOOFC5iBY1G31fjNKKsie+7CTTjEC5UYOVTiv+s6SbGkggjeqR3hSNOuJnNpXiBjrFDuIpvDaDa1Nk+DmGubYjp1iZG1T0wzrCzK2qPypo7VzXm24g7gP5/a3juSAct+SrdxOwgZuOxq5JNWO51ZVRxyRAlS5K6vgg7ZkqodBN1AObyHZGOOISMG3Q6Lo+Zft42+haP7egG70QUZHntaXZZqMHsI7NDR6xO4Uo5lkJGNHGPe+jVZHHJtkbGtj026WMNdpw1s/FE95VoBFhOeszqseWTQ1/jNaIHkFe7yOJhCW1j9g+rB0MF0Md+mafJFBpGaB8QMZWgF/QvLkxeJvpaJ0VFVFgqOSF72yIj/1iKglqTQ/k1YV7t91OGiUHtdIDiKtFdJ2Vg3dGqBkHesKNV3rIcL1ygvyFkSQXolRHPRKdVtRtMEL6s2O6bIr9lcxi02wx1batAZJYHewL11ukoN/r8UmOVgPyjotwXdYgxq+i3TVviV4DBzHIAN5AgPNsmr0W2ZTOdBGkgzaQDZZtIssrE0HJ4gNLXD8E6fIAUE2o4l43NSH8fg8qdFF1AwxRAf5p24rig5yU4LkJ6L4RIHe5JJuXOuO8Az0DR//ALP0EZYvntUn132sg+v4Ra+IvXMN+VMHgtIQtxsZIHaQAVAGAbJQQameaJQxhNLB6W5ekYROUSLpZIfo0/jLjYsh5jpQrVf5avG0VxA75lfwd/xmpL+DgEFzWAeadRAjoVi0RRDLXIpz+HQFQVXkZ0XZctP0bpCOfyFxe0c5IAjrgP7ylN3Udjdgx88bc12D+/uLYooUNUeLVZqbr/b++ZcoD4Iwsi1OXrktumLna1FTNFHY0jIIid0pBQs0NguinJ2gTSgsnoLHdNjZdOs/ws52CcpOv3E8IWzP8zwq0lMcI8IymgcyTljIPCmW0JKEJeIhHBYxmscyjtKxC6J4w9wK64KVUrc0PG1jKMEkloYaNuodY6Eh2V6iBDL329sjqzvfFVqRHPOqYMxdjnLo2yG5XqO11t9xH2SBz4kUqEFWPLzR4j1sj6PLcznhv67ZRCCeewVOXnWWZmhgEd5sR3z99DXvhn1JHS35J8HyTsDvtcq1rC5b8lYVxMeQrWo5+2Y1fOIxWj06WxLosDjSRYsqzHrQDtVbcOrQHuSmq8Ab+fGoccNsxpBgcjIp3nWfe6nz5zsRpEaYXJRredp2FXgoBdC9uqgbeuAYQfhFF3sHgfdHEE067lWcF0+/5ISz5oBlG2Kp3YCHcLMMITksLe+l9edIj5SZ9mrtrny7mcpKst3Sxap9lIGJwZvfQZmCfJOu/aRY3aU5CNSIzcgPvKxu6/pnE4GVDXfv61R3CBqnkS8gb+5BgiexvESzbYw6i9Oso2/acLuJdCQWth/pIh6yT3GvTYEKEsq5RiNbosU/WNRWhzhHVZU+5GgVpYSPzhe6M+h3cY6F+yhB7Nt3PHPqTy7wJVjXm3AVuZIX7EzbyeMlJDKTn3GOfYiv82tTf70nSNvzXmeiqpOqUcyh2+01m98ti0PQBGyrqhxr1vVjnpxYtK12rFrU8hmsr4uVXsW+g2VxOI2YruozbKZ+jLMmvrH4quFBHxxdoHdoojlab+BzjT6t1uOnx8epeX5v0jL8sjq1XvK6LLIYcSEznV7xS8hBvLrKVG9ByvOcz3ik/iDbOq4j1DtbLYbKIyZ5HrVuAWmNW6XkNdJAW1/lWHBDEsPzEHEzOewiFrvIUZYCg+0UQVLP5OIeLfVLqKbUpXmNrS4RtAduV7eloFt4HM0OLSJDdGSRFeX17w0oYXX1cDf6oFrf0uKfaCXQwf3RY0fb66YTNHReIUxoI4dJhCRKATokh4mIhhhq7ec0/4kDfefMYeeqbW2PnmWpd9opa1470k0Ky84V7r39DDgOe41OssWJ/4xxy6IPisZM5+m6DvUyRL0YYAw9xwOVrvqBdW9Z0M26lkMIK1cBFtN3umlCYsMLhZoe6ux0+0ghzIg+N+fxl6r95yAv1G1NFKvAtfqt1J3ijNTodZWd5hg+WEG6hgnilTiDMByrFpX2Tm2kQ8bPxUMxHGrO5oDkx16vsuNi8TLFW6tFXqP10p9HXcD6qSh/ThEcgATTC1nCJ01Zwjx5CfesE5ynz8kS5A+QPBbqi1qzfygbkQfc4RHeamtR4XUmYDF+1VjDfYMUZ8Y8Mg5cMSQGSj8WFjRoI910y+NhN/y0dlG+vOmRHHZUdVuGHfUvo+xt/cQYHi75yyjZJOyzhf/Vdzv5XGAE4WS1MVjOinKFJp26mzZWe5/TVYpY7KZotdXgYBu8wKrlNjmAtujaMz9GIttO8wWa2egalpt58wWQm7mBVk6H5SCa1W215A/VsFos8xhOHn4Zex/dEq4gcfvdZT4mN6lfvWHQHLhR3RYh1Og2RiQ+tfDza1p5H8dMHZP7j0Hy8zwnUZrXzR11hOvtr1ZgPCyJqBM8mgs8VoadOTJRK1hPmvXEBCtYqMYKMb3RqsYkvmkDqPU4ovit6esmN3CF1Omg4CIZuoMgUbc1j3b2PYVPqKvL6Vv2tdjs3UdoGh6K8iUCL/OoDnx84OPJ+LgT7hHYmMN04OIDF0/GxWQq0iQobLlYrYr8DY/pwMbqtuK4j9CIPpZFs56eLVHLM6xCg2tgpCjsMV0FR0l2BZMCv1rt7rHFoQMI3ZsByWHFjb3i5nTYamPgnIN8zxebQ7Dw28PGsLWBR5Vha3IIwdRWgAVZ2N/KkFVYvBnqHxbgPi/A66zRneOO1Gqs/AmfQf7QgIfRdrL7+zRL/eygoW61+XlYSuq2jhaLElbBbzp8gBmMcMfDkz3V70G3o5NvIz173A5Q1A7CF4qbhwAR5pmFoEL8178EFxANxWA6sL66LYPkt73T5Sr5X6oarhAdnoiSMHXzV5h0Y8dFjedi6Jj7Q7qCeeV3ECqslAHZYbEcFss+LZazomxWgWEDBEf1RkB1WCsarqXo9LFJh5bb3zMd+JMZDE5oU6zTZBdDDwT+lQYdqKEE9U8DGphOKn1EtBgeJPddsCyew2rVrNYY0T2Yh2Kt1JsiFibEn/+EifaVwHEC/3Hiq+m96zgHFgi2Qc+rzpg9fjlq6mVRRkR4hTSHdUqlXPDFOZIMpdlYkSiAFiu3LDydD0AJJrn2r4Z1jdnaLB2b3tPQyr5vgEw9pyCDtgDiBEGfPhc+wfeo1gN+m5vCcpD+6rYoMuEmtyY80/i+60gydCTBwpFZca4zwNwK8PThjhJMcsKjho36kh/dkEyoyMq1vY0uSloyxhAnGP4gUtRtzey0MHhq3o/TqlcOA4c4z9UapA8+Doxud67e9DgOrOvNO2NFxRjNlJEaxnkcdm2LjXlZ18HtThZRQHzqsAw5VIfV6L0aR5Lkx0lyiqYs6441oe4asm0XQtd/pIQykdb7NEEW7SQkCX4PJDxRYdQr/dcwX+C7PyDLXkJxHdU1SJbY2Hl3ljpnZNvUfh9U+0/G2tay8gI+VZ8hXuqBRy+DyJRjPEhOdVtyigWfxZAlqROHfxkllUkceRJV7RgzkPlkCZOfRROU/rx90uaNgOqwaDSLZpZIyJ18I+N6ma7XGL67bhUL73l1A55PnyFFDV9UCFF/pTHaO1fb8SJILH1L7enjpYbmhQo57K1Ehm2cgbZ1hJcorCsGvT4hbyWiED6k5jdt1TzFRn7xfSTRfshnP5IEtJdc08ss/lDDWdh5yawrfIEN8/UleMEs8CmtcIinv9BSIDxILXVbPMlChRaZiS0NNuPHKl1nCh66FStv1pltHWGdWVeMus7iLbDDytIw8UuSQfaZZc/VgBFdwjItFuFRajXICL4qDNF1jaYdL8kYfolYztdI95nO87ROQbbNkozuopUUu2VrqEUXA2iUVyy0a3CGcv+fWizzJpuzPPcSyyQRcfov8hYRIYtXpAfBQYDfCAgP4lndVpzryH6XaiV4SFLq+fJ5DO3P1fRUuT2UwqifSan0EVbW7QZ8I2/UUIIg1YAGqnoZWQ19SkPvlFQsooMgUbd12ecIfRcmAgY87+dx0tq7GsuiqtCyyMK5jEd14LNt5TMHrSYkAP4Q+G7YKx8+w0eYBd8xWhZlPVxXmzqW7gyBx2vcYMyth0QRIwdEKW8RGJYjooL++a84hEBMDssSllO0NeFTHaYbfvD3pn2qKOxiB3nT6xOo9BkgfUjnd3VBdWVBe1Uh3hWFa9g9auORKzK/Tx+asrVYOzwHka9ua5540j1MtUjFVvi8SAKe31AYDhzrzbG7miiXpFHw4Rxcr2r/ObCNZu2TG1iRLrOYPPxJ1izgeX6doibB4G/zjwC7BFWFb46hgjpKoEGPcHRa3KS19jW8OK1MEsT/Bdagu8GnU/fjNfYBUqHiE7Q3zVxFvndwBXMkko+qH+miPWQJwtZi+UeRj0+HtqkfJVh3+r/Qd5v6yNR+YuYtbOTHBd7gIsiXMi1KIpWDzpPxwG6ALiuqz1yo9+RmRe3I7WWb4ToIZldkTGPXersiz6uzDDxUQ4v+lzn0LUXb1RFN0ORmL4iG9KywU/AFru5g2cvVrLh7/Ypo67+9/kWYLgYW35YhXgBkEMOh0jt9pfbVgw5WcrumnRD641FVFUlKCNPP0/O6KHG8+H2KEzonS7hoMNtUP7mhneaLV62pqq6ysWrbmcPWMAONuA/ROl0j6iIW+u31/yUM0KqZ4Ux40wzTc64VkYwdqfHxOJIfFZr1NK9FNS3Nk3QNMusOcRgsVT08WUNbfMkHuMYyOq+tiW/Tia4e5mVUU96loWVOLzXR7te3FJfZMB9qKCvaM/ueTfQ8IVZQM94G1p33JO1IOE/L4O/46fj1a96GwrzCcTU4gPYEVAlYiOIcLeBFNK5VD2UynlXPmk0XuiqzserfG9jABbfX3J7pmFVdRcauUmgXntU0J+HaD8VTnhU4uRfbwi9v3szPs+ahTMC15umz6cTZljDtLX372YZlmQoGhvVkU7YJmWil+7x1ktU0kml5VDpfNl3gr8XPzanUYqNZzE3s0YSZTtjSrUrYWbNitoqb9SOaTfRK5tSmL1S12di7i+u4TMnTrrfdv0qeloPLGJmFdOFgRRsStlUgn59l9UOYgE/182Sl1rZVtoYxu6gxW6bhn1wegzG5Z5rFNrafMdkhzMCY7DxZ2lv96+qzMGZ/mGuUlTygjBk3Z8v2bCjgtZeMxIaK4nJSdWICFlLRdRek2oe0atXgozWaFHxc0o0mhZWSkXSVZEzVw7swlbYNCYNZMm4QabqV7kKYvsp4ZBlasBf7EWjS/+iyj2Ct04IssloulIm6X9n2T+YSEuHMk+gl1qxIN4GMsyKRTT/6+nPrcbeIb9JHWL6QAEyDhsUAa3Q4D+WNRS3jNbqfo22fut5Mp4VJ6WzFVVS9uTlrUAeGLcnAAkIFDYf56GfKZhy20a2xFJSjmI5LlfNl0wU6MdZWcKmlIcvDj8yjO2zLqsYwA4PuoDWLc7veA2IUlUaLVgYsY0wazoUrpfjnsG51HZmAr3R03gUrt2NqZhgMTxjEkrKiRgr68pyxSQn/6draGrloHNF0AtI4nzZdoettFWdb7uiyOhPx8w7v7rpxzMTAO7jLkyS2af7wBdbLYnF7BRG2FF84OMG+Aa3z0VxVxsRsLRf+tWhPZkeRwojeyOsaKXVo2h4Ro8DbHr2y0zJoKV1oQCeySFuwp8T8C1o7ggmWsnaOrEzJtspsi7i7km5kRg5OxobD7XZ7BuSx2rNePLVc0YcJmEdB011iG1b0mKaZXS1xWYjFLQuZ10nJ+Owk7c+ETCWltU37TMX5DsT75ACbZ4RUHCCCSg/FhxQFDofiImYJY9kxrc/Yj9MMX1roGzB2k4WPTgUOvT0pxNXlQY3u5tsm14Spv3wFHT2Ga3XuZBGakW1iCrLPr0CZRjGBxDLNl9V+SGW8mUVetakD20uHyLwgPd9k5lPxkLaWjF9lFVyYVt+ghHM3yQW3jXOthjIB+1rNoU0/6ASes/DwcVY8XBZVbeZbAVLGqz2QC3+KiHeKJ5Xdn4APlXOyM7zXPVp+OzCOjkt4YBUHDi+huzGhgF7ChyoG3w4+VI1gIlZUzY9N832duT3yV/AxhU+fYLa+b7Ic67xMgck5b6iu8dMLNT2c9abW1X77bnRbx9VuA5vOkW85zw4+/bbibOyPn788r+HKrAgIkDKm7oFcmFhEvFOKgLL7E3Clck52QhHAve/3joFxdFzCA6s40EMRkKKX8KGKwbeDD1UjmIgVVfNj03xfZz5FoMgysxRkoKTbepE53RpnEe6U5JN2fYq9WDYHOyHxcM+P8uoJRwEUQkJFlikoOBWftSCu3EYjlmmGEgbeDl6TdHwibpPMhZWKh+Bn5bXvRU1cVUlRLm4pjtGxh6KOigdpcFdOVDWl4Eo5u28HbxqGMhGfGubOlmfbWvNxLixXaYUz2XUj6V3+eKDqszF9NSn/cjWc+FffmuZkCAOMcIJoRyQ5ePTzw5mIcfqMs/+C7Kipl5gX20yJ3ARpcrBZ1JYnZFNXdEvOZtOBnTp0dBrSBFLSaY5t+jP7ceRZUTYr4nY1MrgIKuPmAcqFdSWonfg0SkCOuhMTcJaauLvDRiRdvCUfsbBKRuoS2DtyEod8JlaS92IqXpITeHeY6Zb8v31aUjvZFKCSjZw5iEYqYR+qb1u3Z6r6PxXjSebDpulNrW0QYi3XWAiZdshjiK8Ws4r5tpTvJF2fVuAx82HNd1ugflH8YtaSqAHHV8Eo5Cruk/L1lrCgYgyT6nDi/Ng0TyrMxorte+iWkcQyYBkrdk8b2bOhFLF9AHEk/U3XiwkYSUddm+bZmjNzlNEeYMEictFu+jzkfZ+M63bSYujzTH3DT598SlFvypcheZUxU5m0li5TGV3BJ5+bvEFN6rHt41KroUzAtFZzaNOP2VOVSUfSSj4nfmqX8VTc27YmYV2pzN5SvmUGMRfTMvNm0wlSYd7NnYTp6HmUg1Nu765xfzze3WFBRc+n2uHFudglZjOlOxEgR2C4XUxoouz9pGy3g8lLPqb39QnAARGoy0tQwcWPtF5uOEjFLoZ6Mrbsq7hwpakZlViUcX+8u+2WvZqA9yynwYoTpRhmZ0xGiRhYyMQv0lo6pvTVGvUNSthTtQrml6FWQ5mQp7VzaNOPvs528fA3eom5MTJTdTJuZlvdHUXUfjBzMbV0Pm06w1ScV229KGpoYyNt4JQqKwZxVlkpvLvDmoqeT6WsinOx/TbSFXxCy+eyQAiqfv0Yfe+6SjI2lMC7MKS2uZ3y0tuMZAJutZm/nfDgywZipwgYa1qLPan949KQ34IJSnqEE1aSW3N6IrFgqpyZrtceOay7s73IOz7BepXPw/ZvLn2/W5u5ZxUTXzDQOqZz9cXJG5Dl8lNw9fawoHQIE3KidI5s2h8QzBte0GfstYtY4aAjBhvwmEOS3gVpzaqeTKU7q2hsy1PrLYhcuYJ1U+b41StoczNCDi5XByhIN81Z2sSO6cy6MUyiLevmaSf05E2nLeWeqkL0S3sxBaCTZlKQZk9A2e5lx02+yKD2bEZTR66lsOBumoq6KfX5ITWG0TYLi55NooYYp8KmF5taM6rI3EiM24ayxuhMuJv7h3EYc/DrTu4iwihMYRWqCqNz6i7GWpgGMQeb7mDkBZsbz8ShUmhj5kTvdIk7yZjaEUzAldo52iGWvF7DJL3vbv8f1XWZ3jU1tGVRfW0Ny8orerCwoQc7yNJ2I5qOxe3meBdYXj6Sr2v8562CI5W7uzsuqYKhQeOkbHh0R+ZjtlqW8y+VgOFOobH484aVZ1GKYUs3Ei2D+8l2PXln23K03bJea/KlP/+KCx/77DuWDd/4r78Wz9yrsN+Wb8CDJj+cCKtZNx4rhMGsVsFQcbyMZx3O76BMQV4P03JSrO7SnADeohWTPsLy5SZdafLauuHREE6DwoOm1h2S3b2kBz6aF9azo9PJBdc5tekZXW/uxa8bn6VBZ4FiKzh+h+07h2Ftx9LYQUvPYlR/b1AJ4oxveRq0Kmg8W7E0mA5J1gcz8Dk3A1lHt4PjZXNq0zO63rbxvu8O4CH2IzD0Pgn47ZHqeyTK+6GYPXW2COZhcK07ThjjrrH8HN431/l2WARb42zjB/cdZM0wQoFbHVlTi8xhlRA8EZaKvj/qZWNatVu7eqwGPN9KsuIPh1XFo5h7cTmZBrb6v48Xbas0+ZnV9b3QyW+vwWqdwQ/FU54VQJ3ZQA4embs45DK3Yd/P0XlL3pcJuUtOaavTAKbmbBx2k8ISjTqBzIsjSg6Tg8s4bIB04TEFesfnTwifzb1H64cyAYvqp8qmA3S9LWBQkyNCgByBLXfRvaDs/aRMuLsuBCocv/1p6RFT1dPsx+pLDxY7s7K9XWJWy7FMt8WbZtGmJ0zFLWJnd0aejoV9mDembqnuyiy8tw/ic7DUz9KsxtndzdfUHHBY8SRXPYhFNV3xuda2hbLXPMI51oKZBexl8rzX5eqixIpRugLly+lzsgT5A7wCNTxpStRE8qKOIjbVlMYM40pOwcHGVqSGWdf3caSzdZ8m4EzrWbCT1ko028Gg5A83zmSqxGdJFv3MvCjtzNRMKCW4A/cx9Wdju2OQ/DzPUV+Sn9fNXZWUaRtra7xNbKooY0FFHRemNDa7U1eMbUczAW/bzqeLM2vruNpkhBnqTczTu+hTsBzLjAy9g9bdUZJJ3m1XcrEcXMa8A6QLuyrQOx4czM+q+nFMwKH6edqZU4Oj+/s0Q1/grSk5kAAp5ckeyIknBcyTP+Kn7MIUnKQirE3bc2c/I69Xuimf6irKB0p9N2dNUzM99D23pmgm/k7oiGi8j2jFfEGsDx7g7VlZrIx8p6kjd4nS4G4OUHVD07OdRW8mcU4aiW/TC7retjDfTeHKepsaozIe1czsbCf2ZXqmE8lu04dNrRlNiDp9RJPzuXi4pX7jydTYEco6cmNiAHEzJ9StyJQ4rvNbaFYYxzOJbWGcOyvdkK26FexrFJUy4JEYdjfdjroRTMybO6k6nixh8rNo+HB74bM6S64lAmnWXGldpxy6tq3LmFoY49Zxt+vwJuB41/m2WgV85RnTlydNiUjxcAleSFr/8zzFeE0PqWhqyZOZsxXc8pnrGrN/SSSKXmvVmQm40moGrPpB1dsaLuzfzBHYxpZHVAhseNPrsSrL5iXcaloa8wtl19HNwP6m+bbpEl93ttXwuUhAlv4LLvo+3n4G+UODvQEqBlRXkXG8AO3C65qmJNw99HzruNo8jgn42DxvNp3o68zIsRamncmkczTlfE24rbj1NJP1tiNW2ym5UYrq1KgGLPsouxUo6693/4RJjYvgM5rvhHQM5HlREyx/+1bBk6zEbFD99rouG1FkYtTXsO6Qnj6vi7JGI8gKIvSr169aEIqTeBgJU8qQIjLepxlUY+wAjOiukyVcNBm8AdVPGTa63IiMvq8rQ8ZeejYigw1cnK5AmiFTBiRLFQmlgEb0/a1PGcbN5Vn7Php6Zp5X0v8kKRoFo1DlRmSdlvEF1stCOkIGwIzOEP4tbcEY9u/aqFUrtmg1yMysk1bKaerLjEhOQA0fijKVruGu8MU8mjSpm1KKoyuyJYgOEwNhTR78wFVaQtWylYCZUVMZG6U4mWSZdgPXTgQLYzEfLfwXkDf3gJBLtzhoMCNqE04nZP2mK2W+QbkxIOkCRKSEG2JqTB3BLKBaBaTMTPT+yS+1uGMhzAjRho+vCj4i1UfaMwbAmthEs5AvBg7EiPI4Kx4ui0qKqy8zIulVexmSjW1lQoKtDHhdY+P2ClZE75MilMAZkV/Ap0ol+vsy85ossuwor54US3EotUKkQmHNBFhZ1nFAGxNp6gksV2lVIaW0DRSU9oqDsdAs0bacg+yoqZdYs26TZKsb0MEbGyPBQioOHgrt0NwU6zRR4iGlNoiACoNdLz6WRbNW9oKUGhERT6kMR+d2ttx5v+GYhNZtJZesEkCzmP2Y3qONsFwYsUsA7bHrENrRTyUwhkI7NBdFLR3dUGhEIz66LqeX9HF2iz1PpVltXpu2RKKiGPsutnG01Kum8mEyj84a+8Y+HSbvH//Em516Jn8FQKOoqd6fMI1B89qAdDzaZyW8GrNvxpZ45HkCJaXIuwB2iDRZfDUNaJMx+zXs0JptEza4nZFKk/k5dL4LCDA0NqSLkWGmUvcYdehCbk12902N6hK5AKkwDIY7pTad+AKIkFD2pSs3E2aJjFRC0zu5CscAmFV4+T0kqUavukvmYDLewNU6A3KGlMHZOjBedIh5GGsZo8HJgZgVC5hDxLHaFcnDmE3f/oqO1Pbd3J4yoPlWatAMhebe9Nc8pL3Z3JsxsQsEFWKBHzB9WEp3cAbAFt2HFKkQlYKzeRg7Vdu0XiQXO4x8R4fzytmODZ02MsgQyyZnESrE0B4VOXvQo2uDX41rd7UG6YOUeH2ZxfwSUmgFCwtiZe9/hjWyK01TLIc0D5wPjpJSQIxlc0Wr3JpVUXpG5VoWk6AyJxQxJ45t2OC2cw9Rh9DyLkuCBoydxQy10HjzWQjzFJZFVaGKmQYlD2Mxeunqt1n16Fet0lraIrPCAp61zu1NuZneSh+L3L1CHb4qji5v2ZNFqoLsHJOF5k+JNwf36kpDVMAwMvEsVTh+tkLcxwdQiLlTVf6cn6WONeU2p8S3w/Guim4SWNPgxCpqmjGH2kaySTBLiKafDQ+qSQ+Gb88UdNNAq8enriSjnepIW0NATQMSElKH2jGpd8sePmtpx8JaDYypYqCbNbVYpDJ2Yw/cR+I2JgTAlunoSq6sQZNgTBak25FQVzdlHsRlj6Bvh6NokaIKSPXw5BVktBNOyjVEU2CVUEqFLgKVei3FTCVZ/hvteLjEN5GoxCW2EbHGoFKv8+i4SIBRj4EHldGEVsM05BBQjcsu/SnQ7dF6naVwcVPQ/RSJooVXj0pXTUYsKrxGQystVgnd1FMQhXIb/rSh2wDtMr6+UkyaDTjHXXJDw/LYIA3JpBUsRiirF4FwUrQyRUw6zlgCnn0NWS3eNa8my8Sw/HFkC44wI5ORiI3likWbQYZuTFslfURY47CEKho6WUp9JeaxpRjfsFlfEEDtR2XWGDzJNY3SQB+R6BQHKZx6MDJwGYm44D8NeaQYJ1E7mZbZaEQlQ6nrGBlAWVXDYvZkNDYiIakWeyS6mteoFNxtoOa1GkbIaZYsG/R5ewWrukRMDxdUzKlIRYta6sGaK8toKsSvashp0YJs29iMOJysdOzr7RCjK6GkFFAzNBm8lF5ceK6OXFKcI1Ooz7WnoQ0Poh4BBymjBxWHrSEFj2giInCh0mpSsIDmcbBTG0wWFp3Mp6/lOh8/RH9ZjuqoxBMhQmkcCAKw1BuxCf7XOSNEXBKqKIkcQpDjNMuQjBvuEGiowoFaDIetEYE+HMKJiNTdFNhcjdVQSYA1j4qvoqPT5l6DBbkExDI5pJoAD3rJ7h1s7rxLiKavoB6gtp6MfIqbExoa6puQEJK64RFMyP5aiZZ4IpB6NAKsjEjURRcNYURUExDjpFiRk5DNfRs5PQQ4/Th48OCVJ0UqoY+S1P6myxV8TOHTJ5it75ssx9KRKdBYMaaaRoPDgCCYqpbNaKSbqhkPcvd3obRrUwRSj06AldGLup2lIZSIauS1iRvs+X1zSUxODwFOPw4ePJiLpEgl9FGS2mdtFlmmZRQWQLMEaDips6C9MKdbRQyKkRljc9Xvtr2yJx86DaLvOQWpGv5w8dBABBqVzC0io6QnCb4XNdEfyAsD9PVHOTlU4PrxKGqFi10NbgXdFFPgQz3uSuUtd6lTQkB9Dc04tRWl3CbeCdXRUY9fs211Y41n7RmJqIC0MM2MRHM09SYnku6Ordb0s6uoHrJVfXl8oPYWsTZW0KbJkc3F4eqxlroSKPW4RGAZ3egL0RoiSZBNQRES5WsmCQdmGAYLrSRKH3psogqHbgqy3NJ3uxU0oWEMI6BAFdQAZjLQSCQ0YO6qx2SO7qq8ljNaGKt5bMcShydaXHJiCBQNEhp0xgGd2KDgbNY6NZYYooNCp+IQOYE9aNMmjTV7dKVw6sHIwGW06TMqaOgiRTWyF7dtUydPOQhT93VS1JoGU8lOWfqJ203mLHU4mLyCeljaerpwMD7HhUVsmLwNTWzYaJTskoVYklGSjds8PjYZ9wgEZBNwG5nZdwkSf4uSXjyIYf1sIJWr0Owu4zFNQwJNWIkIZNN5TQiJMyGmCRfpk8zcXqIeL0EFFz/SekkljRFJY6qiHpyhpoxsVIIcDdVMiFXsFMu5KMv9c7vJ36OmobyCeaDSejr6OUgmfRsSUirnKBYlv9ETak1OtpbjeJnKYxKWbWh0uYdTOBlEPwVikFMbSKXE6/JJmSQehWlMEkhyTmnVUS28eki6ajJKKVJmaYimbWFkPVbWtnGFmiu5DdZz2Tji9Z0rz7hNciCmJCEHoR4XC6iKvTRfgeTwjLkw6Zxrt5tMbmoisIDmMTDwOpKYVTQ5SlnsnIrK3qZzHwdr9CjwgCYbmIMPsqd5XCM7FZgEewZpLoXUCQhZBblUYNMAaqWOFOnIQnvTeTMLKWEtzoosGMnx9GliduKzK97SWdZl8kgNrhMhylpy2SRkhdTKJzVytTHJZJyPT0XdulQDOwxStzoD6TfVGhUa1vgnlLAO49J4KwIpNo3vgo05M98R4gA1YQIy+Lixc9OSSJ771IJkhorGwerra65aKfO6mglsaHPk61eaZLa3qhS0kuXtgUazPt2xSYWCPrGvTkB4dECmUNsxRfR1os9Q7Lp4tNh8uVtP4QmXmbYj1nOqYKqA5AF9AlWSx1k5ZQyYkQQ0dHDmABqXWka1/Y9FFU1aadtsC9YojBSwxaShtD65tnkSrLswXWYHXZfM+7dN7SCqmHfyMedkUiVK1xH2jTev6WBQBFGFxjTTxDBdkCZKY97MG2uKPFaI/7LwWAtedJ6X6/vWteqrdV3vwWsV1TGorVVMeZgR6U8yJ98qCh3mQo/HmUxadA5z1KeQdp8ofQ/Uk2bkkgCl03JbcJT9tgLeTu+cSVTfXoPVOoObR0fV1OEgzUNiKwRTiEM3cgba4bER5t6FjD4KSPWA5BVk9KGfQ9FQSIFwgisim5Y1W7wIZDMUzTbuTJZJt2rKz9/+NGs/yirGVaGqqVltmiMGpxamJqYTGX0IOCbpZiIa9+6x4RTNpbrD4NVYrEgtvvfsQnlN45OdwuGHnhAHpCtQvpw+J0uQP8ArgEy14SkpievZWEnj5zXVlSe2KkwZk814pVvO5jGtuKQkf1jTkIW2HCRTKQbVWIRjk0vxdpf2GNhYRz1SU1VpjhXlS2S6rCKmhkY+Ila1r9miTFXcB6vZrqISdZqta3i3zKhvKyDVA5NXkGZto15W06VtkyOcQN8e3lO71QQOiUCaofCwUrJQT7zpyCLgGjlISHhqzXxJ2FWkqSspL0ParzgN8pEFGPug3O1ZWax0pNOB63QxZS254sc9g6dV89SopyXdTeFAOArYemybOpGJRiEemWTUC4G3wmuBMgGvBtcJZWUtuain30jUCns1Xpl8Ex5MjEo+HbNJ4ewGpmMxL0pNFlArfdLwVvKKoiTA1rauesi2KKRRa6p3G3XhuLbtycgueX4yQtg3+ybj7Xme1inINDdQdBXUI9fWk8eCCy9KasPBdejHvQwlfTHzVnzt0kxMZV37gatQ2JDY9tKUZYsSqhsn1TdJK/3+pzlDqxxaPWR1JWVuVv7ZUg1BNdhHTmpn2pEsdyLTDmTcecbYcX592yLAQcUgzWE5lP36Fj+xuQLdh1/fIpAErusGZF+KBcyqvqB//H1Ts/vy6noNEizO/8f161fPqyyvfnu9rOv1396+rQjq6s0qTcqiKu7rN0mxegsWxdv3v/zy72/fvXu7anG8TRi77leut0NLaG2h2eZKcbr1BTxLy6r+AGpwBypE8ZPFSgC7XoGy/nr3T5jUJLr6mZMlvw5U7huUPxoqs0FxRawj9TXx787Nh1slrrI3uHvCs6I8sg1lz9BgMQQZN6RYwIwCIblGCwmU/TLqhnK+QKQpsmaVb/7meVFd+7J9UpRHQn22x4X/z6Jpv9hjOM3BXQa5vgwf7fGcV5fNXYYTDTGUGb7aY9rMCJ5+nkxiqT3mbxXO6MZTbPPVYQ5BVT0VJT+Fw1eHPpUZ1x38wb4+fnv1+qWq4eoS1EsWFV/mwBXkec2NY4ZhDq7MEet1Q2SHBOdQ4trP9tVUnlP4MhHrr285GcFLpLeCSOL2Cl7WOUjCy/5l4XAxeCl/UtlaBirrjyMAw4XWWZHh3AQCHvq7k0B+TFG9dqmIWGXl0wvY7hFvNJv4GW9+BsRSJxFSw5I84c5Jj/6zEy3xGk7x/QuOhsN395lBisZ9+oA5Xj4zdLk99q/39xXkBFH/zR7L53SVcki6T/Y4jkGdLK+Rws7ioT470AwxKl7lHKWGr/aYTgocXlX3jy3LdwMlkEM7JQQ1/Ee6PiqTZfrI9VwsdcCcQZDjZJgMwv7j1mwI3eKFeOmG7Qc0Jo/tQF99W3eDoywFHFd2nxykK0yKfMFhGT7a42k92DSS9ssM+0VdrL/mp2VZlNyo6AKHeUKW3lWTf6s5FZ/+7iA1QYX4FjGvgI8tccOI7H8pvv67Y/+aJEHCTN5Dqsyxj+KMUJ9dDK5P6WIBuX1289Vpl33AUhvtEQlxK3JbLFvojrc7XpLjHQq3RhqzcdUh0lgXPW4hjfXVt1Uaf4DD4TVvuVMFDvjSap2Bl/axKQ4jW+SMszuSkKDsSlxW4wd4D5qs5pfj8HmL+Bs2vbpW1yBZhnvjpCi9ON4KzzisTzXOI+KKnDZh7GvFHCouCKHQxf/E+3pcPTxnEhfkmbP/UTTJXI2wcIHzJV1BUe3afN2ahbe5jBKy1lTXbSyWl7rqOCuqb+9jk/JeXKbEyYPbVxUdp1yZez8FlIH4jtMc4Ad3ZSj7MhdznOSyEVmdKXCTALnoQhu+Opgsz6j9SpBv1GeXjfQCv8THbqLkkwuOmxKglgVllilw4Lr1AtRw8VU0ftiSrZE01H4VbWMP286nO1xLixK1zxsb/VeH5VEWK25pkC9uGMTtbfPVwatQcD6Fwq222Iv+mz2WK7jOXviODB+d8YhdYgocxOIJJw1PnDyvCbeiyQcHBU92muVxkHVcLDiebb+4elFlQootcfGJoU2lDd5hvWLDZzdckq5Rn6c69NP0cPEF5A3Ishehk1TJ1gh5eqiBp4gUJp9DRG31ceR8u6mIU+/hBRAlEVNgj+8TeXWZRtR+cdi9ipJ3v5Evux3i0Dqvryt+vjaf3QyA1qOCpNoCcWMKskoYrAxka9ZtF6j4BdbLItAWZFB5rFxD/ZFUNLpR5Zm7CsjFKkmyZgEX9I2rc/6IRwnk0Q6Wf+WLugmq3B17n225pYiyEQmYi4VJekhwYbuti+/jp1sD5uK1ydNVsyKO1qNV+9IW678Ryx2wg2c9dkm5w4EjqXEFq7pMSUyDglA6OAczoskypVNdKNweMWe8tR8k+QzYfYShM8px5OOmAzwetsSBX/uBCAxKF3jgI3dNlEiHUqco0hJewwxZT/wROFe0vXwembFjcPKEoc24YUloc//ZdRkoMAqFLsc87akqf8LTf3WyHxJxa9l8dcfUHbILh+Wy8imPWK6XJFJesQeJpa6xh/QFNDEEUZcnUI35U7qAN8tmdZcLliJXZI/ze1qld/wp9PDRYZ3goPhqyYs46vOcZ9nxPUk750C/7NNYRBDl/gJ8YrEtu27BFblYeSUaGXnZm1xDk+4LChjnBXmeL9LHdCHx3UkB9kR0BtoFSu1usUqRpbJaCcdnbIk7f8EVkkeiFiopdqLv09f8E7LdL4Vtky9zceW1ddTCVQ7hYKHCGvwHfHlqU8swtilT4oZRyQ9CoRvem7Tmtz3qs0uoblY8Db4W8uCKELkrg3BoYb0ui0e4uAI1krDXDXeGJyl2kAZFbUAvh3Dv/U1Rg0xOHymE1xjUjSiBnA/GboqjhNO+2BLHixa4Q32GNeHKBV3o0NOfDddB/MHF15Q39yCpmxKWaEerL5BiyUsLFYx9Kx8RN7E42y8utvXm4WPWsN58d+hPV0emNPBlLofDvzdpCb/WS5yotU8lxp4SyyCcW9ioG3L8TLnD+m2Q3EILP8GKxtFiwWHj17IR2ikAdcjYzEWgDt/dI5iE27fUdwf1P8/a5UmlY2aMAEm5k6+3XU5f7xX45RDu1Dh9XqclidX8AF4qOWV4GPdWSNIcgkF6/1kJ5aDdgIpP8s2oNmKxgzRlagrXFIVSp17jc8yjhxJCUTcVS90OTYeKNzijgXB6yhW7rMshQwm/MKkCF/nVVTp5STL4GeYPfCSwHMK3hUtYpoUwjyoYj1aIgkHQCJJYBuFCeXwYJr1OxRW54DxDfNAfsvFI2TIXi2uR4pUMsr72yRKnLOGtLxWUU2QkeD59RpaWGBm5KXDBd1oJtCWfHMLTwPMJMvseCnw2ykWpsUVOOhmyzc7zR7RiUeX+NFRQyuRATvH9yU+5R5krcnbokfpHjyDNwF2aCejVUH4tyQchh3A65dVgF0sdrIHiqR17l2KOn1xZuZOVlN6/EH/HWVH2/TuGyDYVLCU1oMNhB0h+FtiFgNMFCYcdfKGjva3O/SsY3vZpgtVtEldId3wvn3c5hGsL7RG+rgUewpFucNFVFUIgJcVOqw7vO8fNy3FT18ItOaHUGfOPtFpmaVVr0PMgDpRpZU0GEbtfIjtM9IzJIRyOH5A1RKqmCX/RgSlxCgrnUTnj+JotJGg2X529oSdCtCBT4LAH4Yc6QSbpHVvih5Fcxf6ANmH8fKIYv6uF9GvxNF9YtcfDOXBY5148zXEciozRZADOsU0MFihjHy2gc7STRXs6QCdLjH5whzPDdG/xqHGi/RpW0jcLJeMwAjuNpT9dr47W6yzlbQUpgD3+HzB9WHILvf/mQB2Jnedu2f1IFzyS7pMDvSTj+eQ8nmGP0IsXDZhHWzrBogSa8yA+apgAJFl6uI71H3czOEAz1uHFXzF/IFvmHjiE0zGIF8TZMhfNv4LddiZxVIilHpi/QFA1JWzfOpRiZyA8WpCF2AqFHnjxDy1uGsA1tIwcXorePqHQOWQNljjmTu4kUoB4toFpoG9igPAeRdG6mgB/M0UDtjVhPMPGHXbjvo/ocw/kUVcdJ5IH/z8sXqXvsfTsgStz8rCrYhu5om0Jmuxry8QPX+biL+z0GNGyYUrsMXb6ioCP/u58OlqdFM0aX1dYCHd0xXIH3UGB1Q9bPwvk6J8o/yp+ZSEcIw7aMzakLcgiDujCrZF6ncs8Dcwt23vePcSeuuq2ij0cLydi2Xx1UT7j5r86LmpkrCuxSopd3JzhmRP7uVZF98nKD2FyelxtYKzqVEwsdcCckiAkAeXms0svH6CYtHbz1TMssLop2ms+cvwmWPf+f5UdhwiFju7mK/wSKo+TLpg9xHVkx9fuBABuo5tlv291qKlH7u1/T+HTF0FLFAq3RuHqpGfgbZEWic9tEVXNcZStrjlZcjOuyOXExDWHn1LswEKe2IwpcIlwOeQjE8v9rlPFWScMLv/LVVOvmohXY2Mqcf4bzcw+vc4VEZ68VoIwwNOnxTIOa/VNq9wfftiogVw1/TtCoolsAe4UBiD3uDEFDrZzmuEnUYbMK5zpLJS66ORdCKYctaTYxQnXJgzB2iybl4Z3xqnhfFq7VF6JkEF4tcCmPlK1wkH5tNRPgNS204D5zRIRBrrpGQAcHLvPdQnE/MrU5+0RydSZaqAspjD5CGFt9W31PaK6RfkJPpN8KbxnnCly3ta3JGv+vGpnJH84i83HLW7EsP2qp8on6eeNPK/OIMA6a7+zCDcq+OL951b6qmgUhqUR+jOtHsv2My7df8lFEKbswMBeDByPcwNZdg5eDVcDYh8c0lRQHc6pYA4HdHpch2O03TtGOxwf7cvx0VxxNd1q6d5biaKctZlV/NUyVf3tV8gkothZDLejF2N46e8O2EjGHg5T983lgkS2vm+y/4IVubfO35TgCp3xXhRqtENZ/CVj4O7zCv/+ev/fhCAyds38d59gMhbFVLzeNytYutR3B0Nh3T2NzHVp89nF6OiTM/HWxub7H1OwO8rg46x46NPZufOltvZIPNk2Jwo8psDhfAQN4bKoxPS61PfZZ4nZ6TpJl+MlE7hpalGNun+27St20U2hPd4foOpGw6Kkv88+jxfwqfJfbdra4642UVFhS+Zbv5goskTx9PfZ5/2yyLLvBVajrmCCn47xWLdGFCOtV9TuUV49iVoAW7J1Jkpg5H+HJUBZm1pLE58YZUucripFekEpxhtV8V5h6uucFeUKSFIx86XumK8B//w1W+LgyIqegjh24quj+/s0SyUeVabAAR9OU8S76Lpv07uPzqs2fqd/1I2jGFfoEDWjCCLyixb6DJCGKreruKI5rSHclc/FQ5pLr+mJpW6Y+/RWSuQCwNbsVd3sBJ7K93PsvlUpa46zU52lZVWLrE99dpt5EdXm67R7FBLHa5C/CBpt+9EFjzSWzi8+r0b8jr49pnkiyGih0KGPQqqxE9fUYi3bveP3tf6rM6b3UkzvXTD9I11jcx9k4t1crshBK1gWOZTlwmYKHNZPn8SXWz+bz1NI+bn0erIGgm/0tivJR61X1dzWw3RyvFl1Eb6Sk89NkStOeV4PvszhHOSp+AzrGhmSVSEuP7HUAfOyhFCHW1LuMEcNMq7SRIqZL/O4NfgdyB/V7kpmOp6Nfqwaeig9kzxinxkNk0osLg/hZEKwrTIqdsBPfO48f8gRqds00/zJFVvkEtc+zbO8YzxmO9dqo/XVwMVGo/JZa/r6Yzni4xkGEVSLu7sSPqZAXLdsya5tUTMxd3/WGMbXPRbPc1x51XG4+TPIHxqceJ7DQn+fNozmuFhwRm37xTXysHXLSh/0GooccHbBDF3dE9ETKYdwkAWbZ67UjSiBHOYIPHBEab84uTO2Om/YHzMKeKyI0p1zT/SiK0yG91g8ZLi66rYq/32PT5qMpCGQbgVDocPxZZ7+3sBrKLHKuSIH11sGHs5XqD84gYY4dEmxQ9BlzbmfyYf51+AfWidr74Ze1yWJuKiKpgy1O2QYfda5FZpd0Nv63ovLiS1xxyi5RswVuRwEn5XF6jJrHvjXJ9kSp4P4okmEdUV93ppV0EdRhXF+j8Uz5k1edRc4PIZlQt7H5hTe9pPD7tVk3BbTfplnL9h2Tf5gyena2T6N/49pkc11RX+I+gy8/DXg8QyGVVUeZ1/ALcoCYKe2mfpHfL8XNb/2uKI91s0x4cO5z5PvdlMTCee8NkCxk5qy2MWhaCatYpQUwWTD/9jAqkbaOV5ZEn2ALZ7zsHLbNasdPeqnj2DjXCvAmAKuFsirb6ufT/1etO9r0YpAdq8wdlSpW25i9l2+bOpg9j7OnMy3NAK9LRk7/HyufR6Wq7TCIQntnafAPZ/D5rP/G1Fs6xqMe+1gSN/G2mnD163hoNPnGl9ayo6aeokaTRMSmBCDm3SYPTjLDd04XBb3un+MsPqeKucLTJP7lNeTZOXu2DsRb2pEAmbf1lc8sTfFT8g5DejvjtiOkgRWlQonU+qgkeO4JqR1quSFrHxrVvtZUTar8ECaAY3HOtbUHWfR3hTrNOFRDB/nWvzi9W7Xe93nl/JsIZdbcaNt5xI3Eb4kbBFhbRA8votDUXmc1UFa5FEMH2dbHZgEstclmQLnGwPSywKOzsVWevJ+xe6rPSb8Qg2Hpvvk4gCrallmFvq7OzbVTMrK3bGTrNtSvG3JQWa5ySwQLqx85dSkIupjWTRrqZwaSnb5WsbFsPfwkqX/PIeQwktTqj4xBQeBZckzh3ybI6ltRAREUNsIHl9xqKi8rf6u7b9Ytk3cvWfnNWQuwhZMO53ua0VRb5xlQhqTZQRgChzxiWmkqM8uR5GS94A83gCKa3l1V947H4KQ81EodTl+bo+xFKglxY7zgq8dNgJersi9v3K0YqmDC7F9u0qOWCh0xttecVb6J1VA7hx30pQlzJMX8RaBHMKlhbbeFeDjCdgS9z6zB5aqNSNCOVj2oJZJHOqzK183dzVO2XyeJxnqmIy9eQjPFvAFZ30LA4R7CyTt9PDcoG4scsjAFrVjk0O6ttiJBM3YeAjPFjRj4SE8W0B1xbUnh/CUT0jOp1jHBNkZhFKSWYDHaFtKTAvwGG1LyWwB7hS8gasIV0v7r478Iec6H07rV5sE3abItXdi1nv6u8s1mfsmX8CF7JlRvswF6xMoF5dFmtfVD1hCNLd8YJ8CxGGfW8LkZ9HUR3VdpndNDZV2nh4yoMXqP1eZoakWxH33VoV8ysodQpEi5+kcNO+1QiNfOwdYYrsACV1k+CJxcIJYRKZi6CFdglfKhRjqtfnqhklUQTdfHTFJxuw3wi+g+gkXemqqYNz6fPL4+F7scfvVDdPp8zotSdDLlyKvlyJSAcAX/39BIKEyX+7HwR/SEib1B3iX8rd7VEAuLqihWpd39lORSdxRKqiQlmQcpIbyaukY5D9FU0sK4I1fXKxSAD/85ydq1LjMC2ufdVCFeSj3wn5+B3j3KF/ovi8QnUSauVoO4bDSGqQzlum/yDK9KUFegQT/FDY2DVx4ayKT6iHDW7yCVcPnFTfBukjHNc6woKGnHCKkBdmI1FBOkRCDlqcZkAbM5fJKmSxBBZVeWCmAi12Vyu9eMAXuHr8uR6DU39eXuWPFBhdOeNzU7R6t9dJZV3I58ImT930XjmVoM+oKrkCaCzcrFCD2bXwC1QV86oz1iwJHvCaSfG8asK05DuoN7m8Vsp0+pVUd/hC6BKVPTmE7NOMcHg0eSH7NUN8dHRTSwwnHrWLX8j59TO+J/RSRuSQofZjLDs04zNW3zWOhvztI0AoufqT1UspkQqEbXkmqGOrzH4Bx4/BqAH9OdoW/U8cobpG8G6aGcud+2RkcX+ZgHUi8te5e2vOq7wG5jggEnUkK4D52ZJiuZbaSrNxF80nSdYpVDUGn5Io8cEpuLPFlDnoxzLHWL6q+1HdXbJIOMgUOHkJYVcIt+eGjCzdtyE6UP5GbJAB7LFUHiREh/MgzZZam7ohhSLhBRejQpmh6ffIy4nPdf28AoTKLavPVQfHI0/qyTBP5ybBY6oFZeu4rlrpQUtVf376q++nbR+mBs8excm8DtdurdNAKEN82pGRQgDioDMZj0tDjUc2xqPdx6IfiKc8KsJDkL+OKXDaqvqpS7ZEAuETKJjCvYI9DCNYXix36juTnD5g+LPlXCKnvDiGQTb7IIN5luOBH6ruzfD3BN15lErYt2K7t+YIkyArenjEa3+1ZXnfM7Tl8K70Qsh9d+CU8uimGJ3JlkfR08R5rirSHNo7fSMTo4zaywjIOp8aN9W6HwEml7psrlmNUgl97kSAbitz8TjJrnv4+vS23cysInyK1iVhDVk2PxfPVLXnV7ZbkNyVIfqKByQ4t+TIHrDgMUaanMAWOJ4tQfgTKlzmdE6aPsJS/FiwU/gFWT7izgsYUsIqmdFkMbUrOwfvvHg4Q+dp09h+7Oxpm02Hqpsyv4O8kA2ag8kKh8lJbtPVHYqNIl9xiM1BcRSqe3+sKgqrIz4qynS3e184VuuAl0w6J7c67OIRCb7zqSA4toPu8yZPPi6UunAru7y/EpNH0dwevz2KV5tIQP7bEhdLU8pVfHVSA7GZoz2ybfkH+PEHCN8bGz2Lz2vxNKLZbcl+CslNdxJuudImrf06GkS1xUXY2NJYdScvKZzPaI57XjOGb7keFWBiWcEEOKuTjZiHm2G0P8q7nm+s1TIbIxM3ch6UT1qH2yS3shm8csRhz+clH8nUtDcg2wDrexjlLM7T4hKBYvsxJhD59zTsyqN4XYIqdveK78/yGbrICFQoNZh/lwgndSIqGtA92/D950qT94MQxeDAa9+1QCq1d44ZO/pKHpSPs7AiP/zYurTz3TM87L99BmSIldlgJJ8XqLs1B+K5hxu8/j05IR9pBfjbcZoE/OETk1vzDpe0XhxNGkDf3IMG380pk1kpvA6tgHMPBJHFgTmE4MH/gL4/33+yx/EgXPJLuk8P9KclB2Cf3YB1QQUIE2XGxUOiBF//Q4qYBHLTwqkofcmT9poQjzvkHGGXl7md4OEWpcH2JK3M3vHGQJI+VL3MJPJM9BbT5uj+uESRBk59y7wVX5GjNfW3qr/cERZfYUTTqRJBt3/1G2fLi7XO76HDoKimNLFm5Q3gDfK7RJ/HRMeq728WJ35u0lN2X6L97LOeTIq/LIpN5eVUw+28QxF1zERbb5KvsKEuBIDbJp0kNSs+czNsluMnN0FGkd3vnNJoIV6AbVY7zfVCIYBnYrnEzqluU1783oITV1cMdd8DCFzqaQUeLfzZVLR6vC4UORg0xPlSIxVKXveyyhNcwg4nkVgVTNMVeo8JIloNsY2QKXG585D/x8Z5UfxEK9zD46yZFlj7mxzBhOKDxkHyautuvrsbOkI6fglUdwvdlcxxDu/h15jpHaF8wCDo2wD99Tgnk9eZ2FSsjAErOXUA+ODB9lZ3m4C7jNwr6uwM2mDQlFPpEfXZw2ImPCn1yflGoeCgGlxa3I7BFc+6En+pVdlwsXvgu0t9d9KC8Rozd+90uYP1UlD95dUgO4yRE0IJ7IaulT5IvSmE5jHMrp8/JEuQPkKQn0zemAt0a0dZ1KvRuWT82r0frVVW3VcjpXn7we/FBvJXucSUdL/bPBSoUcpgyRa6aw1lRrkBdC9EyYqmLchz2cJBygTZ3WVot+d2D+jynYN2mgDvlqAucDPA0X6AZ5eaFK9oaEUbYpPsUQUnrwHx1NWX1caRZ2zyPY/PVFZMoHenv41kvcxmpS7iCxN2EdMtAQ5VG5WOs6uuPwz6kUe7Aov005YYqSbLnnGBvRxjuGCQ/z3NyFElnGQ5jPQVSDya0xjQOO0a7dhHRDxP3AsPO3QOmA3du4AopPqEHZTKMHqxqh2ZbbQj83Pkl4MOINl/3+Nj1BE3UQ1G+xOEmHpuPBWpEceCireOiPpA2ChNxyALieg8stDss9BHmsEyTSPEfPDaflMdGFNtuRP4HfGlfAmcwbb46YRKQHGwKKR8fJdkVTIrQXN0DGg/O1dTddpaN6/cY57T1vDpf8G7d/tvWsOG3MgobDmg82FBT94/FhtdZw7nr2y9zBKarnq3UP1c5lyztH70MlKU9Gh9Zqq47DhMr3qT3eos+1jtLMuZTs95cvhEIqqaEbXBcoFOERuXjDdHX31bro33MC6l2T1hoswKLLXI6TE0L4TQVf9pjW6ab/w8parIKdijz2PwZUoPiwJN7zpNnRdms4p1yCOg8uNICx0gnG1Sb4jsYYulc5xOEQDyq4aPDwWKxThMez/Bxj89LLkvyOkCXYDnULUnj8vJK6hFs9yneWVmsVNzNl7lwpgonW+K0tv8Jk1pY0u1HtyuE4uVB12uDQHJlEDh6GzqF/filfUaYRygUe+EeXn5Soqcg9lhiELMGFXwuAsOXKEReLjxN7ZEMz02T0iupYvFsx/bS98rd85juMnNizNEYFAOFMakcw0g72ggmQbihIr0pobkmMdt5/2oN0odA7b/H4nW+r6q6tSZoJI0CX9dgkbRf5oxZDo39ns3BQnToSCFQLDIv94oBw7ay9nGSkOdIO+8yzwCS4l1cNvFOT1pqJNK32Pmy+S9g4PdmcWAeyDI+xw9T4qAj1jVIlngNvDtLxTNVSbEP7vd63O9DcP9Jj/tPatwziboL+FR9hnUNy3jOOzlOD8Fni2gk+SdtXXTo6eAc175kyY93kDadGrAjsTUnS5j8LJpYSYIEdD7qrBnHtm7+25wki7wZhm2nLpRTjlsN5dLvG/B8+gwFMjAFDlQFz30Is+DSZIuctq+tThkWT/HaFsESIXGUHGcMETNpsiihdcGHJwOYUo4dkjCFrd7Z3qNLmhK/7XAJXnCrsd7VlWH1e1vXEtM4645vnscmK7fHHut10p1zofNki8ttEdhsMrn+kmRQloKbKXDEdwnLtFgIOwRb5HI6XIOM1OacIkyBi5mDpk/+Ri1TModrKVas5nme1inIpAucL9vjdU4SrKT/Inn98EBC9xUen8+OYoFjnLV+Gi0w3i98XImNZMGRX1PiyzywKhC645JcX2IKtobrryA+f+hzWYbubTQur51Nj2AcXr/s0wq84w2BzXcPbO8V2N7vtY5+UhZVhayRLApH8dh8bGEjiv3lqtl20sBIKL8IqAkjn1BTn+EjzIRNjfru4rosyroP8uTclXSJQ+hlk2VShEyBg4q47g50OaJtPs8V2YWfYxQyhA4fXYzne1iWsBRwMQVznrjETxZ3VuLXtnHuRi42d/jsEmFHcoB+AhVvHNIFWyOi0Aec9DAw31yLxCfVnKrmtp78/IGu42/OXEItQOr0xiehnK72tvLJzumr5E5L4DRjFD4TLK83ztS2ca7iBNPfXVxGSdYs4Hl+naLagDPExVKngw5QVTgYFxXU8uMOEcBFX2gr8wpD/9XB1ZjWfE6H7tOUkWpfYA266GVOOWNL3DAq3/IRCt3wSmhGfZ4//u0K5kg8HVU/0sUDrHlFkC1zOBYkNf5R5NzQ6e+u2H6UYN2pVjKkTLErbvzeuWSmhEJXvMcFFv78gubLnI5x0SbNP2Kx+eq4lvGjvuJyJl+n366OqqpIUvISrbBnoa2UxIzeHq3XWYo5nVJYNBuVvh6/PfXQKiUE7ZELcfdXNnB7XTSl9BkYq91uQC3b9jAxh74EdvMGlHh5e3azJ5ZjN399K53vEJYY/G9ODLGppWIHhRvNisw99q3mhaGTYZzQoZmaEUiAG/oTbavLYnF7Bau6TLGSdIJHaZIQNrUFW5ap0wLaCAtzW4FswjUQgVksuhwoPlrqTc00vevtlrq2oXssRIQWXwVpYXr/oJm2ItbA6R8QRph4SefCJrrDM9tEH6dZhj4P7luL2earqKbcZa5ZnIE05ZDFnHYOdRhjOncMKfKLFM/nq/Pqosmy317fg6yCjqMPZp5LWK7SCicDapP03dIJEbU8ZKop5L3g4GlwC/bSNxc4ewLyCHxm6HDgnkLhmk3eWLOKqoZK2jiyhhz9Nu40+8ICV/AJlIvLIs3rqguIvf1WwcWPtF52zmBdRImxshhDIlSx4AtjQ4EzwOKKwCfmDm/jJmUiQzyB06vqLhqOUCeGisMhDeQjHltMicPj3kYGMo8/XM3pDPfu3xvwoNdtZOBiIi9SuIGx0WJExKGqi5dLwrpzUfwlCFP0Har3tWKHOEhzWPIggzO3+zL8XfUf8ISCB/ilWMCs2tS7TpZwBciIqjVIiAW/gGdpWeHQcHAHKtiCvH6F+v6YIqnXH7l1Dpffs5OsTZHVA3wBeXoPq/qm+Anz316//+Xd+9evjrIUVDhuILt//ep5leXV3xKybkGeFzUZ+m+vl3W9/tvbtxVpsXqzSpOyqIr7+k1SrN6CRfEW4frT23fv3sLF6i1fvUNrheWXf++xVNUio2ecOjjtI4mf10VZf4DrrJDcVvj1P6DAbf0sX8H7VypZ8utbvuKvEv7C3fjtdYqpS1YkeaCBRJgCfLc7x1CQdPj1Kyxy8MXQQey81aJHf953F/+pVnii/u08X8Dn317/L1Lrb6/O//N2qPhvr8gu+LdXv7z6387Ntwe2bcv5IyiTJUC60Bfw3F7D+O31u19+cUY65B5q8d6ltTOK84o81pmE4NjwSn8jlaKwIy6kcJS5iVZ/ZmlVl2JcizD9w4FxXLwkykyP0hknTlDRypz2SZi4PebTvUTuPEE/5IAZo+ub/Cu2bEaf7VnIvm7F74ngG0XynBUZEoWjoO63VTrQxYFHpxKcWE1YNNiDdwOqn+7bCl8/aHdBEgNxCQ5Q5Ej131bg+b87y8qywKuX6Bhx0JEJRdrcffqAtagoaL/e31ewDtlrSNxJCIJjUCfL6/RfMATJJWJ1vORDmPGkWK3JNUI34e5K8jYS+h/p+gihI5cfA7qcQZDjW1+2KKyFeLeyIF5XfxQZ/hcPIUhMFBesNixyDZMiX1QhC4IkGhW6ZSFSCQ/8b2YEf420OVi0fgGf66smv+2qD/L8nYc8R/yz/pqflmVRhqyxrkvklkGLBd87qFPMTAEj4nYoG7b4DKqaXMD27cuAgPw6zYN7g1DE78s7T8o0SYIEtrQ/HiOj2Ualljkv7PPqU7pA8iuEH9GfD3hnQlteQlwoUiFhqVMQTMPVqKDRWm8tf28A+fQtT/fFL2KxtXg4BKiAY2vUNvPeXUpor2aPg5s64/Lbv3BC/XvQZPUIus3fG9j0Wt6Q0XJPOJEam7s5xVQOsqU6zzFmMZqBvVR7k/vGRxCfeXkxz7QuTKvlQRluaCx3aQ7KFy/LzShy3ntobV/QbinXHJ0QW6/ED8VTnhVgsSeLrx9OmzK1bajJ098bmBKU9ynUKuHu6+xbBftGKSeql/9ZRBPkWejxHRMWj8Pz3d0OGYd64cPrWeYn90J2+lx3j8bFwHZeXcCnsDOJmxKg/lAqoq0RNtysvqWQBJli7F1tD2tB3yXzDuWjG+yJUNrcDPLfg/HDVdFdJxipq0/GZuncFNG7elOM0dEruM5e3HrrgHaMHp+cxMZ4nCSxUdqcm/nojO0l1Qiync2FofZV2GnaeEPc3HDxW98YibE7gYeLFlKerR1mgDAvN0Q3Ieme7sk+0SXp1yn+f/mLu1rZ+gPM1gqL24bXPhWVfpH79PeyKINOs6xCL7w6ZhN74YO4dUxfV0HWA7ZDWn8NEm6EA1GtEdZdl/y1vYq2JwuPGdOoB/bPJEMFc7HkfBH99Gpopr3WZ2rhvU98TNcEezVxhLF0YyDtYduOWKDPTnEzMi9Lnq6aFdndjlZkC+m3XZikK5DhsEj0qyLxje/+ilgOe2rRrLt2Hw1+opZa7P3d0FiUwonbbP3g5jgreynTRuUeN/kCrRC0Hoec/W2MyJ4Ins343FU1um6QosY8h+DPKeyjD2G4uLcF4m9iPHvtCT9d9mk5PaKD24pBnNTS07sTXPWgrvRHnCFc2OdoGEtiD6kt2nNk6gDYK0rCQmNxNuSXJIZeKf59fb6IzamXsQIG/SldwJtls7rLKdPJB9F3NJ93WdAEkAj0ahkWjxnr9Daqh0PvPI4qkvdLEOMWwnbCS1CiLpDM8kOC8jCMHacj8Zs+pgtH90y42LHzHY0id/SqrBfOo8UqzYcH3SMg7Bmne6A3bKpxzrSv+SdkaV4Gbi49jljiiU0SGHfTEnIFRpgWKltg3M4eZVnxNLgD4GMKn4I2xKP1uiwe4eIK4ES/180qZJYuijoqvh4ZeRSHG2xg/2Kh7I4uboqjJEifEJJB+qO6/tlEXyIgb+5BUjclLNEOU18gDWqzoGO18hGxTGyc59XH9L4+AWWQltXjCN+fu4c8v9ZLWF4yqe/8etY/DDps9Xrx6HOKdtRgQ6xOE7z1Hy0WXJNB3T+vhoCiCJEwgXcw86xdhD26oJEhorer5Ou9gM/LuOyQnD6v05JYxB/AiwqjS7gPedCLIAzn7k+gugb4Tk6MWWUxeVw14+qHRAGigeFzoqOHEsIVH6DjceA0IMJPEkcKPxre2wuTJx0S5rG8EHlH47scnsqLgJF5Ki/Ed4jPJKLcKT9Dk9ofcAQpZYs2wwjIemwnS5y0wsexZBt6tnkROoQCp1UQCbl3pAPsBZDjN9vyR7TEELL+tClI06uL5GckNyG2iQi+o0eQorppRuEMcANJ++i1e6V5tPF+Lp7aseKNpnuXL0iHT+9fiB19VpR9/44hMotC0B6D5CdJ7Y1TpwQebGATDeM7b2mITITBuAxSKcj21R1FxpiYFl974BgDHxk4XHSoUsNFXB9VFLE5Bj5uXo6bui6CbmV1uH6k1TJLqzocYbeWM4j4EglmxufhdVUY6dsEVZqE+ZlpBNG3jq/ZYtwGOjPjhIodit7G9RrhAZn3QKzORqg2hkd5b9IVjBDER+M+zRcRMfeuptO8hmUVzIud9GKwwpEZqJNwk7aJ7IWbFLZrN0jmo80TVvVw+HVSrPBtDLyVjMqsqP/9aWPVZZwPGcUPmD4sx1u+rJkSHf2PdDEi9k/j0mbYlWLLnAFxXIET68Qgzulq/0o5hSLmZbBtOXWVjjx9hOULnlV31wtbO8TxQl8997keS9cO6ccxqGC3vQY7CgZcXyComhKSa/X6MEjv7o4bQzg0g3+wTXl4JNt4nvY1I4lbLPQooAsXgiUO4IrhpmEQYjLExHe9LFrPD9iEt0eMaBtilfYjfsIYVeBzt7qnUbhf/FsF4wRtqUPAoisOfVP+EsTSrdapJTHuMrWaSARM3RlXdVI0axwZvQiaNRGLTdpAl5BAcnxMdsEwRt3gwVu3g1/dWvDIn2nbX8HjE86EI49cEVtxS+Qol+OiRhpY9BgpY4I2j4XSs12cQKlDOBITbhjnzOYyJQEmHsHofcUQBR9Hq4XmtGSCs6qbor2RwGP28uF2OL6ypwg2qo5dFCHS4a9A/hA/eWbEmMK4PqktjNzaCZ/FvkWcyyhI7sV+T+HTF4nOZiGB7aPYW9G1J8pQN5qYCYSs8lv92SNiHBaqXEIM6j/5hBweEgHNkQioOyjcsyU17w298XSyEfcaZydYZ+3vUVLJfmQervuhZujM8aS9avr3PWS2rdfucL2GozuougdLh9wIQTFkfThfDGSbZ51vCjZBRIAPfoP0UhlR7jVVFGI6e0Zs5D2FFYZSIPZOyQ+i8OlzXQLP5yDsJRt1ALcnIs3oEPPI2HxSZEX5CT6TFASxkW9f0mZXVWrPfLUz61IKL5lF+5uaQR04r86Qyd1sru5EiDgemQPpi2cHLozBhTRF3fvA1v7DcOMesuEoB1fRzy0ouh9ObuKe3BxOW4y9273TlsNhxG4fRtiHUXR83yXY35NNqR+Vh4o81AxTStbdK4Kx1/x51WeeCAok2lU23TP+1D8X4h4caE5A7JPV1Zge2gOpVVLjALxnRbkCgRmrelzXYPMYVax+Rk/gFOnq9dH9fZqlweoxuaMbpDuNveOfV+0ZQp/KPSQCRJO5WdvlMBbCjxXa7TM+cW4xdwjc08/FQ5pHiqrF+Ppb4BYoXY5nOnLeemQR5RGE2DX98YN/d0QM9v2x3pZ75tuPXfksLas62pkiZtJoyKR7sOdLWqs1yCM9ZyI9frTRsvuKQWsEocdZex/TPPHwP3DVQ3pyQiU8CEzrSNbTu5jI3kdB9o90fVlUOFGOXySbRLtaFjmUJ4DzS7PZJ8qKw9r++5+9UdOugj2RnkZH8DsPRzDxzVXd1hqk1xJEMW7r3TwVnyEm1nlVGK/ivHflu5tlCaE9/j+54kdLBJZpwuEOylj5HQRpz1uVvtrb72m96NmHS/4oa38bDoGiPVD9kCPqtZnUgjzjE73KE+ElG3vuppWqPWHu8bTcGffVu7sSPqZAtrhCXQczCnRrPj3Oigesz+4Ji34G+UODExU68+imZhCTSo+WBSlt54ixeWzT4WC39bFGSW7e4TrR5KewW9Kb/OGxUN6AhzhBqtt5j/4QbhH/YH4KE7cXL3siZ0fZi3sanTQZuXCkPzF2PxYk54rXcARj9SwDD+cr1HV8Bc5IG/fo5Kt6v27UjqvTtAHi1zXOaX0Fq6Ip90YFn12/6elpZHKf/EA9cuntAT/VB+cQL1aXWfOQqnLRWh4qF02iZvuAo6ML+FTt0QuIs3NoRA2cvIIVBxXOQBMHUxQJvJ3K7R/JTtkO/fVgYPRore/RFFl2lFdPexOChwfkEbROakX35/mJxP5doO9FHfZ0ydh3sBDR9oRrZt/m43FPG7XWybw4J/pRtuhoeZ7ItvqxgVWNtFi8RrbhgHE7NZDxjz3pM649kQVmW/Avf3FXmCM9/RQlGBgh6fge41CwvZWGGxwO3IfrEv4ZKVZXPXUhihMsV2mFz12vYIIl7f4yf8g+NMpiGvIOhCG2nuvTZ0xVkB019RLTNSHKwF7N++w3q+JFwvazdU5duYmItpOckbF/xbx1U/yEccxIgu4oSWBVxUNK4j/QBAclabJedGdF2az26Cz/plinifvy6qqFZRaZe3FbvWJqpy9c7sLFmCmuTpL1QbhjTxYIGZA7i3bVdnyB4HnELQfmj2tjd/W+Q49UVRetJA5yRuF0uUEI8MUb3IvAFww6LIop98RG8qgFGuu7KYH2Sfh8LItm7SmBurrjhoR6vVIT+YTiott1gtZyDIGC155MqTqIlVE8nLspnsiy3BMZtRPiYd/YbQ6neku8/WBaMpaIFzgJvqj5VfQpe3fDfmCTFIQmOQjsDJeiIDjHQdglblIT319q+J54DisGri7HdURU7b3DyPnThyxiTVnCPHmJdkm8R3gFNse3hszsf/VelexxVxi1v4M6tjBDRnuN1kJ2nicZ6qolOf7sJ4n7xvCtwSkau8GNDW8GTDRCptFpRtrJhmlG2DU26chQQw6L1b0xRowhwZ/ivQFkZxCOTVN1y2MTWN3y2NTu8Me5Wkb4ZHRGHF4kHrUVIjpGawJp6g3SdhYjP0lyBZ9Aubgs0AZX/YAlRKwVmFdyCZOfRVMf1XWZ3jU1jG01Cg1U/7mKlBepUwIUEX6usWdx0ugNWv86yhhJRBw2Y/DTMSVEcuQEzT+rp3hNO8JCRwiFTQTuUjTFVBhfYB7x6idcqEgX3NOTx8c42ZowstPndVoSy+lLkdfLEfD+FwRxxk7z5YcUCbf6AyR8GBKvOaDpMjx+KrJFpLkSkUdkBAr5Mch/RjOlOLzRlhiN9/wkNso+R1ZktOd3INKG1EloohTEzOeKA3PQgvhX+4g6fu8QJOy77KOgj8ZuqgauYNWI6XQ9pdEaX5iOTxwRccReI7tz0Inid/2ywXUrGNudegnSWMHmXAasKDTtUGJbBSe4bFqX4Si+rrETE2/vgQZtLlzBFUhzKkjdIyr8E8Av3HbG7UWB3fIJk5Eo4lFJbxV+q5Bu/ylF07U3iRdnf6OTVHVvvasWFpA1QQ6Tj+k9Ufb3kHX6oblP3qZm0Px9q+DiR1ovPVmIqx7cFSYDQ3R3y5TMuicM2qsz1Cyjvdzj3q4CT8iBYU/q8MOikd1851XfVXIPC/Dai6fQQKbZOqLBcIUGu8ZvukfT1AaM8e6MXMMc69Wxetiii9e9L7CqqFuzgQ+KbGaE6GazPTnkFg+zR0llhvFEjWOZUV27nPfN0r83oJsIf1n9LU9r8vDe2MeNQ0Ojny5OMZpJRjL2aWhvL7X79dgkY1sbm3ajHdnFP6r7UDzlWQEWwcmIzqseVRTF6DNi8byCPU6lTmi1+yKp/wOmD0svtdCmheMmX2TwA6hBHAdh99Q7vogZh0fdtv0LnOBjn7b9WTboCypNStgFzD6Srhiebtxy5ZH2bu6Xl2f2qOOWqEHBqwTDMcgAflcgRH2rRo+qiWmExT0MoP0fHk/K8fVHeFAOH+Ss9ucd4hkl+U0Jkp9p/hDxDJDEvI2rlJCDPBjrpPEDzNJHWNq8Ebktb4L1C2CPvBjDkLzud7Q1I5xPiX7j0b0HDrpH3ZT5FfwdZ6vbl2kPvy4VaeJm139i+J2uIKiK/KwoW16Jo6R3HAeJ/Wth7/sgtQ1BcLpnxCVLDn3x5P7+oogV5a572ZtPzuv+IgItKGLcCtuWaBOHDbIgoT4noNyrTTJcWl6CslMcgjxPrV/I75iVrhv2/PVmksOPV2eX/zMfe4x3WQKtTlgi9sfO/dHs6Ri7586JuW7mr9eIhn1Y3DCPeyLzZl4Wctp+XbPRuNa2ihZb2PrF1yPO0gyROzArcJtcuyNfaH7tkbPG6wi6JwtAPsRYvLc1meZnYZQ9YZGdmYVOqNyAhz2mvOOb1q7E+w7KFOk6Aw8jK/IuzcE+SbyfjQtNrcIw61T/hLHPiz0gb+5Bgi8BlcisUlzvC23FW222etKB9Gss7D/SxXjIP40b9wAqSCjvfwzo1Az+wTbl84BCVaUPOVqBKWFK0yvx7zxcS/1hCc4l6HGVhKkdYvv35h6Of3PvB1s7pB/nVfirFXtn+KP9IPkZwyAnNs3Xpv56T1CSQY6RIk+xs+7JdhrKX/2/A11uvwDieNsw4C2tNw7c+M7fCxZg54gYgrPNo0/UIznBAfO/N2kZFkRIKX//f3tX1hw3jqT/ykQ/7kN7u2f3zb0Rsmx1K8J2ayS5HbsvDLoKkjhmFWtJlm3Nrx8QvHAkgAQJ8LJfuq1CAnl9SOJMHMs8S8cvf2oH/hPg5FerW1x78ta68EWaxN4fKfA4axx1GnhsmK7v423D0RoVB0dB0Fa6Qp9xfD7EorYmsjTL7/7/HOekuH385FvOeuaw/yd1E7/B633Toz5mNQGj64K2dEdSsht5+t3DZ8YLpO2fHFV2xmj8l/Ztcvxc7YZBg7LlHDa6T0heLz1sKrDOto3kLy109bjh8E3quraPqfeoUwge94LxWwCVDzYCZw/rzsCWda7M/0e3eVekb44V/ajPxh3ZnXPiKB9qFZF/N8TbKtnb7DHrFuGCzMoc2/mjPKSvsv2zr9RXdNZJgdsu570n5dcs/xxg5fsQ58+s67QpvofEb6iVkSd1WJNvvlEtj4+E5ZMaKx/cmIOYDo8s161/L5FwwAtlpgz1Ytvu515D5qhvwsXbrGrAwSj401xXWX6gLuNOmHhqfvA7KED39PHC/PZfnnmdVcnZ3hz31JljP4FuY7BmWXAjAajWcdx3vm4jyKDOeeaBnyI+kQNhE27a1EacyXTyPZBx9SumTSEJme79LudWw4HlVbz7fH1k23h8btONwMbHwfx5FyhmP/4+xb1J/sTOPTnQD/xmdoiCfDuqV3apaEpa+NENBz5meElt+pjlzz98vF0fN2Hvh4u362Jm12S3tc18HxMGqkL9GK1vl9KG/eMPGqsOO2kQboB6sUtvyS7bTFLchU9Lp9jNQm5tX+/7xTKPhwo/5D8ANeU6R3o2r0kOadTH8d4hb4Xho1b7hNhGQDb4lV8/jw1Dz3a4auAIGPzklcQFnbg2qZW24W5rJPhlSCRgT7lQzb5Wwdd367cVAkPv4IQaUTcYep0cyLHYzvLbDxhNC6OrLD8ftriMy6nkNZv67KurzGPjxm732SnZzZZHyGFdiOVobvNrbgSY4/cXrvLsMByGYu1xF2qy4WLwdcedxzx/+ifZWVJvDTg5Ut0W8rPSUV0UikeNRK+LZjj76rl+Z9FTY92LH2PamyIUsKE4/elttpWzBpxGFZcB0yS5gZVvGiLfUl0iJKtfNwLLsENl6+j+1wGNOh9Kdth8PJzi5HErY2LI+gPX7uFv7rDGqtPMs3d7YLFv6DFGhyk8G9h+b/ueQ/r4q92OvZrWrA2qqVtNHJCvNFtHkUPuD6DAPVV6itqEO+j12Kk6B2B5ctxXp3niNH0e085FWca7p2oM8ctVouxEuRm7b+tXj2393bEtdCB5T74Wb0nVcTe4kgMr53VNB3yQETX4rYRLFeEi1mDUTPXFy9d8mPpv90sH44OA148ktJrh22wec0pcPpHd5+y8uaQSQbZhF5kuhT0YUtE3h8Z8tHld3Mff3nwjnKZDmqGNtKcVx31cJ0wJE3wTfXjX3FIuEEW5AcssahPe072ODhk/smX47YEOD8rsznmVLfomfq4Msq0H7WTt3HuP2oKHF4YWv4Ema70ROFw+71Ii5jsd5IGqmRuSJ5n+6VbkNlgZp6y1UW8e3pVxXiKeLpvsuJuHI1TXx6RM4nTgu3hi7cXfSWKXtZN/sYxAjPdG+ht4XBThPsUe0SWdTWTn435UPjLNKUw/Av3XEIHYJX39OX4/kv1dWj0YMj7rJA0l5K/ehBx808Dh65iy/lFPJDfSV2/aK6S/jPkUda38Ov2sEz9jzLOioGPz9IcH/XjQ4UO3lVMYVJO35AtJxw0A756yvMQ+A4074kXJfb4rfX1qds5875uFPDeCyixFLQQkExtkJIpUkuck99XeLAljMIJd5dWDklXiqDHTpzpz2B9xYbuN6mmX647U2Yq2EXmCbJOv4nomt07+vfhygTey2Rnwjdi/PrSGOt6E+lged+l5T66PdwltNu5mdMP2lm7ioqgO09GCcvQyctuYFz3vk1LNLTeoJW+nXN6RMm4OH/p5nblqcFxOe7hNf7bzeJTmltBRVn5RfEz2j2TUXmbdwv9lRz861s19zONTM2rQyIZvq3rZUvDBcC1fZVWIHdkv8yTLR2ZSZupU7+m5Wxwd9elHhZ3+GvMBbtuI1C8BSN/yAujtXw6BmdsEg6s6bumYl99xjtNXDZH/tdVwxAKIqzMbVmv1JS++mwR9zRCeZMda6OSGflqesv1lpeqwzik2hO6iNcNBXgU4ullWaWDkPZJeFcfO2tUMk6u5Xu5o12EG+LZtAuvVhtUgrwq8hi3sjPUkL76bBH3NEJ68IfkhKarDJXVSGD6DzRC3yu2hOy3Hd5CPQcaOUVFtwss1tE6nYdBrqofsx2O97tqZR3t7AT16+d5tvvI3ox631o+RTOSU2bBePO2QRitAK/8gGerKAT16S74k5OsfJD09nNPjwA+wh6UvzKkXL+6oFR7skLb6KFE+xkVj8QAHhl+l2WN7aXirvmz0Q6UiQK3TUZvdZMWAA5R9zRCdtLoP8r340tvCoW9wVE64LomcWwZ5naeuGSSCZ2n6V1Zt/c2ahHGCyE0VvTgWX4fspvN1g39Gt+uBeTP3+4sNtZ+8hYYqxVn/TtCgZ8vqccD/koKdlvbQ1PvMsSUd1i+KItslzLMNhzffTlleTQIeEjrTuNs9kf25WpovPkvIf3Pc/62ajfz2k0DUiHRH0oefxYJ357RMTmmyo3L89tMviqp9g4IMQotSidjkfyhNslMt1cZqnF5mx6LMY2oqtb8mx11yilNIHYkY2b0rg3fNyiWvyanaqzqWoLYYho2EFJaVjDD7josUd2wmefmCwwQGKpRRmrErFhGow3i//iIb5eWfx/p8/t+qg/7VRafLuNjFe7X7UNjvzdL04gMC8YVBsObuey9g4/TC8Gzkmw1o/ziTM9nXGRy6jALRlQlqr7OvxzTrM48xp/Y/is78z59/DoQxUHJBJg1FELR1+k8ANFgvDOOrhUAt4lOG6GMaTyREEKFgkojGCa/DWKg4pjVVcHSh2MnZX+bGFhfGtKqMc+x3EdBcYTBbTONqzga/dn2/ftE5av6vxVxbzvu2+20SrIkCi4JIRUHQBVooDLIkfVCDsv5l7kXACTyHozgT8uL3AifIQsuBU7/8MwucuuNiHuMSG9frlxm6g4B8U/2Pa48p8DlHnftnjibdIbeLEzV5dSS4ET8hRXSXnfOdYa7X1BXnet2PkwQX5XApJEtgbHUqTwAu82FaDU/zgcvZYXYf59XRcS/hYsMwc3L5HDAzHLqeDWbN53VVsQwasillm4lkLsOzJQayDmGWMDb/QHx+fE04FB8CL+MhuknR1f6jSTVZrXfIp8eWGbs4gUFxhPLNxDBeK5c4NvciQkQ9n3wh+fN9daNf61ueSHCqUOA0B3SIhr5woVU1DDacvmucbHNjohvFae7rLWtgLgkN4WlDw3JZI5dR+WJwtZoVyxlBNf2apduq1cyLltXbDQ8xWznLJ1y45NkKzYkFa1/AFLRZwyJmA0cBFXolxjo0bNDRigSWB4Gas//9RCBntnyFRWFvNV+4BYBt+i+de3ib+WsnXVa/JbS1pMrqU98eR+zUiC2IZ4WlokngB6ce0IvVUYQ5eCyaYAIkIlIvaBhjci0sBJO2bR3A6xo/fxcodEHBbPDTJ+2YCnclnadQub5QVUkEir4sjPHyitgSS1aPKUEdByzNBqU2Y4dHEFnmj1I+XNZS99vaAQAmmVm46w2gHd+HFwcG9y46ISQE4eY7DddmHekefbUNrdsaYnjofpzkIyPneYJECRxnWoUnQJUxq5WGpTnzzUzIsgyQHQLEZnHl4uMZYKXPdjY1ql4laXUvFc70NghTtkO8LnFvvWhA8RKNPz8amoS3EazEQj9eUpZeQZSubDMfr1Yjl4/XbLiqX0e6K3OWD6VgA6KofRJLC66OgPdo/+Mk4IIkF+UBCYLADDZYGJiBamH48g+dzYK1NunTOvDVSivI0P+4ehx1qqwCO21OqHVgp5VWkKH/cfXY6VRZBXaqZFbrwE0lqbhZy35YPV6YGqvBSp34LFKFdvVUQJTUMioStD+H2c/HetEDWhpFMNwq8vnwImes5rMKW1cY5dqiP5XCadBlykdukk8kDINA2SRToBGdn12HT1RC9gWA1barrwOCyfHfIUidATI3QC254Kde73SKoIta5rJ1kAnBN+mC11DALQ5sK4qASwTdVBFvNPDmjXRvvlUPVMbpxbl8qlrcsULpk7zoiGfSQMr5aSJcfQQ0qucCyNmweJXl5wNblPUNPP3+YsdTaIn7dfW46HVZDwjYC75To6B+NliBQfPzNnAAPI28aCBE7L+/59n5pEUBR6I4r/l5kg8RY6iKEAg6OsMEBA+KUS/XEkIIILe7x0LiZYagg/fltBGHkS9g8KETe4zrQkJo8tGLo18nHb8wsWYDEXsgJcAhOdPYhfEUGml+WfvZuFoNDCPR4DN7fxWT5mlBM+UwF4+a2Ue4bXqmD0X8SP5IqDT5cwTnlVpoGi5eclAekWAzibgEtTB8Z8/EBWIN6CvuMeI7gBg+psyFLybhvB8+diJuyYjqpFQlCHgUbjLs9IqsCTDLz2EyD2wmzFfiBpy5s5T8njyUl3G+j27O1aOPBdl/TMonjQ7D3Wi5ltRKITTW/xgukqD9NBIUnS4oTICumB0iwlgHVmioSwPFGkhyUJ4JBj1OCPCENudxT1txWVj7wHeFhY6GlgS1ycZIg3EmeHTeYdP7rCTLH2dXUqoS1L+uG0O9IssfZ9+SrxTtNxltoGiD0yrWJwHBBXHA8tWvXUJarWIlE8KZ14+gZTi+HLhMFoaGYkVwy7yJ/dj7qEv+krVCKsn6Ar4rMRmAOj2W/xlrRWUrRrDcQ70WGDnKgoNYECzPKNqxnkCEXpboKsy7Sdumppwukc2PbfqfJKPPOFopz/mxekyHBDhrHGg8zIksDW2Ekg2MgXl9VjH67S7wTB9VfqTH0oaXOce2GRPkMs7rIcur83GfEuP2RLOtwlECO158qRNMZImkIYlcGHJHS2eLMABSdMPw7EVcDoRW8ZVaAsym/FYNQtfsnysFW8vfg18CsCbckR+Eq7k35hv+dyeySx6a24cXZZknn84lWQHGjPJDcuko144+s3prgCIs+p+n6s8Ioxcfeux4wAEhVGg06IqQsiUMsxI1HEJjI6hJWdRkAmxgocEVr21o4HyXsXk02GYP2CYN5sZ8O3S4jx/tOcyWMoroZYZk4Us3MlrgVHIYIcyYfgwAlyXjjtmngDO/B4hhHT4nyij57ED7K86T+Fh2sfUyO3xKjoww0j8Zz9+H4YmEezBCgdOyoF02CFpG8jBXvLQGCoo2k6IYKXixl4y/1czNFw/U6b+7YzE691QdAc9/nGkJ9dKHY6LHqEDEY0Es2GR41Bto0dDjxV4a/tYbEzGQ3Gz0W3PIayFnX5JUCAGXT70MuVg0Trrs6AGWi1lrlJX4K07PHUjNGobBxbTIZepixGwIQ2J4EJ7CQLnW1gHPcgNzw3r24SRmyLDugeHqRn/RXXw4peR19vWYZrH+inBHIKy/dD8uHA2wevMjQTT+bFi4T0hO9dsRIQ+79bwVI8LmX2eYCPIV66QXZOF+3USy9V4fDDNeugXAavlTynlANOG00Q0/C5ko9sdRm4PEyweSIrnbaeb1AUtVCcNUcOeCYOYVYNYh0A+UmJkuDh/dXO4qScsq323w6w1ThSlJM7OQCvF2rk1YdcVHt3mvUZRZXg28kkOcP7/5tnuKj4/kNi7J5TmnLHbPhuF9QyAO7dsf8UGOiSCep6t/CTQgh/QKA5JaD1wE0zpgGdBgf/zAxAyYECw/32vw8e7z9ZHKsvt8d/5U7PKkPiu+iltXGuHFt+J1NKu/g6XTzGWBYHG4W/4Mb3mgm3AcPwZzc4/mL3Yp8OCu32XOQJjrRBcvzve/bmKNs9dnNWucFw8PSUp/IdFEb550DMWG+l/XnvigVwXDbO6EKuyJnqADJz0UFN7qU0gbG/GoOq1irEP1+0IB/Y4CNX4k0VWeHaZDichcGjeJRavHh6QQhiPvjKUA5D77AY+FwKN3xYzD1jL5Qk3+NnuMuH9XjtIPNiQ6cdQolU00gu246qQJNXYx2SwM0HilUKMYUcRFQG0VC0DzoWrKuOUKp9lD1uUT2X3OzvIJWuVnPbYUSgFkaukkaIPVMosW8oysxZ5hwKjREIVLueqMeTB355xq/HgTP7NczNfHpGrXU0pm/XBLZiwltJQL151nWdEHxZPzxGLw0WZ2N2vky8/BMqmCShlFC5syfhhA/ILSJYO8XHc2fL7NdnGa/IvsWxmjt/Hx8azMajjvdwS8u/sfJ0GgIrYojFoaBHWwqcKgTdUJw7SVcEZ8+Z4AhDv/LY/9tzHmX81Yv9nVuyVfEvL1D5KeHs7psUpCXLmkb5ytenaYor+X1bDrPot09a2wa9oA0deVOR/JVOUAdpoBIsQNmFGYazWaZJkM4xEn3nPiUCiwnStoqLQ+nw1Rfo8KQMZYIJiEyvMdVUmzx8vswOaJyKDGVZk6jvGshSMp/O8bilZaUzuxmxhG1b9vMvmVB8mLjEB2Yf3jNAed3JHkKUrB5lkGflrZZsPRe/K1cAxHXJWpwxHPmm9S+H1D4Uhraid2E8Oo+rfxEktHILtwwisrA5DkKRzB5lkGflrZ5ht3Z2n6V1Ymx8fmcCV23ifVm3y+J/MXRuVK4YZClNnyTjznwFf1w8Wx+GpYmeJIZK+2P09zy24MxnzN+DTmWhC2ehGXsXowaOlq1vUq/ILFuiOXp9WLyQG1/FstA+G0vhss7hia9t7KG5YbqupUtAbJ22X+bE+ukrwoX8dl/Cku1H2+qtYdKRv6N99OWV5S4dKs2WKuKTi/qiR3uydyiH/7af8powiIP6VgQ0pEgjhTmz0k1fUYDduuXM+zI7EwrFrYn1NyHxefAX5iMcROpLBwE7OgKdzEYoibSGHlRs5k/+YQJ+lFWca7J40vNXQwf5DUIkif6kvh3RdB7PpSvKpmBa1q2SHKlN/tsjPcMYRiEKAChYVbc1bgHSmfMsh+UjnETyKxMbSma1BlsFYBxbLWcpUUIxpSFixzPUsjI3uPSQodwPoisMd0pRYOl3FJHtnhHIVDXwRx6EttJkp25TmHonhXApqoLcR5wMBFIjD4I2qIoncxe0AQ7Z/qveUkJ5rAClKZvCYQ2kQQ3q9QeQvFIFOBAmdtA2oUCpO9Wyq0wZuK7+Lj+SFmvjJEIpHKJAdPiZbFIoSdu0hh66jyLE/trzIF2G1lIiRbAz8zIyuH7v6qwqArgdrvCm0KVB0Kjm9tCSh+W2gbODavZmu/zDIBOHiUaGw8S9ptKHS/0NkK1OelcpCjSGJh2O8ZKrz6IohNX2rh0J8hVDj0RRCHvtTGoTpER+7KnC0rFfVDaAA3kAzkDFJapOi3OxTOfRHErSq1R0dubVINilwhGAu5cgQfDQd92+hQU6dE0IabutgUcmoKmw4kPyRFQSfO7cqpqo9CAuqmUFknt3RYe4zTi3P5VE3u62cDtVKYyeGpr6mGRTp2PVrT2bkyiC8rLiJUh2e099kp2en4NIUGRg0FhpOOibH9hgLT/u95dj7pmDSFBk4NhYVTc1FEYdL8DrXfFCGHsR+qK63daX3tOFYkMw1kRUqLFL8nD3TYmO8tUsBkkBQwJVIKA2czN5wbNR8BrkzrTtTElFG+z0ooknJlWiZ1sYXJLflKFb7JkmNZ6B0GUkGMQULE+Esz7eqLdGMu1ASrJdR4TCw2cUL57ZbQ6cCxmgASMARL5bAZBRKrehkbe1Lo6lWUSWA1ZSrcXE731rBuVqejN83vNA8/Y2d65iecVXsZyUHbGWsMkg4tl4tEWJ+yd1N1DmSFBm+xchwf42NxOv7GSga5jPWGyYsX0rJuJT1+5LqeghAIJ4kzUjTPAWHN0pA7eK2pYZGOS9qviMKVQXy5Yus8PgNXLJvf4Xl7hlgT7ZOqArOotgieQbWlGNE7kMEadMVaRbAovaf1CXPkJ3BqKJWDXhFJbAseutSL6vqHjhJcDtERO6w03pPDKY3BngqT2VYee0rkVoFBApXEtHWA5tx+GPSMFQrT9wXL9ndyJLQjm4KjSgJODRQq25pon5RRXRXty8B10b7YwuRDrmfClUFMuGKbJn1SQVWTvgzUpC+29Q8SFxTKH0ny+AQNmqVysEeIJDiGrxM6ri/gwKCSGNhyVJjVBktMAmgMaw9OkUhO5wV0RJHAtE4kkVr7BJeOB+gVXCncLzgCPKs6u5WJXU1hYVkTWQPs4RQnj5BL+yI4oLalVtwyW5u+ITIFjFqJCLHq/ZaUJcktyNUR6lbEIVqbkdW8P6q1VRrQ7CqZK3PdcFdHiBIDN8TVZukA1hw0lPDqg4bYURyEHDgBcDtDYsoIeFtIpNHuCYlkVrWrHrTXnyORCWCVRRobCvOsKGjjqZ6rSgIiT6GymhqK3dqYjYnV9F+lZhLSloDzj7bQNvWIvxkOOwil4LSDJ7Cx0uyGGDZCcDsg7Wq8QRGVxLSsH12cTmlC9vdZQ58Q++ZkW1kPOoUCJ0NDbpdA3GbXHwzQ0Nl37m9JQUMwO0CNOJjQ7Fbqzz8oFKZNz4YI4Qh5w9Ky72omx+yIRhUpekmqO6GC2w7GbwsLGftdV8gwC5vI9c2op3OVAsjwoBMGIDXIBFAjzoR0NybVZRG+VHcypCNADB/1rIRS3UARy0q9T6XaVyHRnXgQqVw8bHMrwpcKQ+6qgOagfSQeYucqQMfuRWr5TgP3WhNwtJ4KYD4yL7QA3gZgTZgP+b8QVUabo7+oEHVN64wB0HpXBGhDvZTBNWO4aOFuEvCof3SlMYqBWq+SfC+AqaI78y/UNF5tYM3gbiqMMksknuQ3GkWkNXgZuF1Qe9hwa0BnGp1BgqFD4IEFCV/Jq1oLQYx4Dj3qzqOr9tFQ6pWSDr8zNTTn58V64Nn5urpU5F39doxvVx++1gddfFTlX4z63Ul3g98VGv8el6eArKJ2huqupmmGGN01Z2RVzTHVDN8P6ZZM/f3Q3IABa4JmUQvDmuc+zh8J2CMw1Tz5fGHmaef2TtiRK4VEDhR7lLKQhnFBjVzJf0idySzAxanCaBKwQkicADfEhEZMV74Gf2wj8W6X9lMr0hmUAe6a1VoY7pANxdVwtbvvaB+atKqrtCECqebaHK++xzAqq2YfaymkwQZbE5lAuNBnGHKBdP6HXdBVQVbZeAVwsOel64w8Cy0E9HU8qwWhQtsQWB7ETPZOApIH6ygTm0TcyIjanQzSbH+Yh+/4yoZlQvAiZb1QaLweaWhF2uIBGtNd8wxiQP1QDV/Z8HUClNWotxST8ZdQo65dwDwgYQBTQBdraxMY78u6q97s1pmUlkn8qyvtPLJ6umvVw1WUTKdXVCT07qUJVe92HLtNWUPw1BMbPC7dyq9drrlxD9aEDKCUhTCEPgjqib34cQlmeJWkKQ2kXcsGI0ik4UyAwtAIpdtch33Teq0V2pBdQMqkITSgS4/hbgYoTUD/0BJgC3MFvVpyCgWmjy49gljTkBqhbgWT6sDdNG12CKM5VKIQJpAzXLCa2uwV7qq2yR2MqqpEIVSVU1Cwmv2P4ydXWZoa1RQJQqjI56eo50xQ/olhqtUpMqK6RVg5nmSkkEoNMb1HV0+TuGOAivLZLf7QlGkYg6toUE6Tj6NW0ZJjw9gSdJAMbNWYP8S7KfUDIVxF+wdOUdik4CJNCB7dQw2mHdHnYTRhM/o0pkEMrydH01wmMuXBMQ5HcRVDoAmTG+iuPunlkOPH3XRdph+jnQCqEEZRUhKxqtyvftRlh+nt+kpkwRQWrgP0GsNH/geqHPH5iTT68jQWkYUsSL3IYHYjtbZa0a9nmyaNbq1pRgs6jzdZT+SbNfVYjs6j6BP3W5aqCLGqAtL5X1IREnSxWnD+raGKmmKTRBEiLIVTD8oXFvXnK/QHOeAKhhMMow9yQNnJhFaM2cY8maaxO9IuNfVovy7CHF0GNL0NZBK/iivZ2/p6nhZQevkNe9Iqkf+d6PCqtjnzohs6P3uKC7L/mJRPHAdVcVsVb+oIdeWsgKyqNuPfcEMI/bZvX28GuIInRcCa2h6PSrfoyTQfeL+j7SPW8hsX5jJPl63REhA5kgABkc802dcDk0i6qwjkhTQOhYz0IQZGhjyYrBFMYks/ZrH2C3slv+iYxzRtyk19n5Ao/CotJyW9a0/ReDrey2cUjfp29VqKhJ4EB2sq31dj9tOhE6H2RJJ1yicTrmnOJ+RxtQQ8kDJMqAPy0zY92ZR2dsQCN8LZWlr//g59bEJOoxvxrxFBPVxPbp0RqG8m8XMD/TNIUs+HEwg3vd+SFNiDgUw9Q08conPMbgrDhFFL63/eOLUZjImhEQe7LRX9GwiV+ZpvDZmNeQCCDImnNVmzQXANaMaAH7tdcAZBtCom8Ta0rUnG7RutZuauEDa2NrGpFtoD1KQ8xlMueupgYUJNPcS3wJcGMYbhnIqe2qqURhtAjWWYxJD8HXvLEt2EYdXd1wVMROJ83qIuqe+DmNj+EcfUDtZF12ZO8f3gQTYVmtAbCHrqmJnC9ITxWk08AKbTYxNjrfAmMg4n0XWtyhu1xo0bF2lGluI30rJFm9TcziRGwbQrpEk2NQ6nPh4+EEIGysmjYdgECtFdfDilpE/BpldcojQMXQYneQurcvdci3AuFlJZQ2lfSBp+clZ5hoZV178wM0Z9w7dLJfL/tQqvqrKs2ayV2j/a2irBPtpDV2a9mMXJIJs2RfdduUrSsjpBaFyLd6k+zdq8TSJz0wqxhxv/Gcvolxzi/PnNt91TfHwktzEdnncPTwGLi9ZKphAsvoTVhF/4lStxDYx/nKte7ALf3hppBPYHWnuRel1qax6/Mm7fWOuE2MWxPP7FGsK+5OXPTIagbKviPzTPa6LuiSvrkE1DGXLIpjzdVe8sa1/lGqB++y5WZNgRV4n8b4Urb3vVNbXPdg281oCOFgbqEHFC+xTXXXfNwTPwxRezoqs8O5jsYSIPYRD4cbAmqBjf+hptivvMwRAc8drNwD0xFinPjUHxUE9uCmngc2hNYDO+caZrR9eGZ5OYQAHShcDDNGrDr4BFwBtlwPEpbF2DeXTPqtV2sr2UZm5LXf5Dvnk25NCd+HpYdH1MyiRODQdqTRX0Sg47TAu/kXZXn7+zvHs23hjtgWGVld0u2rqB1DW2BJ9Exj1cNzAFEP8qnD3/D0ytV3Bk8h/o9bu6CeuDdkPMYQ7MwQOyHIj9BOBm3qS8pBRV38XL7FiUeZxUj6HQYVinX5vw6j6LdPUNK03+ednNPSBrFjTl1L5kxc9B7a9SeXSTUOBkdLEmUnW9vos2H/duFxZswlNfwOrFuBbDIxZ46axe4DA8YTbOsH3iMbO5Ojqz8OjEZjOozT3OhvW+8J6baqCRLYbHE/CcHWvD9E7dOMP22d3M5urozMKjs8fNoLb87h76i6i81wd8AHy1PcFXVvOOYf15sL1N6MHsfFI6hCU5crNSTvnv5jUJ/xEeNDCzDkKGtTrxGA8/0BltZPvGtUQYbNPao+ovX9SNVIanXiZ5V/byRf0KaPMD/ZNOWOl87122J2nBfn354vZMax9I/ddrUiSPfRMvaZtHwt4r7httaa6PD1k765Mkakna4vbRDVLG+7iML/IyqZ4moMU7Oupk79uy9Znq+cJPZH99/PNcns4lVZkcPqXCnPLlCzP/ly8UmV/W9w0KHypQMROqAvnz+OqcpPtO7qs4LaR5tK6JS2r93wn9vfYl7ZoleXzuWnqfHZENNeZ7TU7kuKdd7p4cTtU778Wfx7v4Cxki24eCvCWP8e75pkqSzpabdI3YHSGa/eXrJH7M40PRtNHXp39SDO8P3/7n33KJ1/t7hwgA + + + dbo + + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Data/Migrations/201509021536425_ExportFramework1.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201509021536425_ExportFramework1.Designer.cs new file mode 100644 index 0000000000..0bb342e80d --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201509021536425_ExportFramework1.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 ExportFramework1 : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(ExportFramework1)); + + string IMigrationMetadata.Id + { + get { return "201509021536425_ExportFramework1"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201509021536425_ExportFramework1.cs b/src/Libraries/SmartStore.Data/Migrations/201509021536425_ExportFramework1.cs new file mode 100644 index 0000000000..09f2dc6314 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201509021536425_ExportFramework1.cs @@ -0,0 +1,369 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + using SmartStore.Data.Setup; + + public partial class ExportFramework1 : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.ExportDeployment", "CreateZip", c => c.Boolean(nullable: false)); + AddColumn("dbo.ExportDeployment", "HttpTransmissionTypeId", c => c.Int(nullable: false)); + AddColumn("dbo.ExportDeployment", "HttpTransmissionType", c => c.Int(nullable: false)); + AddColumn("dbo.ExportDeployment", "PassiveMode", c => c.Boolean(nullable: false)); + AddColumn("dbo.ExportDeployment", "UseSsl", c => c.Boolean(nullable: false)); + AddColumn("dbo.ExportProfile", "FileNamePattern", c => c.String(maxLength: 400)); + AddColumn("dbo.ExportProfile", "EmailAccountId", c => c.Int(nullable: false)); + } + + public override void Down() + { + DropColumn("dbo.ExportProfile", "EmailAccountId"); + DropColumn("dbo.ExportProfile", "FileNamePattern"); + DropColumn("dbo.ExportDeployment", "UseSsl"); + DropColumn("dbo.ExportDeployment", "PassiveMode"); + DropColumn("dbo.ExportDeployment", "HttpTransmissionType"); + DropColumn("dbo.ExportDeployment", "HttpTransmissionTypeId"); + DropColumn("dbo.ExportDeployment", "CreateZip"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + + context.Execute("DELETE FROM [dbo].[ScheduleTask] WHERE [Type] = 'SmartStore.Billiger.StaticFileGenerationTask, SmartStore.Billiger'"); + context.Execute("DELETE FROM [dbo].[ScheduleTask] WHERE [Type] = 'SmartStore.ElmarShopinfo.StaticFileGenerationTask, SmartStore.ElmarShopinfo'"); + context.Execute("DELETE FROM [dbo].[ScheduleTask] WHERE [Type] = 'SmartStore.Guenstiger.StaticFileGenerationTask, SmartStore.Guenstiger'"); + context.Execute("DELETE FROM [dbo].[ScheduleTask] WHERE [Type] = 'SmartStore.Shopwahl.StaticFileGenerationTask, SmartStore.Shopwahl'"); + + context.MigrateSettings(x => + { + x.DeleteGroup("BilligerSettings"); + x.DeleteGroup("ElmarShopinfoSettings"); + x.DeleteGroup("GuenstigerSettings"); + x.DeleteGroup("ShopwahlSettings"); + }); + } + + public void MigrateLocaleResources(LocaleResourcesBuilder builder) + { + builder.AddOrUpdate("Admin.Common.ExportSelected", "Export selected", "Ausgewhlte exportieren"); + builder.AddOrUpdate("Admin.Common.ExportAll", "Export all", "Alle exportieren"); + builder.AddOrUpdate("Common.Public", "public", "ffentlich"); + + builder.AddOrUpdate("Admin.System.ScheduleTask", "Scheduled task", "Geplante Aufgabe"); + + builder.AddOrUpdate("Admin.DataExchange.Export.NoExportProvider", + "There were no export provider found.", + "Es wurden keine Export-Provider gefunden."); + + builder.AddOrUpdate("Admin.DataExchange.Export.ProgressInfo", + "{0} of {1} records exported", + "{0} von {1} Datenstzen exportiert"); + + builder.AddOrUpdate("Admin.DataExchange.Export.FileNamePattern", + "Pattern for file names", + "Muster fr Dateinamen", + "Specifies the pattern for creating file names.", + "Legt das Muster fest, nach dem Dateinamen erzeugt werden."); + + builder.AddOrUpdate("Admin.DataExchange.Export.EmailAccountId", + "Email notification", + "E-Mail Benachrichtigung", + "Specifies the email account used to send a notification message of the completion of the export.", + "Legt das E-Mail Konto fest, ber welches eine Benachrichtigung ber die Fertigstellung des Exports verschickt werden soll."); + + 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.CompletedEmail.Subject", + "Export of profile \"{0}\" has been finished", + "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.", + "Dies ist eine automatische Benachrichtung von Shop \"{0}\" ber einen erfolgten Datenexport."); + + builder.AddOrUpdate("Admin.DataExchange.Export.FolderName", + "Folder name", + "Ordnername", + "Specifies the name of the folder where to export the data.", + "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: %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", + "Create ZIP archive", + "ZIP-Archiv erstellen", + "Specifies whether to combine the export files in a ZIP archive and only to deploy the archive.", + "Legt fest, ob die Exportdateien in einem ZIP-Archiv zusammengefasst und nur das Archiv bereitgestellt werden soll."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.AttributeCombinationAsProduct", + "Export attribute combinations", + "Attributkombinationen exportieren", + "Specifies whether to export a standalone product for each active attribute combination.", + "Legt fest, ob fr jede aktive Attributkombination ein eigenstndiges Produkt exportiert werden soll."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.AttributeCombinationValueMerging", + "Attribute values", + "Attributwerte", + "Specifies if and how to further process the attribute values.", + "Legt fest, ob und wie die Werte der Attribute weiter verarbeitet werden sollen."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportAttributeValueMerging.None", + "Not specified", "Nicht spezifiziert"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportAttributeValueMerging.AppendAllValuesToName", + "Append all values to the product name", "Alle Werte an den Produktnamen anhngen"); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.HttpTransmissionType", + "HTTP transmission type", + "HTTP bertragungsart", + "Specifies how to transmit the export files via HTTP.", + "Legt fest, aus welcher Art die Exportdateien per HTTP bertragen werden sollen."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportHttpTransmissionType.SimplePost", "Simple POST", "Einfacher POST"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportHttpTransmissionType.MultipartFormDataPost", "Multipart form data POST", "Multipart-Form-Data POST"); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.PassiveMode", + "Passive mode", + "Passiver Modus", + "Specifies whether to exchange data in active or passive mode.", + "Legt fest, ob Daten im aktiven oder passiven Modus ausgetauscht werden sollen."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.UseSsl", + "Use SSL", + "SSL verwenden", + "Specifies whether to use a SSL (Secure Sockets Layer) connection.", + "Legt fest, ob einen SSL (Secure Sockets Layer) Verbindung genutzt werden soll."); + + + builder.AddOrUpdate("Admin.DataExchange.Export.Filter.Note", + "Specify individual filters to limit the exported data.", + "Legen Sie individuelle Filter fest, um die zu exportierenden Daten einzugrenzen."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.Note", + "The following information will be taken into account during the export and integrated in the process.", + "Die folgenden Angaben werden beim Export bercksichtigt und an entsprechenden Stellen in den Vorgang eingebunden."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Configuration.Note", + "The following specific information will be taken into account by the provider during the export.", + "Die folgenden spezifischen Angaben werden durch den Provider beim Export bercksichtigt."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Configuration.NotRequired", + "The export provider {0} requires no further configuration.", + "Der Export-Provider {0} bentigt keine weitergehende Konfiguration."); + + builder.AddOrUpdate("Admin.DataExchange.Export.Deployment.Note", + "Click Insert to add one or multiple publishing profiles to specify how to further proceed with the export files.", + "Legen Sie ber Hinzufgen ein oder mehrere Verffentlichungsprofile an, um festzulegen wie mit den Exportdateien weiter zu verfahren ist."); + + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportOrderStatusChange.None", "None", "Keine"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportOrderStatusChange.Processing", "Processing", "Wird bearbeitet"); + builder.AddOrUpdate("Enums.SmartStore.Core.Domain.DataExchange.ExportOrderStatusChange.Complete", "Complete", "Komplett"); + + builder.AddOrUpdate("Admin.DataExchange.Export.Projection.OrderStatusChange", + "Change order status to", + "Auftragsstatus ndern in", + "Specifies if and how to change the status of the exported orders.", + "Legt fest, ob und wie der Status der exportierten Auftrge gendert werden soll."); + + builder.AddOrUpdate("Admin.DataExchange.Export.EnableProfileForPreview", + "The export profile is disabled. It must be enabled to preview the export data.", + "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."); + + builder.AddOrUpdate("Admin.DataExchange.Export.ProfileForProvider", + "Export profile", + "Exportprofil", + "The export profile for this export provider.", + "Das Exportprofil fr diesen Export-Provider."); + + + RemoveObsoleteResources(builder); + } + + private void RemoveObsoleteResources(LocaleResourcesBuilder builder) + { + builder.Delete( + "Plugins.Feed.FreeShippingThreshold" + ); + + builder.Delete( + "Plugins.Feed.Billiger.ProductPictureSize", + "Plugins.Feed.Billiger.ProductPictureSize.Hint", + "Plugins.Feed.Billiger.TaskEnabled", + "Plugins.Feed.Billiger.TaskEnabled.Hint", + "Plugins.Feed.Billiger.StaticFileUrl", + "Plugins.Feed.Billiger.StaticFileUrl.Hint", + "Plugins.Feed.Billiger.GenerateStaticFileEachMinutes", + "Plugins.Feed.Billiger.GenerateStaticFileEachMinutes.Hint", + "Plugins.Feed.Billiger.BuildDescription", + "Plugins.Feed.Billiger.BuildDescription.Hint", + "Plugins.Feed.Billiger.Automatic", + "Plugins.Feed.Billiger.DescShort", + "Plugins.Feed.Billiger.DescLong", + "Plugins.Feed.Billiger.DescTitleAndShort", + "Plugins.Feed.Billiger.DescTitleAndLong", + "Plugins.Feed.Billiger.DescManuAndTitleAndShort", + "Plugins.Feed.Billiger.DescManuAndTitleAndLong", + "Plugins.Feed.Billiger.DescriptionToPlainText", + "Plugins.Feed.Billiger.DescriptionToPlainText.Hint", + "Plugins.Feed.Billiger.ShippingCost", + "Plugins.Feed.Billiger.ShippingCost.Hint", + "Plugins.Feed.Billiger.ShippingTime", + "Plugins.Feed.Billiger.ShippingTime.Hint", + "Plugins.Feed.Billiger.Brand", + "Plugins.Feed.Billiger.Brand.Hint", + "Plugins.Feed.Billiger.UseOwnProductNo", + "Plugins.Feed.Billiger.UseOwnProductNo.Hint", + "Plugins.Feed.Billiger.Store", + "Plugins.Feed.Billiger.Store.Hint", + "Plugins.Feed.Billiger.ConvertNetToGrossPrices", + "Plugins.Feed.Billiger.ConvertNetToGrossPrices.Hint", + "Plugins.Feed.Billiger.LanguageId", + "Plugins.Feed.Billiger.LanguageId.Hint", + "Plugins.Feed.Billiger.ConfigSaveNote", + "Plugins.Feed.Billiger.GeneratingNow", + "Plugins.Feed.Billiger.SuccessResult" + ); + + builder.Delete( + "Plugins.Feed.ElmarShopinfo.TaskEnabled", + "Plugins.Feed.ElmarShopinfo.TaskEnabled.Hint", + "Plugins.Feed.ElmarShopinfo.StaticFileUrl", + "Plugins.Feed.ElmarShopinfo.StaticFileUrl.Hint", + "Plugins.Feed.ElmarShopinfo.GenerateStaticFileEachMinutes", + "Plugins.Feed.ElmarShopinfo.GenerateStaticFileEachMinutes.Hint", + "Plugins.Feed.ElmarShopinfo.BuildDescription", + "Plugins.Feed.ElmarShopinfo.BuildDescription.Hint", + "Plugins.Feed.ElmarShopinfo.Automatic", + "Plugins.Feed.ElmarShopinfo.DescShort", + "Plugins.Feed.ElmarShopinfo.DescLong", + "Plugins.Feed.ElmarShopinfo.DescTitleAndShort", + "Plugins.Feed.ElmarShopinfo.DescTitleAndLong", + "Plugins.Feed.ElmarShopinfo.DescManuAndTitleAndShort", + "Plugins.Feed.ElmarShopinfo.DescManuAndTitleAndLong", + "Plugins.Feed.ElmarShopinfo.DescriptionToPlainText", + "Plugins.Feed.ElmarShopinfo.DescriptionToPlainText.Hint", + "Plugins.Feed.ElmarShopinfo.ProductPictureSize", + "Plugins.Feed.ElmarShopinfo.ProductPictureSize.Hint", + "Plugins.Feed.ElmarShopinfo.Currency", + "Plugins.Feed.ElmarShopinfo.Currency.Hint", + "Plugins.Feed.ElmarShopinfo.ShippingTime", + "Plugins.Feed.ElmarShopinfo.ShippingTime.Hint", + "Plugins.Feed.ElmarShopinfo.Brand", + "Plugins.Feed.ElmarShopinfo.Brand.Hint", + "Plugins.Feed.ElmarShopinfo.Store", + "Plugins.Feed.ElmarShopinfo.Store.Hint", + "Plugins.Feed.ElmarShopinfo.ConvertNetToGrossPrices", + "Plugins.Feed.ElmarShopinfo.ConvertNetToGrossPrices.Hint", + "Plugins.Feed.ElmarShopInfo.LanguageId", + "Plugins.Feed.ElmarShopInfo.LanguageId.Hint", + "Plugins.Feed.ElmarShopInfo.General", + "Plugins.Feed.ElmarShopInfo.General.Hint", + "Plugins.Feed.ElmarShopInfo.Automation", + "Plugins.Feed.ElmarShopInfo.Automation.Hint", + "Plugins.Feed.ElmarShopInfo.Address", + "Plugins.Feed.ElmarShopInfo.Address.Hint", + "Plugins.Feed.ElmarShopInfo.Contact", + "Plugins.Feed.ElmarShopInfo.Contact.Hint", + "Plugins.Feed.ElmarShopInfo.Generate", + "Plugins.Feed.ElmarShopInfo.ConfigSaveNote" + ); + + builder.Delete( + "Plugins.Feed.Guenstiger.TaskEnabled", + "Plugins.Feed.Guenstiger.TaskEnabled.Hint", + "Plugins.Feed.Guenstiger.StaticFileUrl", + "Plugins.Feed.Guenstiger.StaticFileUrl.Hint", + "Plugins.Feed.Guenstiger.GenerateStaticFileEachMinutes", + "Plugins.Feed.Guenstiger.GenerateStaticFileEachMinutes.Hint", + "Plugins.Feed.Guenstiger.BuildDescription", + "Plugins.Feed.Guenstiger.BuildDescription.Hint", + "Plugins.Feed.Guenstiger.Automatic", + "Plugins.Feed.Guenstiger.DescShort", + "Plugins.Feed.Guenstiger.DescLong", + "Plugins.Feed.Guenstiger.DescTitleAndShort", + "Plugins.Feed.Guenstiger.DescTitleAndLong", + "Plugins.Feed.Guenstiger.DescManuAndTitleAndShort", + "Plugins.Feed.Guenstiger.DescManuAndTitleAndLong", + "Plugins.Feed.Guenstiger.ProductPictureSize", + "Plugins.Feed.Guenstiger.ProductPictureSize.Hint", + "Plugins.Feed.Guenstiger.Brand", + "Plugins.Feed.Guenstiger.Brand.Hint", + "Plugins.Feed.Guenstiger.ShippingTime", + "Plugins.Feed.Guenstiger.ShippingTime.Hint", + "Plugins.Feed.Guenstiger.Store", + "Plugins.Feed.Guenstiger.Store.Hint", + "Plugins.Feed.Guenstiger.ConvertNetToGrossPrices", + "Plugins.Feed.Guenstiger.ConvertNetToGrossPrices.Hint", + "Plugins.Feed.Guenstiger.LanguageId", + "Plugins.Feed.Guenstiger.LanguageId.Hint", + "Plugins.Feed.Guenstiger.NoSpec", + "Plugins.Feed.Guenstiger.NoSpec.Hint", + "Plugins.Feed.Guenstiger.DescriptionToPlainText", + "Plugins.Feed.Guenstiger.DescriptionToPlainText.Hint", + "Plugins.Feed.Guenstiger.Generate", + "Plugins.Feed.Guenstiger.ConfigSaveNote" + ); + + builder.Delete( + "Plugins.Feed.Shopwahl.TaskEnabled", + "Plugins.Feed.Shopwahl.TaskEnabled.Hint", + "Plugins.Feed.Shopwahl.StaticFileUrl", + "Plugins.Feed.Shopwahl.StaticFileUrl.Hint", + "Plugins.Feed.Shopwahl.GenerateStaticFileEachMinutes", + "Plugins.Feed.Shopwahl.GenerateStaticFileEachMinutes.Hint", + "Plugins.Feed.Shopwahl.ShippingCost", + "Plugins.Feed.Shopwahl.ShippingCost.Hint", + "Plugins.Feed.Shopwahl.ShippingTime", + "Plugins.Feed.Shopwahl.ShippingTime.Hint", + "Plugins.Feed.Shopwahl.Currency", + "Plugins.Feed.Shopwahl.Currency.Hint", + "Plugins.Feed.Shopwahl.ProductPictureSize", + "Plugins.Feed.Shopwahl.ProductPictureSize.Hint", + "Plugins.Feed.Shopwahl.BuildDescription", + "Plugins.Feed.Shopwahl.BuildDescription.Hint", + "Plugins.Feed.Shopwahl.Automatic", + "Plugins.Feed.Shopwahl.DescShort", + "Plugins.Feed.Shopwahl.DescLong", + "Plugins.Feed.Shopwahl.DescTitleAndShort", + "Plugins.Feed.Shopwahl.DescTitleAndLong", + "Plugins.Feed.Shopwahl.DescManuAndTitleAndShort", + "Plugins.Feed.Shopwahl.DescManuAndTitleAndLong", + "Plugins.Feed.Shopwahl.NoSpec", + "Plugins.Feed.Shopwahl.UseOwnProductNo", + "Plugins.Feed.Shopwahl.UseOwnProductNo.Hint", + "Plugins.Feed.Shopwahl.DescriptionToPlainText", + "Plugins.Feed.Shopwahl.DescriptionToPlainText.Hint", + "Plugins.Feed.Shopwahl.Brand", + "Plugins.Feed.Shopwahl.Brand.Hint", + "Plugins.Feed.Shopwahl.ExportFormat", + "Plugins.Feed.Shopwahl.ExportFormat.Hint", + "Plugins.Feed.Shopwahl.Store", + "Plugins.Feed.Shopwahl.Store.Hint", + "Plugins.Feed.Shopwahl.ConvertNetToGrossPrices", + "Plugins.Feed.Shopwahl.ConvertNetToGrossPrices.Hint", + "Plugins.Feed.Shopwahl.LanguageId", + "Plugins.Feed.Shopwahl.LanguageId.Hint", + "Plugins.Feed.Shopwahl.Generate", + "Plugins.Feed.Shopwahl.ConfigSaveNote" + ); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201509021536425_ExportFramework1.resx b/src/Libraries/SmartStore.Data/Migrations/201509021536425_ExportFramework1.resx new file mode 100644 index 0000000000..e07999aa4a --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201509021536425_ExportFramework1.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/201509031112324_Merge3.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201509031112324_Merge3.Designer.cs new file mode 100644 index 0000000000..b8acae3dd9 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201509031112324_Merge3.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 Merge3 : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(Merge3)); + + string IMigrationMetadata.Id + { + get { return "201509031112324_Merge3"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201509031112324_Merge3.cs b/src/Libraries/SmartStore.Data/Migrations/201509031112324_Merge3.cs new file mode 100644 index 0000000000..24ce9a2d5f --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201509031112324_Merge3.cs @@ -0,0 +1,16 @@ +namespace SmartStore.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class Merge3 : DbMigration + { + public override void Up() + { + } + + public override void Down() + { + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201509031112324_Merge3.resx b/src/Libraries/SmartStore.Data/Migrations/201509031112324_Merge3.resx new file mode 100644 index 0000000000..e84ddc5393 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201509031112324_Merge3.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/201509150931528_ExportFramework2.Designer.cs b/src/Libraries/SmartStore.Data/Migrations/201509150931528_ExportFramework2.Designer.cs new file mode 100644 index 0000000000..1ff305bd2d --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201509150931528_ExportFramework2.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 ExportFramework2 : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(ExportFramework2)); + + string IMigrationMetadata.Id + { + get { return "201509150931528_ExportFramework2"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/Libraries/SmartStore.Data/Migrations/201509150931528_ExportFramework2.cs b/src/Libraries/SmartStore.Data/Migrations/201509150931528_ExportFramework2.cs new file mode 100644 index 0000000000..5bab264538 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201509150931528_ExportFramework2.cs @@ -0,0 +1,276 @@ +namespace SmartStore.Data.Migrations +{ + using System.Data.Entity.Migrations; + using SmartStore.Data.Setup; + + public partial class ExportFramework2 : DbMigration, ILocaleResourcesProvider, IDataSeeder + { + public override void Up() + { + AddColumn("dbo.ExportProfile", "ResultInfo", c => c.String()); + } + + public override void Down() + { + DropColumn("dbo.ExportProfile", "ResultInfo"); + } + + public bool RollbackOnFailure + { + get { return false; } + } + + public void Seed(SmartObjectContext context) + { + context.MigrateLocaleResources(MigrateLocaleResources); + + context.Execute("DELETE FROM [dbo].[ScheduleTask] WHERE [Type] = 'SmartStore.GoogleMerchantCenter.StaticFileGenerationTask, SmartStore.GoogleMerchantCenter'"); + context.Execute("DELETE FROM [dbo].[ScheduleTask] WHERE [Type] = 'SmartStore.BMEcat.StaticFileGenerationTask, SmartStore.BMEcat'"); + + context.MigrateSettings(x => + { + x.DeleteGroup("FroogleSettings"); + x.DeleteGroup("BMEcatExportSettings"); + x.DeleteGroup("OpenTransSettings"); + }); + } + + 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", "Object", "Objekt"); + builder.AddOrUpdate("Admin.Common.Placeholder", "Placeholder", "Platzhalter"); + + + builder.AddOrUpdate("Admin.Common.FilesDeleted", + "{0} files were deleted", + "{0} Dateien wurden gelscht"); + + builder.AddOrUpdate("Admin.Common.FoldersDeleted", + "{0} folders were deleted", + "{0} Verzeichnisse wurden gelscht"); + + builder.AddOrUpdate("Admin.Common.ProviderNotLoaded", + "Cannot load the provider {0}.", + "Der Provider {0} konnte nicht geladen werden."); + + 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"); + builder.Delete("Admin.System.SeNames.DeleteSelected"); + + builder.AddOrUpdate("Admin.System.SeNames.Name", + "SEO Name", + "SEO Name", + "Specifies the SEO name.", + "Legt den SEO Namen fest."); + + builder.AddOrUpdate("Admin.System.SeNames.EntityId", + "Object ID", + "Objekt-ID", + "Specifies the ID of the associated object.", + "Legt die ID des zugehrigen Objektes fest."); + + builder.AddOrUpdate("Admin.System.SeNames.EntityName", + "Object", + "Objekt", + "Specifies the name of the associated object.", + "Legt den Namen der zugehrigen Objektes fest."); + + builder.AddOrUpdate("Admin.System.SeNames.IsActive", + "Is active", + "Ist aktiv", + "Specifies whether the SEO name is active or inactive.", + "Legt fest, ob der SEO Name aktiv oder inaktiv ist."); + + builder.AddOrUpdate("Admin.System.SeNames.Language", + "Language", + "Sprache", + "Specifies the language of the SEO name.", + "Legt die Sprache des SEO Namens fest."); + + builder.AddOrUpdate("Admin.System.SeNames.SlugsPerEntity", + "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.", + "Diese Option wird in der Vorschau nicht bercksichtigt."); + + builder.AddOrUpdate("Admin.DataExchange.Export.CloneProfile", + "Apply settings from", + "Einstellungen bernehmen von", + "Specifies an export profile from which to apply the settings.", + "Legt das Exportprofil fest, von welchem die Einstellungen bernommen werden sollen."); + + builder.AddOrUpdate("Admin.DataExchange.Export.NonFileBasedExport.Note", + "The export provider does not explicit support any file type. Therefore, the export provider is responsible for futher deployment of export data.", + "Der Export-Provider untersttzt keinen expliziten Dateityp. Fr eine weitere Bereitstellung der Exportdaten ist daher der Export-Provider verantwortlich."); + + 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 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.", + "Mglichkeiten der Filterung stehen nicht zur Verfgung."); + + builder.AddOrUpdate("Admin.DataExchange.Export.NoProjection", + "There is no projection available.", + "Mglichkeiten der Projektion stehen nicht zur Verfgung."); + + builder.AddOrUpdate("Admin.DataExchange.Export.NoPreview", + "There is no preview available for this entity type.", + "Eine Vorschau steht fr diesen Entittstyp nicht zur Verfgung."); + + + builder.Delete( + "Plugins.Feed.Froogle.TaskEnabled", + "Plugins.Feed.Froogle.TaskEnabled.Hint", + "Plugins.Feed.Froogle.StaticFileUrl", + "Plugins.Feed.Froogle.StaticFileUrl.Hint", + "Plugins.Feed.Froogle.GenerateStaticFileEachMinutes", + "Plugins.Feed.Froogle.GenerateStaticFileEachMinutes.Hint", + "Plugins.Feed.Froogle.Currency", + "Plugins.Feed.Froogle.Currency.Hint", + "Plugins.Feed.Froogle.ProductPictureSize", + "Plugins.Feed.Froogle.ProductPictureSize.Hint", + "Plugins.Feed.Froogle.AppendDescriptionText", + "Plugins.Feed.Froogle.AppendDescriptionText.Hint", + "Plugins.Feed.Froogle.BuildDescription", + "Plugins.Feed.Froogle.BuildDescription.Hint", + "Plugins.Feed.Froogle.Automatic", + "Plugins.Feed.Froogle.DescShort", + "Plugins.Feed.Froogle.DescLong", + "Plugins.Feed.Froogle.DescTitleAndShort", + "Plugins.Feed.Froogle.DescTitleAndLong", + "Plugins.Feed.Froogle.DescManuAndTitleAndShort", + "Plugins.Feed.Froogle.DescManuAndTitleAndLong", + "Plugins.Feed.Froogle.UseOwnProductNo", + "Plugins.Feed.Froogle.UseOwnProductNo.Hint", + "Plugins.Feed.Froogle.DescriptionToPlainText", + "Plugins.Feed.Froogle.DescriptionToPlainText.Hint", + "Plugins.Feed.Froogle.Brand", + "Plugins.Feed.Froogle.Brand.Hint", + "Plugins.Feed.Froogle.Store", + "Plugins.Feed.Froogle.Store.Hint", + "Plugins.Feed.Froogle.ConvertNetToGrossPrices", + "Plugins.Feed.Froogle.ConvertNetToGrossPrices.Hint", + "Plugins.Feed.Froogle.LanguageId", + "Plugins.Feed.Froogle.LanguageId.Hint", + "Plugins.Feed.Froogle.Generate", + "Plugins.Feed.Froogle.ConfigSaveNote", + "Plugins.Feed.Froogle.AvailabilityAvailableForOrder", + "Plugins.Feed.Froogle.GridEditNote", + "Plugins.Feed.Froogle.General", + "Plugins.Feed.Froogle.ProductData" + ); + + builder.Delete( + "Plugins.Feed.BMEcat.TaskEnabled", + "Plugins.Feed.BMEcat.TaskEnabled.Hint", + "Plugins.Feed.BMEcat.StaticFileUrl", + "Plugins.Feed.BMEcat.StaticFileUrl.Hint", + "Plugins.Feed.BMEcat.GenerateStaticFileEachMinutes", + "Plugins.Feed.BMEcat.GenerateStaticFileEachMinutes.Hint", + "Plugins.Feed.BMEcat.UseOwnProductNo", + "Plugins.Feed.BMEcat.UseOwnProductNo.Hint", + "Plugins.Feed.BMEcat.ShippingCostAustria", + "Plugins.Feed.BMEcat.ShippingCostAustria.Hint", + "Plugins.Feed.BMEcat.Currency", + "Plugins.Feed.BMEcat.Currency.Hint", + "Plugins.Feed.BMEcat.ProductPictureSize", + "Plugins.Feed.BMEcat.ProductPictureSize.Hint", + "Plugins.Feed.BMEcat.AppendDescriptionText", + "Plugins.Feed.BMEcat.AppendDescriptionText.Hint", + "Plugins.Feed.BMEcat.BuildDescription", + "Plugins.Feed.BMEcat.BuildDescription.Hint", + "Plugins.Feed.BMEcat.Automatic", + "Plugins.Feed.BMEcat.DescShort", + "Plugins.Feed.BMEcat.DescLong", + "Plugins.Feed.BMEcat.DescTitleAndShort", + "Plugins.Feed.BMEcat.DescTitleAndLong", + "Plugins.Feed.BMEcat.DescManuAndTitleAndShort", + "Plugins.Feed.BMEcat.DescManuAndTitleAndLong", + "Plugins.Feed.BMEcat.UseOwnProductNo", + "Plugins.Feed.BMEcat.UseOwnProductNo.Hint", + "Plugins.Feed.BMEcat.DescriptionToPlainText", + "Plugins.Feed.BMEcat.DescriptionToPlainText.Hint", + "Plugins.Feed.BMEcat.ShippingCost", + "Plugins.Feed.BMEcat.ShippingCost.Hint", + "Plugins.Feed.BMEcat.ShippingTime", + "Plugins.Feed.BMEcat.ShippingTime.Hint", + "Plugins.Feed.BMEcat.Brand", + "Plugins.Feed.BMEcat.Brand.Hint", + "Plugins.Feed.BMEcat.Store", + "Plugins.Feed.BMEcat.Store.Hint", + "Plugins.Feed.BMEcat.ConvertNetToGrossPrices", + "Plugins.Feed.BMEcat.ConvertNetToGrossPrices.Hint", + "Plugins.Feed.BMEcat.LanguageId", + "Plugins.Feed.BMEcat.LanguageId.Hint", + "Plugins.Feed.BMEcat.Generate", + "Plugins.Feed.BMEcat.ConfigSaveNote" + ); + + 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/201509150931528_ExportFramework2.resx b/src/Libraries/SmartStore.Data/Migrations/201509150931528_ExportFramework2.resx new file mode 100644 index 0000000000..f24f629459 --- /dev/null +++ b/src/Libraries/SmartStore.Data/Migrations/201509150931528_ExportFramework2.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/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 + + + 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 + + + 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 + + + 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/Jxq8jl9EP5IehySbwJfPfZ2IhOUlQuiJ/zZCsqeOKozy7SW7rYhwtOF2KpLReZ32qgolbO0cl1nKn2Y0qciNMU+27ERg5eTkiXI6k8aMWwIHuSPFfjcGHI105lHCoqwD1e57jKYvdc1SQkM4uJ9MrBtV+qVOp2RCJKhr+UOdhfTNNPiKjHBkTrZo0mdtjtZAtTWk+ySuk/DCpIJ5aPubQ1Or6NPWaQRsjy6qwxOp2JFns5k8yj1V5JMryHdqk+ZPjrQkexV6jydtqVyVflbaMUAeyek/LZrLDuFv/OZjQznm+etYNsTcwCHkJlDZTH+ESpiFthtcgKV6ranNZRFm5TuirVSGmAsI5vk9EVRcM5rBJbTxIga6am5xdGhwgB5oe2p7BU6oBBxfmYJZISXKPPjEZIT3isVyiulTpjm5kF5P4RfWqBx72RDIYYUckBfQLc3308/iN6u+NBnlbf02PH/nVkmpyhWN6izpIsuv8PsGEXfTi9mnZLlWd8HoG0gRyh/q5oEQewrwzx/4O/0mWxTncjx33NE5P9prwc/BBfrm5KZFnxDkNxPJDcRhV8d1F8qenZ/0MC3nzbMf2hKmRxxzpK8029mOwa2F4r3ZQxHchQm1Irdp+5ye1xQbjCL5I5WWP8demtIZbMOf6yMaSOtflUBJTMoBzvV6zpqQmr8NpeZJGt2U/q753qmQtBbNCsXRgLZ0+kbWMEdfxtH5C62tUdEonza9fvqBPFf3+8ieBBUawxDzC5isiue5RX+mNulKzLW1hgdDLZkJUk6TYLHtP0Oh4RN7SghN0kRDVeUYvYplN0yfckWSDu0tS6ZMBjiq/MZmCg7LM44SSsFvf6BMAjaLGSzMVz6vu5UOu/8fZ6kUjtspag5APnhGoAl4X6IgwMbFI//7y/xKGb9pgrxCZBrshcI28GY8JN/Ile4fIGvbigNpc5O5SGUcrUW1iiq7GX1qhIRepsAVVYvZIskrcICZZjOcttRkKh8Rwn0k62TfHl+D1AWVki2gzhyb9YN/JFPvTN8sRU0e7314zzGrAw9i+WnV9M2RgsIqUe1loa9aFm9o9vlWOYy6mVc7bTnAstuXbVQh4mrmUcq26GsS5fA0bxtW0BjDv+KVqZUs2xMrTVC/RIyiQFDlZpy2GP0K4U6IKdn0G6QTnYDcEEvf8ICsfUHFF+UTFFAycjM8aEFtuYxED/AYx8HbwGtDxmbgNmAuTlgn8Yrx2gffTJFSrybh5hW0vbIPFFVodEZcMzU8j4xJ9VYgjx7VsuNKgPWgZoIU6I8mGYnjXh6ivEm9Qrzr00k5D0CBdWEArsoAtmFNiealVjmAG2VXOkUn7bZXFhLj1dWqZkYOD2LAFsWFAHqs56/306pXo2HFiIUkfZmAeCU13iW3Gqkc3zWNpCctCY9wAIym1ZHh2AvszI1OBtDZpf1RxMQbrH8saDmNkHCCCQqw1PO1lzlsAZoCxzJjWZeyHSUqOfbsGtN0cwwenAofenBSidDlQo32Fe3ijTddfvoKKHi2sC1mEZhQOhe0zoHSjmEFj6ebLaD1kHiNcRF8dpvktOcfQO3gESIgvOyAbhhQR75SzR9r9GVhQOic74fQhvT/K1/TIvGccFZfwwDIObOFsmVBAD/ChjMG3gw9lI5iJFWXzY9J8V2c5F2TzsM5ZQl/jvGr/l3siQXDQITmCtHJKwm1AvkkY+fJcqR7CHF5K5TwZOSubKlvDmO2zXaZMw7+SOwVjci/rim1sP2OOh7AAY47nyYgxhwexl9nrtg++anUlDwjuaFoYq60Mj9dcM4ZznMg6MccORELXXdBq75Kyyc1+sMGTQlIttqNRnb+oKkFM1cHbMJWyDWiPbMa4XqRpJd2GMF2V6cjSt2Cu9gPQpPtxjv6okwI1Ib7aTkO1bCgTdL0y7R9AVwBOP4lOas2IdDPoOCMSmfSjq7+0HXeF+Sa5R8UTvfevsbBGwAobzsF4G6OGeI3t52TLp6o381lhIJ2NuIqptzRnHdbZKkWnFVofVFWRXNcVam5NXQ0lOoYzwaHgQ2l1BwY16op87WHGvK2bD5sRzicLNixg5Nnpa22PgLRDMdxLy+oZCYIX53Pt7eAGWzeWJfgankVzXl564y0OyJ6R52NhF+YNaVrIu7II7+2go6dtv3c39FteDRMIFRTc5uL/kTZjsU3fGkUpHcV8XCqdL5MudHW2hksNdSIPPzGP7vBSLhvDAgy6g0qUvP9yE1Gna6H1mEPAEGOycDZcCeJfwnuu6sgMfKWi8y540VumHg1jxBMatSStqNCCrjynbRLgP1VbW6MXtSOaT0Fq59OkK2y9reJswxUdqjMTP+/w6q4ax0IMvIOrfNv+xQbFyU3SpCHp3WimDKyurWBluKIDU2t6sIPsbTai+RjdbI53geXhkXyhL8hdSThSxn4OuMCbhQo0VhcNHboDXeExEsvlRcVjuDMIjgdvmPQOxrClC4mSwd10u5q8iy05ym4Zyxos+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+8EdE9IvtbZkvGb/TYb+bY8Ma5yRB6Yr8xe3HuCJ7bjjKs5vkti6Ak3IJiM2FTIKsu6bC3cMcFdkcmZV1Wp1mN8Lp2/DdgleapGq4DyStmmByCqVbo1wvnrI4zEWkAZHLPSRV7YlOO4JdQ7rI6yJGQjYM5vNSV5roLeXHSkQ3KrAdqZjGmv1uey/mVHhHYvhsi+uiKiSXPrsSW4yHeZ5C+JrvNtZgFoN721HB1qiF40di6LxDmzQP8NgSj83lPFSLYhot0dp6wD2wG2t3nb8hF8q0OS3p/WKOFYevtk6bf/KWEvPZYmfSzy5kDoqlu32C65t35ENVbS6LKCvXCc1ZCtFMBuPXir4NWwOy2RCLYZF8me2RBnz+4374Q2uCJ0DjkqWPXgg/Jvfok5DtYVRgJT9CqEH3bcvWrCB+ghEq59Vq7yeg5MWiW2FFK0r2qNDWsSb20u0aAP7zPsGjk940AMptFtpWfbWswK23XOECPg7vfati9vHECtY489lqjoheFRwL7Hf7GW/cEWIMPFS+lF/hy81NibilpvtmeRIPnL9bxStEVXx3kfzJ8TDz2WIGsDjRJHFjuvdfl14+j/L1hr5CoLIipEAOxvNBEd8JJ6hiqQXmFEUZn+Oz/zj/kn1QlnmcUMefeOENFa3p2CQLvmITM2N6Ku63aWoK99k4eBYcsBdWItsqmrtqnCCAtWC01gvIIZYlZO575d3hy6i4RdBm3KjDLC7Lzv72GuQHc5a5uEvon839vyusW6siIe+uHxFhb+LoFa5Kg9qCg3JUpwEU8r8BU6Bvy5NvuAYCcI1Blz05p6He3EzTMewVo6tV6QFFaDEPYAPTvfOop62I1XP6e4QBJh7onN9Et3gWm+jDJCXGa/8Mp8Fs81VkU24z12OcnjTlkIWcdg61H2Nadwyb26uEzOeL0/Jznaa/v7yJ0pI3N3Wj92aed0lJLcarg80mTUiEapvcR7OoqOvxbNRBd4mDDNhJ1YDnXPWoA3CTspue60ZLrLn1iTiksy49hxVDDLVk7HAGZ9kwInOHfat5oe+kHye0aOZmhK7Z9v/L6Fa9PYHAJWmfBhiTjYiI2Hf34URQ484FmW2MaTFLwnhLKqshsyMst6Aw+m20IZ/LVvMcPUTF6ixPsqr8kOB+4PXna4lW35Pqro2UVOX00lYWs3gJVQz4QtuQ5wyMcQXgE32Ht9H81JEhnMLpNuE2exehTojNC4fUk494bCE1Do97GxlIP349C3W+VhLDFSUZKniQ3pnbfun/LrsPbe4icgyclkM9ct6zjihByk0UU1fNCp0kRVkRTruOStSAvHzRHZJ0x26tZ+2P9Cht3n3uAD5FWXKDyuoy/4Gy31++/enN25cv6NOt5L5eevPyxeM6zcpfYzqNUZblFR367y/vqmrz6+vXJW2xfLVO4iIv85vqVZyvX0er/DXG9fPrN29eo9X6NV+9RWuE5ad/77CU5WqUVYc5NO5CevNNEo956rf/QAIzdExyjm5eyPjpt9d8xd8AniRt//4yISSl4kwfIKN5HpuTUwKFaC9fviBsR44Ae9Z7rUTPHmk2zWT3URHfRcV/W0eP/53FVxXiK0lCb7M4rVfoNLtIMNpo0yG9JgdNll07LbuQGlxQoZi+CO2ObojPCTDOy6RKw1CsufEbANEnVEXtdZIyGMJR/rtAOMPRTrhA7M4d5yjDiuqg/J6syNLmganB8M88CzPGBt33Itq0zw1J+maO6+IufxjNgfsoD3NiBHnKZZ9DjFFzljjocMi+2Z7i7KmmWutHj7Cnbmd1P6T1X774FD1+RNltdff7y7/99JM10vEFMtMpNZ4FbA1VwgWB5zsDbx1moH3OUCMJZtZBd086+DzSZ52SP1FvSj+TGR3ujjCN8Ibpr6d4sXn8/eX/Qyv9+uL0P68EelyR2JK8zlb/9oLK0q8v3rz4f627wz7oG7xDf3PpEH3Oa3hpluf9MD37mfTMU5H1PZ2qk2+DddJZ4s3FteWjZyKlWr37xmUuWhod1Sl5Tkaj163Rf82SP2p0gfLmlT4Vclu78CSNbk/XuOtdRK4S/S8/2eI/r1IfCzGgmc88qeeOZGITp5Hp5rLaOSoBD9bOyp3HctTX7PXnTw7LT0fPSQyvDnlAA+y0JGmNz9L6Nsk8Nn6n5WVex3K299gW8fGFz4RTjTxinh62McP98os16n4/6ofYeK7lB5XPap695+WkQKjz8vssNJfR4/EjWm+8/FEYSbtgNbfywPXKRIl02bx8PLqNMFD+8cDjJlI+Si5P02fC8IsvwaE1a59CKYCzNYh5SPyrX7IPObnYduvF5wdpmj+8r1FZ4fX7W155IXOzWiGfUFSQo0BEsxU0eEgimioh82pH7+NsFQiT8x7BSgccZOUDf3S+s5qADMheCzS1tkQDfK7X16j4ckNko/Rh6om3dH2kV3uE8zwYiH272I6JhppejHS6GYXGmG3lzPZLB5tNkd/7LQTjlKVy/Wbm/qHZvpyQWbPpM+PPJh1u01BNvWgJRXmTEGG3nYc+YYjSRWbNcm3C3bBIZTEPofCe5MU6qmwOiOS4LqK0Ct3Pg9U6yY7y9Zo5N/eLbymD7MkObm6SNMFc7kc6/x3ZO0SvL49QyPW3g4Zud3zdKzYTbfqUXfZjoY9RWZmtM2/skQddIUhPP+a3SRbKpMb4KIdh3WuA0qiPwN0tO8tBQCAzIExGCEVi2nVHxGDeH+NlGbxaurOrMg0lNdoCmDJpMGTgGuyEiRz2RlmYwL72qrSDld1V9JIRjB7RQN4sdnAacdV9enLEBIl5EbSVpzchkb0NguyfyeYsL6sohc543Txad3mGmm1yEHwn0WNAbB7rn/mmBsrTsLPac5LIBepnLNul1dthWQY5+njIm3dNTssJAh4u7wqEzPH/bIsfiwgqkpjD7eRtbdIJXubfIi/recHAhyWcteP8JX8V2XeK1w0c4R+KR05vM0y9ozty1WwSDmENk2fCINNZiguuTdfXBbpPIohBfbffuxANdpjmt8QmfCYsuvjxs9ntIzNnhsnVLXO7qfVTesU3dscWLa4j1unmooU/51VolEPGDM+VZjsPolXX8FSL9dbdyPPtbMCo3Tm2iWdQ2p2dVbPtaEgLvqczBe4CvaxB7zIQrH4YvyVlgqGxgk/uk1X7Tqs7b0xiFF/c0YcmwkraCYYOjTP4AVfHOO17z35THS4SqsMRanex19Ijy6OL68WGNnoIYoCQJwmz24t6Hcj6CIKvQ3aJrcyUG6xn/0Kh7P09B/G23Jm5+FEHF5Eoq2+imFyTKvAKU8EuZd9W3leJesvogPO0fJ/cVEdR4bVn7HD4r8/n6I86KdCX6g4VZ6OMhK55ECi+YanXnnnbq5yabPWqJCZL/8FqxTXp1f3T8l3+kKV55Lelb3H4Tc3XLG2EsEPnNbJP3cHLlxsBn1PEY4vk+HGTNK8xvoueZBiNHH8tQhoyQBH6c/eHqLyISIr9ELM6xuRwlMnV9znLxAMjgWQHtwVCrO3mMq4Rokv0GCrS6RzFdVF4nuP0SI6e4hQ1asNP37H4zlCR5J5i2mOkSzhF6yVYp/T8q3+HxUeXhbpHhJUszUoWpR22xq/e76VRnKyjlCTswr9Kmnnrzd/xrpRcRMXLpEPXg4TJnZbHpRcJmZwtfkyCDRbieszusYhhZM2xkufmqMrjH/+oo3aD76HJmz0RxXdwHyW4bpIyOD2c2mAfnVavJAs23o/5QzPWNjbNbxqwDZ/cPNF99EledP07RHhb5IP2MIp/0ORIJL+eZ5Qn2aIRfKcNDfEWod9cepkUdPnCM5Os63WIiWnwRY+h8NGBo1WLKkHhTVHM5gT4sH46rKsql12xNhUZAv09Ke/SpKz8EbaynCLMl1gxj3weTl5UbG9TVEns5YsZIQi+dHxJV9M20G4zjsiZ2lRtXGwwnih1HojR8QfTRn8UcpmsQxxisLjbo5FAmDtX03FWoaL05sVWe42wookZqNVws7aJ9wuXCWpk10vn48UTldVBVRXJdV2ho3x9nWR0xzMps+L+dw8BlO1DAD6j+I6S27vpxHe8TQmO/nuymhD7h2lp069KoXVOjziswgl1YhAmVmTi6zbbcuMQHHlyj4onMqv2rpdxbR/HS2eXfs0S/hzToB/j2j79OIxK1C6v3o6CHtcnFJV1gUjvlKayfQavvomDNRv/EXqZ6JshP8ZNOXgkD+tslaImNyzgFvM9CmjQn6HitELrEG6aEUJChpD4Lu7yxvODtfYE4ZTd6v5M4ie0UQUu+cE6Gvn7xb+WZFpjPDjPc/X+0SkBW3DDoWvKXYPscHqS9oyrPMrrTZ6xlwKcdvICllARsd0k0eNjugr6MeqAhyzdFn518+s9+5zcWqQk8sgWsRG3BI5yOcwrbIEFj5EiD42EFpSO7cIESu3DkUbhhmHObM4SGmDikOKoq+hj4JNotYvkT+QzglFwVnmZX+DtY1zxmN0eQ2lwfBmfIoTKn0Nt+PMou9X47B04JGBMYVif1BZGbu2EzyKUg2abfR83UZ1W3xL08MntXrd5FHujup6JMdSO5jDJoiHJLabaNf3gtnLhCSR4NGrJXvpRTpKnZzor62eXkMPP6MFHgk/LyyLKyoSPMzJYCns5uGKQeL1IoZYs7y7pr4TZ3gh5ZiIFXuEwMYq6il6376azybbhrmZnTrS7/fXzSfXYjczBdd/X9J05nrTndYoCP194sUGTO6jaNBvwXW3bVacL5wuB7ByVFbbbqe3JZnqXYjVzfHVIz6QR5U5TxSCOngg7NDF5oZF3FJZslDyxt0a+F4WPH6siIi+vTmlXsgdwz0SlaR1iv9hbgUd5mhcf0CP4Aogv8nbxap6HCXy+N/HC2Mp+cyBFTqOeCQ8tbE21B3yuneCqe3UlUCBzPOXpsvywa46Lx4bOrQnuHScpZvM+5MvLo/QhWaHLu3p9nTGZEl0QtbfAl/duPTPvk7tK7hmk4ZdnoqGH8bnqx6aul3LsSet5EaBDQ80LP1ynJVaqzXmCjQDZstczO51deL2XnIuZJPjqa/o9uFCeYDVXD5d151e9thzIXjXfc2EILmQpat+Hce2/DDc+QzacJFQleKQCQ/d9rEbYWI19fIW2d7sXX7EPP/hrbQDpVb+bJKaE7HcYz2SJWthSgmnb6ASH/CNKbH4bVKIIm32/552DJjyrOzr3j06f0iZTEfSZCAA8xFC8tzXvQC7CKM+ERXZmFlqlchndPmPKW26gbIn3LSqSKAPvfz8Tok6QrXCKPILzZECc9E6/+y15E+zOl+SN0hE435F/tpdlD8oyuc2wBHab8wmy7ezvgHP+S/8HPBfe4Qynyv+5DvNyXrA0YXRP86WuvtxQlHSQUzwkw/LDM1lDpwiNUrg1w8QubVFcFCTq7SWQ6Y5aedPumfCir4Lr/u/pcvUpohGdgwa8YmfZ63ID35Zzr0NtsUneC/yJycPpnRS2Sdbss2Yxu4+sKvKUYPI7k5GK6Qx88jb4jZPnJsKTXMAO6LbwOnDzVdNNqPDzmGjJEJ21IEgrWWFIPb4cx5o9CJ3mxcUfdVSg8vz2OnQ/m63r6l94mtjU5OHT4NFN+AwNOca6TbLMBGFp/ZIj9p025L/SfkyyH4EegrLf5JlfV+meC3geerUbzvs66Ruqs+SPGiUU5U1C+DHkqfjXsn/o4GvhFegNoPF7X7bFF/JSOLFDUZOXK1TEPXgT3AnZ8SPuWxkqGmh/mXyZy+R99t5nopKWjqxQXps0C9iRX3E1Cage1Q7hjfYZS8jk08YsjRVJ+Yzu8j2nl4npPbIwqEjuuDCYgoQDbmd6x7/S+83b8a4vcKL+14zLNlbXxI4ssig9qKs7oiKbaKZzFGOCPRMV3i3K7su5pwo/XjPXYr2tfjJbp8wGLyDa1q0QGPsXwluX+Q8URkIouoM4RmUZDin+8x7vmguvzDHGQneSF/X6jDw18zwk7DLfJLG9eLXV/C4/Li3cRk8rmu2hzw5WK7x2hs9Ku2uXEKh8UO54JgJCB2TPom21HRcQMo/+LuY2karaLHKIQPncaGKvxz2/+b7v/TEqK9ILTy98i0Uy5Y7YaHInz53Qbmqg56R83hd5vXHUQG3d4Lc6/J/OCLz5+tyuOl6yHEKhENmDjKq9WpkkDm831RMVy2eio3ZCPTw3dgvoxTPm3IZ4z4Np6VjgKzpung2CT32av9yB2GL7hzYzbrs3dsjXxdX3O2FsE78690ZA4HNISGte4LZqvieOwwqBq028GxBVk8M3cFLnjjuP6qJAWfwEvZvliLhBeI6VjOHh69+dpfIyemxXJf8d9rdIct/QXZnhTXuFZSE9zeIUd3WyWL5RY8eP8zR2SRrrE5nPNMJRo/OMtNUN84ywbWzWkeGGLITVvrGRGsOKPyFrQ5SeIDQ1TeUtT01gectTU7vFHyYpO+WTyRnRJ8WxeStUdUzWBLbUa2ztrCZ+J+EcPUTF6izHC1z5HRUIs5Zf6MjRHYp/5PUQXh161yg0EOxSbGcESGKUbINMbm6SNPFOhNdb/ZsgY6QxNGQbQ96zKBDWI0d4/sd2itO0YyykdpiJIF0KZpgK4/NMdVj+QCsZ6bx7enR//zYYsuPHTVLQndOnPBvyPATE+18oCjN2li/fJVi5Ve/Q9fDAuts9kh7NQUzXgw95ugo0VyLygIzAID+Msh/BtlIc3mAixuI9PQqNsn12JzTa0+so0ILUamhqFLTxf2FkosbGXpH82bzsTOLmo3ic1G4S9MHYTdbAOSqZO/qe2mhD8qiEJ46IOGCv8b6zt4nCd/2sJnVLFNqdehYloSJ5u51is50JQ9MWJdmrYCHc1I3LcBJf19TZUrf3QIPdLpyjdZRkTCZLh3xIHyJyU6rd3H7Oqz7/3wRHJd2u8GuJbfsPCZ6uZ/NiwuIPB9Kq9q231fwCstyFxZh13ic31Nh/hqzTDc1+8oaaXvP3tUSr70l158hCXHXvrowelQvubpmTWZ8Jg3bmDDPLTu8MyfD4HBh2pPY/LJrYzXdadl2lyfAizxQNHTK8NdsE3DCc48FuyN3gYJZajzHcnZELlBG7OlQPG3ThuvcJlSWTENw7EVU3I9Q283QNz6D8esF+JtqvH0/QOJYFzbWzZa+0h7gHTnJQ0rvgUx839g1Nfro4x2hmGcnUp6HdfqlZr6cm2bi1qWk32ZFd+KO6Lj+N9z3r07JDFcQw+ohZPBuy+ng9OU60/vdpU2jTpzcdXzWXqvYjchEzDI/aLfvYRnkuiV8WXKApFXXsYJPI7TLvTki23XhkvZvPy8uzeNRxQ1Sv4FWK4TBKo2zIwuNkvpWTR9WE3ISFPQxg/R9Xnt62KxvnifkzQHfJhh6hPg/JW1CTXxZR/CPJbgOeAdKYt2mNEnqQh0KdNHbvWQRCN8cq1AnAM/Ji9ENyut/R1AxwPiX6jSf3HljYHlVdZCTxO3o2eUwCXJcKNHGL2z8h/E7nKCrz7CQvGl4JY6S3HIfo/tdgv++C1DQEweqeEZcHzvM5nOjmhmx8wqA7WK2TTBIJxucds84+M1IUIW6FbUu0icUCmdNQn6OoeFaLpL+2PIuK1nDw8jw1fiG3Y1a2rs/RKjvJ/seri+v/hY89prssgaUTFSRXfbjcuNOsnjun5oSLLs9Ez0FH55YPsgIb0218goluZQl8K8QhcJ6Wl9Hj8SNiRuqCBiM5wtN5mxdPnoEr8z0zZU0p24cf3UXzOb0vJAzOYdEUUUyb7cpFZexf4AkrgRaujrguyEWvNu77eR218KOzlx4RQ/DY7K08fPvf7V1bb9y4kv4rg3lcLE42szjAYpGzgO04EwPJxGM7Gey+CEo33RailnoldRIfYP/7UtSNl+JVpG7Jy0zcLBarip94LVaxWm8EDlfPuxSxSbydeqBmc4uKJJc7FZhN/PUJPuE26jbONENAoJkaPOgX3uTYipElVRKnjjc2bO3FP2kgFsc/vcsPG/nSKI2ARahBDwoM1h0o2PQp6hIhWf+6EVg2bw/bnBnKRexLh0WsdmXsEujxOquJLcZS4y7ezmiDNXmHvqJ0bKC4vKhMvUWMONaZhLy+ATiZRNH/zSGKfsDx0ew58wH5SpeIkYqKAhW++M1yVmmUc7Go752y3biT2CYR5Nu4FEKScJt6Ty5FJLwv/eh+I2MQrZLX9yGzr12gBA/WG53R8b6nWPTcFsTrvBuyNwLM8VeZb4r86A5DtvbIfBvuYtB1x8XJDZStw2O2nfIOxSNvNto9/OVzEznGE7P+DePS3c/7iHEbGQWcgzr7iS3t4UQo2AXS/e4J7c8peojLLxvpbe1O9O8OO9EL/D2odx5/d8nflWfX3081xACPN35Lbs2enF0ITE1mixof/8e0/x+edu8Grf+Bp4K7cxa11UdlDMfYOn3Iroti3CDeiuSadlymkcNmsc6yQo68XWXpGZB/XWejpcEs/Mvy0tEyZ5I+0sP+kghEw8bbV3lTvk32e2QRWAq6Mc4P9ahxi4odtZ5w8LDrOElOakZre5d/+4QKengr8m9fu18kQ38TNq0eOXyd56abSnzbKDhuW9PwCOLP0G017vLUxWWDqT1q4XWDV25piFXTx+InoCYE1H16Pnhn6uOu1yVAt3n4rLp/k93WvC59YA2r0CRJ8w0KzNg7TyaQ27izDevjLGOwPTyhI/oUF0nNaiNIIzrZdKdRKknLAc6EJ4gQ/lrLmms4sBDOGwFJkAkLuH4bPwmWqfV1OMCldm6DrhvHYvgtnRrV2/bhXX7Ib5NdHQF7Gb7cb6tjepnvqflr3P1qcwPZPQ7/A1Xf8uKL7765LZJjXDyTT6fLYubyAAjiMvIxEmF5/R1rmR0QCZk9Vj6YmYWY5s73LfcfZSR0uNZRJeFjedu7N4dMw9cOF+/ymoGFUcz9Td7kxRF3GRW13BN751SvwOd5/pwm5dMcPvxeL76ACdpnLPrXeR1//jrb484cOwXarcHex+Rp6kYGoKWfQgRbT1/Guy83Gea/+7JF5yAPz8nnfbQ8uxfSFJf+7+Ps/BiTpXbxgI54zN7MkVOQ4eBTUucLEZOZjWYc+NFb99r2Zx9vt4/bYe9nF2+3i+9QSqza9PRGerifrl96WS38NoZL6GG4yMvyHqXpzx7004PmgyOdwXorZqd1kieZU8b8Mn7Vo4jKbhlUzLjP/jyjM9qT5CQXVRXvnjb0MJvSzX6DwVQe59mN1YoPqD7yonvWLfqXZj51uQp4k7h4kzS1xvi40VHpsS6fkywunp0O+7VfpMsTyfd4Cwg7eloxdvkSN/L93RZJXoyM6VU/8/Duc1wzNTiUt/cLzr2L+pCHEPQOndJnO2kt2IaQ+EpIOj2W4+Vu55ulyRselxG6vhf1cyvq86j/Hg8LD0UyMsoIZuLl3WqzhNk5Jklla4+b7lG2r4/84jTtO82jhyQt6UbmCTA1HjvN/t3+GrPdZurXBixvU5cM7/Le5oXM39zs5q3EvWCprOGisyy1kS1cGDdeN/dlav6ZgGq/Ro/xOa3w4EYQiGsF+O6u4uMpTg5buTuCPgtH10l40nNjZjTTTX+r7Xz1b35J1DwT+dEOll02Znjh1kyCzRtSMaL72Lgli1/G+fCwV6yXpvo4fK2dRD7DUdZL4HTDztgDr9888vp3S17GA8kf6Fv5DtUf7gY9HmDlvAZGkSxGjR6jfitTQbhmaxG1vhrsO8yR66fxg4Df/SDgjuLbbL/5czJ9j+LyXKA21dVGvg/dfOsSgC5seLu7Gheh3U1D3TW2GHqNP5es3NAw+xNGk8Lo5njCm3/c5mOymTdiQTD0Jk/rLMBBWGPT1yx9eMn64DP+QdKX5DRGhIf4CxpTv/EE/5CNW8ljFL9JULqv//L/Cqrr9Ks8e0wO5yKGrt4d3x+l52PWuZH74HiHynNa3WSPwrWNW0K0Jj4Rlq6OUGS/bOPrh3gHdP+c7X4AT3wTaw+WiC6fGy6Dxf/1l5vyI9n4/OcvD7jvXW4t8nOxQ+ArbWvxGl5q8egP1+H5k+o1wlhzvhTlNVUVqDp6YiIvGb9XYXT9zV1XoOr4NSJpYnTYWMCIxE43oyL6NDzuq8LXiN4wvMxz2QWI0TiO+yjwA4br7/Xy9DU6pfmW8na0622nty+PSu+n+RbIHtaNNyV5nbgbw6M5V/qfYQHqlu+jA9z49bTR5ajLA3CTu1EHvgZxBOwDCVTV6aGIs/KYkLCI460KcRzl7IW/quaYwPbhgLmHiNGlkYt5CXujKL6uovu4rKkBm3xF76ln44637lb39pZzzc+jkLmPQup/tZr7BrO5F731aFzkXxNslZB++jdlO0K1EB1zBevhkGe+XTyIG4wXX6tj/Gc9lPo6kOmw0Rz10L7nSzqV+fD4WKJRbmDkNn4Mg8u42j3dJ/8cNZPf4o+QxLJahIdCHfaURAq3WwK4edfiVe8FZjf2ZjZFcXa2WD+bv9JCRbtcayJt0mFBXSZcnl8kzsBgPbpdoI5+1gYbttzJiSy8xDPodbITh6se5Ei1TliNh+jmQdtV/W255WVlGRl3etOgU38DLVpOdjyDkcn3BlUsu7mvGSZ4VgOiLqmXQ992LEx7tW3KqVeZttw+l7E9SYtvJ8FQM0RPvk5KMvl1gSVcurLjYfyBtm059SXTmJ0lqarjvkpafks4DVVD9uat+7N0285sm1prX9LiW5/QtjVD9GTLvf3fQ+x0OSnvGxU5bsxtrTStKaUCdPI7ydBUDjlnjl0L206co9fAC5g9l7/mvc3T9FNeR4ScNbeASYbxkR8ZVvQiK7+5BFSj64bohNrNt8tXvVX7t/o9JJUY29TRi6hhaJLDzyhQAu6EmwrxeTcNvbSbmiHAcZnmhx8FHL76srbZbV46PHYeagZcHd2hrwn69halp8dzmjluRFfRsYzCzoubrvooUf6Ky9biIQ4OaUG33ptzRUT1N3c0/eRtuKkd7IebHSdvgQaZ/43KB2zF1AOrP3JLTjKsX5RlvktIz3bXKCRAeePsdYdK4pcWdVmJOPBfZ/tf6uXrkLaok+gepY9/G358f06r5JQmOyzCP359+Sv/yXzImoymv1yQe6/6NKPcxXvRHFiNvVQGQHJWHpCAle1fhCbxZ4yK5nn9VZ6VVRFjc4vffJLtklOc8vbgCA2Hh1rTniVf8hqdUFZfyav0NmmXzjUltt83w/WAzh6vXlCgMsBa8k8Sc5LItiKg0WKLKGNLtwExRqdV4Eu45aL33GV0D3wqVDfztZleFgsngZ7yDlMlH0sYBJCCSSYApvmdrqR9s0vcBYD1IS4OiN8kDsCQAkHV8T8gSK0BMjdANSelU4EzT9N1TM61pCzIyA+rn4KJGuuYdfsD1UgU2ranAqKkkVGQoPs5zBxp2ose0NIqYjQLYvLZ8MJ5p+AtDN7K7CrUOukk+pUay4HpUb5oEnTBvkZysXqKIJjjTDAB+gx8rSQNmzhXLQSTugUZ0OuSfv4hUGiDgtngJ/fSmwp3VVwh4rGd7VAEeykuCmO0vCy22JLVY4pRxwJLs0Gpc9HzCKJ/+9vfXgo9N3DqHC9pTv1vawcA6FW68K5XgHb8N7w4MNh/ohNCghFuNmD0rk/9UxDd0rqrAZ4pTTTJ8I7dkCiBx5lO4QlQpXRjlzSpdr+bCVmaBbLFALFZXNn08Qywkj9vmBpVl0lav9WEn3Y4YUozfVmNe+tFg1FbrPHnRwMJDJRVEazEQievVmhQlL5sM5NXp5HN5DUbrjq3wHVccHTSMjIMP67+oqNXxaSt+S872icxCUlzHbX/l995tOXMpUP32zQ3H4zArCBcUZgbEMhCYZDE6WPSYku7GDiBT+aEzoR68UeBE2Sh5cBp8KmdZ8XUvez0OC7pFs7dm11modP/uPYxBX6SLOv+mUeT/j3qxQmbvM5U0opvcKfa1WX6cfhxksFFeAcOyRIYW73KE4BL/e5d0qb6bfTsMNNdk9oMFxuGmVWXzwEzRXyE2WDWTq+rGsugJZtQtpmRzGZ5tsSBrEeYZhibfyE+P74mXIq7wEsZd2JSdHX/uEP/e04KVD+OlR9pL2nsogQGxWHKNzOG0VrZjGNzHyJEuOeTr6h4fqhDokv7liZiOpUpsNoDWoyGvnAhVTUMNqzmNUq2uTFxec72KarjGVxUVZF8PleoCV4aDSW6SY6iBDqYLp3yBEqqmVpIgTjk1CizcVCMynU1kWGovRzotlhdzeGo6wezvnWZG86Z7lwQzLwCTDtb/kSJutGF4KM/B5EEp1zW0RYnNISsDR1s8RrZnGstBlermdZmBNX0g5Xdvc/MQ1WdSf4xJndPxYRXf3SzDDu2YO1XgIw2RnBYhlMBgwq5EmM7NOygIxUJLA8CNev+9zMCWTdLV1gU9lYzwy0AbNPPdPbD2zIW5vcntEsekx0p6o861gM2WH5ILhnlRgAoUW8NUIRF/3Cq/4xM9KKf5enxYAaEUE9/FboaSNkRhnkK6A6hse+EVcqatA8zWOjgaq5taOD8kGPzaLDNPmCrNJgb81T2BX2swKWsIgaZQV9sqnQjqwVKJYsVwoyR1ABwmTkXSfoU6MwfAWKmHT4nyuCkNdMC7VNcJHFW9WPrVX78nGSEcHYHEYVsELSU5FtyK1EpaiLFkjxOVPhbzd588UCdft4di9G5t+oG8PzzHJOg+R+zRI5RhojGAluwyeFRbqBFQ48We2n4W++YaALJzY5+ax7yOsjpjyQFQqDLpz6GXCwaJz129ADLxZw18kp8itNzD1K1hmFwMS1yibomYraEITHshKcwUG60tcAzz2BuWM++nJzwOcZMC8PVrf6i+/h4StHr/FuW5jGfn5U6g+kImPOX/seFowFWb34ksMafDQsPCSqwfnW8Z3kWZsqt2CXTC8FEkFmsl56Rhfp1E2lcBn1MGqOlWwCslr+lnAdEE24b7fAz90axy3m8joB4nbSMDMOPqw+I16ti0tbsAfH6kK1WufYWFalTN8FOmKZskpBWkFo209z8EWHXmytviaCbalE1Gnh1hfnAd/29QkUWpxfn6qnm2DgxcckbFz3iqTRg5FITrn4EVKpnA8jZsPgmL85HEsPYN/DkBwl9mwwn6tfV42LQZT0geMhPyW5qFJBGRRi0P28DB40y6wFCRP77e5GfT1IUUCRC57U/TzIRkQZFEQJBR2aYgOAxamiQawlDCCC3fY+FxMsMg455X0474hDyBSw+ZGKP6bqQEJp89WLZr5OuX4hYs4HoQ7EPkudHtXYhbTJM2l/Wnt6nUcOkIdbgM/f+KjbN04JmymWuOWpmX+F2MTY/lvEBvU2wNMVzBAcHXWgsVVpyUB6WYDPRVBm1TNqdPZwqiDXgW7EfI34AiJmPKXPhi0g478RH7lqXjKheSlGCgJesk2FnUGRNgFm+W8c8sJnQrcMOOHO7dfyePFZXcbGPbs/F7iku0f6vpHqS6ODejRr/w04KhtnwY7iRZKq4y70uRpgAu2J2iDBrHVgh1y4NNNZAkoPyTLDosUKAJ7RZr3u6isvC2kf6U1joamhJUJtsjeSMM6ZH5102/ZFXaPnr7FpKUYLm13VjaFBk+evsO/QNo/02xwzKbnBaxfkkIDgjDli++rNLSKtVnGRCOPM6CWqW48uBy2TDkCtWmG6Zz+/w/ik51cmoFj2TdUKyAf36H9cNoF6P5U9jnajkxAiW27XXAiNHOHBgC8IEVLTpWE8gMj6W6CvMe0lbi3Hye0evmaJ+XtP/yhl9xtVKdS6yOiMiCuBrHGg9TInMLW2Ykg2sgWl9VrH67R/wTD+qWGFzfWOLFfA468+5ts2JIFdxUVGp/kKmpdTAhJeIW5LwhdtJHynoZtLmAtJFChBaxSy1BJhNOVc5oWv26UrA1vLv4JcArAlv5J1wNffF/NUT2n3Jz3zIM+Fn+QgmUDJDmVg6zdtmUC21aCGDmmnsGQaQEg2Nhju+6ozbvt25wBofbuNncvR4kyU1X08nkKrTabZhbv/GF677WFHQx6hNqicWg4/uIkOtka9+DnZwACqlFC3sDYkbQPyC0ubChK87Gz5rGHzFPfEuP0TUv+uOlJ80cHTMiQNfNgkgqVZl0oQ6t1DZLAzuaKVMmuNEXATUVrHznA9VU+43beE0+1bTP37CxYDkobMNyKwGKuSZ7/35c7krkibV5MThP+i2xefUbOnqYSHqtAqQYP2+xhV6j8ragTN6U+TH6VDCNs6dhrFFq8cHp5BJi3RnLAUgD/lPeCwEHkNXzLeqfXxMUvwLiiYKzdA3yDIafl37/eygitHuZma/j4tdygU2rLXQjgyEaOYIm73o3O4mnSR8oWimQHDq9bFZjdR157tMq/KiDk6eHOPi+fr77inODugOfxFX5wI3sXtWwKslYKHV/Wg+yhAR2Bux5pdAmID0CoOHRg+ThhQdsAxokD9+YmIGTDCWnw0Ml/Huy02GZdl9CbrJDTL7SIRnRJLSrH6xK9NsFTtmGe6W7/OxPNBN6AEyBnNzO4L8eUZntL8+xkl6UVXx7olctr9JFEtt+9RQQSAHSs4lMAMpVp9xCtbL6NgnmXH1TYkdNcLv1MHTGCImODxTMMkARwkvw1ggZMlNFRxdRs3R8i0BW9QwJlVlXMf+EAOaLQxmG9OomrPB7+Z4yosKy/aIB9jofveE9ucUPcTlF/nDR5qI2fsxBea7SEYGhiNXEuYdo1TnMHhhdTJpsJUwyQ61jDPmcJkfKowMXNqV7UGF1Wl1UMENpXnjAwvqML5fg+X6YcUHBKILwyyerPveC9govQw3gY9zrsxv8zT9lFcY7+2VS91RA1Nydd8ffuHfqxqMD3nE19Oei7V14XRjXZnFLS7fPnMAIhQavMIbdb7VaTABzNSWt2pzDnzVP1xk5TfFUSpFwvdq9/Mko9gojPk6y5KYa0HYGkScNYfsVX4kM6XhAEZVmXrsopvm08j2v29oxJKa2qq5iWEEJ8J1zQQcMBuxJZI8jUtWeYInxk8n23yXOWl+sByOqCpTD0d008ylDf37hoYjqamtmpsYRvW/xeRKXC8KGaaGH6e5CrRHkqfhCDbPMvDTyTajsy25aLxDXxP07S1KT4/nNKsjr5ju9ST1J9/zyeQALr8Bog0NYWY9YtX2nDhkCnSuDi2VtM9nQ5Rf7wXIGAsEE1N5GchyGtZmHcvMwbydUWtFQ9UKnLAc4bQ+hyt7DE3rZnWN61TP9UeFa6Cicy/M9+hNUpTV67iKP8eleI9T17pHVf8Ih+SmbX6mOrP9vb6kOsb/+HX/Ocd9HX9OhyrCgMMxjr9fxRU6kLARInu6FGyEJtA0hf9VHyQCzfQlUBN9oYb9u3wXp8k/0b7rcaAhgAZqEiDTNR5nhzN5Iia22ReBTfWlJuqh+6ogZ7Flfi52YGsgmVRJgVIjxS0qjklZYox3h9yCBCIJ1LpIpWmZfbsjtMoWQy2yFDo98zSFdCM/g/qQEgOu3YUFyLsrlLXQlRvaql+GSM3VU6gs1hMZNqtoT92QtoX+KZ/QQF8C8e8LdQrUfmvgQNiXgOJ3hboBsI1z+h5VTzn06fAE4HDI0ejarPDwjIexr3g6hb4brhxskSXRNDicMAltDUVQM0Op7ivqFlLiJ9SVgN9PV6hhP2TkFfgPRVADQ6kOZvIJVz3bGk+1t8muOhdQf/cloIm6QrMeULTCESj6I2qJovcxwbVx/9SRppMCHeEBDqRS9RpDqBMBpclXVDw/JEdIe7YYbJShMLM2HUNYZnCaRmFzmsy28T4c4JskreCJTFvFSDShlpmkio9LoFDBsqMyxmVb8X2cnR9jAmmFcVgqlRw0pbEsGiH0rbMUZprfn9AueUx2ZAtERfuU2UBGr7IGXMfYLnD1D+3bI3FyVJKDc6WyhpN0xnLZSGTapw8xtF+jCxW9RcrN2vkUF0mcDbFGr/Lj5ySLJf1iUkkhl7KeRt4/zzH55WOWQNMOWwzJwFK4WcfcJJqpt/m//XfEV5QLZCaJNS55PdvAvaZmacktMNLW0C0M+pdv4qKgLwIXBH2p7rwmQcVtkYBreaoMPKsZijWNDE4rQhtDEdREXarlfv0dT+dZnF6cq6f6RK0ZmqTnCWpySAp1DY10JMCWZANDlUHtkuIyMtrEEFrZqR5dqGjI7ISPEMsaUfJvKUz4/17k55OskbZQ0VJLoWmpjd8sNNL+DvFviwy3FGzGXOmegiVTbSpYSo0UcN5eQQqYDJICpjSUQtGyujWzbpQML1SZtDuN9i1U1lq4kaZM2khTrGkEzE4pNAdSQQ2DhAZnSpIt8FAkO0cy2uyyie+krcg7jaXQmpTJigUYkymHzciQaNXjk1AAKvIksJo8le4YSEyMIJ4HiTTgwZBIZtu4bAUlIzQSw2zVJA1jDvS+hBLGgYTYUhwDOcwE0B9v00GPxSNuuhQ85qYJzJtqYoirmmsoNE02RNpbLEgzqUYmmgBRXuFlCEujWI2whNoNCRsiEtiCsASqlSNHquvDIR6h2HtDGdhvQ7FufGTelYpjI1MMjosMhRaWqXQjQJXBUEwNl/gfC3kjVBnUCFWsW0ehDOHNlmpwF0nAdZVApdsiYh6I7F8/g1elXDm4VWRJtHdMOXgL0P4O3ynlBvcMQwA1cVrqi+ALxa7URPT+1APWoC+WKmJ6bCKNeiQ0LKUEb85kxBanxA/oeErhoQQm050aD5SGt2EKCUQS1e2YccvdMaW8YYFCddpp2uwdqsn28mtMngBeW7A0OiMXeVli5qm8VZEENLJApTNys/aR3nlz5aCBWRLtCS0YGAU4qgXp4DNbkNRcEHXz2kb1R2pMECTxCI0pBo/MGArtF3s8xckBGreGIvgL7Up1I1OzAlINSjwFOB7xRAbnnu9QVaFCMzzLCGVnohCt1gRxicfRv1ByeIL6lCuH1WdIzBp8nWBwl7DaIomiWYpK0zIXk0ZoliuH2uRIdLP+c7ZTTPp0KTjn0wTaw24+tgVwwM2TwIfaPJVRy3KrcuXyNk2tyrv0aTz21OQmPoNRTWp+98s4TsldvSR0el+sO1TiJTrx2TZwNWu1lXu0CRQqz7mWCOkb7o6mFX4SIonqjDu6OJ3SBO0f8pY+sZBCvhwRKMxkaMn1EnQuSgb33IbX3dFAZwzK3sXSzLvV3MuViZRvfq8rxLAQbSKQyJxVWSqDqbd/EQrOt32pbJLtCQw8FOVNMaUyP0XTpuQvxWRYA0gVkAOobSTSiWHQttAg9QxC7bweDa7vVB2FG/tQgX+1QeUC4/zwsQhyH3u2psK/vuFi4i//glXf1DT0WwO9XWDqYEaB3lQMFlE+k7A3hzDH0+NYGd23ZhctY1ZRrqrsZQPRVPdaQckJGttBrsoXCt5N+RAXB1Q5mLKtKDeAVGGVgss0IZ7GlN8jSxDiE6RfojQqQy9N3FRrnpNEDUdYOZpkpJBCDfYpTF9P8sjFXkV2eR5163PULuoT5XhiXlmuJPzggyiqfsah4MJtXABmsucoQQwoH0XMKytGEkBZiXpLMRn9WCbq+QLmAQkDmAJ6ANSYQPmux171dg+qUpon8a8ut58m9WTPv9xV5EwnV5Ql9N5LE6rebyz7owbF4Ckn1q8awBWDUnHJWQrDIKgh5IOgnNhLPy7BDJdJWkdH7jkrjMCRhjOBEYZGKN0FDRlYy7UWaEN+AtyLX4aB7BmvvRm6p5rKpbFIFGJ5zD83JTWlT0kdlsnMS8aof9EIrJZhSsUSmH0+2ax/4ReYbD3w9WVTnSvyrn53GKtXH44AIqoByb8Y9ftHgIp+F2j89zh/Vt981rLHwPZqqo7yFbO8STW5Uvw7a6KU7A01WBM0i1gY1jzyud+kmqc+X5h5uksYK+zwlUIiBxp7hLKQhrFBDV/J/5A6k1mAp/fg+lFdISROgBgDDBNV0ADnyTZiowNIp1qWTqEMEK2g0UIRhcAVV+5qS1/+R3S4Aqk1jKprVRSDLNDKyuMmqHlJYibArHVxDzwYuDWJfjUnrRJsWefaAV7MYmWQDZqiX8oOqwOpJUTaEGsZSVAP2hAeVzK8ano8CKTB0DCRCZhwI4pdD0jnf+cDBTIhlZUBSpx7ngu2QjchhYC8jme1IFRIGYHlQcyk/0hA8mAfyjwmkQSm0RtHUzGYmdTBZmhuhhFkHO7KFMFyJJF+wJs0BzaKmxi9XcwMYsCVDTyk4C0JIOQbrerGbSGs5DaxqRb6BYieo0oXJzl1uGNWwT+WOWmlSoMYQ+GkJKfWKiXRBlBjGSZRBKwyPSwwZjHBOYJBsC/aojbhuoKYWD+Jm9QO9omuzZxsBDQnmzIs5AaCgrURU6iCsK3VxA4wnR6bJtYKbyLlctK4rlZ5pdZm68ZFmpFEdYmkzRqbVM1nEqOY8GUi46iYw9Fu3BdChgPl5KNh2HuA6D4+nlI0RESUK85RKpYuXFzGZtkiibk4ocp9SMaIdVwXVZZQ6j2C3J3whVCTpLo8iuQY9RVzl0jkf7YKr2oXN1PpMiUSyUV2d5nio3uSmsOP/jwDTZ/uqCvoIT7CXU73kXh+TyLR1MB/dPInOHOZSBW8VelvaVYxBJpMAtoSblaBae1N14enVdoJoAphFCGOLqlK/epHXRIDV68vRxZMYSai76AxHKnXUeWIDqor0Zem0YjMhO4dRAZD8oq1xYp+e7ZlqezWhma0oPP0JvkSabaqL5ai8yj6xN8tia9r8GwApJML7vpmgIkqTWrBQaNdFVWNTRxFiGEpnHpQkOtocK+T+/HBFRS7t9F+fFBIbYaLMkS2J9O0dje0S0M9ul8XYY4+bLfcBjyJX8WFkONDPU+bnkF+xVZWJPK/lQ2vahfoPbrF+7OnuET7v5LqiWpBVFxXxZs6TF0+lD2pKg1T724I5rsd+MvNAFfwpAhYU/rFG+UI8GSaj3S/G9uHreV3XJjLPH2KAc2ASJEEGBDp9AhDPTDzgb2KQDID5VJISR9iYaRI3kCYmGRj8GMW7Xehr+QXHfOYpssTIf8mOAq/SvOZNO67MBGeXnfQaTCiga9cS5bQk+BgTWF+VabscN0IdSE3tFs+nnBNez4m+YhmwAMpwwx1QFKV9ktW5UoZccBt0NlSWv/9HTouAJ/7RfNkSUWu3RF4eaIky3rTfv2aTDYeDKT6MuTEIT6O2U2h2DBKaf3vG6c2A5wrKAIyGQEjh2ldBV5kyZca4OjyKal5iR4jhpmRXOYbNsdQdJMlVRKnirWkqoLvdSScSamdejTZkcYbo1sri03p7SKtG0hdJSd4EW6W3sohytWQ1CkSEjyJllORK6ZyOAFVM6Wrs0rJ+Mh4eDaJauoC6ULMWtOorVM3uJq8en7UEtKB6S+QYepg98hQ/onhIk6ZSMLF8Y5OOxa9KfKjyh4q8hAGgTOstQsbZcK00aZ4yC0MQRGv3Qx9erhIsX8Tifxv3IQUd01NafY6l5E9BaIcw2M7SKnva3cnLCEdXjvGy5LQucQrzUm0ruQYF8/X33dPcXZAd9i0Qwo1YFuiraQyCpvTrTUInK+N3aHQaeaabQmYRW6kEcgfxtqz1OtSW5I3TjkZauuEGPo0efMII9MkeP7MpNi366r4377PayIwQVr0JoEHUgW1wnfB2YVfmRDuvnkAYZLfbZRZIjbDmtIoLK1cMSjrG9FHlc1NZhqZQYKhg2nDFCR0Ja9qLQQxTB6ziE2dK9pIQa2YT4CEvs20okjUy3AA87URFuo0bC7O4DbmUFCHNAeYaK35DpX501zNMeSAi3rWMmMAtN4VAXiI+e4oNoocdm5pLOhEU1F9YHSVZ2VVxEk9q+Ddab846UIxP+SRmKAK2N/54q1fEznEbWaXDZLEXc36QZeMy4PZ6cQdBpakyNVKWeUImdUkVAoyU6AwWctEq43kGB51QNK2++4xmSxF2jjDDs/U1Obq6dTCGz+Dm0FtKs+cae8zqemAXck4juHxBGTmazYuipR74ww7RIpXm6unUwtvHIl+BrXbXaSQMtB4VpPUVzzQ9t/WBDOpVE1xR65Pv+ixm5gCK6OzNQ1Vl+u7HvM5wU1rWjeuEyPXvPtGG1kfd4Yj9H/65V/1Vy8aJrXhcS+joi979aJJitr+gP+s8iI+oPf5HqUl+fXVi7szrn1EzV+vUZkcBhavMM8MkTzRA9OO5iZ7rAO+k3SanEQdSVfc522v4n1cxRdFldRxLXHxDn9LJN0v8TKpz4M+o/1N9uFcnc4VVhkdP6fMofOrF+r2X70QZH7VBNorfaiAxUywCuhDdnlO0n0v95s4LbntpozFFbb+7wj/3vQl/jQrdHjuOf2RZ4aMWvPhTSrK9viTe0DHU4qZlR+y+/grcpHtY4neoUO8e76t03gRpxkZE31HsGZ/9TqJD0V8LFseQ338J8bw/vj9v/4fmgz6ciGyCAA= + + + 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/Migrations/MigrationsConfiguration.cs b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs index ea5109256d..49ae7dd7a8 100644 --- a/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs +++ b/src/Libraries/SmartStore.Data/Migrations/MigrationsConfiguration.cs @@ -10,6 +10,7 @@ public sealed class MigrationsConfiguration : DbMigrationsConfiguration - /// Object context - ///
+ /// + /// Object context + /// [DbConfigurationType(typeof(SmartDbConfiguration))] public abstract class ObjectContextBase : DbContext, IDbContext { @@ -44,6 +41,7 @@ protected ObjectContextBase(string nameOrConnectionString, string alias = null) : base(nameOrConnectionString) { this.HooksEnabled = true; + this.AutoCommitEnabled = true; this.Alias = null; this.EventPublisher = NullEventPublisher.Instance; } @@ -193,7 +191,7 @@ private IEnumerable ToParameters(params object[] parameters) { for (int i = 0; i < result.Count; i++) { - result[i] = AttachEntityToContext(result[i]); + result[i] = Attach(result[i]); } } } @@ -226,7 +224,7 @@ private IEnumerable ToParameters(params object[] parameters) { for (int i = 0; i < result.Count; i++) { - result[i] = AttachEntityToContext(result[i]); + result[i] = Attach(result[i]); } } // close up the reader, we're done saving results @@ -334,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; @@ -354,7 +359,7 @@ public override int SaveChanges() // SAVE NOW!!! bool validateOnSaveEnabled = this.Configuration.ValidateOnSaveEnabled; this.Configuration.ValidateOnSaveEnabled = false; - int result = this.Commit(); + int result = base.SaveChanges(); this.Configuration.ValidateOnSaveEnabled = validateOnSaveEnabled; PerformPostSaveActions(modifiedEntries, modifiedHookEntries); @@ -371,7 +376,7 @@ public override Task SaveChangesAsync() // SAVE NOW!!! bool validateOnSaveEnabled = this.Configuration.ValidateOnSaveEnabled; this.Configuration.ValidateOnSaveEnabled = false; - var result = this.CommitAsync(); + var result = base.SaveChangesAsync(); result.ContinueWith((t) => { @@ -382,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 @@ -423,8 +428,22 @@ public bool ProxyCreationEnabled } } + public bool LazyLoadingEnabled + { + get + { + return this.Configuration.LazyLoadingEnabled; + } + set + { + this.Configuration.LazyLoadingEnabled = value; + } + } + public bool ForceNoTracking { get; set; } + public bool AutoCommitEnabled { get; set; } + public ITransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Unspecified) { var dbContextTransaction = this.Database.BeginTransaction(isolationLevel); @@ -477,79 +496,70 @@ 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 AttachEntityToContext(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 alreadyAttached = Set().Local.Where(x => x.Id == entity.Id).FirstOrDefault(); + var dbSet = Set(); + var alreadyAttached = dbSet.Local.FirstOrDefault(x => x.Id == entity.Id); + if (alreadyAttached == null) { - // attach new entity - Set().Attach(entity); + dbSet.Attach(entity); return entity; } - else + + return alreadyAttached; + } + + public bool IsAttached(TEntity entity) where TEntity : BaseEntity + { + if (entity != null) { - // entity is already loaded. - return alreadyAttached; + return Set().Local.Any(x => x.Id == entity.Id); } - } - public bool IsAttached(TEntity entity) where TEntity : BaseEntity, new() - { - Guard.ArgumentNotNull(() => entity); - return Set().Local.Where(x => x.Id == entity.Id).FirstOrDefault() != null; + return false; } - public void DetachEntity(TEntity entity) where TEntity : BaseEntity, new() + public void DetachEntity(TEntity entity) where TEntity : BaseEntity { - Guard.ArgumentNotNull(() => entity); - if (this.IsAttached(entity)) - { - ((IObjectContextAdapter)this).ObjectContext.Detach(entity); - } + this.Entry(entity).State = System.Data.Entity.EntityState.Detached; } - public void Detach(object entity) + public int DetachEntities(bool unchangedEntitiesOnly = true) where TEntity : class { - ((IObjectContextAdapter)this).ObjectContext.Detach(entity); - } + Func predicate = x => + { + if (x.Entity is TEntity) + { + if (x.State == System.Data.Entity.EntityState.Detached) + return false; - public int DetachAll() - { - var attachedEntities = this.ChangeTracker.Entries() - .Where(x => x.State != System.Data.Entity.EntityState.Detached) - .ToList(); - attachedEntities.Each(x => this.Entry(x.Entity).State = System.Data.Entity.EntityState.Detached); + if (unchangedEntitiesOnly) + return x.State == System.Data.Entity.EntityState.Unchanged; + + return true; + } + + return false; + }; + + var attachedEntities = this.ChangeTracker.Entries().Where(predicate).ToList(); + attachedEntities.Each(entry => entry.State = System.Data.Entity.EntityState.Detached); return attachedEntities.Count; } - public void ChangeState(TEntity entity, System.Data.Entity.EntityState newState) + public void ChangeState(TEntity entity, System.Data.Entity.EntityState newState) where TEntity : BaseEntity { - ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager.ChangeObjectState(entity, newState); + Console.WriteLine("ChangeState ORIGINAL"); + this.Entry(entity).State = newState; } - public bool SetToUnchanged(TEntity entity) + public void ReloadEntity(TEntity entity) where TEntity : BaseEntity { - try - { - ChangeState(entity, System.Data.Entity.EntityState.Unchanged); - return true; - } - catch (Exception exc) - { - exc.Dump(); - return false; - } + this.Entry(entity).Reload(); } - private string FormatValidationExceptionMessage(IEnumerable results) + private string FormatValidationExceptionMessage(IEnumerable results) { var sb = new StringBuilder(); sb.Append("Entity validation failed" + Environment.NewLine); @@ -591,69 +601,6 @@ private void IgnoreMergedData(IList entries, bool ignore) #endregion - #region EF helpers - - private int Commit() - { - int result = 0; - bool commitFailed = false; - do - { - commitFailed = false; - - try - { - result = base.SaveChanges(); - } - catch (DbUpdateConcurrencyException ex) - { - commitFailed = true; - - foreach (var entry in ex.Entries) - { - entry.Reload(); - } - } - } - while (commitFailed); - - return result; - } - - private Task CommitAsync() - { - var tcs = new TaskCompletionSource(); - - base.SaveChangesAsync().ContinueWith((t) => - { - if (!t.IsFaulted) - { - //if (t.IsCanceled) - //{ - // tcs.TrySetCanceled(); - // return; - //} - tcs.TrySetResult(t.Result); - return; - } - - var ex = t.Exception.InnerException; - if (ex != null && ex is DbUpdateConcurrencyException) - { - // try again - tcs.TrySetResult(this.CommitAsync().Result); - } - else - { - tcs.TrySetException(ex); - } - }); - - return tcs.Task; - } - - #endregion - #region Nested classes private class DbContextTransactionWrapper : ITransaction 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 cdef6d5f8d..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(); @@ -102,7 +102,7 @@ public void Migrate(IEnumerable entries, bool updateTouched toUpdate.Each(x => _ctx.Entry(x).State = System.Data.Entity.EntityState.Modified); // save now - _ctx.SaveChanges(); + int affectedRows = _ctx.SaveChanges(); } } diff --git a/src/Libraries/SmartStore.Data/Setup/Builder/PermissionMigrator.cs b/src/Libraries/SmartStore.Data/Setup/Builder/PermissionMigrator.cs new file mode 100644 index 0000000000..fe509e1cdf --- /dev/null +++ b/src/Libraries/SmartStore.Data/Setup/Builder/PermissionMigrator.cs @@ -0,0 +1,55 @@ +using System.Data.Entity; +using System.Linq; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Security; + +namespace SmartStore.Data.Setup +{ + internal class PermissionMigrator + { + private readonly SmartObjectContext _ctx; + private readonly DbSet _permissionRecords; + private readonly IQueryable _customerRoles; + + public PermissionMigrator(SmartObjectContext ctx) + { + Guard.ArgumentNotNull(() => ctx); + + _ctx = ctx; + _permissionRecords = _ctx.Set(); + _customerRoles = _ctx.Set().Expand(x => x.PermissionRecords); + } + + public void AddPermission(PermissionRecord permission, string[] rolesToMap) + { + Guard.ArgumentNotNull(() => permission); + Guard.ArgumentNotNull(() => rolesToMap); + + if (permission.SystemName.IsEmpty()) + return; + + var permissionRecord = _permissionRecords.FirstOrDefault(x => x.SystemName == permission.SystemName); + + if (permissionRecord == null) + { + _permissionRecords.Add(permission); + + _ctx.SaveChanges(); + } + + permissionRecord = _permissionRecords.FirstOrDefault(x => x.SystemName == permission.SystemName); + + foreach (var roleName in rolesToMap) + { + var role = _customerRoles.FirstOrDefault(x => x.SystemName == roleName); + if (role != null && !role.PermissionRecords.Any(x => x.SystemName == permission.SystemName)) + { + role.PermissionRecords.Add(permissionRecord); + } + } + + _ctx.SaveChanges(); + } + } +} diff --git a/src/Libraries/SmartStore.Data/Setup/IDbMigrationExtensions.cs b/src/Libraries/SmartStore.Data/Setup/IDbMigrationExtensions.cs index 70bebdd73d..44f470e318 100644 --- a/src/Libraries/SmartStore.Data/Setup/IDbMigrationExtensions.cs +++ b/src/Libraries/SmartStore.Data/Setup/IDbMigrationExtensions.cs @@ -16,7 +16,7 @@ namespace SmartStore.Data.Setup public static class IDbMigrationExtensions { - public static void SqlFile(this IDbMigration migration, string fileName, Assembly assembly = null, string location = null) + public static void SqlFileOrResource(this IDbMigration migration, string fileName, Assembly assembly = null, string location = null) { Guard.ArgumentNotEmpty(() => fileName); diff --git a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs index d6f664d19d..c5c2745302 100644 --- a/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs +++ b/src/Libraries/SmartStore.Data/Setup/SeedData/InvariantSeedData.cs @@ -56,7 +56,7 @@ public void Initialize(SmartObjectContext context) public IList Pictures() { - var entities = new List() + var entities = new List { CreatePicture(File.ReadAllBytes(_sampleImagesPath + "company_logo.png"), "image/png", GetSeName("company-logo")), CreatePicture(File.ReadAllBytes(_sampleImagesPath + "clouds.png"), "image/png", GetSeName("slider-bg")), @@ -73,6 +73,10 @@ public IList Stores() var seName = GetSeName("company-logo"); var imgCompanyLogo = _ctx.Set().Where(x => x.SeoFilename == seName).FirstOrDefault(); + var currency = _ctx.Set().FirstOrDefault(x => x.CurrencyCode == "EUR"); + if (currency == null) + currency = _ctx.Set().First(); + var entities = new List() { new Store() @@ -82,7 +86,9 @@ public IList Stores() Hosts = "yourstore.com,www.yourstore.com", SslEnabled = false, DisplayOrder = 1, - LogoPictureId = imgCompanyLogo.Id + LogoPictureId = imgCompanyLogo.Id, + PrimaryStoreCurrencyId = currency.Id, + PrimaryExchangeRateCurrencyId = currency.Id } }; this.Alter(entities); @@ -3977,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, }, @@ -3985,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, }, @@ -3993,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, }, @@ -4002,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, }, @@ -4010,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, }, @@ -4018,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, }, @@ -4042,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, }, @@ -4082,7 +4088,7 @@ public IList MessageTemplates() { Name = "Product.AskQuestion", Subject = "%Store.Name% - Question concerning '%Product.Name%' from %ProductQuestion.SenderName%", - Body = templateHeader + "

%ProductQuestion.Message%

%ProductQuestion.Message%

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

" + templateFooter, + Body = templateHeader + "

%ProductQuestion.Message%

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

" + templateFooter, IsActive = true, EmailAccountId = eaGeneral.Id, }, @@ -4244,8 +4250,6 @@ public IList Settings() }, new CurrencySettings() { - PrimaryStoreCurrencyId = _ctx.Set().First().Id, - PrimaryExchangeRateCurrencyId = _ctx.Set().First().Id, }, new MeasureSettings() { @@ -4767,23 +4771,15 @@ public IList ScheduleTasks() new ScheduleTask { Name = "Send emails", - Seconds = 60, + CronExpression = "* * * * *", // every Minute Type = "SmartStore.Services.Messages.QueuedMessagesSendTask, SmartStore.Services", Enabled = true, StopOnError = false, }, new ScheduleTask - { - Name = "Keep alive", - Seconds = 300, - Type = "SmartStore.Services.Common.KeepAliveTask, SmartStore.Services", - Enabled = true, - StopOnError = false, - }, - new ScheduleTask { Name = "Delete guests", - Seconds = 600, + CronExpression = "*/10 * * * *", // Every 10 minutes Type = "SmartStore.Services.Customers.DeleteGuestsTask, SmartStore.Services", Enabled = true, StopOnError = false, @@ -4791,7 +4787,7 @@ public IList ScheduleTasks() new ScheduleTask { Name = "Delete logs", - Seconds = 86400, // 1 day + CronExpression = "0 1 * * *", // At 01:00 Type = "SmartStore.Services.Logging.DeleteLogsTask, SmartStore.Services", Enabled = true, StopOnError = false, @@ -4799,7 +4795,7 @@ public IList ScheduleTasks() new ScheduleTask { Name = "Clear cache", - Seconds = 600, + CronExpression = "0 */4 * * *", // Every 04 hours Type = "SmartStore.Services.Caching.ClearCacheTask, SmartStore.Services", Enabled = false, StopOnError = false, @@ -4807,11 +4803,35 @@ public IList ScheduleTasks() new ScheduleTask { Name = "Update currency exchange rates", - Seconds = 900, + CronExpression = "0/15 * * * *", // Every 15 minutes Type = "SmartStore.Services.Directory.UpdateExchangeRateTask, SmartStore.Services", Enabled = true, StopOnError = false, }, + new ScheduleTask + { + Name = "Clear transient uploads", + CronExpression = "30 1,13 * * *", // At 01:30 and 13:30 + Type = "SmartStore.Services.Media.TransientMediaClearTask, SmartStore.Services", + Enabled = true, + StopOnError = false, + }, + new ScheduleTask + { + Name = "Clear email queue", + CronExpression = "0 2 * * *", // At 02:00 + Type = "SmartStore.Services.Messages.QueuedMessagesClearTask, SmartStore.Services", + Enabled = true, + StopOnError = false, + }, + new ScheduleTask + { + Name = "Cleanup temporary files", + CronExpression = "30 3 * * *", // At 03:30 + Type = "SmartStore.Services.Common.TempFileCleanupTask, SmartStore.Services", + Enabled = true, + StopOnError = false + } }; this.Alter(entities); return entities; @@ -7894,7 +7914,7 @@ public IList Products() #region Antonio Vivaldi: then spring - var productInstantDownloadVivaldi = new Product() + var productInstantDownloadVivaldi = new Product { ProductType = ProductType.SimpleProduct, VisibleIndividually = true, @@ -7918,7 +7938,7 @@ public IList Products() AllowBackInStockSubscriptions = false, IsDownload = true, HasSampleDownload = true, - SampleDownload = new Download() + SampleDownload = new Download { DownloadGuid = Guid.NewGuid(), ContentType = "audio/mp3", diff --git a/src/Libraries/SmartStore.Data/SmartDbConfiguration.cs b/src/Libraries/SmartStore.Data/SmartDbConfiguration.cs index 476f612085..f67db1a271 100644 --- a/src/Libraries/SmartStore.Data/SmartDbConfiguration.cs +++ b/src/Libraries/SmartStore.Data/SmartDbConfiguration.cs @@ -1,11 +1,15 @@ using System; using System.Collections.Generic; using System.Data.Entity; +using System.Data.Entity.Core.Common; using System.Data.Entity.Infrastructure; using System.Data.Entity.Infrastructure.DependencyResolution; using System.Linq; +using EFCache; using SmartStore.Core.Data; +using SmartStore.Core.Infrastructure; using SmartStore.Data.Setup; +using SmartStore.Data.Caching; namespace SmartStore.Data { @@ -24,6 +28,26 @@ public SmartDbConfiguration() if (provider != null) { base.SetDefaultConnectionFactory(provider.GetConnectionFactory()); + + // prepare EntityFramework 2nd level cache + ICache cache = null; + try + { + var innerCache = EngineContext.Current.Resolve>(); + cache = new EfCacheImpl(innerCache(typeof(SmartStore.Core.Caching.StaticCache))); + } + catch + { + cache = new InMemoryCache(); + } + + var transactionHandler = new CacheTransactionHandler(cache); + AddInterceptor(transactionHandler); + + Loaded += + (sender, args) => args.ReplaceService( + (s, _) => new CachingProviderServices(s, transactionHandler, + new EfCachingPolicy())); } } } diff --git a/src/Libraries/SmartStore.Data/SmartObjectContext.cs b/src/Libraries/SmartStore.Data/SmartObjectContext.cs index 97a8982b71..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 @@ -53,7 +51,7 @@ protected override void OnModelCreating(DbModelBuilder modelBuilder) // && type.BaseType != null // && type.BaseType.IsGenericType // && type.BaseType.GetGenericTypeDefinition() == typeof(EntityTypeConfiguration<>)); - + var typesToRegister = from t in Assembly.GetExecutingAssembly().GetTypes() where t.Namespace.HasValue() && t.BaseType != null && diff --git a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj index a378765f5d..ff7fd39e2b 100644 --- a/src/Libraries/SmartStore.Data/SmartStore.Data.csproj +++ b/src/Libraries/SmartStore.Data/SmartStore.Data.csproj @@ -62,20 +62,24 @@ 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 False - ..\..\packages\EntityFramework.6.1.0\lib\net45\EntityFramework.dll + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll False - ..\..\packages\EntityFramework.6.1.0\lib\net45\EntityFramework.SqlServer.dll + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll False - ..\..\packages\EntityFramework.SqlServerCompact.6.1.0\lib\net45\EntityFramework.SqlServerCompact.dll + ..\..\packages\EntityFramework.SqlServerCompact.6.1.3\lib\net45\EntityFramework.SqlServerCompact.dll True @@ -89,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 @@ -115,7 +119,15 @@ Properties\AssemblyVersionInfo.cs + + + + + + + + 201403112331027_Initial.cs @@ -276,6 +288,128 @@ 201506181858349_AclRecordCustomerRole.cs + + + 201506211043073_PaymentShippingRestrictions.cs + + + + 201506261018157_Merge.cs + + + + 201506261756463_PrimaryStoreCurrencyMultiStore.cs + + + + 201507132241575_QueuedEmailAttachments.cs + + + + 201507072138058_MessageTemplateAttachments.cs + + + + 201507092146153_DownloadGuidIndex.cs + + + + 201507102159496_TransientMedia.cs + + + + 201507141647299_CustomerTablePerf.cs + + + + 201507200832223_SortFilterHomepageProducts.cs + + + + 201507210952098_PaymentMethodDescription.cs + + + + 201507242008201_WebScheduler.cs + + + + 201507250039446_Merge2.cs + + + + 201508042207146_RemoveKeepAlive.cs + + + + 201508121735397_AddSyncMapping.cs + + + + 201508142203054_CronExpressions.cs + + + + 201509031112324_Merge3.cs + + + + 201508091512101_ExportFramework.cs + + + + 201508211346171_Merge1.cs + + + + 201509021536425_ExportFramework1.cs + + + + 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 + + + @@ -284,7 +418,6 @@ - @@ -536,6 +669,96 @@ 201506181858349_AclRecordCustomerRole.cs + + 201506211043073_PaymentShippingRestrictions.cs + + + 201506261018157_Merge.cs + + + 201506261756463_PrimaryStoreCurrencyMultiStore.cs + + + 201507132241575_QueuedEmailAttachments.cs + + + 201507072138058_MessageTemplateAttachments.cs + + + 201507092146153_DownloadGuidIndex.cs + + + 201507102159496_TransientMedia.cs + + + 201507141647299_CustomerTablePerf.cs + + + 201507200832223_SortFilterHomepageProducts.cs + + + 201507210952098_PaymentMethodDescription.cs + + + 201507242008201_WebScheduler.cs + + + 201507250039446_Merge2.cs + + + 201508042207146_RemoveKeepAlive.cs + + + 201508121735397_AddSyncMapping.cs + + + 201508142203054_CronExpressions.cs + + + 201509031112324_Merge3.cs + + + 201508091512101_ExportFramework.cs + + + 201508211346171_Merge1.cs + + + 201509021536425_ExportFramework1.cs + + + 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 + @@ -554,6 +777,7 @@ + diff --git a/src/Libraries/SmartStore.Data/Sql/LatestProductLoadAllPaged.sql b/src/Libraries/SmartStore.Data/Sql/LatestProductLoadAllPaged.sql index f5b367a08b..522b70dc27 100644 --- a/src/Libraries/SmartStore.Data/Sql/LatestProductLoadAllPaged.sql +++ b/src/Libraries/SmartStore.Data/Sql/LatestProductLoadAllPaged.sql @@ -24,8 +24,16 @@ @PageSize int = 2147483644, @ShowHidden bit = 0, @LoadFilterableSpecificationAttributeOptionIds bit = 0, --a value indicating whether we should load the specification attribute option identifiers applied to loaded products (all pages) - @WithoutCategories bit = 0, - @WithoutManufacturers bit = 0, + @WithoutCategories bit = null, + @WithoutManufacturers bit = null, + @IsPublished bit = null, + @HomePageProducts bit = null, + @IdMin int = 0, + @IdMax int = 0, + @AvailabilityMin int = null, + @AvailabilityMax int = null, + @CreatedFromUtc nvarchar(MAX) = null, + @CreatedToUtc nvarchar(MAX) = null, @FilterableSpecificationAttributeOptionIds nvarchar(MAX) = null OUTPUT, --the specification attribute option identifiers applied to loaded products (all pages). returned as a comma separated list of identifiers @TotalRecords int = null OUTPUT ) @@ -396,12 +404,38 @@ BEGIN SET @sql = @sql + ' AND pptm.ProductTag_Id = ' + CAST(@ProductTagId AS nvarchar(max)) END - + + --homepage products + IF (@HomePageProducts is not null) + BEGIN + SET @sql = @sql + ' + AND p.ShowOnHomePage = ' + CAST(@HomePageProducts AS nvarchar(max)) + END + + --is published + IF (@IsPublished is null) + BEGIN + IF @ShowHidden = 0 + BEGIN + SET @sql = @sql + ' + AND p.Published = 1' + END + END + ELSE IF (@IsPublished = 1) + BEGIN + SET @sql = @sql + ' + AND p.Published = 1' + END + ELSE IF (@IsPublished = 0) + BEGIN + SET @sql = @sql + ' + AND p.Published = 0' + END + --show hidden IF @ShowHidden = 0 BEGIN SET @sql = @sql + ' - AND p.Published = 1 AND p.Deleted = 0 AND (getutcdate() BETWEEN ISNULL(p.AvailableStartDateTimeUtc, ''1/1/1900'') and ISNULL(p.AvailableEndDateTimeUtc, ''1/1/2999''))' END @@ -472,6 +506,45 @@ BEGIN WHERE [sm].EntityId = p.Id AND [sm].EntityName = ''Product'' and [sm].StoreId=' + CAST(@StoreId AS nvarchar(max)) + ' ))' END + + --filter by product identifier + IF @IdMin != 0 + BEGIN + SET @sql = @sql + ' + AND p.Id >= ' + CAST(@IdMin AS nvarchar(max)) + END + + IF @IdMax != 0 + BEGIN + SET @sql = @sql + ' + AND p.Id <= ' + CAST(@IdMax AS nvarchar(max)) + END + + --filter by availability + IF @AvailabilityMin is not null + BEGIN + SET @sql = @sql + ' + AND p.StockQuantity >= ' + CAST(@AvailabilityMin AS nvarchar(max)) + END + + IF @AvailabilityMax is not null + BEGIN + SET @sql = @sql + ' + AND p.StockQuantity <= ' + CAST(@AvailabilityMax AS nvarchar(max)) + END + + --filter by creation date + IF @CreatedFromUtc is not null + BEGIN + SET @sql = @sql + ' + AND p.CreatedOnUtc >= ''' + CAST(@CreatedFromUtc AS nvarchar(max)) + '''' + END + + IF @CreatedToUtc is not null + BEGIN + SET @sql = @sql + ' + AND p.CreatedOnUtc <= ''' + CAST(@CreatedToUtc AS nvarchar(max)) + '''' + END --filter by specs IF @SpecAttributesCount > 0 @@ -488,22 +561,32 @@ BEGIN )' END - IF @WithoutCategories = 1 + IF (@WithoutCategories is not null) BEGIN - SET @sql = @sql + ' - AND NOT EXISTS ( - SELECT 1 FROM [Product_Category_Mapping] pcm with (NOLOCK) - WHERE [pcm].[ProductId] = p.Id - )' + IF (@WithoutCategories = 1) + BEGIN + SET @sql = @sql + ' + AND NOT EXISTS (SELECT 1 FROM [Product_Category_Mapping] pcm with (NOLOCK) WHERE [pcm].[ProductId] = p.Id)' + END + ELSE IF (@WithoutCategories = 0) + BEGIN + SET @sql = @sql + ' + AND EXISTS (SELECT 1 FROM [Product_Category_Mapping] pcm with (NOLOCK) WHERE [pcm].[ProductId] = p.Id)' + END END - IF @WithoutManufacturers = 1 + IF (@WithoutManufacturers is not null) BEGIN - SET @sql = @sql + ' - AND NOT EXISTS ( - SELECT 1 FROM [Product_Manufacturer_Mapping] pmm with (NOLOCK) - WHERE [pmm].[ProductId] = p.Id - )' + IF (@WithoutManufacturers = 1) + BEGIN + SET @sql = @sql + ' + AND NOT EXISTS (SELECT 1 FROM [Product_Manufacturer_Mapping] pmm with (NOLOCK) WHERE [pmm].[ProductId] = p.Id)' + END + ELSE IF (@WithoutManufacturers = 0) + BEGIN + SET @sql = @sql + ' + AND EXISTS (SELECT 1 FROM [Product_Manufacturer_Mapping] pmm with (NOLOCK) WHERE [pmm].[ProductId] = p.Id)' + END END --sorting 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 14cad35b89..b1c169951c 100644 --- a/src/Libraries/SmartStore.Data/packages.config +++ b/src/Libraries/SmartStore.Data/packages.config @@ -1,9 +1,10 @@  - - - + + + + - + \ No newline at end of file diff --git a/src/Libraries/SmartStore.Services/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 c43d075957..05cddc7f7b 100644 --- a/src/Libraries/SmartStore.Services/Authentication/FormsAuthenticationService.cs +++ b/src/Libraries/SmartStore.Services/Authentication/FormsAuthenticationService.cs @@ -1,6 +1,7 @@ using System; using System.Web; using System.Web.Security; +using SmartStore.Core; using SmartStore.Core.Domain.Customers; using SmartStore.Services.Customers; @@ -11,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; @@ -24,8 +25,7 @@ public partial class FormsAuthenticationService : IAuthenticationService /// HTTP context /// Customer service /// Customer settings - public FormsAuthenticationService(HttpContextBase httpContext, - ICustomerService customerService, CustomerSettings customerSettings) + public FormsAuthenticationService(HttpContextBase httpContext, ICustomerService customerService, CustomerSettings customerSettings) { this._httpContext = httpContext; this._customerService = customerService; @@ -33,7 +33,6 @@ public FormsAuthenticationService(HttpContextBase httpContext, this._expirationTimeSpan = FormsAuthentication.Timeout; } - public virtual void SignIn(Customer customer, bool createPersistentCookie) { var now = DateTime.UtcNow.ToLocalTime(); @@ -77,18 +76,42 @@ public virtual Customer GetAuthenticatedCustomer() if (_cachedCustomer != null) return _cachedCustomer; - if (_httpContext == null || - _httpContext.Request == null || - !_httpContext.Request.IsAuthenticated || - !(_httpContext.User.Identity is FormsIdentity)) - { + if (_httpContext == null || _httpContext.Request == null || !_httpContext.Request.IsAuthenticated || _httpContext.User == null) return null; - } - var formsIdentity = (FormsIdentity)_httpContext.User.Identity; - var customer = GetAuthenticatedCustomerFromTicket(formsIdentity.Ticket); - if (customer != null && customer.Active && !customer.Deleted && customer.IsRegistered()) - _cachedCustomer = customer; + Customer customer = null; + FormsIdentity formsIdentity = null; + SmartStoreIdentity smartNetIdentity = null; + + if ((formsIdentity = _httpContext.User.Identity as FormsIdentity) != null) + { + customer = GetAuthenticatedCustomerFromTicket(formsIdentity.Ticket); + } + else if ((smartNetIdentity = _httpContext.User.Identity as SmartStoreIdentity) != null) + { + customer = _customerService.GetCustomerById(smartNetIdentity.CustomerId); + } + + 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; + } + return _cachedCustomer; } @@ -108,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 79a9d607e2..f3bb8103bb 100644 --- a/src/Libraries/SmartStore.Services/Blogs/BlogService.cs +++ b/src/Libraries/SmartStore.Services/Blogs/BlogService.cs @@ -2,11 +2,11 @@ using System.Collections.Generic; using System.Linq; using SmartStore.Core; -using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Blogs; using SmartStore.Core.Domain.Stores; using SmartStore.Core.Events; +using SmartStore.Services.Localization; namespace SmartStore.Services.Blogs { @@ -15,13 +15,14 @@ namespace SmartStore.Services.Blogs /// public partial class BlogService : IBlogService { - #region Fields private readonly IRepository _blogPostRepository; private readonly IRepository _storeMappingRepository; - private readonly ICacheManager _cacheManager; - private readonly IEventPublisher _eventPublisher; + private readonly ICommonServices _services; + private readonly ILanguageService _languageService; + + private readonly BlogSettings _blogSettings; #endregion @@ -29,13 +30,15 @@ public partial class BlogService : IBlogService public BlogService(IRepository blogPostRepository, IRepository storeMappingRepository, - ICacheManager cacheManager, - IEventPublisher eventPublisher) + ICommonServices services, + ILanguageService languageService, + BlogSettings blogSettings) { _blogPostRepository = blogPostRepository; _storeMappingRepository = storeMappingRepository; - _cacheManager = cacheManager; - _eventPublisher = eventPublisher; + _services = services; + _languageService = languageService; + _blogSettings = blogSettings; this.QuerySettings = DbQuerySettings.Default; } @@ -58,7 +61,7 @@ public virtual void DeleteBlogPost(BlogPost blogPost) _blogPostRepository.Delete(blogPost); //event notification - _eventPublisher.EntityDeleted(blogPost); + _services.EventPublisher.EntityDeleted(blogPost); } /// @@ -84,17 +87,25 @@ public virtual BlogPost GetBlogPostById(int blogPostId) /// Page index /// Page size /// A value indicating whether to show hidden records + /// The maximum age of returned blog posts /// Blog posts public virtual IPagedList GetAllBlogPosts(int storeId, int languageId, - DateTime? dateFrom, DateTime? dateTo, int pageIndex, int pageSize, bool showHidden = false) + DateTime? dateFrom, DateTime? dateTo, int pageIndex, int pageSize, bool showHidden = false, DateTime? maxAge = null) { var query = _blogPostRepository.Table; + if (dateFrom.HasValue) query = query.Where(b => dateFrom.Value <= b.CreatedOnUtc); + if (dateTo.HasValue) query = query.Where(b => dateTo.Value >= b.CreatedOnUtc); + if (languageId > 0) query = query.Where(b => languageId == b.LanguageId); + + if (maxAge.HasValue) + query = query.Where(b => b.CreatedOnUtc >= maxAge.Value); + if (!showHidden) { var utcNow = DateTime.UtcNow; @@ -202,7 +213,7 @@ public virtual void InsertBlogPost(BlogPost blogPost) _blogPostRepository.Insert(blogPost); //event notification - _eventPublisher.EntityInserted(blogPost); + _services.EventPublisher.EntityInserted(blogPost); } /// @@ -217,7 +228,7 @@ public virtual void UpdateBlogPost(BlogPost blogPost) _blogPostRepository.Update(blogPost); //event notification - _eventPublisher.EntityUpdated(blogPost); + _services.EventPublisher.EntityUpdated(blogPost); } /// diff --git a/src/Libraries/SmartStore.Services/Blogs/IBlogService.cs b/src/Libraries/SmartStore.Services/Blogs/IBlogService.cs index eabf47a802..682ddbe191 100644 --- a/src/Libraries/SmartStore.Services/Blogs/IBlogService.cs +++ b/src/Libraries/SmartStore.Services/Blogs/IBlogService.cs @@ -33,9 +33,10 @@ public partial interface IBlogService /// Page index /// Page size /// A value indicating whether to show hidden records + /// The maximum age of returned blog posts /// Blog posts IPagedList GetAllBlogPosts(int storeId, int languageId, - DateTime? dateFrom, DateTime? dateTo, int pageIndex, int pageSize, bool showHidden = false); + DateTime? dateFrom, DateTime? dateTo, int pageIndex, int pageSize, bool showHidden = false, DateTime? maxAge = null); /// /// Gets all blog posts diff --git a/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs b/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs index f496feb5b4..86d8e52b2a 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CategoryService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Caching; using SmartStore.Core.Data; @@ -119,7 +120,7 @@ private void DeleteAllCategories(IList categories, bool delete) var childCategories = GetAllCategoriesByParentCategoryId(category.Id, true); DeleteAllCategories(childCategories, delete); - } + } } #endregion @@ -153,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) { @@ -185,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) { @@ -313,35 +314,24 @@ public virtual void DeleteCategory(Category category, bool deleteChilds = false) var childCategories = GetAllCategoriesByParentCategoryId(category.Id, true); DeleteAllCategories(childCategories, deleteChilds); } - - /// - /// Gets all categories - /// - /// Category name - /// Page index - /// Page size - /// A value indicating whether to show hidden records - /// Alias to be filtered - /// Whether to apply instances to the actual categories query. Never applied when is true - /// A value indicating whether categories without parent category in provided category list (source) should be ignored - /// Store identifier; 0 to load all records - /// Categories - public virtual IPagedList GetAllCategories(string categoryName = "", int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false, string alias = null, - bool applyNavigationFilters = true, bool ignoreCategoriesWithoutExistingParent = true, int storeId = 0) - { - var query = _categoryRepository.Table; - if (!showHidden) - query = query.Where(c => c.Published); + public virtual IQueryable GetCategories( + string categoryName = "", + bool showHidden = false, + string alias = null, + bool applyNavigationFilters = true, + int storeId = 0) + { + var query = _categoryRepository.Table; - if (!String.IsNullOrWhiteSpace(categoryName)) - query = query.Where(c => c.Name.Contains(categoryName) || c.FullName.Contains(categoryName)); + if (!showHidden) + query = query.Where(c => c.Published); - if (!String.IsNullOrWhiteSpace(alias)) - query = query.Where(c => c.Alias.Contains(alias)); + if (categoryName.HasValue()) + query = query.Where(c => c.Name.Contains(categoryName) || c.FullName.Contains(categoryName)); - query = query.Where(c => !c.Deleted); - query = query.OrderBy(c => c.ParentCategoryId).ThenBy(c => c.DisplayOrder); + if (alias.HasValue()) + query = query.Where(c => c.Alias.Contains(alias)); if (showHidden) { @@ -361,10 +351,36 @@ orderby cGroup.Key } } else - { + { query = ApplyHiddenCategoriesFilter(query, applyNavigationFilters, _storeContext.CurrentStore.Id); - query = query.OrderBy(c => c.ParentCategoryId).ThenBy(c => c.DisplayOrder); - } + } + + query = query.Where(c => !c.Deleted); + + return query; + } + + /// + /// Gets all categories + /// + /// Category name + /// Page index + /// Page size + /// A value indicating whether to show hidden records + /// Alias to be filtered + /// Whether to apply instances to the actual categories query. Never applied when is true + /// A value indicating whether categories without parent category in provided category list (source) should be ignored + /// Store identifier; 0 to load all records + /// Categories + public virtual IPagedList GetAllCategories(string categoryName = "", int pageIndex = 0, int pageSize = int.MaxValue, bool showHidden = false, string alias = null, + bool applyNavigationFilters = true, bool ignoreCategoriesWithoutExistingParent = true, int storeId = 0) + { + var query = GetCategories(categoryName, showHidden, alias, applyNavigationFilters, storeId); + + query = query + .OrderBy(x => x.ParentCategoryId) + .ThenBy(x => x.DisplayOrder) + .ThenBy(x => x.Name); var unsortedCategories = query.ToList(); @@ -581,24 +597,27 @@ public virtual IPagedList GetProductCategoriesByCategoryId(int if (categoryId == 0) return new PagedList(new List(), pageIndex, pageSize); - string key = string.Format(PRODUCTCATEGORIES_ALLBYCATEGORYID_KEY, showHidden, categoryId, pageIndex, pageSize, _workContext.CurrentCustomer.Id, _storeContext.CurrentStore.Id); + int storeId = _storeContext.CurrentStore.Id; + string key = string.Format(PRODUCTCATEGORIES_ALLBYCATEGORYID_KEY, showHidden, categoryId, pageIndex, pageSize, _workContext.CurrentCustomer.Id, storeId); + return _cacheManager.Get(key, () => { var query = from pc in _productCategoryRepository.Table join p in _productRepository.Table on pc.ProductId equals p.Id - where pc.CategoryId == categoryId && - !p.Deleted && - (showHidden || p.Published) - orderby pc.DisplayOrder + where pc.CategoryId == categoryId && !p.Deleted && (showHidden || p.Published) select pc; if (!showHidden) { - query = ApplyHiddenProductCategoriesFilter(query); - query = query.OrderBy(pc => pc.DisplayOrder); + query = ApplyHiddenProductCategoriesFilter(query, storeId); } + query = query + .OrderBy(pc => pc.DisplayOrder) + .ThenBy(pc => pc.Id); // required for paging! + var productCategories = new PagedList(query, pageIndex, pageSize); + return productCategories; }); } @@ -646,35 +665,89 @@ orderby pc.DisplayOrder }); } - protected virtual IQueryable ApplyHiddenProductCategoriesFilter(IQueryable query) + public virtual Multimap GetProductCategoriesByProductIds(int[] productIds, bool? hasDiscountsApplied = null, bool showHidden = false) + { + Guard.ArgumentNotNull(() => productIds); + + var query = + from pc in _productCategoryRepository.TableUntracked.Expand(x => x.Category).Expand(x => x.Category.Picture) + join c in _categoryRepository.Table on pc.CategoryId equals c.Id + where productIds.Contains(pc.ProductId) && !c.Deleted && (showHidden || c.Published) + orderby pc.DisplayOrder + select pc; + + if (hasDiscountsApplied.HasValue) + { + query = query.Where(x => x.Category.HasDiscountsApplied == hasDiscountsApplied); + } + + var list = query.ToList(); + + if (!showHidden) + { + list = list.Where(x => _aclService.Authorize(x.Category) && _storeMappingService.Authorize(x.Category)).ToList(); + } + + var map = list.ToMultimap(x => x.ProductId, x => x); + + return map; + } + + public virtual Multimap GetProductCategoriesByCategoryIds(int[] categoryIds) + { + Guard.ArgumentNotNull(() => categoryIds); + + var query = _productCategoryRepository.TableUntracked + .Where(x => categoryIds.Contains(x.CategoryId)) + .OrderBy(x => x.DisplayOrder); + + var map = query + .ToList() + .ToMultimap(x => x.CategoryId, x => x); + + return map; + } + + protected virtual IQueryable ApplyHiddenProductCategoriesFilter(IQueryable query, int storeId = 0) { + bool group = false; + //ACL (access control list) - var allowedCustomerRolesIds = _workContext.CurrentCustomer.CustomerRoles - .Where(cr => cr.Active).Select(cr => cr.Id).ToList(); + if (!QuerySettings.IgnoreAcl) + { + group = true; + var allowedCustomerRolesIds = _workContext.CurrentCustomer.CustomerRoles.Where(cr => cr.Active).Select(cr => cr.Id).ToList(); - query = from pc in query - join c in _categoryRepository.Table on pc.CategoryId equals c.Id - join acl in _aclRepository.Table - on new { c1 = c.Id, c2 = "Category" } equals new { c1 = acl.EntityId, c2 = acl.EntityName } into c_acl - from acl in c_acl.DefaultIfEmpty() - where !c.SubjectToAcl || allowedCustomerRolesIds.Contains(acl.CustomerRoleId) - select pc; + query = from pc in query + join c in _categoryRepository.Table on pc.CategoryId equals c.Id + join acl in _aclRepository.Table + on new { c1 = c.Id, c2 = "Category" } equals new { c1 = acl.EntityId, c2 = acl.EntityName } into c_acl + from acl in c_acl.DefaultIfEmpty() + where !c.SubjectToAcl || allowedCustomerRolesIds.Contains(acl.CustomerRoleId) + select pc; + } //Store mapping - var currentStoreId = _storeContext.CurrentStore.Id; - query = from pc in query - join c in _categoryRepository.Table on pc.CategoryId equals c.Id - join sm in _storeMappingRepository.Table - on new { c1 = c.Id, c2 = "Category" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into c_sm - from sm in c_sm.DefaultIfEmpty() - where !c.LimitedToStores || currentStoreId == sm.StoreId - select pc; - - //only distinct categories (group by ID) - query = from pc in query - group pc by pc.Id into pcGroup - orderby pcGroup.Key - select pcGroup.FirstOrDefault(); + if (!QuerySettings.IgnoreMultiStore && storeId > 0) + { + group = true; + query = from pc in query + join c in _categoryRepository.Table on pc.CategoryId equals c.Id + join sm in _storeMappingRepository.Table + on new { c1 = c.Id, c2 = "Category" } equals new { c1 = sm.EntityId, c2 = sm.EntityName } into c_sm + from sm in c_sm.DefaultIfEmpty() + where !c.LimitedToStores || storeId == sm.StoreId + select pc; + } + + if (group) + { + //only distinct categories (group by ID) + query = from pc in query + group pc by pc.Id into pcGroup + orderby pcGroup.Key + select pcGroup.FirstOrDefault(); + } return query; } @@ -730,7 +803,8 @@ public virtual void UpdateProductCategory(ProductCategory productCategory) _eventPublisher.EntityUpdated(productCategory); } - public virtual string GetCategoryPath(Product product, int? languageId, Func pathLookup, Action addPathToCache, Func categoryLookup) + public virtual string GetCategoryPath(Product product, int? languageId, Func pathLookup, Action addPathToCache, Func categoryLookup, + ProductCategory prodCategory = null) { if (product == null) return string.Empty; @@ -742,7 +816,7 @@ public virtual string GetCategoryPath(Product product, int? languageId, Func(); var path = new List(); - var productCategory = GetProductCategoriesByProductId(product.Id).FirstOrDefault(); + var productCategory = prodCategory ?? GetProductCategoriesByProductId(product.Id).FirstOrDefault(); if (productCategory != null && productCategory.Category != null) { diff --git a/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs b/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs index 9f84b9a8de..f786bf9c78 100644 --- a/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/CopyProductService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Media; using SmartStore.Services.Localization; @@ -83,15 +84,15 @@ 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) { var download = _downloadService.GetDownloadById(product.DownloadId); if (download != null) { - var downloadCopy = new Download() + var downloadCopy = new Download { DownloadGuid = Guid.NewGuid(), UseDownloadUrl = download.UseDownloadUrl, @@ -102,6 +103,7 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli Extension = download.Extension, IsNew = download.IsNew, }; + _downloadService.InsertDownload(downloadCopy); downloadId = downloadCopy.Id; } @@ -111,7 +113,7 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli var sampleDownload = _downloadService.GetDownloadById(product.SampleDownloadId.GetValueOrDefault()); if (sampleDownload != null) { - var sampleDownloadCopy = new Download() + var sampleDownloadCopy = new Download { DownloadGuid = Guid.NewGuid(), UseDownloadUrl = sampleDownload.UseDownloadUrl, @@ -122,6 +124,7 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli Extension = sampleDownload.Extension, IsNew = sampleDownload.IsNew }; + _downloadService.InsertDownload(sampleDownloadCopy); sampleDownloadId = sampleDownloadCopy.Id; } @@ -129,7 +132,7 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli } // product - productCopy = new Product() + productCopy = new Product { ProductTypeId = product.ProductTypeId, ParentGroupedProductId = product.ParentGroupedProductId, @@ -140,6 +143,7 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli ProductTemplateId = product.ProductTemplateId, AdminComment = product.AdminComment, ShowOnHomePage = product.ShowOnHomePage, + HomePageDisplayOrder = product.HomePageDisplayOrder, MetaKeywords = product.MetaKeywords, MetaDescription = product.MetaDescription, MetaTitle = product.MetaTitle, @@ -275,8 +279,11 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli _pictureService.LoadPictureBinary(picture), picture.MimeType, _pictureService.GetPictureSeName(newName), - true); - _productService.InsertProductPicture(new ProductPicture() + true, + false, + false); + + _productService.InsertProductPicture(new ProductPicture { ProductId = productCopy.Id, PictureId = pictureCopy.Id, @@ -288,7 +295,7 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli // product <-> categories mappings foreach (var productCategory in product.ProductCategories) { - var productCategoryCopy = new ProductCategory() + var productCategoryCopy = new ProductCategory { ProductId = productCopy.Id, CategoryId = productCategory.CategoryId, @@ -302,7 +309,7 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli // product <-> manufacturers mappings foreach (var productManufacturers in product.ProductManufacturers) { - var productManufacturerCopy = new ProductManufacturer() + var productManufacturerCopy = new ProductManufacturer { ProductId = productCopy.Id, ManufacturerId = productManufacturers.ManufacturerId, @@ -316,30 +323,28 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli // product <-> releated products mappings foreach (var relatedProduct in _productService.GetRelatedProductsByProductId1(product.Id, true)) { - _productService.InsertRelatedProduct( - new RelatedProduct() - { - ProductId1 = productCopy.Id, - ProductId2 = relatedProduct.ProductId2, - DisplayOrder = relatedProduct.DisplayOrder - }); + _productService.InsertRelatedProduct(new RelatedProduct + { + ProductId1 = productCopy.Id, + ProductId2 = relatedProduct.ProductId2, + DisplayOrder = relatedProduct.DisplayOrder + }); } // product <-> cross sells mappings foreach (var csProduct in _productService.GetCrossSellProductsByProductId1(product.Id, true)) { - _productService.InsertCrossSellProduct( - new CrossSellProduct() - { - ProductId1 = productCopy.Id, - ProductId2 = csProduct.ProductId2, - }); + _productService.InsertCrossSellProduct(new CrossSellProduct + { + ProductId1 = productCopy.Id, + ProductId2 = csProduct.ProductId2, + }); } // product specifications foreach (var productSpecificationAttribute in product.ProductSpecificationAttributes) { - var psaCopy = new ProductSpecificationAttribute() + var psaCopy = new ProductSpecificationAttribute { ProductId = productCopy.Id, SpecificationAttributeOptionId = productSpecificationAttribute.SpecificationAttributeOptionId, @@ -347,6 +352,7 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli ShowOnProductPage = productSpecificationAttribute.ShowOnProductPage, DisplayOrder = productSpecificationAttribute.DisplayOrder }; + _specificationAttributeService.InsertProductSpecificationAttribute(psaCopy); } @@ -360,9 +366,10 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli // product <-> attributes mappings var associatedAttributes = new Dictionary(); var associatedAttributeValues = new Dictionary(); + foreach (var productVariantAttribute in _productAttributeService.GetProductVariantAttributesByProductId(product.Id)) { - var productVariantAttributeCopy = new ProductVariantAttribute() + var productVariantAttributeCopy = new ProductVariantAttribute { ProductId = productCopy.Id, ProductAttributeId = productVariantAttribute.ProductAttributeId, @@ -371,15 +378,17 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli AttributeControlTypeId = productVariantAttribute.AttributeControlTypeId, DisplayOrder = productVariantAttribute.DisplayOrder }; + _productAttributeService.InsertProductVariantAttribute(productVariantAttributeCopy); //save associated value (used for combinations copying) associatedAttributes.Add(productVariantAttribute.Id, productVariantAttributeCopy.Id); // product variant attribute values var productVariantAttributeValues = _productAttributeService.GetProductVariantAttributeValues(productVariantAttribute.Id); + foreach (var productVariantAttributeValue in productVariantAttributeValues) { - var pvavCopy = new ProductVariantAttributeValue() + var pvavCopy = new ProductVariantAttributeValue { ProductVariantAttributeId = productVariantAttributeCopy.Id, Name = productVariantAttributeValue.Name, @@ -392,6 +401,7 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli LinkedProductId = productVariantAttributeValue.LinkedProductId, Quantity = productVariantAttributeValue.Quantity, }; + _productAttributeService.InsertProductVariantAttributeValue(pvavCopy); //save associated value (used for combinations copying) @@ -408,7 +418,12 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli } // attribute combinations - foreach (var combination in _productAttributeService.GetAllProductVariantAttributeCombinations(product.Id)) + using (var scope = new DbContextScope(lazyLoading: false, forceNoTracking: false)) + { + scope.LoadCollection(product, (Product p) => p.ProductVariantAttributeCombinations); + } + + foreach (var combination in product.ProductVariantAttributeCombinations) { //generate new AttributesXml according to new value IDs string newAttributesXml = ""; @@ -434,22 +449,20 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli var newPvav = _productAttributeService.GetProductVariantAttributeValueById(newPvavId); if (newPvav != null) { - newAttributesXml = _productAttributeParser.AddProductAttribute(newAttributesXml, - newPva, newPvav.Id.ToString()); + newAttributesXml = _productAttributeParser.AddProductAttribute(newAttributesXml, newPva, newPvav.Id.ToString()); } } } else { //just a text - newAttributesXml = _productAttributeParser.AddProductAttribute(newAttributesXml, - newPva, oldPvaValueStr); + newAttributesXml = _productAttributeParser.AddProductAttribute(newAttributesXml, newPva, oldPvaValueStr); } } } } } - var combinationCopy = new ProductVariantAttributeCombination() + var combinationCopy = new ProductVariantAttributeCombination { ProductId = productCopy.Id, AttributesXml = newAttributesXml, @@ -468,7 +481,7 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli BasePriceAmount = combination.BasePriceAmount, BasePriceBaseAmount = combination.BasePriceBaseAmount, DeliveryTimeId = combination.DeliveryTimeId, - QuantityUnitId = combination.QuantityUnitId, + QuantityUnitId = combination.QuantityUnitId, IsActive = combination.IsActive //IsDefaultCombination = combination.IsDefaultCombination }; @@ -478,15 +491,14 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli // tier prices foreach (var tierPrice in product.TierPrices) { - _productService.InsertTierPrice( - new TierPrice() - { - ProductId = productCopy.Id, - StoreId = tierPrice.StoreId, - CustomerRoleId = tierPrice.CustomerRoleId, - Quantity = tierPrice.Quantity, - Price = tierPrice.Price - }); + _productService.InsertTierPrice(new TierPrice + { + ProductId = productCopy.Id, + StoreId = tierPrice.StoreId, + CustomerRoleId = tierPrice.CustomerRoleId, + Quantity = tierPrice.Quantity, + Price = tierPrice.Price + }); } // product <-> discounts mapping @@ -504,8 +516,9 @@ public virtual Product CopyProduct(Product product, string newName, bool isPubli // associated products if (copyAssociatedProducts && product.ProductType != ProductType.BundledProduct) { - var searchContext = new ProductSearchContext() + var searchContext = new ProductSearchContext { + OrderBy = ProductSortingEnum.Position, ParentGroupedProductId = product.Id, PageSize = int.MaxValue, ShowHidden = true diff --git a/src/Libraries/SmartStore.Services/Catalog/ICategoryService.cs b/src/Libraries/SmartStore.Services/Catalog/ICategoryService.cs index 2942ed7276..c19242429d 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ICategoryService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ICategoryService.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; +using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Domain.Catalog; @@ -42,6 +44,22 @@ void InheritStoresIntoChildren(int categoryId, /// Whether to delete child categories or to set them to no parent. void DeleteCategory(Category category, bool deleteChilds = false); + /// + /// Gets categories + /// + /// Category name + /// A value indicating whether to show hidden records + /// Alias to be filtered + /// Whether to apply instances to the actual categories query. Never applied when is true + /// Store identifier; 0 to load all records + /// Category query + IQueryable GetCategories( + string categoryName = "", + bool showHidden = false, + string alias = null, + bool applyNavigationFilters = true, + int storeId = 0); + /// /// Gets all categories /// @@ -121,6 +139,22 @@ IPagedList GetProductCategoriesByCategoryId(int categoryId, /// Product category mapping collection IList GetProductCategoriesByProductId(int productId, bool showHidden = false); + /// + /// Gets product category mappings + /// + /// Product identifiers + /// A value indicating whether to filter categories with applied discounts + /// A value indicating whether to show hidden records + /// Map with product category mappings + Multimap GetProductCategoriesByProductIds(int[] productIds, bool? hasDiscountsApplied = null, bool showHidden = false); + + /// + /// Gets product category mappings + /// + /// Category identifiers + /// Map with product category mappings + Multimap GetProductCategoriesByCategoryIds(int[] categoryIds); + /// /// Gets a product category mapping /// @@ -148,8 +182,10 @@ IPagedList GetProductCategoriesByCategoryId(int categoryId, /// A delegate for fast (cached) path lookup /// A callback that saves the resolved path to a cache (when pathLookup returned null) /// A delegate for fast (cached) category lookup + /// First product category of product /// Category breadcrumb for product - string GetCategoryPath(Product product, int? languageId, Func pathLookup, Action addPathToCache, Func categoryLookup); + string GetCategoryPath(Product product, int? languageId, Func pathLookup, Action addPathToCache, Func categoryLookup, + ProductCategory prodCategory = null); } public static class ICategoryServiceExtensions diff --git a/src/Libraries/SmartStore.Services/Catalog/IManufacturerService.cs b/src/Libraries/SmartStore.Services/Catalog/IManufacturerService.cs index 63f8b647a8..24917da095 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IManufacturerService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IManufacturerService.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Linq; +using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Domain.Catalog; @@ -15,6 +17,14 @@ public partial interface IManufacturerService /// Manufacturer void DeleteManufacturer(Manufacturer manufacturer); + /// + /// Get manufacturer query + /// + /// A value indicating whether to show hidden records + /// Store identifier + /// Manufacturer query + IQueryable GetManufacturers(bool showHidden = false, int storeId = 0); + /// /// Gets all manufacturers /// @@ -22,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 @@ -85,6 +97,20 @@ IPagedList GetProductManufacturersByManufacturerId(int manu /// Product manufacturer mapping collection IList GetProductManufacturersByProductId(int productId, bool showHidden = false); + /// + /// Get product manufacturers by manufacturer identifiers + /// + /// Manufacturer identifiers + /// Product manufacturers + Multimap GetProductManufacturersByManufacturerIds(int[] manufacturerIds); + + /// + /// Get product manufacturers by product identifiers + /// + /// Product identifiers + /// Product manufacturers + Multimap GetProductManufacturersByProductIds(int[] productIds); + /// /// Gets a product manufacturer mapping /// diff --git a/src/Libraries/SmartStore.Services/Catalog/IPriceCalculationService.cs b/src/Libraries/SmartStore.Services/Catalog/IPriceCalculationService.cs index 703033f375..e3c7b9d5a6 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IPriceCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IPriceCalculationService.cs @@ -11,6 +11,13 @@ namespace SmartStore.Services.Catalog /// public partial interface IPriceCalculationService { + /// + /// Creates a price calculation context + /// + /// Products. null to lazy load data if required. + /// Price calculation context + PriceCalculationContext CreatePriceCalculationContext(IEnumerable products = null); + /// /// Get product special price (is valid) /// @@ -59,13 +66,15 @@ decimal GetFinalPrice(Product product, /// A value indicating whether include discounts or not for final price computation /// Shopping cart item quantity /// A product bundle item + /// Object with cargo data for better performance /// Final price decimal GetFinalPrice(Product product, Customer customer, decimal additionalCharge, bool includeDiscounts, int quantity, - ProductBundleItemData bundleItem = null); + ProductBundleItemData bundleItem = null, + PriceCalculationContext context = null); /// /// Gets the final price including bundle per-item pricing @@ -77,33 +86,43 @@ decimal GetFinalPrice(Product product, /// A value indicating whether include discounts or not for final price computation /// Shopping cart item quantity /// A product bundle item + /// Object with cargo data for better performance /// Final price - decimal GetFinalPrice(Product product, IList bundleItems, - Customer customer, decimal additionalCharge, bool includeDiscounts, int quantity, ProductBundleItemData bundleItem = null); + decimal GetFinalPrice(Product product, + IEnumerable bundleItems, + Customer customer, + decimal additionalCharge, + bool includeDiscounts, + int quantity, + ProductBundleItemData bundleItem = null, + PriceCalculationContext context = null); /// /// Get the lowest possible price for a product. /// /// Product + /// Object with cargo data for better performance /// Whether to display the from message. /// The lowest price. - decimal GetLowestPrice(Product product, out bool displayFromMessage); + decimal GetLowestPrice(Product product, PriceCalculationContext context, out bool displayFromMessage); /// /// Get the lowest price of a grouped product. /// - /// Grouped product. - /// Products associated to product. - /// The associated product with the lowest price. + /// Grouped product + /// Object with cargo data for better performance + /// Products associated to product + /// The associated product with the lowest price /// The lowest price. - decimal? GetLowestPrice(Product product, IEnumerable associatedProducts, out Product lowestPriceProduct); + decimal? GetLowestPrice(Product product, PriceCalculationContext context, IEnumerable associatedProducts, out Product lowestPriceProduct); /// /// Get the initial price including preselected attributes /// /// Product + /// Object with cargo data for better performance /// Preselected price - decimal GetPreselectedPrice(Product product); + decimal GetPreselectedPrice(Product product, PriceCalculationContext context); /// /// Gets the product cost @@ -162,13 +181,15 @@ decimal GetDiscountAmount(Product product, /// Product quantity /// Applied discount /// A product bundle item + /// Object with cargo data for better performance /// Discount amount decimal GetDiscountAmount(Product product, Customer customer, decimal additionalCharge, int quantity, out Discount appliedDiscount, - ProductBundleItemData bundleItem = null); + ProductBundleItemData bundleItem = null, + PriceCalculationContext context = null); /// diff --git a/src/Libraries/SmartStore.Services/Catalog/IProductAttributeParser.cs b/src/Libraries/SmartStore.Services/Catalog/IProductAttributeParser.cs index 21fd8bcdc7..f8565562e5 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IProductAttributeParser.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IProductAttributeParser.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -using SmartStore.Core.Domain.Catalog; using SmartStore.Collections; +using SmartStore.Core.Domain.Catalog; namespace SmartStore.Services.Catalog { @@ -14,63 +14,63 @@ public partial interface IProductAttributeParser /// /// Gets selected product variant attributes as a map of integer ids with their corresponding values. /// - /// Attributes XML + /// XML formatted attributes /// The deserialized map - Multimap DeserializeProductVariantAttributes(string attributes); - - /// - /// Gets selected product variant attributes - /// - /// Attributes - /// Selected product variant attributes - IList ParseProductVariantAttributes(string attributes); + Multimap DeserializeProductVariantAttributes(string attributesXml); - /// - /// Gets selected product variant attributes - /// - /// The attribute ids - /// Selected product variant attributes - IEnumerable ParseProductVariantAttributes(ICollection ids); + /// + /// Gets selected product variant attributes + /// + /// XML formatted attributes + /// Selected product variant attributes + IList ParseProductVariantAttributes(string attributesXml); /// /// Get product variant attribute values /// - /// Attributes + /// XML formatted attributes /// Product variant attribute values - IEnumerable ParseProductVariantAttributeValues(string attributes); + IEnumerable ParseProductVariantAttributeValues(string attributesXml); - /// - /// Gets selected product variant attribute value - /// - /// Attributes - /// Product variant attribute identifier - /// Product variant attribute value - IList ParseValues(string attributes, int productVariantAttributeId); + /// + /// Get list of product variant attribute values + /// + /// Map of combined attributes + /// Product variant attributes + /// List of product variant attribute values + IList ParseProductVariantAttributeValues(Multimap attributeCombination, IEnumerable attributes); + + /// + /// Gets selected product variant attribute value + /// + /// XML formatted attributes + /// Product variant attribute identifier + /// Product variant attribute value + IList ParseValues(string attributesXml, int productVariantAttributeId); /// /// Adds an attribute /// - /// Attributes + /// XML formatted attributes /// Product variant attribute /// Value /// Attributes - string AddProductAttribute(string attributes, ProductVariantAttribute pva, string value); + string AddProductAttribute(string attributesXml, ProductVariantAttribute pva, string value); /// /// Are attributes equal /// - /// The attributes of the first product - /// The attributes of the second product + /// The attributes of the first product + /// The attributes of the second product /// Result - bool AreProductAttributesEqual(string attributes1, string attributes2); + bool AreProductAttributesEqual(string attributeXml1, string attributeXml2); - /// - /// Finds a product variant attribute combination by attributes stored in XML - /// - /// Product - /// Attributes in XML format - /// Found product variant attribute combination - ProductVariantAttributeCombination FindProductVariantAttributeCombination(Product product, string attributesXml); + /// + /// Finds a product variant attribute combination by attributes stored in XML + /// + /// Product identifier + /// XML formatted attributes + /// Found product variant attribute combination ProductVariantAttributeCombination FindProductVariantAttributeCombination(int productId, string attributesXml); /// @@ -80,14 +80,48 @@ public partial interface IProductAttributeParser /// List items with following structure: Product.Id, ProductAttribute.Id, Product_ProductAttribute_Mapping.Id, ProductVariantAttributeValue.Id List> DeserializeQueryData(string jsonData); + /// + /// Deserializes attribute data + /// + /// List with deserialized data + /// XML formatted attributes + /// Product identifier + /// Bundle item identifier + void DeserializeQueryData(List> queryData, string attributesXml, int productId, int bundleItemId = 0); + /// /// Serializes attribute data /// + /// XML formatted attributes /// Product identifier - /// Attribute XML string /// Whether to URL encode /// Json string with attribute data - string SerializeQueryData(int productId, string attributesXml, bool urlEncode = true); + string SerializeQueryData(string attributesXml, int productId, bool urlEncode = true); + + /// + /// Serializes attribute data + /// + /// List with deserialized data + /// Whether to URL encode + /// Json string with attribute data + string SerializeQueryData(List> queryData, bool urlEncode = true); + + /// + /// Gets the URL of the product page including attributes query string + /// + /// XML formatted attributes + /// Product identifier + /// Product SEO name + /// URL of the product page including attributes query string + string GetProductUrlWithAttributes(string attributesXml, int productId, string productSeName); + + /// + /// Gets the URL of the product page including attributes query string + /// + /// Attribute query data + /// Product SEO name + /// URL of the product page including attributes query string + string GetProductUrlWithAttributes(List> queryData, string productSeName); #endregion @@ -96,26 +130,26 @@ public partial interface IProductAttributeParser /// /// Add gift card attrbibutes /// - /// Attributes + /// XML formatted attributes /// Recipient name /// Recipient email /// Sender name /// Sender email /// Message /// Attributes - string AddGiftCardAttribute(string attributes, string recipientName, + string AddGiftCardAttribute(string attributesXml, string recipientName, string recipientEmail, string senderName, string senderEmail, string giftCardMessage); /// /// Get gift card attrbibutes /// - /// Attributes + /// XML formatted attributes /// Recipient name /// Recipient email /// Sender name /// Sender email /// Message - void GetGiftCardAttribute(string attributes, out string recipientName, + void GetGiftCardAttribute(string attributesXml, out string recipientName, out string recipientEmail, out string senderName, out string senderEmail, out string giftCardMessage); diff --git a/src/Libraries/SmartStore.Services/Catalog/IProductAttributeService.cs b/src/Libraries/SmartStore.Services/Catalog/IProductAttributeService.cs index 5a5a2f8616..eacf65250e 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IProductAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IProductAttributeService.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; +using SmartStore.Collections; using SmartStore.Core.Domain.Catalog; -using SmartStore.Core.Domain.Media; +using SmartStore.Core; namespace SmartStore.Services.Catalog { @@ -59,6 +60,14 @@ public partial interface IProductAttributeService /// Product variant attribute mapping collection IList GetProductVariantAttributesByProductId(int productId); + /// + /// Gets product variant attribute mappings by multiple product identifiers + /// + /// The product identifiers + /// An optional control type filter. null loads all controls regardless of type. + /// A map with product id as key and a collection of variant attributes as value. + Multimap GetProductVariantAttributesByProductIds(int[] productIds, AttributeControlType? controlType); + /// /// Gets a product variant attribute mapping /// @@ -66,12 +75,13 @@ public partial interface IProductAttributeService /// Product variant attribute mapping ProductVariantAttribute GetProductVariantAttributeById(int productVariantAttributeId); - /// - /// Gets multiple product variant attribute mappings by their keys - /// - /// a list of keys - /// Product variant attribute mappings - IEnumerable GetProductVariantAttributesByIds(params int[] ids); + /// + /// Gets product variant attribute mappings + /// + /// Enumerable of product variant attribute mapping identifiers + /// Collection of already loaded product attribute mappings to reduce database round trips + /// + IList GetProductVariantAttributesByIds(IEnumerable productVariantAttributeIds, IEnumerable attributes = null); /// /// Inserts a product variant attribute mapping @@ -138,12 +148,30 @@ public partial interface IProductAttributeService /// Product variant attribute combination void DeleteProductVariantAttributeCombination(ProductVariantAttributeCombination combination); - /// - /// Gets all product variant attribute combinations - /// + /// + /// Gets all product variant attribute combinations + /// /// Product identifier - /// Product variant attribute combination collection - IList GetAllProductVariantAttributeCombinations(int productId); + /// Page index + /// Page size + /// Specifies whether loaded entities should be tracked by the state manager + /// Product variant attribute combination collection + IPagedList GetAllProductVariantAttributeCombinations(int productId, int pageIndex, int pageSize, bool untracked = true); + + /// + /// Gets a distinct list of picture identifiers. + /// Only pictures that are explicitly assigned to combinations are taken into account. + /// + /// Product id + /// Picture ids + IList GetAllProductVariantAttributeCombinationPictureIds(int productId); + + /// + /// Gets product variant attribute combinations by multiple product identifiers + /// + /// The product identifiers + /// A map with product id as key and a collection of product variant attribute combinations as value. + Multimap GetProductVariantAttributeCombinations(int[] productIds); /// /// Get the lowest price of all combinations for a product @@ -153,12 +181,19 @@ public partial interface IProductAttributeService decimal? GetLowestCombinationPrice(int productId); /// - /// Gets a product variant attribute combination + /// Gets a product variant attribute combination by identifier /// /// Product variant attribute combination identifier /// Product variant attribute combination ProductVariantAttributeCombination GetProductVariantAttributeCombinationById(int productVariantAttributeCombinationId); + /// + /// /// Gets a product variant attribute combination by SKU + /// + /// SKU + /// Product variant attribute combination + ProductVariantAttributeCombination GetProductVariantAttributeCombinationBySku(string sku); + /// /// Inserts a product variant attribute combination /// @@ -224,4 +259,17 @@ public partial interface IProductAttributeService #endregion } + + public static class IProductAttributeServiceExtensions + { + /// + /// Gets all product variant attribute combinations + /// + /// Product identifier + /// Product variant attribute combination collection + public static IList GetAllProductVariantAttributeCombinations(this IProductAttributeService service, int productId) + { + return service.GetAllProductVariantAttributeCombinations(productId, 0, int.MaxValue, true); + } + } } diff --git a/src/Libraries/SmartStore.Services/Catalog/IProductService.cs b/src/Libraries/SmartStore.Services/Catalog/IProductService.cs index aa3199641f..0b1f7649d5 100644 --- a/src/Libraries/SmartStore.Services/Catalog/IProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/IProductService.cs @@ -2,8 +2,11 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +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; namespace SmartStore.Services.Catalog @@ -105,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 /// @@ -151,6 +168,27 @@ public partial interface IProductService /// Product void UpdateHasDiscountsApplied(Product product); + /// + /// Get product tags by product identifiers + /// + /// Product identifiers + /// Map of product tags + Multimap GetProductTagsByProductIds(int[] productIds); + + /// + /// Get applied discounts by product identifiers + /// + /// Product identifiers + /// Map of applied discounts + Multimap GetAppliedDiscountsByProductIds(int[] productIds); + + /// + /// Get product specification attributes by product identifiers + /// + /// Product identifiers + /// Map of product specification attributes + Multimap GetProductSpecificationAttributesByProductIds(int[] productIds); + #endregion #region Related products @@ -264,6 +302,15 @@ public partial interface IProductService /// Tier price TierPrice GetTierPriceById(int tierPriceId); + /// + /// Gets tier prices by product identifiers + /// + /// Product identifiers + /// Filter tier prices by customer + /// Filter tier prices by store + /// Map of tier prices + Multimap GetTierPricesByProductIds(int[] productIds, Customer customer = null, int storeId = 0); + /// /// Inserts a tier price /// @@ -293,6 +340,14 @@ public partial interface IProductService /// Product pictures IList GetProductPicturesByProductId(int productId); + /// + /// Get product pictures by product identifiers + /// + /// Product identifiers + /// Whether to only load the first picture for each product + /// Product pictures + Multimap GetProductPicturesByProductIds(int[] productIds, bool onlyFirstPicture = false); + /// /// Gets a product picture /// @@ -349,6 +404,14 @@ public partial interface IProductService /// List of bundle items IList GetBundleItems(int bundleProductId, bool showHidden = false); + /// + /// Get bundle items by product identifiers + /// + /// Product identifiers + /// A value indicating whether to show hidden records + /// Map of bundle items + Multimap GetBundleItemsByProductIds(int[] productIds, bool showHidden = false); + #endregion } 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/ManufacturerExtensions.cs b/src/Libraries/SmartStore.Services/Catalog/ManufacturerExtensions.cs deleted file mode 100644 index 9d29a61c29..0000000000 --- a/src/Libraries/SmartStore.Services/Catalog/ManufacturerExtensions.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Collections.Generic; -using SmartStore.Core.Domain.Catalog; - -namespace SmartStore.Services.Catalog -{ - /// - /// Extensions - /// - public static class ManufacturerExtensions - { - /// - /// Returns a ProductManufacturer that has the specified values - /// - /// Source - /// Product identifier - /// Manufacturer identifier - /// A ProductManufacturer that has the specified values; otherwise null - public static ProductManufacturer FindProductManufacturer(this IList source, - int productId, int manufacturerId) - { - foreach (var productManufacturer in source) - if (productManufacturer.ProductId == productId && productManufacturer.ManufacturerId == manufacturerId) - return productManufacturer; - - return null; - } - - } -} diff --git a/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs b/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs index 2bbf0d4f65..9128610551 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ManufacturerService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Caching; using SmartStore.Core.Data; @@ -89,6 +90,32 @@ public virtual void DeleteManufacturer(Manufacturer manufacturer) UpdateManufacturer(manufacturer); } + public virtual IQueryable GetManufacturers(bool showHidden = false, int storeId = 0) + { + var query = _manufacturerRepository.Table + .Where(m => !m.Deleted); + + if (!showHidden) + query = query.Where(m => m.Published); + + 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(); + } + + return query; + } + /// /// Gets all manufacturers /// @@ -96,65 +123,43 @@ public virtual void DeleteManufacturer(Manufacturer manufacturer) /// 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 = _manufacturerRepository.Table; - if (!showHidden) - query = query.Where(m => m.Published); - if (!String.IsNullOrWhiteSpace(manufacturerName)) - query = query.Where(m => m.Name.Contains(manufacturerName)); - query = query.Where(m => !m.Deleted); - query = query.OrderBy(m => m.DisplayOrder); - - //Store mapping - if (!showHidden) - { - //Store mapping - if (!QuerySettings.IgnoreMultiStore) - { - var currentStoreId = _storeContext.CurrentStore.Id; - 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 || currentStoreId == sm.StoreId - select m; - } + var query = GetManufacturers(showHidden, storeId); - //only distinct manufacturers (group by ID) - query = from m in query - group m by m.Id into mGroup - orderby mGroup.Key - select mGroup.FirstOrDefault(); + if (manufacturerName.HasValue()) + query = query.Where(m => m.Name.Contains(manufacturerName)); - query = query.OrderBy(m => m.DisplayOrder); - } + query = query.OrderBy(m => m.DisplayOrder) + .ThenBy(m => m.Name); 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); } @@ -335,6 +340,40 @@ orderby mGroup.Key return productManufacturers; }); } + + public virtual Multimap GetProductManufacturersByManufacturerIds(int[] manufacturerIds) + { + Guard.ArgumentNotNull(() => manufacturerIds); + + var query = _productManufacturerRepository.TableUntracked + .Where(x => manufacturerIds.Contains(x.ManufacturerId)) + .OrderBy(x => x.DisplayOrder); + + var map = query + .ToList() + .ToMultimap(x => x.ManufacturerId, x => x); + + return map; + } + + public virtual Multimap GetProductManufacturersByProductIds(int[] productIds) + { + Guard.ArgumentNotNull(() => productIds); + + var query = + from pm in _productManufacturerRepository.TableUntracked.Expand(x => x.Manufacturer).Expand(x => x.Manufacturer.Picture) + join m in _manufacturerRepository.TableUntracked on pm.ManufacturerId equals m.Id + where productIds.Contains(pm.ProductId) + select pm; + + var map = query + .OrderBy(x => x.ProductId) + .ThenBy(x => x.DisplayOrder) + .ToList() + .ToMultimap(x => x.ProductId, x => x); + + return map; + } /// /// Gets a product manufacturer mapping diff --git a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationContext.cs b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationContext.cs new file mode 100644 index 0000000000..4645f4eac0 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationContext.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SmartStore.Collections; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Discounts; + +namespace SmartStore.Services.Catalog +{ + /// + /// Cargo data to reduce database round trips during price calculation + /// + public class PriceCalculationContext + { + protected List _productIds; + private List _productIdsTierPrices; + private List _productIdsAppliedDiscounts; + + private Func> _funcAttributes; + private Func> _funcAttributeCombinations; + private Func> _funcTierPrices; + private Func> _funcProductCategories; + private Func> _funcAppliedDiscounts; + + private LazyMultimap _attributes; + private LazyMultimap _attributeCombinations; + private LazyMultimap _tierPrices; + private LazyMultimap _productCategories; + private LazyMultimap _appliedDiscounts; + + public PriceCalculationContext(IEnumerable products, + Func> attributes, + Func> attributeCombinations, + Func> tierPrices, + Func> productCategories, + Func> appliedDiscounts) + { + if (products == null) + { + _productIds = new List(); + _productIdsTierPrices = new List(); + _productIdsAppliedDiscounts = new List(); + } + else + { + _productIds = new List(products.Select(x => x.Id)); + _productIdsTierPrices = new List(products.Where(x => x.HasTierPrices).Select(x => x.Id)); + _productIdsAppliedDiscounts = new List(products.Where(x => x.HasDiscountsApplied).Select(x => x.Id)); + } + + _funcAttributes = attributes; + _funcAttributeCombinations = attributeCombinations; + _funcTierPrices = tierPrices; + _funcProductCategories = productCategories; + _funcAppliedDiscounts = appliedDiscounts; + } + + public void Clear() + { + if (_attributes != null) + _attributes.Clear(); + if (_attributeCombinations != null) + _attributeCombinations.Clear(); + if (_tierPrices != null) + _tierPrices.Clear(); + if (_productCategories != null) + _productCategories.Clear(); + if (_appliedDiscounts != null) + _appliedDiscounts.Clear(); + } + + public LazyMultimap Attributes + { + get + { + if (_attributes == null) + { + _attributes = new LazyMultimap(keys => _funcAttributes(keys), _productIds); + } + return _attributes; + } + } + + public LazyMultimap AttributeCombinations + { + get + { + if (_attributeCombinations == null) + { + _attributeCombinations = new LazyMultimap(keys => _funcAttributeCombinations(keys), _productIds); + } + return _attributeCombinations; + } + } + + public LazyMultimap TierPrices + { + get + { + if (_tierPrices == null) + { + _tierPrices = new LazyMultimap(keys => _funcTierPrices(keys), _productIdsTierPrices); + } + return _tierPrices; + } + } + + public LazyMultimap ProductCategories + { + get + { + if (_productCategories == null) + { + _productCategories = new LazyMultimap(keys => _funcProductCategories(keys), _productIds); + } + return _productCategories; + } + } + + public LazyMultimap AppliedDiscounts + { + get + { + if (_appliedDiscounts == null) + { + _appliedDiscounts = new LazyMultimap(keys => _funcAppliedDiscounts(keys), _productIdsAppliedDiscounts); + } + return _appliedDiscounts; + } + } + + public void Collect(IEnumerable productIds) + { + Attributes.Collect(productIds); + AttributeCombinations.Collect(productIds); + TierPrices.Collect(productIds); + ProductCategories.Collect(productIds); + AppliedDiscounts.Collect(productIds); + } + } +} diff --git a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs index 4a7ea62463..ca45f495d7 100644 --- a/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/PriceCalculationService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; @@ -26,7 +27,7 @@ public partial class PriceCalculationService : IPriceCalculationService private readonly CatalogSettings _catalogSettings; private readonly IProductAttributeService _productAttributeService; private readonly IDownloadService _downloadService; - private readonly ICommonServices _commonServices; + private readonly ICommonServices _services; private readonly HttpRequestBase _httpRequestBase; private readonly ITaxService _taxService; @@ -39,7 +40,7 @@ public PriceCalculationService( CatalogSettings catalogSettings, IProductAttributeService productAttributeService, IDownloadService downloadService, - ICommonServices commonServices, + ICommonServices services, HttpRequestBase httpRequestBase, ITaxService taxService) { @@ -51,7 +52,7 @@ public PriceCalculationService( this._catalogSettings = catalogSettings; this._productAttributeService = productAttributeService; this._downloadService = downloadService; - this._commonServices = commonServices; + this._services = services; this._httpRequestBase = httpRequestBase; this._taxService = taxService; } @@ -64,31 +65,44 @@ public PriceCalculationService( /// Product /// Customer /// Discounts - protected virtual IList GetAllowedDiscounts(Product product, Customer customer) + protected virtual IList GetAllowedDiscounts(Product product, Customer customer, PriceCalculationContext context = null) { - var allowedDiscounts = new List(); + var result = new List(); if (_catalogSettings.IgnoreDiscounts) - return allowedDiscounts; + return result; if (product.HasDiscountsApplied) { //we use this property ("HasDiscountsApplied") for performance optimziation to avoid unnecessary database calls - foreach (var discount in product.AppliedDiscounts) - { - if (_discountService.IsDiscountValid(discount, customer) && - discount.DiscountType == DiscountType.AssignedToSkus && - !allowedDiscounts.ContainsDiscount(discount)) + IEnumerable appliedDiscounts = null; + + if (context == null) + appliedDiscounts = product.AppliedDiscounts; + else + appliedDiscounts = context.AppliedDiscounts.Load(product.Id); + + if (appliedDiscounts != null) + { + foreach (var discount in appliedDiscounts) { - allowedDiscounts.Add(discount); + if (discount.DiscountType == DiscountType.AssignedToSkus && !result.Any(x => x.Id == discount.Id) && _discountService.IsDiscountValid(discount, customer)) + { + result.Add(discount); + } } - } + } } - //performance optimization - //load all category discounts just to ensure that we have at least one + //performance optimization. load all category discounts just to ensure that we have at least one if (_discountService.GetAllDiscounts(DiscountType.AssignedToCategories).Any()) { - var productCategories = _categoryService.GetProductCategoriesByProductId(product.Id); + IEnumerable productCategories = null; + + if (context == null) + productCategories = _categoryService.GetProductCategoriesByProductId(product.Id); + else + productCategories = context.ProductCategories.Load(product.Id); + if (productCategories != null) { foreach (var productCategory in productCategories) @@ -99,20 +113,20 @@ protected virtual IList GetAllowedDiscounts(Product product, Customer { //we use this property ("HasDiscountsApplied") for performance optimziation to avoid unnecessary database calls var categoryDiscounts = category.AppliedDiscounts; + foreach (var discount in categoryDiscounts) { - if (_discountService.IsDiscountValid(discount, customer) && - discount.DiscountType == DiscountType.AssignedToCategories && - !allowedDiscounts.ContainsDiscount(discount)) + if (discount.DiscountType == DiscountType.AssignedToCategories && !result.Any(x => x.Id == discount.Id) && _discountService.IsDiscountValid(discount, customer)) { - allowedDiscounts.Add(discount); + result.Add(discount); } } } } } } - return allowedDiscounts; + + return result; } /// @@ -122,20 +136,34 @@ protected virtual IList GetAllowedDiscounts(Product product, Customer /// Customer /// Quantity /// Price - protected virtual decimal? GetMinimumTierPrice(Product product, Customer customer, int quantity) + protected virtual decimal? GetMinimumTierPrice(Product product, Customer customer, int quantity, PriceCalculationContext context = null) { if (!product.HasTierPrices) return decimal.Zero; - var tierPrices = product.TierPrices - .OrderBy(tp => tp.Quantity) - .FilterByStore(_commonServices.StoreContext.CurrentStore.Id) - .FilterForCustomer(customer) - .ToList() - .RemoveDuplicatedQuantities(); + IEnumerable tierPrices = null; + + if (context == null) + { + tierPrices = product.TierPrices + .OrderBy(tp => tp.Quantity) + .FilterByStore(_services.StoreContext.CurrentStore.Id) + .FilterForCustomer(customer) + .ToList() + .RemoveDuplicatedQuantities(); + } + else + { + tierPrices = context.TierPrices.Load(product.Id) + .RemoveDuplicatedQuantities(); + } + + if (tierPrices == null) + return decimal.Zero; int previousQty = 1; decimal? previousPrice = null; + foreach (var tierPrice in tierPrices) { //check quantity @@ -152,7 +180,7 @@ protected virtual IList GetAllowedDiscounts(Product product, Customer return previousPrice; } - protected virtual decimal GetPreselectedPrice(Product product, ProductBundleItemData bundleItem, IList bundleItems) + protected virtual decimal GetPreselectedPrice(Product product, PriceCalculationContext context, ProductBundleItemData bundleItem, IEnumerable bundleItems) { var taxRate = decimal.Zero; var attributesTotalPriceBase = decimal.Zero; @@ -161,77 +189,80 @@ protected virtual decimal GetPreselectedPrice(Product product, ProductBundleItem var isBundleItemPricing = (bundleItem != null && bundleItem.Item.BundleProduct.BundlePerItemPricing); var isBundlePricing = (bundleItem != null && !bundleItem.Item.BundleProduct.BundlePerItemPricing); var bundleItemId = (bundleItem == null ? 0 : bundleItem.Item.Id); - var attributes = (isBundle ? new List() : _productAttributeService.GetProductVariantAttributesByProductId(product.Id)); + var selectedAttributes = new NameValueCollection(); - var clearDataMerging = false; - List selectedAttributeValues = null; + var selectedAttributeValues = new List(); + var attributes = context.Attributes.Load(product.Id); - foreach (var attribute in attributes) + // 1. fill selectedAttributes with initially selected attributes + foreach (var attribute in attributes.Where(x => x.ProductVariantAttributeValues.Count > 0 && x.ShouldHaveValues())) { int preSelectedValueId = 0; ProductVariantAttributeValue defaultValue = null; - - if (attribute.ShouldHaveValues()) + var selectedValueIds = new List(); + var pvaValues = attribute.ProductVariantAttributeValues; + + foreach (var pvaValue in pvaValues) { - var pvaValues = _productAttributeService.GetProductVariantAttributeValues(attribute.Id); - if (pvaValues.Count == 0) - continue; + ProductBundleItemAttributeFilter attributeFilter = null; - foreach (var pvaValue in pvaValues) - { - ProductBundleItemAttributeFilter attributeFilter = null; - - if (bundleItem.FilterOut(pvaValue, out attributeFilter)) - continue; + if (bundleItem.FilterOut(pvaValue, out attributeFilter)) + continue; - if (preSelectedValueId == 0 && attributeFilter != null && attributeFilter.IsPreSelected) - preSelectedValueId = attributeFilter.AttributeValueId; + if (preSelectedValueId == 0 && attributeFilter != null && attributeFilter.IsPreSelected) + preSelectedValueId = attributeFilter.AttributeValueId; - if (!isBundlePricing && pvaValue.IsPreSelected) - { - decimal attributeValuePriceAdjustment = GetProductVariantAttributeValuePriceAdjustment(pvaValue); - decimal priceAdjustmentBase = _taxService.GetProductPrice(product, attributeValuePriceAdjustment, out taxRate); + if (!isBundlePricing && pvaValue.IsPreSelected) + { + decimal attributeValuePriceAdjustment = GetProductVariantAttributeValuePriceAdjustment(pvaValue); + decimal priceAdjustmentBase = _taxService.GetProductPrice(product, attributeValuePriceAdjustment, out taxRate); - preSelectedPriceAdjustmentBase = decimal.Add(preSelectedPriceAdjustmentBase, priceAdjustmentBase); - } + preSelectedPriceAdjustmentBase = decimal.Add(preSelectedPriceAdjustmentBase, priceAdjustmentBase); } + } - // value pre-selected by a bundle item filter discards the default pre-selection - if (preSelectedValueId != 0 && (defaultValue = pvaValues.FirstOrDefault(x => x.Id == preSelectedValueId)) != null) - defaultValue.IsPreSelected = true; - - if (defaultValue == null) - defaultValue = pvaValues.FirstOrDefault(x => x.IsPreSelected); - - if (defaultValue == null && attribute.IsRequired) - defaultValue = pvaValues.First(); - - if (defaultValue != null) - selectedAttributes.AddProductAttribute(attribute.ProductAttributeId, attribute.Id, defaultValue.Id, product.Id, bundleItemId); + // value pre-selected by a bundle item filter discards the default pre-selection + if (preSelectedValueId != 0 && (defaultValue = pvaValues.FirstOrDefault(x => x.Id == preSelectedValueId)) != null) + { + //defaultValue.IsPreSelected = true; + selectedAttributeValues.Add(defaultValue); + selectedAttributes.AddProductAttribute(attribute.ProductAttributeId, attribute.Id, defaultValue.Id, product.Id, bundleItemId); + } + else + { + foreach (var value in pvaValues.Where(x => x.IsPreSelected)) + { + selectedAttributeValues.Add(value); + selectedAttributes.AddProductAttribute(attribute.ProductAttributeId, attribute.Id, value.Id, product.Id, bundleItemId); + } } } + // 2. find attribute combination for selected attributes and merge it if (!isBundle && selectedAttributes.Count > 0) { - string attributeXml = selectedAttributes.CreateSelectedAttributesXml(product.Id, attributes, _productAttributeParser, _commonServices.Localization, + var attributeXml = selectedAttributes.CreateSelectedAttributesXml(product.Id, attributes, _productAttributeParser, _services.Localization, _downloadService, _catalogSettings, _httpRequestBase, new List(), true, bundleItemId); - selectedAttributeValues = _productAttributeParser.ParseProductVariantAttributeValues(attributeXml).ToList(); - - var combinations = _productAttributeService.GetAllProductVariantAttributeCombinations(product.Id); + var combinations = context.AttributeCombinations.Load(product.Id); var selectedCombination = combinations.FirstOrDefault(x => _productAttributeParser.AreProductAttributesEqual(x.AttributesXml, attributeXml)); - if (selectedCombination != null && selectedCombination.IsActive) + if (selectedCombination != null && selectedCombination.IsActive && selectedCombination.Price.HasValue) { - clearDataMerging = true; - product.MergeWithCombination(selectedCombination); + 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); } } if (_catalogSettings.EnableDynamicPriceUpdate && !isBundlePricing) { - if (selectedAttributeValues != null) + if (selectedAttributeValues.Count > 0) { selectedAttributeValues.Each(x => attributesTotalPriceBase += GetProductVariantAttributeValuePriceAdjustment(x)); } @@ -246,17 +277,23 @@ protected virtual decimal GetPreselectedPrice(Product product, ProductBundleItem bundleItem.AdditionalCharge = attributesTotalPriceBase; } - var result = GetFinalPrice(product, bundleItems, _commonServices.WorkContext.CurrentCustomer, attributesTotalPriceBase, true, 1, bundleItem); - - if (clearDataMerging && product.MergedDataValues != null) - { - // GetPreselectedPrice should not leave product with merged values. - product.MergedDataValues.Clear(); - } - + var result = GetFinalPrice(product, bundleItems, _services.WorkContext.CurrentCustomer, attributesTotalPriceBase, true, 1, bundleItem, context); return result; } + public virtual PriceCalculationContext CreatePriceCalculationContext(IEnumerable products = null) + { + var context = new PriceCalculationContext(products, + x => _productAttributeService.GetProductVariantAttributesByProductIds(x, null), + x => _productAttributeService.GetProductVariantAttributeCombinations(x), + x => _productService.GetTierPricesByProductIds(x, _services.WorkContext.CurrentCustomer, _services.StoreContext.CurrentStore.Id), + x => _categoryService.GetProductCategoriesByProductIds(x, true), + x => _productService.GetAppliedDiscountsByProductIds(x) + ); + + return context; + } + #endregion #region Methods @@ -301,7 +338,7 @@ protected virtual decimal GetPreselectedPrice(Product product, ProductBundleItem public virtual decimal GetFinalPrice(Product product, bool includeDiscounts) { - var customer = _commonServices.WorkContext.CurrentCustomer; + var customer = _services.WorkContext.CurrentCustomer; return GetFinalPrice(product, customer, includeDiscounts); } @@ -335,23 +372,14 @@ public virtual decimal GetFinalPrice(Product product, return GetFinalPrice(product, customer, additionalCharge, includeDiscounts, 1); } - /// - /// Gets the final price - /// - /// Product - /// The customer - /// Additional charge - /// A value indicating whether include discounts or not for final price computation - /// Shopping cart item quantity - /// A product bundle item - /// Final price public virtual decimal GetFinalPrice( Product product, Customer customer, decimal additionalCharge, bool includeDiscounts, int quantity, - ProductBundleItemData bundleItem = null) + ProductBundleItemData bundleItem = null, + PriceCalculationContext context = null) { //initial price decimal result = product.Price; @@ -364,7 +392,7 @@ public virtual decimal GetFinalPrice( //tier prices if (product.HasTierPrices && !bundleItem.IsValid()) { - decimal? tierPrice = GetMinimumTierPrice(product, customer, quantity); + decimal? tierPrice = GetMinimumTierPrice(product, customer, quantity, context); if (tierPrice.HasValue) result = Math.Min(result, tierPrice.Value); } @@ -373,37 +401,29 @@ public virtual decimal GetFinalPrice( if (includeDiscounts) { Discount appliedDiscount = null; - decimal discountAmount = GetDiscountAmount(product, customer, additionalCharge, quantity, out appliedDiscount, bundleItem); + decimal discountAmount = GetDiscountAmount(product, customer, additionalCharge, quantity, out appliedDiscount, bundleItem, context); result = result + additionalCharge - discountAmount; } else { result = result + additionalCharge; } + if (result < decimal.Zero) result = decimal.Zero; + return result; } - /// - /// Gets the final price including bundle per-item pricing - /// - /// Product - /// Bundle items - /// The customer - /// Additional charge - /// A value indicating whether include discounts or not for final price computation - /// Shopping cart item quantity - /// A product bundle item - /// Final price public virtual decimal GetFinalPrice( Product product, - IList bundleItems, + IEnumerable bundleItems, Customer customer, decimal additionalCharge, bool includeDiscounts, int quantity, - ProductBundleItemData bundleItem = null) + ProductBundleItemData bundleItem = null, + PriceCalculationContext context = null) { if (product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing) { @@ -412,23 +432,24 @@ public virtual decimal GetFinalPrice( foreach (var itemData in items.Where(x => x.IsValid())) { - decimal itemPrice = GetFinalPrice(itemData.Item.Product, customer, itemData.AdditionalCharge, includeDiscounts, 1, itemData); + decimal itemPrice = GetFinalPrice(itemData.Item.Product, customer, itemData.AdditionalCharge, includeDiscounts, 1, itemData, context); result = result + decimal.Multiply(itemPrice, itemData.Item.Quantity); } return (result < decimal.Zero ? decimal.Zero : result); } - return GetFinalPrice(product, customer, additionalCharge, includeDiscounts, quantity, bundleItem); + return GetFinalPrice(product, customer, additionalCharge, includeDiscounts, quantity, bundleItem, context); } /// /// Get the lowest possible price for a product. /// /// Product + /// Object with cargo data for better performance /// Whether to display the from message. /// The lowest price. - public virtual decimal GetLowestPrice(Product product, out bool displayFromMessage) + public virtual decimal GetLowestPrice(Product product, PriceCalculationContext context, out bool displayFromMessage) { if (product == null) throw new ArgumentNullException("product"); @@ -436,38 +457,16 @@ public virtual decimal GetLowestPrice(Product product, out bool displayFromMessa if (product.ProductType == ProductType.GroupedProduct) throw Error.InvalidOperation("Choose the other override for products of type grouped product."); - displayFromMessage = false; + // note: attribute price adjustments were never regarded here cause of many reasons + + if (context == null) + context = CreatePriceCalculationContext(); - IList bundleItems = null; bool isBundlePerItemPricing = (product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing); - if (isBundlePerItemPricing) - { - bundleItems = _productService.GetBundleItems(product.Id); + displayFromMessage = isBundlePerItemPricing; - // sepcial case: one bundle item with one attribute and one attribute value and one attribute combination - if (bundleItems.Count == 1) - { - var firstBundleItem = bundleItems.First(); - if (firstBundleItem.Item.Product.ProductVariantAttributes.Count == 1) - { - var firstAttribute = firstBundleItem.Item.Product.ProductVariantAttributes.First(); - if (firstAttribute.ProductVariantAttributeValues.Count == 1) - { - var firstAttributeValue = firstAttribute.ProductVariantAttributeValues.First(); - firstBundleItem.AdditionalCharge = firstAttributeValue.PriceAdjustment; - - var combinations = _productAttributeService.GetAllProductVariantAttributeCombinations(firstBundleItem.Item.ProductId); - if (combinations.Count == 1) - { - firstBundleItem.Item.Product.MergeWithCombination(combinations.First()); - } - } - } - } - } - - decimal lowestPrice = GetFinalPrice(product, bundleItems, _commonServices.WorkContext.CurrentCustomer, decimal.Zero, true, int.MaxValue); + var lowestPrice = GetFinalPrice(product, null, _services.WorkContext.CurrentCustomer, decimal.Zero, true, int.MaxValue, null, context); if (product.LowestAttributeCombinationPrice.HasValue && product.LowestAttributeCombinationPrice.Value < lowestPrice) { @@ -475,29 +474,29 @@ public virtual decimal GetLowestPrice(Product product, out bool displayFromMessa displayFromMessage = true; } - if (!displayFromMessage) + if (lowestPrice == decimal.Zero && product.Price == decimal.Zero) { - foreach (var attribute in product.ProductVariantAttributes) - { - if (attribute.ProductVariantAttributeValues.Any(x => x.PriceAdjustment != decimal.Zero)) - { - displayFromMessage = true; - break; - } - } + lowestPrice = product.LowestAttributeCombinationPrice ?? decimal.Zero; + } + + if (!displayFromMessage && product.ProductType != ProductType.BundledProduct) + { + var attributes = context.Attributes.Load(product.Id); + displayFromMessage = attributes.Any(x => x.ProductVariantAttributeValues.Any(y => y.PriceAdjustment != decimal.Zero)); + } + + if (!displayFromMessage && product.HasTierPrices && !isBundlePerItemPricing) + { + var tierPrices = context.TierPrices.Load(product.Id) + .RemoveDuplicatedQuantities(); + + displayFromMessage = (tierPrices.Count > 0 && !(tierPrices.Count == 1 && tierPrices.First().Quantity <= 1)); } return lowestPrice; } - /// - /// Get the lowest price of a grouped product. - /// - /// Grouped product. - /// Products associated to product. - /// The associated product with the lowest price. - /// The lowest price. - public virtual decimal? GetLowestPrice(Product product, IEnumerable associatedProducts, out Product lowestPriceProduct) + public virtual decimal? GetLowestPrice(Product product, PriceCalculationContext context, IEnumerable associatedProducts, out Product lowestPriceProduct) { if (product == null) throw new ArgumentNullException("product"); @@ -508,12 +507,16 @@ public virtual decimal GetLowestPrice(Product product, out bool displayFromMessa if (product.ProductType != ProductType.GroupedProduct) throw Error.InvalidOperation("Choose the other override for products not of type grouped product."); - lowestPriceProduct = product; + lowestPriceProduct = null; decimal? lowestPrice = null; + var customer = _services.WorkContext.CurrentCustomer; + + if (context == null) + context = CreatePriceCalculationContext(); foreach (var associatedProduct in associatedProducts) { - var tmpPrice = GetFinalPrice(associatedProduct, _commonServices.WorkContext.CurrentCustomer, decimal.Zero, true, int.MaxValue); + var tmpPrice = GetFinalPrice(associatedProduct, customer, decimal.Zero, true, int.MaxValue, null, context); if (associatedProduct.LowestAttributeCombinationPrice.HasValue && associatedProduct.LowestAttributeCombinationPrice.Value < tmpPrice) { @@ -526,6 +529,10 @@ public virtual decimal GetLowestPrice(Product product, out bool displayFromMessa lowestPriceProduct = associatedProduct; } } + + if (lowestPriceProduct == null) + lowestPriceProduct = associatedProducts.FirstOrDefault(); + return lowestPrice; } @@ -533,33 +540,44 @@ public virtual decimal GetLowestPrice(Product product, out bool displayFromMessa /// Get the initial price including preselected attributes /// /// Product + /// Object with cargo data for better performance /// Preselected price - public virtual decimal GetPreselectedPrice(Product product) + public virtual decimal GetPreselectedPrice(Product product, PriceCalculationContext context) { if (product == null) throw new ArgumentNullException("product"); var result = decimal.Zero; + if (context == null) + context = CreatePriceCalculationContext(); + if (product.ProductType == ProductType.BundledProduct) { var bundleItems = _productService.GetBundleItems(product.Id); + var productIds = bundleItems.Select(x => x.Item.ProductId).ToList(); + productIds.Add(product.Id); + + context.Collect(productIds); + foreach (var bundleItem in bundleItems.Where(x => x.Item.Product.CanBeBundleItem())) { // fetch bundleItems.AdditionalCharge for all bundle items - var unused = GetPreselectedPrice(bundleItem.Item.Product, bundleItem, bundleItems); + var unused = GetPreselectedPrice(bundleItem.Item.Product, context, bundleItem, bundleItems); } - result = GetPreselectedPrice(product, null, bundleItems); + result = GetPreselectedPrice(product, context, null, bundleItems); } else { - result = GetPreselectedPrice(product, null, null); + result = GetPreselectedPrice(product, context, null, null); } + return result; } + /// /// Gets the product cost /// @@ -576,6 +594,7 @@ public virtual decimal GetProductCost(Product product, string attributesXml) _productAttributeParser .ParseProductVariantAttributeValues(attributesXml) .Where(x => x.ValueType == ProductVariantAttributeValueType.ProductLinkage) + .ToList() .Each(x => { var linkedProduct = _productService.GetProductById(x.LinkedProductId); @@ -594,7 +613,7 @@ public virtual decimal GetProductCost(Product product, string attributesXml) /// Discount amount public virtual decimal GetDiscountAmount(Product product) { - var customer = _commonServices.WorkContext.CurrentCustomer; + var customer = _services.WorkContext.CurrentCustomer; return GetDiscountAmount(product, customer, decimal.Zero); } @@ -641,23 +660,14 @@ public virtual decimal GetDiscountAmount(Product product, return GetDiscountAmount(product, customer, additionalCharge, 1, out appliedDiscount); } - /// - /// Gets discount amount - /// - /// Product - /// The customer - /// Additional charge - /// Product quantity - /// Applied discount - /// A product bundle item - /// Discount amount public virtual decimal GetDiscountAmount( Product product, Customer customer, decimal additionalCharge, int quantity, out Discount appliedDiscount, - ProductBundleItemData bundleItem = null) + ProductBundleItemData bundleItem = null, + PriceCalculationContext context = null) { appliedDiscount = null; decimal appliedDiscountAmount = decimal.Zero; @@ -667,14 +677,14 @@ public virtual decimal GetDiscountAmount( { if (bundleItem.Item.Discount.HasValue && bundleItem.Item.BundleProduct.BundlePerItemPricing) { - appliedDiscount = new Discount() + appliedDiscount = new Discount { UsePercentage = bundleItem.Item.DiscountPercentage, DiscountPercentage = bundleItem.Item.Discount.Value, DiscountAmount = bundleItem.Item.Discount.Value }; - finalPriceWithoutDiscount = GetFinalPrice(product, customer, additionalCharge, false, quantity, bundleItem); + finalPriceWithoutDiscount = GetFinalPrice(product, customer, additionalCharge, false, quantity, bundleItem, context); appliedDiscountAmount = appliedDiscount.GetDiscountAmount(finalPriceWithoutDiscount); } } @@ -686,13 +696,13 @@ public virtual decimal GetDiscountAmount( return appliedDiscountAmount; } - var allowedDiscounts = GetAllowedDiscounts(product, customer); + var allowedDiscounts = GetAllowedDiscounts(product, customer, context); if (allowedDiscounts.Count == 0) { return appliedDiscountAmount; } - finalPriceWithoutDiscount = GetFinalPrice(product, customer, additionalCharge, false, quantity, bundleItem); + finalPriceWithoutDiscount = GetFinalPrice(product, customer, additionalCharge, false, quantity, bundleItem, context); appliedDiscount = allowedDiscounts.GetPreferredDiscount(finalPriceWithoutDiscount); if (appliedDiscount != null) @@ -740,7 +750,7 @@ public virtual decimal GetUnitPrice(OrganizedShoppingCartItem shoppingCartItem, { foreach (var bundleItem in shoppingCartItem.ChildItems) { - bundleItem.Item.Product.MergeWithCombination(bundleItem.Item.AttributesXml); + bundleItem.Item.Product.MergeWithCombination(bundleItem.Item.AttributesXml, _productAttributeParser); } var bundleItems = shoppingCartItem.ChildItems.Where(x => x.BundleItemData.IsValid()).Select(x => x.BundleItemData).ToList(); @@ -750,13 +760,20 @@ public virtual decimal GetUnitPrice(OrganizedShoppingCartItem shoppingCartItem, } else { - decimal attributesTotalPrice = decimal.Zero; - var pvaValues = _productAttributeParser.ParseProductVariantAttributeValues(shoppingCartItem.Item.AttributesXml); + product.MergeWithCombination(shoppingCartItem.Item.AttributesXml, _productAttributeParser); + + 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); @@ -798,7 +815,7 @@ public virtual decimal GetDiscountAmount(OrganizedShoppingCartItem shoppingCartI { decimal attributesTotalPrice = decimal.Zero; - var pvaValues = _productAttributeParser.ParseProductVariantAttributeValues(shoppingCartItem.Item.AttributesXml); + var pvaValues = _productAttributeParser.ParseProductVariantAttributeValues(shoppingCartItem.Item.AttributesXml).ToList(); foreach (var pvaValue in pvaValues) { attributesTotalPrice += GetProductVariantAttributeValuePriceAdjustment(pvaValue); diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs index 35b07ac42f..cf4531f322 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeParser.cs @@ -1,44 +1,60 @@ using System; -using System.Linq; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Web; +using System.Web.Mvc; +using System.Web.Routing; using System.Xml; -using SmartStore.Core.Domain.Catalog; -using SmartStore.Collections; +using System.Xml.Linq; using Newtonsoft.Json; -using System.Web; +using SmartStore.Collections; +using SmartStore.Core.Caching; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Catalog; namespace SmartStore.Services.Catalog { - /// - /// Product attribute parser - /// - public partial class ProductAttributeParser : IProductAttributeParser + /// + /// Product attribute parser + /// + public partial class ProductAttributeParser : IProductAttributeParser { - private readonly IProductAttributeService _productAttributeService; + // 0 = ProductId, 1 = AttributeXml Hash + private const string ATTRIBUTECOMBINATION_BY_ID_HASH = "SmartStore.parsedattributecombination.id-{0}-{1}"; + + private readonly IProductAttributeService _productAttributeService; + private readonly IRepository _pvacRepository; + private readonly ICacheManager _cacheManager; - public ProductAttributeParser(IProductAttributeService productAttributeService) + public ProductAttributeParser( + IProductAttributeService productAttributeService, + IRepository pvacRepository, + ICacheManager cacheManager) { - this._productAttributeService = productAttributeService; + _productAttributeService = productAttributeService; + _pvacRepository = pvacRepository; + _cacheManager = cacheManager; } - #region Product attributes + #region Product attributes - /// + /// /// Gets selected product variant attribute identifiers /// - /// Attributes + /// Attributes /// Selected product variant attribute identifiers - private IEnumerable ParseProductVariantAttributeIds(string attributes) + private IEnumerable ParseProductVariantAttributeIds(string attributesXml) { var ids = new List(); - if (String.IsNullOrEmpty(attributes)) + if (String.IsNullOrEmpty(attributesXml)) yield break; try { var xmlDoc = new XmlDocument(); - xmlDoc.LoadXml(attributes); + xmlDoc.LoadXml(attributesXml); var nodeList = xmlDoc.SelectNodes(@"//Attributes/ProductVariantAttribute"); foreach (var node in nodeList.Cast()) @@ -55,124 +71,177 @@ private IEnumerable ParseProductVariantAttributeIds(string attributes) } } finally { } - } - public virtual Multimap DeserializeProductVariantAttributes(string attributes) - { - var attrs = new Multimap(); - if (String.IsNullOrEmpty(attributes)) - return attrs; - - try - { - var xmlDoc = new XmlDocument(); - xmlDoc.LoadXml(attributes); - - var nodeList1 = xmlDoc.SelectNodes(@"//Attributes/ProductVariantAttribute"); - foreach (var node1 in nodeList1.Cast()) - { - string sid = node1.GetAttribute("ID").Trim(); - if (sid.HasValue()) - { - int id = 0; - if (int.TryParse(sid, out id)) - { - - var nodeList2 = node1.SelectNodes(@"ProductVariantAttributeValue/Value").Cast(); - foreach (var node2 in nodeList2) - { - string value = node2.InnerText.Trim(); - attrs.Add(id, value); - } - } - } - } - } - catch (Exception exc) - { - Debug.Write(exc.ToString()); - } + //public virtual Multimap DeserializeProductVariantAttributes(string attributesXml) + //{ + // var attrs = new Multimap(); + // if (String.IsNullOrEmpty(attributesXml)) + // return attrs; + + // try + // { + // var xmlDoc = new XmlDocument(); + // xmlDoc.LoadXml(attributesXml); + + // var nodeList1 = xmlDoc.SelectNodes(@"//Attributes/ProductVariantAttribute"); + // foreach (var node1 in nodeList1.Cast()) + // { + // string sid = node1.GetAttribute("ID").Trim(); + // if (sid.HasValue()) + // { + // int id = 0; + // if (int.TryParse(sid, out id)) + // { + + // var nodeList2 = node1.SelectNodes(@"ProductVariantAttributeValue/Value").Cast(); + // foreach (var node2 in nodeList2) + // { + // string value = node2.InnerText.Trim(); + // attrs.Add(id, value); + // } + // } + // } + // } + // } + // catch (Exception exc) + // { + // Debug.Write(exc.ToString()); + // } + + // return attrs; + //} + + public virtual Multimap DeserializeProductVariantAttributes(string attributesXml) + { + var attrs = new Multimap(); + if (String.IsNullOrEmpty(attributesXml)) + return attrs; - return attrs; - } + try + { + var doc = XDocument.Parse(attributesXml); - /// - /// Gets selected product variant attributes - /// - /// Attributes - /// Selected product variant attributes - public virtual IList ParseProductVariantAttributes(string attributes) - { - var pvaCollection = new List(); - var ids = ParseProductVariantAttributeIds(attributes); - return this.ParseProductVariantAttributes(ids.ToList()).ToList(); - } + // Attributes/ProductVariantAttribute + foreach (var node1 in doc.Descendants("ProductVariantAttribute")) + { + string sid = node1.Attribute("ID").Value; + if (sid.HasValue()) + { + int id = 0; + if (int.TryParse(sid, out id)) + { + // ProductVariantAttributeValue/Value + foreach (var node2 in node1.Descendants("Value")) + { + attrs.Add(id, node2.Value); + } + } + } + } + } + catch (Exception exc) + { + Debug.Write(exc.ToString()); + } - public virtual IEnumerable ParseProductVariantAttributes(ICollection ids) - { + return attrs; + } - if (ids != null) - { - if (ids.Count == 1) - { - var pva = _productAttributeService.GetProductVariantAttributeById(ids.ElementAt(0)); - if (pva != null) - { - return new ProductVariantAttribute[] { pva }; - } - } - else - { - return _productAttributeService.GetProductVariantAttributesByIds(ids.ToArray()).ToList(); - } - } + /// + /// Gets selected product variant attributes + /// + /// Attributes + /// Selected product variant attributes + public virtual IList ParseProductVariantAttributes(string attributesXml) + { + var ids = ParseProductVariantAttributeIds(attributesXml); - return Enumerable.Empty(); - } + return _productAttributeService.GetProductVariantAttributesByIds(ids.ToList()); + } - /// - /// Get product variant attribute values - /// - /// Attributes - /// Product variant attribute values - public virtual IEnumerable ParseProductVariantAttributeValues(string attributes) + public virtual IEnumerable ParseProductVariantAttributeValues(string attributeXml) { - var pvaValues = Enumerable.Empty(); + //var pvaValues = Enumerable.Empty(); - var attrs = DeserializeProductVariantAttributes(attributes); - var pvaCollection = ParseProductVariantAttributes(attrs.Keys); + var allIds = new List(); + var attrs = DeserializeProductVariantAttributes(attributeXml); + var pvaCollection = _productAttributeService.GetProductVariantAttributesByIds(attrs.Keys); foreach (var pva in pvaCollection) { if (!pva.ShouldHaveValues()) continue; - var pvaValuesStr = attrs[pva.Id]; //ParseValues(attributes, pva.Id); - var ids = from id in pvaValuesStr - where id.HasValue() - select id.ToInt(); - var values = _productAttributeService.GetProductVariantAttributeValuesByIds(ids.ToArray()); + var pvaValuesStr = attrs[pva.Id]; + + var ids = + from id in pvaValuesStr + where id.HasValue() + select id.ToInt(); + + allIds.AddRange(ids); - pvaValues = pvaValues.Concat(values); + //var values = _productAttributeService.GetProductVariantAttributeValuesByIds(ids.ToArray()); + //pvaValues = pvaValues.Concat(values); } - return pvaValues; + int[] allDistinctIds = allIds.Distinct().ToArray(); + + var values = _productAttributeService.GetProductVariantAttributeValuesByIds(allDistinctIds); + + return values; } - /// - /// Gets selected product variant attribute value - /// - /// Attributes - /// Product variant attribute identifier - /// Product variant attribute value - public virtual IList ParseValues(string attributes, int productVariantAttributeId) + public virtual IList ParseProductVariantAttributeValues(Multimap attributeCombination, IEnumerable attributes) + { + var result = new List(); + + if (attributeCombination == null || !attributeCombination.Any()) + return result; + + var allValueIds = new List(); + + foreach (var pva in attributes.Where(x => x.ShouldHaveValues()).OrderBy(x => x.DisplayOrder)) + { + if (attributeCombination.ContainsKey(pva.Id)) + { + var pvaValuesStr = attributeCombination[pva.Id]; + var ids = pvaValuesStr.Where(x => x.HasValue()).Select(x => x.ToInt()); + + allValueIds.AddRange(ids); + } + } + + foreach (int id in allValueIds.Distinct()) + { + foreach (var attribute in attributes) + { + var attributeValue = attribute.ProductVariantAttributeValues.FirstOrDefault(x => x.Id == id); + if (attributeValue != null && !result.Any(x => x.Id == attributeValue.Id)) + { + result.Add(attributeValue); + break; + } + } + } + + 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) { var selectedProductVariantAttributeValues = new List(); try { var xmlDoc = new XmlDocument(); - xmlDoc.LoadXml(attributes); + xmlDoc.LoadXml(attributesXml); var nodeList1 = xmlDoc.SelectNodes(@"//Attributes/ProductVariantAttribute"); foreach (XmlNode node1 in nodeList1) @@ -207,85 +276,80 @@ public virtual IList ParseValues(string attributes, int productVariantAt /// /// Adds an attribute /// - /// Attributes + /// Attributes /// Product variant attribute /// Value /// Attributes - public virtual string AddProductAttribute(string attributes, ProductVariantAttribute pva, string value) + public virtual string AddProductAttribute(string attributesXml, ProductVariantAttribute pva, string value) { - return pva.AddProductAttribute(attributes, value); + return pva.AddProductAttribute(attributesXml, value); } - /// - /// Are attributes equal - /// - /// The attributes of the first product - /// The attributes of the second product - /// Result - public virtual bool AreProductAttributesEqual(string attributes1, string attributes2) + public virtual bool AreProductAttributesEqual(string attributeXml1, string attributeXml2) { - var attrs1 = DeserializeProductVariantAttributes(attributes1); - var attrs2 = DeserializeProductVariantAttributes(attributes2); + if (attributeXml1.IsCaseInsensitiveEqual(attributeXml2)) + return true; - if (attrs1.Count == attrs2.Count) - { - var pva1Collection = ParseProductVariantAttributes(attrs2.Keys); - var pva2Collection = ParseProductVariantAttributes(attrs1.Keys); - foreach (var pva1 in pva1Collection) - { - foreach (var pva2 in pva2Collection) - { - if (pva1.Id == pva2.Id) - { - var pvaValues1Str = attrs2[pva1.Id]; // ParseValues(attributes2, pva1.Id); - var pvaValues2Str = attrs1[pva2.Id]; // ParseValues(attributes1, pva2.Id); - if (pvaValues1Str.Count == pvaValues2Str.Count) - { - foreach (string str1 in pvaValues1Str) - { - bool hasAttribute = pvaValues2Str.Any(x => x.IsCaseInsensitiveEqual(str1)); - if (!hasAttribute) - { - return false; - } - } - } - else - { - return false; - } - } - } - } - } - else - { - return false; - } + var attributes1 = DeserializeProductVariantAttributes(attributeXml1); + var attributes2 = DeserializeProductVariantAttributes(attributeXml2); - return true; - } + if (attributes1.Count != attributes2.Count) + return false; - /// - /// Finds a product variant attribute combination by attributes stored in XML - /// - /// Product - /// Attributes in XML format - /// Found product variant attribute combination - public virtual ProductVariantAttributeCombination FindProductVariantAttributeCombination(Product product, string attributesXml) - { - if (product == null) - throw new ArgumentNullException("product"); + foreach (var kvp in attributes1) + { + if (!attributes2.ContainsKey(kvp.Key)) + { + // the second list does not contain this id: not equal! + return false; + } + + // compare the values + var values1 = kvp.Value; + var values2 = attributes2[kvp.Key]; - return FindProductVariantAttributeCombination(product.Id, attributesXml); + if (values1.Count != values2.Count) + { + // number of values differ: not equal! + return false; + } + + foreach (var value1 in values1) + { + var str1 = value1.TrimSafe(); + + if (!values2.Any(x => x.TrimSafe().IsCaseInsensitiveEqual(str1))) + { + // the second values list for this attribute does not contain this value: not equal! + return false; + } + } + } + + return true; } - public virtual ProductVariantAttributeCombination FindProductVariantAttributeCombination(int productId, string attributesXml) + public virtual ProductVariantAttributeCombination FindProductVariantAttributeCombination( + int productId, + string attributesXml) { - if (attributesXml.HasValue()) + if (attributesXml.IsEmpty()) + return null; + + var attributesHash = attributesXml.Hash(Encoding.UTF8); + var cacheKey = ATTRIBUTECOMBINATION_BY_ID_HASH.FormatInvariant(productId, attributesHash); + + var result = _cacheManager.Get(cacheKey, () => { - //existing combinations - var combinations = _productAttributeService.GetAllProductVariantAttributeCombinations(productId); + var query = from x in _pvacRepository.TableUntracked + where x.ProductId == productId + select new + { + x.Id, + x.AttributesXml + }; + + var combinations = query.ToList(); if (combinations.Count == 0) return null; @@ -293,63 +357,110 @@ public virtual ProductVariantAttributeCombination FindProductVariantAttributeCom { bool attributesEqual = AreProductAttributesEqual(combination.AttributesXml, attributesXml); if (attributesEqual) - return combination; + return _productAttributeService.GetProductVariantAttributeCombinationById(combination.Id); } - } - return null; + + return null; + }); + + return result; } - /// - /// Deserializes attribute data from an URL query string - /// - /// Json data query string - /// List items with following structure: Product.Id, ProductAttribute.Id, Product_ProductAttribute_Mapping.Id, ProductVariantAttributeValue.Id public virtual List> DeserializeQueryData(string jsonData) { - if (jsonData.HasValue()) + try { - if (jsonData.StartsWith("[")) - return JsonConvert.DeserializeObject>>(jsonData); + if (jsonData.HasValue()) + { + if (jsonData.StartsWith("[")) + { + return JsonConvert.DeserializeObject>>(jsonData); + } - return new List>() { JsonConvert.DeserializeObject>(jsonData) }; + return new List> { JsonConvert.DeserializeObject>(jsonData) }; + } } + catch { } + return new List>(); } - - /// - /// Serializes attribute data - /// - /// Product identifier - /// Attribute XML string - /// Whether to URL encode - /// Json string with attribute data - public virtual string SerializeQueryData(int productId, string attributesXml, bool urlEncode = true) + + public virtual void DeserializeQueryData(List> queryData, string attributesXml, int productId, int bundleItemId = 0) { + Guard.ArgumentNotNull(() => queryData); + if (attributesXml.HasValue() && productId != 0) { - var data = new List>(); var attributeValues = ParseProductVariantAttributeValues(attributesXml).ToList(); foreach (var value in attributeValues) { - data.Add(new List + var lst = new List { productId, value.ProductVariantAttribute.ProductAttributeId, value.ProductVariantAttributeId, value.Id - }); - } + }; - if (data.Count > 0) - { - string result = JsonConvert.SerializeObject(data); - return (urlEncode ? HttpUtility.UrlEncode(result) : result); + if (bundleItemId != 0) + lst.Add(bundleItemId); + + queryData.Add(lst); } } + } + + public virtual string SerializeQueryData(string attributesXml, int productId, bool urlEncode = true) + { + var data = new List>(); + + DeserializeQueryData(data, attributesXml, productId); + + return SerializeQueryData(data, urlEncode); + } + + public virtual string SerializeQueryData(List> queryData, bool urlEncode = true) + { + if (queryData.Count > 0) + { + var result = JsonConvert.SerializeObject(queryData); + + return (urlEncode ? HttpUtility.UrlEncode(result) : result); + } + return ""; } + private string CreateProductUrl(string queryString, string productSeName) + { + var url = UrlHelper.GenerateUrl( + "Product", + null, + null, + new RouteValueDictionary(new { SeName = productSeName }), + RouteTable.Routes, + HttpContext.Current.Request.RequestContext, + false); + + if (queryString.HasValue()) + { + url = string.Concat(url, url.Contains("?") ? "&" : "?", "attributes=", queryString); + } + + return url; + } + + public virtual string GetProductUrlWithAttributes(string attributesXml, int productId, string productSeName) + { + return CreateProductUrl(SerializeQueryData(attributesXml, productId), productSeName); + } + + public virtual string GetProductUrlWithAttributes(List> queryData, string productSeName) + { + return CreateProductUrl(SerializeQueryData(queryData), productSeName); + } + #endregion #region Gift card attributes @@ -357,14 +468,14 @@ public virtual string SerializeQueryData(int productId, string attributesXml, bo /// /// Add gift card attrbibutes /// - /// Attributes + /// Attributes /// Recipient name /// Recipient email /// Sender name /// Sender email /// Message /// Attributes - public string AddGiftCardAttribute(string attributes, string recipientName, + public string AddGiftCardAttribute(string attributesXml, string recipientName, string recipientEmail, string senderName, string senderEmail, string giftCardMessage) { string result = string.Empty; @@ -376,14 +487,14 @@ public string AddGiftCardAttribute(string attributes, string recipientName, senderEmail = senderEmail.Trim(); var xmlDoc = new XmlDocument(); - if (String.IsNullOrEmpty(attributes)) + if (String.IsNullOrEmpty(attributesXml)) { var element1 = xmlDoc.CreateElement("Attributes"); xmlDoc.AppendChild(element1); } else { - xmlDoc.LoadXml(attributes); + xmlDoc.LoadXml(attributesXml); } var rootElement = (XmlElement)xmlDoc.SelectSingleNode(@"//Attributes"); @@ -427,13 +538,13 @@ public string AddGiftCardAttribute(string attributes, string recipientName, /// /// Get gift card attrbibutes /// - /// Attributes + /// Attributes /// Recipient name /// Recipient email /// Sender name /// Sender email /// Message - public void GetGiftCardAttribute(string attributes, out string recipientName, + public void GetGiftCardAttribute(string attributesXml, out string recipientName, out string recipientEmail, out string senderName, out string senderEmail, out string giftCardMessage) { @@ -446,7 +557,7 @@ public void GetGiftCardAttribute(string attributes, out string recipientName, try { var xmlDoc = new XmlDocument(); - xmlDoc.LoadXml(attributes); + xmlDoc.LoadXml(attributesXml); var recipientNameElement = (XmlElement)xmlDoc.SelectSingleNode(@"//Attributes/GiftCardInfo/RecipientName"); var recipientEmailElement = (XmlElement)xmlDoc.SelectSingleNode(@"//Attributes/GiftCardInfo/RecipientEmail"); diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeService.cs b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeService.cs index c7fc659823..3a886d0913 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductAttributeService.cs @@ -1,51 +1,42 @@ using System; using System.Collections.Generic; using System.Linq; +using SmartStore.Collections; using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Events; using SmartStore.Services.Media; -using SmartStore.Core.Infrastructure; -using SmartStore.Data; -using System.Text; -using System.Linq.Expressions; -using SmartStore.Core.Domain.Media; +using SmartStore.Core; namespace SmartStore.Services.Catalog { - /// - /// Product attribute service - /// + public partial class ProductAttributeService : IProductAttributeService { - #region Constants private const string PRODUCTATTRIBUTES_ALL_KEY = "SmartStore.productattribute.all"; - private const string PRODUCTVARIANTATTRIBUTES_ALL_KEY = "SmartStore.productvariantattribute.all-{0}"; - private const string PRODUCTVARIANTATTRIBUTEVALUES_ALL_KEY = "SmartStore.productvariantattributevalue.all-{0}"; - private const string PRODUCTATTRIBUTES_PATTERN_KEY = "SmartStore.productattribute."; - private const string PRODUCTVARIANTATTRIBUTES_PATTERN_KEY = "SmartStore.productvariantattribute."; - private const string PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY = "SmartStore.productvariantattributevalue."; - private const string PRODUCTATTRIBUTES_BY_ID_KEY = "SmartStore.productattribute.id-{0}"; - private const string PRODUCTVARIANTATTRIBUTES_BY_ID_KEY = "SmartStore.productvariantattribute.id-{0}"; - private const string PRODUCTVARIANTATTRIBUTEVALUES_BY_ID_KEY = "SmartStore.productvariantattributevalue.id-{0}"; + private const string PRODUCTATTRIBUTES_BY_ID_KEY = "SmartStore.productattribute.id-{0}"; + private const string PRODUCTATTRIBUTES_PATTERN_KEY = "SmartStore.productattribute."; - #endregion + private const string PRODUCTVARIANTATTRIBUTES_ALL_KEY = "SmartStore.productvariantattribute.all-{0}"; + private const string PRODUCTVARIANTATTRIBUTES_BY_ID_KEY = "SmartStore.productvariantattribute.id-{0}"; + // 0 = ProductId, 1 = PageIndex, 2 = PageSize + private const string PRODUCTVARIANTATTRIBUTES_COMBINATIONS_BY_ID_KEY = "SmartStore.productvariantattribute.combinations.id-{0}-{1}-{2}"; + private const string PRODUCTVARIANTATTRIBUTES_PATTERN_KEY = "SmartStore.productvariantattribute."; - #region Fields + private const string PRODUCTVARIANTATTRIBUTEVALUES_ALL_KEY = "SmartStore.productvariantattributevalue.all-{0}"; + private const string PRODUCTVARIANTATTRIBUTEVALUES_BY_ID_KEY = "SmartStore.productvariantattributevalue.id-{0}"; + private const string PRODUCTVARIANTATTRIBUTEVALUES_PATTERN_KEY = "SmartStore.productvariantattributevalue."; - private readonly IRepository _productAttributeRepository; + private readonly IRepository _productAttributeRepository; private readonly IRepository _productVariantAttributeRepository; - private readonly IRepository _productVariantAttributeCombinationRepository; + private readonly IRepository _pvacRepository; private readonly IRepository _productVariantAttributeValueRepository; private readonly IRepository _productBundleItemAttributeFilterRepository; private readonly IEventPublisher _eventPublisher; private readonly ICacheManager _cacheManager; private readonly IPictureService _pictureService; - #endregion - - #region Ctor /// /// Ctor @@ -53,13 +44,13 @@ public partial class ProductAttributeService : IProductAttributeService /// Cache manager /// Product attribute repository /// Product variant attribute mapping repository - /// Product variant attribute combination repository + /// Product variant attribute combination repository /// Product variant attribute value repository /// Event published public ProductAttributeService(ICacheManager cacheManager, IRepository productAttributeRepository, IRepository productVariantAttributeRepository, - IRepository productVariantAttributeCombinationRepository, + IRepository pvacRepository, IRepository productVariantAttributeValueRepository, IRepository productBundleItemAttributeFilterRepository, IEventPublisher eventPublisher, @@ -68,27 +59,43 @@ public ProductAttributeService(ICacheManager cacheManager, _cacheManager = cacheManager; _productAttributeRepository = productAttributeRepository; _productVariantAttributeRepository = productVariantAttributeRepository; - _productVariantAttributeCombinationRepository = productVariantAttributeCombinationRepository; + _pvacRepository = pvacRepository; _productVariantAttributeValueRepository = productVariantAttributeValueRepository; _productBundleItemAttributeFilterRepository = productBundleItemAttributeFilterRepository; _eventPublisher = eventPublisher; _pictureService = pictureService; } - #endregion + #region Utilities + + private IList GetSwitchedLoadedAttributeMappings(ICollection productVariantAttributeIds) + { + if (productVariantAttributeIds != null && productVariantAttributeIds.Count > 0) + { + if (productVariantAttributeIds.Count == 1) + { + var pva = GetProductVariantAttributeById(productVariantAttributeIds.ElementAt(0)); + if (pva != null) + { + return new List { pva }; + } + } + else + { + return _productVariantAttributeRepository.GetMany(productVariantAttributeIds).ToList(); + } + } - //// Autowired Dependency (is a proptery dependency to avoid circularity) - //public virtual IProductAttributeParser AttributeParser { get; set; } + return new List(); + } - #region Methods + #endregion - #region Product attributes + #region Methods - /// - /// Deletes a product attribute - /// - /// Product attribute - public virtual void DeleteProductAttribute(ProductAttribute productAttribute) + #region Product attributes + + public virtual void DeleteProductAttribute(ProductAttribute productAttribute) { if (productAttribute == null) throw new ArgumentNullException("productAttribute"); @@ -104,10 +111,6 @@ public virtual void DeleteProductAttribute(ProductAttribute productAttribute) _eventPublisher.EntityDeleted(productAttribute); } - /// - /// Gets all product attributes - /// - /// Product attribute collection public virtual IList GetAllProductAttributes() { string key = PRODUCTATTRIBUTES_ALL_KEY; @@ -121,11 +124,6 @@ orderby pa.Name }); } - /// - /// Gets a product attribute - /// - /// Product attribute identifier - /// Product attribute public virtual ProductAttribute GetProductAttributeById(int productAttributeId) { if (productAttributeId == 0) @@ -137,10 +135,6 @@ public virtual ProductAttribute GetProductAttributeById(int productAttributeId) }); } - /// - /// Inserts a product attribute - /// - /// Product attribute public virtual void InsertProductAttribute(ProductAttribute productAttribute) { if (productAttribute == null) @@ -156,10 +150,6 @@ public virtual void InsertProductAttribute(ProductAttribute productAttribute) _eventPublisher.EntityInserted(productAttribute); } - /// - /// Updates the product attribute - /// - /// Product attribute public virtual void UpdateProductAttribute(ProductAttribute productAttribute) { if (productAttribute == null) @@ -179,10 +169,6 @@ public virtual void UpdateProductAttribute(ProductAttribute productAttribute) #region Product variant attributes mappings (ProductVariantAttribute) - /// - /// Deletes a product variant attribute mapping - /// - /// Product variant attribute mapping public virtual void DeleteProductVariantAttribute(ProductVariantAttribute productVariantAttribute) { if (productVariantAttribute == null) @@ -198,11 +184,6 @@ public virtual void DeleteProductVariantAttribute(ProductVariantAttribute produc _eventPublisher.EntityDeleted(productVariantAttribute); } - /// - /// Gets product variant attribute mappings by product identifier - /// - /// The product identifier - /// Product variant attribute mapping collection public virtual IList GetProductVariantAttributesByProductId(int productId) { string key = string.Format(PRODUCTVARIANTATTRIBUTES_ALL_KEY, productId); @@ -218,31 +199,72 @@ orderby pva.DisplayOrder }); } - /// - /// Gets a product variant attribute mapping - /// - /// Product variant attribute mapping identifier - /// Product variant attribute mapping + public virtual Multimap GetProductVariantAttributesByProductIds(int[] productIds, AttributeControlType? controlType) + { + Guard.ArgumentNotNull(() => productIds); + + var query = + from pva in _productVariantAttributeRepository.TableUntracked.Expand(x => x.ProductAttribute).Expand(x => x.ProductVariantAttributeValues) + where productIds.Contains(pva.ProductId) + select pva; + + if (controlType.HasValue) + { + query = query.Where(x => x.AttributeControlTypeId == ((int)controlType.Value)); + } + + var map = query + .OrderBy(x => x.ProductId) + .ThenBy(x => x.DisplayOrder) + .ToList() + .ToMultimap(x => x.ProductId, x => x); + + return map; + } + public virtual ProductVariantAttribute GetProductVariantAttributeById(int productVariantAttributeId) { if (productVariantAttributeId == 0) return null; string key = string.Format(PRODUCTVARIANTATTRIBUTES_BY_ID_KEY, productVariantAttributeId); - return _cacheManager.Get(key, () => { + + return _cacheManager.Get(key, () => + { return _productVariantAttributeRepository.GetById(productVariantAttributeId); }); } - public virtual IEnumerable GetProductVariantAttributesByIds(params int[] ids) - { - if (ids == null || ids.Length == 0) - { - return Enumerable.Empty(); - } + public virtual IList GetProductVariantAttributesByIds(IEnumerable productVariantAttributeIds, IEnumerable attributes = null) + { + if (productVariantAttributeIds != null) + { + if (attributes != null) + { + var ids = new List(); + var result = new List(); - return _productVariantAttributeRepository.GetMany(ids); - } + foreach (var id in productVariantAttributeIds) + { + var pva = attributes.FirstOrDefault(x => x.Id == id); + if (pva == null) + ids.Add(id); + else + result.Add(pva); + } + + var newLoadedMappings = GetSwitchedLoadedAttributeMappings(ids); + + result.AddRange(newLoadedMappings); + + return result; + } + + return GetSwitchedLoadedAttributeMappings(productVariantAttributeIds.ToList()); + } + + return new List(); + } public virtual IEnumerable GetProductVariantAttributeValuesByIds(params int[] productVariantAttributeValueIds) { @@ -254,10 +276,6 @@ public virtual IEnumerable GetProductVariantAttrib return _productVariantAttributeValueRepository.GetMany(productVariantAttributeValueIds); } - /// - /// Inserts a product variant attribute mapping - /// - /// The product variant attribute mapping public virtual void InsertProductVariantAttribute(ProductVariantAttribute productVariantAttribute) { if (productVariantAttribute == null) @@ -273,10 +291,6 @@ public virtual void InsertProductVariantAttribute(ProductVariantAttribute produc _eventPublisher.EntityInserted(productVariantAttribute); } - /// - /// Updates the product variant attribute mapping - /// - /// The product variant attribute mapping public virtual void UpdateProductVariantAttribute(ProductVariantAttribute productVariantAttribute) { if (productVariantAttribute == null) @@ -296,10 +310,6 @@ public virtual void UpdateProductVariantAttribute(ProductVariantAttribute produc #region Product variant attribute values (ProductVariantAttributeValue) - /// - /// Deletes a product variant attribute value - /// - /// Product variant attribute value public virtual void DeleteProductVariantAttributeValue(ProductVariantAttributeValue productVariantAttributeValue) { if (productVariantAttributeValue == null) @@ -315,11 +325,6 @@ public virtual void DeleteProductVariantAttributeValue(ProductVariantAttributeVa _eventPublisher.EntityDeleted(productVariantAttributeValue); } - /// - /// Gets product variant attribute values by product identifier - /// - /// The product variant attribute mapping identifier - /// Product variant attribute mapping collection public virtual IList GetProductVariantAttributeValues(int productVariantAttributeId) { string key = string.Format(PRODUCTVARIANTATTRIBUTEVALUES_ALL_KEY, productVariantAttributeId); @@ -334,11 +339,6 @@ orderby pvav.DisplayOrder }); } - /// - /// Gets a product variant attribute value - /// - /// Product variant attribute value identifier - /// Product variant attribute value public virtual ProductVariantAttributeValue GetProductVariantAttributeValueById(int productVariantAttributeValueId) { if (productVariantAttributeValueId == 0) @@ -351,10 +351,6 @@ public virtual ProductVariantAttributeValue GetProductVariantAttributeValueById( }); } - /// - /// Inserts a product variant attribute value - /// - /// The product variant attribute value public virtual void InsertProductVariantAttributeValue(ProductVariantAttributeValue productVariantAttributeValue) { if (productVariantAttributeValue == null) @@ -370,10 +366,6 @@ public virtual void InsertProductVariantAttributeValue(ProductVariantAttributeVa _eventPublisher.EntityInserted(productVariantAttributeValue); } - /// - /// Updates the product variant attribute value - /// - /// The product variant attribute value public virtual void UpdateProductVariantAttributeValue(ProductVariantAttributeValue productVariantAttributeValue) { if (productVariantAttributeValue == null) @@ -409,52 +401,95 @@ private void CombineAll(List> toCombine, List } } - /// - /// Deletes a product variant attribute combination - /// - /// Product variant attribute combination public virtual void DeleteProductVariantAttributeCombination(ProductVariantAttributeCombination combination) { if (combination == null) throw new ArgumentNullException("combination"); - _productVariantAttributeCombinationRepository.Delete(combination); + _pvacRepository.Delete(combination); //event notification _eventPublisher.EntityDeleted(combination); } - /// - /// Gets all product variant attribute combinations - /// - /// Product identifier - /// Product variant attribute combination collection - public virtual IList GetAllProductVariantAttributeCombinations(int productId) - { + public virtual IPagedList GetAllProductVariantAttributeCombinations( + int productId, + int pageIndex, + int pageSize, + bool untracked = true) + { + if (productId == 0) + { + return new PagedList(new List(), pageIndex, pageSize); + } + + string key = string.Format(PRODUCTVARIANTATTRIBUTES_COMBINATIONS_BY_ID_KEY, productId, 0, int.MaxValue); + return _cacheManager.Get(key, () => + { + var query = from pvac in (untracked ? _pvacRepository.TableUntracked : _pvacRepository.Table) + orderby pvac.Id + where pvac.ProductId == productId + select pvac; + + var combinations = new PagedList(query, pageIndex, pageSize); + return combinations; + }); + } + + public virtual IList GetAllProductVariantAttributeCombinationPictureIds(int productId) + { + var pictureIds = new List(); + if (productId == 0) - return new List(); + return pictureIds; - var query = from pvac in _productVariantAttributeCombinationRepository.Table - orderby pvac.Id - where pvac.ProductId == productId - select pvac; + var query = from pvac in _pvacRepository.TableUntracked + where + pvac.ProductId == productId + && pvac.IsActive + && !String.IsNullOrEmpty(pvac.AssignedPictureIds) + select pvac.AssignedPictureIds; - var combinations = query.ToList(); - return combinations; - } + var data = query.ToList(); + if (data.Any()) + { + int id; + var ids = string.Join(",", data).SplitSafe(",").Distinct(); + + foreach (string str in ids) + { + if (int.TryParse(str, out id) && !pictureIds.Exists(i => i == id)) + pictureIds.Add(id); + } + } + + return pictureIds; + } + + public virtual Multimap GetProductVariantAttributeCombinations(int[] productIds) + { + Guard.ArgumentNotNull(() => productIds); + + var query = + from pvac in _pvacRepository.TableUntracked + where productIds.Contains(pvac.ProductId) + select pvac; + + var map = query + .OrderBy(x => x.ProductId) + .ToList() + .ToMultimap(x => x.ProductId, x => x); + + return map; + } - /// - /// Get the lowest price of all combinations for a product - /// - /// Product identifier - /// Lowest price public virtual decimal? GetLowestCombinationPrice(int productId) { if (productId == 0) return null; var query = - from pvac in _productVariantAttributeCombinationRepository.Table + from pvac in _pvacRepository.Table where pvac.ProductId == productId && pvac.Price != null && pvac.IsActive orderby pvac.Price ascending select pvac.Price; @@ -463,24 +498,24 @@ orderby pvac.Price ascending return price; } - /// - /// Gets a product variant attribute combination - /// - /// Product variant attribute combination identifier - /// Product variant attribute combination public virtual ProductVariantAttributeCombination GetProductVariantAttributeCombinationById(int productVariantAttributeCombinationId) { if (productVariantAttributeCombinationId == 0) return null; - var combination = _productVariantAttributeCombinationRepository.GetById(productVariantAttributeCombinationId); + var combination = _pvacRepository.GetById(productVariantAttributeCombinationId); return combination; } - /// - /// Inserts a product variant attribute combination - /// - /// Product variant attribute combination + public virtual ProductVariantAttributeCombination GetProductVariantAttributeCombinationBySku(string sku) + { + if (sku.IsEmpty()) + return null; + + var combination = _pvacRepository.Table.FirstOrDefault(x => x.Sku == sku); + return combination; + } + public virtual void InsertProductVariantAttributeCombination(ProductVariantAttributeCombination combination) { if (combination == null) @@ -491,16 +526,12 @@ public virtual void InsertProductVariantAttributeCombination(ProductVariantAttri // EnsureSingleDefaultVariant(combination); //} - _productVariantAttributeCombinationRepository.Insert(combination); + _pvacRepository.Insert(combination); //event notification _eventPublisher.EntityInserted(combination); } - /// - /// Updates a product variant attribute combination - /// - /// Product variant attribute combination public virtual void UpdateProductVariantAttributeCombination(ProductVariantAttributeCombination combination) { if (combination == null) @@ -530,23 +561,16 @@ public virtual void UpdateProductVariantAttributeCombination(ProductVariantAttri // } //} - _productVariantAttributeCombinationRepository.Update(combination); + _pvacRepository.Update(combination); //event notification _eventPublisher.EntityUpdated(combination); } - /// - /// Creates all variant attribute combinations - /// - /// The product public virtual void CreateAllProductVariantAttributeCombinations(Product product) { // delete all existing combinations - foreach(var itm in GetAllProductVariantAttributeCombinations(product.Id)) - { - DeleteProductVariantAttributeCombination(itm); - } + _pvacRepository.DeleteAll(x => x.ProductId == product.Id); var attributes = GetProductVariantAttributesByProductId(product.Id); if (attributes == null || attributes.Count <= 0) @@ -567,26 +591,43 @@ public virtual void CreateAllProductVariantAttributeCombinations(Product product { CombineAll(toCombine, resultMatrix, 0, tmp); - foreach (var values in resultMatrix) + using (var scope = new DbContextScope(ctx: _pvacRepository.Context, autoCommit: false, autoDetectChanges: false, validateOnSave: false, hooksEnabled: false)) { - string attrXml = ""; - foreach (var x in values) - { - attrXml = attributes[values.IndexOf(x)].AddProductAttribute(attrXml, x.Id.ToString()); + ProductVariantAttributeCombination combination = null; + + var idx = 0; + foreach (var values in resultMatrix) + { + idx++; + + string attrXml = ""; + for (var i = 0; i < values.Count; ++i) + { + var value = values[i]; + attrXml = attributes[i].AddProductAttribute(attrXml, value.Id.ToString()); + } + + combination = new ProductVariantAttributeCombination + { + ProductId = product.Id, + AttributesXml = attrXml, + StockQuantity = 10000, + AllowOutOfStockOrders = true, + IsActive = true + }; + + _pvacRepository.Insert(combination); } - var combination = new ProductVariantAttributeCombination() + scope.Commit(); + + if (combination != null) { - ProductId = product.Id, - AttributesXml = attrXml, - StockQuantity = 10000, - AllowOutOfStockOrders = true, - IsActive = true - }; - - _productVariantAttributeCombinationRepository.Insert(combination); - _eventPublisher.EntityInserted(combination); + // Perf: publish event for last one only + _eventPublisher.EntityInserted(combination); + } } + } //foreach (var y in resultMatrix) { @@ -604,7 +645,7 @@ public virtual bool VariantHasAttributeCombinations(int productId) return false; var query = - from c in _productVariantAttributeCombinationRepository.Table + from c in _pvacRepository.Table where c.ProductId == productId select c; @@ -615,10 +656,6 @@ from c in _productVariantAttributeCombinationRepository.Table #region Product bundle item attribute filter - /// - /// Inserts a product bundle item attribute filter - /// - /// Product bundle item attribute filter public virtual void InsertProductBundleItemAttributeFilter(ProductBundleItemAttributeFilter attributeFilter) { if (attributeFilter == null) @@ -632,10 +669,6 @@ public virtual void InsertProductBundleItemAttributeFilter(ProductBundleItemAttr } } - /// - /// Updates the product bundle item attribute filter - /// - /// Product bundle item attribute filter public virtual void UpdateProductBundleItemAttributeFilter(ProductBundleItemAttributeFilter attributeFilter) { if (attributeFilter == null) @@ -646,10 +679,6 @@ public virtual void UpdateProductBundleItemAttributeFilter(ProductBundleItemAttr _eventPublisher.EntityUpdated(attributeFilter); } - /// - /// Deletes a product bundle item attribute filter - /// - /// Product bundle item attribute filter public virtual void DeleteProductBundleItemAttributeFilter(ProductBundleItemAttributeFilter attributeFilter) { if (attributeFilter == null) @@ -660,10 +689,6 @@ public virtual void DeleteProductBundleItemAttributeFilter(ProductBundleItemAttr _eventPublisher.EntityDeleted(attributeFilter); } - /// - /// Deletes all attribute filters of a bundle item - /// - /// Bundle item public virtual void DeleteProductBundleItemAttributeFilter(ProductBundleItem bundleItem) { if (bundleItem != null && bundleItem.Id != 0) @@ -677,11 +702,6 @@ from x in _productBundleItemAttributeFilterRepository.Table } } - /// - /// Deletes product bundle item attribute filters - /// - /// Attribute identifier - /// Attribute value identifier public virtual void DeleteProductBundleItemAttributeFilter(int attributeId, int attributeValueId) { var attributeFilterQuery = @@ -692,10 +712,6 @@ from x in _productBundleItemAttributeFilterRepository.Table attributeFilterQuery.ToList().Each(x => DeleteProductBundleItemAttributeFilter(x)); } - /// - /// Deletes product bundle item attribute filters - /// - /// Attribute identifier public virtual void DeleteProductBundleItemAttributeFilter(int attributeId) { var attributeFilterQuery = diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs b/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs index 21d749a085..051fc841f2 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductExtensions.cs @@ -1,17 +1,20 @@ using System; using System.Collections.Generic; using System.Linq; -using SmartStore.Core.Data; +using SmartStore.Core; using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Media; using SmartStore.Core.Infrastructure; +using SmartStore.Services.Directory; using SmartStore.Services.Localization; using SmartStore.Services.Media; using SmartStore.Services.Seo; +using SmartStore.Services.Tax; namespace SmartStore.Services.Catalog { - public static class ProductExtensions + public static class ProductExtensions { public static ProductVariantAttributeCombination MergeWithCombination(this Product product, string selectedAttributes) { @@ -25,13 +28,10 @@ public static ProductVariantAttributeCombination MergeWithCombination(this Produ if (selectedAttributes.IsEmpty()) return null; - // let's find appropriate record - var combination = product - .ProductVariantAttributeCombinations - .Where(x => x.IsActive == true) - .FirstOrDefault(x => productAttributeParser.AreProductAttributesEqual(x.AttributesXml, selectedAttributes)); + // let's find appropriate record + var combination = productAttributeParser.FindProductVariantAttributeCombination(product.Id, selectedAttributes); - if (combination != null) + if (combination != null && combination.IsActive) { product.MergeWithCombination(combination); } @@ -87,9 +87,9 @@ public static void MergeWithCombination(this Product product, ProductVariantAttr product.MergedDataValues.Add("BasePriceBaseAmount", combination.BasePriceBaseAmount); } - public static void GetAllCombinationImageIds(this IList combinations, List imageIds) + public static IList GetAllCombinationPictureIds(this IEnumerable combinations) { - Guard.ArgumentNotNull(imageIds, "imageIds"); + var pictureIds = new List(); if (combinations != null) { @@ -105,21 +105,23 @@ public static void GetAllCombinationImageIds(this IList i == id)) - imageIds.Add(id); + if (int.TryParse(str, out id) && !pictureIds.Exists(i => i == id)) + pictureIds.Add(id); } } } + + return pictureIds; } - /// - /// Finds a related product item by specified identifiers - /// - /// Source - /// The first product identifier - /// The second product identifier - /// Related product - public static RelatedProduct FindRelatedProduct(this IList source, + /// + /// Finds a related product item by specified identifiers + /// + /// Source + /// The first product identifier + /// The second product identifier + /// Related product + public static RelatedProduct FindRelatedProduct(this IList source, int productId1, int productId2) { foreach (RelatedProduct relatedProduct in source) @@ -297,41 +299,93 @@ public static int[] ParseRequiredProductIds(this Product product) } /// - /// Gets the base price + /// Gets the base price info /// /// Product /// Localization service /// Price formatter + /// Currency service + /// Tax service + /// 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, - 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) { - decimal price = decimal.Add(product.Price, priceAdjustment); - decimal basePriceValue = Convert.ToDecimal((price / product.BasePriceAmount) * product.BasePriceBaseAmount); + var workContext = EngineContext.Current.Resolve(); - string basePrice = priceFormatter.FormatPrice(basePriceValue, true, false); - string unit = "{0} {1}".FormatWith(product.BasePriceBaseAmount, product.BasePriceMeasureUnit); + var taxrate = decimal.Zero; + var currentPrice = priceCalculationService.GetFinalPrice(product, workContext.CurrentCustomer, true); + var price = taxService.GetProductPrice(product, decimal.Add(currentPrice, priceAdjustment), out taxrate); + + price = currencyService.ConvertFromPrimaryStoreCurrency(price, currency); - if (languageIndependent) - { - return "{0} / {1}".FormatWith(basePrice, unit); - } + return product.GetBasePriceInfo(price, localizationService, priceFormatter, currency, languageInsensitive); + } - return localizationService.GetResource("Products.BasePriceInfo").FormatWith(basePrice, unit); - } return ""; } + /// + /// 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 ""; + } + public static string GetProductTypeLabel(this Product product, ILocalizationService localizationService) { if (product != null && product.ProductType != ProductType.SimpleProduct) diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductSearchContext.cs b/src/Libraries/SmartStore.Services/Catalog/ProductSearchContext.cs index 3e8fd2d1f0..001e18789b 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductSearchContext.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductSearchContext.cs @@ -31,6 +31,16 @@ public ProductSearchContext() /// Only implemented in LINQ mode at the moment public IList ProductIds { get; set; } + /// + /// Minimum product identifier + /// + public int IdMin { get; set; } + + /// + /// Maximum product identifier + /// + public int IdMax { get; set; } + /// /// A value indicating whether ALL given must be assigned to the resulting products (default is ANY) /// @@ -40,7 +50,7 @@ public ProductSearchContext() /// /// A value indicating whether to load products without any catgory mapping /// - public bool WithoutCategories { get; set; } + public bool? WithoutCategories { get; set; } /// /// Manufacturer identifier; 0 to load all records @@ -50,7 +60,7 @@ public ProductSearchContext() /// /// A value indicating whether to load products without any manufacturer mapping /// - public bool WithoutManufacturers { get; set; } + public bool? WithoutManufacturers { get; set; } /// /// A value indicating whether loaded products are marked as featured (relates only to categories and manufacturers). 0 to load featured products only, 1 to load not featured products only, null to load all products @@ -158,5 +168,35 @@ public ProductSearchContext() /// Can be useful in customization scenarios. /// public string Origin { get; set; } + + /// + /// A value indicating whether to load only published or non published products + /// + public bool? IsPublished { get; set; } + + /// + /// A value indicating whether to load only products displayed on the homepage + /// + public bool? HomePageProducts { get; set; } + + /// + /// Search by minimum availability + /// + public int? AvailabilityMinimum { get; set; } + + /// + /// Search by maximum availability + /// + public int? AvailabilityMaximum { get; set; } + + /// + /// Search by created from date + /// + public DateTime? CreatedFromUtc { get; set; } + + /// + /// Search by created to date + /// + public DateTime? CreatedToUtc { get; set; } } } diff --git a/src/Libraries/SmartStore.Services/Catalog/ProductService.cs b/src/Libraries/SmartStore.Services/Catalog/ProductService.cs index 2f8c1270e3..6dc93b582d 100644 --- a/src/Libraries/SmartStore.Services/Catalog/ProductService.cs +++ b/src/Libraries/SmartStore.Services/Catalog/ProductService.cs @@ -1,13 +1,17 @@ using System; using System.Collections.Generic; using System.Data; +using System.Globalization; using System.Linq; using System.Linq.Expressions; +using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Discounts; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Security; @@ -50,12 +54,10 @@ public partial class ProductService : IProductService private readonly IWorkflowMessageService _workflowMessageService; private readonly IDataProvider _dataProvider; private readonly IDbContext _dbContext; - private readonly ICacheManager _cacheManager; - private readonly IWorkContext _workContext; - private readonly IStoreContext _storeContext; + private readonly ICacheManager _cacheManager; private readonly LocalizationSettings _localizationSettings; private readonly CommonSettings _commonSettings; - private readonly IEventPublisher _eventPublisher; + private readonly ICommonServices _services; #endregion @@ -85,7 +87,7 @@ public partial class ProductService : IProductService /// Localization settings /// Common settings /// Event published - public ProductService(ICacheManager cacheManager, + public ProductService( IRepository productRepository, IRepository relatedProductRepository, IRepository crossSellProductRepository, @@ -101,13 +103,13 @@ public ProductService(ICacheManager cacheManager, IProductAttributeParser productAttributeParser, ILanguageService languageService, IWorkflowMessageService workflowMessageService, - IDataProvider dataProvider, IDbContext dbContext, - IWorkContext workContext, - IStoreContext storeContext, - LocalizationSettings localizationSettings, CommonSettings commonSettings, - IEventPublisher eventPublisher) + IDataProvider dataProvider, + IDbContext dbContext, + ICacheManager cacheManager, + LocalizationSettings localizationSettings, + CommonSettings commonSettings, + ICommonServices services) { - this._cacheManager = cacheManager; this._productRepository = productRepository; this._relatedProductRepository = relatedProductRepository; this._crossSellProductRepository = crossSellProductRepository; @@ -125,11 +127,10 @@ public ProductService(ICacheManager cacheManager, this._workflowMessageService = workflowMessageService; this._dataProvider = dataProvider; this._dbContext = dbContext; - this._workContext = workContext; - this._storeContext = storeContext; + this._cacheManager = cacheManager; this._localizationSettings = localizationSettings; this._commonSettings = commonSettings; - this._eventPublisher = eventPublisher; + this._services = services; this.QuerySettings = DbQuerySettings.Default; } @@ -235,6 +236,17 @@ public virtual void DeleteProduct(Product product) product.QuantityUnitId = null; UpdateProduct(product); + + if (product.ProductType == ProductType.GroupedProduct) + { + var associatedProducts = _productRepository.Table + .Where(x => x.ParentGroupedProductId == product.Id) + .ToList(); + + associatedProducts.ForEach(x => x.ParentGroupedProductId = 0); + + _dbContext.SaveChanges(); + } } /// @@ -243,12 +255,12 @@ public virtual void DeleteProduct(Product product) /// Product collection public virtual IList GetAllProductsDisplayedOnHomePage() { - var query = from p in _productRepository.Table - orderby p.Name - where p.Published && - !p.Deleted && - p.ShowOnHomePage - select p; + var query = + from p in _productRepository.Table + orderby p.HomePageDisplayOrder + where p.Published && !p.Deleted && p.ShowOnHomePage + select p; + var products = query.ToList(); return products; } @@ -309,7 +321,7 @@ public virtual void InsertProduct(Product product) _cacheManager.RemoveByPattern(PRODUCTS_PATTERN_KEY); //event notification - _eventPublisher.EntityInserted(product); + _services.EventPublisher.EntityInserted(product); } /// @@ -336,7 +348,7 @@ public virtual void UpdateProduct(Product product, bool publishEvent = true) // event notification if (publishEvent && modified) { - _eventPublisher.EntityUpdated(product); + _services.EventPublisher.EntityUpdated(product); } } @@ -355,7 +367,7 @@ public virtual IPagedList SearchProducts(ProductSearchContext ctx) ctx.FilterableSpecificationAttributeOptionIds = new List(); - _eventPublisher.Publish(new ProductsSearchingEvent(ctx)); + _services.EventPublisher.Publish(new ProductsSearchingEvent(ctx)); //search by keyword bool searchLocalizedValue = false; @@ -378,7 +390,7 @@ public virtual IPagedList SearchProducts(ProductSearchContext ctx) ctx.CategoryIds.Remove(0); //Access control list. Allowed customer roles - var allowedCustomerRolesIds = _workContext.CurrentCustomer.CustomerRoles + var allowedCustomerRolesIds = _services.WorkContext.CurrentCustomer.CustomerRoles .Where(cr => cr.Active).Select(cr => cr.Id).ToList(); if (_commonSettings.UseStoredProceduresIfSupported && _dataProvider.StoredProceduresSupported) @@ -390,7 +402,7 @@ public virtual IPagedList SearchProducts(ProductSearchContext ctx) //pass categry identifiers as comma-delimited string string commaSeparatedCategoryIds = ""; - if (ctx.CategoryIds != null && !ctx.WithoutCategories) + if (ctx.CategoryIds != null && !(ctx.WithoutCategories ?? false)) { for (int i = 0; i < ctx.CategoryIds.Count; i++) { @@ -440,7 +452,7 @@ public virtual IPagedList SearchProducts(ProductSearchContext ctx) var pManufacturerId = _dataProvider.GetParameter(); pManufacturerId.ParameterName = "ManufacturerId"; - pManufacturerId.Value = (ctx.WithoutManufacturers ? 0 : ctx.ManufacturerId); + pManufacturerId.Value = (ctx.WithoutManufacturers ?? false) ? 0 : ctx.ManufacturerId; pManufacturerId.DbType = DbType.Int32; var pStoreId = _dataProvider.GetParameter(); @@ -555,14 +567,55 @@ public virtual IPagedList SearchProducts(ProductSearchContext ctx) var pWithoutCategories = _dataProvider.GetParameter(); pWithoutCategories.ParameterName = "WithoutCategories"; - pWithoutCategories.Value = ctx.WithoutCategories; + pWithoutCategories.Value = (ctx.WithoutCategories.HasValue ? (object)ctx.WithoutCategories.Value : DBNull.Value); pWithoutCategories.DbType = DbType.Boolean; var pWithoutManufacturers = _dataProvider.GetParameter(); pWithoutManufacturers.ParameterName = "WithoutManufacturers"; - pWithoutManufacturers.Value = ctx.WithoutManufacturers; + pWithoutManufacturers.Value = (ctx.WithoutManufacturers.HasValue ? (object)ctx.WithoutManufacturers.Value : DBNull.Value); pWithoutManufacturers.DbType = DbType.Boolean; + var pIsPublished = _dataProvider.GetParameter(); + pIsPublished.ParameterName = "IsPublished"; + pIsPublished.Value = (ctx.IsPublished.HasValue ? (object)ctx.IsPublished.Value : DBNull.Value); + pIsPublished.DbType = DbType.Boolean; + + var pHomePageProducts = _dataProvider.GetParameter(); + pHomePageProducts.ParameterName = "HomePageProducts"; + pHomePageProducts.Value = (ctx.HomePageProducts.HasValue ? (object)ctx.HomePageProducts.Value : DBNull.Value); + pHomePageProducts.DbType = DbType.Boolean; + + var pIdMin = _dataProvider.GetParameter(); + pIdMin.ParameterName = "IdMin"; + pIdMin.Value = ctx.IdMin; + pIdMin.DbType = DbType.Int32; + + var pIdMax = _dataProvider.GetParameter(); + pIdMax.ParameterName = "IdMax"; + pIdMax.Value = ctx.IdMin; + pIdMax.DbType = DbType.Int32; + + var pAvailabilityMin = _dataProvider.GetParameter(); + pAvailabilityMin.ParameterName = "AvailabilityMin"; + pAvailabilityMin.Value = ctx.AvailabilityMinimum.HasValue ? (object)ctx.AvailabilityMinimum.Value : DBNull.Value; + pAvailabilityMin.DbType = DbType.Int32; + + var pAvailabilityMax = _dataProvider.GetParameter(); + pAvailabilityMax.ParameterName = "AvailabilityMax"; + pAvailabilityMax.Value = ctx.AvailabilityMaximum.HasValue ? (object)ctx.AvailabilityMaximum.Value : DBNull.Value; + pAvailabilityMax.DbType = DbType.Int32; + + var pCreatedFromUtc = _dataProvider.GetParameter(); + pCreatedFromUtc.ParameterName = "CreatedFromUtc"; + pCreatedFromUtc.Value = ctx.CreatedFromUtc.HasValue ? (object)ctx.CreatedFromUtc.Value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) : DBNull.Value; + pCreatedFromUtc.DbType = DbType.String; + + var pCreatedToUtc = _dataProvider.GetParameter(); + pCreatedToUtc.ParameterName = "CreatedToUtc"; + pCreatedToUtc.Value = ctx.CreatedToUtc.HasValue ? (object)ctx.CreatedToUtc.Value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) : DBNull.Value; + pCreatedToUtc.DbType = DbType.String; + + var pFilterableSpecificationAttributeOptionIds = _dataProvider.GetParameter(); pFilterableSpecificationAttributeOptionIds.ParameterName = "FilterableSpecificationAttributeOptionIds"; pFilterableSpecificationAttributeOptionIds.Direction = ParameterDirection.Output; @@ -603,6 +656,14 @@ public virtual IPagedList SearchProducts(ProductSearchContext ctx) pLoadFilterableSpecificationAttributeOptionIds, pWithoutCategories, pWithoutManufacturers, + pIsPublished, + pHomePageProducts, + pIdMin, + pIdMax, + pAvailabilityMin, + pAvailabilityMax, + pCreatedFromUtc, + pCreatedToUtc, pFilterableSpecificationAttributeOptionIds, pTotalRecords); @@ -735,16 +796,21 @@ public virtual IQueryable PrepareProductSearchQuery( if (allowedCustomerRolesIds == null) { - allowedCustomerRolesIds = _workContext.CurrentCustomer.CustomerRoles.Where(cr => cr.Active).Select(cr => cr.Id).ToList(); + allowedCustomerRolesIds = _services.WorkContext.CurrentCustomer.CustomerRoles.Where(cr => cr.Active).Select(cr => cr.Id).ToList(); } // products var query = ctx.Query ?? _productRepository.Table; query = query.Where(p => !p.Deleted); - if (!ctx.ShowHidden) + if (!ctx.IsPublished.HasValue) { - query = query.Where(p => p.Published); + if (!ctx.ShowHidden) + query = query.Where(p => p.Published); + } + else + { + query = query.Where(p => p.Published == ctx.IsPublished.Value); } if (ctx.ParentGroupedProductId > 0) @@ -757,6 +823,11 @@ public virtual IQueryable PrepareProductSearchQuery( query = query.Where(p => p.VisibleIndividually); } + if (ctx.HomePageProducts.HasValue) + { + query = query.Where(p => p.ShowOnHomePage == ctx.HomePageProducts.Value); + } + if (ctx.ProductType.HasValue) { int productTypeId = (int)ctx.ProductType.Value; @@ -767,6 +838,34 @@ public virtual IQueryable PrepareProductSearchQuery( { query = query.Where(x => ctx.ProductIds.Contains(x.Id)); } + else + { + if (ctx.IdMin != 0) + query = query.Where(x => x.Id >= ctx.IdMin); + + if (ctx.IdMax != 0) + query = query.Where(x => x.Id <= ctx.IdMax); + } + + if (ctx.AvailabilityMinimum.HasValue) + { + query = query.Where(x => x.StockQuantity >= ctx.AvailabilityMinimum.Value); + } + + if (ctx.AvailabilityMaximum.HasValue) + { + query = query.Where(x => x.StockQuantity <= ctx.AvailabilityMaximum.Value); + } + + if (ctx.CreatedFromUtc.HasValue) + { + query = query.Where(x => x.CreatedOnUtc >= ctx.CreatedFromUtc.Value); + } + + if (ctx.CreatedToUtc.HasValue) + { + query = query.Where(x => x.CreatedOnUtc <= ctx.CreatedToUtc.Value); + } //The function 'CurrentUtcDateTime' is not supported by SQL Server Compact. //That's why we pass the date value @@ -875,9 +974,12 @@ from p in query } // category filtering - if (ctx.WithoutCategories) + if (ctx.WithoutCategories.HasValue) { - query = query.Where(x => x.ProductCategories.Count == 0); + if (ctx.WithoutCategories.Value) + query = query.Where(x => x.ProductCategories.Count == 0); + else + query = query.Where(x => x.ProductCategories.Count > 0); } else if (ctx.CategoryIds != null && ctx.CategoryIds.Count > 0) { @@ -900,9 +1002,12 @@ from pc in p.ProductCategories.Where(pc => ctx.CategoryIds.Contains(pc.CategoryI } // manufacturer filtering - if (ctx.WithoutManufacturers) + if (ctx.WithoutManufacturers.HasValue) { - query = query.Where(x => x.ProductManufacturers.Count == 0); + if (ctx.WithoutManufacturers.Value) + query = query.Where(x => x.ProductManufacturers.Count == 0); + else + query = query.Where(x => x.ProductManufacturers.Count > 0); } else if (ctx.ManufacturerId > 0) { @@ -973,24 +1078,25 @@ public virtual void UpdateProductReviewTotals(Product product) /// Result public virtual IList GetLowStockProducts() { - //Track inventory for product + // Track inventory for product var query1 = from p in _productRepository.Table orderby p.MinStockQuantity where !p.Deleted && - p.ManageInventoryMethodId == (int)ManageInventoryMethod.ManageStock && - p.MinStockQuantity >= p.StockQuantity + p.ManageInventoryMethodId == (int)ManageInventoryMethod.ManageStock && + p.MinStockQuantity >= p.StockQuantity select p; var products1 = query1.ToList(); - //Track inventory for product by product attributes + // Track inventory for product by product attributes var query2 = from p in _productRepository.Table from pvac in p.ProductVariantAttributeCombinations where !p.Deleted && - p.ManageInventoryMethodId == (int)ManageInventoryMethod.ManageStockByAttributes && - pvac.StockQuantity <= 0 + p.ManageInventoryMethodId == (int)ManageInventoryMethod.ManageStockByAttributes && + pvac.StockQuantity <= 0 select p; - //only distinct products (group by ID) - //if we use standard Distinct() method, then all fields will be compared (low performance) + + // only distinct products (group by ID) + // if we use standard Distinct() method, then all fields will be compared (low performance) query2 = from p in query2 group p by p.Id into pGroup orderby pGroup.Key @@ -1016,8 +1122,7 @@ public virtual Product GetProductBySku(string sku) var query = from p in _productRepository.Table orderby p.DisplayOrder, p.Id - where !p.Deleted && - p.Sku == sku + where !p.Deleted && p.Sku == sku select p; var product = query.FirstOrDefault(); return product; @@ -1044,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 /// @@ -1169,7 +1304,7 @@ public virtual AdjustInventoryResult AdjustInventory(Product product, bool decre break; case ManageInventoryMethod.ManageStockByAttributes: { - var combination = _productAttributeParser.FindProductVariantAttributeCombination(product, attributesXml); + var combination = _productAttributeParser.FindProductVariantAttributeCombination(product.Id, attributesXml); if (combination != null) { result.StockQuantityOld = combination.StockQuantity; @@ -1249,6 +1384,71 @@ public virtual void UpdateHasDiscountsApplied(Product product) UpdateProduct(product); } + public virtual Multimap GetProductTagsByProductIds(int[] productIds) + { + Guard.ArgumentNotNull(() => productIds); + + var query = _productRepository.TableUntracked + .Expand(x => x.ProductTags) + .Where(x => productIds.Contains(x.Id)) + .Select(x => new + { + ProductId = x.Id, + Tags = x.ProductTags + }); + + var map = new Multimap(); + + foreach (var item in query.ToList()) + { + foreach (var tag in item.Tags) + map.Add(item.ProductId, tag); + } + + return map; + } + + public virtual Multimap GetAppliedDiscountsByProductIds(int[] productIds) + { + Guard.ArgumentNotNull(() => productIds); + + var query = _productRepository.TableUntracked + .Expand(x => x.AppliedDiscounts.Select(y => y.DiscountRequirements)) + .Where(x => productIds.Contains(x.Id)) + .Select(x => new + { + ProductId = x.Id, + Discounts = x.AppliedDiscounts + }); + + var map = new Multimap(); + + foreach (var item in query.ToList()) + { + foreach (var discount in item.Discounts) + map.Add(item.ProductId, discount); + } + + return map; + } + + public virtual Multimap GetProductSpecificationAttributesByProductIds(int[] productIds) + { + Guard.ArgumentNotNull(() => productIds); + + var query = _productSpecificationAttributeRepository.TableUntracked + .Expand(x => x.SpecificationAttributeOption) + .Expand(x => x.SpecificationAttributeOption.SpecificationAttribute) + .Where(x => productIds.Contains(x.ProductId)); + + var map = query + .OrderBy(x => x.DisplayOrder) + .ToList() + .ToMultimap(x => x.ProductId, x => x); + + return map; + } + #endregion #region Related products @@ -1265,7 +1465,7 @@ public virtual void DeleteRelatedProduct(RelatedProduct relatedProduct) _relatedProductRepository.Delete(relatedProduct); //event notification - _eventPublisher.EntityDeleted(relatedProduct); + _services.EventPublisher.EntityDeleted(relatedProduct); } /// @@ -1312,7 +1512,7 @@ public virtual void InsertRelatedProduct(RelatedProduct relatedProduct) _relatedProductRepository.Insert(relatedProduct); //event notification - _eventPublisher.EntityInserted(relatedProduct); + _services.EventPublisher.EntityInserted(relatedProduct); } /// @@ -1327,7 +1527,7 @@ public virtual void UpdateRelatedProduct(RelatedProduct relatedProduct) _relatedProductRepository.Update(relatedProduct); //event notification - _eventPublisher.EntityUpdated(relatedProduct); + _services.EventPublisher.EntityUpdated(relatedProduct); } /// @@ -1363,7 +1563,7 @@ public virtual void DeleteCrossSellProduct(CrossSellProduct crossSellProduct) _crossSellProductRepository.Delete(crossSellProduct); //event notification - _eventPublisher.EntityDeleted(crossSellProduct); + _services.EventPublisher.EntityDeleted(crossSellProduct); } /// @@ -1411,7 +1611,7 @@ public virtual void InsertCrossSellProduct(CrossSellProduct crossSellProduct) _crossSellProductRepository.Insert(crossSellProduct); //event notification - _eventPublisher.EntityInserted(crossSellProduct); + _services.EventPublisher.EntityInserted(crossSellProduct); } /// @@ -1426,7 +1626,7 @@ public virtual void UpdateCrossSellProduct(CrossSellProduct crossSellProduct) _crossSellProductRepository.Update(crossSellProduct); //event notification - _eventPublisher.EntityUpdated(crossSellProduct); + _services.EventPublisher.EntityUpdated(crossSellProduct); } /// @@ -1513,7 +1713,7 @@ public virtual void DeleteTierPrice(TierPrice tierPrice) _cacheManager.RemoveByPattern(PRODUCTS_PATTERN_KEY); //event notification - _eventPublisher.EntityDeleted(tierPrice); + _services.EventPublisher.EntityDeleted(tierPrice); } /// @@ -1530,6 +1730,31 @@ public virtual TierPrice GetTierPriceById(int tierPriceId) return tierPrice; } + public virtual Multimap GetTierPricesByProductIds(int[] productIds, Customer customer = null, int storeId = 0) + { + Guard.ArgumentNotNull(() => productIds); + + var query = + from x in _tierPriceRepository.TableUntracked + where productIds.Contains(x.ProductId) + select x; + + if (storeId != 0) + query = query.Where(x => x.StoreId == 0 || x.StoreId == storeId); + + query = query.OrderBy(x => x.ProductId).ThenBy(x => x.Quantity); + + var list = query.ToList(); + + if (customer != null) + list = list.FilterForCustomer(customer).ToList(); + + var map = list + .ToMultimap(x => x.ProductId, x => x); + + return map; + } + /// /// Inserts a tier price /// @@ -1544,7 +1769,7 @@ public virtual void InsertTierPrice(TierPrice tierPrice) _cacheManager.RemoveByPattern(PRODUCTS_PATTERN_KEY); //event notification - _eventPublisher.EntityInserted(tierPrice); + _services.EventPublisher.EntityInserted(tierPrice); } /// @@ -1561,7 +1786,7 @@ public virtual void UpdateTierPrice(TierPrice tierPrice) _cacheManager.RemoveByPattern(PRODUCTS_PATTERN_KEY); //event notification - _eventPublisher.EntityUpdated(tierPrice); + _services.EventPublisher.EntityUpdated(tierPrice); } #endregion @@ -1582,7 +1807,7 @@ public virtual void DeleteProductPicture(ProductPicture productPicture) _productPictureRepository.Delete(productPicture); //event notification - _eventPublisher.EntityDeleted(productPicture); + _services.EventPublisher.EntityDeleted(productPicture); } private void UnassignDeletedPictureFromVariantCombinations(ProductPicture productPicture) @@ -1630,6 +1855,33 @@ orderby pp.DisplayOrder return productPictures; } + public virtual Multimap GetProductPicturesByProductIds(int[] productIds, bool onlyFirstPicture = false) + { + var query = + from pp in _productPictureRepository.TableUntracked.Expand(x => x.Picture) + where productIds.Contains(pp.ProductId) + orderby pp.ProductId, pp.DisplayOrder + select pp; + + if (onlyFirstPicture) + { + var map = query.GroupBy(x => x.ProductId, x => x) + .Select(x => x.FirstOrDefault()) + .ToList() + .ToMultimap(x => x.ProductId, x => x); + + return map; + } + else + { + var map = query + .ToList() + .ToMultimap(x => x.ProductId, x => x); + + return map; + } + } + /// /// Gets a product picture /// @@ -1656,7 +1908,7 @@ public virtual void InsertProductPicture(ProductPicture productPicture) _productPictureRepository.Insert(productPicture); //event notification - _eventPublisher.EntityInserted(productPicture); + _services.EventPublisher.EntityInserted(productPicture); } /// @@ -1671,7 +1923,7 @@ public virtual void UpdateProductPicture(ProductPicture productPicture) _productPictureRepository.Update(productPicture); //event notification - _eventPublisher.EntityUpdated(productPicture); + _services.EventPublisher.EntityUpdated(productPicture); } #endregion @@ -1699,7 +1951,7 @@ public virtual void InsertBundleItem(ProductBundleItem bundleItem) _productBundleItemRepository.Insert(bundleItem); //event notification - _eventPublisher.EntityInserted(bundleItem); + _services.EventPublisher.EntityInserted(bundleItem); } /// @@ -1714,7 +1966,7 @@ public virtual void UpdateBundleItem(ProductBundleItem bundleItem) _productBundleItemRepository.Update(bundleItem); //event notification - _eventPublisher.EntityUpdated(bundleItem); + _services.EventPublisher.EntityUpdated(bundleItem); } /// @@ -1729,7 +1981,7 @@ public virtual void DeleteBundleItem(ProductBundleItem bundleItem) _productBundleItemRepository.Delete(bundleItem); //event notification - _eventPublisher.EntityDeleted(bundleItem); + _services.EventPublisher.EntityDeleted(bundleItem); } /// @@ -1769,6 +2021,24 @@ orderby pbi.DisplayOrder return bundleItemData; } + public virtual Multimap GetBundleItemsByProductIds(int[] productIds, bool showHidden = false) + { + Guard.ArgumentNotNull(() => productIds); + + var query = + from pbi in _productBundleItemRepository.TableUntracked + join p in _productRepository.TableUntracked on pbi.ProductId equals p.Id + where productIds.Contains(pbi.BundleProductId) && !p.Deleted && (showHidden || (pbi.Published && p.Published)) + orderby pbi.DisplayOrder + select pbi; + + var map = query + .ToList() + .ToMultimap(x => x.BundleProductId, x => x); + + return map; + } + #endregion #endregion 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/AddressService.cs b/src/Libraries/SmartStore.Services/Common/AddressService.cs index 29d705e13b..6ba5546ccb 100644 --- a/src/Libraries/SmartStore.Services/Common/AddressService.cs +++ b/src/Libraries/SmartStore.Services/Common/AddressService.cs @@ -4,6 +4,7 @@ using SmartStore.Core.Domain.Common; using SmartStore.Services.Directory; using SmartStore.Core.Events; +using System.Collections.Generic; namespace SmartStore.Services.Common { @@ -115,6 +116,18 @@ public virtual Address GetAddressById(int addressId) return address; } + public virtual IList
GetAddressByIds(int[] addressIds) + { + Guard.ArgumentNotNull(() => addressIds); + + var query = + from x in _addressRepository.TableUntracked.Expand(x => x.Country).Expand(x => x.StateProvince) + where addressIds.Contains(x.Id) + select x; + + return query.ToList(); + } + /// /// Inserts an address /// diff --git a/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs b/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs index f8d0ae9280..8f23424525 100644 --- a/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Common/GenericAttributeService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Caching; using SmartStore.Core.Data; @@ -165,6 +166,20 @@ public virtual IList GetAttributesForEntity(int entityId, stri }); } + public virtual Multimap GetAttributesForEntity(int[] entityIds, string keyGroup) + { + Guard.ArgumentNotNull(() => entityIds); + + var query = _genericAttributeRepository.TableUntracked + .Where(x => entityIds.Contains(x.EntityId) && x.KeyGroup == keyGroup); + + var map = query + .ToList() + .ToMultimap(x => x.EntityId, x => x); + + return map; + } + /// /// Get queryable attributes /// @@ -194,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/IAddressService.cs b/src/Libraries/SmartStore.Services/Common/IAddressService.cs index e1fcde7e67..37faa65bea 100644 --- a/src/Libraries/SmartStore.Services/Common/IAddressService.cs +++ b/src/Libraries/SmartStore.Services/Common/IAddressService.cs @@ -1,5 +1,6 @@ +using System.Collections.Generic; using SmartStore.Core.Domain.Common; namespace SmartStore.Services.Common @@ -38,6 +39,13 @@ public partial interface IAddressService /// Address Address GetAddressById(int addressId); + /// + /// Gets addresses by address identifiers + /// + /// Address identifiers + /// Addresses + IList
GetAddressByIds(int[] addressIds); + /// /// Inserts an address /// diff --git a/src/Libraries/SmartStore.Services/Common/IGenericAttributeService.cs b/src/Libraries/SmartStore.Services/Common/IGenericAttributeService.cs index 4aabb70b7b..c584c4ad4c 100644 --- a/src/Libraries/SmartStore.Services/Common/IGenericAttributeService.cs +++ b/src/Libraries/SmartStore.Services/Common/IGenericAttributeService.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Domain.Common; @@ -40,9 +41,17 @@ public partial interface IGenericAttributeService ///
/// Entity identifier /// Key group - /// Get attributes + /// Generic attributes IList GetAttributesForEntity(int entityId, string keyGroup); + /// + /// Get attributes + /// + /// Entity identifiers + /// Key group + /// Generic attributes + Multimap GetAttributesForEntity(int[] entityIds, string keyGroup); + /// /// Get queryable attributes /// @@ -60,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/KeepAliveTask.cs b/src/Libraries/SmartStore.Services/Common/KeepAliveTask.cs deleted file mode 100644 index 74a382ccae..0000000000 --- a/src/Libraries/SmartStore.Services/Common/KeepAliveTask.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Net; -using SmartStore.Core; -using SmartStore.Services.Tasks; - -namespace SmartStore.Services.Common -{ - /// - /// Represents a task for keeping the site alive - /// - public partial class KeepAliveTask : ITask - { - private readonly IStoreContext _storeContext; - - public KeepAliveTask(IStoreContext storeContext) - { - this._storeContext = storeContext; - } - - public void Execute(TaskExecutionContext ctx) - { - var storeUrl = _storeContext.CurrentStore.Url.TrimEnd('\\').EnsureEndsWith("/"); - string url = storeUrl + "keepalive/index"; - - try - { - using (var wc = new WebClient()) - { - // FAKE a user-agent - wc.Headers.Add("user-agent", @"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36"); - if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && !url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) - { - url = "http://" + url; - } - wc.DownloadString(url); - } - } - catch (WebException ex) - { - if (ex.Status == WebExceptionStatus.ProtocolError && ex.Response != null) - { - var resp = (HttpWebResponse)ex.Response; - if (resp.StatusCode == HttpStatusCode.NotFound) // HTTP 404 - { - // the page was not found (as it can be expected with some webservers) - return; - } - } - // throw any other exception - this should not occur - throw; - } - } - } -} 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 f9b7c7afe8..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 { @@ -81,7 +77,7 @@ protected virtual IDictionary> GetAllSettingsCa orderby s.Name, s.StoreId select s; var settings = query.ToList(); - var dictionary = new Dictionary>(); + var dictionary = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (var s in settings) { var settingName = s.Name.ToLowerInvariant(); @@ -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,16 +389,16 @@ 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 { //insert - var setting = new Setting() + 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/CustomerRegisteredEvent.cs b/src/Libraries/SmartStore.Services/Customers/CustomerRegisteredEvent.cs new file mode 100644 index 0000000000..76bc89b1de --- /dev/null +++ b/src/Libraries/SmartStore.Services/Customers/CustomerRegisteredEvent.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using SmartStore.Core.Domain.Customers; + +namespace SmartStore.Services.Customers +{ + /// + /// An event message, which gets published after customer was registered + /// + public class CustomerRegisteredEvent + { + public Customer Customer + { + get; + set; + } + } +} diff --git a/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationService.cs b/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationService.cs index 2ee2141309..49fb4587aa 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationService.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerRegistrationService.cs @@ -2,26 +2,27 @@ using System.Linq; using SmartStore.Core; using SmartStore.Core.Domain.Customers; -using SmartStore.Services.Localization; +using SmartStore.Core.Events; +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; + private readonly IEventPublisher _eventPublisher; #endregion @@ -33,30 +34,33 @@ public partial class CustomerRegistrationService : ICustomerRegistrationService public CustomerRegistrationService(ICustomerService customerService, IEncryptionService encryptionService, INewsLetterSubscriptionService newsLetterSubscriptionService, - ILocalizationService localizationService, RewardPointsSettings rewardPointsSettings, CustomerSettings customerSettings, - IStoreContext storeContext) + 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) @@ -109,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; } } @@ -158,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; } } @@ -177,44 +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 }); + return result; } @@ -231,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; } @@ -268,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; @@ -323,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); @@ -366,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 7641b6bd45..d48fe10d6c 100644 --- a/src/Libraries/SmartStore.Services/Customers/CustomerService.cs +++ b/src/Libraries/SmartStore.Services/Customers/CustomerService.cs @@ -4,35 +4,36 @@ using System.Globalization; using System.Linq; using System.Linq.Expressions; +using System.Web; +using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Caching; using SmartStore.Core.Data; -using SmartStore.Core.Domain.Blogs; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Forums; -using SmartStore.Core.Domain.News; using SmartStore.Core.Domain.Orders; -using SmartStore.Core.Domain.Polls; 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."; + #endregion #region Fields @@ -40,39 +41,40 @@ public partial class CustomerService : ICustomerService private readonly IRepository _customerRepository; private readonly IRepository _customerRoleRepository; 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 HttpContextBase _httpContext; + private readonly IUserAgent _userAgent; - #endregion + #endregion - #region Ctor - - /// - /// Ctor - /// - /// Cache manager - /// Customer repository - /// Customer role repository - /// Generic attribute repository - /// Generic attribute service - /// Event published - public CustomerService(ICacheManager cacheManager, + #region Ctor + + public CustomerService( IRepository customerRepository, IRepository customerRoleRepository, IRepository gaRepository, + IRepository rewardPointsHistoryRepository, IGenericAttributeService genericAttributeService, - IEventPublisher eventPublisher, - RewardPointsSettings rewardPointsSettings) + ICommonServices services, + ICacheManager cache, + RewardPointsSettings rewardPointsSettings, + 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._httpContext = httpContext; + this._userAgent = userAgent; T = NullLocalizer.Instance; } @@ -89,26 +91,6 @@ public CustomerService(ICacheManager cacheManager, #region Customers - /// - /// Gets all customers - /// - /// Customer registration from; null to load all customers - /// Customer registration to; null to load all customers - /// A list of customer role identifiers to filter by (at least one match); pass null or empty list in order to load all customers; - /// Email; null to load all customers - /// Username; null to load all customers - /// First name; null to load all customers - /// Last name; null to load all customers - /// Day of birth; 0 to load all customers - /// Month of birth; 0 to load all customers - /// Company; null to load all customers - /// Phone; null to load all customers - /// Phone; null to load all customers - /// Value indicating whther to load customers only with shopping cart - /// Value indicating what shopping cart type to filter; userd when 'loadOnlyWithShoppingCart' param is 'true' - /// Page index - /// Page size - /// Customer collection public virtual IPagedList GetAllCustomers(DateTime? registrationFrom, DateTime? registrationTo, int[] customerRoleIds, string email, string username, string firstName, string lastName, int dayOfBirth, int monthOfBirth, @@ -241,13 +223,6 @@ public virtual IPagedList GetAllCustomers(DateTime? registrationFrom, return customers; } - /// - /// Gets all customers by affiliate identifier - /// - /// Affiliate identifier - /// Page index - /// Page size - /// Customers public virtual IPagedList GetAllCustomers(int affiliateId, int pageIndex, int pageSize) { var query = _customerRepository.Table; @@ -259,11 +234,6 @@ public virtual IPagedList GetAllCustomers(int affiliateId, int pageInd return customers; } - /// - /// Gets all customers by customer format (including deleted ones) - /// - /// Password format - /// Customers public virtual IList GetAllCustomersByPasswordFormat(PasswordFormat passwordFormat) { int passwordFormatId = (int)passwordFormat; @@ -275,14 +245,6 @@ public virtual IList GetAllCustomersByPasswordFormat(PasswordFormat pa return customers; } - /// - /// Gets online customers - /// - /// Customer last activity date (from) - /// A list of customer role identifiers to filter by (at least one match); pass null or empty list in order to load all customers; - /// Page index - /// Page size - /// Customer collection public virtual IPagedList GetOnlineCustomers(DateTime lastActivityFromUtc, int[] customerRoleIds, int pageIndex, int pageSize) { @@ -297,10 +259,6 @@ public virtual IPagedList GetOnlineCustomers(DateTime lastActivityFrom return customers; } - /// - /// Delete a customer - /// - /// Customer public virtual void DeleteCustomer(Customer customer) { if (customer == null) @@ -313,11 +271,6 @@ public virtual void DeleteCustomer(Customer customer) UpdateCustomer(customer); } - /// - /// Gets a customer - /// - /// Customer identifier - /// A customer public virtual Customer GetCustomerById(int customerId) { if (customerId == 0) @@ -327,11 +280,6 @@ public virtual Customer GetCustomerById(int customerId) return customer; } - /// - /// Get customers by identifiers - /// - /// Customer identifiers - /// Customers public virtual IList GetCustomersByIds(int[] customerIds) { if (customerIds == null || customerIds.Length == 0) @@ -352,11 +300,11 @@ where customerIds.Contains(c.Id) return sortedCustomers; } - /// - /// Gets a customer by GUID - /// - /// Customer GUID - /// A customer + public virtual IList GetSystemAccountCustomers() + { + return _customerRepository.Table.Where(x => x.IsSystemAccount).ToList(); + } + public virtual Customer GetCustomerByGuid(Guid customerGuid) { if (customerGuid == Guid.Empty) @@ -370,11 +318,6 @@ orderby c.Id return customer; } - /// - /// Get customer by email - /// - /// Email - /// Customer public virtual Customer GetCustomerByEmail(string email) { if (string.IsNullOrWhiteSpace(email)) @@ -388,29 +331,20 @@ orderby c.Id return customer; } - /// - /// Get customer by system name - /// - /// System name - /// Customer public virtual Customer GetCustomerBySystemName(string systemName) { if (string.IsNullOrWhiteSpace(systemName)) return null; - var query = from c in _customerRepository.Table - orderby c.Id - where c.SystemName == systemName - select c; - var customer = query.FirstOrDefault(); - return customer; + var query = from c in _customerRepository.Table + orderby c.Id + where c.SystemName == systemName + select c; + var customer = query.FirstOrDefault(); + + return customer; } - /// - /// Get customer by username - /// - /// Username - /// Customer public virtual Customer GetCustomerByUsername(string username) { if (string.IsNullOrWhiteSpace(username)) @@ -425,35 +359,63 @@ orderby c.Id return customer; } - /// - /// Insert a guest customer - /// - /// Customer - public virtual Customer InsertGuestCustomer() + public virtual Customer InsertGuestCustomer(Guid? customerGuid = null) { - 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(); + } - /// - /// Insert a customer - /// - /// Customer public virtual void InsertCustomer(Customer customer) { if (customer == null) @@ -461,14 +423,9 @@ public virtual void InsertCustomer(Customer customer) _customerRepository.Insert(customer); - //event notification - _eventPublisher.EntityInserted(customer); + _services.EventPublisher.EntityInserted(customer); } - /// - /// Updates the customer - /// - /// Customer public virtual void UpdateCustomer(Customer customer) { if (customer == null) @@ -476,20 +433,9 @@ public virtual void UpdateCustomer(Customer customer) _customerRepository.Update(customer); - //event notification - _eventPublisher.EntityUpdated(customer); + _services.EventPublisher.EntityUpdated(customer); } - /// - /// Reset data required for checkout - /// - /// Customer - /// Store identifier - /// A value indicating whether to clear coupon code - /// A value indicating whether to clear selected checkout attributes - /// A value indicating whether to clear "Use reward points" flag - /// A value indicating whether to clear selected shipping method - /// A value indicating whether to clear selected payment method public virtual void ResetCheckoutData(Customer customer, int storeId, bool clearCouponCodes = false, bool clearCheckoutAttributes = false, bool clearRewardPoints = true, bool clearShippingMethod = true, @@ -533,18 +479,11 @@ public virtual void ResetCheckoutData(Customer customer, int storeId, UpdateCustomer(customer); } - /// - /// Delete guest customer records - /// - /// Customer registration from; null to load all customers - /// Customer registration to; null to load all customers - /// A value indicating whether to delete customers only without shopping cart - /// Number of deleted customers public virtual int DeleteGuestCustomers(DateTime? registrationFrom, DateTime? registrationTo, bool onlyWithoutShoppingCart, int maxItemsToDelete = 5000) { var ctx = _customerRepository.Context; - using (var scope = new DbContextScope(ctx: ctx, autoDetectChanges: false, proxyCreation: true, validateOnSave: false, forceNoTracking: true)) + using (var scope = new DbContextScope(ctx: ctx, autoDetectChanges: false, proxyCreation: true, validateOnSave: false, forceNoTracking: true, autoCommit: false)) { var guestRole = GetCustomerRoleBySystemName(SystemCustomerRoleNames.Guests); if (guestRole == null) @@ -586,12 +525,7 @@ orderby cGroup.Key query = query.OrderBy(c => c.Id); var customers = query.Take(maxItemsToDelete).ToList(); - - var crAutoCommit = _customerRepository.AutoCommitEnabled; - var gaAutoCommit = _gaRepository.AutoCommitEnabled; - _customerRepository.AutoCommitEnabled = false; - _gaRepository.AutoCommitEnabled = false; - + int numberOfDeletedCustomers = 0; foreach (var c in customers) { @@ -604,10 +538,7 @@ orderby cGroup.Key select ga; var attributes = gaQuery.ToList(); - foreach (var attribute in attributes) - { - _gaRepository.Delete(attribute); - } + _gaRepository.DeleteRange(attributes); // delete customer _customerRepository.Delete(c); @@ -635,9 +566,6 @@ orderby cGroup.Key // save the rest scope.Commit(); - _customerRepository.AutoCommitEnabled = crAutoCommit; - _gaRepository.AutoCommitEnabled = gaAutoCommit; - return numberOfDeletedCustomers; } } @@ -674,10 +602,6 @@ from inner in c_inner.DefaultIfEmpty() #region Customer roles - /// - /// Delete a customer role - /// - /// Customer role public virtual void DeleteCustomerRole(CustomerRole customerRole) { if (customerRole == null) @@ -688,17 +612,11 @@ 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); } - /// - /// Gets a customer role - /// - /// Customer role identifier - /// Customer role public virtual CustomerRole GetCustomerRoleById(int customerRoleId) { if (customerRoleId == 0) @@ -707,18 +625,13 @@ public virtual CustomerRole GetCustomerRoleById(int customerRoleId) return _customerRoleRepository.GetById(customerRoleId); } - /// - /// Gets a customer role - /// - /// Customer role system name - /// Customer role public virtual CustomerRole GetCustomerRoleBySystemName(string systemName) { if (String.IsNullOrWhiteSpace(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 @@ -729,15 +642,10 @@ orderby cr.Id }); } - /// - /// Gets all customer roles - /// - /// A value indicating whether to show hidden records - /// Customer role collection 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 @@ -748,10 +656,6 @@ orderby cr.Name }); } - /// - /// Inserts a customer role - /// - /// Customer role public virtual void InsertCustomerRole(CustomerRole customerRole) { if (customerRole == null) @@ -759,16 +663,11 @@ 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); } - /// - /// Updates the customer role - /// - /// Customer role public virtual void UpdateCustomerRole(CustomerRole customerRole) { if (customerRole == null) @@ -776,22 +675,15 @@ 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 #region Reward points - /// - /// Add or remove reward points for a product review - /// - /// The customer - /// The product - /// Whether to add or remove points public virtual void RewardPointsForProductReview(Customer customer, Product product, bool add) { if (_rewardPointsSettings.Enabled && _rewardPointsSettings.PointsForProductReview > 0) @@ -804,6 +696,25 @@ public virtual void RewardPointsForProductReview(Customer customer, Product prod } } + public virtual Multimap GetRewardPointsHistoriesByCustomerIds(int[] customerIds) + { + Guard.ArgumentNotNull(() => customerIds); + + var query = + from x in _rewardPointsHistoryRepository.TableUntracked + where customerIds.Contains(x.CustomerId) + select x; + + var map = query + .OrderBy(x => x.CustomerId) + .ThenByDescending(x => x.CreatedOnUtc) + .ThenByDescending(x => x.Id) + .ToList() + .ToMultimap(x => x.CustomerId, x => x); + + return map; + } + #endregion Reward points #endregion diff --git a/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs b/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs index d5d3ce672c..57a2c6f8c9 100644 --- a/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs +++ b/src/Libraries/SmartStore.Services/Customers/ICustomerService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Customers; @@ -87,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 @@ -118,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 @@ -212,6 +231,13 @@ void ResetCheckoutData(Customer customer, int storeId, /// Whether to add or remove points void RewardPointsForProductReview(Customer customer, Product product, bool add); + /// + /// Gets reward points histories + /// + /// Customer identifiers + /// Reward points histories + Multimap GetRewardPointsHistoriesByCustomerIds(int[] customerIds); + #endregion Reward points } } \ No newline at end of file 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/Export/DataExportResult.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportResult.cs new file mode 100644 index 0000000000..c9d3fa26f2 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/DataExportResult.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace SmartStore.Services.DataExchange.Export +{ + [Serializable] + public class DataExportResult + { + public DataExportResult() + { + Files = new List(); + } + + /// + /// Whether the export succeeded + /// + public bool Succeeded + { + get { return LastError.IsEmpty(); } + } + + /// + /// Last error + /// + [XmlIgnore] + public string LastError { get; set; } + + /// + /// Files created by last export + /// + public List Files { get; set; } + + /// + /// The path of the folder with the export files + /// + [XmlIgnore] + public string FileFolder { get; set; } + + [Serializable] + public class ExportFileInfo + { + /// + /// Store identifier, can be 0. + /// + public int StoreId { get; set; } + + /// + /// Name of file + /// + public string FileName { 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/Export/ExportConfigurationInfo.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportConfigurationInfo.cs new file mode 100644 index 0000000000..2cfb17796d --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportConfigurationInfo.cs @@ -0,0 +1,25 @@ +using System; + +namespace SmartStore.Services.DataExchange.Export +{ + /// + /// Serves information about export provider specific configuration + /// + public class ExportConfigurationInfo + { + /// + /// The partial view name for the configuration + /// + public string PartialViewName { get; set; } + + /// + /// Type of the view model + /// + public Type ModelType { get; set; } + + /// + /// Callback to initialize the view model. Can be null. + /// + public Action Initialize { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportDataSegmenter.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportDataSegmenter.cs new file mode 100644 index 0000000000..c84182c6e0 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportDataSegmenter.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using SmartStore.Core; + +namespace SmartStore.Services.DataExchange.Export +{ + public interface IExportDataSegmenterConsumer + { + /// + /// Total number of records + /// + int TotalRecords { get; } + + /// + /// Gets current data segment + /// + IReadOnlyCollection CurrentSegment { get; } + + /// + /// Reads the next segment + /// + /// + bool ReadNextSegment(); + } + + internal interface IExportDataSegmenterProvider : IExportDataSegmenterConsumer, IDisposable + { + /// + /// Whether there is data available + /// + bool HasData { get; } + + /// + /// Record per segment counter + /// + int RecordPerSegmentCount { get; set; } + } + + public class ExportDataSegmenter : IExportDataSegmenterProvider where T : BaseEntity + { + private Func> _load; + private Action> _loaded; + private Func> _convert; + + private int _offset; + private int _take; + private int _limit; + private int _recordsPerSegment; + private int _totalRecords; + + private int _skip; + private int _countRecords; + + private Queue _data; + + public ExportDataSegmenter( + Func> load, + Action> loaded, + Func> convert, + int offset, + int take, + int limit, + int recordsPerSegment, + int totalRecords) + { + _load = load; + _loaded = loaded; + _convert = convert; + _offset = offset; + _take = take; + _limit = limit; + _recordsPerSegment = recordsPerSegment; + _totalRecords = totalRecords; + + _skip = _offset; + } + + /// + /// Total number of records + /// + public int TotalRecords + { + get + { + var total = Math.Max(_totalRecords - _offset, 0); + + if (_limit > 0 && _limit < total) + return _limit; + + return total; + } + } + + /// + /// Number of processed records + /// + public int RecordCount + { + get { return _countRecords; } + } + + /// + /// Record per segment counter + /// + public int RecordPerSegmentCount { get; set; } + + /// + /// Whether there is data available + /// + public bool HasData + { + get + { + if (_limit > 0 && _countRecords >= _limit) + return false; + + if (_data != null && _data.Count > 0) + return true; + + if (_skip >= _totalRecords) + return false; + + return true; + } + } + + /// + /// Gets current data segment + /// + public IReadOnlyCollection CurrentSegment + { + get + { + T entity; + var records = new List(); + + while (_data.Count > 0 && (entity = _data.Dequeue()) != null) + { + _convert(entity).Each(x => records.Add(x)); + + if (++_countRecords >= _limit && _limit > 0) + return records; + + if (++RecordPerSegmentCount >= _recordsPerSegment && _recordsPerSegment > 0) + return records; + } + + return records; + } + } + + /// + /// Reads the next segment + /// + /// + public bool ReadNextSegment() + { + if (_limit > 0 && _countRecords >= _limit) + return false; + + if (_recordsPerSegment > 0 && RecordPerSegmentCount >= _recordsPerSegment) + return false; + + // do not make the queue longer than necessary + if (_recordsPerSegment > 0 && _data != null && _data.Count >= _recordsPerSegment) + return true; + + if (_skip >= _totalRecords) + return false; + + if (_data != null) + _skip += _take; + + if (_data != null && _data.Count > 0) + { + var data = new List(_data); + data.AddRange(_load(_skip)); + + _data = new Queue(data); + } + else + { + _data = new Queue(_load(_skip)); + } + + // give provider the opportunity to make something with entity ids + if (_loaded != null) + { + _loaded(_data.AsReadOnly()); + } + + return (_data.Count > 0); + } + + /// + /// Dispose and reset segmenter instance + /// + public void Dispose() + { + if (_data != null) + _data.Clear(); + + _skip = _offset; + _countRecords = 0; + RecordPerSegmentCount = 0; + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExecuteContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExecuteContext.cs new file mode 100644 index 0000000000..d5f2860ff8 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportExecuteContext.cs @@ -0,0 +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.Export +{ + 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 + /// + public IExportDataSegmenterConsumer DataSegmenter { get; set; } + + /// + /// The store context to be used for the export + /// + public dynamic Store { get; internal set; } + + /// + /// The customer context to be used for the export + /// + public dynamic Customer { get; internal set; } + + /// + /// The currency context to be used for the export + /// + public dynamic Currency { get; internal set; } + + /// + /// The language context to be used for the export + /// + public dynamic Language { get; internal set; } + + /// + /// Projection data + /// + public ExportProjection Projection { get; internal set; } + + /// + /// To log information into the export log file + /// + public ILogger Log { get; internal set; } + + /// + /// Indicates whether and how to abort the export + /// + public DataExchangeAbortion Abort + { + get + { + if (_cancellation.IsCancellationRequested || IsMaxFailures) + return DataExchangeAbortion.Hard; + + return _providerAbort; + } + set + { + _providerAbort = value; + } + } + + public bool IsMaxFailures + { + get { return RecordsFailed > 11; } + } + + /// + /// Identifier of current data stream. Can be null. + /// + public string DataStreamId { get; set; } + + /// + /// Stream used to write data to + /// + public Stream DataStream { get; internal set; } + + /// + /// List with extra data units/streams required by provider + /// + public List ExtraDataUnits { get; private set; } + + /// + /// The maximum allowed file name length + /// + public int MaxFileNameLength { get; internal set; } + + /// + /// The name of the current export file + /// + public string FileName { get; internal set; } + + /// + /// The path of the export content folder + /// + public string Folder { get; private set; } + + /// + /// Whether the profile has a public deployment into "Exchange" folder + /// + 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 + /// + public object ConfigurationData { get; internal set; } + + /// + /// Use this dictionary for any custom data required along the export + /// + public Dictionary CustomProperties { get; set; } + + /// + /// Number of successful processed records + /// + public int RecordsSucceeded { get; set; } + + /// + /// Number of failed records + /// + public int RecordsFailed { get; set; } + + /// + /// Processes an exception that occurred while exporting a record + /// + /// Exception + public void RecordException(Exception exception, int entityId) + { + ++RecordsFailed; + + Log.Error("Error while processing record with id {0}: {1}".FormatInvariant(entityId, exception.ToAllMessages()), exception); + + if (IsMaxFailures) + _result.LastError = exception.ToString(); + } + + public ProgressValueSetter ProgressValueSetter { get; internal set; } + + /// + /// Allows to set a progress message + /// + /// Output message + public void SetProgress(string message) + { + if (ProgressValueSetter != null && message.HasValue()) + { + try + { + ProgressValueSetter.Invoke(0, 0, message); + } + catch { } + } + } + } + + public class ExportDataUnit + { + /// + /// Your Id to identify this stream within a list of streams + /// + public string Id { get; set; } + + /// + /// Stream used to write data to + /// + public Stream DataStream { get; internal set; } + + /// + /// 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/Export/ExportFeaturesAttribute.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportFeaturesAttribute.cs new file mode 100644 index 0000000000..eee6f1554e --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportFeaturesAttribute.cs @@ -0,0 +1,15 @@ +using System; +using SmartStore.Core.Domain.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 ExportFeaturesAttribute : Attribute + { + public ExportFeatures Features { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs new file mode 100644 index 0000000000..1814ae56ae --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/ExportProfileService.cs @@ -0,0 +1,365 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SmartStore.Core; +using SmartStore.Core.Data; +using SmartStore.Core.Domain; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Domain.Tasks; +using SmartStore.Core.Events; +using SmartStore.Core.Plugins; +using SmartStore.Services.Localization; +using SmartStore.Services.Tasks; +using SmartStore.Utilities; + +namespace SmartStore.Services.DataExchange.Export +{ + public partial class ExportProfileService : IExportProfileService + { + private const string _defaultFileNamePattern = "%Store.Id%-%Profile.Id%-%File.Index%-%Profile.SeoName%"; + + private readonly IRepository _exportProfileRepository; + private readonly IRepository _exportDeploymentRepository; + private readonly IEventPublisher _eventPublisher; + private readonly IScheduleTaskService _scheduleTaskService; + private readonly IProviderManager _providerManager; + private readonly DataExchangeSettings _dataExchangeSettings; + private readonly ILocalizationService _localizationService; + + public ExportProfileService( + IRepository exportProfileRepository, + IRepository exportDeploymentRepository, + IEventPublisher eventPublisher, + IScheduleTaskService scheduleTaskService, + IProviderManager providerManager, + DataExchangeSettings dataExchangeSettings, + ILocalizationService localizationService) + { + _exportProfileRepository = exportProfileRepository; + _exportDeploymentRepository = exportDeploymentRepository; + _eventPublisher = eventPublisher; + _scheduleTaskService = scheduleTaskService; + _providerManager = providerManager; + _dataExchangeSettings = dataExchangeSettings; + _localizationService = localizationService; + } + + public virtual ExportProfile InsertExportProfile( + string providerSystemName, + string name, + string fileExtension, + ExportFeatures features, + bool isSystemProfile = false, + string profileSystemName = null, + int cloneFromProfileId = 0) + { + Guard.ArgumentNotEmpty(() => providerSystemName); + + var profileCount = _exportProfileRepository.Table.Count(x => x.ProviderSystemName == providerSystemName); + + if (name.IsEmpty()) + name = providerSystemName; + + if (!isSystemProfile) + name = string.Concat(_localizationService.GetResource("Common.My"), " ", name); + + name = string.Concat(name, " ", profileCount + 1); + + var cloneProfile = GetExportProfileById(cloneFromProfileId); + + ScheduleTask task = null; + ExportProfile profile = null; + + if (cloneProfile == null) + { + task = new ScheduleTask + { + CronExpression = "0 */6 * * *", // every six hours + Type = typeof(DataExportTask).AssemblyQualifiedNameWithoutVersion(), + Enabled = false, + StopOnError = false, + IsHidden = true + }; + } + else + { + task = cloneProfile.ScheduleTask.Clone(); + task.LastEndUtc = task.LastStartUtc = task.LastSuccessUtc = null; + } + + task.Name = string.Concat(name, " Task"); + + _scheduleTaskService.InsertTask(task); + + if (cloneProfile == null) + { + profile = new ExportProfile + { + FileNamePattern = _defaultFileNamePattern + }; + + if (isSystemProfile) + { + profile.Enabled = true; + profile.PerStore = false; + profile.CreateZipArchive = false; + profile.Cleanup = false; + } + else + { + // what we do here is to preset typical settings for feed creation + // but on the other hand they may be untypical for generic data export\exchange + var projection = new ExportProjection + { + RemoveCriticalCharacters = true, + CriticalCharacters = "¼,½,¾", + PriceType = PriceDisplayType.PreSelectedPrice, + NoGroupedProducts = (features.HasFlag(ExportFeatures.CanOmitGroupedProducts) ? 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.IsSystemProfile = isSystemProfile; + profile.Name = name; + 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 (fileExtension.HasValue() && !isSystemProfile) + { + if (cloneProfile == null) + { + if (features.HasFlag(ExportFeatures.CreatesInitialPublicDeployment)) + { + var subFolder = FileSystemHelper.CreateNonExistingDirectoryName(CommonHelper.MapPath("~/" + DataExporter.PublicFolder), folderName); + + profile.Deployments.Add(new ExportDeployment + { + ProfileId = profile.Id, + Enabled = true, + DeploymentType = ExportDeploymentType.PublicFolder, + Name = profile.Name, + SubFolder = subFolder + }); + + UpdateExportProfile(profile); + } + } + else + { + foreach (var deployment in cloneProfile.Deployments) + { + profile.Deployments.Add(deployment.Clone()); + } + + UpdateExportProfile(profile); + } + } + + _eventPublisher.EntityInserted(profile); + + 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, 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(); + + _exportProfileRepository.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 GetExportProfiles(bool? enabled = null) + { + var query = _exportProfileRepository.Table + .Expand(x => x.ScheduleTask) + .Expand(x => x.Deployments); + + if (enabled.HasValue) + { + query = query.Where(x => x.Enabled == enabled.Value); + } + + query = query + .OrderBy(x => x.IsSystemProfile) + .ThenBy(x => x.Name); + + return query; + } + + public virtual ExportProfile GetExportProfileById(int id) + { + if (id == 0) + return null; + + var profile = _exportProfileRepository.Table + .Expand(x => x.ScheduleTask) + .Expand(x => x.Deployments) + .FirstOrDefault(x => x.Id == id); + + return profile; + } + + 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 (providerSystemName.IsEmpty()) + return new List(); + + var profiles = _exportProfileRepository.Table + .Expand(x => x.ScheduleTask) + .Expand(x => x.Deployments) + .Where(x => x.ProviderSystemName == providerSystemName) + .ToList(); + + return profiles; + } + + + public virtual IEnumerable> LoadAllExportProviders(int storeId = 0, bool showHidden = true) + { + var allProviders = _providerManager.GetAllProviders(storeId) + .Where(x => x.IsValid() && (showHidden || !x.Metadata.IsHidden)) + //.OrderBy(x => x.Metadata.SystemName) + .OrderBy(x => x.Metadata.FriendlyName); + + return allProviders; + } + + public virtual Provider LoadProvider(string systemName, int storeId = 0) + { + var provider = _providerManager.GetProvider(systemName, storeId); + + return (provider.IsValid() ? provider : null); + } + + public virtual ExportDeployment GetExportDeploymentById(int id) + { + if (id == 0) + return null; + + var deployment = _exportDeploymentRepository.Table + .Expand(x => x.Profile) + .FirstOrDefault(x => x.Id == 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) + throw new ArgumentNullException("deployment"); + + _exportDeploymentRepository.Delete(deployment); + + _eventPublisher.EntityDeleted(deployment); + } + } +} 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/Export/IExportProfileService.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProfileService.cs new file mode 100644 index 0000000000..b0b0c8a6bd --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProfileService.cs @@ -0,0 +1,122 @@ +using System.Collections.Generic; +using System.Linq; +using SmartStore.Core.Domain; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Plugins; + +namespace SmartStore.Services.DataExchange.Export +{ + public interface IExportProfileService + { + /// + /// Inserts an export profile + /// + /// 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 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, + bool isSystemProfile = false, + string profileSystemName = null, + int cloneFromProfileId = 0); + + /// + /// Updates an export profile + /// + /// Export profile + void UpdateExportProfile(ExportProfile profile); + + /// + /// Deletes an export profile + /// + /// Export profile + /// Whether to delete system profiles + void DeleteExportProfile(ExportProfile profile, bool force = false); + + /// + /// Get queryable export profiles + /// + /// Whether to filter enabled or disabled profiles + /// Export profiles + IQueryable GetExportProfiles(bool? enabled = null); + + /// + /// Gets an export profile by identifier + /// + /// Export profile identifier + /// 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 + /// List of export profiles + IList GetExportProfilesBySystemName(string providerSystemName); + + + /// + /// Load all export providers + /// + /// Store identifier + /// Whether to load hidden providers + /// Export providers + IEnumerable> LoadAllExportProviders(int storeId = 0, bool showHidden = true); + + /// + /// Load export provider by system name + /// + /// Provider system name + /// Store identifier + /// Export provider + Provider LoadProvider(string systemName, int storeId = 0); + + /// + /// Get export deployment by identifier + /// + /// Export deployment identifier + /// Export deployment + ExportDeployment GetExportDeploymentById(int id); + + /// + /// 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/Export/IExportProvider.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProvider.cs new file mode 100644 index 0000000000..60be2e11a9 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/IExportProvider.cs @@ -0,0 +1,35 @@ +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Plugins; + +namespace SmartStore.Services.DataExchange.Export +{ + public partial interface IExportProvider : IProvider, IUserEditable + { + /// + /// The exported entity type + /// + ExportEntityType EntityType { get; } + + /// + /// File extension of the export files (without dot). Return null for a non file based, on-the-fly export. + /// + string FileExtension { get; } + + /// + /// Get provider specific configuration information. Return null when no provider specific configuration is required. + /// + ExportConfigurationInfo ConfigurationInfo { get; } + + /// + /// Export data to a file + /// + /// Export execution context + void Execute(ExportExecuteContext context); + + /// + /// Called once per store when export execution ended + /// + /// Export execution context + void OnExecuted(ExportExecuteContext context); + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CategoryExportContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CategoryExportContext.cs new file mode 100644 index 0000000000..ab6665b31e --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CategoryExportContext.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SmartStore.Collections; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Media; + +namespace SmartStore.Services.DataExchange.Export.Internal +{ + internal class CategoryExportContext + { + protected List _categoryIds; + protected List _pictureIds; + + private Func> _funcProductCategories; + private Func> _funcPictures; + + private LazyMultimap _productCategories; + private LazyMultimap _pictures; + + public CategoryExportContext( + IEnumerable categories, + Func> productCategories, + Func> pictures) + { + if (categories == null) + { + _categoryIds = new List(); + _pictureIds = new List(); + } + else + { + _categoryIds = new List(categories.Select(x => x.Id)); + _pictureIds = new List(categories.Where(x => (x.PictureId ?? 0) != 0).Select(x => x.PictureId ?? 0)); + } + + _funcProductCategories = productCategories; + _funcPictures = pictures; + } + + public void Clear() + { + if (_productCategories != null) + _productCategories.Clear(); + if (_pictures != null) + _pictures.Clear(); + + _categoryIds.Clear(); + _pictureIds.Clear(); + } + + public LazyMultimap ProductCategories + { + get + { + if (_productCategories == null) + { + _productCategories = new LazyMultimap(keys => _funcProductCategories(keys), _categoryIds); + } + return _productCategories; + } + } + + public LazyMultimap Pictures + { + get + { + if (_pictures == null) + { + _pictures = new LazyMultimap(keys => _funcPictures(keys).ToMultimap(x => x.Id, x => x), _pictureIds); + } + return _pictures; + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CustomerExportContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CustomerExportContext.cs new file mode 100644 index 0000000000..918cb3e110 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/CustomerExportContext.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SmartStore.Collections; +using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Customers; + +namespace SmartStore.Services.DataExchange.Export.Internal +{ + public class CustomerExportContext + { + protected List _customerIds; + + private Func> _funcGenericAttributes; + + private LazyMultimap _genericAttributes; + + public CustomerExportContext( + IEnumerable customers, + Func> genericAttributes) + { + if (customers == null) + { + _customerIds = new List(); + } + else + { + _customerIds = new List(customers.Select(x => x.Id)); + } + + _funcGenericAttributes = genericAttributes; + } + + public void Clear() + { + if (_genericAttributes != null) + _genericAttributes.Clear(); + + _customerIds.Clear(); + } + + public LazyMultimap GenericAttributes + { + get + { + if (_genericAttributes == null) + { + _genericAttributes = new LazyMultimap(keys => _funcGenericAttributes(keys), _customerIds); + } + return _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/Export/Internal/ManufacturerExportContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ManufacturerExportContext.cs new file mode 100644 index 0000000000..82c607f5ad --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ManufacturerExportContext.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SmartStore.Collections; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Media; + +namespace SmartStore.Services.DataExchange.Export.Internal +{ + internal class ManufacturerExportContext + { + protected List _manufacturerIds; + protected List _pictureIds; + + private Func> _funcProductManufacturers; + private Func> _funcPictures; + + private LazyMultimap _productManufacturers; + private LazyMultimap _pictures; + + public ManufacturerExportContext( + IEnumerable manufacturers, + Func> productManufacturers, + Func> pictures) + { + if (manufacturers == null) + { + _manufacturerIds = new List(); + _pictureIds = new List(); + } + else + { + _manufacturerIds = new List(manufacturers.Select(x => x.Id)); + _pictureIds = new List(manufacturers.Where(x => (x.PictureId ?? 0) != 0).Select(x => x.PictureId ?? 0)); + } + + _funcProductManufacturers = productManufacturers; + _funcPictures = pictures; + } + + public void Clear() + { + if (_productManufacturers != null) + _productManufacturers.Clear(); + if (_pictures != null) + _pictures.Clear(); + + _manufacturerIds.Clear(); + _pictureIds.Clear(); + } + + public LazyMultimap ProductManufacturers + { + get + { + if (_productManufacturers == null) + { + _productManufacturers = new LazyMultimap(keys => _funcProductManufacturers(keys), _manufacturerIds); + } + return _productManufacturers; + } + } + + public LazyMultimap Pictures + { + get + { + if (_pictures == null) + { + _pictures = new LazyMultimap(keys => _funcPictures(keys).ToMultimap(x => x.Id, x => x), _pictureIds); + } + return _pictures; + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/OrderExportContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/OrderExportContext.cs new file mode 100644 index 0000000000..9ebe5c25a5 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/OrderExportContext.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SmartStore.Collections; +using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Shipping; + +namespace SmartStore.Services.DataExchange.Export.Internal +{ + internal class OrderExportContext + { + protected List _orderIds; + protected List _customerIds; + protected List _addressIds; + + private Func> _funcCustomers; + private Func> _funcRewardPointsHistories; + private Func> _funcAddresses; + private Func> _funcOrderItems; + private Func> _funcShipments; + + private LazyMultimap _customers; + private LazyMultimap _rewardPointsHistories; + private LazyMultimap
_addresses; + private LazyMultimap _orderItems; + private LazyMultimap _shipments; + + public OrderExportContext(IEnumerable orders, + Func> customers, + Func> rewardPointsHistory, + Func> addresses, + Func> orderItems, + Func> shipments) + { + if (orders == null) + { + _orderIds = new List(); + _customerIds = new List(); + _addressIds = new List(); + } + else + { + _orderIds = new List(orders.Select(x => x.Id)); + _customerIds = new List(orders.Select(x => x.CustomerId)); + + _addressIds = orders.Select(x => x.BillingAddressId) + .Union(orders.Select(x => x.ShippingAddressId ?? 0)) + .Where(x => x != 0) + .Distinct() + .ToList(); + } + + _funcCustomers = customers; + _funcRewardPointsHistories = rewardPointsHistory; + _funcAddresses = addresses; + _funcOrderItems = orderItems; + _funcShipments = shipments; + } + + public void Clear() + { + if (_customers != null) + _customers.Clear(); + if (_rewardPointsHistories != null) + _rewardPointsHistories.Clear(); + if (_addresses != null) + _addresses.Clear(); + if (_orderItems != null) + _orderItems.Clear(); + if (_shipments != null) + _shipments.Clear(); + + _orderIds.Clear(); + _customerIds.Clear(); + _addressIds.Clear(); + } + + public LazyMultimap Customers + { + get + { + if (_customers == null) + { + _customers = new LazyMultimap(keys => _funcCustomers(keys).ToMultimap(x => x.Id, x => x), _customerIds); + } + return _customers; + } + } + + public LazyMultimap RewardPointsHistories + { + get + { + if (_rewardPointsHistories == null) + { + _rewardPointsHistories = new LazyMultimap(keys => _funcRewardPointsHistories(keys), _customerIds); + } + return _rewardPointsHistories; + } + } + + public LazyMultimap
Addresses + { + get + { + if (_addresses == null) + { + _addresses = new LazyMultimap
(keys => _funcAddresses(keys).ToMultimap(x => x.Id, x => x), _addressIds); + } + return _addresses; + } + } + + public LazyMultimap OrderItems + { + get + { + if (_orderItems == null) + { + _orderItems = new LazyMultimap(keys => _funcOrderItems(keys), _orderIds); + } + return _orderItems; + } + } + + public LazyMultimap Shipments + { + get + { + if (_shipments == null) + { + _shipments = new LazyMultimap(keys => _funcShipments(keys), _orderIds); + } + return _shipments; + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ProductExportContext.cs b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ProductExportContext.cs new file mode 100644 index 0000000000..6a76e1ffef --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/Export/Internal/ProductExportContext.cs @@ -0,0 +1,148 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using SmartStore.Collections; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.Discounts; +using SmartStore.Services.Catalog; + +namespace SmartStore.Services.DataExchange.Export.Internal +{ + internal class ProductExportContext : PriceCalculationContext + { + private List _productIdsBundleItems; + + private Func> _funcProductManufacturers; + private Func> _funcProductPictures; + private Func> _funcProductTags; + private Func> _funcProductSpecificationAttributes; + private Func> _funcProductBundleItems; + + private LazyMultimap _productManufacturers; + private LazyMultimap _productPictures; + private LazyMultimap _productTags; + private LazyMultimap _productSpecificationAttributes; + private LazyMultimap _productBundleItems; + + public ProductExportContext( + IEnumerable products, + Func> attributes, + Func> attributeCombinations, + Func> tierPrices, + Func> productCategories, + Func> productManufacturers, + Func> productPictures, + Func> productTags, + Func> productAppliedDiscounts, + Func> productSpecificationAttributes, + Func> productBundleItems) + : base(products, + attributes, + attributeCombinations, + tierPrices, + productCategories, + productAppliedDiscounts) + { + if (products == null) + { + _productIdsBundleItems = new List(); + } + else + { + _productIdsBundleItems = new List(products.Where(x => x.ProductType == ProductType.BundledProduct).Select(x => x.Id)); + } + + _funcProductManufacturers = productManufacturers; + _funcProductPictures = productPictures; + _funcProductTags = productTags; + _funcProductSpecificationAttributes = productSpecificationAttributes; + _funcProductBundleItems = productBundleItems; + } + + public new void Clear() + { + if (_productManufacturers != null) + _productManufacturers.Clear(); + if (_productPictures != null) + _productPictures.Clear(); + if (_productTags != null) + _productTags.Clear(); + if (_productSpecificationAttributes != null) + _productSpecificationAttributes.Clear(); + if (_productBundleItems != null) + _productBundleItems.Clear(); + + _productIdsBundleItems.Clear(); + + base.Clear(); + } + + //public new void Collect(IEnumerable productIds) + //{ + // ProductManufacturers.Collect(productIds); + // ProductPictures.Collect(productIds); + + // base.Collect(productIds); + //} + + public LazyMultimap ProductManufacturers + { + get + { + if (_productManufacturers == null) + { + _productManufacturers = new LazyMultimap(keys => _funcProductManufacturers(keys), _productIds); + } + return _productManufacturers; + } + } + + public LazyMultimap ProductPictures + { + get + { + if (_productPictures == null) + { + _productPictures = new LazyMultimap(keys => _funcProductPictures(keys), _productIds); + } + return _productPictures; + } + } + + public LazyMultimap ProductTags + { + get + { + if (_productTags == null) + { + _productTags = new LazyMultimap(keys => _funcProductTags(keys), _productIds); + } + return _productTags; + } + } + + public LazyMultimap ProductSpecificationAttributes + { + get + { + if (_productSpecificationAttributes == null) + { + _productSpecificationAttributes = new LazyMultimap(keys => _funcProductSpecificationAttributes(keys), _productIds); + } + return _productSpecificationAttributes; + } + } + + public LazyMultimap ProductBundleItems + { + get + { + if (_productBundleItems == null) + { + _productBundleItems = new LazyMultimap(keys => _funcProductBundleItems(keys), _productIdsBundleItems); + } + return _productBundleItems; + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/DataExchange/ISyncMappingService.cs b/src/Libraries/SmartStore.Services/DataExchange/ISyncMappingService.cs new file mode 100644 index 0000000000..eb72d07e19 --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/ISyncMappingService.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SmartStore.Core; +using SmartStore.Core.Domain.DataExchange; + +namespace SmartStore.Services.DataExchange +{ + public partial interface ISyncMappingService + { + /// + /// Inserts a sync mapping entity + /// + /// Sync mapping + void InsertSyncMapping(SyncMapping mapping); + + /// + /// Inserts a range of sync mapping entities + /// + /// A sequence of sync mappings + void InsertSyncMappings(IEnumerable mappings); + + /// + /// Gets all sync mappings + /// + /// The context (external application) name. Leave null to load all records regardless of context. + /// The entity name. Leave null to load all records regardless of entity name. + /// SyncMappings + IList GetAllSyncMappings(string contextName = null, string entityName = null); + + /// + /// Gets a sync mapping record by (target) entity id, name and context name. + /// + /// Entity nd + /// Entity name + /// Context name + /// SyncMapping + SyncMapping GetSyncMappingByEntity(int entityId, string entityName, string contextName); + + /// + /// Gets a sync mapping record by (external) source key, entity name and context name. + /// + /// Source key + /// Entity name + /// Context name + /// SyncMappings + SyncMapping GetSyncMappingBySource(string sourceKey, string entityName, string contextName); + + /// + /// Deletes a sync mapping entity + /// + /// Sync mapping + void DeleteSyncMapping(SyncMapping mapping); + + /// + /// Deletes a range of sync mapping entities + /// + /// Sync mappings + void DeleteSyncMappings(IEnumerable mappings); + + /// + /// Deletes all sync mapping entities referencing the specified entity + /// + /// The entity + void DeleteSyncMappingsFor(T entity) where T : BaseEntity; + + /// + /// Deletes a range of sync mapping entities + /// + /// The context (external application) name. + /// The entity name. Leave null to delete all context specific mappings regardless of entity name. + /// SyncMappings + void DeleteSyncMappings(string contextName, string entityName = null); + + /// + /// Updates a sync mapping entity + /// + /// Sync mapping + void UpdateSyncMapping(SyncMapping mapping); + } + + + public static class ISyncMappingServiceExtensions + { + + public static SyncMapping InsertSyncMapping(this ISyncMappingService svc, T entity, string contextName, string sourceKey) where T : BaseEntity + { + Guard.ArgumentNotNull(() => entity); + Guard.ArgumentNotEmpty(() => contextName); + Guard.ArgumentNotEmpty(() => sourceKey); + + if (entity is SyncMapping) + { + throw Error.InvalidOperation("Cannot insert a sync mapping record for a SyncMapping entity"); + } + + if (entity.IsTransientRecord()) + { + throw Error.InvalidOperation("Cannot insert a sync mapping record for a transient (unsaved) entity"); + } + + var mapping = new SyncMapping + { + ContextName = contextName, + EntityId = entity.Id, + EntityName = typeof(T).Name, + SourceKey = sourceKey + }; + + return mapping; + } + + public static SyncMapping GetSyncMappingByEntity(this ISyncMappingService svc, T entity, string contextName) where T : BaseEntity + { + Guard.ArgumentNotNull(() => entity); + Guard.ArgumentNotEmpty(() => contextName); + + if (entity is SyncMapping) + { + throw Error.InvalidOperation("Cannot get a sync mapping record for a SyncMapping entity"); + } + + if (entity.IsTransientRecord()) + { + throw Error.InvalidOperation("Cannot get a sync mapping record for a transient (unsaved) entity"); + } + + return svc.GetSyncMappingByEntity(entity.Id, typeof(T).Name, contextName); + } + + /// + /// Inserts a range of sync mapping entities by specifying two equal sized sequences for entity ids and source keys. + /// + /// Context name for all mappings + /// Entity name for all mappings + /// A sequence of entity ids + /// A sequence of source keys + /// List of persisted sync mappings + /// Both sequences must contain at least one element and must be of equal size. + public static IList InsertSyncMappings(this ISyncMappingService svc, string contextName, string entityName, IEnumerable entityIds, IEnumerable sourceKeys) + { + Guard.ArgumentNotEmpty(() => contextName); + Guard.ArgumentNotEmpty(() => entityName); + Guard.ArgumentNotNull(() => entityIds); + Guard.ArgumentNotNull(() => sourceKeys); + + if (!entityIds.Any() || !sourceKeys.Any() || entityIds.Count() != sourceKeys.Count()) + { + throw Error.InvalidOperation("Both sequences must contain at least one element and must be of equal size."); + } + + var mappings = new List(); + var arrIds = entityIds.ToArray(); + var arrKeys = sourceKeys.ToArray(); + + for (int i = 0; i < arrIds.Length; i++) + { + mappings.Add(new SyncMapping + { + ContextName = contextName, + EntityName = entityName, + EntityId = arrIds[i], + SourceKey = arrKeys[i] + }); + } + + svc.InsertSyncMappings(mappings); + + return mappings; + } + + } +} 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 new file mode 100644 index 0000000000..3c3a11551c --- /dev/null +++ b/src/Libraries/SmartStore.Services/DataExchange/SyncMappingService.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using SmartStore.Core; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.DataExchange; + +namespace SmartStore.Services.DataExchange +{ + + public partial class SyncMappingService : ISyncMappingService + { + private readonly IRepository _syncMappingsRepository; + + public SyncMappingService(IRepository syncMappingsRepository) + { + this._syncMappingsRepository = syncMappingsRepository; + } + + public void InsertSyncMapping(SyncMapping mapping) + { + Guard.ArgumentNotNull(() => mapping); + + _syncMappingsRepository.Insert(mapping); + } + + public void InsertSyncMappings(IEnumerable mappings) + { + Guard.ArgumentNotNull(() => mappings); + + _syncMappingsRepository.InsertRange(mappings); + } + + public IList GetAllSyncMappings(string contextName = null, string entityName = null) + { + var query = _syncMappingsRepository.Table; + + if (entityName.HasValue()) + { + query = query.Where(x => x.EntityName == entityName); + } + + if (contextName.HasValue()) + { + query = query.Where(x => x.ContextName == contextName); + } + + return query.ToList(); + } + + public SyncMapping GetSyncMappingByEntity(int entityId, string entityName, string contextName) + { + Guard.ArgumentIsPositive(entityId, "entityId"); + Guard.ArgumentNotEmpty(() => entityName); + Guard.ArgumentNotEmpty(() => contextName); + + var query = from x in _syncMappingsRepository.Table + where + x.EntityId == entityId + && x.EntityName == entityName + && x.ContextName == contextName + select x; + + return query.FirstOrDefault(); + } + + public SyncMapping GetSyncMappingBySource(string sourceKey, string entityName, string contextName) + { + Guard.ArgumentNotEmpty(() => sourceKey); + Guard.ArgumentNotEmpty(() => entityName); + Guard.ArgumentNotEmpty(() => contextName); + + var query = from x in _syncMappingsRepository.Table + where + x.SourceKey == sourceKey + && x.EntityName == entityName + && x.ContextName == contextName + select x; + + return query.FirstOrDefault(); + } + + public void DeleteSyncMapping(SyncMapping mapping) + { + Guard.ArgumentNotNull(() => mapping); + + _syncMappingsRepository.Delete(mapping); + } + + public void DeleteSyncMappings(IEnumerable mappings) + { + Guard.ArgumentNotNull(() => mappings); + + _syncMappingsRepository.DeleteRange(mappings); + } + + public void DeleteSyncMappingsFor(T entity) where T : BaseEntity + { + Guard.ArgumentNotNull(() => entity); + + if (entity is SyncMapping) + { + throw Error.InvalidOperation("Cannot delete a sync mapping record for a SyncMapping entity"); + } + + if (entity.IsTransientRecord()) + { + return; + } + + _syncMappingsRepository.DeleteAll(x => x.EntityId == entity.Id && x.EntityName == typeof(T).Name); + } + + public void DeleteSyncMappings(string contextName, string entityName = null) + { + Guard.ArgumentNotEmpty(() => contextName); + + if (entityName.HasValue()) + { + _syncMappingsRepository.DeleteAll(x => x.ContextName == contextName && x.EntityName == entityName); + } + else + { + _syncMappingsRepository.DeleteAll(x => x.ContextName == contextName); + } + } + + + public void UpdateSyncMapping(SyncMapping mapping) + { + Guard.ArgumentNotNull(() => mapping); + + mapping.SyncedOnUtc = DateTime.UtcNow; + _syncMappingsRepository.Update(mapping); + } + + } + +} diff --git a/src/Libraries/SmartStore.Services/Directory/CurrencyService.cs b/src/Libraries/SmartStore.Services/Directory/CurrencyService.cs index 6b436fd14d..5dd60700e7 100644 --- a/src/Libraries/SmartStore.Services/Directory/CurrencyService.cs +++ b/src/Libraries/SmartStore.Services/Directory/CurrencyService.cs @@ -8,7 +8,6 @@ using SmartStore.Core.Domain.Stores; using SmartStore.Core.Events; using SmartStore.Core.Plugins; -using SmartStore.Services.Customers; using SmartStore.Services.Stores; namespace SmartStore.Services.Directory @@ -33,6 +32,7 @@ public partial class CurrencyService : ICurrencyService private readonly IPluginFinder _pluginFinder; private readonly IEventPublisher _eventPublisher; private readonly IProviderManager _providerManager; + private readonly IStoreContext _storeContext; #endregion @@ -53,7 +53,8 @@ public CurrencyService(ICacheManager cacheManager, CurrencySettings currencySettings, IPluginFinder pluginFinder, IEventPublisher eventPublisher, - IProviderManager providerManager) + IProviderManager providerManager, + IStoreContext storeContext) { this._cacheManager = cacheManager; this._currencyRepository = currencyRepository; @@ -62,6 +63,7 @@ public CurrencyService(ICacheManager cacheManager, this._pluginFinder = pluginFinder; this._eventPublisher = eventPublisher; this._providerManager = providerManager; + this._storeContext = storeContext; } #endregion @@ -189,7 +191,6 @@ public virtual void UpdateCurrency(Currency currency) } - /// /// Converts currency /// @@ -209,16 +210,17 @@ public virtual decimal ConvertCurrency(decimal amount, decimal exchangeRate) /// Amount /// Source currency code /// Target currency code + /// Store to get the primary currencies from /// Converted value - public virtual decimal ConvertCurrency(decimal amount, Currency sourceCurrencyCode, Currency targetCurrencyCode) + public virtual decimal ConvertCurrency(decimal amount, Currency sourceCurrencyCode, Currency targetCurrencyCode, Store store = null) { decimal result = amount; if (sourceCurrencyCode.Id == targetCurrencyCode.Id) return result; if (result != decimal.Zero && sourceCurrencyCode.Id != targetCurrencyCode.Id) { - result = ConvertToPrimaryExchangeRateCurrency(result, sourceCurrencyCode); - result = ConvertFromPrimaryExchangeRateCurrency(result, targetCurrencyCode); + result = ConvertToPrimaryExchangeRateCurrency(result, sourceCurrencyCode, store); + result = ConvertFromPrimaryExchangeRateCurrency(result, targetCurrencyCode, store); } return result; } @@ -228,11 +230,13 @@ public virtual decimal ConvertCurrency(decimal amount, Currency sourceCurrencyCo /// /// Amount /// Source currency code + /// Store to get the primary exchange rate currency from /// Converted value - public virtual decimal ConvertToPrimaryExchangeRateCurrency(decimal amount, Currency sourceCurrencyCode) + public virtual decimal ConvertToPrimaryExchangeRateCurrency(decimal amount, Currency sourceCurrencyCode, Store store = null) { decimal result = amount; - var primaryExchangeRateCurrency = GetCurrencyById(_currencySettings.PrimaryExchangeRateCurrencyId); + var primaryExchangeRateCurrency = (store == null ? _storeContext.CurrentStore.PrimaryExchangeRateCurrency : store.PrimaryExchangeRateCurrency); + if (result != decimal.Zero && sourceCurrencyCode.Id != primaryExchangeRateCurrency.Id) { decimal exchangeRate = sourceCurrencyCode.Rate; @@ -248,11 +252,13 @@ public virtual decimal ConvertToPrimaryExchangeRateCurrency(decimal amount, Curr /// /// Amount /// Target currency code + /// Store to get the primary exchange rate currency from /// Converted value - public virtual decimal ConvertFromPrimaryExchangeRateCurrency(decimal amount, Currency targetCurrencyCode) + public virtual decimal ConvertFromPrimaryExchangeRateCurrency(decimal amount, Currency targetCurrencyCode, Store store = null) { decimal result = amount; - var primaryExchangeRateCurrency = GetCurrencyById(_currencySettings.PrimaryExchangeRateCurrencyId); + var primaryExchangeRateCurrency = (store == null ? _storeContext.CurrentStore.PrimaryExchangeRateCurrency : store.PrimaryExchangeRateCurrency); + if (result != decimal.Zero && targetCurrencyCode.Id != primaryExchangeRateCurrency.Id) { decimal exchangeRate = targetCurrencyCode.Rate; @@ -268,11 +274,13 @@ public virtual decimal ConvertFromPrimaryExchangeRateCurrency(decimal amount, Cu /// /// Amount /// Source currency code + /// Store to get the primary store currency from /// Converted value - public virtual decimal ConvertToPrimaryStoreCurrency(decimal amount, Currency sourceCurrencyCode) + public virtual decimal ConvertToPrimaryStoreCurrency(decimal amount, Currency sourceCurrencyCode, Store store = null) { decimal result = amount; - var primaryStoreCurrency = GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId); + var primaryStoreCurrency = (store == null ? _storeContext.CurrentStore.PrimaryStoreCurrency : store.PrimaryStoreCurrency); + if (result != decimal.Zero && sourceCurrencyCode.Id != primaryStoreCurrency.Id) { decimal exchangeRate = sourceCurrencyCode.Rate; @@ -289,11 +297,11 @@ public virtual decimal ConvertToPrimaryStoreCurrency(decimal amount, Currency so /// Amount /// Target currency code /// Converted value - public virtual decimal ConvertFromPrimaryStoreCurrency(decimal amount, Currency targetCurrencyCode) + public virtual decimal ConvertFromPrimaryStoreCurrency(decimal amount, Currency targetCurrencyCode, Store store = null) { decimal result = amount; - var primaryStoreCurrency = GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId); - result = ConvertCurrency(amount, primaryStoreCurrency, targetCurrencyCode); + var primaryStoreCurrency = (store == null ? _storeContext.CurrentStore.PrimaryStoreCurrency : store.PrimaryStoreCurrency); + result = ConvertCurrency(amount, primaryStoreCurrency, targetCurrencyCode, store); return result; } diff --git a/src/Libraries/SmartStore.Services/Directory/ICurrencyService.cs b/src/Libraries/SmartStore.Services/Directory/ICurrencyService.cs index b1481d03f3..65f633fed4 100644 --- a/src/Libraries/SmartStore.Services/Directory/ICurrencyService.cs +++ b/src/Libraries/SmartStore.Services/Directory/ICurrencyService.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using SmartStore.Core.Domain.Directory; +using SmartStore.Core.Domain.Stores; using SmartStore.Core.Plugins; namespace SmartStore.Services.Directory @@ -72,40 +73,45 @@ public partial interface ICurrencyService /// Amount /// Source currency code /// Target currency code + /// Store to get the primary currencies from /// Converted value - decimal ConvertCurrency(decimal amount, Currency sourceCurrencyCode, Currency targetCurrencyCode); + decimal ConvertCurrency(decimal amount, Currency sourceCurrencyCode, Currency targetCurrencyCode, Store store = null); /// /// Converts to primary exchange rate currency /// /// Amount /// Source currency code + /// Store to get the primary exchange rate currency from /// Converted value - decimal ConvertToPrimaryExchangeRateCurrency(decimal amount, Currency sourceCurrencyCode); + decimal ConvertToPrimaryExchangeRateCurrency(decimal amount, Currency sourceCurrencyCode, Store store = null); /// /// Converts from primary exchange rate currency /// /// Amount /// Target currency code + /// Store to get the primary exchange rate currency from /// Converted value - decimal ConvertFromPrimaryExchangeRateCurrency(decimal amount, Currency targetCurrencyCode); + decimal ConvertFromPrimaryExchangeRateCurrency(decimal amount, Currency targetCurrencyCode, Store store = null); /// /// Converts to primary store currency /// /// Amount /// Source currency code + /// Store to get the primary store currency from /// Converted value - decimal ConvertToPrimaryStoreCurrency(decimal amount, Currency sourceCurrencyCode); + decimal ConvertToPrimaryStoreCurrency(decimal amount, Currency sourceCurrencyCode, Store store = null); /// /// Converts from primary store currency /// /// Amount /// Target currency code + /// Store to get the primary store currency from /// Converted value - decimal ConvertFromPrimaryStoreCurrency(decimal amount, Currency targetCurrencyCode); + decimal ConvertFromPrimaryStoreCurrency(decimal amount, Currency targetCurrencyCode, Store store = null); 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/Directory/UpdateExchangeRateTask.cs b/src/Libraries/SmartStore.Services/Directory/UpdateExchangeRateTask.cs index a3d52f317c..0882af90c5 100644 --- a/src/Libraries/SmartStore.Services/Directory/UpdateExchangeRateTask.cs +++ b/src/Libraries/SmartStore.Services/Directory/UpdateExchangeRateTask.cs @@ -1,6 +1,5 @@ using System; using SmartStore.Core.Domain.Directory; -using SmartStore.Services.Configuration; using SmartStore.Services.Tasks; namespace SmartStore.Services.Directory @@ -11,15 +10,17 @@ namespace SmartStore.Services.Directory public partial class UpdateExchangeRateTask : ITask { private readonly ICurrencyService _currencyService; - private readonly ISettingService _settingService; private readonly CurrencySettings _currencySettings; + private readonly ICommonServices _services; - public UpdateExchangeRateTask(ICurrencyService currencyService, - ISettingService settingService, CurrencySettings currencySettings) + public UpdateExchangeRateTask( + ICurrencyService currencyService, + CurrencySettings currencySettings, + ICommonServices services) { this._currencyService = currencyService; - this._settingService = settingService; this._currencySettings = currencySettings; + this._services = services; } /// @@ -33,10 +34,11 @@ public void Execute(TaskExecutionContext ctx) long lastUpdateTimeTicks = _currencySettings.LastUpdateTime; DateTime lastUpdateTime = DateTime.FromBinary(lastUpdateTimeTicks); lastUpdateTime = DateTime.SpecifyKind(lastUpdateTime, DateTimeKind.Utc); + if (lastUpdateTime.AddHours(1) < DateTime.UtcNow) { //update rates each one hour - var exchangeRates = _currencyService.GetCurrencyLiveRates(_currencyService.GetCurrencyById(_currencySettings.PrimaryExchangeRateCurrencyId).CurrencyCode); + var exchangeRates = _currencyService.GetCurrencyLiveRates(_services.StoreContext.CurrentStore.PrimaryExchangeRateCurrency.CurrencyCode); foreach (var exchageRate in exchangeRates) { @@ -51,7 +53,7 @@ public void Execute(TaskExecutionContext ctx) //save new update time value _currencySettings.LastUpdateTime = DateTime.UtcNow.ToBinary(); - _settingService.SaveSetting(_currencySettings); + _services.Settings.SaveSetting(_currencySettings); } } } diff --git a/src/Libraries/SmartStore.Services/Discounts/DiscountExtentions.cs b/src/Libraries/SmartStore.Services/Discounts/DiscountExtentions.cs index d8c29939db..ccc62d5921 100644 --- a/src/Libraries/SmartStore.Services/Discounts/DiscountExtentions.cs +++ b/src/Libraries/SmartStore.Services/Discounts/DiscountExtentions.cs @@ -29,7 +29,6 @@ public static decimal GetDiscountAmount(this Discount discount, decimal amount) return result; } - public static Discount GetPreferredDiscount(this IList discounts, decimal amount) { @@ -47,20 +46,5 @@ public static Discount GetPreferredDiscount(this IList discounts, deci return preferredDiscount; } - - public static bool ContainsDiscount(this IList discounts, Discount discount) - { - if (discounts == null) - throw new ArgumentNullException("discounts"); - - if (discount == null) - throw new ArgumentNullException("discount"); - - foreach (var dis1 in discounts) - if (discount.Id == dis1.Id) - return true; - - return false; - } } } 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/ExportManager.cs b/src/Libraries/SmartStore.Services/ExportImport/ExportManager.cs deleted file mode 100644 index 21cb3055a1..0000000000 --- a/src/Libraries/SmartStore.Services/ExportImport/ExportManager.cs +++ /dev/null @@ -1,2097 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Xml; -using OfficeOpenXml; -using OfficeOpenXml.Style; -using SmartStore.Core; -using SmartStore.Core.Domain.Catalog; -using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Directory; -using SmartStore.Core.Domain.Localization; -using SmartStore.Core.Domain.Media; -using SmartStore.Core.Domain.Orders; -using SmartStore.Core.Domain.Stores; -using SmartStore.Core.Logging; -using SmartStore.Services.Catalog; -using SmartStore.Services.Common; -using SmartStore.Services.Customers; -using SmartStore.Services.Localization; -using SmartStore.Services.Media; -using SmartStore.Services.Messages; -using SmartStore.Services.Seo; -using SmartStore.Services.Stores; - -namespace SmartStore.Services.ExportImport -{ - /// - /// Export manager - /// - public partial class ExportManager : IExportManager - { - #region Fields - - private readonly ICategoryService _categoryService; - private readonly IManufacturerService _manufacturerService; - private readonly IProductService _productService; - private readonly IProductTemplateService _productTemplateService; - private readonly IPictureService _pictureService; - private readonly INewsLetterSubscriptionService _newsLetterSubscriptionService; - private readonly ILanguageService _languageService; - private readonly MediaSettings _mediaSettings; - private readonly ICommonServices _commonServices; - private readonly IStoreMappingService _storeMappingService; - - #endregion - - #region Ctor - - public ExportManager(ICategoryService categoryService, - IManufacturerService manufacturerService, - IProductService productService, - IProductTemplateService productTemplateService, - IPictureService pictureService, - INewsLetterSubscriptionService newsLetterSubscriptionService, - ILanguageService languageService, - MediaSettings mediaSettings, - ICommonServices commonServices, - IStoreMappingService storeMappingService) - { - this._categoryService = categoryService; - this._manufacturerService = manufacturerService; - this._productService = productService; - this._productTemplateService = productTemplateService; - this._pictureService = pictureService; - this._newsLetterSubscriptionService = newsLetterSubscriptionService; - this._languageService = languageService; - this._mediaSettings = mediaSettings; - this._commonServices = commonServices; - this._storeMappingService = storeMappingService; - - Logger = NullLogger.Instance; - } - - public ILogger Logger { get; set; } - - #endregion - - #region Utilities - - protected Action> WriteLocalized = (writer, context, content) => - { - if (context.Languages.Count > 1) - { - writer.WriteStartElement("Localized"); - foreach (var language in context.Languages) - { - content(language); - } - writer.WriteEndElement(); - } - }; - - protected virtual void WriteCategories(XmlWriter writer, int parentCategoryId) - { - var categories = _categoryService.GetAllCategoriesByParentCategoryId(parentCategoryId, true); - if (categories != null && categories.Count > 0) - { - foreach (var category in categories) - { - writer.WriteStartElement("Category"); - writer.Write("Id", category.Id.ToString()); - writer.Write("Name", category.Name); - writer.Write("FullName", category.FullName); - writer.Write("Description", category.Description); - writer.Write("BottomDescription", category.BottomDescription); - writer.Write("CategoryTemplateId", category.CategoryTemplateId.ToString()); - writer.Write("MetaKeywords", category.MetaKeywords); - writer.Write("MetaDescription", category.MetaDescription); - writer.Write("MetaTitle", category.MetaTitle); - writer.Write("SeName", category.GetSeName(0, true, false)); - writer.Write("ParentCategoryId", category.ParentCategoryId.ToString()); - writer.Write("PageSize", category.PageSize.ToString()); - writer.Write("AllowCustomersToSelectPageSize", category.AllowCustomersToSelectPageSize.ToString()); - writer.Write("PageSizeOptions", category.PageSizeOptions); - writer.Write("PriceRanges", category.PriceRanges); - writer.Write("ShowOnHomePage", category.ShowOnHomePage.ToString()); - writer.Write("HasDiscountsApplied", category.HasDiscountsApplied.ToString()); - writer.Write("Published", category.Published.ToString()); - writer.Write("Deleted", category.Deleted.ToString()); - writer.Write("DisplayOrder", category.DisplayOrder.ToString()); - writer.Write("CreatedOnUtc", category.CreatedOnUtc.ToString()); - writer.Write("UpdatedOnUtc", category.UpdatedOnUtc.ToString()); - writer.Write("SubjectToAcl", category.SubjectToAcl.ToString()); - writer.Write("LimitedToStores", category.LimitedToStores.ToString()); - writer.Write("Alias", category.Alias); - writer.Write("DefaultViewMode", category.DefaultViewMode); - - writer.WriteStartElement("Products"); - var productCategories = _categoryService.GetProductCategoriesByCategoryId(category.Id, 0, int.MaxValue, true); - foreach (var productCategory in productCategories) - { - var product = productCategory.Product; - if (product != null && !product.Deleted) - { - writer.WriteStartElement("ProductCategory"); - writer.Write("ProductCategoryId", productCategory.Id.ToString()); - writer.Write("ProductId", productCategory.ProductId.ToString()); - writer.Write("IsFeaturedProduct", productCategory.IsFeaturedProduct.ToString()); - writer.Write("DisplayOrder", productCategory.DisplayOrder.ToString()); - writer.WriteEndElement(); - } - } - writer.WriteEndElement(); - - writer.WriteStartElement("SubCategories"); - WriteCategories(writer, category.Id); - writer.WriteEndElement(); - writer.WriteEndElement(); - } - } - } - - protected virtual void WritePicture(XmlWriter writer, XmlExportContext context, Picture picture, int thumbSize, int defaultSize) - { - if (picture != null) - { - writer.WriteStartElement("Picture"); - writer.Write("Id", picture.Id.ToString()); - writer.Write("SeoFileName", picture.SeoFilename); - writer.Write("MimeType", picture.MimeType); - writer.Write("ThumbImageUrl", _pictureService.GetPictureUrl(picture, thumbSize, false, context.Store.Url)); - writer.Write("ImageUrl", _pictureService.GetPictureUrl(picture, defaultSize, false, context.Store.Url)); - writer.Write("FullSizeImageUrl", _pictureService.GetPictureUrl(picture, 0, false, context.Store.Url)); - writer.WriteEndElement(); - } - } - - protected virtual void WriteQuantityUnit(XmlWriter writer, XmlExportContext context, QuantityUnit quantityUnit) - { - if (quantityUnit != null) - { - writer.WriteStartElement("QuantityUnit"); - writer.Write("Id", quantityUnit.Id.ToString()); - writer.Write("Name", quantityUnit.Name); - writer.Write("Description", quantityUnit.Description); - writer.Write("DisplayLocale", quantityUnit.DisplayLocale); - writer.Write("DisplayOrder", quantityUnit.DisplayOrder.ToString()); - writer.Write("IsDefault", quantityUnit.IsDefault.ToString()); - WriteLocalized(writer, context, lang => - { - writer.Write("Name", quantityUnit.GetLocalized(x => x.Name, lang.Id, false, false), lang); - writer.Write("Description", quantityUnit.GetLocalized(x => x.Description, lang.Id, false, false), lang); - }); - writer.WriteEndElement(); - } - } - - #endregion - - #region Methods - - /// - /// Export manufacturer list to xml - /// - /// Manufacturers - /// Result in XML format - public virtual string ExportManufacturersToXml(IList manufacturers) - { - var sb = new StringBuilder(); - var stringWriter = new StringWriter(sb); - var xmlWriter = new XmlTextWriter(stringWriter); - xmlWriter.WriteStartDocument(); - xmlWriter.WriteStartElement("Manufacturers"); - xmlWriter.WriteAttributeString("Version", SmartStoreVersion.CurrentVersion); - - foreach (var manufacturer in manufacturers) - { - xmlWriter.WriteStartElement("Manufacturer"); - - xmlWriter.WriteElementString("Id", null, manufacturer.Id.ToString()); - xmlWriter.WriteElementString("Name", null, manufacturer.Name); - xmlWriter.WriteElementString("SeName", null, manufacturer.GetSeName(0, true, false)); - xmlWriter.WriteElementString("Description", null, manufacturer.Description); - xmlWriter.WriteElementString("ManufacturerTemplateId", null, manufacturer.ManufacturerTemplateId.ToString()); - xmlWriter.WriteElementString("MetaKeywords", null, manufacturer.MetaKeywords); - xmlWriter.WriteElementString("MetaDescription", null, manufacturer.MetaDescription); - xmlWriter.WriteElementString("MetaTitle", null, manufacturer.MetaTitle); - xmlWriter.WriteElementString("PictureId", null, manufacturer.PictureId.ToString()); - xmlWriter.WriteElementString("PageSize", null, manufacturer.PageSize.ToString()); - xmlWriter.WriteElementString("AllowCustomersToSelectPageSize", null, manufacturer.AllowCustomersToSelectPageSize.ToString()); - xmlWriter.WriteElementString("PageSizeOptions", null, manufacturer.PageSizeOptions); - xmlWriter.WriteElementString("PriceRanges", null, manufacturer.PriceRanges); - xmlWriter.WriteElementString("Published", null, manufacturer.Published.ToString()); - xmlWriter.WriteElementString("Deleted", null, manufacturer.Deleted.ToString()); - xmlWriter.WriteElementString("DisplayOrder", null, manufacturer.DisplayOrder.ToString()); - xmlWriter.WriteElementString("CreatedOnUtc", null, manufacturer.CreatedOnUtc.ToString()); - xmlWriter.WriteElementString("UpdatedOnUtc", null, manufacturer.UpdatedOnUtc.ToString()); - - xmlWriter.WriteStartElement("Products"); - var productManufacturers = _manufacturerService.GetProductManufacturersByManufacturerId(manufacturer.Id, 0, int.MaxValue, true); - if (productManufacturers != null) - { - foreach (var productManufacturer in productManufacturers) - { - var product = productManufacturer.Product; - if (product != null && !product.Deleted) - { - xmlWriter.WriteStartElement("ProductManufacturer"); - xmlWriter.WriteElementString("Id", null, productManufacturer.Id.ToString()); - xmlWriter.WriteElementString("ProductId", null, productManufacturer.ProductId.ToString()); - xmlWriter.WriteElementString("IsFeaturedProduct", null, productManufacturer.IsFeaturedProduct.ToString()); - xmlWriter.WriteElementString("DisplayOrder", null, productManufacturer.DisplayOrder.ToString()); - xmlWriter.WriteEndElement(); - } - } - } - xmlWriter.WriteEndElement(); - - xmlWriter.WriteEndElement(); - } - - xmlWriter.WriteEndElement(); - xmlWriter.WriteEndDocument(); - xmlWriter.Close(); - return stringWriter.ToString(); - } - - /// - /// Export category list to xml - /// - /// Result in XML format - public virtual string ExportCategoriesToXml() - { - var sb = new StringBuilder(); - var stringWriter = new StringWriter(sb); - var xmlWriter = new XmlTextWriter(stringWriter); - xmlWriter.WriteStartDocument(); - xmlWriter.WriteStartElement("Categories"); - xmlWriter.WriteAttributeString("Version", SmartStoreVersion.CurrentVersion); - WriteCategories(xmlWriter, 0); - xmlWriter.WriteEndElement(); - xmlWriter.WriteEndDocument(); - xmlWriter.Close(); - return stringWriter.ToString(); - } - - /// - /// Writes a single product - /// - /// The XML writer - /// The product - /// Context objects - public virtual void WriteProductToXml(XmlWriter writer, Product product, XmlExportContext context) - { - var culture = CultureInfo.InvariantCulture; - var productTemplate = context.ProductTemplates.FirstOrDefault(x => x.Id == product.ProductTemplateId); - - writer.Write("Id", product.Id.ToString()); - writer.Write("Name", product.Name); - writer.Write("SeName", product.GetSeName(0, true, false)); - - writer.Write("ShortDescription", product.ShortDescription, null, true); - writer.Write("FullDescription", product.FullDescription, null, true); - - writer.Write("AdminComment", product.AdminComment); - writer.Write("ProductTemplateId", product.ProductTemplateId.ToString()); - writer.Write("ProductTemplateViewPath", productTemplate == null ? "" : productTemplate.ViewPath); - writer.Write("ShowOnHomePage", product.ShowOnHomePage.ToString()); - writer.Write("MetaKeywords", product.MetaKeywords); - writer.Write("MetaDescription", product.MetaDescription); - writer.Write("MetaTitle", product.MetaTitle); - writer.Write("AllowCustomerReviews", product.AllowCustomerReviews.ToString()); - writer.Write("ApprovedRatingSum", product.ApprovedRatingSum.ToString()); - writer.Write("NotApprovedRatingSum", product.NotApprovedRatingSum.ToString()); - writer.Write("ApprovedTotalReviews", product.ApprovedTotalReviews.ToString()); - writer.Write("NotApprovedTotalReviews", product.NotApprovedTotalReviews.ToString()); - writer.Write("Published", product.Published.ToString()); - writer.Write("CreatedOnUtc", product.CreatedOnUtc.ToString(culture)); - writer.Write("UpdatedOnUtc", product.UpdatedOnUtc.ToString(culture)); - writer.Write("SubjectToAcl", product.SubjectToAcl.ToString()); - writer.Write("LimitedToStores", product.LimitedToStores.ToString()); - writer.Write("ProductTypeId", product.ProductTypeId.ToString()); - writer.Write("ParentGroupedProductId", product.ParentGroupedProductId.ToString()); - writer.Write("Sku", product.Sku); - writer.Write("ManufacturerPartNumber", product.ManufacturerPartNumber); - writer.Write("Gtin", product.Gtin); - writer.Write("IsGiftCard", product.IsGiftCard.ToString()); - writer.Write("GiftCardTypeId", product.GiftCardTypeId.ToString()); - writer.Write("RequireOtherProducts", product.RequireOtherProducts.ToString()); - writer.Write("RequiredProductIds", product.RequiredProductIds); - writer.Write("AutomaticallyAddRequiredProducts", product.AutomaticallyAddRequiredProducts.ToString()); - writer.Write("IsDownload", product.IsDownload.ToString()); - writer.Write("DownloadId", product.DownloadId.ToString()); - writer.Write("UnlimitedDownloads", product.UnlimitedDownloads.ToString()); - writer.Write("MaxNumberOfDownloads", product.MaxNumberOfDownloads.ToString()); - writer.Write("DownloadExpirationDays", product.DownloadExpirationDays.HasValue ? product.DownloadExpirationDays.ToString() : ""); - writer.Write("DownloadActivationType", product.DownloadActivationType.ToString()); - writer.Write("HasSampleDownload", product.HasSampleDownload.ToString()); - writer.Write("SampleDownloadId", product.SampleDownloadId.ToString()); - writer.Write("HasUserAgreement", product.HasUserAgreement.ToString()); - writer.Write("UserAgreementText", product.UserAgreementText); - writer.Write("IsRecurring", product.IsRecurring.ToString()); - writer.Write("RecurringCycleLength", product.RecurringCycleLength.ToString()); - writer.Write("RecurringCyclePeriodId", product.RecurringCyclePeriodId.ToString()); - writer.Write("RecurringTotalCycles", product.RecurringTotalCycles.ToString()); - writer.Write("IsShipEnabled", product.IsShipEnabled.ToString()); - writer.Write("IsFreeShipping", product.IsFreeShipping.ToString()); - writer.Write("AdditionalShippingCharge", product.AdditionalShippingCharge.ToString(culture)); - writer.Write("IsTaxExempt", product.IsTaxExempt.ToString()); - writer.Write("TaxCategoryId", product.TaxCategoryId.ToString()); - writer.Write("ManageInventoryMethodId", product.ManageInventoryMethodId.ToString()); - writer.Write("StockQuantity", product.StockQuantity.ToString()); - writer.Write("DisplayStockAvailability", product.DisplayStockAvailability.ToString()); - writer.Write("DisplayStockQuantity", product.DisplayStockQuantity.ToString()); - writer.Write("MinStockQuantity", product.MinStockQuantity.ToString()); - writer.Write("LowStockActivityId", product.LowStockActivityId.ToString()); - writer.Write("NotifyAdminForQuantityBelow", product.NotifyAdminForQuantityBelow.ToString()); - writer.Write("BackorderModeId", product.BackorderModeId.ToString()); - writer.Write("AllowBackInStockSubscriptions", product.AllowBackInStockSubscriptions.ToString()); - writer.Write("OrderMinimumQuantity", product.OrderMinimumQuantity.ToString()); - writer.Write("OrderMaximumQuantity", product.OrderMaximumQuantity.ToString()); - writer.Write("AllowedQuantities", product.AllowedQuantities); - writer.Write("DisableBuyButton", product.DisableBuyButton.ToString()); - writer.Write("DisableWishlistButton", product.DisableWishlistButton.ToString()); - writer.Write("AvailableForPreOrder", product.AvailableForPreOrder.ToString()); - writer.Write("CallForPrice", product.CallForPrice.ToString()); - writer.Write("Price", product.Price.ToString(culture)); - writer.Write("OldPrice", product.OldPrice.ToString(culture)); - writer.Write("ProductCost", product.ProductCost.ToString(culture)); - writer.Write("SpecialPrice", product.SpecialPrice.HasValue ? product.SpecialPrice.Value.ToString(culture) : ""); - writer.Write("SpecialPriceStartDateTimeUtc", product.SpecialPriceStartDateTimeUtc.HasValue ? product.SpecialPriceStartDateTimeUtc.Value.ToString(culture) : ""); - writer.Write("SpecialPriceEndDateTimeUtc", product.SpecialPriceEndDateTimeUtc.HasValue ? product.SpecialPriceEndDateTimeUtc.Value.ToString(culture) : ""); - writer.Write("CustomerEntersPrice", product.CustomerEntersPrice.ToString()); - writer.Write("MinimumCustomerEnteredPrice", product.MinimumCustomerEnteredPrice.ToString(culture)); - writer.Write("MaximumCustomerEnteredPrice", product.MaximumCustomerEnteredPrice.ToString(culture)); - writer.Write("HasTierPrices", product.HasTierPrices.ToString()); - writer.Write("HasDiscountsApplied", product.HasDiscountsApplied.ToString()); - writer.Write("Weight", product.Weight.ToString(culture)); - writer.Write("Length", product.Length.ToString(culture)); - writer.Write("Width", product.Width.ToString(culture)); - writer.Write("Height", product.Height.ToString(culture)); - writer.Write("AvailableStartDateTimeUtc", product.AvailableStartDateTimeUtc.HasValue ? product.AvailableStartDateTimeUtc.Value.ToString(culture) : ""); - writer.Write("AvailableEndDateTimeUtc", product.AvailableEndDateTimeUtc.HasValue ? product.AvailableEndDateTimeUtc.Value.ToString(culture) : ""); - writer.Write("BasePriceEnabled", product.BasePriceEnabled.ToString()); - writer.Write("BasePriceMeasureUnit", product.BasePriceMeasureUnit); - writer.Write("BasePriceAmount", product.BasePriceAmount.HasValue ? product.BasePriceAmount.Value.ToString(culture) : ""); - writer.Write("BasePriceBaseAmount", product.BasePriceBaseAmount.HasValue ? product.BasePriceBaseAmount.Value.ToString() : ""); - writer.Write("VisibleIndividually", product.VisibleIndividually.ToString()); - writer.Write("DisplayOrder", product.DisplayOrder.ToString()); - writer.Write("BundleTitleText", product.BundleTitleText); - writer.Write("BundlePerItemPricing", product.BundlePerItemPricing.ToString()); - writer.Write("BundlePerItemShipping", product.BundlePerItemShipping.ToString()); - writer.Write("BundlePerItemShoppingCart", product.BundlePerItemShoppingCart.ToString()); - writer.Write("LowestAttributeCombinationPrice", product.LowestAttributeCombinationPrice.HasValue ? product.LowestAttributeCombinationPrice.Value.ToString(culture) : ""); - writer.Write("IsEsd", product.IsEsd.ToString()); - - WriteLocalized(writer, context, lang => - { - writer.Write("Name", product.GetLocalized(x => x.Name, lang.Id, false, false), lang); - writer.Write("SeName", product.GetSeName(lang.Id, false, false), lang); - writer.Write("ShortDescription", product.GetLocalized(x => x.ShortDescription, lang.Id, false, false), lang, true); - writer.Write("FullDescription", product.GetLocalized(x => x.FullDescription, lang.Id, false, false), lang, true); - writer.Write("MetaKeywords", product.GetLocalized(x => x.MetaKeywords, lang.Id, false, false), lang); - writer.Write("MetaDescription", product.GetLocalized(x => x.MetaDescription, lang.Id, false, false), lang); - writer.Write("MetaTitle", product.GetLocalized(x => x.MetaTitle, lang.Id, false, false), lang); - writer.Write("BundleTitleText", product.GetLocalized(x => x.BundleTitleText, lang.Id, false, false), lang); - }); - - if (product.DeliveryTime != null) - { - writer.WriteStartElement("DeliveryTime"); - writer.Write("Id", product.DeliveryTime.Id.ToString()); - writer.Write("Name", product.DeliveryTime.Name); - writer.Write("DisplayLocale", product.DeliveryTime.DisplayLocale); - writer.Write("ColorHexValue", product.DeliveryTime.ColorHexValue); - writer.Write("DisplayOrder", product.DeliveryTime.DisplayOrder.ToString()); - WriteLocalized(writer, context, lang => - { - writer.Write("Name", product.DeliveryTime.GetLocalized(x => x.Name, lang.Id, false, false), lang); - }); - writer.WriteEndElement(); - } - - WriteQuantityUnit(writer, context, product.QuantityUnit); - - writer.WriteStartElement("ProductTags"); - foreach (var tag in product.ProductTags) - { - writer.WriteStartElement("ProductTag"); - writer.Write("Id", tag.Id.ToString()); - writer.Write("Name", tag.Name); - - WriteLocalized(writer, context, lang => - { - writer.Write("Name", tag.GetLocalized(x => x.Name, lang.Id, false, false), lang); - }); - - writer.WriteEndElement(); - } - writer.WriteEndElement(); - - writer.WriteStartElement("ProductDiscounts"); - foreach (var discount in product.AppliedDiscounts) - { - writer.WriteStartElement("ProductDiscount"); - writer.Write("DiscountId", discount.Id.ToString()); - writer.WriteEndElement(); - } - writer.WriteEndElement(); - - writer.WriteStartElement("TierPrices"); - foreach (var tierPrice in product.TierPrices) - { - writer.WriteStartElement("TierPrice"); - writer.Write("Id", tierPrice.Id.ToString()); - writer.Write("StoreId", tierPrice.StoreId.ToString()); - writer.Write("CustomerRoleId", tierPrice.CustomerRoleId.HasValue ? tierPrice.CustomerRoleId.ToString() : "0"); - writer.Write("Quantity", tierPrice.Quantity.ToString()); - writer.Write("Price", tierPrice.Price.ToString(culture)); - writer.WriteEndElement(); - } - writer.WriteEndElement(); - - writer.WriteStartElement("ProductAttributes"); - foreach (var pva in product.ProductVariantAttributes.OrderBy(x => x.DisplayOrder)) - { - writer.WriteStartElement("ProductAttribute"); - - writer.Write("Id", pva.Id.ToString()); - writer.Write("TextPrompt", pva.TextPrompt); - writer.Write("IsRequired", pva.IsRequired.ToString()); - writer.Write("AttributeControlTypeId", pva.AttributeControlTypeId.ToString()); - writer.Write("DisplayOrder", pva.DisplayOrder.ToString()); - - writer.WriteStartElement("Attribute"); - writer.Write("Id", pva.ProductAttribute.Id.ToString()); - writer.Write("Alias", pva.ProductAttribute.Alias); - writer.Write("Name", pva.ProductAttribute.Name); - writer.Write("Description", pva.ProductAttribute.Description); - WriteLocalized(writer, context, lang => - { - writer.Write("Name", pva.ProductAttribute.GetLocalized(x => x.Name, lang.Id, false, false), lang); - writer.Write("Description", pva.ProductAttribute.GetLocalized(x => x.Description, lang.Id, false, false), lang); - }); - writer.WriteEndElement(); // Attribute - - writer.WriteStartElement("AttributeValues"); - foreach (var value in pva.ProductVariantAttributeValues.OrderBy(x => x.DisplayOrder)) - { - writer.WriteStartElement("AttributeValue"); - writer.Write("Id", value.Id.ToString()); - writer.Write("Alias", value.Alias); - writer.Write("Name", value.Name); - writer.Write("ColorSquaresRgb", value.ColorSquaresRgb); - writer.Write("PriceAdjustment", value.PriceAdjustment.ToString(culture)); - writer.Write("WeightAdjustment", value.WeightAdjustment.ToString(culture)); - writer.Write("IsPreSelected", value.IsPreSelected.ToString()); - writer.Write("DisplayOrder", value.DisplayOrder.ToString()); - writer.Write("ValueTypeId", value.ValueTypeId.ToString()); - writer.Write("LinkedProductId", value.LinkedProductId.ToString()); - writer.Write("Quantity", value.Quantity.ToString()); - WriteLocalized(writer, context, lang => - { - writer.Write("Name", value.GetLocalized(x => x.Name, lang.Id, false, false), lang); - }); - writer.WriteEndElement(); // AttributeValue - } - writer.WriteEndElement(); // AttributeValues - - writer.WriteEndElement(); // ProductAttribute - } - writer.WriteEndElement(); // ProductAttributes - - writer.WriteStartElement("ProductAttributeCombinations"); - foreach (var combination in product.ProductVariantAttributeCombinations) - { - writer.WriteStartElement("ProductAttributeCombination"); - - writer.Write("Id", combination.Id.ToString()); - writer.Write("StockQuantity", combination.StockQuantity.ToString()); - writer.Write("AllowOutOfStockOrders", combination.AllowOutOfStockOrders.ToString()); - writer.Write("AttributesXml", combination.AttributesXml, null, true); - writer.Write("Sku", combination.Sku); - writer.Write("Gtin", combination.Gtin); - writer.Write("ManufacturerPartNumber", combination.ManufacturerPartNumber); - writer.Write("Price", combination.Price.HasValue ? combination.Price.Value.ToString(culture) : ""); - writer.Write("Length", combination.Length.HasValue ? combination.Length.Value.ToString(culture) : ""); - writer.Write("Width", combination.Width.HasValue ? combination.Width.Value.ToString(culture) : ""); - writer.Write("Height", combination.Height.HasValue ? combination.Height.Value.ToString(culture) : ""); - writer.Write("BasePriceAmount", combination.BasePriceAmount.HasValue ? combination.BasePriceAmount.Value.ToString(culture) : ""); - writer.Write("BasePriceBaseAmount", combination.BasePriceBaseAmount.HasValue ? combination.BasePriceBaseAmount.Value.ToString() : ""); - writer.Write("DeliveryTimeId", combination.DeliveryTimeId.HasValue ? combination.DeliveryTimeId.Value.ToString() : ""); - writer.Write("IsActive", combination.IsActive.ToString()); - - WriteQuantityUnit(writer, context, combination.QuantityUnit); - - writer.WriteStartElement("Pictures"); - foreach (int pictureId in combination.GetAssignedPictureIds()) - { - WritePicture(writer, context, _pictureService.GetPictureById(pictureId), _mediaSettings.ProductThumbPictureSize, _mediaSettings.ProductDetailsPictureSize); - } - writer.WriteEndElement(); // Pictures - - writer.WriteEndElement(); // ProductAttributeCombination - } - writer.WriteEndElement(); // ProductAttributeCombinations - - writer.WriteStartElement("ProductPictures"); - foreach (var productPicture in product.ProductPictures.OrderBy(x => x.DisplayOrder)) - { - writer.WriteStartElement("ProductPicture"); - writer.Write("Id", productPicture.Id.ToString()); - writer.Write("DisplayOrder", productPicture.DisplayOrder.ToString()); - - WritePicture(writer, context, productPicture.Picture, _mediaSettings.ProductThumbPictureSize, _mediaSettings.ProductDetailsPictureSize); - - writer.WriteEndElement(); - } - writer.WriteEndElement(); - - writer.WriteStartElement("ProductCategories"); - var productCategories = _categoryService.GetProductCategoriesByProductId(product.Id); - if (productCategories != null) - { - foreach (var productCategory in productCategories.OrderBy(x => x.DisplayOrder)) - { - var category = productCategory.Category; - writer.WriteStartElement("ProductCategory"); - writer.Write("IsFeaturedProduct", productCategory.IsFeaturedProduct.ToString()); - writer.Write("DisplayOrder", productCategory.DisplayOrder.ToString()); - - writer.WriteStartElement("Category"); - writer.Write("Id", category.Id.ToString()); - writer.Write("Name", category.Name); - writer.Write("FullName", category.FullName); - writer.Write("Description", category.Description); - writer.Write("BottomDescription", category.BottomDescription); - writer.Write("CategoryTemplateId", category.CategoryTemplateId.ToString()); - writer.Write("MetaKeywords", category.MetaKeywords); - writer.Write("MetaDescription", category.MetaDescription); - writer.Write("MetaTitle", category.MetaTitle); - writer.Write("SeName", category.GetSeName(0)); - writer.Write("ParentCategoryId", category.ParentCategoryId.ToString()); - writer.Write("PageSize", category.PageSize.ToString()); - writer.Write("AllowCustomersToSelectPageSize", category.AllowCustomersToSelectPageSize.ToString()); - writer.Write("PageSizeOptions", category.PageSizeOptions); - writer.Write("PriceRanges", category.PriceRanges); - writer.Write("ShowOnHomePage", category.ShowOnHomePage.ToString()); - writer.Write("HasDiscountsApplied", category.HasDiscountsApplied.ToString()); - writer.Write("Published", category.Published.ToString()); - writer.Write("Deleted", category.Deleted.ToString()); - writer.Write("DisplayOrder", category.DisplayOrder.ToString()); - writer.Write("CreatedOnUtc", category.CreatedOnUtc.ToString(culture)); - writer.Write("UpdatedOnUtc", category.UpdatedOnUtc.ToString(culture)); - writer.Write("SubjectToAcl", category.SubjectToAcl.ToString()); - writer.Write("LimitedToStores", category.LimitedToStores.ToString()); - writer.Write("Alias", category.Alias); - writer.Write("DefaultViewMode", category.DefaultViewMode); - - WritePicture(writer, context, category.Picture, _mediaSettings.CategoryThumbPictureSize, _mediaSettings.CategoryThumbPictureSize); - - WriteLocalized(writer, context, lang => - { - writer.Write("Name", category.GetLocalized(x => x.Name, lang.Id, false, false), lang); - writer.Write("FullName", category.GetLocalized(x => x.FullName, lang.Id, false, false), lang); - writer.Write("Description", category.GetLocalized(x => x.Description, lang.Id, false, false), lang); - writer.Write("BottomDescription", category.GetLocalized(x => x.BottomDescription, lang.Id, false, false), lang); - writer.Write("MetaKeywords", category.GetLocalized(x => x.MetaKeywords, lang.Id, false, false), lang); - writer.Write("MetaDescription", category.GetLocalized(x => x.MetaDescription, lang.Id, false, false), lang); - writer.Write("MetaTitle", category.GetLocalized(x => x.MetaTitle, lang.Id, false, false), lang); - writer.Write("SeName", category.GetSeName(lang.Id, false, false)); - }); - - writer.WriteEndElement(); - - writer.WriteEndElement(); - } - } - writer.WriteEndElement(); - - writer.WriteStartElement("ProductManufacturers"); - var productManufacturers = _manufacturerService.GetProductManufacturersByProductId(product.Id); - if (productManufacturers != null) - { - foreach (var productManufacturer in productManufacturers.OrderBy(x => x.DisplayOrder)) - { - var manu = productManufacturer.Manufacturer; - writer.WriteStartElement("ProductManufacturer"); - - writer.Write("Id", productManufacturer.Id.ToString()); - writer.Write("IsFeaturedProduct", productManufacturer.IsFeaturedProduct.ToString()); - writer.Write("DisplayOrder", productManufacturer.DisplayOrder.ToString()); - - writer.WriteStartElement("Manufacturer"); - writer.Write("Id", manu.Id.ToString()); - writer.Write("Name", manu.Name); - writer.Write("SeName", manu.GetSeName(0, true, false)); - writer.Write("Description", manu.Description); - writer.Write("MetaKeywords", manu.MetaKeywords); - writer.Write("MetaDescription", manu.MetaDescription); - writer.Write("MetaTitle", manu.MetaTitle); - - WritePicture(writer, context, manu.Picture, _mediaSettings.ManufacturerThumbPictureSize, _mediaSettings.ManufacturerThumbPictureSize); - - WriteLocalized(writer, context, lang => - { - writer.Write("Name", manu.GetLocalized(x => x.Name, lang.Id, false, false), lang); - writer.Write("SeName", manu.GetSeName(lang.Id, false, false), lang); - writer.Write("Description", manu.GetLocalized(x => x.Description, lang.Id, false, false), lang); - writer.Write("MetaKeywords", manu.GetLocalized(x => x.MetaKeywords, lang.Id, false, false), lang); - writer.Write("MetaDescription", manu.GetLocalized(x => x.MetaDescription, lang.Id, false, false), lang); - writer.Write("MetaTitle", manu.GetLocalized(x => x.MetaTitle, lang.Id, false, false), lang); - }); - - writer.WriteEndElement(); - - writer.WriteEndElement(); - } - } - writer.WriteEndElement(); - - writer.WriteStartElement("ProductSpecificationAttributes"); - foreach (var pca in product.ProductSpecificationAttributes.OrderBy(x => x.DisplayOrder)) - { - writer.WriteStartElement("ProductSpecificationAttribute"); - writer.Write("Id", pca.Id.ToString()); - writer.Write("AllowFiltering", pca.AllowFiltering.ToString()); - writer.Write("ShowOnProductPage", pca.ShowOnProductPage.ToString()); - writer.Write("DisplayOrder", pca.DisplayOrder.ToString()); - - writer.WriteStartElement("SpecificationAttributeOption"); - writer.Write("Id", pca.SpecificationAttributeOption.Id.ToString()); - writer.Write("DisplayOrder", pca.SpecificationAttributeOption.DisplayOrder.ToString()); - writer.Write("Name", pca.SpecificationAttributeOption.Name); - WriteLocalized(writer, context, lang => - { - writer.Write("Name", pca.SpecificationAttributeOption.GetLocalized(x => x.Name, lang.Id, false, false), lang); - }); - - writer.WriteStartElement("SpecificationAttribute"); - writer.Write("Id", pca.SpecificationAttributeOption.SpecificationAttribute.Id.ToString()); - writer.Write("DisplayOrder", pca.SpecificationAttributeOption.SpecificationAttribute.DisplayOrder.ToString()); - writer.Write("Name", pca.SpecificationAttributeOption.SpecificationAttribute.Name); - WriteLocalized(writer, context, lang => - { - writer.Write("Name", pca.SpecificationAttributeOption.SpecificationAttribute.GetLocalized(x => x.Name, lang.Id, false, false), lang); - }); - writer.WriteEndElement(); // SpecificationAttribute - - writer.WriteEndElement(); // SpecificationAttributeOption - - writer.WriteEndElement(); // ProductSpecificationAttribute - } - writer.WriteEndElement(); - - writer.WriteStartElement("ProductBundleItems"); - var bundleItems = _productService.GetBundleItems(product.Id, true); - foreach (var bundleItem in bundleItems.Select(x => x.Item).OrderBy(x => x.DisplayOrder)) - { - writer.WriteStartElement("ProductBundleItem"); - writer.Write("ProductId", bundleItem.ProductId.ToString()); - writer.Write("BundleProductId", bundleItem.BundleProductId.ToString()); - writer.Write("Quantity", bundleItem.Quantity.ToString()); - writer.Write("Discount", bundleItem.Discount.HasValue ? bundleItem.Discount.Value.ToString(culture) : ""); - writer.Write("DiscountPercentage", bundleItem.DiscountPercentage.ToString()); - writer.Write("Name", bundleItem.GetLocalizedName()); - writer.Write("ShortDescription", bundleItem.ShortDescription); - writer.Write("FilterAttributes", bundleItem.FilterAttributes.ToString()); - writer.Write("HideThumbnail", bundleItem.HideThumbnail.ToString()); - writer.Write("Visible", bundleItem.Visible.ToString()); - writer.Write("Published", bundleItem.Published.ToString()); - writer.Write("DisplayOrder", bundleItem.DisplayOrder.ToString()); - writer.Write("CreatedOnUtc", bundleItem.CreatedOnUtc.ToString(culture)); - writer.Write("UpdatedOnUtc", bundleItem.UpdatedOnUtc.ToString(culture)); - writer.WriteEndElement(); - } - writer.WriteEndElement(); - } - - /// - /// Export product list to XML - /// - /// Stream to write - /// Search context - public virtual void ExportProductsToXml(Stream stream, ProductSearchContext searchContext) - { - var settings = new XmlWriterSettings() - { - Encoding = new UTF8Encoding(false), - CheckCharacters = false - }; - - var context = new XmlExportContext() - { - ProductTemplates = _productTemplateService.GetAllProductTemplates(), - Languages = _languageService.GetAllLanguages(true), - Store = _commonServices.StoreContext.CurrentStore - }; - - using (var writer = XmlWriter.Create(stream, settings)) - { - writer.WriteStartDocument(); - writer.WriteStartElement("Products"); - writer.WriteAttributeString("Version", SmartStoreVersion.CurrentVersion); - - for (int i = 0; i < 9999999; ++i) - { - searchContext.PageIndex = i; - - var products = _productService.SearchProducts(searchContext); - - foreach (var product in products) - { - writer.WriteStartElement("Product"); - - try - { - WriteProductToXml(writer, product, context); - } - catch (Exception exc) - { - Logger.Error("{0} (Product.Id {1})".FormatWith(exc.Message, product.Id), exc); - } - - writer.WriteEndElement(); // Product - } - - if (!products.HasNextPage) - break; - } - - writer.WriteEndElement(); - writer.WriteEndDocument(); - writer.Flush(); - writer.Close(); - - stream.Seek(0, SeekOrigin.Begin); - } - } - - /// - /// Export products to XLSX - /// - /// Stream - /// Products - public virtual void ExportProductsToXlsx(Stream stream, IList products) - { - if (stream == null) - throw new ArgumentNullException("stream"); - - // ok, we can run the real code of the sample now - 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; - - //Create Headers and format them - var properties = new string[] - { - "ProductTypeId", - "ParentGroupedProductId", - "VisibleIndividually", - "Name", - "ShortDescription", - "FullDescription", - "ProductTemplateId", - "ShowOnHomePage", - "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" - }; - - //BEGIN: add headers for languages - var languages = _languageService.GetAllLanguages(true); - var headlines = new string[properties.Length + languages.Count * 3]; - var languageFields = new string[languages.Count * 3]; - 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); - //END: add headers for languages - - 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; - } - - - int row = 2; - foreach (var p in products) - { - int col = 1; - - cells[row, col].Value = p.ProductTypeId; - col++; - - cells[row, col].Value = p.ParentGroupedProductId; - col++; - - cells[row, col].Value = p.VisibleIndividually; - col++; - - cells[row, col].Value = p.Name; - col++; - - cells[row, col].Value = p.ShortDescription; - col++; - - cells[row, col].Value = p.FullDescription; - col++; - - cells[row, col].Value = p.ProductTemplateId; - col++; - - cells[row, col].Value = p.ShowOnHomePage; - col++; - - cells[row, col].Value = p.MetaKeywords; - col++; - - cells[row, col].Value = p.MetaDescription; - col++; - - cells[row, col].Value = p.MetaTitle; - col++; - - cells[row, col].Value = p.GetSeName(0); - col++; - - cells[row, col].Value = p.AllowCustomerReviews; - col++; - - cells[row, col].Value = p.Published; - col++; - - cells[row, col].Value = p.Sku; - col++; - - cells[row, col].Value = p.ManufacturerPartNumber; - col++; - - cells[row, col].Value = p.Gtin; - col++; - - cells[row, col].Value = p.IsGiftCard; - col++; - - cells[row, col].Value = p.GiftCardTypeId; - col++; - - cells[row, col].Value = p.RequireOtherProducts; - col++; - - cells[row, col].Value = p.RequiredProductIds; - col++; - - cells[row, col].Value = p.AutomaticallyAddRequiredProducts; - col++; - - cells[row, col].Value = p.IsDownload; - col++; - - cells[row, col].Value = p.DownloadId; - col++; - - cells[row, col].Value = p.UnlimitedDownloads; - col++; - - cells[row, col].Value = p.MaxNumberOfDownloads; - col++; - - cells[row, col].Value = p.DownloadActivationTypeId; - col++; - - cells[row, col].Value = p.HasSampleDownload; - col++; - - cells[row, col].Value = p.SampleDownloadId; - col++; - - cells[row, col].Value = p.HasUserAgreement; - col++; - - cells[row, col].Value = p.UserAgreementText; - col++; - - cells[row, col].Value = p.IsRecurring; - col++; - - cells[row, col].Value = p.RecurringCycleLength; - col++; - - cells[row, col].Value = p.RecurringCyclePeriodId; - col++; - - cells[row, col].Value = p.RecurringTotalCycles; - col++; - - cells[row, col].Value = p.IsShipEnabled; - col++; - - cells[row, col].Value = p.IsFreeShipping; - col++; - - cells[row, col].Value = p.AdditionalShippingCharge; - col++; - - cells[row, col].Value = p.IsEsd; - col++; - - cells[row, col].Value = p.IsTaxExempt; - col++; - - cells[row, col].Value = p.TaxCategoryId; - col++; - - cells[row, col].Value = p.ManageInventoryMethodId; - col++; - - cells[row, col].Value = p.StockQuantity; - col++; - - cells[row, col].Value = p.DisplayStockAvailability; - col++; - - cells[row, col].Value = p.DisplayStockQuantity; - col++; - - cells[row, col].Value = p.MinStockQuantity; - col++; - - cells[row, col].Value = p.LowStockActivityId; - col++; - - cells[row, col].Value = p.NotifyAdminForQuantityBelow; - col++; - - cells[row, col].Value = p.BackorderModeId; - col++; - - cells[row, col].Value = p.AllowBackInStockSubscriptions; - col++; - - cells[row, col].Value = p.OrderMinimumQuantity; - col++; - - cells[row, col].Value = p.OrderMaximumQuantity; - col++; - - cells[row, col].Value = p.AllowedQuantities; - col++; - - cells[row, col].Value = p.DisableBuyButton; - col++; - - cells[row, col].Value = p.DisableWishlistButton; - col++; - - cells[row, col].Value = p.AvailableForPreOrder; - col++; - - cells[row, col].Value = p.CallForPrice; - col++; - - cells[row, col].Value = p.Price; - col++; - - cells[row, col].Value = p.OldPrice; - col++; - - cells[row, col].Value = p.ProductCost; - col++; - - cells[row, col].Value = p.SpecialPrice; - col++; - - cells[row, col].Value = p.SpecialPriceStartDateTimeUtc; - col++; - - cells[row, col].Value = p.SpecialPriceEndDateTimeUtc; - col++; - - cells[row, col].Value = p.CustomerEntersPrice; - col++; - - cells[row, col].Value = p.MinimumCustomerEnteredPrice; - col++; - - cells[row, col].Value = p.MaximumCustomerEnteredPrice; - col++; - - cells[row, col].Value = p.Weight; - col++; - - cells[row, col].Value = p.Length; - col++; - - cells[row, col].Value = p.Width; - col++; - - cells[row, col].Value = p.Height; - col++; - - cells[row, col].Value = p.CreatedOnUtc.ToOADate(); - col++; - - //category identifiers - string categoryIds = null; - foreach (var pc in _categoryService.GetProductCategoriesByProductId(p.Id)) - { - categoryIds += pc.CategoryId; - categoryIds += ";"; - } - cells[row, col].Value = categoryIds; - col++; - - //manufacturer identifiers - string manufacturerIds = null; - foreach (var pm in _manufacturerService.GetProductManufacturersByProductId(p.Id)) - { - manufacturerIds += pm.ManufacturerId; - manufacturerIds += ";"; - } - cells[row, col].Value = manufacturerIds; - col++; - - //pictures (up to 3 pictures) - var pics = new string[] { null, null, null }; - var pictures = _pictureService.GetPicturesByProductId(p.Id, 3); - for (int i = 0; i < pictures.Count; i++) - { - pics[i] = _pictureService.GetThumbLocalPath(pictures[i]); - pictures[i].PictureBinary = null; - } - cells[row, col].Value = pics[0]; - col++; - cells[row, col].Value = pics[1]; - col++; - cells[row, col].Value = pics[2]; - col++; - - cells[row, col].Value = p.DeliveryTimeId; - col++; - cells[row, col].Value = p.QuantityUnitId; - col++; - cells[row, col].Value = p.BasePriceEnabled; - col++; - cells[row, col].Value = p.BasePriceMeasureUnit; - col++; - cells[row, col].Value = p.BasePriceAmount; - col++; - cells[row, col].Value = p.BasePriceBaseAmount; - col++; - - cells[row, col].Value = p.BundleTitleText; - col++; - - cells[row, col].Value = p.BundlePerItemShipping; - col++; - - cells[row, col].Value = p.BundlePerItemPricing; - col++; - - cells[row, col].Value = p.BundlePerItemShoppingCart; - col++; - - string bundleItemSkus = ""; - - if (p.ProductType == ProductType.BundledProduct) - { - bundleItemSkus = string.Join(",", _productService.GetBundleItems(p.Id, true).Select(x => x.Item.Product.Sku)); - } - - cells[row, col].Value = bundleItemSkus; - col++; - - cells[row, col].Value = p.AvailableStartDateTimeUtc; - col++; - - cells[row, col].Value = p.AvailableEndDateTimeUtc; - col++; - - string storeIds = ""; - - if (p.LimitedToStores) - { - storeIds = string.Join(";", _storeMappingService.GetStoreMappings(p).Select(x => x.StoreId)); - } - cells[row, col].Value = storeIds; - col++; - - cells[row, col].Value = p.LimitedToStores; - col++; - - //BEGIN: export localized values - foreach (var lang in languages) - { - worksheet.Cells[row, col].Value = p.GetLocalized(x => x.Name, lang.Id, false, false); - col++; - - worksheet.Cells[row, col].Value = p.GetLocalized(x => x.ShortDescription, lang.Id, false, false); - col++; - - worksheet.Cells[row, col].Value = p.GetLocalized(x => x.FullDescription, lang.Id, false, false); - col++; - } - //END: export localized values - - 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(); - } - - /// - /// Export order list to xml - /// - /// Orders - /// Result in XML format - public virtual string ExportOrdersToXml(IList orders) - { - var sb = new StringBuilder(); - var stringWriter = new StringWriter(sb); - var xmlWriter = new XmlTextWriter(stringWriter); - xmlWriter.WriteStartDocument(); - xmlWriter.WriteStartElement("Orders"); - xmlWriter.WriteAttributeString("Version", SmartStoreVersion.CurrentVersion); - - - foreach (var order in orders) - { - xmlWriter.WriteStartElement("Order"); - - xmlWriter.WriteElementString("OrderId", null, order.GetOrderNumber()); - xmlWriter.WriteElementString("OrderGuid", null, order.OrderGuid.ToString()); - xmlWriter.WriteElementString("StoreId", null, order.StoreId.ToString()); - xmlWriter.WriteElementString("CustomerId", null, order.CustomerId.ToString()); - xmlWriter.WriteElementString("CustomerLanguageId", null, order.CustomerLanguageId.ToString()); - xmlWriter.WriteElementString("CustomerTaxDisplayTypeId", null, order.CustomerTaxDisplayTypeId.ToString()); - xmlWriter.WriteElementString("CustomerIp", null, order.CustomerIp); - xmlWriter.WriteElementString("OrderSubtotalInclTax", null, order.OrderSubtotalInclTax.ToString()); - xmlWriter.WriteElementString("OrderSubtotalExclTax", null, order.OrderSubtotalExclTax.ToString()); - xmlWriter.WriteElementString("OrderSubTotalDiscountInclTax", null, order.OrderSubTotalDiscountInclTax.ToString()); - xmlWriter.WriteElementString("OrderSubTotalDiscountExclTax", null, order.OrderSubTotalDiscountExclTax.ToString()); - xmlWriter.WriteElementString("OrderShippingInclTax", null, order.OrderShippingInclTax.ToString()); - xmlWriter.WriteElementString("OrderShippingExclTax", null, order.OrderShippingExclTax.ToString()); - xmlWriter.WriteElementString("PaymentMethodAdditionalFeeInclTax", null, order.PaymentMethodAdditionalFeeInclTax.ToString()); - xmlWriter.WriteElementString("PaymentMethodAdditionalFeeExclTax", null, order.PaymentMethodAdditionalFeeExclTax.ToString()); - xmlWriter.WriteElementString("TaxRates", null, order.TaxRates); - xmlWriter.WriteElementString("OrderTax", null, order.OrderTax.ToString()); - xmlWriter.WriteElementString("OrderTotal", null, order.OrderTotal.ToString()); - xmlWriter.WriteElementString("RefundedAmount", null, order.RefundedAmount.ToString()); - xmlWriter.WriteElementString("OrderDiscount", null, order.OrderDiscount.ToString()); - xmlWriter.WriteElementString("CurrencyRate", null, order.CurrencyRate.ToString()); - xmlWriter.WriteElementString("CustomerCurrencyCode", null, order.CustomerCurrencyCode); - xmlWriter.WriteElementString("AffiliateId", null, order.AffiliateId.ToString()); - xmlWriter.WriteElementString("OrderStatusId", null, order.OrderStatusId.ToString()); - xmlWriter.WriteElementString("AllowStoringCreditCardNumber", null, order.AllowStoringCreditCardNumber.ToString()); - xmlWriter.WriteElementString("CardType", null, order.CardType); - xmlWriter.WriteElementString("CardName", null, order.CardName); - xmlWriter.WriteElementString("CardNumber", null, order.CardNumber); - xmlWriter.WriteElementString("MaskedCreditCardNumber", null, order.MaskedCreditCardNumber); - xmlWriter.WriteElementString("CardCvv2", null, order.CardCvv2); - xmlWriter.WriteElementString("CardExpirationMonth", null, order.CardExpirationMonth); - xmlWriter.WriteElementString("CardExpirationYear", null, order.CardExpirationYear); - - xmlWriter.WriteElementString("DirectDebitAccountHolder", null, order.DirectDebitAccountHolder); - xmlWriter.WriteElementString("DirectDebitAccountHolder", null, order.DirectDebitAccountNumber); - xmlWriter.WriteElementString("DirectDebitAccountHolder", null, order.DirectDebitBankCode); - xmlWriter.WriteElementString("DirectDebitAccountHolder", null, order.DirectDebitBankName); - xmlWriter.WriteElementString("DirectDebitAccountHolder", null, order.DirectDebitBIC); - xmlWriter.WriteElementString("DirectDebitAccountHolder", null, order.DirectDebitCountry); - xmlWriter.WriteElementString("DirectDebitAccountHolder", null, order.DirectDebitIban); - - xmlWriter.WriteElementString("PaymentMethodSystemName", null, order.PaymentMethodSystemName); - xmlWriter.WriteElementString("AuthorizationTransactionId", null, order.AuthorizationTransactionId); - xmlWriter.WriteElementString("AuthorizationTransactionCode", null, order.AuthorizationTransactionCode); - xmlWriter.WriteElementString("AuthorizationTransactionResult", null, order.AuthorizationTransactionResult); - xmlWriter.WriteElementString("CaptureTransactionId", null, order.CaptureTransactionId); - xmlWriter.WriteElementString("CaptureTransactionResult", null, order.CaptureTransactionResult); - xmlWriter.WriteElementString("SubscriptionTransactionId", null, order.SubscriptionTransactionId); - xmlWriter.WriteElementString("PurchaseOrderNumber", null, order.PurchaseOrderNumber); - xmlWriter.WriteElementString("PaymentStatusId", null, order.PaymentStatusId.ToString()); - xmlWriter.WriteElementString("PaidDateUtc", null, (order.PaidDateUtc == null) ? string.Empty : order.PaidDateUtc.Value.ToString()); - xmlWriter.WriteElementString("ShippingStatusId", null, order.ShippingStatusId.ToString()); - xmlWriter.WriteElementString("ShippingMethod", null, order.ShippingMethod); - xmlWriter.WriteElementString("ShippingRateComputationMethodSystemName", null, order.ShippingRateComputationMethodSystemName); - xmlWriter.WriteElementString("VatNumber", null, order.VatNumber); - xmlWriter.WriteElementString("Deleted", null, order.Deleted.ToString()); - xmlWriter.WriteElementString("CreatedOnUtc", null, order.CreatedOnUtc.ToString()); - xmlWriter.WriteElementString("UpdatedOnUtc", null, order.UpdatedOnUtc.ToString()); - xmlWriter.WriteElementString("RewardPointsRemaining", null, order.RewardPointsRemaining.HasValue ? order.RewardPointsRemaining.Value.ToString() : ""); - xmlWriter.WriteElementString("HasNewPaymentNotification", null, order.HasNewPaymentNotification.ToString()); - - //products - var orderItems = order.OrderItems; - if (orderItems.Count > 0) - { - xmlWriter.WriteStartElement("OrderItems"); - foreach (var orderItem in orderItems) - { - xmlWriter.WriteStartElement("OrderItem"); - xmlWriter.WriteElementString("Id", null, orderItem.Id.ToString()); - xmlWriter.WriteElementString("OrderItemGuid", null, orderItem.OrderItemGuid.ToString()); - xmlWriter.WriteElementString("ProductId", null, orderItem.ProductId.ToString()); - - xmlWriter.WriteElementString("ProductName", null, orderItem.Product.Name); - xmlWriter.WriteElementString("UnitPriceInclTax", null, orderItem.UnitPriceInclTax.ToString()); - xmlWriter.WriteElementString("UnitPriceExclTax", null, orderItem.UnitPriceExclTax.ToString()); - xmlWriter.WriteElementString("PriceInclTax", null, orderItem.PriceInclTax.ToString()); - xmlWriter.WriteElementString("PriceExclTax", null, orderItem.PriceExclTax.ToString()); - xmlWriter.WriteElementString("AttributeDescription", null, orderItem.AttributeDescription); - xmlWriter.WriteElementString("AttributesXml", null, orderItem.AttributesXml); - xmlWriter.WriteElementString("Quantity", null, orderItem.Quantity.ToString()); - xmlWriter.WriteElementString("DiscountAmountInclTax", null, orderItem.DiscountAmountInclTax.ToString()); - xmlWriter.WriteElementString("DiscountAmountExclTax", null, orderItem.DiscountAmountExclTax.ToString()); - xmlWriter.WriteElementString("DownloadCount", null, orderItem.DownloadCount.ToString()); - xmlWriter.WriteElementString("IsDownloadActivated", null, orderItem.IsDownloadActivated.ToString()); - xmlWriter.WriteElementString("LicenseDownloadId", null, orderItem.LicenseDownloadId.ToString()); - xmlWriter.WriteElementString("BundleData", null, orderItem.BundleData); - xmlWriter.WriteElementString("ProductCost", null, orderItem.ProductCost.ToString()); - xmlWriter.WriteEndElement(); - } - xmlWriter.WriteEndElement(); - } - - //shipments - var shipments = order.Shipments.OrderBy(x => x.CreatedOnUtc).ToList(); - if (shipments.Count > 0) - { - xmlWriter.WriteStartElement("Shipments"); - foreach (var shipment in shipments) - { - xmlWriter.WriteStartElement("Shipment"); - xmlWriter.WriteElementString("ShipmentId", null, shipment.Id.ToString()); - xmlWriter.WriteElementString("TrackingNumber", null, shipment.TrackingNumber); - xmlWriter.WriteElementString("TotalWeight", null, shipment.TotalWeight.HasValue ? shipment.TotalWeight.Value.ToString() : ""); - - xmlWriter.WriteElementString("ShippedDateUtc", null, shipment.ShippedDateUtc.HasValue ? - shipment.ShippedDateUtc.ToString() : ""); - xmlWriter.WriteElementString("DeliveryDateUtc", null, shipment.DeliveryDateUtc.HasValue ? - shipment.DeliveryDateUtc.Value.ToString() : ""); - xmlWriter.WriteElementString("CreatedOnUtc", null, shipment.CreatedOnUtc.ToString()); - xmlWriter.WriteEndElement(); - } - xmlWriter.WriteEndElement(); - } - xmlWriter.WriteEndElement(); - } - - xmlWriter.WriteEndElement(); - xmlWriter.WriteEndDocument(); - xmlWriter.Close(); - return stringWriter.ToString(); - } - - /// - /// Export orders to XLSX - /// - /// Stream - /// Orders - public virtual void ExportOrdersToXlsx(Stream stream, IList orders) - { - if (stream == null) - throw new ArgumentNullException("stream"); - - // ok, we can run the real code of the sample now - 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 - var properties = new string[] - { - //order properties - "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", - "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", - }; - 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; - foreach (var order in orders) - { - int col = 1; - - //order properties - worksheet.Cells[row, col].Value = order.GetOrderNumber(); - col++; - - worksheet.Cells[row, col].Value = order.OrderGuid; - col++; - - worksheet.Cells[row, col].Value = order.CustomerId; - col++; - - worksheet.Cells[row, col].Value = order.OrderSubtotalInclTax; - col++; - - worksheet.Cells[row, col].Value = order.OrderSubtotalExclTax; - col++; - - worksheet.Cells[row, col].Value = order.OrderSubTotalDiscountInclTax; - col++; - - worksheet.Cells[row, col].Value = order.OrderSubTotalDiscountExclTax; - col++; - - worksheet.Cells[row, col].Value = order.OrderShippingInclTax; - col++; - - worksheet.Cells[row, col].Value = order.OrderShippingExclTax; - col++; - - worksheet.Cells[row, col].Value = order.PaymentMethodAdditionalFeeInclTax; - col++; - - worksheet.Cells[row, col].Value = order.PaymentMethodAdditionalFeeExclTax; - col++; - - worksheet.Cells[row, col].Value = order.TaxRates; - col++; - - worksheet.Cells[row, col].Value = order.OrderTax; - col++; - - worksheet.Cells[row, col].Value = order.OrderTotal; - col++; - - worksheet.Cells[row, col].Value = order.RefundedAmount; - col++; - - worksheet.Cells[row, col].Value = order.OrderDiscount; - col++; - - worksheet.Cells[row, col].Value = order.CurrencyRate; - col++; - - worksheet.Cells[row, col].Value = order.CustomerCurrencyCode; - col++; - - worksheet.Cells[row, col].Value = order.AffiliateId; - col++; - - worksheet.Cells[row, col].Value = order.OrderStatusId; - col++; - - worksheet.Cells[row, col].Value = order.PaymentMethodSystemName; - col++; - - worksheet.Cells[row, col].Value = order.PurchaseOrderNumber; - col++; - - worksheet.Cells[row, col].Value = order.PaymentStatusId; - col++; - - worksheet.Cells[row, col].Value = order.ShippingStatusId; - col++; - - worksheet.Cells[row, col].Value = order.ShippingMethod; - col++; - - worksheet.Cells[row, col].Value = order.ShippingRateComputationMethodSystemName; - col++; - - worksheet.Cells[row, col].Value = order.VatNumber; - col++; - - worksheet.Cells[row, col].Value = order.CreatedOnUtc.ToOADate(); - col++; - - worksheet.Cells[row, col].Value = order.UpdatedOnUtc.ToOADate(); - col++; - - worksheet.Cells[row, col].Value = (order.RewardPointsRemaining.HasValue ? order.RewardPointsRemaining.Value.ToString() : ""); - col++; - - worksheet.Cells[row, col].Value = order.HasNewPaymentNotification; - col++; - - - //billing address - worksheet.Cells[row, col].Value = order.BillingAddress != null ? order.BillingAddress.FirstName : ""; - col++; - - worksheet.Cells[row, col].Value = order.BillingAddress != null ? order.BillingAddress.LastName : ""; - col++; - - worksheet.Cells[row, col].Value = order.BillingAddress != null ? order.BillingAddress.Email : ""; - col++; - - worksheet.Cells[row, col].Value = order.BillingAddress != null ? order.BillingAddress.Company : ""; - col++; - - worksheet.Cells[row, col].Value = order.BillingAddress != null && order.BillingAddress.Country != null ? order.BillingAddress.Country.Name : ""; - col++; - - worksheet.Cells[row, col].Value = order.BillingAddress != null && order.BillingAddress.StateProvince != null ? order.BillingAddress.StateProvince.Name : ""; - col++; - - worksheet.Cells[row, col].Value = order.BillingAddress != null ? order.BillingAddress.City : ""; - col++; - - worksheet.Cells[row, col].Value = order.BillingAddress != null ? order.BillingAddress.Address1 : ""; - col++; - - worksheet.Cells[row, col].Value = order.BillingAddress != null ? order.BillingAddress.Address2 : ""; - col++; - - worksheet.Cells[row, col].Value = order.BillingAddress != null ? order.BillingAddress.ZipPostalCode : ""; - col++; - - worksheet.Cells[row, col].Value = order.BillingAddress != null ? order.BillingAddress.PhoneNumber : ""; - col++; - - worksheet.Cells[row, col].Value = order.BillingAddress != null ? order.BillingAddress.FaxNumber : ""; - col++; - - //shipping address - worksheet.Cells[row, col].Value = order.ShippingAddress != null ? order.ShippingAddress.FirstName : ""; - col++; - - worksheet.Cells[row, col].Value = order.ShippingAddress != null ? order.ShippingAddress.LastName : ""; - col++; - - worksheet.Cells[row, col].Value = order.ShippingAddress != null ? order.ShippingAddress.Email : ""; - col++; - - worksheet.Cells[row, col].Value = order.ShippingAddress != null ? order.ShippingAddress.Company : ""; - col++; - - worksheet.Cells[row, col].Value = order.ShippingAddress != null && order.ShippingAddress.Country != null ? order.ShippingAddress.Country.Name : ""; - col++; - - worksheet.Cells[row, col].Value = order.ShippingAddress != null && order.ShippingAddress.StateProvince != null ? order.ShippingAddress.StateProvince.Name : ""; - col++; - - worksheet.Cells[row, col].Value = order.ShippingAddress != null ? order.ShippingAddress.City : ""; - col++; - - worksheet.Cells[row, col].Value = order.ShippingAddress != null ? order.ShippingAddress.Address1 : ""; - col++; - - worksheet.Cells[row, col].Value = order.ShippingAddress != null ? order.ShippingAddress.Address2 : ""; - col++; - - worksheet.Cells[row, col].Value = order.ShippingAddress != null ? order.ShippingAddress.ZipPostalCode : ""; - col++; - - worksheet.Cells[row, col].Value = order.ShippingAddress != null ? order.ShippingAddress.PhoneNumber : ""; - col++; - - worksheet.Cells[row, col].Value = order.ShippingAddress != null ? order.ShippingAddress.FaxNumber : ""; - col++; - - //next row - 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(); - } - } - - /// - /// Export customer list to XLSX - /// - /// Stream - /// Customers - public virtual void ExportCustomersToXlsx(Stream stream, IList customers) - { - if (stream == null) - throw new ArgumentNullException("stream"); - - // ok, we can run the real code of the sample now - 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 - var properties = new string[] - { - "Id", - "CustomerGuid", - "Email", - "Username", - "PasswordStr",//why can't we use 'Password' name? - "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", - }; - - 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; - foreach (var customer in customers) - { - int col = 1; - - worksheet.Cells[row, col].Value = customer.Id; - col++; - - worksheet.Cells[row, col].Value = customer.CustomerGuid; - col++; - - worksheet.Cells[row, col].Value = customer.Email; - col++; - - worksheet.Cells[row, col].Value = customer.Username; - col++; - - worksheet.Cells[row, col].Value = customer.Password; - col++; - - worksheet.Cells[row, col].Value = customer.PasswordFormatId; - col++; - - worksheet.Cells[row, col].Value = customer.PasswordSalt; - col++; - - worksheet.Cells[row, col].Value = customer.AdminComment; - col++; - - worksheet.Cells[row, col].Value = customer.IsTaxExempt; - col++; - - worksheet.Cells[row, col].Value = customer.AffiliateId; - col++; - - worksheet.Cells[row, col].Value = customer.Active; - col++; - - worksheet.Cells[row, col].Value = customer.IsSystemAccount; - col++; - - worksheet.Cells[row, col].Value = customer.SystemName; - col++; - - worksheet.Cells[row, col].Value = customer.LastIpAddress; - col++; - - worksheet.Cells[row, col].Value = customer.CreatedOnUtc.ToString(); - col++; - - worksheet.Cells[row, col].Value = (customer.LastLoginDateUtc.HasValue ? customer.LastLoginDateUtc.Value.ToString() : null); - col++; - - worksheet.Cells[row, col].Value = customer.LastActivityDateUtc.ToString(); - col++; - - - //roles - worksheet.Cells[row, col].Value = customer.IsGuest(); - col++; - - worksheet.Cells[row, col].Value = customer.IsRegistered(); - col++; - - worksheet.Cells[row, col].Value = customer.IsAdmin(); - col++; - - worksheet.Cells[row, col].Value = customer.IsForumModerator(); - col++; - - //attributes - var firstName = customer.GetAttribute(SystemCustomerAttributeNames.FirstName); - var lastName = customer.GetAttribute(SystemCustomerAttributeNames.LastName); - var gender = customer.GetAttribute(SystemCustomerAttributeNames.Gender); - var company = customer.GetAttribute(SystemCustomerAttributeNames.Company); - var streetAddress = customer.GetAttribute(SystemCustomerAttributeNames.StreetAddress); - var streetAddress2 = customer.GetAttribute(SystemCustomerAttributeNames.StreetAddress2); - var zipPostalCode = customer.GetAttribute(SystemCustomerAttributeNames.ZipPostalCode); - var city = customer.GetAttribute(SystemCustomerAttributeNames.City); - var countryId = customer.GetAttribute(SystemCustomerAttributeNames.CountryId); - var stateProvinceId = customer.GetAttribute(SystemCustomerAttributeNames.StateProvinceId); - var phone = customer.GetAttribute(SystemCustomerAttributeNames.Phone); - var fax = customer.GetAttribute(SystemCustomerAttributeNames.Fax); - var vatNumber = customer.GetAttribute(SystemCustomerAttributeNames.VatNumber); - var vatNumberStatusId = customer.GetAttribute(SystemCustomerAttributeNames.VatNumberStatusId); - var timeZoneId = customer.GetAttribute(SystemCustomerAttributeNames.TimeZoneId); - - var newsletter = _newsLetterSubscriptionService.GetNewsLetterSubscriptionByEmail(customer.Email); - bool subscribedToNewsletters = newsletter != null && newsletter.Active; - - var avatarPictureId = customer.GetAttribute(SystemCustomerAttributeNames.AvatarPictureId); - var forumPostCount = customer.GetAttribute(SystemCustomerAttributeNames.ForumPostCount); - var signature = customer.GetAttribute(SystemCustomerAttributeNames.Signature); - - worksheet.Cells[row, col].Value = firstName; - col++; - - worksheet.Cells[row, col].Value = lastName; - col++; - - worksheet.Cells[row, col].Value = gender; - col++; - - worksheet.Cells[row, col].Value = company; - col++; - - worksheet.Cells[row, col].Value = streetAddress; - col++; - - worksheet.Cells[row, col].Value = streetAddress2; - col++; - - worksheet.Cells[row, col].Value = zipPostalCode; - col++; - - worksheet.Cells[row, col].Value = city; - col++; - - worksheet.Cells[row, col].Value = countryId; - col++; - - worksheet.Cells[row, col].Value = stateProvinceId; - col++; - - worksheet.Cells[row, col].Value = phone; - col++; - - worksheet.Cells[row, col].Value = fax; - col++; - - worksheet.Cells[row, col].Value = vatNumber; - col++; - - worksheet.Cells[row, col].Value = vatNumberStatusId; - col++; - - worksheet.Cells[row, col].Value = timeZoneId; - col++; - - worksheet.Cells[row, col].Value = subscribedToNewsletters; - col++; - - worksheet.Cells[row, col].Value = avatarPictureId; - col++; - - worksheet.Cells[row, col].Value = forumPostCount; - col++; - - worksheet.Cells[row, col].Value = signature; - col++; - - 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} customers", storeName); - //xlPackage.Workbook.Properties.Author = storeName; - //xlPackage.Workbook.Properties.Subject = string.Format("{0} customers", storeName); - //xlPackage.Workbook.Properties.Keywords = string.Format("{0} customers", storeName); - //xlPackage.Workbook.Properties.Category = "Customers"; - //xlPackage.Workbook.Properties.Comments = string.Format("{0} customers", storeName); - - // set some extended property values - //xlPackage.Workbook.Properties.Company = storeName; - //xlPackage.Workbook.Properties.HyperlinkBase = new Uri(storeUrl); - - // save the new spreadsheet - xlPackage.Save(); - } - } - - /// - /// Export customer list to xml - /// - /// Customers - /// Result in XML format - public virtual string ExportCustomersToXml(IList customers) - { - var sb = new StringBuilder(); - var stringWriter = new StringWriter(sb); - var xmlWriter = new XmlTextWriter(stringWriter); - xmlWriter.WriteStartDocument(); - xmlWriter.WriteStartElement("Customers"); - xmlWriter.WriteAttributeString("Version", SmartStoreVersion.CurrentVersion); - - foreach (var customer in customers) - { - xmlWriter.WriteStartElement("Customer"); - - xmlWriter.WriteElementString("Id", null, customer.Id.ToString()); - xmlWriter.WriteElementString("CustomerGuid", null, customer.CustomerGuid.ToString()); - xmlWriter.WriteElementString("Email", null, customer.Email); - xmlWriter.WriteElementString("Username", null, customer.Username); - xmlWriter.WriteElementString("Password", null, customer.Password); - xmlWriter.WriteElementString("PasswordFormatId", null, customer.PasswordFormatId.ToString()); - xmlWriter.WriteElementString("PasswordSalt", null, customer.PasswordSalt); - xmlWriter.WriteElementString("AdminComment", null, customer.AdminComment); - xmlWriter.WriteElementString("IsTaxExempt", null, customer.IsTaxExempt.ToString()); - xmlWriter.WriteElementString("AffiliateId", null, customer.AffiliateId.ToString()); - xmlWriter.WriteElementString("Active", null, customer.Active.ToString()); - xmlWriter.WriteElementString("IsSystemAccount", null, customer.IsSystemAccount.ToString()); - xmlWriter.WriteElementString("SystemName", null, customer.SystemName); - xmlWriter.WriteElementString("LastIpAddress", null, customer.LastIpAddress); - xmlWriter.WriteElementString("CreatedOnUtc", null, customer.CreatedOnUtc.ToString()); - xmlWriter.WriteElementString("LastLoginDateUtc", null, customer.LastLoginDateUtc.HasValue ? customer.LastLoginDateUtc.Value.ToString() : ""); - xmlWriter.WriteElementString("LastActivityDateUtc", null, customer.LastActivityDateUtc.ToString()); - - xmlWriter.WriteElementString("IsGuest", null, customer.IsGuest().ToString()); - xmlWriter.WriteElementString("IsRegistered", null, customer.IsRegistered().ToString()); - xmlWriter.WriteElementString("IsAdministrator", null, customer.IsAdmin().ToString()); - xmlWriter.WriteElementString("IsForumModerator", null, customer.IsForumModerator().ToString()); - - xmlWriter.WriteElementString("FirstName", null, customer.GetAttribute(SystemCustomerAttributeNames.FirstName)); - xmlWriter.WriteElementString("LastName", null, customer.GetAttribute(SystemCustomerAttributeNames.LastName)); - xmlWriter.WriteElementString("Gender", null, customer.GetAttribute(SystemCustomerAttributeNames.Gender)); - xmlWriter.WriteElementString("Company", null, customer.GetAttribute(SystemCustomerAttributeNames.Company)); - - xmlWriter.WriteElementString("CountryId", null, customer.GetAttribute(SystemCustomerAttributeNames.CountryId).ToString()); - xmlWriter.WriteElementString("StreetAddress", null, customer.GetAttribute(SystemCustomerAttributeNames.StreetAddress)); - xmlWriter.WriteElementString("StreetAddress2", null, customer.GetAttribute(SystemCustomerAttributeNames.StreetAddress2)); - xmlWriter.WriteElementString("ZipPostalCode", null, customer.GetAttribute(SystemCustomerAttributeNames.ZipPostalCode)); - xmlWriter.WriteElementString("City", null, customer.GetAttribute(SystemCustomerAttributeNames.City)); - xmlWriter.WriteElementString("CountryId", null, customer.GetAttribute(SystemCustomerAttributeNames.CountryId).ToString()); - xmlWriter.WriteElementString("StateProvinceId", null, customer.GetAttribute(SystemCustomerAttributeNames.StateProvinceId).ToString()); - xmlWriter.WriteElementString("Phone", null, customer.GetAttribute(SystemCustomerAttributeNames.Phone)); - xmlWriter.WriteElementString("Fax", null, customer.GetAttribute(SystemCustomerAttributeNames.Fax)); - xmlWriter.WriteElementString("VatNumber", null, customer.GetAttribute(SystemCustomerAttributeNames.VatNumber)); - xmlWriter.WriteElementString("VatNumberStatusId", null, customer.GetAttribute(SystemCustomerAttributeNames.VatNumberStatusId).ToString()); - xmlWriter.WriteElementString("TimeZoneId", null, customer.GetAttribute(SystemCustomerAttributeNames.TimeZoneId)); - - var newsletter = _newsLetterSubscriptionService.GetNewsLetterSubscriptionByEmail(customer.Email); - bool subscribedToNewsletters = newsletter != null && newsletter.Active; - xmlWriter.WriteElementString("Newsletter", null, subscribedToNewsletters.ToString()); - - xmlWriter.WriteElementString("AvatarPictureId", null, customer.GetAttribute(SystemCustomerAttributeNames.AvatarPictureId).ToString()); - xmlWriter.WriteElementString("ForumPostCount", null, customer.GetAttribute(SystemCustomerAttributeNames.ForumPostCount).ToString()); - xmlWriter.WriteElementString("Signature", null, customer.GetAttribute(SystemCustomerAttributeNames.Signature)); - - xmlWriter.WriteStartElement("Addresses"); - - foreach (var address in customer.Addresses) - { - bool isCurrentBillingAddress = (customer.BillingAddress != null && customer.BillingAddress.Id == address.Id); - bool isCurrentShippingAddress = (customer.ShippingAddress != null && customer.ShippingAddress.Id == address.Id); - - xmlWriter.WriteStartElement("Address"); - xmlWriter.WriteElementString("IsCurrentBillingAddress", null, isCurrentBillingAddress.ToString()); - xmlWriter.WriteElementString("IsCurrentShippingAddress", null, isCurrentShippingAddress.ToString()); - - xmlWriter.WriteElementString("Id", null, address.Id.ToString()); - xmlWriter.WriteElementString("FirstName", null, address.FirstName); - xmlWriter.WriteElementString("LastName", null, address.LastName); - xmlWriter.WriteElementString("Email", null, address.Email); - xmlWriter.WriteElementString("Company", null, address.Company); - xmlWriter.WriteElementString("City", null, address.City); - xmlWriter.WriteElementString("Address1", null, address.Address1); - xmlWriter.WriteElementString("Address2", null, address.Address2); - xmlWriter.WriteElementString("ZipPostalCode", null, address.ZipPostalCode); - xmlWriter.WriteElementString("PhoneNumber", null, address.PhoneNumber); - xmlWriter.WriteElementString("FaxNumber", null, address.FaxNumber); - xmlWriter.WriteElementString("CreatedOnUtc", null, address.CreatedOnUtc.ToString()); - - if (address.Country != null) - { - xmlWriter.WriteStartElement("Country"); - xmlWriter.WriteElementString("Id", null, address.Country.Id.ToString()); - xmlWriter.WriteElementString("Name", null, address.Country.Name); - xmlWriter.WriteElementString("AllowsBilling", null, address.Country.AllowsBilling.ToString()); - xmlWriter.WriteElementString("AllowsShipping", null, address.Country.AllowsShipping.ToString()); - xmlWriter.WriteElementString("TwoLetterIsoCode", null, address.Country.TwoLetterIsoCode); - xmlWriter.WriteElementString("ThreeLetterIsoCode", null, address.Country.ThreeLetterIsoCode); - xmlWriter.WriteElementString("NumericIsoCode", null, address.Country.NumericIsoCode.ToString()); - xmlWriter.WriteElementString("SubjectToVat", null, address.Country.SubjectToVat.ToString()); - xmlWriter.WriteElementString("Published", null, address.Country.Published.ToString()); - xmlWriter.WriteElementString("DisplayOrder", null, address.Country.DisplayOrder.ToString()); - xmlWriter.WriteElementString("LimitedToStores", null, address.Country.LimitedToStores.ToString()); - xmlWriter.WriteEndElement(); // Country - } - - if (address.StateProvince != null) - { - xmlWriter.WriteStartElement("StateProvince"); - xmlWriter.WriteElementString("Id", null, address.StateProvince.Id.ToString()); - xmlWriter.WriteElementString("CountryId", null, address.StateProvince.CountryId.ToString()); - xmlWriter.WriteElementString("Name", null, address.StateProvince.Name); - xmlWriter.WriteElementString("Abbreviation", null, address.StateProvince.Abbreviation); - xmlWriter.WriteElementString("Published", null, address.StateProvince.Published.ToString()); - xmlWriter.WriteElementString("DisplayOrder", null, address.StateProvince.DisplayOrder.ToString()); - xmlWriter.WriteEndElement(); // StateProvince - } - - xmlWriter.WriteEndElement(); // Address - } - - xmlWriter.WriteEndElement(); // Addresses - - xmlWriter.WriteEndElement(); // Customer - } - - xmlWriter.WriteEndElement(); - xmlWriter.WriteEndDocument(); - xmlWriter.Close(); - return stringWriter.ToString(); - } - - #endregion - } - - - public class XmlExportContext - { - public IList ProductTemplates { get; set; } - public IList Languages { get; set; } - public Store Store { get; set; } - } -} diff --git a/src/Libraries/SmartStore.Services/ExportImport/IExportManager.cs b/src/Libraries/SmartStore.Services/ExportImport/IExportManager.cs deleted file mode 100644 index 0c8afdbcfd..0000000000 --- a/src/Libraries/SmartStore.Services/ExportImport/IExportManager.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Xml; -using SmartStore.Core.Domain.Catalog; -using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Orders; -using SmartStore.Services.Catalog; - -namespace SmartStore.Services.ExportImport -{ - /// - /// Export manager interface - /// - public interface IExportManager - { - /// - /// Export manufacturer list to xml - /// - /// Manufacturers - /// Result in XML format - string ExportManufacturersToXml(IList manufacturers); - - /// - /// Export category list to xml - /// - /// Result in XML format - string ExportCategoriesToXml(); - - /// - /// Writes a single product - /// - /// The XML writer - /// The product - /// Context objects - void WriteProductToXml(XmlWriter writer, Product product, XmlExportContext context); - - /// - /// Export product list to XML - /// - /// Stream to write - /// Search context - void ExportProductsToXml(Stream stream, ProductSearchContext searchContext); - - /// - /// Export products to XLSX - /// - /// Stream - /// Products - void ExportProductsToXlsx(Stream stream, IList products); - - /// - /// Export order list to xml - /// - /// Orders - /// Result in XML format - string ExportOrdersToXml(IList orders); - - /// - /// Export orders to XLSX - /// - /// Stream - /// Orders - void ExportOrdersToXlsx(Stream stream, IList orders); - - /// - /// Export customer list to XLSX - /// - /// Stream - /// Customers - void ExportCustomersToXlsx(Stream stream, IList customers); - - /// - /// Export customer list to xml - /// - /// Customers - /// Result in XML format - string ExportCustomersToXml(IList customers); - } -} 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 2dd762b398..0000000000 --- a/src/Libraries/SmartStore.Services/ExportImport/ImportManager.cs +++ /dev/null @@ -1,775 +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; - -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)) - { - 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(); - - // 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"); - } - } - - } - } - } - - 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.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, true); - 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 99ee1e5744..216894b2d2 100644 --- a/src/Libraries/SmartStore.Services/Extensions/NameValueCollectionExtensions.cs +++ b/src/Libraries/SmartStore.Services/Extensions/NameValueCollectionExtensions.cs @@ -13,53 +13,85 @@ 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) - return "product_attribute_{0}_{1}".FormatWith(productAttributeId, attributeId); + return "product_attribute_{0}_{1}".FormatInvariant(productAttributeId, attributeId); else - return "product_attribute_{0}_{1}_{2}_{3}".FormatWith(productId, bundleItemId, productAttributeId, attributeId); + return "product_attribute_{0}_{1}_{2}_{3}".FormatInvariant(productId, bundleItemId, productAttributeId, attributeId); } public static void AddProductAttribute(this NameValueCollection collection, int productAttributeId, int attributeId, int valueId, int productId = 0, int bundleItemId = 0) { 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 - /// Attribute query data items with following structure: Product.Id, ProductAttribute.Id, Product_ProductAttribute_Mapping.Id, ProductVariantAttributeValue.Id + /// 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 enm = 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) - enm = enm.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 itm in enm) + if (attributes != null && attributes.Count > 0) { - string name = AttributeFormatedName(itm[1], itm[2], itm[0]); + var items = attributes.Where(i => i.Count > 3); + + if (productId != 0) + items = items.Where(i => i[0] == productId); - collection.Add(name, itm[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()); + } } } /// Takes selected elements from collection and creates a attribute XML string from it. /// how the name of the controls are formatted. frontend includes productId, backend does not. - public static string CreateSelectedAttributesXml(this NameValueCollection collection, int productId, IList variantAttributes, - IProductAttributeParser productAttributeParser, ILocalizationService localizationService, IDownloadService downloadService, CatalogSettings catalogSettings, - HttpRequestBase request, List warnings, bool formatWithProductId = true, int bundleItemId = 0) + public static string CreateSelectedAttributesXml(this NameValueCollection collection, + int productId, + IEnumerable variantAttributes, + IProductAttributeParser productAttributeParser, + ILocalizationService localizationService, + IDownloadService downloadService, + CatalogSettings catalogSettings, + HttpRequestBase request, List warnings, + bool formatWithProductId = true, + int bundleItemId = 0) { if (collection == null) return ""; @@ -80,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()); } @@ -94,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()); } @@ -142,16 +174,18 @@ public static string CreateSelectedAttributesXml(this NameValueCollection collec var download = downloadService.GetDownloadByGuid(downloadGuid); if (download != null) { + download.IsTransient = false; + downloadService.UpdateDownload(download); selectedAttributes = productAttributeParser.AddProductAttribute(selectedAttributes, attribute, download.DownloadGuid.ToString()); } } else { - var httpPostedFile = request.Files[controlId]; - if (httpPostedFile != null && httpPostedFile.FileName.HasValue()) + var postedFile = request.Files[controlId]; + if (postedFile != null && postedFile.FileName.HasValue()) { int fileMaxSize = catalogSettings.FileUploadMaximumSizeBytes; - if (httpPostedFile.ContentLength > fileMaxSize) + if (postedFile.ContentLength > fileMaxSize) { warnings.Add(string.Format(localizationService.GetResource("ShoppingCart.MaximumUploadedFileSize"), (int)(fileMaxSize / 1024))); } @@ -163,10 +197,10 @@ public static string CreateSelectedAttributesXml(this NameValueCollection collec DownloadGuid = Guid.NewGuid(), UseDownloadUrl = false, DownloadUrl = "", - DownloadBinary = httpPostedFile.GetDownloadBits(), - ContentType = httpPostedFile.ContentType, - Filename = System.IO.Path.GetFileNameWithoutExtension(httpPostedFile.FileName), - Extension = System.IO.Path.GetExtension(httpPostedFile.FileName), + DownloadBinary = postedFile.InputStream.ToByteArray(), + ContentType = postedFile.ContentType, + Filename = System.IO.Path.GetFileNameWithoutExtension(postedFile.FileName), + Extension = System.IO.Path.GetExtension(postedFile.FileName), IsNew = true }; downloadService.InsertDownload(download); diff --git a/src/Libraries/SmartStore.Services/Filter/FilterCriteria.cs b/src/Libraries/SmartStore.Services/Filter/FilterCriteria.cs index 59de0fa9c1..6bbd9756f9 100644 --- a/src/Libraries/SmartStore.Services/Filter/FilterCriteria.cs +++ b/src/Libraries/SmartStore.Services/Filter/FilterCriteria.cs @@ -33,10 +33,14 @@ public class FilterCriteria : IComparable [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public int? ID { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public int? PId { get; set; } + // Metadata public int MatchCount { get; set; } + public int DisplayOrder { get; set; } + public int DisplayOrderValues { get; set; } public bool IsInactive { get; set; } - public int ParentId { get; set; } public string NameLocalized { get; set; } public string ValueLocalized { get; set; } @@ -44,11 +48,13 @@ public string SqlName { get { - if (Entity == "Manufacturer" && !Name.Contains('.')) - return "{0}.{1}".FormatWith(Entity, Name); + if (Entity.IsCaseInsensitiveEqual("Manufacturer") && !Name.Contains('.')) + return "{0}.{1}".FormatInvariant(Entity, Name); + return Name; } } + public bool IsRange { get @@ -59,9 +65,9 @@ public bool IsRange int IComparable.CompareTo(object obj) { - FilterCriteria filter = (FilterCriteria)obj; + var filter = (FilterCriteria)obj; - int compare = string.Compare(this.Entity, filter.Entity, true); + var compare = string.Compare(this.Entity, filter.Entity, true); if (compare == 0) { @@ -72,20 +78,8 @@ 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() { try diff --git a/src/Libraries/SmartStore.Services/Filter/FilterExtensions.cs b/src/Libraries/SmartStore.Services/Filter/FilterExtensions.cs index a466c8adeb..56f0655f26 100644 --- a/src/Libraries/SmartStore.Services/Filter/FilterExtensions.cs +++ b/src/Libraries/SmartStore.Services/Filter/FilterExtensions.cs @@ -17,6 +17,7 @@ private static string FormatPrice(string value) if (value.HasValue()) { decimal d = 0; + if (StringToPrice(value, out d)) return EngineContext.Current.Resolve().FormatPrice(d, true, false); } @@ -35,33 +36,33 @@ public static string ToDescription(this FilterCriteria criteria) string valueLeft, valueRight; criteria.Value.SplitToPair(out valueLeft, out valueRight, "~"); - if (criteria.Entity == FilterService.ShortcutPrice) - return "{0} - {1}".FormatWith(FormatPrice(valueLeft), FormatPrice(valueRight)); + if (criteria.Entity.IsCaseInsensitiveEqual(FilterService.ShortcutPrice)) + return "{0} - {1}".FormatInvariant(FormatPrice(valueLeft), FormatPrice(valueRight)); - return "{0} - {1}".FormatWith(valueLeft, valueRight); + return "{0} - {1}".FormatInvariant(valueLeft, valueRight); } - string value = (criteria.ValueLocalized.HasValue() ? criteria.ValueLocalized : criteria.Value); + 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) - return "≠ {0}".FormatWith(value); + return "≠ {0}".FormatInvariant(value); if (criteria.Operator == FilterOperator.Greater) - return "> {0}".FormatWith(value); + return "> {0}".FormatInvariant(value); if (criteria.Operator == FilterOperator.GreaterEqual) - return "≥ {0}".FormatWith(value); + return "≥ {0}".FormatInvariant(value); if (criteria.Operator == FilterOperator.Less) - return "< {0}".FormatWith(value); + return "< {0}".FormatInvariant(value); if (criteria.Operator == FilterOperator.LessEqual) - return "≤ {0}".FormatWith(value); + return "≤ {0}".FormatInvariant(value); if (criteria.Operator == FilterOperator.Contains) - return "{0} {1}".FormatWith(localize.GetResource("Products.Filter.Contains"), value); + return "{0} {1}".FormatInvariant(localize.GetResource("Products.Filter.Contains"), value); if (criteria.Operator == FilterOperator.StartsWith) - return "{0} {1}".FormatWith(localize.GetResource("Products.Filter.StartsWith"), value); + return "{0} {1}".FormatInvariant(localize.GetResource("Products.Filter.StartsWith"), value); if (criteria.Operator == FilterOperator.EndsWith) - return "{0} {1}".FormatWith(localize.GetResource("Products.Filter.EndsWith"), value); + return "{0} {1}".FormatInvariant(localize.GetResource("Products.Filter.EndsWith"), value); return value; } @@ -132,7 +133,8 @@ public static bool StringToPrice(this string[] range, int index, out decimal res result = 0; if (range != null && index < range.Length) { - string value = range[index].Trim(); + var value = range[index].Trim(); + return StringToPrice(value, out result); } return false; @@ -140,11 +142,11 @@ public static bool StringToPrice(this string[] range, int index, out decimal res public static string GetUrl(this FilterProductContext context, FilterCriteria criteriaAdd = null, FilterCriteria criteriaRemove = null) { - string url = "{0}?pagesize={1}&viewmode={2}".FormatWith(context.Path, context.PageSize, context.ViewMode); + var url = "{0}?pagesize={1}&viewmode={2}".FormatInvariant(context.Path, context.PageSize, context.ViewMode); if (context.OrderBy.HasValue) { - url = "{0}&orderby={1}".FormatWith(url, context.OrderBy.Value); + url = "{0}&orderby={1}".FormatInvariant(url, context.OrderBy.Value); } try @@ -159,10 +161,10 @@ 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}".FormatWith(url, HttpUtility.UrlEncode(JsonConvert.SerializeObject(criterias))); + url = "{0}&filter={1}".FormatInvariant(url, HttpUtility.UrlEncode(JsonConvert.SerializeObject(criterias))); } } catch (Exception exc) @@ -177,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 66eaf99627..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 { @@ -24,7 +24,7 @@ public partial class FilterService : IFilterService private readonly IRepository _productRepository; private readonly IRepository _productCategoryRepository; private readonly ILocalizedEntityService _localizedEntityService; - private readonly ICommonServices _commonServices; + private readonly ICommonServices _services; private IQueryable _products; @@ -34,7 +34,7 @@ public FilterService(IProductService productService, IRepository productRepository, IRepository productCategoryRepository, ILocalizedEntityService localizedEntityService, - ICommonServices commonServices) + ICommonServices services) { _productService = productService; _categoryService = categoryService; @@ -42,7 +42,7 @@ public FilterService(IProductService productService, _productRepository = productRepository; _productCategoryRepository = productCategoryRepository; _localizedEntityService = localizedEntityService; - _commonServices = commonServices; + _services = services; } public static string ShortcutPrice { get { return "_Price"; } } @@ -53,6 +53,7 @@ private string ValidateValue(string value, string alternativeValue) { if (value.HasValue() && !value.IsCaseInsensitiveEqual("null")) return value; + return alternativeValue; } @@ -78,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. @@ -118,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)); @@ -163,10 +164,11 @@ private IQueryable AllProducts(List categoryIds) { if (_products == null) { - var searchContext = new ProductSearchContext() + var searchContext = new ProductSearchContext { + Query = _productRepository.TableUntracked, FeaturedProducts = (_catalogSettings.IncludeFeaturedProductsInNormalLists ? null : (bool?)false), - StoreId = _commonServices.StoreContext.CurrentStoreIdIfMultiStoreMode, + StoreId = _services.StoreContext.CurrentStoreIdIfMultiStoreMode, VisibleIndividuallyOnly = true }; @@ -191,17 +193,6 @@ join x in distinctIds on p.Id equals x _products = _productService.PrepareProductSearchQuery(searchContext); } - - //string.Join(", ", distinctIds.ToList()).Dump(); - - //_products - // .Select(x => new { x.Id, x.Name }) - // .ToList() - // .ForEach(x => { - // "{0} {1}".FormatWith(x.Id, x.Name).Dump(); - // }); - - //_products.ToString().Dump(true); } return _products; } @@ -262,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(); @@ -290,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 @@ -310,27 +313,55 @@ from a in attributes Name = g.FirstOrDefault().SpecificationAttribute.Name, Value = g.FirstOrDefault().Name, ID = g.Key.Id, - ParentId = g.FirstOrDefault().SpecificationAttribute.Id, - MatchCount = g.Count() + PId = g.FirstOrDefault().SpecificationAttribute.Id, + 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 = _commonServices.WorkContext.WorkingLanguage.Id; - - lst.ForEach(c => + criterias.ForEach(c => { c.Entity = ShortcutSpecAttribute; c.IsInactive = true; - if (c.ParentId != 0) - c.NameLocalized = _localizedEntityService.GetLocalizedValue(languageId, c.ParentId, "SpecificationAttribute", "Name"); + if (c.PId.HasValue) + c.NameLocalized = _localizedEntityService.GetLocalizedValue(languageId, c.PId.Value, "SpecificationAttribute", "Name"); if (c.ID.HasValue) 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) @@ -352,12 +383,13 @@ public virtual string Serialize(List criteria) //criteria.FindAll(c => c.Type.IsNullOrEmpty()).ForEach(c => c.Type = _defaultType); if (criteria != null && criteria.Count > 0) return JsonConvert.SerializeObject(criteria); + return ""; } public virtual FilterProductContext CreateFilterProductContext(string filter, int categoryID, string path, int? pagesize, int? orderby, string viewmode) { - var context = new FilterProductContext() + var context = new FilterProductContext { Filter = filter, ParentCategoryID = categoryID, @@ -371,9 +403,18 @@ 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.IsCaseInsensitiveEqual(ShortcutSpecAttribute))) + { + if (criteria.PId.HasValue) + criteria.NameLocalized = _localizedEntityService.GetLocalizedValue(languageId, criteria.PId.Value, "SpecificationAttribute", "Name"); + + if (criteria.ID.HasValue) + criteria.ValueLocalized = _localizedEntityService.GetLocalizedValue(languageId, criteria.ID.Value, "SpecificationAttributeOption", "Name"); } return context; @@ -458,6 +499,7 @@ public virtual bool ToWhereClause(FilterSql context, List findIn context.Criteria.Clear(); // ! context.Criteria = findIn.FindAll(match); + return ToWhereClause(context); } @@ -467,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 @@ -487,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 @@ -497,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(); @@ -552,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)); @@ -568,18 +610,18 @@ 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); } // filters WITHOUT the multiple selectable filters - string excludedFilter = Serialize(context.Criteria); + var excludedFilter = Serialize(context.Criteria); // filters WITH the multiple selectable filters (required for highlighting selected values) context.Criteria = Deserialize(context.Filter); diff --git a/src/Libraries/SmartStore.Services/Forums/ForumService.cs b/src/Libraries/SmartStore.Services/Forums/ForumService.cs index 4f5ca20ce4..5871f34ab6 100644 --- a/src/Libraries/SmartStore.Services/Forums/ForumService.cs +++ b/src/Libraries/SmartStore.Services/Forums/ForumService.cs @@ -41,11 +41,9 @@ public partial class ForumService : IForumService private readonly ICacheManager _cacheManager; private readonly IGenericAttributeService _genericAttributeService; private readonly ICustomerService _customerService; - private readonly IWorkContext _workContext; private readonly IWorkflowMessageService _workflowMessageService; - private readonly IEventPublisher _eventPublisher; - private readonly IStoreContext _storeContext; private readonly IRepository _storeMappingRepository; + private readonly ICommonServices _services; #endregion @@ -62,11 +60,9 @@ public ForumService(ICacheManager cacheManager, IRepository customerRepository, IGenericAttributeService genericAttributeService, ICustomerService customerService, - IWorkContext workContext, IWorkflowMessageService workflowMessageService, - IEventPublisher eventPublisher, - IStoreContext storeContext, - IRepository storeMappingRepository) + IRepository storeMappingRepository, + ICommonServices services) { _cacheManager = cacheManager; _forumGroupRepository = forumGroupRepository; @@ -79,11 +75,9 @@ public ForumService(ICacheManager cacheManager, _customerRepository = customerRepository; _genericAttributeService = genericAttributeService; _customerService = customerService; - _workContext = workContext; _workflowMessageService = workflowMessageService; - _eventPublisher = eventPublisher; - _storeContext = storeContext; _storeMappingRepository = storeMappingRepository; + _services = services; } public DbQuerySettings QuerySettings { get; set; } @@ -263,7 +257,7 @@ public virtual void DeleteForumGroup(ForumGroup forumGroup) _cacheManager.RemoveByPattern(FORUM_PATTERN_KEY); //event notification - _eventPublisher.EntityDeleted(forumGroup); + _services.EventPublisher.EntityDeleted(forumGroup); } /// @@ -292,7 +286,7 @@ public virtual IList GetAllForumGroups(bool showHidden = false) if (!showHidden && !QuerySettings.IgnoreMultiStore) { - var currentStoreId = _storeContext.CurrentStore.Id; + var currentStoreId = _services.StoreContext.CurrentStore.Id; query = from fg in query @@ -332,7 +326,7 @@ public virtual void InsertForumGroup(ForumGroup forumGroup) _cacheManager.RemoveByPattern(FORUM_PATTERN_KEY); //event notification - _eventPublisher.EntityInserted(forumGroup); + _services.EventPublisher.EntityInserted(forumGroup); } /// @@ -353,7 +347,7 @@ public virtual void UpdateForumGroup(ForumGroup forumGroup) _cacheManager.RemoveByPattern(FORUM_PATTERN_KEY); //event notification - _eventPublisher.EntityUpdated(forumGroup); + _services.EventPublisher.EntityUpdated(forumGroup); } /// @@ -378,7 +372,7 @@ where queryTopicIds.Contains(fs.TopicId) { _forumSubscriptionRepository.Delete(fs); //event notification - _eventPublisher.EntityDeleted(fs); + _services.EventPublisher.EntityDeleted(fs); } //delete forum subscriptions (forum) @@ -389,7 +383,7 @@ where queryTopicIds.Contains(fs.TopicId) { _forumSubscriptionRepository.Delete(fs2); //event notification - _eventPublisher.EntityDeleted(fs2); + _services.EventPublisher.EntityDeleted(fs2); } //delete forum @@ -399,7 +393,7 @@ where queryTopicIds.Contains(fs.TopicId) _cacheManager.RemoveByPattern(FORUM_PATTERN_KEY); //event notification - _eventPublisher.EntityDeleted(forum); + _services.EventPublisher.EntityDeleted(forum); } /// @@ -451,7 +445,7 @@ public virtual void InsertForum(Forum forum) _cacheManager.RemoveByPattern(FORUM_PATTERN_KEY); //event notification - _eventPublisher.EntityInserted(forum); + _services.EventPublisher.EntityInserted(forum); } /// @@ -471,7 +465,7 @@ public virtual void UpdateForum(Forum forum) _cacheManager.RemoveByPattern(FORUM_PATTERN_KEY); //event notification - _eventPublisher.EntityUpdated(forum); + _services.EventPublisher.EntityUpdated(forum); } /// @@ -500,7 +494,7 @@ public virtual void DeleteTopic(ForumTopic forumTopic) { _forumSubscriptionRepository.Delete(fs); //event notification - _eventPublisher.EntityDeleted(fs); + _services.EventPublisher.EntityDeleted(fs); } //update stats @@ -511,7 +505,7 @@ public virtual void DeleteTopic(ForumTopic forumTopic) _cacheManager.RemoveByPattern(FORUM_PATTERN_KEY); //event notification - _eventPublisher.EntityDeleted(forumTopic); + _services.EventPublisher.EntityDeleted(forumTopic); } /// @@ -613,7 +607,7 @@ from ft in _forumTopicRepository.Table if (!QuerySettings.IgnoreMultiStore) { - var currentStoreId = _storeContext.CurrentStore.Id; + var currentStoreId = _services.StoreContext.CurrentStore.Id; query = from ft in query @@ -659,14 +653,14 @@ public virtual void InsertTopic(ForumTopic forumTopic, bool sendNotifications) _cacheManager.RemoveByPattern(FORUM_PATTERN_KEY); //event notification - _eventPublisher.EntityInserted(forumTopic); + _services.EventPublisher.EntityInserted(forumTopic); //send notifications if (sendNotifications) { var forum = forumTopic.Forum; var subscriptions = GetAllSubscriptions(0, forum.Id, 0, 0, int.MaxValue); - var languageId = _workContext.WorkingLanguage.Id; + var languageId = _services.WorkContext.WorkingLanguage.Id; foreach (var subscription in subscriptions) { @@ -700,7 +694,7 @@ public virtual void UpdateTopic(ForumTopic forumTopic) _cacheManager.RemoveByPattern(FORUM_PATTERN_KEY); //event notification - _eventPublisher.EntityUpdated(forumTopic); + _services.EventPublisher.EntityUpdated(forumTopic); } /// @@ -715,7 +709,7 @@ public virtual ForumTopic MoveTopic(int forumTopicId, int newForumId) if (forumTopic == null) return forumTopic; - if (this.IsCustomerAllowedToMoveTopic(_workContext.CurrentCustomer, forumTopic)) + if (this.IsCustomerAllowedToMoveTopic(_services.WorkContext.CurrentCustomer, forumTopic)) { int previousForumId = forumTopic.ForumId; var newForum = GetForumById(newForumId); @@ -783,7 +777,7 @@ public virtual void DeletePost(ForumPost forumPost) _cacheManager.RemoveByPattern(FORUM_PATTERN_KEY); //event notification - _eventPublisher.EntityDeleted(forumPost); + _services.EventPublisher.EntityDeleted(forumPost); } @@ -887,7 +881,7 @@ public virtual void InsertPost(ForumPost forumPost, bool sendNotifications) _cacheManager.RemoveByPattern(FORUM_PATTERN_KEY); //event notification - _eventPublisher.EntityInserted(forumPost); + _services.EventPublisher.EntityInserted(forumPost); //notifications if (sendNotifications) @@ -895,7 +889,7 @@ public virtual void InsertPost(ForumPost forumPost, bool sendNotifications) var forum = forumTopic.Forum; var subscriptions = GetAllSubscriptions(0, 0, forumTopic.Id, 0, int.MaxValue); - var languageId = _workContext.WorkingLanguage.Id; + var languageId = _services.WorkContext.WorkingLanguage.Id; int friendlyTopicPageIndex = CalculateTopicPageIndex(forumPost.TopicId, _forumSettings.PostsPageSize > 0 ? _forumSettings.PostsPageSize : 10, forumPost.Id) + 1; @@ -932,7 +926,7 @@ public virtual void UpdatePost(ForumPost forumPost) _cacheManager.RemoveByPattern(FORUM_PATTERN_KEY); //event notification - _eventPublisher.EntityUpdated(forumPost); + _services.EventPublisher.EntityUpdated(forumPost); } /// @@ -949,7 +943,7 @@ public virtual void DeletePrivateMessage(PrivateMessage privateMessage) _forumPrivateMessageRepository.Delete(privateMessage); //event notification - _eventPublisher.EntityDeleted(privateMessage); + _services.EventPublisher.EntityDeleted(privateMessage); } /// @@ -1028,7 +1022,7 @@ public virtual void InsertPrivateMessage(PrivateMessage privateMessage) _forumPrivateMessageRepository.Insert(privateMessage); //event notification - _eventPublisher.EntityInserted(privateMessage); + _services.EventPublisher.EntityInserted(privateMessage); var customerTo = _customerService.GetCustomerById(privateMessage.ToCustomerId); if (customerTo == null) @@ -1042,7 +1036,7 @@ public virtual void InsertPrivateMessage(PrivateMessage privateMessage) //Email notification if (_forumSettings.NotifyAboutPrivateMessages) { - _workflowMessageService.SendPrivateMessageNotification(customerTo, privateMessage, _workContext.WorkingLanguage.Id); + _workflowMessageService.SendPrivateMessageNotification(customerTo, privateMessage, _services.WorkContext.WorkingLanguage.Id); } } @@ -1059,13 +1053,13 @@ public virtual void UpdatePrivateMessage(PrivateMessage privateMessage) { _forumPrivateMessageRepository.Delete(privateMessage); //event notification - _eventPublisher.EntityDeleted(privateMessage); + _services.EventPublisher.EntityDeleted(privateMessage); } else { _forumPrivateMessageRepository.Update(privateMessage); //event notification - _eventPublisher.EntityUpdated(privateMessage); + _services.EventPublisher.EntityUpdated(privateMessage); } } @@ -1083,7 +1077,7 @@ public virtual void DeleteSubscription(ForumSubscription forumSubscription) _forumSubscriptionRepository.Delete(forumSubscription); //event notification - _eventPublisher.EntityDeleted(forumSubscription); + _services.EventPublisher.EntityDeleted(forumSubscription); } /// @@ -1151,7 +1145,7 @@ public virtual void InsertSubscription(ForumSubscription forumSubscription) _forumSubscriptionRepository.Insert(forumSubscription); //event notification - _eventPublisher.EntityInserted(forumSubscription); + _services.EventPublisher.EntityInserted(forumSubscription); } /// @@ -1168,7 +1162,7 @@ public virtual void UpdateSubscription(ForumSubscription forumSubscription) _forumSubscriptionRepository.Update(forumSubscription); //event notification - _eventPublisher.EntityUpdated(forumSubscription); + _services.EventPublisher.EntityUpdated(forumSubscription); } /// @@ -1477,7 +1471,7 @@ public virtual int CalculateTopicPageIndex(int forumTopicId, int pageSize, int p return pageIndex; } - + #endregion } } diff --git a/src/Libraries/SmartStore.Services/Hooks/SoftDeletablePreUpdateHook.cs b/src/Libraries/SmartStore.Services/Hooks/SoftDeletablePreUpdateHook.cs index 129ca13338..82e9a734cd 100644 --- a/src/Libraries/SmartStore.Services/Hooks/SoftDeletablePreUpdateHook.cs +++ b/src/Libraries/SmartStore.Services/Hooks/SoftDeletablePreUpdateHook.cs @@ -27,7 +27,6 @@ public override void Hook(ISoftDeletable entity, HookEntityMetadata metadata) return; var dbContext = _ctx.Resolve(); - var autoCommitEnabled = false; var modifiedProps = dbContext.GetModifiedProperties(baseEntity); if (!modifiedProps.ContainsKey("Deleted")) @@ -35,47 +34,41 @@ public override void Hook(ISoftDeletable entity, HookEntityMetadata metadata) var entityType = baseEntity.GetUnproxiedType(); - // mark orphaned ACL records as idle - var aclSupported = baseEntity as IAclSupported; - if (aclSupported != null && aclSupported.SubjectToAcl) + using (var scope = new DbContextScope(ctx: dbContext, autoCommit: false)) { - var shouldSetIdle = entity.Deleted; - - var rsAclRecord = _ctx.Resolve>(); - autoCommitEnabled = rsAclRecord.AutoCommitEnabled; - rsAclRecord.AutoCommitEnabled = false; - - var aclService = _ctx.Resolve(); - var records = aclService.GetAclRecordsFor(entityType.Name, baseEntity.Id); - foreach (var record in records) + // mark orphaned ACL records as idle + var aclSupported = baseEntity as IAclSupported; + if (aclSupported != null && aclSupported.SubjectToAcl) { - record.IsIdle = shouldSetIdle; - aclService.UpdateAclRecord(record); - } + var shouldSetIdle = entity.Deleted; + var rsAclRecord = _ctx.Resolve>(); - rsAclRecord.AutoCommitEnabled = autoCommitEnabled; - } + var aclService = _ctx.Resolve(); + var records = aclService.GetAclRecordsFor(entityType.Name, baseEntity.Id); + foreach (var record in records) + { + record.IsIdle = shouldSetIdle; + aclService.UpdateAclRecord(record); + } + } - // Delete orphaned inactive UrlRecords. - // We keep the active ones on purpose in order to be able to fully restore a soft deletable entity once we implemented the "recycle bin" feature - var slugSupported = baseEntity as ISlugSupported; - if (slugSupported != null && entity.Deleted) - { - var rsUrlRecord = _ctx.Resolve>(); - autoCommitEnabled = rsUrlRecord.AutoCommitEnabled; - rsUrlRecord.AutoCommitEnabled = false; - - var urlRecordService = _ctx.Resolve(); - var activeRecords = urlRecordService.GetUrlRecordsFor(entityType.Name, baseEntity.Id); - foreach (var record in activeRecords) + // Delete orphaned inactive UrlRecords. + // We keep the active ones on purpose in order to be able to fully restore a soft deletable entity once we implemented the "recycle bin" feature + var slugSupported = baseEntity as ISlugSupported; + if (slugSupported != null && entity.Deleted) { - if (!record.IsActive) + var rsUrlRecord = _ctx.Resolve>(); + + var urlRecordService = _ctx.Resolve(); + var activeRecords = urlRecordService.GetUrlRecordsFor(entityType.Name, baseEntity.Id); + foreach (var record in activeRecords) { - urlRecordService.DeleteUrlRecord(record); + if (!record.IsActive) + { + urlRecordService.DeleteUrlRecord(record); + } } } - - rsUrlRecord.AutoCommitEnabled = autoCommitEnabled; } } 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 872e2d7474..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,180 +419,272 @@ public virtual string ExportResourcesToXml(Language language) return stringWriter.ToString(); } - /// - /// Import language resources from XML file - /// - /// Language - /// XML document - /// Prefix for resource key name - public virtual void ImportResourcesFromXml( - Language language, - XmlDocument xmlDocument, - string rootKey = null, - bool sourceIsPlugin = false, - ImportModeFlags mode = ImportModeFlags.Insert | ImportModeFlags.Update, - bool updateTouchedResources = false) - { - var autoCommit = _lsrRepository.AutoCommitEnabled; - var validateOnSave = _lsrRepository.Context.ValidateOnSaveEnabled; - var autoDetectChanges = _lsrRepository.Context.AutoDetectChangesEnabled; - var proxyCreation = _lsrRepository.Context.ProxyCreationEnabled; + 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")); - try - { - _lsrRepository.Context.ValidateOnSaveEnabled = false; - _lsrRepository.Context.AutoDetectChangesEnabled = false; - _lsrRepository.Context.ProxyCreationEnabled = false; + if (!directory.Exists) + return; - var toAdd = new List(); - var toUpdate = new List(); - var nodes = xmlDocument.SelectNodes(@"//Language/LocaleResource"); + if (targetList == null && updateTouchedResources) + { + DeleteLocaleStringResources(pluginDescriptor.ResourceRootKey); + } - foreach (var xel in nodes.Cast()) - { + var unprocessedLanguages = new List(); - string name = xel.GetAttribute("Name").TrimSafe(); - string value = ""; - var valueNode = xel.SelectSingleNode("Value"); - if (valueNode != null) - value = valueNode.InnerText; + var defaultLanguageId = _languageService.GetDefaultLanguageId(); + var languages = filterLanguages ?? _languageService.GetAllLanguages(true); - if (String.IsNullOrEmpty(name)) - continue; + 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 (rootKey.HasValue()) - { - if (!xel.GetAttributeText("AppendRootKey").IsCaseInsensitiveEqual("false")) - name = "{0}.{1}".FormatWith(rootKey, name); - } + 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); + } + } + } + } - // 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) - { - if (mode.IsSet(ImportModeFlags.Update)) - { - if (updateTouchedResources || !resource.IsTouched.GetValueOrDefault()) - { - resource.ResourceValue = value; - resource.IsTouched = null; - toUpdate.Add(resource); - } - } - } - else - { - if (mode.IsSet(ImportModeFlags.Insert)) - { - toAdd.Add( - new LocaleStringResource() - { - LanguageId = language.Id, - ResourceName = name, - ResourceValue = value, - IsFromPlugin = sourceIsPlugin - }); - } - } - } + /// + /// Resolves a resource file for the specified language and processes the import + /// + /// Language + /// 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"; - _lsrRepository.AutoCommitEnabled = true; - _lsrRepository.InsertRange(toAdd, 500); - toAdd.Clear(); + var codeCandidates = GetResourceFileCodeCandidates( + fileCode ?? language.LanguageCulture, + directory, + canFallBackToAnyResourceFile); - _lsrRepository.AutoCommitEnabled = false; - toUpdate.Each(x => - { - _lsrRepository.Update(x); - }); - - _lsrRepository.Context.SaveChanges(); - toUpdate.Clear(); + string path = null; + string code = null; - //clear cache - _cacheManager.RemoveByPattern(LOCALESTRINGRESOURCES_PATTERN_KEY); - } - catch (Exception ex) - { - throw ex; - } - finally - { - _lsrRepository.AutoCommitEnabled = autoCommit; - _lsrRepository.Context.ValidateOnSaveEnabled = validateOnSave; - _lsrRepository.Context.AutoDetectChangesEnabled = autoDetectChanges; - _lsrRepository.Context.ProxyCreationEnabled = proxyCreation; - } + 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(); - /// - /// 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"); + doc.Load(path); + doc = FlattenResourceFile(doc); - if (!System.IO.Directory.Exists(localizationDir)) - return; + 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 (forceToList == null && updateTouchedResources) - DeleteLocaleStringResources(pluginDescriptor.ResourceRootKey); + if (res.ResourceName.HasValue()) + { + targetList.Add(res); + } + } + } + } - var languages = _languageService.GetAllLanguages(true); - var doc = new XmlDocument(); + return code; + } - foreach (var filePath in System.IO.Directory.EnumerateFiles(localizationDir, "*.xml")) + 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) { - Match match = Regex.Match(Path.GetFileName(filePath), Regex.Escape("resources.") + "(.*?)" + Regex.Escape(".xml")); - string languageCode = match.Groups[1].Value; + code = ci.Parent.Name; + yield return code; + } + + var rgFileName = new Regex("^resources.(.+?).xml$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - Language language = languages.Where(l => l.LanguageCulture.IsCaseInsensitiveEqual(languageCode)).FirstOrDefault(); - if (language != null) + // 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)) { - language = _languageService.GetLanguageById(language.Id); + yield return code; + yield break; } + } - if (languageCode.HasValue() && language != null) + if (canFallBackToAnyResourceFile) + { + foreach (var fi in directory.EnumerateFiles("resources.*.xml", SearchOption.TopDirectoryOnly)) { - if (filterLanguages != null && !filterLanguages.Any(x => x.Id == language.Id)) + 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, + bool sourceIsPlugin = false, + ImportModeFlags mode = ImportModeFlags.Insert | ImportModeFlags.Update, + bool updateTouchedResources = 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"); + if (valueNode != null) + value = valueNode.InnerText; + + if (String.IsNullOrEmpty(name)) continue; + + if (rootKey.HasValue()) + { + if (!xel.GetAttributeText("AppendRootKey").IsCaseInsensitiveEqual("false")) + name = "{0}.{1}".FormatWith(rootKey, name); } - doc.Load(filePath); - doc = FlattenResourceFile(doc); + resource = null; - if (forceToList == 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 (resources.TryGetValue(name, out resource)) { - ImportResourcesFromXml(language, doc, pluginDescriptor.ResourceRootKey, true, updateTouchedResources: updateTouchedResources); + if (mode.HasFlag(ImportModeFlags.Update)) + { + if (updateTouchedResources || !resource.IsTouched.GetValueOrDefault()) + { + if (value != resource.ResourceValue) + { + resource.ResourceValue = value; + resource.IsTouched = null; + toUpdate.Add(resource); + } + } + } } else { - var nodes = doc.SelectNodes(@"//Language/LocaleResource"); - foreach (XmlNode node in nodes) + if (mode.HasFlag(ImportModeFlags.Insert)) { - 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); + toAdd.Add( + new LocaleStringResource + { + LanguageId = language.Id, + ResourceName = name, + ResourceValue = value, + IsFromPlugin = sourceIsPlugin + }); } } } + + //_lsrRepository.AutoCommitEnabled = true; + + if (toAdd.Any() || toUpdate.Any()) + { + _lsrRepository.InsertRange(toAdd); + toAdd.Clear(); + + _lsrRepository.UpdateRange(toUpdate); + toUpdate.Clear(); + + int num = _lsrRepository.Context.SaveChanges(); + + // clear cache + _cacheManager.RemoveByPattern(LOCALESTRINGRESOURCES_PATTERN_KEY); + + return num; + } + + return 0; } } diff --git a/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs b/src/Libraries/SmartStore.Services/Localization/LocalizedEntityService.cs index 0aad0675e5..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,104 +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_ENTITYID_KEY = "SmartStore.localizedproperty.entityid-{0}-{1}-{2}"; - private const string LOCALIZEDPROPERTY_PATTERN_KEY = "SmartStore.localizedproperty."; - - #endregion - - #region Fields + private const string LOCALIZEDPROPERTY_ALL_KEY = "SmartStore.localizedproperty.all"; 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) + 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 key = string.Format(LOCALIZEDPROPERTY_KEY, languageId, entityId, localeKeyGroup, localeKey); - return _cacheManager.Get(key, () => - { - var query = from lp in _localizedPropertyRepository.Table - where lp.EntityId == entityId && - lp.LocaleKey == localeKey && - lp.LocaleKeyGroup == localeKeyGroup && - lp.LanguageId == languageId - select lp.LocaleValue; - var localeValue = query.FirstOrDefault(); - //little hack here. nulls aren't cacheable so set it to "" - if (localeValue == null) - localeValue = ""; - return localeValue; - }); - } + protected virtual ConcurrentDictionary GetAllProperties() + { + var result = _cacheManager.Get(LOCALIZEDPROPERTY_ALL_KEY, () => + { + if (_localizationSettings.LoadAllLocalizedPropertiesOnStartup) + { + var props = _localizedPropertyRepository.TableUntracked.ToDictionarySafe( + x => GenerateKey(x.LanguageId, x.LocaleKeyGroup, x.LocaleKey, x.EntityId), + x => x.LocaleValue.EmptyNull()); + return new ConcurrentDictionary(props); + } + else + { + return new ConcurrentDictionary(); + } + }); + + return result; + } + + 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 + { + 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 + 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 @@ -120,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; - _localizedPropertyRepository.Insert(localizedProperty); + return query.FirstOrDefault(); + } - //cache - _cacheManager.RemoveByPattern(LOCALIZEDPROPERTY_PATTERN_KEY); + public virtual void InsertLocalizedProperty(LocalizedProperty localizedProperty) + { + Guard.ArgumentNotNull(() => localizedProperty); + + 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"); - - _localizedPropertyRepository.Update(localizedProperty); - - //cache - _cacheManager.RemoveByPattern(LOCALIZEDPROPERTY_PATTERN_KEY); - } - - /// - /// Save localized value - /// - /// Type - /// Entity - /// Key selector - /// Locale value - /// Language ID - public virtual void SaveLocalizedValue(T entity, + Guard.ArgumentNotNull(() => localizedProperty); + + try + { + // db + _localizedPropertyRepository.Update(localizedProperty); + + // cache + var key = GenerateKey(localizedProperty); + var val = localizedProperty.LocaleValue.EmptyNull(); + GetAllProperties().AddOrUpdate( + key, + val, + (k, v) => val); + } + catch { } + } + + 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; + + var localizedProperty = _localizedPropertyRepository.GetById(localizedPropertyId); + return localizedProperty; + } + + public virtual void SaveLocalizedValue( + T entity, Expression> keySelector, string localeValue, int languageId) where T : BaseEntity, ILocalizedEntity @@ -170,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) @@ -200,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, @@ -238,6 +253,14 @@ public virtual void SaveLocalizedValue(T entity, } } - #endregion + 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); + } } } \ 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 d952288f74..8387de3969 100644 --- a/src/Libraries/SmartStore.Services/Logging/DefaultLogger.cs +++ b/src/Libraries/SmartStore.Services/Logging/DefaultLogger.cs @@ -7,6 +7,7 @@ using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Logging; using SmartStore.Core.Logging; +using SmartStore.Data; namespace SmartStore.Services.Logging { @@ -22,7 +23,7 @@ public partial class DefaultLogger : ILogger private readonly IRepository _logRepository; private readonly IWebHelper _webHelper; private readonly IDbContext _dbContext; - private readonly IDataProvider _dataProvider; + private readonly IWorkContext _workContext; private readonly IList _entries = new List(); @@ -30,12 +31,12 @@ public partial class DefaultLogger : ILogger #region Ctor - public DefaultLogger(IRepository logRepository, IWebHelper webHelper, IDbContext dbContext, IDataProvider dataProvider) + public DefaultLogger(IRepository logRepository, IWebHelper webHelper, IDbContext dbContext, IWorkContext workContext) { this._logRepository = logRepository; this._webHelper = webHelper; this._dbContext = dbContext; - this._dataProvider = dataProvider; + this._workContext = workContext; } #endregion @@ -93,14 +94,7 @@ public virtual void ClearLog() } } - if (DataSettings.Current.IsSqlServer) - { - try - { - _dbContext.ExecuteSqlCommand("DBCC SHRINKDATABASE(0)", true); - } - catch { } - } + _dbContext.ShrinkDatabase(); } public virtual void ClearLog(DateTime toUtc, LogLevel logLevel) @@ -115,10 +109,7 @@ public virtual void ClearLog(DateTime toUtc, LogLevel logLevel) break; } - if (DataSettings.Current.IsSqlServer) - { - _dbContext.ExecuteSqlCommand("DBCC SHRINKDATABASE(0)", true); - } + _dbContext.ShrinkDatabase(); } catch { } } @@ -128,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; @@ -138,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); @@ -208,6 +200,7 @@ public void Flush() string ipAddress = ""; string pageUrl = ""; string referrerUrl = ""; + var currentCustomer = _workContext.CurrentCustomer; try { @@ -217,9 +210,7 @@ public void Flush() } catch { } - _logRepository.AutoCommitEnabled = false; - - using (var scope = new DbContextScope(autoDetectChanges: false, proxyCreation: false, validateOnSave: false)) + using (var scope = new DbContextScope(autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) { foreach (var context in _entries) { @@ -257,7 +248,7 @@ public void Flush() ShortMessage = shortMessage, FullMessage = fullMessage, IpAddress = ipAddress, - Customer = context.Customer, + Customer = context.Customer ?? currentCustomer, PageUrl = pageUrl, ReferrerUrl = referrerUrl, CreatedOnUtc = DateTime.UtcNow, @@ -273,7 +264,7 @@ public void Flush() log.LogLevel = context.LogLevel; log.IpAddress = ipAddress; - log.Customer = context.Customer; + log.Customer = context.Customer ?? currentCustomer; log.PageUrl = pageUrl; log.ReferrerUrl = referrerUrl; log.UpdatedOnUtc = DateTime.UtcNow; @@ -295,8 +286,6 @@ public void Flush() catch { } } - _logRepository.AutoCommitEnabled = true; - _entries.Clear(); } diff --git a/src/Libraries/SmartStore.Services/Media/DownloadService.cs b/src/Libraries/SmartStore.Services/Media/DownloadService.cs index 71117a3bcd..b7f03930e8 100644 --- a/src/Libraries/SmartStore.Services/Media/DownloadService.cs +++ b/src/Libraries/SmartStore.Services/Media/DownloadService.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; +using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Media; @@ -23,13 +26,7 @@ public partial class DownloadService : IDownloadService #region Ctor - /// - /// Ctor - /// - /// Download repository - /// - public DownloadService(IRepository downloadRepository, - IEventPublisher eventPubisher) + public DownloadService(IRepository downloadRepository, IEventPublisher eventPubisher) { _downloadRepository = downloadRepository; _eventPubisher = eventPubisher; @@ -39,11 +36,6 @@ public DownloadService(IRepository downloadRepository, #region Methods - /// - /// Gets a download - /// - /// Download identifier - /// Download public virtual Download GetDownloadById(int downloadId) { if (downloadId == 0) @@ -53,11 +45,25 @@ public virtual Download GetDownloadById(int downloadId) return download; } - /// - /// Gets a download by GUID - /// - /// Download GUID - /// Download + public virtual IList GetDownloadsByIds(int[] downloadIds) + { + if (downloadIds == null || downloadIds.Length == 0) + return new List(); + + var query = from dl in _downloadRepository.Table + where downloadIds.Contains(dl.Id) + select dl; + + var downloads = query.ToList(); + + // sort by passed identifier sequence + var sortQuery = from i in downloadIds + join d in downloads on i equals d.Id + select d; + + return sortQuery.ToList(); + } + public virtual Download GetDownloadByGuid(Guid downloadGuid) { if (downloadGuid == Guid.Empty) @@ -70,10 +76,6 @@ public virtual Download GetDownloadByGuid(Guid downloadGuid) return order; } - /// - /// Deletes a download - /// - /// Download public virtual void DeleteDownload(Download download) { if (download == null) @@ -84,39 +86,28 @@ public virtual void DeleteDownload(Download download) _eventPubisher.EntityDeleted(download); } - /// - /// Inserts a download - /// - /// Download public virtual void InsertDownload(Download download) { if (download == null) throw new ArgumentNullException("download"); + download.UpdatedOnUtc = DateTime.UtcNow; _downloadRepository.Insert(download); _eventPubisher.EntityInserted(download); } - /// - /// Updates the download - /// - /// Download public virtual void UpdateDownload(Download download) { if (download == null) throw new ArgumentNullException("download"); + download.UpdatedOnUtc = DateTime.UtcNow; _downloadRepository.Update(download); _eventPubisher.EntityUpdated(download); } - /// - /// Gets a value indicating whether download is allowed - /// - /// Order item to check - /// True if download is allowed; otherwise, false. public virtual bool IsDownloadAllowed(OrderItem orderItem) { if (orderItem == null) @@ -182,11 +173,6 @@ public virtual bool IsDownloadAllowed(OrderItem orderItem) return false; } - /// - /// Gets a value indicating whether license download is allowed - /// - /// Order item to check - /// True if license download is allowed; otherwise, false. public virtual bool IsLicenseDownloadAllowed(OrderItem orderItem) { if (orderItem == null) diff --git a/src/Libraries/SmartStore.Services/Media/Extensions.cs b/src/Libraries/SmartStore.Services/Media/Extensions.cs deleted file mode 100644 index fcc6269049..0000000000 --- a/src/Libraries/SmartStore.Services/Media/Extensions.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.IO; -using System.Web; - -namespace SmartStore.Services.Media -{ - /// - /// Extensions - /// - public static class Extensions - { - /// - /// Gets the download binary array - /// - /// Posted file - /// Download binary array - public static byte[] GetDownloadBits(this HttpPostedFileBase postedFile) - { - Stream fs = postedFile.InputStream; - int size = postedFile.ContentLength; - byte[] binary = new byte[size]; - fs.Read(binary, 0, size); - return binary; - } - - /// - /// Gets the picture binary array - /// - /// Posted file - /// Picture binary array - public static byte[] GetPictureBits(this HttpPostedFileBase postedFile) - { - Stream fs = postedFile.InputStream; - int size = postedFile.ContentLength; - byte[] img = new byte[size]; - fs.Read(img, 0, size); - return img; - } - } -} diff --git a/src/Libraries/SmartStore.Services/Media/IDownloadService.cs b/src/Libraries/SmartStore.Services/Media/IDownloadService.cs index 73afed9a32..80faf7f50a 100644 --- a/src/Libraries/SmartStore.Services/Media/IDownloadService.cs +++ b/src/Libraries/SmartStore.Services/Media/IDownloadService.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using SmartStore.Core; using SmartStore.Core.Domain.Media; using SmartStore.Core.Domain.Orders; @@ -16,6 +19,13 @@ public partial interface IDownloadService /// Download Download GetDownloadById(int downloadId); + /// + /// Gets downloads by identifiers + /// + /// Download identifiers + /// List of download entities + IList GetDownloadsByIds(int[] downloadIds); + /// /// Gets a download by GUID /// diff --git a/src/Libraries/SmartStore.Services/Media/IPictureService.cs b/src/Libraries/SmartStore.Services/Media/IPictureService.cs index f86c307925..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 @@ -137,16 +136,24 @@ string GetPictureUrl(Picture picture, /// Pictures IList GetPicturesByProductId(int productId, int recordsToReturn = 0); - /// - /// Inserts a picture - /// - /// The picture binary - /// The picture MIME type - /// The SEO filename - /// A value indicating whether the picture is new - /// A value indicating whether to validated provided picture binary - /// Picture - Picture InsertPicture(byte[] pictureBinary, string mimeType, string seoFilename, bool isNew, bool validateBinary = true); + /// + /// Gets pictures by picture identifier + /// + /// Picture identifier + /// Pictures + IList GetPicturesByIds(int[] pictureIds); + + /// + /// Inserts a picture + /// + /// The picture binary + /// The picture MIME type + /// The SEO filename + /// A value indicating whether the picture is new + /// A value indicating whether the picture is initially in transient state + /// A value indicating whether to validated provided picture binary + /// Picture + Picture InsertPicture(byte[] pictureBinary, string mimeType, string seoFilename, bool isNew, bool isTransient = true, bool validateBinary = true); /// /// Updates the picture diff --git a/src/Libraries/SmartStore.Services/Media/MediaExtensions.cs b/src/Libraries/SmartStore.Services/Media/MediaExtensions.cs new file mode 100644 index 0000000000..b776fd8de7 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/MediaExtensions.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using SmartStore.Core; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.Infrastructure; + +namespace SmartStore.Services.Media +{ + + public static class MediaHelper + { + + public static void UpdateDownloadTransientStateFor(TEntity entity, Expression> downloadIdProp, bool save = false) where TEntity : BaseEntity + { + Guard.ArgumentNotNull(() => entity); + Guard.ArgumentNotNull(() => downloadIdProp); + + var propName = downloadIdProp.ExtractMemberInfo().Name; + int currentDownloadId = downloadIdProp.Compile().Invoke(entity); + var rs = EngineContext.Current.Resolve>(); + + UpdateTransientStateForEntityInternal(entity, propName, currentDownloadId, rs, null, save); + } + + public static void UpdateDownloadTransientStateFor(TEntity entity, Expression> downloadIdProp, bool save = false) where TEntity : BaseEntity + { + Guard.ArgumentNotNull(() => entity); + Guard.ArgumentNotNull(() => downloadIdProp); + + var propName = downloadIdProp.ExtractMemberInfo().Name; + int currentDownloadId = downloadIdProp.Compile().Invoke(entity).GetValueOrDefault(); + var rs = EngineContext.Current.Resolve>(); + + UpdateTransientStateForEntityInternal(entity, propName, currentDownloadId, rs, null, save); + } + + public static void UpdatePictureTransientStateFor(TEntity entity, Expression> pictureIdProp, bool save = false) where TEntity : BaseEntity + { + Guard.ArgumentNotNull(() => entity); + Guard.ArgumentNotNull(() => pictureIdProp); + + var propName = pictureIdProp.ExtractMemberInfo().Name; + int currentPictureId = pictureIdProp.Compile().Invoke(entity); + var rs = EngineContext.Current.Resolve>(); + + UpdateTransientStateForEntityInternal(entity, propName, currentPictureId, rs, GetPictureDeleteAction(), save); + } + + public static void UpdatePictureTransientStateFor(TEntity entity, Expression> pictureIdProp, bool save = false) where TEntity : BaseEntity + { + Guard.ArgumentNotNull(() => entity); + Guard.ArgumentNotNull(() => pictureIdProp); + + var propName = pictureIdProp.ExtractMemberInfo().Name; + int currentPictureId = pictureIdProp.Compile().Invoke(entity).GetValueOrDefault(); + var rs = EngineContext.Current.Resolve>(); + + UpdateTransientStateForEntityInternal(entity, propName, currentPictureId, rs, GetPictureDeleteAction(), save); + } + + public static void UpdateDownloadTransientState(int? prevDownloadId, int? currentDownloadId, bool save = false) + { + var rs = EngineContext.Current.Resolve>(); + UpdateTransientStateCore(prevDownloadId.GetValueOrDefault(), currentDownloadId.GetValueOrDefault(), rs, null, save); + } + + public static void UpdateDownloadTransientState(int prevDownloadId, int currentDownloadId, bool save = false) + { + var rs = EngineContext.Current.Resolve>(); + UpdateTransientStateCore(prevDownloadId, currentDownloadId, rs, null, save); + } + + public static void UpdatePictureTransientState(int? prevPictureId, int? currentPictureId, bool save = false) + { + var rs = EngineContext.Current.Resolve>(); + UpdateTransientStateCore(prevPictureId.GetValueOrDefault(), currentPictureId.GetValueOrDefault(), rs, GetPictureDeleteAction(), save); + } + + public static void UpdatePictureTransientState(int prevPictureId, int currentPictureId, bool save = false) + { + var rs = EngineContext.Current.Resolve>(); + UpdateTransientStateCore(prevPictureId, currentPictureId, rs, GetPictureDeleteAction(), save); + } + + private static Action GetPictureDeleteAction() + { + Action deleteAction = pic => + { + var svc = EngineContext.Current.Resolve(); + svc.DeletePicture((Picture)pic); + }; + + return deleteAction; + } + + internal static void UpdateTransientStateForEntityInternal( + TEntity entity, + string propName, + int currentMediaId, + IRepository rs, + Action deleteAction, + bool save) where TEntity : BaseEntity where TMedia : BaseEntity + { + bool editMode = !entity.IsTransientRecord(); + var modifiedProperties = editMode ? rs.Context.GetModifiedProperties(entity) : new Dictionary(); + + object obj = null; + int prevMediaId = 0; + if (modifiedProperties.TryGetValue(propName, out obj)) + { + prevMediaId = ((int?)obj).GetValueOrDefault(); + } + + UpdateTransientStateCore(prevMediaId, currentMediaId, rs, deleteAction, save); + } + + internal static void UpdateTransientStateCore( + int prevMediaId, + int currentMediaId, + IRepository rs, + Action deleteAction, + bool save) + where TMedia : BaseEntity + { + var autoCommit = rs.AutoCommitEnabled; + rs.AutoCommitEnabled = false; + + try + { + TMedia media = null; + bool shouldSave = false; + + bool isModified = prevMediaId != currentMediaId; + + if (currentMediaId > 0 && isModified) + { + // new entity with a media or media replaced + media = rs.GetById(currentMediaId); + if (media != null) + { + var transient = media as ITransient; + if (transient != null) + { + transient.IsTransient = false; + shouldSave = true; + } + } + } + + if (prevMediaId > 0 && isModified) + { + // ID changed, so delete old record + media = rs.GetById(prevMediaId); + if (media != null) + { + if (deleteAction == null) + { + rs.Delete(media); + } + else + { + deleteAction(media); + } + shouldSave = true; + } + } + + if (save && shouldSave) + { + rs.Context.SaveChanges(); + } + } + finally + { + rs.AutoCommitEnabled = autoCommit; + } + } + + + } + +} diff --git a/src/Libraries/SmartStore.Services/Media/PictureService.cs b/src/Libraries/SmartStore.Services/Media/PictureService.cs index bc11e71a36..563993234f 100644 --- a/src/Libraries/SmartStore.Services/Media/PictureService.cs +++ b/src/Libraries/SmartStore.Services/Media/PictureService.cs @@ -51,16 +51,7 @@ public partial class PictureService : IPictureService #region Ctor - /// - /// Ctor - /// - /// Picture repository - /// Product picture repository - /// Setting service - /// Web helper - /// Logger - /// Event publisher - /// Media settings + public PictureService( IRepository pictureRepository, IRepository productPictureRepository, @@ -147,12 +138,6 @@ private string GetDefaultImageFileName(PictureType defaultPictureType = PictureT return defaultImageFileName; } - /// - /// Validates input picture dimensions and prevents that the image size exceeds global max size - /// - /// Picture binary - /// MIME type - /// Picture binary or throws an exception public virtual byte[] ValidatePicture(byte[] pictureBinary) { Size originalSize = this.GetPictureSize(pictureBinary); @@ -169,31 +154,21 @@ public virtual byte[] ValidatePicture(byte[] pictureBinary) } } - /// - /// 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 - /// Id of equal picture if any - /// The picture binary for path when no picture equals in the sequence, null otherwise. - 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); } - /// - /// Finds an equal picture by comparing the binary buffer - /// - /// Binary picture data - /// The sequence of product 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. - 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); @@ -216,27 +191,11 @@ public byte[] FindEqualPicture(byte[] pictureBinary, IEnumerable produc } } - #endregion - - #region Methods - - /// - /// Get picture SEO friendly name - /// - /// Name - /// Result - public virtual string GetPictureSeName(string name) + public virtual string GetPictureSeName(string name) { return SeoHelper.GetSeName(name, true, false); } - /// - /// Get a picture local path - /// - /// Picture instance - /// The target picture size (longest side) - /// A value indicating whether the default picture is shown - /// public virtual string GetThumbLocalPath(Picture picture, int targetSize = 0, bool showDefaultPicture = true) { // 'GetPictureUrl' takes care of creating the thumb when not created already @@ -271,13 +230,6 @@ public virtual string GetThumbLocalPath(Picture picture, int targetSize = 0, boo } - /// - /// Gets the default picture URL - /// - /// The target picture size (longest side) - /// Default picture type - /// Store location URL; null to use determine the current store location automatically - /// Picture URL public virtual string GetDefaultPictureUrl(int targetSize = 0, PictureType defaultPictureType = PictureType.Entity, string storeLocation = null) { string defaultImageFileName = GetDefaultImageFileName(defaultPictureType); @@ -387,22 +339,11 @@ private byte[] LoadPictureFromFile(int pictureId, string mimeType, out string fi return File.ReadAllBytes(filePath); } - /// - /// Gets the loaded picture binary depending on picture storage settings - /// - /// Picture - /// Picture binary public virtual byte[] LoadPictureBinary(Picture picture) { return LoadPictureBinary(picture, this.StoreInDb); } - /// - /// Gets the loaded picture binary depending on picture storage settings - /// - /// Picture - /// Load from database; otherwise, from file system - /// Picture binary public virtual byte[] LoadPictureBinary(Picture picture, bool fromDb) { if (picture == null) @@ -462,15 +403,6 @@ internal Size GetPictureSize(byte[] pictureBinary) return size; } - /// - /// Get a picture URL - /// - /// Picture identifier - /// The target picture size (longest side) - /// A value indicating whether the default picture is shown - /// Store location URL; null to use determine the current store location automatically - /// Default picture type - /// Picture URL public virtual string GetPictureUrl( int pictureId, int targetSize = 0, @@ -482,15 +414,6 @@ public virtual string GetPictureUrl( return GetPictureUrl(picture, targetSize, showDefaultPicture, storeLocation, defaultPictureType); } - /// - /// Get a picture URL - /// - /// Picture instance - /// The target picture size (longest side) - /// A value indicating whether the default picture is shown - /// Store location URL; null to use determine the current store location automatically - /// Default picture type - /// Picture URL public virtual string GetPictureUrl( Picture picture, int targetSize = 0, @@ -580,37 +503,27 @@ public virtual Picture GetPictureById(int pictureId) return picture; } - /// - /// Deletes a picture - /// - /// Picture public virtual void DeletePicture(Picture picture) { if (picture == null) throw new ArgumentNullException("picture"); - //delete thumbs + // delete thumbs _imageCache.DeleteCachedImages(picture); - //delete from file system + // delete from file system if (!this.StoreInDb) { DeletePictureOnFileSystem(picture); } - //delete from database + // delete from database _pictureRepository.Delete(picture); - //event notification + // event notification _eventPublisher.EntityDeleted(picture); } - /// - /// Gets a collection of pictures - /// - /// Current page - /// Items on each page - /// Paged list of pictures public virtual IPagedList GetPictures(int pageIndex, int pageSize) { var query = from p in _pictureRepository.Table @@ -620,12 +533,6 @@ orderby p.Id descending return pics; } - /// - /// Gets pictures by product identifier - /// - /// Product identifier - /// Number of records to return. 0 if you want to get all items - /// Pictures public virtual IList GetPicturesByProductId(int productId, int recordsToReturn = 0) { if (productId == 0) @@ -644,16 +551,17 @@ orderby pp.DisplayOrder return pics; } - /// - /// Inserts a picture - /// - /// The picture binary - /// The picture MIME type - /// The SEO filename - /// A value indicating whether the picture is new - /// A value indicating whether to validated provided picture binary - /// Picture - public virtual Picture InsertPicture(byte[] pictureBinary, string mimeType, string seoFilename, bool isNew, bool validateBinary = true) + public virtual IList GetPicturesByIds(int[] pictureIds) + { + Guard.ArgumentNotNull(() => pictureIds); + + var query = _pictureRepository.Table + .Where(x => pictureIds.Contains(x.Id)); + + return query.ToList(); + } + + public virtual Picture InsertPicture(byte[] pictureBinary, string mimeType, string seoFilename, bool isNew, bool isTransient = true, bool validateBinary = true) { mimeType = mimeType.EmptyNull(); mimeType = mimeType.Truncate(20); @@ -670,6 +578,8 @@ public virtual Picture InsertPicture(byte[] pictureBinary, string mimeType, stri picture.MimeType = mimeType; picture.SeoFilename = seoFilename; picture.IsNew = isNew; + picture.IsTransient = isTransient; + picture.UpdatedOnUtc = DateTime.UtcNow; _pictureRepository.Insert(picture); @@ -684,16 +594,6 @@ public virtual Picture InsertPicture(byte[] pictureBinary, string mimeType, stri return picture; } - /// - /// Updates the picture - /// - /// The picture identifier - /// The picture binary - /// The picture MIME type - /// The SEO filename - /// A value indicating whether the picture is new - /// A value indicating whether to validated provided picture binary - /// Picture public virtual Picture UpdatePicture(int pictureId, byte[] pictureBinary, string mimeType, string seoFilename, bool isNew, bool validateBinary = true) { mimeType = mimeType.EmptyNull().Truncate(20); @@ -718,6 +618,7 @@ public virtual Picture UpdatePicture(int pictureId, byte[] pictureBinary, string picture.MimeType = mimeType; picture.SeoFilename = seoFilename; picture.IsNew = isNew; + picture.UpdatedOnUtc = DateTime.UtcNow; _pictureRepository.Update(picture); @@ -732,22 +633,15 @@ public virtual Picture UpdatePicture(int pictureId, byte[] pictureBinary, string return picture; } - /// - /// Updates a SEO filename of a picture - /// - /// The picture identifier - /// The SEO filename - /// Picture public virtual Picture SetSeoFilename(int pictureId, string seoFilename) { var picture = GetPictureById(pictureId); if (picture == null) throw new ArgumentException("No picture found with the specified id"); - //update if it has been changed + // update if it has been changed if (seoFilename != picture.SeoFilename) { - //update picture picture = UpdatePicture(picture.Id, LoadPictureBinary(picture), picture.MimeType, seoFilename, true, false); } return picture; @@ -757,10 +651,6 @@ public virtual Picture SetSeoFilename(int pictureId, string seoFilename) #region Properties - - /// - /// Gets or sets a value indicating whether the images should be stored in data base. - /// public virtual bool StoreInDb { get @@ -789,14 +679,10 @@ protected int MovePictures(bool toDb) var affectedFiles = new List(1000); var ctx = _pictureRepository.Context; - - _pictureRepository.AutoCommitEnabled = false; - var failed = false; - int i = 0; - using (var scope = new DbContextScope(ctx: ctx, autoDetectChanges: false, proxyCreation: false, validateOnSave: false)) + using (var scope = new DbContextScope(ctx: ctx, autoDetectChanges: false, proxyCreation: false, validateOnSave: false, autoCommit: false)) { using (var tx = ctx.BeginTransaction()) { @@ -811,7 +697,7 @@ protected int MovePictures(bool toDb) if (pictures != null) { // detach all entities from previous page to save memory - pictures.Each(x => ctx.Detach(x)); + ctx.DetachEntities(pictures); // breathe pictures.Clear(); @@ -854,6 +740,7 @@ protected int MovePictures(bool toDb) } // explicitly attach modified entity to context, because we disabled AutoCommit + picture.UpdatedOnUtc = DateTime.UtcNow; _pictureRepository.Update(picture); i++; @@ -878,8 +765,6 @@ protected int MovePictures(bool toDb) } } - _pictureRepository.AutoCommitEnabled = true; - if (affectedFiles.Count > 0) { if ((toDb && !failed) || (!toDb && failed)) @@ -898,13 +783,9 @@ protected int MovePictures(bool toDb) } // shrink database (only when DB > FS and success) - if (!toDb && !failed && DataSettings.Current.IsSqlServer) + if (!toDb && !failed) { - try - { - ctx.ExecuteSqlCommand("DBCC SHRINKDATABASE(0)", true); - } - catch { } + ctx.ShrinkDatabase(); } } diff --git a/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs b/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs new file mode 100644 index 0000000000..b67c04fbe5 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Media/TransientMediaClearTask.cs @@ -0,0 +1,70 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Media; +using SmartStore.Services.Tasks; + +namespace SmartStore.Services.Media +{ + /// + /// Represents a task for deleting transient media from the database + /// (pictures and downloads which have been uploaded but never assigned to an entity) + /// + public partial class TransientMediaClearTask : ITask + { + private readonly IPictureService _pictureService; + private readonly IRepository _pictureRepository; + private readonly IRepository _downloadRepository; + + public TransientMediaClearTask( + IPictureService pictureService, + IRepository pictureRepository, + IRepository downloadRepository) + { + this._pictureService = pictureService; + this._pictureRepository = pictureRepository; + this._downloadRepository = downloadRepository; + } + + public void Execute(TaskExecutionContext ctx) + { + // delete all media records which are in transient state since at least 3 hours + var olderThan = DateTime.UtcNow.AddHours(-3); + + // delete Downloads + _downloadRepository.DeleteAll(x => x.IsTransient && x.UpdatedOnUtc < olderThan); + + // delete Pictures + var autoCommit = _pictureRepository.AutoCommitEnabled; + _pictureRepository.AutoCommitEnabled = false; + + try + { + using (var scope = new DbContextScope(autoDetectChanges: false, validateOnSave: false, hooksEnabled: false)) + { + var pictures = _pictureRepository.Table.Where(x => x.IsTransient && x.UpdatedOnUtc < olderThan).ToList(); + foreach (var picture in pictures) + { + _pictureService.DeletePicture(picture); + } + + _pictureRepository.Context.SaveChanges(); + + if (DataSettings.Current.IsSqlServer) + { + try + { + _pictureRepository.Context.ExecuteSqlCommand("DBCC SHRINKDATABASE(0)", true); + } + catch { } + } + } + } + finally + { + _pictureRepository.AutoCommitEnabled = autoCommit; + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/Messages/EmailAccountService.cs b/src/Libraries/SmartStore.Services/Messages/EmailAccountService.cs index ebf71742dc..639c0391cb 100644 --- a/src/Libraries/SmartStore.Services/Messages/EmailAccountService.cs +++ b/src/Libraries/SmartStore.Services/Messages/EmailAccountService.cs @@ -2,36 +2,37 @@ using System.Collections.Generic; using System.Linq; using SmartStore.Core; +using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.Messages; using SmartStore.Core.Events; namespace SmartStore.Services.Messages { - public partial class EmailAccountService:IEmailAccountService + public partial class EmailAccountService : IEmailAccountService { - private readonly IRepository _emailAccountRepository; + private const string EMAILACCOUNT_BY_ID_KEY = "SmartStore.emailaccount.id-{0}"; + private const string EMAILACCOUNT_PATTERN_KEY = "SmartStore.emailaccount."; + + private readonly IRepository _emailAccountRepository; private readonly EmailAccountSettings _emailAccountSettings; private readonly IEventPublisher _eventPublisher; + private readonly ICacheManager _cacheManager; - /// - /// Ctor - /// - /// Email account repository - /// - /// Event published - public EmailAccountService(IRepository emailAccountRepository, - EmailAccountSettings emailAccountSettings, IEventPublisher eventPublisher) + private EmailAccount _defaultEmailAccount; + + public EmailAccountService( + IRepository emailAccountRepository, + EmailAccountSettings emailAccountSettings, + IEventPublisher eventPublisher, + ICacheManager cacheManager /* request */) { - _emailAccountRepository = emailAccountRepository; - _emailAccountSettings = emailAccountSettings; - _eventPublisher = eventPublisher; + this._emailAccountRepository = emailAccountRepository; + this._emailAccountSettings = emailAccountSettings; + this._eventPublisher = eventPublisher; + this._cacheManager = cacheManager; } - /// - /// Inserts an email account - /// - /// Email account public virtual void InsertEmailAccount(EmailAccount emailAccount) { if (emailAccount == null) @@ -57,14 +58,12 @@ public virtual void InsertEmailAccount(EmailAccount emailAccount) _emailAccountRepository.Insert(emailAccount); - //event notification + _cacheManager.RemoveByPattern(EMAILACCOUNT_PATTERN_KEY); + _defaultEmailAccount = null; + _eventPublisher.EntityInserted(emailAccount); } - /// - /// Updates an email account - /// - /// Email account public virtual void UpdateEmailAccount(EmailAccount emailAccount) { if (emailAccount == null) @@ -90,14 +89,12 @@ public virtual void UpdateEmailAccount(EmailAccount emailAccount) _emailAccountRepository.Update(emailAccount); - //event notification + _cacheManager.RemoveByPattern(EMAILACCOUNT_PATTERN_KEY); + _defaultEmailAccount = null; + _eventPublisher.EntityUpdated(emailAccount); } - /// - /// Deletes an email account - /// - /// Email account public virtual void DeleteEmailAccount(EmailAccount emailAccount) { if (emailAccount == null) @@ -108,28 +105,38 @@ public virtual void DeleteEmailAccount(EmailAccount emailAccount) _emailAccountRepository.Delete(emailAccount); - //event notification + _cacheManager.RemoveByPattern(EMAILACCOUNT_PATTERN_KEY); + _defaultEmailAccount = null; + _eventPublisher.EntityDeleted(emailAccount); } - /// - /// Gets an email account by identifier - /// - /// The email account identifier - /// Email account public virtual EmailAccount GetEmailAccountById(int emailAccountId) { if (emailAccountId == 0) return null; - - var emailAccount = _emailAccountRepository.GetById(emailAccountId); - return emailAccount; + + string key = string.Format(EMAILACCOUNT_BY_ID_KEY, emailAccountId); + return _cacheManager.Get(key, () => + { + return _emailAccountRepository.GetById(emailAccountId); + }); } - /// - /// Gets all email accounts - /// - /// Email accounts list + public virtual EmailAccount GetDefaultEmailAccount() + { + if (_defaultEmailAccount == null) + { + _defaultEmailAccount = GetEmailAccountById(_emailAccountSettings.DefaultEmailAccountId); + if (_defaultEmailAccount == null) + { + _defaultEmailAccount = GetAllEmailAccounts().FirstOrDefault(); + } + } + + return _defaultEmailAccount; + } + public virtual IList GetAllEmailAccounts() { var query = from ea in _emailAccountRepository.Table diff --git a/src/Libraries/SmartStore.Services/Messages/EventPublisherExtensions.cs b/src/Libraries/SmartStore.Services/Messages/EventPublisherExtensions.cs index ada12006c8..681cb588b6 100644 --- a/src/Libraries/SmartStore.Services/Messages/EventPublisherExtensions.cs +++ b/src/Libraries/SmartStore.Services/Messages/EventPublisherExtensions.cs @@ -1,4 +1,5 @@ -using SmartStore.Core; +using System.Collections.Generic; +using SmartStore.Core; using SmartStore.Core.Domain.Messages; using SmartStore.Core.Events; @@ -26,12 +27,12 @@ public static void PublishNewsletterUnsubscribe(this IEventPublisher eventPublis eventPublisher.Publish(new EmailUnsubscribedEvent(email)); } - public static void EntityTokensAdded(this IEventPublisher eventPublisher, T entity, System.Collections.Generic.IList tokens) where T : BaseEntity + public static void EntityTokensAdded(this IEventPublisher eventPublisher, T entity, IList tokens) where T : BaseEntity { eventPublisher.Publish(new EntityTokensAddedEvent(entity, tokens)); } - public static void MessageTokensAdded(this IEventPublisher eventPublisher, MessageTemplate message, System.Collections.Generic.IList tokens) + public static void MessageTokensAdded(this IEventPublisher eventPublisher, MessageTemplate message, IList tokens) { eventPublisher.Publish(new MessageTokensAddedEvent(message, tokens)); } diff --git a/src/Libraries/SmartStore.Services/Messages/IEmailAccountService.cs b/src/Libraries/SmartStore.Services/Messages/IEmailAccountService.cs index 5f03d721bd..6b3df6f6eb 100644 --- a/src/Libraries/SmartStore.Services/Messages/IEmailAccountService.cs +++ b/src/Libraries/SmartStore.Services/Messages/IEmailAccountService.cs @@ -30,6 +30,12 @@ public partial interface IEmailAccountService /// Email account EmailAccount GetEmailAccountById(int emailAccountId); + /// + /// Gets the default email account + /// + /// Emil account + EmailAccount GetDefaultEmailAccount(); + /// /// Gets all email accounts /// diff --git a/src/Libraries/SmartStore.Services/Messages/IMessageTokenProvider.cs b/src/Libraries/SmartStore.Services/Messages/IMessageTokenProvider.cs index add0f4e57a..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/IQueuedEmailService.cs b/src/Libraries/SmartStore.Services/Messages/IQueuedEmailService.cs index 08c82ec8cd..12baf20719 100644 --- a/src/Libraries/SmartStore.Services/Messages/IQueuedEmailService.cs +++ b/src/Libraries/SmartStore.Services/Messages/IQueuedEmailService.cs @@ -25,6 +25,12 @@ public partial interface IQueuedEmailService /// Queued email void DeleteQueuedEmail(QueuedEmail queuedEmail); + /// + /// Deletes all queued emails + /// + /// The count of deleted entries + int DeleteAllQueuedEmails(); + /// /// Gets a queued email by identifier /// @@ -42,22 +48,9 @@ public partial interface IQueuedEmailService /// /// Search queued emails /// - /// From Email - /// To Email - /// The start time - /// The end time - /// A value indicating whether to load only not sent emails - /// Maximum send tries - /// A value indicating whether we should sort queued email descending; otherwise, ascending. - /// Page index - /// Page size - /// A value indicating whether to load manually send emails + /// An object containing the query criteria /// Email item collection - IPagedList SearchEmails(string fromEmail, - string toEmail, DateTime? startTime, DateTime? endTime, - bool loadNotSentItemsOnly, int maxSendTries, - bool loadNewest, int pageIndex, int pageSize, - bool? sendManually = null); + IPagedList SearchEmails(SearchEmailsQuery query); /// /// Sends a queued email @@ -65,5 +58,18 @@ IPagedList SearchEmails(string fromEmail, /// Queued email /// Whether the operation succeeded bool SendEmail(QueuedEmail queuedEmail); + + /// + /// Gets a queued email attachment by identifier + /// + /// Queued email attachment identifier + /// Queued email attachment + QueuedEmailAttachment GetQueuedEmailAttachmentById(int id); + + /// + /// Deleted a queued email attachment + /// + /// Queued email attachment + void DeleteQueuedEmailAttachment(QueuedEmailAttachment qea); } } 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/MessageTemplateService.cs b/src/Libraries/SmartStore.Services/Messages/MessageTemplateService.cs index 92ae48a637..65d8fe735c 100644 --- a/src/Libraries/SmartStore.Services/Messages/MessageTemplateService.cs +++ b/src/Libraries/SmartStore.Services/Messages/MessageTemplateService.cs @@ -45,7 +45,8 @@ public partial class MessageTemplateService: IMessageTemplateService /// Store mapping service /// Message template repository /// Event published - public MessageTemplateService(ICacheManager cacheManager, + public MessageTemplateService( + ICacheManager cacheManager, IRepository storeMappingRepository, ILanguageService languageService, ILocalizedEntityService localizedEntityService, @@ -209,7 +210,7 @@ public virtual MessageTemplate CopyMessageTemplate(MessageTemplate messageTempla if (messageTemplate == null) throw new ArgumentNullException("messageTemplate"); - var mtCopy = new MessageTemplate() + var mtCopy = new MessageTemplate { Name = messageTemplate.Name, BccEmailAddresses = messageTemplate.BccEmailAddresses, @@ -218,25 +219,26 @@ public virtual MessageTemplate CopyMessageTemplate(MessageTemplate messageTempla IsActive = messageTemplate.IsActive, EmailAccountId = messageTemplate.EmailAccountId, LimitedToStores = messageTemplate.LimitedToStores + // INFO: we do not copy attachments }; InsertMessageTemplate(mtCopy); var languages = _languageService.GetAllLanguages(true); - //localization + // localization foreach (var lang in languages) { var bccEmailAddresses = messageTemplate.GetLocalized(x => x.BccEmailAddresses, lang.Id, false, false); - if (!String.IsNullOrEmpty(bccEmailAddresses)) + if (bccEmailAddresses.HasValue()) _localizedEntityService.SaveLocalizedValue(mtCopy, x => x.BccEmailAddresses, bccEmailAddresses, lang.Id); var subject = messageTemplate.GetLocalized(x => x.Subject, lang.Id, false, false); - if (!String.IsNullOrEmpty(subject)) + if (subject.HasValue()) _localizedEntityService.SaveLocalizedValue(mtCopy, x => x.Subject, subject, lang.Id); var body = messageTemplate.GetLocalized(x => x.Body, lang.Id, false, false); - if (!String.IsNullOrEmpty(body)) + if (body.HasValue()) _localizedEntityService.SaveLocalizedValue(mtCopy, x => x.Body, subject, lang.Id); var emailAccountId = messageTemplate.GetLocalized(x => x.EmailAccountId, lang.Id, false, false); @@ -244,7 +246,7 @@ public virtual MessageTemplate CopyMessageTemplate(MessageTemplate messageTempla _localizedEntityService.SaveLocalizedValue(mtCopy, x => x.EmailAccountId, emailAccountId, lang.Id); } - //store mapping + // store mapping var selectedStoreIds = _storeMappingService.GetStoresIdsWithAccess(messageTemplate); foreach (var id in selectedStoreIds) { diff --git a/src/Libraries/SmartStore.Services/Messages/MessageTokenProvider.cs b/src/Libraries/SmartStore.Services/Messages/MessageTokenProvider.cs index c91c6cf510..d0ff917259 100644 --- a/src/Libraries/SmartStore.Services/Messages/MessageTokenProvider.cs +++ b/src/Libraries/SmartStore.Services/Messages/MessageTokenProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Linq.Expressions; using System.Text; using System.Web; using SmartStore.Core; @@ -10,19 +11,23 @@ using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Forums; +using SmartStore.Core.Domain.Localization; +using SmartStore.Core.Domain.Media; using SmartStore.Core.Domain.Messages; using SmartStore.Core.Domain.News; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Domain.Stores; using SmartStore.Core.Domain.Tax; +using SmartStore.Core.Events; using SmartStore.Core.Html; +using SmartStore.Core.Plugins; using SmartStore.Services.Catalog; using SmartStore.Services.Common; using SmartStore.Services.Customers; using SmartStore.Services.Directory; -using SmartStore.Core.Events; using SmartStore.Services.Forums; using SmartStore.Services.Helpers; using SmartStore.Services.Localization; @@ -30,10 +35,8 @@ using SmartStore.Services.Orders; using SmartStore.Services.Payments; using SmartStore.Services.Seo; +using SmartStore.Services.Stores; using SmartStore.Services.Topics; -using SmartStore.Core.Domain.Directory; -using SmartStore.Core.Plugins; -using System.Linq.Expressions; namespace SmartStore.Services.Messages { @@ -52,7 +55,7 @@ public partial class MessageTokenProvider : IMessageTokenProvider private readonly IStoreContext _storeContext; private readonly IDownloadService _downloadService; private readonly IOrderService _orderService; - private readonly IPaymentService _paymentService; + private readonly IProviderManager _providerManager; private readonly IProductAttributeParser _productAttributeParser; private readonly StoreInformationSettings _storeSettings; private readonly MessageTemplatesSettings _templatesSettings; @@ -67,25 +70,34 @@ public partial class MessageTokenProvider : IMessageTokenProvider private readonly ShoppingCartSettings _shoppingCartSettings; private readonly IDeliveryTimeService _deliveryTimeService; private readonly IQuantityUnitService _quantityUnitService; - - #endregion + private readonly IUrlRecordService _urlRecordService; + private readonly IStoreService _storeService; + private readonly IGenericAttributeService _attrService; + private readonly IPictureService _pictureService; + private readonly MediaSettings _mediaSettings; - #region Ctor + #endregion - public MessageTokenProvider(ILanguageService languageService, + #region Ctor + + public MessageTokenProvider(ILanguageService languageService, ILocalizationService localizationService, IDateTimeHelper dateTimeHelper, IEmailAccountService emailAccountService, IPriceFormatter priceFormatter, ICurrencyService currencyService, IWebHelper webHelper, IWorkContext workContext, IStoreContext storeContext, IDownloadService downloadService, ShoppingCartSettings shoppingCartSettings, - IOrderService orderService, IPaymentService paymentService, + IOrderService orderService, IProviderManager providerManager, IProductAttributeParser productAttributeParser, StoreInformationSettings storeSettings, MessageTemplatesSettings templatesSettings, EmailAccountSettings emailAccountSettings, CatalogSettings catalogSettings, TaxSettings taxSettings, IEventPublisher eventPublisher, CompanyInformationSettings companyInfoSettings, BankConnectionSettings bankConnectionSettings, ContactDataSettings contactDataSettings, ITopicService topicService, - IDeliveryTimeService deliveryTimeService, IQuantityUnitService quantityUnitService) + IDeliveryTimeService deliveryTimeService, IQuantityUnitService quantityUnitService, + IUrlRecordService urlRecordService, IStoreService storeService, + IGenericAttributeService attrService, + IPictureService pictureService, + MediaSettings mediaSettings) { this._languageService = languageService; this._localizationService = localizationService; @@ -98,7 +110,7 @@ public MessageTokenProvider(ILanguageService languageService, this._storeContext = storeContext; this._downloadService = downloadService; this._orderService = orderService; - this._paymentService = paymentService; + this._providerManager = providerManager; this._productAttributeParser = productAttributeParser; this._storeSettings = storeSettings; this._templatesSettings = templatesSettings; @@ -113,33 +125,90 @@ public MessageTokenProvider(ILanguageService languageService, this._shoppingCartSettings = shoppingCartSettings; this._deliveryTimeService = deliveryTimeService; this._quantityUnitService = quantityUnitService; + 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(); + } - var language = _languageService.GetLanguageById(languageId); + 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 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(); @@ -152,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) { @@ -164,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(""); @@ -228,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) @@ -252,6 +330,7 @@ protected virtual string ProductListToHtmlTable(Order order, int languageId) sb.AppendLine(""); } + #endregion #region Checkout Attributes @@ -276,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) { @@ -374,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) { @@ -416,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}
" + HttpUtility.HtmlEncode(productName)); - //add download link - if (_downloadService.IsDownloadAllowed(orderItem)) + sb.AppendLine(""); + + if (_mediaSettings.MessageProductThumbPictureSize > 0) + { + var pictureHtml = ProductPictureToHtml(GetPictureFor(product, orderItem.AttributesXml), language, productName, productUrl, storeLocation); + if (pictureHtml.HasValue()) + { + sb.AppendLine("
{0}
".FormatInvariant(pictureHtml)); + } + } + + sb.AppendLine("{1}".FormatInvariant(productUrl, HttpUtility.HtmlEncode(productName))); + + //add download link + if (_downloadService.IsDownloadAllowed(orderItem)) { //TODO add a method for getting URL (use routing because it handles all SEO friendly URLs) - string downloadUrl = string.Format("{0}download/getdownload/{1}", _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(")"); @@ -186,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("
"); @@ -204,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 @@ -462,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(); @@ -488,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(""); @@ -515,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); @@ -506,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) { @@ -554,7 +636,7 @@ protected virtual string GetSupplierIdentification() sb.AppendLine(""); sb.AppendLine(""); - sb.AppendLine(" 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/AdminMenu.cs b/src/Plugins/SmartStore.DevTools/AdminMenu.cs index e0893067e1..25ae68e381 100644 --- a/src/Plugins/SmartStore.DevTools/AdminMenu.cs +++ b/src/Plugins/SmartStore.DevTools/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.DevTools { @@ -18,8 +12,17 @@ protected override void BuildMenuCore(TreeNode pluginsNode) .Icon("code") .Action("ConfigurePlugin", "Plugin", new { systemName = "SmartStore.DevTools", area = "Admin" }) .ToItem(); - + pluginsNode.Prepend(menuItem); + + // uncomment to add to admin menu (see plugin sub-menu) + //var backendExtensionItem = new MenuItem().ToBuilder() + // .Text("Backend extension") + // .Icon("area-chart") + // .Action("BackendExtension", "DevTools", new { area = "SmartStore.DevTools" }) + // .ToItem(); + + //pluginsNode.Append(backendExtensionItem); } } } diff --git a/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs b/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs index 42d23e84a8..5700d55470 100644 --- a/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs +++ b/src/Plugins/SmartStore.DevTools/Controllers/DevToolsController.cs @@ -1,13 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; -using System.Web.Mvc; -using SmartStore.Core; +using System.Web.Mvc; +using SmartStore.DevTools.Models; using SmartStore.Services; -using SmartStore.Services.Configuration; -using SmartStore.Services.Stores; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.Settings; namespace SmartStore.DevTools.Controllers @@ -15,40 +10,27 @@ namespace SmartStore.DevTools.Controllers public class DevToolsController : SmartController { - private readonly IWorkContext _workContext; - private readonly IStoreContext _storeContext; - private readonly IStoreService _storeService; - private readonly ISettingService _settingService; - - public DevToolsController( - IWorkContext workContext, - IStoreContext storeContext, - IStoreService storeService, - ISettingService settingService) + private readonly ICommonServices _services; + + public DevToolsController(ICommonServices services) { - _workContext = workContext; - _storeContext = storeContext; - _storeService = storeService; - _settingService = settingService; + _services = services; } - [AdminAuthorize] - [ChildActionOnly] + [AdminAuthorize, ChildActionOnly] public ActionResult Configure() { // load settings for a chosen store scope - var storeScope = this.GetActiveStoreScopeConfiguration(_storeService, _workContext); - var settings = _settingService.LoadSetting(storeScope); + var storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); + var settings = _services.Settings.LoadSetting(storeScope); var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - storeDependingSettingHelper.GetOverrideKeys(settings, settings, storeScope, _settingService); + storeDependingSettingHelper.GetOverrideKeys(settings, settings, storeScope, _services.Settings); return View(settings); } - [AdminAuthorize] - [HttpPost] - [ChildActionOnly] + [HttpPost, AdminAuthorize, ChildActionOnly] public ActionResult Configure(ProfilerSettings model, FormCollection form) { if (!ModelState.IsValid) @@ -58,10 +40,10 @@ public ActionResult Configure(ProfilerSettings model, FormCollection form) // load settings for a chosen store scope var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - var storeScope = this.GetActiveStoreScopeConfiguration(_storeService, _workContext); + var storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); - storeDependingSettingHelper.UpdateSettings(model /*settings*/, form, storeScope, _settingService); - _settingService.ClearCache(); + storeDependingSettingHelper.UpdateSettings(model /*settings*/, form, storeScope, _services.Settings); + _services.Settings.ClearCache(); return Configure(); } @@ -71,5 +53,30 @@ public ActionResult MiniProfiler() return View(); } + public ActionResult WidgetZone(string widgetZone) + { + var storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); + var settings = _services.Settings.LoadSetting(storeScope); + + if (settings.DisplayWidgetZones) + { + ViewData["widgetZone"] = widgetZone; + + return View(); + } + + return new EmptyResult(); + } + + [AdminAuthorize] + public ActionResult BackendExtension() + { + var model = new BackendExtensionModel + { + Welcome = "Hello world!" + }; + + return View(model); + } } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs b/src/Plugins/SmartStore.DevTools/DependencyRegistrar.cs index e562ef1d9f..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,17 +9,18 @@ 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(); - + builder.RegisterType().AsActionFilterFor(); + //// intercept CatalogController's Product action //builder.RegisterType().AsResultFilterFor(x => x.Product(default(int), default(string))).InstancePerRequest(); //builder.RegisterType().AsActionFilterFor().InstancePerRequest(); 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/ProfilerFilter.cs b/src/Plugins/SmartStore.DevTools/Filters/ProfilerFilter.cs index 85a252e7f1..2971dabe08 100644 --- a/src/Plugins/SmartStore.DevTools/Filters/ProfilerFilter.cs +++ b/src/Plugins/SmartStore.DevTools/Filters/ProfilerFilter.cs @@ -54,7 +54,10 @@ public void OnActionExecuted(ActionExecutedContext filterContext) if (!_profilerSettings.EnableMiniProfilerInPublicStore) return; - this._profiler.Value.StepStop("ActionFilter"); + if (!(filterContext.Result is ViewResultBase)) + { + this._profiler.Value.StepStop("ActionFilter"); + } } public void OnResultExecuting(ResultExecutingContext filterContext) @@ -74,15 +77,6 @@ public void OnResultExecuting(ResultExecutingContext filterContext) return; } - if (!filterContext.IsChildAction) - { - _widgetProvider.Value.RegisterAction( - "head_html_tag", - "MiniProfiler", - "DevTools", - new { area = "SmartStore.DevTools" }); - } - var viewName = result.ViewName; if (viewName.IsEmpty()) { @@ -91,6 +85,15 @@ public void OnResultExecuting(ResultExecutingContext filterContext) } this._profiler.Value.StepStart("ResultFilter", string.Format("{0}: {1}", result is PartialViewResult ? "Partial" : "View", viewName)); + + if (!filterContext.IsChildAction) + { + _widgetProvider.Value.RegisterAction( + "head_html_tag", + "MiniProfiler", + "DevTools", + new { area = "SmartStore.DevTools" }); + } } public void OnResultExecuted(ResultExecutedContext filterContext) @@ -110,6 +113,7 @@ public void OnResultExecuted(ResultExecutedContext filterContext) } this._profiler.Value.StepStop("ResultFilter"); + this._profiler.Value.StepStop("ActionFilter"); } private bool ShouldProfile(HttpContextBase ctx) 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/Filters/WidgetZoneFilter.cs b/src/Plugins/SmartStore.DevTools/Filters/WidgetZoneFilter.cs new file mode 100644 index 0000000000..253fdd0b60 --- /dev/null +++ b/src/Plugins/SmartStore.DevTools/Filters/WidgetZoneFilter.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using System.Web.Mvc; +using SmartStore.Core.Logging; +using SmartStore.Core.Localization; +using SmartStore.DevTools.Services; +using SmartStore.Core; +using SmartStore.Services; +using SmartStore.Services.Customers; +using SmartStore.Web.Framework.UI; +using SmartStore.Utilities; +using System.IO; + +namespace SmartStore.DevTools.Filters +{ + public class WidgetZoneFilter : IActionFilter, IResultFilter + { + private readonly ICommonServices _services; + private readonly Lazy _widgetProvider; + private readonly ProfilerSettings _profilerSettings; + + public WidgetZoneFilter( + ICommonServices services, + Lazy widgetProvider, + ProfilerSettings profilerSettings) + { + this._services = services; + this._widgetProvider = widgetProvider; + this._profilerSettings = profilerSettings; + } + + public void OnActionExecuting(ActionExecutingContext filterContext) + { + if (!_profilerSettings.DisplayWidgetZones) + return; + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { + if (!_profilerSettings.DisplayWidgetZones) + return; + } + + public void OnResultExecuting(ResultExecutingContext filterContext) + { + if (!_profilerSettings.DisplayWidgetZones) + return; + + // should only run on a full view rendering result + var result = filterContext.Result as ViewResultBase; + if (result == null) + { + return; + } + + if (!this.ShouldRender(filterContext.HttpContext)) + { + return; + } + + if (!filterContext.IsChildAction) + { + _widgetProvider.Value.RegisterAction( + new Wildcard("*"), + "WidgetZone", + "DevTools", + new { area = "SmartStore.DevTools" }); + } + + var viewName = result.ViewName; + + if (viewName.IsEmpty()) + { + string action = (filterContext.RouteData.Values["action"] as string).EmptyNull(); + viewName = action; + + if (action == "WidgetsByZone") + { + var model = result.Model as WidgetZoneModel; + + filterContext.Result = new ViewResult + { + ViewName = "~/Plugins/SmartStore.DevTools/Views/DevTools/WidgetZone.cshtml", + }; + filterContext.RouteData.Values.Add("widgetZone", model.WidgetZone); + } + } + } + + public void OnResultExecuted(ResultExecutedContext filterContext) + { + if (!_profilerSettings.DisplayWidgetZones) + return; + + // should only run on a full view rendering result + if (!(filterContext.Result is ViewResultBase)) + { + return; + } + + if (!this.ShouldRender(filterContext.HttpContext)) + { + return; + } + } + + private bool ShouldRender(HttpContextBase ctx) + { + if (!_services.WorkContext.CurrentCustomer.IsAdmin()) + { + return ctx.Request.IsLocal; + } + + return true; + } + + } +} diff --git a/src/Plugins/SmartStore.DevTools/Models/BackendExtensionModel.cs b/src/Plugins/SmartStore.DevTools/Models/BackendExtensionModel.cs new file mode 100644 index 0000000000..2db540028f --- /dev/null +++ b/src/Plugins/SmartStore.DevTools/Models/BackendExtensionModel.cs @@ -0,0 +1,9 @@ +using SmartStore.Web.Framework.Modelling; + +namespace SmartStore.DevTools.Models +{ + public class BackendExtensionModel : ModelBase + { + public string Welcome { get; set; } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/ProfilerHttpModule.cs b/src/Plugins/SmartStore.DevTools/ProfilerHttpModule.cs index 1cbc8981b3..c2e585d0eb 100644 --- a/src/Plugins/SmartStore.DevTools/ProfilerHttpModule.cs +++ b/src/Plugins/SmartStore.DevTools/ProfilerHttpModule.cs @@ -1,25 +1,12 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Web; -using Microsoft.Web.Infrastructure.DynamicModuleHelper; +using SmartStore.Core.Data; using SmartStore.Core.Infrastructure; -using SmartStore.Core.Plugins; -using SmartStore.Web.Framework; using StackExchange.Profiling; namespace SmartStore.DevTools { - public class ProfilerStarter : IPreApplicationStart - { - public void Start() - { - DynamicModuleUtility.RegisterModule(typeof(ProfilerHttpModule)); - SmartUrlRoutingModule.RegisterRoutablePath("/mini-profiler-resources/(.*)"); - } - } - public class ProfilerHttpModule : IHttpModule { private const string MP_KEY = "sm.miniprofiler.started"; @@ -57,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/Services/ProfilerService.cs b/src/Plugins/SmartStore.DevTools/Services/ProfilerService.cs index 462451a123..e490e592f2 100644 --- a/src/Plugins/SmartStore.DevTools/Services/ProfilerService.cs +++ b/src/Plugins/SmartStore.DevTools/Services/ProfilerService.cs @@ -43,9 +43,12 @@ public void StepStop(string key) } IDisposable step; - if (this.steps[key].TryPop(out step)) + if (this.steps.ContainsKey(key)) { - step.Dispose(); + if (this.steps[key].TryPop(out step)) + { + step.Dispose(); + } } } diff --git a/src/Plugins/SmartStore.DevTools/Settings/ProfilerSettings.cs b/src/Plugins/SmartStore.DevTools/Settings/ProfilerSettings.cs index be2df2e227..88da20c658 100644 --- a/src/Plugins/SmartStore.DevTools/Settings/ProfilerSettings.cs +++ b/src/Plugins/SmartStore.DevTools/Settings/ProfilerSettings.cs @@ -9,5 +9,8 @@ namespace SmartStore.DevTools public class ProfilerSettings : ISettings { public bool EnableMiniProfilerInPublicStore { get; set; } + + public bool DisplayWidgetZones { get; set; } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj b/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj index e1c88f8d2c..c8f46ee502 100644 --- a/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj +++ b/src/Plugins/SmartStore.DevTools/SmartStore.DevTools.csproj @@ -35,6 +35,7 @@ + true @@ -74,19 +75,19 @@ 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\EntityFramework.6.1.0\lib\net45\EntityFramework.dll - False + + False + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll - - ..\..\packages\EntityFramework.6.1.0\lib\net45\EntityFramework.SqlServer.dll - False + + False + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll @@ -96,6 +97,11 @@ ..\..\packages\MiniProfiler.3.1.1.140\lib\net40\MiniProfiler.dll True + + False + ..\..\packages\MiniProfiler.EF6.3.0.11\lib\net40\MiniProfiler.EntityFramework6.dll + True + @@ -144,17 +150,20 @@ + + + @@ -198,6 +207,12 @@ Designer PreserveNewest + + PreserveNewest + + + PreserveNewest + diff --git a/src/Plugins/SmartStore.DevTools/Starter.cs b/src/Plugins/SmartStore.DevTools/Starter.cs new file mode 100644 index 0000000000..b8b9d3c97c --- /dev/null +++ b/src/Plugins/SmartStore.DevTools/Starter.cs @@ -0,0 +1,31 @@ +using Microsoft.Web.Infrastructure.DynamicModuleHelper; +using SmartStore.Core.Infrastructure; +using SmartStore.Core.Plugins; +using SmartStore.Web.Framework; + +namespace SmartStore.DevTools +{ + + public class ProfilerPreApplicationStart : IPreApplicationStart + { + public void Start() + { + DynamicModuleUtility.RegisterModule(typeof(ProfilerHttpModule)); + SmartUrlRoutingModule.RegisterRoutablePath("/mini-profiler-resources/(.*)"); + } + } + + public class ProfilerStartupTask : IStartupTask + { + public void Execute() + { + StackExchange.Profiling.EntityFramework6.MiniProfilerEF6.Initialize(); + } + + public int Order + { + get { return int.MinValue; } + } + } + +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.DevTools/Views/DevTools/BackendExtension.cshtml b/src/Plugins/SmartStore.DevTools/Views/DevTools/BackendExtension.cshtml new file mode 100644 index 0000000000..d2e818b648 --- /dev/null +++ b/src/Plugins/SmartStore.DevTools/Views/DevTools/BackendExtension.cshtml @@ -0,0 +1,18 @@ +@using SmartStore.DevTools.Models; +@model BackendExtensionModel +@{ + Layout = "~/Administration/Views/Shared/_AdminLayout.cshtml"; + + ViewBag.Title = "My SmartStore.NET backend extension page"; +} + +
+
+ + My SmartStore.NET backend extension page +
+
+ +
+ @Model.Welcome +
diff --git a/src/Plugins/SmartStore.DevTools/Views/DevTools/Configure.cshtml b/src/Plugins/SmartStore.DevTools/Views/DevTools/Configure.cshtml index ebeb4a5ae8..7f1bdc6101 100644 --- a/src/Plugins/SmartStore.DevTools/Views/DevTools/Configure.cshtml +++ b/src/Plugins/SmartStore.DevTools/Views/DevTools/Configure.cshtml @@ -14,6 +14,15 @@ @Html.SettingEditorFor(model => model.EnableMiniProfilerInPublicStore) @Html.ValidationMessageFor(model => model.EnableMiniProfilerInPublicStore) + +
+ + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - - - - + + + + + + + + + + + + - - + + + + + + + } +
"); + sb.AppendLine(""); sb.AppendLine(String.Format("{0}
", _companyInfoSettings.CompanyName )); @@ -597,7 +679,7 @@ protected virtual string GetSupplierIdentification() sb.AppendLine("
"); - sb.AppendLine(""); + sb.AppendLine(""); if (!String.IsNullOrEmpty(_storeContext.CurrentStore.Url)) { @@ -618,7 +700,7 @@ protected virtual string GetSupplierIdentification() sb.AppendLine(""); - sb.AppendLine(""); + sb.AppendLine(""); if (!String.IsNullOrEmpty(_bankConnectionSettings.Bankname)) { @@ -666,9 +748,7 @@ public virtual void AddStoreTokens(IList tokens, Store store) { tokens.Add(new Token("Store.Name", store.Name)); tokens.Add(new Token("Store.URL", store.Url, true)); - var defaultEmailAccount = _emailAccountService.GetEmailAccountById(_emailAccountSettings.DefaultEmailAccountId); - if (defaultEmailAccount == null) - defaultEmailAccount = _emailAccountService.GetAllEmailAccounts().FirstOrDefault(); + var defaultEmailAccount = _emailAccountService.GetDefaultEmailAccount(); tokens.Add(new Token("Store.SupplierIdentification", GetSupplierIdentification(), true)); tokens.Add(new Token("Store.Email", defaultEmailAccount.Email)); } @@ -715,26 +795,26 @@ 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.OrderNumber", order.GetOrderNumber())); + tokens.Add(new Token("Order.ID", order.Id.ToString())); + tokens.Add(new Token("Order.OrderNumber", order.GetOrderNumber())); tokens.Add(new Token("Order.CustomerFullName", string.Format("{0} {1}", order.BillingAddress.FirstName, order.BillingAddress.LastName))); tokens.Add(new Token("Order.CustomerEmail", order.BillingAddress.Email)); - - tokens.Add(new Token("Order.BillingFirstName", order.BillingAddress.FirstName)); - tokens.Add(new Token("Order.BillingLastName", order.BillingAddress.LastName)); - tokens.Add(new Token("Order.BillingPhoneNumber", order.BillingAddress.PhoneNumber)); - tokens.Add(new Token("Order.BillingEmail", order.BillingAddress.Email)); - tokens.Add(new Token("Order.BillingFaxNumber", order.BillingAddress.FaxNumber)); - tokens.Add(new Token("Order.BillingCompany", order.BillingAddress.Company)); - tokens.Add(new Token("Order.BillingAddress1", order.BillingAddress.Address1)); - tokens.Add(new Token("Order.BillingAddress2", order.BillingAddress.Address2)); - tokens.Add(new Token("Order.BillingCity", order.BillingAddress.City)); - tokens.Add(new Token("Order.BillingStateProvince", order.BillingAddress.StateProvince != null ? order.BillingAddress.StateProvince.GetLocalized(x => x.Name) : "")); - tokens.Add(new Token("Order.BillingZipPostalCode", order.BillingAddress.ZipPostalCode)); - tokens.Add(new Token("Order.BillingCountry", order.BillingAddress.Country != null ? order.BillingAddress.Country.GetLocalized(x => x.Name) : "")); + tokens.Add(new Token("Order.BillingFirstName", order.BillingAddress.FirstName)); + tokens.Add(new Token("Order.BillingLastName", order.BillingAddress.LastName)); + tokens.Add(new Token("Order.BillingPhoneNumber", order.BillingAddress.PhoneNumber)); + tokens.Add(new Token("Order.BillingEmail", order.BillingAddress.Email)); + tokens.Add(new Token("Order.BillingFaxNumber", order.BillingAddress.FaxNumber)); + tokens.Add(new Token("Order.BillingCompany", order.BillingAddress.Company)); + tokens.Add(new Token("Order.BillingAddress1", order.BillingAddress.Address1)); + tokens.Add(new Token("Order.BillingAddress2", order.BillingAddress.Address2)); + tokens.Add(new Token("Order.BillingCity", order.BillingAddress.City)); + tokens.Add(new Token("Order.BillingStateProvince", order.BillingAddress.StateProvince != null ? order.BillingAddress.StateProvince.GetLocalized(x => x.Name) : "")); + tokens.Add(new Token("Order.BillingZipPostalCode", order.BillingAddress.ZipPostalCode)); + tokens.Add(new Token("Order.BillingCountry", order.BillingAddress.Country != null ? order.BillingAddress.Country.GetLocalized(x => x.Name) : "")); tokens.Add(new Token("Order.ShippingMethod", order.ShippingMethod)); tokens.Add(new Token("Order.ShippingFirstName", order.ShippingAddress != null ? order.ShippingAddress.FirstName : "")); @@ -750,14 +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) : "")); - var paymentMethod = _paymentService.LoadPaymentMethodBySystemName(order.PaymentMethodSystemName); - var paymentMethodName = paymentMethod != null ? GetLocalizedValue(paymentMethod.Metadata, "FriendlyName", x => x.FriendlyName) : order.PaymentMethodSystemName; - tokens.Add(new Token("Order.PaymentMethod", paymentMethodName)); + string paymentMethodName = null; + var paymentMethod = _providerManager.GetProvider(order.PaymentMethodSystemName); + if (paymentMethod != null) + { + paymentMethodName = GetLocalizedValue(paymentMethod.Metadata, "FriendlyName", x => x.FriendlyName); + } + if (paymentMethodName.IsEmpty()) + { + paymentMethodName = order.PaymentMethodSystemName; + } + + tokens.Add(new Token("Order.PaymentMethod", paymentMethodName)); tokens.Add(new Token("Order.VatNumber", order.VatNumber)); - tokens.Add(new Token("Order.Product(s)", ProductListToHtmlTable(order, 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)); @@ -768,11 +856,11 @@ public virtual void AddOrderTokens(IList tokens, Order order, int languag tokens.Add(new Token("Order.CreatedOn", order.CreatedOnUtc.ToString("D"))); } - //TODO add a method for getting URL (use routing because it handles all SEO friendly URLs) + // 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); @@ -795,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 @@ -839,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)); @@ -857,13 +958,13 @@ public virtual void AddGiftCardTokens(IList tokens, GiftCard giftCard) public virtual void AddCustomerTokens(IList tokens, Customer customer) { - tokens.Add(new Token("Customer.Email", customer.Email)); + tokens.Add(new Token("Customer.ID", customer.Id.ToString())); + tokens.Add(new Token("Customer.Email", customer.Email)); tokens.Add(new Token("Customer.Username", customer.Username)); tokens.Add(new Token("Customer.FullName", customer.GetFullName())); tokens.Add(new Token("Customer.VatNumber", customer.GetAttribute(SystemCustomerAttributeNames.VatNumber))); tokens.Add(new Token("Customer.VatNumberStatus", ((VatNumberStatus)customer.GetAttribute(SystemCustomerAttributeNames.VatNumberStatusId)).ToString())); - //note: we do not use SEO friendly URLS because we can get errors caused by having .(dot) in the URL (from the emauk address) //TODO add a method for getting URL (use routing because it handles all SEO friendly URLs) string passwordRecoveryUrl = string.Format("{0}customer/passwordrecoveryconfirm?token={1}&email={2}", _webHelper.GetStoreLocation(), @@ -921,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, @@ -964,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); @@ -986,7 +1103,15 @@ public virtual void AddPrivateMessageTokens(IList tokens, PrivateMessage public virtual void AddBackInStockTokens(IList tokens, BackInStockSubscription subscription) { - tokens.Add(new Token("BackInStockSubscription.ProductName", subscription.Product.Name)); + var customerLangId = subscription.Customer.GetAttribute( + SystemCustomerAttributeNames.LanguageId, + _attrService, + _storeContext.CurrentStore.Id); + + var store = _storeService.GetStoreById(subscription.StoreId); + var productLink = "{0}{1}".FormatWith(store.Url, subscription.Product.GetSeName(customerLangId, _urlRecordService, _languageService)); + + tokens.Add(new Token("BackInStockSubscription.ProductName", "{1}".FormatWith(productLink, subscription.Product.Name), true)); //event notification _eventPublisher.EntityTokensAdded(subscription, tokens); @@ -1059,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)%", @@ -1076,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 74bd6a41c3..dde1d076c9 100644 --- a/src/Libraries/SmartStore.Services/Messages/NewsLetterSubscriptionService.cs +++ b/src/Libraries/SmartStore.Services/Messages/NewsLetterSubscriptionService.cs @@ -1,167 +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(); - var autoCommit = _subscriptionRepository.AutoCommitEnabled; - var validateOnSave = _subscriptionRepository.Context.ValidateOnSaveEnabled; - var autoDetectChanges = _subscriptionRepository.Context.AutoDetectChangesEnabled; - var proxyCreation = _subscriptionRepository.Context.ProxyCreationEnabled; - - try - { - using (var reader = new StreamReader(stream)) - { - _subscriptionRepository.Context.ValidateOnSaveEnabled = false; - _subscriptionRepository.Context.AutoDetectChangesEnabled = false; - _subscriptionRepository.Context.ProxyCreationEnabled = false; - - 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 = false; - toUpdate.Each(x => - { - _subscriptionRepository.Update(x); - }); - _subscriptionRepository.Context.SaveChanges(); - toUpdate.Clear(); - } - catch (Exception ex) - { - throw ex; - } - finally - { - _subscriptionRepository.AutoCommitEnabled = autoCommit; - _subscriptionRepository.Context.ValidateOnSaveEnabled = validateOnSave; - _subscriptionRepository.Context.AutoDetectChangesEnabled = autoDetectChanges; - _subscriptionRepository.Context.ProxyCreationEnabled = proxyCreation; - } - - return result; } /// @@ -255,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); @@ -266,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/QueuedEmailService.cs b/src/Libraries/SmartStore.Services/Messages/QueuedEmailService.cs index fb53c2aa04..b03c247f39 100644 --- a/src/Libraries/SmartStore.Services/Messages/QueuedEmailService.cs +++ b/src/Libraries/SmartStore.Services/Messages/QueuedEmailService.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; +using System.Net.Mail; +using System.IO; using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Domain.Messages; @@ -8,40 +11,37 @@ using SmartStore.Core.Events; using SmartStore.Core.Logging; using SmartStore.Services.Localization; +using SmartStore.Utilities; +using System.Web; +using SmartStore.Core.Localization; namespace SmartStore.Services.Messages { public partial class QueuedEmailService : IQueuedEmailService { private readonly IRepository _queuedEmailRepository; - private readonly IEventPublisher _eventPublisher; + private readonly IRepository _queuedEmailAttachmentRepository; private readonly IEmailSender _emailSender; - private readonly ILogger _logger; - private readonly ILocalizationService _localizationService; - - /// - /// Ctor - /// - /// Queued email repository - /// Event published + private readonly ICommonServices _services; + public QueuedEmailService( IRepository queuedEmailRepository, - IEventPublisher eventPublisher, - IEmailSender emailSender, - ILogger logger, - ILocalizationService localizationService) + IRepository queuedEmailAttachmentRepository, + IEmailSender emailSender, + ICommonServices services) { - _queuedEmailRepository = queuedEmailRepository; - _eventPublisher = eventPublisher; - _emailSender = emailSender; - _logger = logger; - _localizationService = localizationService; + this._queuedEmailRepository = queuedEmailRepository; + this._queuedEmailAttachmentRepository = queuedEmailAttachmentRepository; + this._emailSender = emailSender; + this._services = services; + + T = NullLocalizer.Instance; + Logger = NullLogger.Instance; } - /// - /// Inserts a queued email - /// - /// Queued email + public Localizer T { get; set; } + public ILogger Logger { get; set; } + public virtual void InsertQueuedEmail(QueuedEmail queuedEmail) { if (queuedEmail == null) @@ -50,13 +50,9 @@ public virtual void InsertQueuedEmail(QueuedEmail queuedEmail) _queuedEmailRepository.Insert(queuedEmail); //event notification - _eventPublisher.EntityInserted(queuedEmail); + _services.EventPublisher.EntityInserted(queuedEmail); } - /// - /// Updates a queued email - /// - /// Queued email public virtual void UpdateQueuedEmail(QueuedEmail queuedEmail) { if (queuedEmail == null) @@ -65,13 +61,9 @@ public virtual void UpdateQueuedEmail(QueuedEmail queuedEmail) _queuedEmailRepository.Update(queuedEmail); //event notification - _eventPublisher.EntityUpdated(queuedEmail); + _services.EventPublisher.EntityUpdated(queuedEmail); } - /// - /// Deleted a queued email - /// - /// Queued email public virtual void DeleteQueuedEmail(QueuedEmail queuedEmail) { if (queuedEmail == null) @@ -80,14 +72,15 @@ public virtual void DeleteQueuedEmail(QueuedEmail queuedEmail) _queuedEmailRepository.Delete(queuedEmail); //event notification - _eventPublisher.EntityDeleted(queuedEmail); + _services.EventPublisher.EntityDeleted(queuedEmail); } - /// - /// Gets a queued email by identifier - /// - /// Queued email identifier - /// Queued email + public virtual int DeleteAllQueuedEmails() + { + // do not delete e-mails which are about to be sent + return _queuedEmailRepository.DeleteAll(x => x.SentOnUtc.HasValue || x.SentTries >= 3); + } + public virtual QueuedEmail GetQueuedEmailById(int queuedEmailId) { if (queuedEmailId == 0) @@ -95,14 +88,8 @@ public virtual QueuedEmail GetQueuedEmailById(int queuedEmailId) var queuedEmail = _queuedEmailRepository.GetById(queuedEmailId); return queuedEmail; - } - /// - /// Get queued emails by identifiers - /// - /// queued email identifiers - /// Queued emails public virtual IList GetQueuedEmailsByIds(int[] queuedEmailIds) { if (queuedEmailIds == null || queuedEmailIds.Length == 0) @@ -114,7 +101,7 @@ where queuedEmailIds.Contains(qe.Id) var queuedEmails = query.ToList(); - //sort by passed identifiers + // sort by passed identifiers var sortedQueuedEmails = new List(); foreach (int id in queuedEmailIds) @@ -126,97 +113,59 @@ where queuedEmailIds.Contains(qe.Id) return sortedQueuedEmails; } - /// - /// Gets all queued emails - /// - /// From Email - /// To Email - /// The start time - /// The end time - /// A value indicating whether to load only not sent emails - /// Maximum send tries - /// A value indicating whether we should sort queued email descending; otherwise, ascending. - /// Page index - /// Page size - /// A value indicating whether to load manually send emails - /// Email item list - public virtual IPagedList SearchEmails(string fromEmail, - string toEmail, DateTime? startTime, DateTime? endTime, - bool loadNotSentItemsOnly, int maxSendTries, - bool loadNewest, int pageIndex, int pageSize, - bool? sendManually = null) + public virtual IPagedList SearchEmails(SearchEmailsQuery query) { - fromEmail = (fromEmail ?? String.Empty).Trim(); - toEmail = (toEmail ?? String.Empty).Trim(); + Guard.ArgumentNotNull(() => query); - var query = _queuedEmailRepository.Table; + var q = _queuedEmailRepository.Table; - if (!String.IsNullOrEmpty(fromEmail)) - query = query.Where(qe => qe.From.Contains(fromEmail)); + if (query.Expand.HasValue()) + { + var expands = query.Expand.Split(','); + foreach (var expand in expands) + { + q = q.Expand(expand.Trim()); + } + } - if (!String.IsNullOrEmpty(toEmail)) - query = query.Where(qe => qe.To.Contains(toEmail)); + if (query.From.HasValue()) + q = q.Where(qe => qe.From.Contains(query.From.Trim())); - if (startTime.HasValue) - query = query.Where(qe => qe.CreatedOnUtc >= startTime); + if (query.To.HasValue()) + q = q.Where(qe => qe.To.Contains(query.To.Trim())); - if (endTime.HasValue) - query = query.Where(qe => qe.CreatedOnUtc <= endTime); + if (query.StartTime.HasValue) + q = q.Where(qe => qe.CreatedOnUtc >= query.StartTime); - if (loadNotSentItemsOnly) - query = query.Where(qe => !qe.SentOnUtc.HasValue); + if (query.EndTime.HasValue) + q = q.Where(qe => qe.CreatedOnUtc <= query.EndTime); - if (sendManually.HasValue) - query = query.Where(qe => qe.SendManually == sendManually.Value); + if (query.UnsentOnly) + q = q.Where(qe => !qe.SentOnUtc.HasValue); - query = query.Where(qe => qe.SentTries < maxSendTries); + if (query.SendManually.HasValue) + q = q.Where(qe => qe.SendManually == query.SendManually.Value); + + q = q.Where(qe => qe.SentTries < query.MaxSendTries); - query = query.OrderByDescending(qe => qe.Priority); + q = q.OrderByDescending(qe => qe.Priority); - query = loadNewest ? - ((IOrderedQueryable)query).ThenByDescending(qe => qe.CreatedOnUtc) : - ((IOrderedQueryable)query).ThenBy(qe => qe.CreatedOnUtc); + q = query.OrderByLatest ? + ((IOrderedQueryable)q).ThenByDescending(qe => qe.CreatedOnUtc) : + ((IOrderedQueryable)q).ThenBy(qe => qe.CreatedOnUtc); - var queuedEmails = new PagedList(query, pageIndex, pageSize); + var queuedEmails = new PagedList(q, query.PageIndex, query.PageSize); return queuedEmails; } - /// - /// Sends a queued email - /// - /// Queued email - /// Whether the operation succeeded public virtual bool SendEmail(QueuedEmail queuedEmail) { var result = false; try { - var bcc = String.IsNullOrWhiteSpace(queuedEmail.Bcc) ? null : queuedEmail.Bcc.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - var cc = String.IsNullOrWhiteSpace(queuedEmail.CC) ? null : queuedEmail.CC.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - var smtpContext = new SmtpContext(queuedEmail.EmailAccount); - - var msg = new EmailMessage( - new EmailAddress(queuedEmail.To, queuedEmail.ToName), - queuedEmail.Subject, - queuedEmail.Body, - new EmailAddress(queuedEmail.From, queuedEmail.FromName)); - - if (queuedEmail.ReplyTo.HasValue()) - { - msg.ReplyTo.Add(new EmailAddress(queuedEmail.ReplyTo, queuedEmail.ReplyToName)); - } - - if (cc != null) - { - msg.Cc.AddRange(cc.Where(x => x.HasValue()).Select(x => new EmailAddress(x))); - } - - if (bcc != null) - { - msg.Bcc.AddRange(bcc.Where(x => x.HasValue()).Select(x => new EmailAddress(x))); - } + var msg = ConvertEmail(queuedEmail); _emailSender.SendEmail(smtpContext, msg); @@ -225,14 +174,134 @@ public virtual bool SendEmail(QueuedEmail queuedEmail) } catch (Exception exc) { - _logger.Error(string.Concat(_localizationService.GetResource("Admin.Common.ErrorSendingEmail"), ": ", exc.Message), exc); + Logger.Error(string.Concat(T("Admin.Common.ErrorSendingEmail"), ": ", exc.Message), exc); } finally { queuedEmail.SentTries = queuedEmail.SentTries + 1; UpdateQueuedEmail(queuedEmail); } + return result; } - } + + private void AddEmailAddresses(string addresses, ICollection target) + { + var arr = addresses.IsEmpty() ? null : addresses.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + if (arr != null && arr.Length > 0) + { + target.AddRange(arr.Where(x => x.Trim().HasValue()).Select(x => new EmailAddress(x))); + } + } + + internal EmailMessage ConvertEmail(QueuedEmail qe) + { + // 'internal' for testing purposes + + var msg = new EmailMessage( + new EmailAddress(qe.To, qe.ToName), + qe.Subject, + qe.Body, + new EmailAddress(qe.From, qe.FromName)); + + if (qe.ReplyTo.HasValue()) + { + msg.ReplyTo.Add(new EmailAddress(qe.ReplyTo, qe.ReplyToName)); + } + + AddEmailAddresses(qe.CC, msg.Cc); + AddEmailAddresses(qe.Bcc, msg.Bcc); + + if (qe.Attachments != null && qe.Attachments.Count > 0) + { + foreach (var qea in qe.Attachments) + { + Attachment attachment = null; + + if (qea.StorageLocation == EmailAttachmentStorageLocation.Blob) + { + var data = qea.Data; + if (data != null && data.Length > 0) + { + attachment = new Attachment(data.ToStream(), qea.Name, qea.MimeType); + } + } + else if (qea.StorageLocation == EmailAttachmentStorageLocation.Path) + { + var path = qea.Path; + if (path.HasValue()) + { + if (path[0] == '~' || path[0] == '/') + { + path = CommonHelper.MapPath(VirtualPathUtility.ToAppRelative(path), false); + } + if (File.Exists(path)) + { + attachment = new Attachment(path, qea.MimeType); + attachment.Name = qea.Name; + } + } + } + else if (qea.StorageLocation == EmailAttachmentStorageLocation.FileReference) + { + var file = qea.File; + if (file != null && file.UseDownloadUrl == false && file.DownloadBinary != null && file.DownloadBinary.Length > 0) + { + attachment = new Attachment(file.DownloadBinary.ToStream(), file.Filename + file.Extension, file.ContentType); + } + } + + if (attachment != null) + { + msg.Attachments.Add(attachment); + } + } + } + + return msg; + } + + #region Attachments + + public virtual QueuedEmailAttachment GetQueuedEmailAttachmentById(int id) + { + if (id == 0) + return null; + + var qea = _queuedEmailAttachmentRepository.GetById(id); + return qea; + } + + public virtual void DeleteQueuedEmailAttachment(QueuedEmailAttachment qea) + { + if (qea == null) + throw new ArgumentNullException("qea"); + + _queuedEmailAttachmentRepository.Delete(qea); + + _services.EventPublisher.EntityDeleted(qea); + } + + #endregion + } + + public class SearchEmailsQuery + { + public string From { get; set; } + public string To { get; set; } + public DateTime? StartTime { get; set; } + public DateTime? EndTime { get; set; } + public bool UnsentOnly { get; set; } + public int MaxSendTries { get; set; } + public bool OrderByLatest { get; set; } + public int PageIndex { get; set; } + public int PageSize { get; set; } + public bool? SendManually { get; set; } + + /// + /// Navigation properties to eager load (comma separataed) + /// + public string Expand { get; set; } + } + } diff --git a/src/Libraries/SmartStore.Services/Messages/QueuedMessagesClearTask.cs b/src/Libraries/SmartStore.Services/Messages/QueuedMessagesClearTask.cs new file mode 100644 index 0000000000..b31f3b4ce3 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Messages/QueuedMessagesClearTask.cs @@ -0,0 +1,31 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Messages; +using SmartStore.Data; +using SmartStore.Services.Tasks; + +namespace SmartStore.Services.Messages +{ + /// + /// Represents a task for deleting sent emails from the message queue + /// + public partial class QueuedMessagesClearTask : ITask + { + private readonly IRepository _qeRepository; + + public QueuedMessagesClearTask(IRepository qeRepository) + { + this._qeRepository = qeRepository; + } + + public void Execute(TaskExecutionContext ctx) + { + var olderThan = DateTime.UtcNow.AddDays(-14); + _qeRepository.DeleteAll(x => x.SentOnUtc.HasValue && x.CreatedOnUtc < olderThan); + + _qeRepository.Context.ShrinkDatabase(); + } + } +} diff --git a/src/Libraries/SmartStore.Services/Messages/QueuedMessagesSendTask.cs b/src/Libraries/SmartStore.Services/Messages/QueuedMessagesSendTask.cs index cd3110c81d..a37e7e82c4 100644 --- a/src/Libraries/SmartStore.Services/Messages/QueuedMessagesSendTask.cs +++ b/src/Libraries/SmartStore.Services/Messages/QueuedMessagesSendTask.cs @@ -9,20 +9,27 @@ public partial class QueuedMessagesSendTask : ITask { private readonly IQueuedEmailService _queuedEmailService; - public QueuedMessagesSendTask( - IQueuedEmailService queuedEmailService) + public QueuedMessagesSendTask(IQueuedEmailService queuedEmailService) { _queuedEmailService = queuedEmailService; } public void Execute(TaskExecutionContext ctx) { - const int pageSize = 100; + const int pageSize = 1000; const int maxTries = 3; for (int i = 0; i < 9999999; ++i) { - var queuedEmails = _queuedEmailService.SearchEmails(null, null, null, null, true, maxTries, false, i, pageSize, false); + var q = new SearchEmailsQuery + { + MaxSendTries = maxTries, + PageIndex = i, + PageSize = pageSize, + Expand = "Attachments", + UnsentOnly = true + }; + var queuedEmails = _queuedEmailService.SearchEmails(q); foreach (var queuedEmail in queuedEmails) { diff --git a/src/Libraries/SmartStore.Services/Messages/QueuingEmailEvent.cs b/src/Libraries/SmartStore.Services/Messages/QueuingEmailEvent.cs new file mode 100644 index 0000000000..d9efdacd84 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Messages/QueuingEmailEvent.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using SmartStore.Core.Domain.Messages; + +namespace SmartStore.Services.Messages +{ + /// + /// An event message, which gets published just before a new instance + /// of is persisted to the database + /// + public class QueuingEmailEvent + { + public QueuedEmail QueuedEmail + { + get; + set; + } + + public MessageTemplate MessageTemplate + { + get; + set; + } + + public EmailAccount EmailAccount + { + get; + set; + } + + public IList Tokens + { + get; + set; + } + + public int LanguageId + { + get; + set; + } + } +} diff --git a/src/Libraries/SmartStore.Services/Messages/QueuingEmailEventConsumer.cs b/src/Libraries/SmartStore.Services/Messages/QueuingEmailEventConsumer.cs new file mode 100644 index 0000000000..11eca682cc --- /dev/null +++ b/src/Libraries/SmartStore.Services/Messages/QueuingEmailEventConsumer.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Mime; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using System.Web.Mvc; +using System.Web.Security; +using SmartStore.Core; +using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Messages; +using SmartStore.Core.Events; +using SmartStore.Core.Localization; +using SmartStore.Core.Logging; +using SmartStore.Utilities; + +namespace SmartStore.Services.Messages +{ + public class QueuingEmailEventConsumer : IConsumer + { + private readonly PdfSettings _pdfSettings; + private readonly HttpRequestBase _httpRequest; + private readonly Lazy _fileDownloadManager; + + public QueuingEmailEventConsumer( + PdfSettings pdfSettings, + HttpRequestBase httpRequest, + Lazy fileDownloadManager) + { + this._pdfSettings = pdfSettings; + this._httpRequest = httpRequest; + this._fileDownloadManager = fileDownloadManager; + + Logger = NullLogger.Instance; + T = NullLocalizer.Instance; + } + + public ILogger Logger { get; set; } + public Localizer T { get; set; } + + public void HandleEvent(QueuingEmailEvent eventMessage) + { + var qe = eventMessage.QueuedEmail; + var tpl = eventMessage.MessageTemplate; + + var handledTemplates = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "OrderPlaced.CustomerNotification", _pdfSettings.AttachOrderPdfToOrderPlacedEmail }, + { "OrderCompleted.CustomerNotification", _pdfSettings.AttachOrderPdfToOrderCompletedEmail } + }; + + bool shouldHandle = false; + if (handledTemplates.TryGetValue(tpl.Name, out shouldHandle) && shouldHandle) + { + var orderId = eventMessage.Tokens.First(x => x.Key.IsCaseInsensitiveEqual("Order.ID")).Value.ToInt(); + try + { + var qea = CreatePdfInvoiceAttachment(orderId); + qe.Attachments.Add(qea); + } + catch (Exception ex) + { + Logger.Error(T("Admin.System.QueuedEmails.ErrorCreatingAttachment"), ex); + } + } + } + + private QueuedEmailAttachment CreatePdfInvoiceAttachment(int orderId) + { + var urlHelper = new UrlHelper(_httpRequest.RequestContext); + var path = urlHelper.Action("Print", "Order", new { id = orderId, pdf = true, area = "" }); + + var fileResponse = _fileDownloadManager.Value.DownloadFile(path, true, 5000); + + if (fileResponse == null) + { + throw new InvalidOperationException(T("Admin.System.QueuedEmails.ErrorEmptyAttachmentResult", path)); + } + + if (!fileResponse.ContentType.IsCaseInsensitiveEqual("application/pdf")) + { + throw new InvalidOperationException(T("Admin.System.QueuedEmails.ErrorNoPdfAttachment")); + } + + return new QueuedEmailAttachment + { + StorageLocation = EmailAttachmentStorageLocation.Blob, + Data = fileResponse.Data, + MimeType = fileResponse.ContentType, + Name = fileResponse.FileName + }; + } + + } +} diff --git a/src/Libraries/SmartStore.Services/Messages/WorkflowMessageService.cs b/src/Libraries/SmartStore.Services/Messages/WorkflowMessageService.cs index bbad203299..1f2746245e 100644 --- a/src/Libraries/SmartStore.Services/Messages/WorkflowMessageService.cs +++ b/src/Libraries/SmartStore.Services/Messages/WorkflowMessageService.cs @@ -8,14 +8,17 @@ using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Forums; +using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Messages; using SmartStore.Core.Domain.News; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Domain.Stores; using SmartStore.Core.Events; +using SmartStore.Core.Localization; using SmartStore.Services.Customers; using SmartStore.Services.Localization; +using SmartStore.Services.Media; using SmartStore.Services.Stores; namespace SmartStore.Services.Messages @@ -36,21 +39,26 @@ public partial class WorkflowMessageService : IWorkflowMessageService private readonly IEventPublisher _eventPublisher; private readonly IWorkContext _workContext; private readonly HttpRequestBase _httpRequest; + private readonly IDownloadService _downloadServioce; #endregion #region Ctor - public WorkflowMessageService(IMessageTemplateService messageTemplateService, - IQueuedEmailService queuedEmailService, ILanguageService languageService, - ITokenizer tokenizer, IEmailAccountService emailAccountService, + public WorkflowMessageService( + IMessageTemplateService messageTemplateService, + IQueuedEmailService queuedEmailService, + ILanguageService languageService, + ITokenizer tokenizer, + IEmailAccountService emailAccountService, IMessageTokenProvider messageTokenProvider, IStoreService storeService, IStoreContext storeContext, EmailAccountSettings emailAccountSettings, IEventPublisher eventPublisher, IWorkContext workContext, - HttpRequestBase httpRequest) + HttpRequestBase httpRequest, + IDownloadService downloadServioce) { this._messageTemplateService = messageTemplateService; this._queuedEmailService = queuedEmailService; @@ -63,18 +71,23 @@ public WorkflowMessageService(IMessageTemplateService messageTemplateService, this._emailAccountSettings = emailAccountSettings; this._eventPublisher = eventPublisher; this._workContext = workContext; - _httpRequest = httpRequest; - } + 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, - IEnumerable tokens, + IList tokens, string toEmailAddress, string toName, string replyTo = null, @@ -90,7 +103,7 @@ protected int SendNotification( var bodyReplaced = _tokenizer.Replace(body, tokens, true); bodyReplaced = WebHelper.MakeAllUrlsAbsolute(bodyReplaced, _httpRequest); - + var email = new QueuedEmail { Priority = 5, @@ -109,16 +122,52 @@ protected int SendNotification( SendManually = messageTemplate.SendManually }; + // create attachments if any + var fileIds = (new int?[] + { + messageTemplate.GetLocalized(x => x.Attachment1FileId, languageId), + messageTemplate.GetLocalized(x => x.Attachment2FileId, languageId), + messageTemplate.GetLocalized(x => x.Attachment3FileId, languageId) + }) + .Where(x => x.HasValue) + .Select(x => x.Value) + .ToArray(); + + if (fileIds.Any()) + { + var files = _downloadServioce.GetDownloadsByIds(fileIds); + foreach (var file in files) + { + email.Attachments.Add(new QueuedEmailAttachment + { + StorageLocation = EmailAttachmentStorageLocation.FileReference, + FileId = file.Id, + Name = (file.Filename.NullEmpty() ?? file.Id.ToString()) + file.Extension.EmptyNull(), + MimeType = file.ContentType.NullEmpty() ?? "application/octet-stream" + }); + } + } + + + // publish event so that integrators can add attachments, alter the email etc. + _eventPublisher.Publish(new QueuingEmailEvent + { + EmailAccount = emailAccount, + LanguageId = languageId, + MessageTemplate = messageTemplate, + QueuedEmail = email, + Tokens = tokens + }); + _queuedEmailService.InsertQueuedEmail(email); + return email.Id; } - protected MessageTemplate GetLocalizedActiveMessageTemplate(string messageTemplateName, int languageId, int storeId) + protected MessageTemplate GetActiveMessageTemplate(string messageTemplateName, int storeId) { - //TODO remove languageId parameter var messageTemplate = _messageTemplateService.GetMessageTemplateByName(messageTemplateName, storeId); - //no template found if (messageTemplate == null) return null; @@ -135,9 +184,7 @@ protected EmailAccount GetEmailAccountOfMessageTemplate(MessageTemplate messageT var emailAccounId = messageTemplate.GetLocalized(mt => mt.EmailAccountId, languageId); var emailAccount = _emailAccountService.GetEmailAccountById(emailAccounId); if (emailAccount == null) - emailAccount = _emailAccountService.GetEmailAccountById(_emailAccountSettings.DefaultEmailAccountId); - if (emailAccount == null) - emailAccount = _emailAccountService.GetAllEmailAccounts().FirstOrDefault(); + emailAccount = _emailAccountService.GetDefaultEmailAccount(); return emailAccount; } @@ -191,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); @@ -208,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 @@ -230,9 +278,9 @@ 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 = GetLocalizedActiveMessageTemplate("NewCustomer.Notification", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("NewCustomer.Notification", store.Id); if (messageTemplate == null) return 0; @@ -244,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); } /// @@ -269,9 +314,9 @@ 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 = GetLocalizedActiveMessageTemplate("Customer.WelcomeMessage", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("Customer.WelcomeMessage", store.Id); if (messageTemplate == null) return 0; @@ -283,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); } /// @@ -303,9 +347,9 @@ 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 = GetLocalizedActiveMessageTemplate("Customer.EmailValidationMessage", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("Customer.EmailValidationMessage", store.Id); if (messageTemplate == null) return 0; @@ -317,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); } /// @@ -337,9 +380,9 @@ 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 = GetLocalizedActiveMessageTemplate("Customer.PasswordRecovery", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("Customer.PasswordRecovery", store.Id); if (messageTemplate == null) return 0; @@ -351,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 @@ -375,22 +417,22 @@ 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 = GetLocalizedActiveMessageTemplate("OrderPlaced.StoreOwnerNotification", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("OrderPlaced.StoreOwnerNotification", store.Id); if (messageTemplate == null) return 0; //tokens var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddOrderTokens(tokens, order, 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; @@ -402,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); } /// @@ -420,31 +459,30 @@ 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 = GetLocalizedActiveMessageTemplate("OrderPlaced.CustomerNotification", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("OrderPlaced.CustomerNotification", store.Id); if (messageTemplate == null) return 0; //tokens var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddOrderTokens(tokens, order, languageId); + _messageTokenProvider.AddOrderTokens(tokens, order, language); _messageTokenProvider.AddCustomerTokens(tokens, order.Customer); _messageTokenProvider.AddCompanyTokens(tokens); _messageTokenProvider.AddBankConnectionTokens(tokens); _messageTokenProvider.AddContactDataTokens(tokens); - //event notification + // 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); } /// @@ -460,31 +498,30 @@ 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 = GetLocalizedActiveMessageTemplate("ShipmentSent.CustomerNotification", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("ShipmentSent.CustomerNotification", store.Id); if (messageTemplate == null) return 0; //tokens var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddShipmentTokens(tokens, shipment, 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); } /// @@ -500,31 +537,30 @@ 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 = GetLocalizedActiveMessageTemplate("ShipmentDelivered.CustomerNotification", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("ShipmentDelivered.CustomerNotification", store.Id); if (messageTemplate == null) return 0; //tokens var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddShipmentTokens(tokens, shipment, 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); } /// @@ -539,16 +575,16 @@ 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 = GetLocalizedActiveMessageTemplate("OrderCompleted.CustomerNotification", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("OrderCompleted.CustomerNotification", store.Id); if (messageTemplate == null) return 0; //tokens var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddOrderTokens(tokens, order, languageId); + _messageTokenProvider.AddOrderTokens(tokens, order, language); _messageTokenProvider.AddCustomerTokens(tokens, order.Customer); _messageTokenProvider.AddCompanyTokens(tokens); @@ -558,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); } /// @@ -578,27 +613,26 @@ 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 = GetLocalizedActiveMessageTemplate("OrderCancelled.CustomerNotification", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("OrderCancelled.CustomerNotification", store.Id); if (messageTemplate == null) return 0; //tokens var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddOrderTokens(tokens, order, 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); } /// @@ -615,9 +649,9 @@ 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 = GetLocalizedActiveMessageTemplate("Customer.NewOrderNote", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("Customer.NewOrderNote", store.Id); if (messageTemplate == null) return 0; @@ -625,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); } /// @@ -651,29 +684,27 @@ 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 = GetLocalizedActiveMessageTemplate("RecurringPaymentCancelled.StoreOwnerNotification", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("RecurringPaymentCancelled.StoreOwnerNotification", store.Id); if (messageTemplate == null) return 0; //tokens var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddOrderTokens(tokens, recurringPayment.InitialOrder, 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 @@ -686,16 +717,15 @@ 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 = GetLocalizedActiveMessageTemplate("NewsLetterSubscription.ActivationMessage", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("NewsLetterSubscription.ActivationMessage", store.Id); if (messageTemplate == null) return 0; @@ -707,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); } /// @@ -721,16 +750,15 @@ 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 = GetLocalizedActiveMessageTemplate("NewsLetterSubscription.DeactivationMessage", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("NewsLetterSubscription.DeactivationMessage", store.Id); if (messageTemplate == null) return 0; @@ -741,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 @@ -773,9 +800,9 @@ 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 = GetLocalizedActiveMessageTemplate("Service.EmailAFriend", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("Service.EmailAFriend", store.Id); if (messageTemplate == null) return 0; @@ -783,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, @@ -811,16 +838,16 @@ 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 = GetLocalizedActiveMessageTemplate("Product.AskQuestion", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("Product.AskQuestion", store.Id); if (messageTemplate == null) return 0; var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); _messageTokenProvider.AddCustomerTokens(tokens, customer); - _messageTokenProvider.AddProductTokens(tokens, product, languageId); + _messageTokenProvider.AddProductTokens(tokens, product, language); tokens.Add(new Token("ProductQuestion.Message", question, true)); tokens.Add(new Token("ProductQuestion.SenderEmail", senderEmail)); @@ -830,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); } /// @@ -853,9 +880,9 @@ 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 = GetLocalizedActiveMessageTemplate("Wishlist.EmailAFriend", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("Wishlist.EmailAFriend", store.Id); if (messageTemplate == null) return 0; @@ -869,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 @@ -894,9 +920,9 @@ 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 = GetLocalizedActiveMessageTemplate("NewReturnRequest.StoreOwnerNotification", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("NewReturnRequest.StoreOwnerNotification", store.Id); if (messageTemplate == null) return 0; @@ -909,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); } /// @@ -935,9 +958,9 @@ 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 = GetLocalizedActiveMessageTemplate("ReturnRequestStatusChanged.CustomerNotification", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("ReturnRequestStatusChanged.CustomerNotification", store.Id); if (messageTemplate == null) return 0; @@ -950,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 @@ -972,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) { @@ -981,8 +1003,9 @@ public int SendNewForumTopicMessage(Customer customer, } var store = _storeContext.CurrentStore; + var language = EnsureLanguageIsActive(languageId, store.Id); - var messageTemplate = GetLocalizedActiveMessageTemplate("Forums.NewForumTopic", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("Forums.NewForumTopic", store.Id); if (messageTemplate == null) { return 0; @@ -993,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); } /// @@ -1015,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) { @@ -1025,8 +1046,9 @@ public int SendNewForumPostMessage(Customer customer, } var store = _storeContext.CurrentStore; + var language = EnsureLanguageIsActive(languageId, store.Id); - var messageTemplate = GetLocalizedActiveMessageTemplate("Forums.NewForumPost", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("Forums.NewForumPost", store.Id); if (messageTemplate == null) { return 0; @@ -1038,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); } /// @@ -1065,7 +1087,7 @@ public int SendPrivateMessageNotification(Customer customer, PrivateMessage priv var store = _storeService.GetStoreById(privateMessage.StoreId) ?? _storeContext.CurrentStore; - var messageTemplate = GetLocalizedActiveMessageTemplate("Customer.NewPM", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("Customer.NewPM", store.Id); if (messageTemplate == null) { return 0; @@ -1122,9 +1144,10 @@ public virtual int SendGenericMessage(string messageTemplateName, Action @@ -1190,16 +1212,15 @@ 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 = GetLocalizedActiveMessageTemplate("Product.ProductReview", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("Product.ProductReview", store.Id); if (messageTemplate == null) return 0; @@ -1211,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); } /// @@ -1236,25 +1254,24 @@ 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 = GetLocalizedActiveMessageTemplate("QuantityBelow.StoreOwnerNotification", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("QuantityBelow.StoreOwnerNotification", store.Id); if (messageTemplate == null) return 0; var tokens = new List(); _messageTokenProvider.AddStoreTokens(tokens, store); - _messageTokenProvider.AddProductTokens(tokens, product, 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); } /// @@ -1265,16 +1282,15 @@ 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 = GetLocalizedActiveMessageTemplate("NewVATSubmitted.StoreOwnerNotification", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("NewVATSubmitted.StoreOwnerNotification", store.Id); if (messageTemplate == null) return 0; @@ -1288,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); } /// @@ -1313,9 +1326,9 @@ 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 = GetLocalizedActiveMessageTemplate("Blog.BlogComment", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("Blog.BlogComment", store.Id); if (messageTemplate == null) return 0; @@ -1327,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); } /// @@ -1352,9 +1362,9 @@ 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 = GetLocalizedActiveMessageTemplate("News.NewsComment", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("News.NewsComment", store.Id); if (messageTemplate == null) return 0; @@ -1366,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); } /// @@ -1391,9 +1398,9 @@ 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 = GetLocalizedActiveMessageTemplate("Customer.BackInStock", languageId, store.Id); + var messageTemplate = GetActiveMessageTemplate("Customer.BackInStock", store.Id); if (messageTemplate == null) return 0; @@ -1406,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 e2308c6260..d25a8d8afa 100644 --- a/src/Libraries/SmartStore.Services/News/INewsService.cs +++ b/src/Libraries/SmartStore.Services/News/INewsService.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using SmartStore.Core; using SmartStore.Core.Domain.News; @@ -37,8 +38,9 @@ public partial interface INewsService /// Page index /// Page size /// A value indicating whether to show hidden records + /// The maximum age of returned news /// News items - IPagedList GetAllNews(int languageId, int storeId, int pageIndex, int pageSize, bool showHidden = false); + IPagedList GetAllNews(int languageId, int storeId, int pageIndex, int pageSize, bool showHidden = false, DateTime? maxAge = null); /// /// Inserts a news item diff --git a/src/Libraries/SmartStore.Services/News/NewsService.cs b/src/Libraries/SmartStore.Services/News/NewsService.cs index c715f78a84..743ab7f9f4 100644 --- a/src/Libraries/SmartStore.Services/News/NewsService.cs +++ b/src/Libraries/SmartStore.Services/News/NewsService.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using SmartStore.Core; -using SmartStore.Core.Caching; using SmartStore.Core.Data; using SmartStore.Core.Domain.News; using SmartStore.Core.Domain.Stores; @@ -18,8 +17,9 @@ public partial class NewsService : INewsService private readonly IRepository _newsItemRepository; private readonly IRepository _storeMappingRepository; - private readonly ICacheManager _cacheManager; - private readonly IEventPublisher _eventPublisher; + private readonly ICommonServices _services; + + private readonly NewsSettings _newsSettings; #endregion @@ -27,13 +27,13 @@ public partial class NewsService : INewsService public NewsService(IRepository newsItemRepository, IRepository storeMappingRepository, - ICacheManager cacheManager, - IEventPublisher eventPublisher) + ICommonServices services, + NewsSettings newsSettings) { _newsItemRepository = newsItemRepository; _storeMappingRepository = storeMappingRepository; - _cacheManager = cacheManager; - _eventPublisher = eventPublisher; + _services = services; + _newsSettings = newsSettings; this.QuerySettings = DbQuerySettings.Default; } @@ -56,7 +56,7 @@ public virtual void DeleteNews(NewsItem newsItem) _newsItemRepository.Delete(newsItem); //event notification - _eventPublisher.EntityDeleted(newsItem); + _services.EventPublisher.EntityDeleted(newsItem); } /// @@ -98,12 +98,22 @@ where newsIds.Contains(x.Id) /// Page index /// Page size /// A value indicating whether to show hidden records + /// The maximum age of returned news /// News items - public virtual IPagedList GetAllNews(int languageId, int storeId, int pageIndex, int pageSize, bool showHidden = false) + public virtual IPagedList GetAllNews(int languageId, int storeId, int pageIndex, int pageSize, bool showHidden = false, DateTime? maxAge = null) { var query = _newsItemRepository.Table; - if (languageId > 0) - query = query.Where(n => languageId == n.LanguageId); + + if (languageId > 0) + { + query = query.Where(n => languageId == n.LanguageId); + } + + if (maxAge.HasValue) + { + query = query.Where(n => n.CreatedOnUtc >= maxAge.Value); + } + if (!showHidden) { var utcNow = DateTime.UtcNow; @@ -111,6 +121,7 @@ public virtual IPagedList GetAllNews(int languageId, int storeId, int query = query.Where(n => !n.StartDateUtc.HasValue || n.StartDateUtc <= utcNow); query = query.Where(n => !n.EndDateUtc.HasValue || n.EndDateUtc >= utcNow); } + query = query.OrderByDescending(n => n.CreatedOnUtc); //Store mapping @@ -128,6 +139,7 @@ from sm in n_sm.DefaultIfEmpty() group n by n.Id into nGroup orderby nGroup.Key select nGroup.FirstOrDefault(); + query = query.OrderByDescending(n => n.CreatedOnUtc); } @@ -147,7 +159,7 @@ public virtual void InsertNews(NewsItem news) _newsItemRepository.Insert(news); //event notification - _eventPublisher.EntityInserted(news); + _services.EventPublisher.EntityInserted(news); } /// @@ -162,7 +174,7 @@ public virtual void UpdateNews(NewsItem news) _newsItemRepository.Update(news); //event notification - _eventPublisher.EntityUpdated(news); + _services.EventPublisher.EntityUpdated(news); } /// 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 d0a6a6769d..9fca02b17f 100644 --- a/src/Libraries/SmartStore.Services/Orders/IOrderService.cs +++ b/src/Libraries/SmartStore.Services/Orders/IOrderService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; @@ -69,6 +70,32 @@ public partial interface IOrderService /// The order void DeleteOrder(Order order); + /// + /// Get orders + /// + /// Store identifier; null to load all orders + /// Customer identifier; null to load all orders + /// Order start time; null to load all orders + /// Order end time; null to load all orders + /// Filter by order status + /// Filter by payment status + /// Filter by shipping status + /// Billing email. Leave empty to load all records. + /// Filter by order number + /// Billing name. Leave empty to load all records. + /// Order query + IQueryable GetOrders( + int storeId, + int customerId, + DateTime? startTime, + DateTime? endTime, + int[] orderStatusIds, + int[] paymentStatusIds, + int[] shippingStatusIds, + string billingEmail, + string orderNumber, + string billingName = null); + /// /// Search orders /// @@ -137,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 @@ -173,6 +208,13 @@ IList GetAllOrderItems(int? orderId, OrderStatus? os, PaymentStatus? ps, ShippingStatus? ss, bool loadDownloableProductsOnly = false); + /// + /// Get order items by order identifiers + /// + /// Order identifiers + /// Order items + Multimap GetOrderItemsByOrderIds(int[] orderIds); + /// /// Delete an order item /// diff --git a/src/Libraries/SmartStore.Services/Orders/IShoppingCartService.cs b/src/Libraries/SmartStore.Services/Orders/IShoppingCartService.cs index 517811cc78..d8479eb218 100644 --- a/src/Libraries/SmartStore.Services/Orders/IShoppingCartService.cs +++ b/src/Libraries/SmartStore.Services/Orders/IShoppingCartService.cs @@ -58,18 +58,25 @@ IList GetStandardWarnings(Customer customer, ShoppingCartType shoppingCa Product product, string selectedAttributes, decimal customerEnteredPrice, int quantity); - /// - /// Validates shopping cart item attributes - /// + /// + /// Validates shopping cart item attributes + /// /// The customer - /// Shopping cart type + /// Shopping cart type /// Product - /// Selected attributes + /// Selected attributes + /// The product variant attribute combination instance (reduces database roundtrips) /// Quantity /// Product bundle item - /// Warnings - IList GetShoppingCartItemAttributeWarnings(Customer customer, ShoppingCartType shoppingCartType, - Product product, string selectedAttributes, int quantity = 1, ProductBundleItem bundleItem = null); + /// Warnings + IList GetShoppingCartItemAttributeWarnings( + Customer customer, + ShoppingCartType shoppingCartType, + Product product, + string selectedAttributes, + int quantity = 1, + ProductBundleItem bundleItem = null, + ProductVariantAttributeCombination combination = null); /// /// Validates shopping cart item (gift card) diff --git a/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs b/src/Libraries/SmartStore.Services/Orders/OrderProcessingService.cs index a1b07c3749..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 T(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,49 +365,25 @@ 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(T("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) + if (prevOrderStatus != OrderStatus.Complete && os == OrderStatus.Complete && notifyCustomer) { //notification int orderCompletedCustomerNotificationQueuedEmailId = _workflowMessageService.SendOrderCompletedCustomerNotification(order, order.CustomerLanguageId); if (orderCompletedCustomerNotificationQueuedEmailId > 0) { - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(T("CustomerCompletedEmailQueued"), orderCompletedCustomerNotificationQueuedEmailId), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerCompletedEmailQueued", orderCompletedCustomerNotificationQueuedEmailId)); } } - if (prevOrderStatus != OrderStatus.Cancelled && - os == OrderStatus.Cancelled - && notifyCustomer) + if (prevOrderStatus != OrderStatus.Cancelled && os == OrderStatus.Cancelled && notifyCustomer) { //notification int orderCancelledCustomerNotificationQueuedEmailId = _workflowMessageService.SendOrderCancelledCustomerNotification(order, order.CustomerLanguageId); if (orderCancelledCustomerNotificationQueuedEmailId > 0) { - order.OrderNotes.Add(new OrderNote() - { - Note = string.Format(T("CustomerCancelledEmailQueued"), orderCancelledCustomerNotificationQueuedEmailId), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerCancelledEmailQueued", orderCancelledCustomerNotificationQueuedEmailId)); } } @@ -404,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); } @@ -437,8 +429,7 @@ public void CheckOrderStatus(Order order) if (order.OrderStatus == OrderStatus.Pending) { - if (order.PaymentStatus == PaymentStatus.Authorized || - order.PaymentStatus == PaymentStatus.Paid) + if (order.PaymentStatus == PaymentStatus.Authorized || order.PaymentStatus == PaymentStatus.Paid) { SetOrderStatus(order, OrderStatus.Processing, false); } @@ -446,16 +437,13 @@ public void CheckOrderStatus(Order order) if (order.OrderStatus == OrderStatus.Pending) { - if (order.ShippingStatus == ShippingStatus.PartiallyShipped || - order.ShippingStatus == ShippingStatus.Shipped || - order.ShippingStatus == ShippingStatus.Delivered) + if (order.ShippingStatus == ShippingStatus.PartiallyShipped || order.ShippingStatus == ShippingStatus.Shipped || order.ShippingStatus == ShippingStatus.Delivered) { SetOrderStatus(order, OrderStatus.Processing, false); } } - if (order.OrderStatus != OrderStatus.Cancelled && - order.OrderStatus != OrderStatus.Complete) + if (order.OrderStatus != OrderStatus.Cancelled && order.OrderStatus != OrderStatus.Complete) { if (order.PaymentStatus == PaymentStatus.Paid) { @@ -476,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"); @@ -492,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) @@ -521,7 +513,9 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR var currencyTmp = _currencyService.GetCurrencyById(customer.GetAttribute(SystemCustomerAttributeNames.CurrencyId, processPaymentRequest.StoreId)); var customerCurrency = (currencyTmp != null && currencyTmp.Published) ? currencyTmp : _workContext.WorkingCurrency; customerCurrencyCode = customerCurrency.CurrencyCode; - var primaryStoreCurrency = _currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId); + + var primaryStoreCurrency = _storeContext.CurrentStore.PrimaryStoreCurrency; + customerCurrencyRate = customerCurrency.Rate / primaryStoreCurrency.Rate; } else @@ -530,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) { @@ -621,7 +605,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR customerTaxDisplayType = initialOrder.CustomerTaxDisplayType; } - //checkout attributes + // checkout attributes string checkoutAttributeDescription, checkoutAttributesXml; if (!processPaymentRequest.IsRecurringPayment) { @@ -634,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; @@ -654,11 +638,11 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR orderSubTotalInclTax = subTotalWithoutDiscountBase1; orderSubTotalDiscountInclTax = orderSubTotalDiscountAmount1; - //discount history - if (orderSubTotalAppliedDiscount1 != null && !appliedDiscounts.ContainsDiscount(orderSubTotalAppliedDiscount1)) + // 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; @@ -677,7 +661,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR } - //shipping info + // shipping info bool shoppingCartRequiresShipping = false; if (!processPaymentRequest.IsRecurringPayment) { @@ -693,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; @@ -707,7 +691,7 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR } } - //shipping total + // shipping total decimal? orderShippingTotalInclTax, orderShippingTotalExclTax = null; decimal orderShippingTaxRate = decimal.Zero; if (!processPaymentRequest.IsRecurringPayment) @@ -716,9 +700,9 @@ 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.ContainsDiscount(shippingTotalDiscount)) + if (shippingTotalDiscount != null && !appliedDiscounts.Any(x => x.Id == shippingTotalDiscount.Id)) appliedDiscounts.Add(shippingTotalDiscount); } else @@ -755,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) @@ -783,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.ContainsDiscount(orderAppliedDiscount)) - appliedDiscounts.Add(orderAppliedDiscount); + //discount history + if (orderAppliedDiscount != null && !appliedDiscounts.Any(x => x.Id == orderAppliedDiscount.Id)) + { + appliedDiscounts.Add(orderAppliedDiscount); + } } else { @@ -802,41 +790,38 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR #endregion #region Addresses & pre payment workflow - + // give payment processor the opportunity to fullfill billing address var preProcessPaymentResult = _paymentService.PreProcessPayment(processPaymentRequest); if (!preProcessPaymentResult.Success) { - foreach (var paymentError in preProcessPaymentResult.Errors) - { - result.AddError(string.Format("Payment error: {0}", paymentError)); - } - - throw new SmartException("Error while pre-processing the payment"); + result.Errors.AddRange(preProcessPaymentResult.Errors); + result.Errors.Add(T("Common.Error.PreProcessPayment")); + return result; } Address billingAddress = null; 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) @@ -844,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; @@ -912,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); } } @@ -944,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; @@ -961,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; @@ -970,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) @@ -1001,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, @@ -1069,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; @@ -1095,16 +1092,19 @@ 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.ContainsDiscount(scDiscount)) - appliedDiscounts.Add(scDiscount); + + if (scDiscount != null && !appliedDiscounts.Any(x => x.Id == scDiscount.Id)) + { + 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); //save order item - var orderItem = new OrderItem() + var orderItem = new OrderItem { OrderItemGuid = Guid.NewGuid(), Order = order, @@ -1132,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); } @@ -1149,15 +1149,14 @@ 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++) { - var gc = new GiftCard() + var gc = new GiftCard { GiftCardType = sc.Item.Product.GiftCardType, PurchasedWithOrderItem = orderItem, @@ -1176,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 { @@ -1190,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, @@ -1221,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, @@ -1245,7 +1243,6 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR } } - //inventory _productService.AdjustInventory(orderItem, true, orderItem.Quantity); } } @@ -1255,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, @@ -1270,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, @@ -1287,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); } @@ -1297,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, @@ -1309,7 +1307,6 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR }; _orderService.InsertRecurringPayment(rp); - var recurringPaymentType = _paymentService.GetRecurringPaymentType(processPaymentRequest.PaymentMethodSystemName); switch (recurringPaymentType) { @@ -1341,50 +1338,34 @@ public virtual PlaceOrderResult PlaceOrder(ProcessPaymentRequest processPaymentR } } - #endregion + #endregion - #region Notifications, notes and attributes - - //notes, messages - order.OrderNotes.Add(new OrderNote() - { - Note = T("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(T("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(T("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)) @@ -1395,29 +1376,50 @@ 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 if (order.PaymentStatus == PaymentStatus.Paid) { _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) @@ -1460,14 +1462,8 @@ public virtual void DeleteOrder(Order order) _productService.AdjustInventory(orderItem, false, orderItem.Quantity); } - //add a note - order.OrderNotes.Add(new OrderNote() - { - Note = T("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); @@ -1482,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, @@ -1514,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; } } @@ -1558,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 = T("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(T("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; } /// @@ -1674,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); @@ -1687,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(T("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(T("CustomerShippedEmailQueued"), queuedEmailId), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerShippedEmailQueued", queuedEmailId)); } } @@ -1730,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(T("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(T("CustomerDeliveredEmailQueued"), queuedEmailId), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.CustomerDeliveredEmailQueued", queuedEmailId)); } } @@ -1800,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 = T("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); @@ -1923,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 = T("OrderMarkedAsAuthorized"), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsAuthorized")); //check order status CheckOrderStatus(order); @@ -1957,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)) { @@ -1985,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; @@ -2007,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; @@ -2020,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 = T("OrderCaptured"), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderCaptured")); + CheckOrderStatus(order); //raise event @@ -2047,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(T("OrderCaptureError"), order.GetOrderNumber(), error); - _logger.InsertLog(LogLevel.Error, logError, logError); - } - return result.Errors; + return result.Errors; } /// @@ -2112,24 +2015,18 @@ 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 = T("OrderMarkedAsPaid"), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); + _orderService.UpdateOrder(order); + + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsPaid")); CheckOrderStatus(order); - //raise event + // raise event if (order.PaymentStatus == PaymentStatus.Paid) { _eventPublisher.PublishOrderPaid(order); @@ -2173,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; @@ -2182,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 @@ -2191,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(T("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(T("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; } /// @@ -2273,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; @@ -2284,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(T("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); } @@ -2344,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; @@ -2364,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(T("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(T("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; } /// @@ -2456,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; @@ -2465,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(T("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); } @@ -2517,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; @@ -2530,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 = T("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(T("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; } @@ -2611,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 = T("OrderMarkedAsVoided"), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); _orderService.UpdateOrder(order); + _orderService.AddOrderNote(order, T("Admin.OrderNotice.OrderMarkedAsVoided")); + //check orer status CheckOrderStatus(order); } @@ -2642,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(); @@ -2681,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; @@ -2708,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; @@ -2737,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; } @@ -2744,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 296951ab48..7ed45bb61a 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; @@ -12,10 +13,10 @@ namespace SmartStore.Services.Orders { - /// - /// Order service - /// - public partial class OrderService : IOrderService + /// + /// Order service + /// + public partial class OrderService : IOrderService { #region Fields @@ -182,54 +183,45 @@ from o in _orderRepository.Table return query.FirstOrDefault(); } - /// - /// Search orders - /// - /// Store identifier; 0 to load all orders - /// Customer identifier; 0 to load all orders - /// Order start time; null to load all orders - /// Order end time; null to load all orders - /// Filter by order status - /// Filter by payment status - /// Filter by shipping status - /// Billing email. Leave empty to load all records. - /// Search by order GUID (Global unique identifier) or part of GUID. Leave empty to load all orders. - /// Filter by order number - /// Page index - /// Page size - /// Billing name. Leave empty to load all records. - /// Order collection - public virtual IPagedList SearchOrders(int storeId, int customerId, DateTime? startTime, DateTime? endTime, - int[] orderStatusIds, int[] paymentStatusIds, int[] shippingStatusIds, - string billingEmail, string orderGuid, string orderNumber, int pageIndex, int pageSize, string billingName = null) - { - var query = _orderRepository.Table; + public virtual IQueryable GetOrders( + int storeId, + int customerId, + DateTime? startTime, + DateTime? endTime, + int[] orderStatusIds, + int[] paymentStatusIds, + int[] shippingStatusIds, + string billingEmail, + string orderNumber, + string billingName = null) + { + var query = _orderRepository.Table; if (storeId > 0) - query = query.Where(o => o.StoreId == storeId); + query = query.Where(x => x.StoreId == storeId); if (customerId > 0) - query = query.Where(o => o.CustomerId == customerId); + query = query.Where(x => x.CustomerId == customerId); - if (startTime.HasValue) - query = query.Where(o => startTime.Value <= o.CreatedOnUtc); + if (startTime.HasValue) + query = query.Where(x => startTime.Value <= x.CreatedOnUtc); - if (endTime.HasValue) - query = query.Where(o => endTime.Value >= o.CreatedOnUtc); + if (endTime.HasValue) + query = query.Where(x => endTime.Value >= x.CreatedOnUtc); - if (!String.IsNullOrEmpty(billingEmail)) - query = query.Where(o => o.BillingAddress != null && !String.IsNullOrEmpty(o.BillingAddress.Email) && o.BillingAddress.Email.Contains(billingEmail)); + if (billingEmail.HasValue()) + query = query.Where(x => x.BillingAddress != null && !String.IsNullOrEmpty(x.BillingAddress.Email) && x.BillingAddress.Email.Contains(billingEmail)); if (billingName.HasValue()) { - query = query.Where(o => o.BillingAddress != null && ( - (!String.IsNullOrEmpty(o.BillingAddress.LastName) && o.BillingAddress.LastName.Contains(billingName)) || - (!String.IsNullOrEmpty(o.BillingAddress.FirstName) && o.BillingAddress.FirstName.Contains(billingName)) + query = query.Where(x => x.BillingAddress != null && ( + (!String.IsNullOrEmpty(x.BillingAddress.LastName) && x.BillingAddress.LastName.Contains(billingName)) || + (!String.IsNullOrEmpty(x.BillingAddress.FirstName) && x.BillingAddress.FirstName.Contains(billingName)) )); } - if (orderNumber.HasValue()) - query = query.Where(o => o.OrderNumber.ToLower().Contains(orderNumber.ToLower())); + if (orderNumber.HasValue()) + query = query.Where(x => x.OrderNumber.ToLower().Contains(orderNumber.ToLower())); if (orderStatusIds != null && orderStatusIds.Count() > 0) query = query.Where(x => orderStatusIds.Contains(x.OrderStatusId)); @@ -240,14 +232,43 @@ public virtual IPagedList SearchOrders(int storeId, int customerId, DateT if (shippingStatusIds != null && shippingStatusIds.Count() > 0) query = query.Where(x => shippingStatusIds.Contains(x.ShippingStatusId)); - query = query.Where(o => !o.Deleted); - query = query.OrderByDescending(o => o.CreatedOnUtc); + query = query.Where(x => !x.Deleted); + + return query; + } + + /// + /// Search orders + /// + /// Store identifier; 0 to load all orders + /// Customer identifier; 0 to load all orders + /// Order start time; null to load all orders + /// Order end time; null to load all orders + /// Filter by order status + /// Filter by payment status + /// Filter by shipping status + /// Billing email. Leave empty to load all records. + /// Search by order GUID (Global unique identifier) or part of GUID. Leave empty to load all orders. + /// Filter by order number + /// Page index + /// Page size + /// Billing name. Leave empty to load all records. + /// Order collection + public virtual IPagedList SearchOrders(int storeId, int customerId, DateTime? startTime, DateTime? endTime, + int[] orderStatusIds, int[] paymentStatusIds, int[] shippingStatusIds, + string billingEmail, string orderGuid, string orderNumber, int pageIndex, int pageSize, string billingName = null) + { + var query = GetOrders(storeId, customerId, startTime, endTime, orderStatusIds, paymentStatusIds, shippingStatusIds, + billingEmail, orderNumber, billingName); + + query = query.OrderByDescending(x => x.CreatedOnUtc); - if (!String.IsNullOrEmpty(orderGuid)) + if (orderGuid.HasValue()) { //filter by GUID. Filter in BLL because EF doesn't support casting of GUID to string var orders = query.ToList(); - orders = orders.FindAll(o => o.OrderGuid.ToString().ToLowerInvariant().Contains(orderGuid.ToLowerInvariant())); + orders = orders.FindAll(x => x.OrderGuid.ToString().ToLowerInvariant().Contains(orderGuid.ToLowerInvariant())); + return new PagedList(orders, pageIndex, pageSize); } else @@ -371,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)) @@ -385,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; @@ -469,6 +504,23 @@ join p in _productRepository.Table on orderItem.ProductId equals p.Id return orderItems; } + public virtual Multimap GetOrderItemsByOrderIds(int[] orderIds) + { + Guard.ArgumentNotNull(() => orderIds); + + var query = + from x in _orderItemRepository.TableUntracked.Expand(x => x.Product) + where orderIds.Contains(x.OrderId) + select x; + + var map = query + .OrderBy(x => x.OrderId) + .ToList() + .ToMultimap(x => x.OrderId, x => x); + + return map; + } + /// /// Delete an Order item /// diff --git a/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs b/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs index 8beaa3da51..e154dd519d 100644 --- a/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs +++ b/src/Libraries/SmartStore.Services/Orders/OrderTotalCalculationService.cs @@ -9,6 +9,8 @@ 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; using SmartStore.Services.Discounts; @@ -18,10 +20,10 @@ namespace SmartStore.Services.Orders { - /// - /// Order service - /// - public partial class OrderTotalCalculationService : IOrderTotalCalculationService + /// + /// Order service + /// + public partial class OrderTotalCalculationService : IOrderTotalCalculationService { #region Fields @@ -30,7 +32,7 @@ public partial class OrderTotalCalculationService : IOrderTotalCalculationServic private readonly IPriceCalculationService _priceCalculationService; private readonly ITaxService _taxService; private readonly IShippingService _shippingService; - private readonly IPaymentService _paymentService; + private readonly IProviderManager _providerManager; private readonly ICheckoutAttributeParser _checkoutAttributeParser; private readonly IDiscountService _discountService; private readonly IGiftCardService _giftCardService; @@ -69,7 +71,7 @@ public OrderTotalCalculationService(IWorkContext workContext, IPriceCalculationService priceCalculationService, ITaxService taxService, IShippingService shippingService, - IPaymentService paymentService, + IProviderManager providerManager, ICheckoutAttributeParser checkoutAttributeParser, IDiscountService discountService, IGiftCardService giftCardService, @@ -86,7 +88,7 @@ public OrderTotalCalculationService(IWorkContext workContext, this._priceCalculationService = priceCalculationService; this._taxService = taxService; this._shippingService = shippingService; - this._paymentService = paymentService; + this._providerManager = providerManager; this._checkoutAttributeParser = checkoutAttributeParser; this._discountService = discountService; this._giftCardService = giftCardService; @@ -97,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) @@ -354,8 +360,7 @@ public virtual decimal GetOrderSubtotalDiscount(Customer customer, { foreach (var discount in allDiscounts) { - if (_discountService.IsDiscountValid(discount, customer) && discount.DiscountType == DiscountType.AssignedToOrderSubTotal && - !allowedDiscounts.ContainsDiscount(discount)) + if (discount.DiscountType == DiscountType.AssignedToOrderSubTotal && !allowedDiscounts.Any(x => x.Id == discount.Id) && _discountService.IsDiscountValid(discount, customer)) { allowedDiscounts.Add(discount); } @@ -613,11 +618,11 @@ public virtual decimal AdjustShippingRate(decimal shippingRate, IList(); - if (allDiscounts != null) - foreach (var discount in allDiscounts) - if (_discountService.IsDiscountValid(discount, customer) && - discount.DiscountType == DiscountType.AssignedToShipping && - !allowedDiscounts.ContainsDiscount(discount)) - allowedDiscounts.Add(discount); + + if (allDiscounts != null) + { + foreach (var discount in allDiscounts) + { + if (discount.DiscountType == DiscountType.AssignedToShipping && !allowedDiscounts.Any(x => x.Id == discount.Id) && _discountService.IsDiscountValid(discount, customer)) + { + allowedDiscounts.Add(discount); + } + } + } appliedDiscount = allowedDiscounts.GetPreferredDiscount(shippingTotal); if (appliedDiscount != null) @@ -798,8 +808,15 @@ public virtual decimal GetTaxTotal(IList cart, out So if (usePaymentMethodAdditionalFee && _taxSettings.PaymentMethodAdditionalFeeIsTaxable) { decimal taxRate = decimal.Zero; + + var provider = _providerManager.GetProvider(paymentMethodSystemName); + var paymentMethodAdditionalFee = (provider != null ? provider.Value.GetAdditionalHandlingFee(cart) : decimal.Zero); + + if (_shoppingCartSettings.RoundPricesDuringCalculation) + { + paymentMethodAdditionalFee = Math.Round(paymentMethodAdditionalFee, 2); + } - decimal paymentMethodAdditionalFee = _paymentService.GetAdditionalHandlingFee(cart, paymentMethodSystemName); decimal paymentMethodAdditionalFeeExclTax = _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, false, customer, out taxRate); decimal paymentMethodAdditionalFeeInclTax = _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, true, customer, out taxRate); @@ -852,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); } /// @@ -892,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; @@ -906,8 +921,15 @@ public virtual decimal GetTaxTotal(IList cart, out So decimal paymentMethodAdditionalFeeWithoutTax = decimal.Zero; if (usePaymentMethodAdditionalFee && !String.IsNullOrEmpty(paymentMethodSystemName)) { - decimal paymentMethodAdditionalFee = _paymentService.GetAdditionalHandlingFee(cart, paymentMethodSystemName); - paymentMethodAdditionalFeeWithoutTax = _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, false, customer); + var provider = _providerManager.GetProvider(paymentMethodSystemName); + var paymentMethodAdditionalFee = (provider != null ? provider.Value.GetAdditionalHandlingFee(cart) : decimal.Zero); + + if (_shoppingCartSettings.RoundPricesDuringCalculation) + { + paymentMethodAdditionalFee = Math.Round(paymentMethodAdditionalFee, 2); + } + + paymentMethodAdditionalFeeWithoutTax = _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, false, customer); } //tax @@ -1063,8 +1085,7 @@ public virtual decimal GetOrderTotalDiscount(Customer customer, decimal orderTot { foreach (var discount in allDiscounts) { - if (_discountService.IsDiscountValid(discount, customer) && discount.DiscountType == DiscountType.AssignedToOrderTotal && - !allowedDiscounts.ContainsDiscount(discount)) + if (discount.DiscountType == DiscountType.AssignedToOrderTotal && !allowedDiscounts.Any(x => x.Id == discount.Id) &&_discountService.IsDiscountValid(discount, customer)) { allowedDiscounts.Add(discount); } @@ -1116,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/ShoppingCartExtensions.cs b/src/Libraries/SmartStore.Services/Orders/ShoppingCartExtensions.cs index 9054c7ffe7..84dad37b83 100644 --- a/src/Libraries/SmartStore.Services/Orders/ShoppingCartExtensions.cs +++ b/src/Libraries/SmartStore.Services/Orders/ShoppingCartExtensions.cs @@ -177,7 +177,7 @@ public static IList Organize(this IList - /// Shopping cart service - /// - public partial class ShoppingCartService : IShoppingCartService + /// + /// Shopping cart service + /// + public partial class ShoppingCartService : IShoppingCartService { #region Fields @@ -71,8 +72,10 @@ public partial class ShoppingCartService : IShoppingCartService /// ACL service /// Store mapping service /// Generic attribute service - public ShoppingCartService(IRepository sciRepository, - IWorkContext workContext, IStoreContext storeContext, + public ShoppingCartService( + IRepository sciRepository, + IWorkContext workContext, + IStoreContext storeContext, ICurrencyService currencyService, IProductService productService, ILocalizationService localizationService, IProductAttributeParser productAttributeParser, @@ -110,20 +113,24 @@ public ShoppingCartService(IRepository sciRepository, 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) @@ -269,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))); } } } @@ -312,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 @@ -371,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)) + ); } } @@ -383,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; @@ -412,30 +423,30 @@ 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)); } } } break; case ManageInventoryMethod.ManageStockByAttributes: { - var combination = product.ProductVariantAttributeCombinations - .FirstOrDefault(x => _productAttributeParser.AreProductAttributesEqual(x.AttributesXml, selectedAttributes)); + var combination = _productAttributeParser.FindProductVariantAttributeCombination(product.Id, selectedAttributes); - if (combination != null) + if (combination != null) { if (!combination.AllowOutOfStockOrders && combination.StockQuantity < quantity) { 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)); } } } @@ -446,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; } } @@ -463,27 +474,22 @@ 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; } - /// - /// Validates shopping cart item attributes - /// - /// The customer - /// Shopping cart type - /// Product - /// Selected attributes - /// Quantity - /// Product bundle item - /// Warnings - public virtual IList GetShoppingCartItemAttributeWarnings(Customer customer, ShoppingCartType shoppingCartType, - Product product, string selectedAttributes, int quantity = 1, ProductBundleItem bundleItem = null) + public virtual IList GetShoppingCartItemAttributeWarnings( + Customer customer, + ShoppingCartType shoppingCartType, + Product product, + string selectedAttributes, + int quantity = 1, + ProductBundleItem bundleItem = null, + ProductVariantAttributeCombination combination = null) { - if (product == null) - throw new ArgumentNullException("product"); + Guard.ArgumentNotNull(() => product); var warnings = new List(); @@ -501,7 +507,7 @@ public virtual IList GetShoppingCartItemAttributeWarnings(Customer custo if (pv1 == null || pv1.Id != product.Id) { - warnings.Add(_localizationService.GetResource("ShoppingCart.AttributeError")); + warnings.Add(T("ShoppingCart.AttributeError")); return warnings; } } @@ -537,8 +543,7 @@ public virtual IList GetShoppingCartItemAttributeWarnings(Customer custo 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))); } } } @@ -546,19 +551,20 @@ public virtual IList GetShoppingCartItemAttributeWarnings(Customer custo // check if there is a selected attribute combination and if it is active if (warnings.Count == 0 && selectedAttributes.HasValue()) { - var combination = product - .ProductVariantAttributeCombinations - .FirstOrDefault(x => _productAttributeParser.AreProductAttributesEqual(x.AttributesXml, selectedAttributes)); + if (combination == null) + { + combination = _productAttributeParser.FindProductVariantAttributeCombination(product.Id, selectedAttributes); + } if (combination != null && !combination.IsActive) { - warnings.Add(_localizationService.GetResource("ShoppingCart.NotAvailable")); + warnings.Add(T("ShoppingCart.NotAvailable")); } } if (warnings.Count == 0) { - var pvaValues = _productAttributeParser.ParseProductVariantAttributeValues(selectedAttributes); + var pvaValues = _productAttributeParser.ParseProductVariantAttributeValues(selectedAttributes).ToList(); foreach (var pvaValue in pvaValues) { if (pvaValue.ValueType == ProductVariantAttributeValueType.ProductLinkage) @@ -571,18 +577,16 @@ public virtual IList GetShoppingCartItemAttributeWarnings(Customer custo 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)); } } } @@ -599,8 +603,11 @@ public virtual IList GetShoppingCartItemAttributeWarnings(Customer custo /// bool public virtual bool AreAllAttributesForCombinationSelected(string selectedAttributes, Product product) { - if (product.ProductVariantAttributeCombinations.Count == 0) - return true; + Guard.ArgumentNotNull(() => product); + + var hasAttributeCombinations = _sciRepository.Context.QueryForCollection(product, (Product p) => p.ProductVariantAttributeCombinations).Any(); + if (!hasAttributeCombinations) + return true; //selected attributes var pva1Collection = _productAttributeParser.ParseProductVariantAttributes(selectedAttributes); @@ -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 new file mode 100644 index 0000000000..4ecd12457d --- /dev/null +++ b/src/Libraries/SmartStore.Services/Payments/IPaymentMethodFilter.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Plugins; + +namespace SmartStore.Services.Payments +{ + public partial interface IPaymentMethodFilter + { + /// + /// Gets a value indicating whether a payment method should be filtered out + /// + /// Payment filter request + /// true filter out method, false do not filter out method + bool IsExcluded(PaymentFilterRequest request); + + /// + /// Get URL for filter configuration + /// + /// Payment provider system name + /// URL for filter configuration + string GetConfigurationUrl(string systemName); + } + + + public partial class PaymentFilterRequest + { + /// + /// The payment method to be checked + /// + public Provider PaymentMethod { get; set; } + + /// + /// The context shopping cart + /// + public IList Cart { get; set; } + + /// + /// The context store identifier + /// + public int StoreId { get; set; } + + /// + /// The context customer + /// + public Customer Customer { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Services/Payments/IPaymentService.cs b/src/Libraries/SmartStore.Services/Payments/IPaymentService.cs index 0d56507652..ced73b9b10 100644 --- a/src/Libraries/SmartStore.Services/Payments/IPaymentService.cs +++ b/src/Libraries/SmartStore.Services/Payments/IPaymentService.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Payments; using SmartStore.Core.Plugins; namespace SmartStore.Services.Payments @@ -12,10 +14,18 @@ public partial interface IPaymentService /// /// Load active payment methods /// - /// Filter payment methods by customer; null to load all records - /// Load records allows only in specified store; pass 0 to load all records + /// 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 - IEnumerable> LoadActivePaymentMethods(int? filterByCustomerId = null, int storeId = 0); + IEnumerable> LoadActivePaymentMethods( + Customer customer = null, + IList cart = null, + int storeId = 0, + PaymentMethodType[] types = null, + bool provideFallbackMethod = true); /// /// Determines whether a payment method is active\enabled for a shop @@ -37,6 +47,37 @@ public partial interface IPaymentService IEnumerable> LoadAllPaymentMethods(int storeId = 0); + /// + /// Gets all payment method extra data + /// + /// List of payment method objects + IList GetAllPaymentMethods(); + + /// + /// Gets payment method extra data by system name + /// + /// Provider system name + /// Payment method entity + PaymentMethod GetPaymentMethodBySystemName(string systemName); + + /// + /// Insert payment method extra data + /// + /// Payment method + void InsertPaymentMethod(PaymentMethod paymentMethod); + + /// + /// Updates payment method extra data + /// + /// Payment method + void UpdatePaymentMethod(PaymentMethod paymentMethod); + + /// + /// Delete payment method extra data + /// + /// Payment method + void DeletePaymentMethod(PaymentMethod paymentMethod); + /// /// Pre process a payment @@ -168,5 +209,10 @@ public partial interface IPaymentService /// Masked credit card number string GetMaskedCreditCardNumber(string creditCardNumber); + /// + /// Gets all payment filters + /// + /// List of payment filters + IList GetAllPaymentMethodFilters(); } } diff --git a/src/Libraries/SmartStore.Services/Payments/PaymentExtentions.cs b/src/Libraries/SmartStore.Services/Payments/PaymentExtentions.cs index 2051adef9c..d9cd258259 100644 --- a/src/Libraries/SmartStore.Services/Payments/PaymentExtentions.cs +++ b/src/Libraries/SmartStore.Services/Payments/PaymentExtentions.cs @@ -1,15 +1,15 @@ using System; -using System.Linq; using System.Collections.Generic; +using System.Linq; +using System.Web.Routing; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; using SmartStore.Core.Plugins; using SmartStore.Services.Orders; -using System.Web.Routing; namespace SmartStore.Services.Payments { - public static class PaymentExtentions + public static class PaymentExtentions { /// /// Is payment method active? diff --git a/src/Libraries/SmartStore.Services/Payments/PaymentService.cs b/src/Libraries/SmartStore.Services/Payments/PaymentService.cs index 69f2d391de..0bce57bb5e 100644 --- a/src/Libraries/SmartStore.Services/Payments/PaymentService.cs +++ b/src/Libraries/SmartStore.Services/Payments/PaymentService.cs @@ -2,27 +2,38 @@ using System.Collections.Generic; using System.Linq; using SmartStore.Core.Data; +using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; +using SmartStore.Core.Events; +using SmartStore.Core.Infrastructure; +using SmartStore.Core.Localization; using SmartStore.Core.Plugins; -using SmartStore.Services.Configuration; -using SmartStore.Services.Localization; namespace SmartStore.Services.Payments { - /// - /// Payment service - /// - public partial class PaymentService : IPaymentService + /// + /// Payment service + /// + public partial class PaymentService : IPaymentService { - #region Fields + #region Constants + + private const string PAYMENTMETHOD_ALL_KEY = "SmartStore.paymentmethod.all"; + #endregion + + #region Fields + + private readonly static object _lock = new object(); + private static IList _paymentMethodFilterTypes = null; + + private readonly IRepository _paymentMethodRepository; private readonly PaymentSettings _paymentSettings; - private readonly IPluginFinder _pluginFinder; private readonly ShoppingCartSettings _shoppingCartSettings; - private readonly ISettingService _settingService; - private readonly ILocalizationService _localizationService; private readonly IProviderManager _providerManager; + private readonly ICommonServices _services; + private readonly ITypeFinder _typeFinder; #endregion @@ -36,55 +47,97 @@ public partial class PaymentService : IPaymentService /// Shopping cart settings /// Plugin service public PaymentService( + IRepository paymentMethodRepository, PaymentSettings paymentSettings, - IPluginFinder pluginFinder, ShoppingCartSettings shoppingCartSettings, - ISettingService settingService, - ILocalizationService localizationService, - IProviderManager providerManager) + IProviderManager providerManager, + ICommonServices services, + ITypeFinder typeFinder) { - this._paymentSettings = paymentSettings; - this._pluginFinder = pluginFinder; - this._shoppingCartSettings = shoppingCartSettings; - this._settingService = settingService; - this._localizationService = localizationService; - this._providerManager = providerManager; - } + _paymentMethodRepository = paymentMethodRepository; + _paymentSettings = paymentSettings; + _shoppingCartSettings = shoppingCartSettings; + _providerManager = providerManager; + _services = services; + _typeFinder = typeFinder; + + T = NullLocalizer.Instance; + } - #endregion + public Localizer T { get; set; } - #region Methods + #endregion - /// - /// Load active payment methods - /// - /// Filter payment methods by customer; null to load all records - /// Load records allows only in specified store; pass 0 to load all records - /// Payment methods - public virtual IEnumerable> LoadActivePaymentMethods(int? filterByCustomerId = null, int storeId = 0) + #region 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 + public virtual IEnumerable> LoadActivePaymentMethods( + Customer customer = null, + IList cart = null, + int storeId = 0, + PaymentMethodType[] types = null, + bool provideFallbackMethod = true) { - var allMethods = LoadAllPaymentMethods(storeId); - var activeMethods = allMethods - .Where(p => p.Value.IsActive && _paymentSettings.ActivePaymentMethodSystemNames.Contains(p.Metadata.SystemName, StringComparer.InvariantCultureIgnoreCase)); + IList allFilters = null; + IEnumerable> allProviders = null; + + var filterRequest = new PaymentFilterRequest + { + Cart = cart, + StoreId = storeId, + Customer = customer + }; + + if (types != null && types.Any()) + allProviders = LoadAllPaymentMethods(storeId).Where(x => types.Contains(x.Value.PaymentMethodType)); + else + allProviders = LoadAllPaymentMethods(storeId); + + var activeProviders = allProviders + .Where(p => + { + if (!p.Value.IsActive || !_paymentSettings.ActivePaymentMethodSystemNames.Contains(p.Metadata.SystemName, StringComparer.InvariantCultureIgnoreCase)) + return false; + + // payment method filtering + if (allFilters == null) + allFilters = GetAllPaymentMethodFilters(); + + filterRequest.PaymentMethod = p; + + if (allFilters.Any(x => x.IsExcluded(filterRequest))) + return false; + + return true; + }); - if (!activeMethods.Any()) + if (!activeProviders.Any() && provideFallbackMethod) { - var fallbackMethod = allMethods.FirstOrDefault(); + var fallbackMethod = allProviders.FirstOrDefault(x => x.IsPaymentMethodActive(_paymentSettings)); + + if (fallbackMethod == null) + fallbackMethod = allProviders.FirstOrDefault(); + if (fallbackMethod != null) { - _paymentSettings.ActivePaymentMethodSystemNames.Clear(); - _paymentSettings.ActivePaymentMethodSystemNames.Add(fallbackMethod.Metadata.SystemName); - _settingService.SaveSetting(_paymentSettings); return new Provider[] { fallbackMethod }; } else { if (DataSettings.DatabaseIsInstalled()) - throw Error.Application("At least one payment method provider is required to be active."); + throw new SmartException(T("Payment.OneActiveMethodProviderRequired")); } } - return activeMethods; + return activeProviders; } /// @@ -122,6 +175,73 @@ public virtual IEnumerable> LoadAllPaymentMethods(int s } + /// + /// Gets all payment method extra data + /// + /// List of payment method objects + public virtual IList GetAllPaymentMethods() + { + var methods = _paymentMethodRepository.TableUntracked.ToList(); + return methods; + } + + /// + /// Gets payment method extra data by system name + /// + /// Provider system name + /// Payment method entity + public virtual PaymentMethod GetPaymentMethodBySystemName(string systemName) + { + if (systemName.HasValue()) + { + return _paymentMethodRepository.Table.FirstOrDefault(x => x.PaymentMethodSystemName == systemName); + } + return null; + } + + /// + /// Insert payment method extra data + /// + /// Payment method + public virtual void InsertPaymentMethod(PaymentMethod paymentMethod) + { + if (paymentMethod == null) + throw new ArgumentNullException("paymentMethod"); + + _paymentMethodRepository.Insert(paymentMethod); + + _services.EventPublisher.EntityInserted(paymentMethod); + } + + /// + /// Updates payment method extra data + /// + /// Payment method + public virtual void UpdatePaymentMethod(PaymentMethod paymentMethod) + { + if (paymentMethod == null) + throw new ArgumentNullException("paymentMethod"); + + _paymentMethodRepository.Update(paymentMethod); + + _services.EventPublisher.EntityUpdated(paymentMethod); + } + + /// + /// Delete payment method extra data + /// + /// Payment method + public virtual void DeletePaymentMethod(PaymentMethod paymentMethod) + { + if (paymentMethod == null) + throw new ArgumentNullException("paymentMethod"); + + _paymentMethodRepository.Delete(paymentMethod); + + _services.EventPublisher.EntityDeleted(paymentMethod); + } + + /// /// Pre process a payment /// @@ -138,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); } @@ -167,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); } } @@ -181,10 +304,14 @@ public virtual ProcessPaymentResult ProcessPayment(ProcessPaymentRequest process /// Payment info required for an order processing public virtual void PostProcessPayment(PostProcessPaymentRequest postProcessPaymentRequest) { - var paymentMethod = LoadPaymentMethodBySystemName(postProcessPaymentRequest.Order.PaymentMethodSystemName); - if (paymentMethod == null) - throw new SmartException("Payment method couldn't be loaded"); - paymentMethod.Value.PostProcessPayment(postProcessPaymentRequest); + if (postProcessPaymentRequest.Order.PaymentMethodSystemName.HasValue()) + { + var paymentMethod = LoadPaymentMethodBySystemName(postProcessPaymentRequest.Order.PaymentMethodSystemName); + if (paymentMethod == null) + throw new SmartException(T("Payment.CouldNotLoadMethod")); + + paymentMethod.Value.PostProcessPayment(postProcessPaymentRequest); + } } /// @@ -230,15 +357,14 @@ public virtual bool CanRePostProcessPayment(Order order) public virtual decimal GetAdditionalHandlingFee(IList cart, string paymentMethodSystemName) { var paymentMethod = LoadPaymentMethodBySystemName(paymentMethodSystemName); - if (paymentMethod == null) - return decimal.Zero; + var paymentMethodAdditionalFee = (paymentMethod != null ? paymentMethod.Value.GetAdditionalHandlingFee(cart) : decimal.Zero); - decimal result = paymentMethod.Value.GetAdditionalHandlingFee(cart); - - if (_shoppingCartSettings.RoundPricesDuringCalculation) - result = Math.Round(result, 2); + if (_shoppingCartSettings.RoundPricesDuringCalculation) + { + paymentMethodAdditionalFee = Math.Round(paymentMethodAdditionalFee, 2); + } - return result; + return paymentMethodAdditionalFee; } @@ -265,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 { @@ -274,7 +400,7 @@ public virtual CapturePaymentResult Capture(CapturePaymentRequest capturePayment catch (NotSupportedException) { var result = new CapturePaymentResult(); - result.AddError(_localizationService.GetResource("Common.Payment.NoCaptureSupport")); + result.AddError(T("Common.Payment.NoCaptureSupport")); return result; } catch @@ -320,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 { @@ -329,7 +455,7 @@ public virtual RefundPaymentResult Refund(RefundPaymentRequest refundPaymentRequ catch (NotSupportedException) { var result = new RefundPaymentResult(); - result.AddError(_localizationService.GetResource("Common.Payment.NoRefundSupport")); + result.AddError(T("Common.Payment.NoRefundSupport")); return result; } catch @@ -362,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 { @@ -371,7 +497,7 @@ public virtual VoidPaymentResult Void(VoidPaymentRequest voidPaymentRequest) catch (NotSupportedException) { var result = new VoidPaymentResult(); - result.AddError(_localizationService.GetResource("Common.Payment.NoVoidSupport")); + result.AddError(T("Common.Payment.NoVoidSupport")); return result; } catch @@ -392,6 +518,7 @@ public virtual RecurringPaymentType GetRecurringPaymentType(string paymentMethod var paymentMethod = LoadPaymentMethodBySystemName(paymentMethodSystemName); if (paymentMethod == null) return RecurringPaymentType.NotSupported; + return paymentMethod.Value.RecurringPaymentType; } @@ -414,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 { @@ -423,7 +550,7 @@ public virtual ProcessPaymentResult ProcessRecurringPayment(ProcessPaymentReques catch (NotSupportedException) { var result = new ProcessPaymentResult(); - result.AddError(_localizationService.GetResource("Common.Payment.NoRecurringPaymentSupport")); + result.AddError(T("Common.Payment.NoRecurringPaymentSupport")); return result; } catch @@ -445,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 { @@ -454,7 +581,7 @@ public virtual CancelRecurringPaymentResult CancelRecurringPayment(CancelRecurri catch (NotSupportedException) { var result = new CancelRecurringPaymentResult(); - result.AddError(_localizationService.GetResource("Common.Payment.NoRecurringPaymentSupport")); + result.AddError(T("Common.Payment.NoRecurringPaymentSupport")); return result; } catch @@ -475,6 +602,7 @@ public virtual PaymentMethodType GetPaymentMethodType(string paymentMethodSystem var paymentMethod = LoadPaymentMethodBySystemName(paymentMethodSystemName); if (paymentMethod == null) return PaymentMethodType.Unknown; + return paymentMethod.Value.PaymentMethodType; } @@ -500,6 +628,27 @@ public virtual string GetMaskedCreditCardNumber(string creditCardNumber) return maskedChars + last4; } - #endregion - } + public virtual IList GetAllPaymentMethodFilters() + { + 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 + } } diff --git a/src/Libraries/SmartStore.Services/Payments/PostProcessPaymentRequest.cs b/src/Libraries/SmartStore.Services/Payments/PostProcessPaymentRequest.cs index 1ca115635f..f14a6cc6aa 100644 --- a/src/Libraries/SmartStore.Services/Payments/PostProcessPaymentRequest.cs +++ b/src/Libraries/SmartStore.Services/Payments/PostProcessPaymentRequest.cs @@ -16,5 +16,10 @@ public partial class PostProcessPaymentRequest /// Whether the customer clicked the button to re-post the payment process /// public bool IsRePostProcessPayment { get; set; } + + /// + /// URL to a payment provider to fulfill the payment. The .NET core will redirect to it. + /// + public string RedirectUrl { get; set; } } } 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 a105e18520..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; } @@ -182,70 +184,59 @@ public virtual void UpdatePermissionRecord(PermissionRecord permission) /// Permission provider public virtual void InstallPermissions(IPermissionProvider permissionProvider) { - using (var scope = new DbContextScope(_permissionRecordRepository.Context, autoDetectChanges: false)) + using (var scope = new DbContextScope(_permissionRecordRepository.Context, autoDetectChanges: false, autoCommit: false)) { - try + //install new permissions + var permissions = permissionProvider.GetPermissions(); + foreach (var permission in permissions) { - _permissionRecordRepository.AutoCommitEnabled = false; - _customerRoleRepository.AutoCommitEnabled = false; - - //install new permissions - var permissions = permissionProvider.GetPermissions(); - foreach (var permission in permissions) + var permission1 = GetPermissionRecordBySystemName(permission.SystemName); + if (permission1 == null) { - var permission1 = GetPermissionRecordBySystemName(permission.SystemName); - if (permission1 == null) + //new permission (install it) + permission1 = new PermissionRecord() { - //new permission (install it) - permission1 = new PermissionRecord() - { - Name = permission.Name, - SystemName = permission.SystemName, - Category = permission.Category, - }; - - // default customer role mappings - var defaultPermissions = permissionProvider.GetDefaultPermissions(); - foreach (var defaultPermission in defaultPermissions) + Name = permission.Name, + SystemName = permission.SystemName, + Category = permission.Category, + }; + + // default customer role mappings + var defaultPermissions = permissionProvider.GetDefaultPermissions(); + foreach (var defaultPermission in defaultPermissions) + { + var customerRole = _customerService.GetCustomerRoleBySystemName(defaultPermission.CustomerRoleSystemName); + if (customerRole == null) { - var customerRole = _customerService.GetCustomerRoleBySystemName(defaultPermission.CustomerRoleSystemName); - if (customerRole == null) + //new role (save it) + customerRole = new CustomerRole { - //new role (save it) - customerRole = new CustomerRole() - { - Name = defaultPermission.CustomerRoleSystemName, - Active = true, - SystemName = defaultPermission.CustomerRoleSystemName - }; - _customerService.InsertCustomerRole(customerRole); - } - - - var defaultMappingProvided = (from p in defaultPermission.PermissionRecords - where p.SystemName == permission1.SystemName - select p).Any(); - var mappingExists = (from p in customerRole.PermissionRecords - where p.SystemName == permission1.SystemName - select p).Any(); - if (defaultMappingProvided && !mappingExists) - { - permission1.CustomerRoles.Add(customerRole); - } + Name = defaultPermission.CustomerRoleSystemName, + Active = true, + SystemName = defaultPermission.CustomerRoleSystemName + }; + _customerService.InsertCustomerRole(customerRole); } - //save new permission - InsertPermissionRecord(permission1); + + var defaultMappingProvided = (from p in defaultPermission.PermissionRecords + where p.SystemName == permission1.SystemName + select p).Any(); + var mappingExists = (from p in customerRole.PermissionRecords + where p.SystemName == permission1.SystemName + select p).Any(); + if (defaultMappingProvided && !mappingExists) + { + permission1.CustomerRoles.Add(customerRole); + } } - } - scope.Commit(); - } - finally - { - _permissionRecordRepository.AutoCommitEnabled = true; - _customerRoleRepository.AutoCommitEnabled = true; + //save new permission + InsertPermissionRecord(permission1); + } } + + scope.Commit(); } } diff --git a/src/Libraries/SmartStore.Services/Security/StandardPermissionProvider.cs b/src/Libraries/SmartStore.Services/Security/StandardPermissionProvider.cs index 01b83e6a38..4f1032077a 100644 --- a/src/Libraries/SmartStore.Services/Security/StandardPermissionProvider.cs +++ b/src/Libraries/SmartStore.Services/Security/StandardPermissionProvider.cs @@ -49,10 +49,12 @@ public partial class StandardPermissionProvider : IPermissionProvider public static readonly PermissionRecord ManageMaintenance = new PermissionRecord { Name = "Admin area. Manage Maintenance", SystemName = "ManageMaintenance", Category = "Configuration" }; 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" }; @@ -103,7 +105,10 @@ public virtual IEnumerable GetPermissions() ManageMaintenance, UploadPictures, ManageScheduleTasks, - DisplayPrices, + ManageExports, + ManageImports, + ManageUrlRecords, + DisplayPrices, EnableShoppingCart, EnableWishlist, PublicStoreAllowNavigation, @@ -160,6 +165,9 @@ public virtual IEnumerable GetDefaultPermissions() ManageMaintenance, 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 3f3451c12d..d3bfa5a04d 100644 --- a/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs +++ b/src/Libraries/SmartStore.Services/Seo/IUrlRecordService.cs @@ -24,6 +24,13 @@ public partial interface IUrlRecordService /// URL record UrlRecord GetUrlRecordById(int urlRecordId); + /// + /// Gets URL records by identifiers + /// + /// + /// List of URL records + IList GetUrlRecordsByIds(int[] urlRecordIds); + /// /// Inserts an URL record /// @@ -46,11 +53,15 @@ public partial interface IUrlRecordService /// /// Gets all URL records /// - /// Slug /// Page index /// Page size + /// Slug + /// Entity name + /// Entity identifier + /// Whether to load only active records + /// Language identifier /// Customer collection - IPagedList GetAllUrlRecords(string slug, int pageIndex, int pageSize); + IPagedList GetAllUrlRecords(int pageIndex, int pageSize, string slug, string entityName, int? entityId, int? languageId, bool? isActive); /// /// Gets all URL records for the specified entity @@ -90,5 +101,20 @@ public partial interface IUrlRecordService /// Name of a property /// Url record UrlRecord SaveSlug(T entity, Expression> nameProperty) where T : BaseEntity, ISlugSupported; + + /// + /// Get number of slugs per entity + /// + /// 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 1d1c5a6c9d..29c29d13ef 100644 --- a/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs +++ b/src/Libraries/SmartStore.Services/Seo/SeoExtensions.cs @@ -9,6 +9,7 @@ using SmartStore.Core.Infrastructure; using SmartStore.Services.Localization; using SmartStore.Utilities; +using SmartStore.Core.Localization; namespace SmartStore.Services.Seo { @@ -242,6 +243,15 @@ public static string ValidateSeName(this T entity, } } + // validate and alter SeName if it could be interpreted as SEO code + if (LocalizationHelper.IsValidCultureCode(seName)) + { + if (seName.Length == 2) + { + seName = seName + "-0"; + } + } + // ensure this sename is not reserved yet string entityName = typeof(T).Name; int i = 2; @@ -294,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/SitemapGenerator.cs b/src/Libraries/SmartStore.Services/Seo/SitemapGenerator.cs index 776d424311..497555026d 100644 --- a/src/Libraries/SmartStore.Services/Seo/SitemapGenerator.cs +++ b/src/Libraries/SmartStore.Services/Seo/SitemapGenerator.cs @@ -96,23 +96,31 @@ private void WriteManufacturers(UrlHelper urlHelper) private void WriteProducts(UrlHelper urlHelper) { - var ctx = new ProductSearchContext() + var protocol = _securitySettings.ForceSslForAllPages ? "https" : "http"; + + var ctx = new ProductSearchContext { OrderBy = ProductSortingEnum.CreatedOn, - PageSize = int.MaxValue, + PageSize = 500, StoreId = _storeContext.CurrentStoreIdIfMultiStoreMode, VisibleIndividuallyOnly = true }; - var products = _productService.SearchProducts(ctx); - var protocol = _securitySettings.ForceSslForAllPages ? "https" : "http"; - foreach (var product in products) - { - var url = urlHelper.RouteUrl("Product", new { SeName = product.GetSeName() }, protocol); - var updateFrequency = UpdateFrequency.Weekly; - var updateTime = product.UpdatedOnUtc; - WriteUrlLocation(url, updateFrequency, updateTime); - } + for (ctx.PageIndex = 0; ctx.PageIndex < 9999999; ++ctx.PageIndex) + { + var products = _productService.SearchProducts(ctx); + + foreach (var product in products) + { + var url = urlHelper.RouteUrl("Product", new { SeName = product.GetSeName() }, protocol); + var updateFrequency = UpdateFrequency.Weekly; + var updateTime = product.UpdatedOnUtc; + WriteUrlLocation(url, updateFrequency, updateTime); + } + + if (!products.HasNextPage) + break; + } } private void WriteTopics(UrlHelper urlHelper) diff --git a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs index b49a0044e1..38fe4a7bda 100644 --- a/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs +++ b/src/Libraries/SmartStore.Services/Seo/UrlRecordService.cs @@ -16,8 +16,10 @@ public partial class UrlRecordService : IUrlRecordService { #region Constants - private const string URLRECORD_ACTIVE_BY_ID_NAME_LANGUAGE_KEY = "SmartStore.urlrecord.active.id-name-language-{0}-{1}-{2}"; - private const string URLRECORD_PATTERN_KEY = "SmartStore.urlrecord."; + // {0} = id, {1} = name, {2} = language + private const string URLRECORD_KEY = "SmartStore.urlrecord.{0}-{1}-{2}"; + private const string URLRECORD_ALL_ACTIVESLUGS_KEY = "SmartStore.urlrecord.all-active-slugs"; + private const string URLRECORD_PATTERN_KEY = "SmartStore.urlrecord."; #endregion @@ -25,15 +27,17 @@ public partial class UrlRecordService : IUrlRecordService private readonly IRepository _urlRecordRepository; private readonly ICacheManager _cacheManager; + private readonly SeoSettings _seoSettings; #endregion #region Ctor - public UrlRecordService(ICacheManager cacheManager, IRepository urlRecordRepository) + public UrlRecordService(ICacheManager cacheManager, IRepository urlRecordRepository, SeoSettings seoSettings) { this._cacheManager = cacheManager; this._urlRecordRepository = urlRecordRepository; + this._seoSettings = seoSettings; } #endregion @@ -59,6 +63,18 @@ public virtual UrlRecord GetUrlRecordById(int urlRecordId) return urlRecord; } + public virtual IList GetUrlRecordsByIds(int[] urlRecordIds) + { + if (urlRecordIds == null || urlRecordIds.Length == 0) + return new List(); + + var urlRecords = _urlRecordRepository.Table + .Where(x => urlRecordIds.Contains(x.Id)) + .ToList(); + + return urlRecords; + } + public virtual void InsertUrlRecord(UrlRecord urlRecord) { if (urlRecord == null) @@ -81,27 +97,29 @@ public virtual void UpdateUrlRecord(UrlRecord urlRecord) _cacheManager.RemoveByPattern(URLRECORD_PATTERN_KEY); } - public virtual UrlRecord GetBySlug(string slug) + public virtual IPagedList GetAllUrlRecords(int pageIndex, int pageSize, string slug, string entityName, int? entityId, int? languageId, bool? isActive) { - if (String.IsNullOrEmpty(slug)) - return null; + var query = _urlRecordRepository.Table; - var query = from ur in _urlRecordRepository.Table - where ur.Slug == slug - select ur; - var urlRecord = query.FirstOrDefault(); - return urlRecord; - } + if (slug.HasValue()) + query = query.Where(x => x.Slug.Contains(slug)); - public virtual IPagedList GetAllUrlRecords(string slug, int pageIndex, int pageSize) - { - var query = _urlRecordRepository.Table; - if (!String.IsNullOrWhiteSpace(slug)) - query = query.Where(ur => ur.Slug.Contains(slug)); - query = query.OrderBy(ur => ur.Slug); + if (entityName.HasValue()) + query = query.Where(x => x.EntityName == entityName); + + if (entityId.HasValue) + query = query.Where(x => x.EntityId == entityId.Value); + + 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); - return urlRecords; + var urlRecords = new PagedList(query, pageIndex, pageSize); + return urlRecords; } public virtual IList GetUrlRecordsFor(string entityName, int entityId, bool activeOnly = false) @@ -121,21 +139,63 @@ public virtual IList GetUrlRecordsFor(string entityName, int entityId return query.ToList(); } + public virtual UrlRecord GetBySlug(string slug) + { + // INFO: (mc) Caching unnecessary here. This is not a 'bottleneck' function. + if (String.IsNullOrEmpty(slug)) + return null; + + var query = from ur in _urlRecordRepository.Table + where ur.Slug == slug + select ur; + var urlRecord = query.FirstOrDefault(); + return urlRecord; + } + public virtual string GetActiveSlug(int entityId, string entityName, int languageId) - { - string key = string.Format(URLRECORD_ACTIVE_BY_ID_NAME_LANGUAGE_KEY, entityId, entityName, languageId); - return _cacheManager.Get(key, () => - { - var query = from ur in _urlRecordRepository.Table - where ur.EntityId == entityId && - ur.EntityName == entityName && - ur.LanguageId == languageId && - ur.IsActive - orderby ur.Id descending - select ur.Slug; - var slug = query.FirstOrDefault(); - return slug ?? ""; - }); + { + string slug = null; + + if (_seoSettings.LoadAllUrlAliasesOnStartup) + { + var allActiveSlugs = _cacheManager.Get(URLRECORD_ALL_ACTIVESLUGS_KEY, () => + { + var query = from x in _urlRecordRepository.TableUntracked + where x.IsActive + orderby x.Id descending + select x; + + var result = query.ToDictionarySafe( + x => GenerateKey(x.EntityId, x.EntityName, x.LanguageId), + x => x.Slug, + StringComparer.OrdinalIgnoreCase); + + return result; + }); + + var key = GenerateKey(entityId, entityName, languageId); + if (!allActiveSlugs.TryGetValue(key, out slug)) + { + return string.Empty; + } + } + else + { + string cacheKey = string.Format(URLRECORD_KEY, entityId, entityName, languageId); + slug = _cacheManager.Get(cacheKey, () => + { + var query = from ur in _urlRecordRepository.Table + where ur.EntityId == entityId && + ur.EntityName == entityName && + ur.LanguageId == languageId && + ur.IsActive + orderby ur.Id descending + select ur.Slug; + return query.FirstOrDefault() ?? string.Empty; + }); + } + + return slug; } public virtual UrlRecord SaveSlug(T entity, string slug, int languageId) where T : BaseEntity, ISlugSupported @@ -265,6 +325,41 @@ public virtual UrlRecord SaveSlug(T entity, Expression> nameP return SaveSlug(entity, existingSeName, 0); } + private string GenerateKey(int entityId, string entityName, int languageId) + { + return "{0}.{1}.{2}".FormatInvariant(entityId, entityName, languageId); + } + + public virtual Dictionary CountSlugsPerEntity(int[] urlRecordIds) + { + if (urlRecordIds == null || urlRecordIds.Length == 0) + return new Dictionary(); + + var query = + from x in _urlRecordRepository.TableUntracked + where urlRecordIds.Contains(x.Id) + select new + { + Id = x.Id, + Count = _urlRecordRepository.TableUntracked.Where(y => y.EntityName == x.EntityName && y.EntityId == x.EntityId).Count() + }; + + var result = query + .ToList() + .ToDictionary(x => x.Id, x => x.Count); + + 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/ServiceCacheConsumer.cs b/src/Libraries/SmartStore.Services/ServiceCacheConsumer.cs index 702c0e504e..e3fd3f18b8 100644 --- a/src/Libraries/SmartStore.Services/ServiceCacheConsumer.cs +++ b/src/Libraries/SmartStore.Services/ServiceCacheConsumer.cs @@ -6,6 +6,8 @@ using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Stores; using SmartStore.Core.Events; +using SmartStore.Services.Tasks; +using SmartStore.Services.Stores; namespace SmartStore.Services { @@ -20,7 +22,7 @@ public class ServiceCacheConsumer : private readonly ICacheManager _cacheManager; - public ServiceCacheConsumer(Func cache) + public ServiceCacheConsumer(Func cache) { this._cacheManager = cache("static"); } @@ -49,5 +51,5 @@ public void HandleEvent(EntityDeleted eventMessage) { _cacheManager.Remove(STORE_LANGUAGE_MAP_KEY); } - } + } } diff --git a/src/Libraries/SmartStore.Services/Shipping/GetShippingOptionRequest.cs b/src/Libraries/SmartStore.Services/Shipping/GetShippingOptionRequest.cs index 1962f97177..3439b6fd4f 100644 --- a/src/Libraries/SmartStore.Services/Shipping/GetShippingOptionRequest.cs +++ b/src/Libraries/SmartStore.Services/Shipping/GetShippingOptionRequest.cs @@ -16,6 +16,11 @@ public GetShippingOptionRequest() this.Items = new List(); } + /// + /// The context store identifier + /// + public int StoreId { get; set; } + /// /// Gets or sets a customer /// diff --git a/src/Libraries/SmartStore.Services/Shipping/IShipmentService.cs b/src/Libraries/SmartStore.Services/Shipping/IShipmentService.cs index ac79efe3dc..0a544651ad 100644 --- a/src/Libraries/SmartStore.Services/Shipping/IShipmentService.cs +++ b/src/Libraries/SmartStore.Services/Shipping/IShipmentService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Domain.Shipping; @@ -29,12 +30,19 @@ IPagedList GetAllShipments(string trackingNumber, DateTime? createdFro int pageIndex, int pageSize); /// - /// Get shipment by identifiers + /// Get shipments by identifiers /// /// Shipment identifiers /// Shipments IList GetShipmentsByIds(int[] shipmentIds); + /// + /// Get shipments by order identifiers + /// + /// Order identifiers + /// Shipments + Multimap GetShipmentsByOrderIds(int[] orderIds); + /// /// Gets a shipment /// diff --git a/src/Libraries/SmartStore.Services/Shipping/IShippingMethodFilter.cs b/src/Libraries/SmartStore.Services/Shipping/IShippingMethodFilter.cs new file mode 100644 index 0000000000..2fbed519bc --- /dev/null +++ b/src/Libraries/SmartStore.Services/Shipping/IShippingMethodFilter.cs @@ -0,0 +1,35 @@ +using SmartStore.Core.Domain.Shipping; + +namespace SmartStore.Services.Shipping +{ + public partial interface IShippingMethodFilter + { + /// + /// Gets a value indicating whether a shipping method should be filtered out + /// + /// Shipping filter request + /// true filter out method, false do not filter out method + bool IsExcluded(ShippingFilterRequest request); + + /// + /// Get URL for filter configuration + /// + /// Shipping method identifier + /// URL for filter configuration + string GetConfigurationUrl(int shippingMethodId); + } + + + public partial class ShippingFilterRequest + { + /// + /// The shipping method to be checked + /// + public ShippingMethod ShippingMethod { get; set; } + + /// + /// Shipping method request + /// + public GetShippingOptionRequest Option { get; set; } + } +} diff --git a/src/Libraries/SmartStore.Services/Shipping/IShippingService.cs b/src/Libraries/SmartStore.Services/Shipping/IShippingService.cs index a8fccc065b..9018fab4b6 100644 --- a/src/Libraries/SmartStore.Services/Shipping/IShippingService.cs +++ b/src/Libraries/SmartStore.Services/Shipping/IShippingService.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Plugins; @@ -52,9 +53,9 @@ public partial interface IShippingService /// /// Gets all shipping methods /// - /// The country indentifier to filter by + /// Shipping option request to filter out shipping methods. null to load all shipping methods. /// Shipping method collection - IList GetAllShippingMethods(int? filterByCountryId = null); + IList GetAllShippingMethods(GetShippingOptionRequest request = null); /// /// Inserts a shipping method @@ -96,8 +97,9 @@ public partial interface IShippingService /// /// Shopping cart /// Shipping address + /// Store identifier /// Shipment package - GetShippingOptionRequest CreateShippingOptionRequest(IList cart, Address shippingAddress); + GetShippingOptionRequest CreateShippingOptionRequest(IList cart, Address shippingAddress, int storeId); /// /// Gets available shipping options @@ -109,5 +111,11 @@ public partial interface IShippingService /// Shipping options GetShippingOptionResponse GetShippingOptions(IList cart, Address shippingAddress, string allowedShippingRateComputationMethodSystemName = "", int storeId = 0); + + /// + /// Gets all shipping method filters + /// + /// List of shipping method filters + IList GetAllShippingMethodFilters(); } } diff --git a/src/Libraries/SmartStore.Services/Shipping/ShipmentService.cs b/src/Libraries/SmartStore.Services/Shipping/ShipmentService.cs index 7afbb90cbb..3a8c261164 100644 --- a/src/Libraries/SmartStore.Services/Shipping/ShipmentService.cs +++ b/src/Libraries/SmartStore.Services/Shipping/ShipmentService.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; +using SmartStore.Collections; using SmartStore.Core; using SmartStore.Core.Data; 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 @@ -123,6 +123,24 @@ where shipmentIds.Contains(o.Id) return sortedOrders; } + public virtual Multimap GetShipmentsByOrderIds(int[] orderIds) + { + Guard.ArgumentNotNull(() => orderIds); + + var query = + from x in _shipmentRepository.TableUntracked.Expand(x => x.ShipmentItems) + where orderIds.Contains(x.OrderId) + select x; + + var map = query + .OrderBy(x => x.OrderId) + .ThenBy(x => x.CreatedOnUtc) + .ToList() + .ToMultimap(x => x.OrderId, x => x); + + return map; + } + /// /// Gets a shipment /// @@ -221,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); + } } /// @@ -237,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 2b834b4194..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; @@ -9,34 +8,36 @@ using SmartStore.Core.Domain.Orders; 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; private readonly IProviderManager _providerManager; + private readonly ITypeFinder _typeFinder; #endregion @@ -58,67 +59,75 @@ 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) + 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); + var activeMethods = allMethods .Where(p => p.Value.IsActive && _shippingSettings.ActiveShippingRateComputationMethodSystemNames.Contains(p.Metadata.SystemName, StringComparer.InvariantCultureIgnoreCase)); if (!activeMethods.Any()) { - var fallbackMethod = allMethods.FirstOrDefault(); + var fallbackMethod = allMethods.FirstOrDefault(x => x.IsShippingRateComputationMethodActive(_shippingSettings)); + + if (fallbackMethod == null) + fallbackMethod = allMethods.FirstOrDefault(); + if (fallbackMethod != null) { _shippingSettings.ActiveShippingRateComputationMethodSystemNames.Clear(); _shippingSettings.ActiveShippingRateComputationMethodSystemNames.Add(fallbackMethod.Metadata.SystemName); _settingService.SaveSetting(_shippingSettings); + return new Provider[] { fallbackMethod }; } else { - throw Error.Application("At least one shipping method provider is required to be active."); + if (DataSettings.DatabaseIsInstalled()) + throw new SmartException(T("Shipping.OneActiveMethodProviderRequired")); } } @@ -177,37 +186,37 @@ public virtual ShippingMethod GetShippingMethodById(int shippingMethodId) return _shippingMethodRepository.GetById(shippingMethodId); } - - /// - /// Gets all shipping methods - /// - /// The country indentifier to filter by - /// Shipping method collection - public virtual IList GetAllShippingMethods(int? filterByCountryId = null) + + public virtual IList GetAllShippingMethods(GetShippingOptionRequest request = null) { - if (filterByCountryId.HasValue && filterByCountryId.Value > 0) - { - var query1 = from sm in _shippingMethodRepository.Table - where - sm.RestrictedCountries.Select(c => c.Id).Contains(filterByCountryId.Value) - select sm.Id; - - var query2 = from sm in _shippingMethodRepository.Table - where !query1.Contains(sm.Id) - orderby sm.DisplayOrder - select sm; - - var shippingMethods = query2.ToList(); - return shippingMethods; - } - else - { - var query = from sm in _shippingMethodRepository.Table - orderby sm.DisplayOrder - select sm; - var shippingMethods = query.ToList(); - return shippingMethods; - } + var query = + from sm in _shippingMethodRepository.Table + orderby sm.DisplayOrder + select sm; + + var allMethods = query.ToList(); + + if (request == null) + return allMethods; + + 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.IsExcluded(filterRequest))) + return false; + + return true; + }); + + return activeShippingMethods.ToList(); } /// @@ -262,7 +271,7 @@ public virtual decimal GetShoppingCartItemWeight(OrganizedShoppingCartItem shopp if (!String.IsNullOrEmpty(shoppingCartItem.Item.AttributesXml)) { - var pvaValues = _productAttributeParser.ParseProductVariantAttributeValues(shoppingCartItem.Item.AttributesXml); + var pvaValues = _productAttributeParser.ParseProductVariantAttributeValues(shoppingCartItem.Item.AttributesXml).ToList(); foreach (var pvaValue in pvaValues) { @@ -308,11 +317,11 @@ public virtual decimal GetShoppingCartTotalWeight(IList(SystemCustomerAttributeNames.CheckoutAttributes, _genericAttributeService); @@ -323,6 +332,7 @@ public virtual decimal GetShoppingCartTotalWeight(IList /// Shopping cart /// Shipping address + /// Store identifier /// Shipment package - public virtual GetShippingOptionRequest CreateShippingOptionRequest(IList cart, - Address shippingAddress) + public virtual GetShippingOptionRequest CreateShippingOptionRequest(IList cart, Address shippingAddress, int storeId) { var request = new GetShippingOptionRequest(); + request.StoreId = storeId; request.Customer = cart.GetCustomer(); - request.Items = new List(); - foreach (var sc in cart) - if (sc.Item.IsShipEnabled) - request.Items.Add(sc); request.ShippingAddress = shippingAddress; request.CountryFrom = null; request.StateProvinceFrom = null; request.ZipPostalCodeFrom = string.Empty; + + request.Items = new List(); + + foreach (var sc in cart) + { + if (sc.Item.IsShipEnabled) + request.Items.Add(sc); + } + return request; } @@ -357,7 +373,8 @@ public virtual GetShippingOptionRequest CreateShippingOptionRequest(IListFilter by shipping rate computation method identifier; null to load shipping options of all shipping rate computation methods /// Load records allows only in specified store; pass 0 to load all records /// Shipping options - public virtual GetShippingOptionResponse GetShippingOptions(IList cart, Address shippingAddress, string allowedShippingRateComputationMethodSystemName = "", int storeId = 0) + public virtual GetShippingOptionResponse GetShippingOptions(IList cart, Address shippingAddress, + string allowedShippingRateComputationMethodSystemName = "", int storeId = 0) { if (cart == null) throw new ArgumentNullException("cart"); @@ -365,7 +382,7 @@ public virtual GetShippingOptionResponse GetShippingOptions(IList @@ -374,7 +391,7 @@ public virtual GetShippingOptionResponse GetShippingOptions(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() + { + 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 #endregion diff --git a/src/Libraries/SmartStore.Services/SmartStore.Services.csproj b/src/Libraries/SmartStore.Services/SmartStore.Services.csproj index 91cca5e43a..1edfe58738 100644 --- a/src/Libraries/SmartStore.Services/SmartStore.Services.csproj +++ b/src/Libraries/SmartStore.Services/SmartStore.Services.csproj @@ -62,31 +62,36 @@ 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 False - ..\..\packages\EntityFramework.6.1.0\lib\net45\EntityFramework.dll + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll - - ..\..\packages\EntityFramework.6.1.0\lib\net45\EntityFramework.SqlServer.dll + + False + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll 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 @@ -96,9 +101,12 @@ True ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - False - ..\..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + ..\..\packages\ncrontab.2.0.0\lib\net20\NCrontab.dll + + + ..\..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True False @@ -110,9 +118,12 @@ + ..\..\packages\System.Linq.Dynamic.1.0.0\lib\net40\System.Linq.Dynamic.dll + + False @@ -143,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 @@ -154,14 +166,82 @@ Properties\AssemblyVersionInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -205,7 +285,6 @@ - @@ -237,7 +316,6 @@ - @@ -254,7 +332,6 @@ - @@ -288,8 +365,6 @@ - - @@ -308,12 +383,9 @@ - - - @@ -421,6 +493,7 @@ + @@ -436,13 +509,17 @@ + + + + + - - - + + @@ -526,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 f763985753..a773079402 100644 --- a/src/Libraries/SmartStore.Services/Stores/StoreService.cs +++ b/src/Libraries/SmartStore.Services/Stores/StoreService.cs @@ -81,9 +81,12 @@ public virtual IList GetAllStores() string key = STORES_ALL_KEY; return _cacheManager.Get(key, () => { - var query = from s in _storeRepository.Table - orderby s.DisplayOrder, s.Name - select s; + var query = _storeRepository.Table + .Expand(x => x.PrimaryStoreCurrency) + .Expand(x => x.PrimaryExchangeRateCurrency) + .OrderBy(x => x.DisplayOrder) + .ThenBy(x => x.Name); + var stores = query.ToList(); return stores; }); @@ -147,9 +150,7 @@ public virtual bool IsSingleStoreMode() { if (!_isSingleStoreMode.HasValue) { - var query = from s in _storeRepository.Table - select s; - _isSingleStoreMode = query.Count() <= 1; + _isSingleStoreMode = (_storeRepository.TableUntracked.Count() <= 1); } return _isSingleStoreMode.Value; @@ -176,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/ChangeTaskSchedulerBaseUrlConsumer.cs b/src/Libraries/SmartStore.Services/Tasks/ChangeTaskSchedulerBaseUrlConsumer.cs new file mode 100644 index 0000000000..a79a13c4ec --- /dev/null +++ b/src/Libraries/SmartStore.Services/Tasks/ChangeTaskSchedulerBaseUrlConsumer.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Events; +using SmartStore.Services.Stores; +using SmartStore.Utilities; + +namespace SmartStore.Services.Tasks +{ + public class ChangeTaskSchedulerBaseUrlConsumer : + IConsumer>, + IConsumer>, + IConsumer> + { + private readonly ITaskScheduler _taskScheduler; + private readonly IStoreService _storeService; + private readonly HttpContextBase _httpContext; + private readonly bool _shouldChange; + + public ChangeTaskSchedulerBaseUrlConsumer(ITaskScheduler taskScheduler, IStoreService storeService, HttpContextBase httpContext) + { + this._taskScheduler = taskScheduler; + this._storeService = storeService; + this._httpContext = httpContext; + this._shouldChange = CommonHelper.GetAppSetting("sm:TaskSchedulerBaseUrl").IsWebUrl() == false; + } + + public void HandleEvent(EntityInserted eventMessage) + { + HandleEventCore(); + } + + public void HandleEvent(EntityUpdated eventMessage) + { + HandleEventCore(); + } + + public void HandleEvent(EntityDeleted eventMessage) + { + HandleEventCore(); + } + + private void HandleEventCore() + { + if (_shouldChange) + { + _taskScheduler.SetBaseUrl(_storeService, _httpContext); + } + } + } +} diff --git a/src/Libraries/SmartStore.Services/Tasks/CronExpression.cs b/src/Libraries/SmartStore.Services/Tasks/CronExpression.cs new file mode 100644 index 0000000000..c35b4b7128 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Tasks/CronExpression.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NCrontab; +using CronExpressionDescriptor; +using System.Threading; + +namespace SmartStore.Services.Tasks +{ + + public static class CronExpression + { + + public static bool IsValid(string expression) + { + try + { + CrontabSchedule.Parse(expression); + return true; + } + catch { } + + return false; + } + + public static DateTime GetNextSchedule(string expression, DateTime baseTime) + { + return GetFutureSchedules(expression, baseTime, 1).FirstOrDefault(); + } + + public static DateTime GetNextSchedule(string expression, DateTime baseTime, DateTime endTime) + { + return GetFutureSchedules(expression, baseTime, endTime, 1).FirstOrDefault(); + } + + public static IEnumerable GetFutureSchedules(string expression, DateTime baseTime, int max = 10) + { + return GetFutureSchedules(expression, baseTime, DateTime.MaxValue); + } + + public static IEnumerable GetFutureSchedules(string expression, DateTime baseTime, DateTime endTime, int max = 10) + { + Guard.ArgumentNotEmpty(() => expression); + + var schedule = CrontabSchedule.Parse(expression); + return schedule.GetNextOccurrences(baseTime, endTime).Take(max); + } + + public static string GetFriendlyDescription(string expression) + { + Guard.ArgumentNotEmpty(() => expression); + + var options = new Options + { + DayOfWeekStartIndexZero = true, + ThrowExceptionOnParseError = true, + Verbose = false, + Use24HourTimeFormat = Thread.CurrentThread.CurrentUICulture.DateTimeFormat.AMDesignator.IsEmpty() + }; + + if (expression.HasValue()) + { + try + { + return ExpressionDescriptor.GetDescription(expression, options); + } + catch { } + } + + return "?"; + } + + } + +} diff --git a/src/Libraries/SmartStore.Services/Tasks/IScheduleTaskService.cs b/src/Libraries/SmartStore.Services/Tasks/IScheduleTaskService.cs index fcd853d341..440fe98379 100644 --- a/src/Libraries/SmartStore.Services/Tasks/IScheduleTaskService.cs +++ b/src/Libraries/SmartStore.Services/Tasks/IScheduleTaskService.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using SmartStore.Core.Domain.Tasks; @@ -31,9 +32,34 @@ public partial interface IScheduleTaskService /// /// Gets all tasks /// - /// A value indicating whether to show hidden records + /// A value indicating whether to load disabled tasks also /// Tasks - IList GetAllTasks(bool showHidden = false); + IList GetAllTasks(bool includeDisabled = false); + + /// + /// Gets all pending tasks + /// + /// Tasks + IList GetPendingTasks(); + + /// + /// Gets a value indicating whether at least one task is running currently. + /// + /// + bool HasRunningTasks(); + + /// + /// Gets a value indicating whether a task is currently running + /// + /// A identifier + /// true if the task is running, false otherwise + bool IsTaskRunning(int taskId); + + /// + /// Gets a list of currently running instances. + /// + /// Tasks + IList GetRunningTasks(); /// /// Inserts a task @@ -48,9 +74,17 @@ public partial interface IScheduleTaskService void UpdateTask(ScheduleTask task); /// - /// Ensures that a task is not marked as running + /// Calculates - according to their cron expressions - all task future schedules + /// and saves them to the database. + /// + /// When true, determines stale tasks and fixes their states to idle. + void CalculateFutureSchedules(IEnumerable tasks, bool isAppStart = false); + + /// + /// Calculates the next schedule according to the task's cron expression /// - /// Task identifier - void EnsureTaskIsNotRunning(int taskId); + /// ScheduleTask + /// The next schedule or null if the task is disabled + DateTime? GetNextSchedule(ScheduleTask task); } } diff --git a/src/Libraries/SmartStore.Services/Tasks/ITaskExecutor.cs b/src/Libraries/SmartStore.Services/Tasks/ITaskExecutor.cs new file mode 100644 index 0000000000..053cf45937 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Tasks/ITaskExecutor.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using SmartStore.Core.Domain.Tasks; + +namespace SmartStore.Services.Tasks +{ + public interface ITaskExecutor + { + 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 new file mode 100644 index 0000000000..f852d12952 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Tasks/ITaskScheduler.cs @@ -0,0 +1,98 @@ +using System; +using System.Web; +using System.Linq; +using SmartStore.Core; +using SmartStore.Services.Stores; +using System.Web.Hosting; +using System.Collections.Generic; +using System.Threading; +using SmartStore.Core.Domain.Tasks; + +namespace SmartStore.Services.Tasks +{ + /// + /// Task scheduler interface + /// + public interface ITaskScheduler + { + /// + /// The interval in which the scheduler triggers the sweep url + /// (which determines pending tasks and executes them in the scope of a regular HTTP request). + /// + int SweepIntervalMinutes { get; set; } + + /// + /// The fully qualified base url + /// + string BaseUrl { get; set; } + + /// + /// Gets a value indicating whether the scheduler is active and periodically sweeps all tasks. + /// + bool IsActive { get; } + + /// + /// Gets a instance used + /// to signal a task cancellation request. + /// + /// A identifier + /// A instance if the task is running, null otherwise + CancellationTokenSource GetCancelTokenSourceFor(int scheduleTaskId); + + /// + /// Starts/initializes the scheduler + /// + void Start(); + + /// + /// Stops the scheduler + /// + void Stop(); + + /// + /// Executes a single task immediately + /// + /// + /// Optional task parameters + void RunSingleTask(int scheduleTaskId, IDictionary taskParameters = null); + + /// + /// Verifies the authentication token which is generated right before the HTTP endpoint gets called. + /// + /// The authentication token to verify + /// true if the validation succeeds, false otherwise + /// + /// The task scheduler sends the token as a HTTP request header item. + /// The called endpoint (e.g. a controller action) is reponsible for invoking + /// this method and quitting the tasks's execution - preferrably with HTTP 403 - + /// if the verification fails. + /// + bool VerifyAuthToken(string authToken); + } + + public static class ITaskSchedulerExtensions + { + + internal static void SetBaseUrl(this ITaskScheduler scheduler, IStoreService storeService, HttpContextBase httpContext) + { + string url = ""; + + if (!httpContext.Request.IsLocal) + { + var defaultStore = storeService.GetAllStores().FirstOrDefault(x => storeService.IsStoreDataValid(x)); + if (defaultStore != null) + { + url = defaultStore.Url.EnsureEndsWith("/") + "TaskScheduler"; + } + } + + if (url.IsEmpty()) + { + url = WebHelper.GetAbsoluteUrl(VirtualPathUtility.ToAbsolute("~/TaskScheduler"), httpContext.Request); + } + + scheduler.BaseUrl = url; + } + + } +} diff --git a/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs b/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs new file mode 100644 index 0000000000..3329fbf3a7 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Tasks/InitializeSchedulerFilter.cs @@ -0,0 +1,95 @@ +using SmartStore.Core; +using SmartStore.Core.Events; +using SmartStore.Core.Infrastructure; +using SmartStore.Services.Stores; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web.Mvc; +using SmartStore.Core.Data; +using SmartStore.Core.Logging; +using SmartStore.Utilities; + +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) + { + 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; + + 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 tasks = taskService.GetAllTasks(true); + taskService.CalculateFutureSchedules(tasks, true /* isAppStart */); + + var baseUrl = CommonHelper.GetAppSetting("sm:TaskSchedulerBaseUrl"); + if (baseUrl.IsWebUrl()) + { + taskScheduler.BaseUrl = baseUrl; + } + else + { + // autoresolve base url + taskScheduler.SetBaseUrl(storeService, filterContext.HttpContext); + } + + taskScheduler.SweepIntervalMinutes = CommonHelper.GetAppSetting("sm:TaskSchedulerSweepInterval", 1); + taskScheduler.Start(); + + logger.Information("Initialized TaskScheduler with base url '{0}'".FormatInvariant(taskScheduler.BaseUrl)); + + eventPublisher.Publish(new AppInitScheduledTasksEvent { ScheduledTasks = tasks }); + } + catch (Exception ex) + { + s_errCount++; + s_initializing = false; + logger.Error("Error while initializing Task Scheduler", ex); + } + finally + { + 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/Job.cs b/src/Libraries/SmartStore.Services/Tasks/Job.cs deleted file mode 100644 index c38203e95c..0000000000 --- a/src/Libraries/SmartStore.Services/Tasks/Job.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System; -using System.Threading; -using Autofac; -using SmartStore.Core.Domain.Tasks; -using SmartStore.Core.Infrastructure; -using SmartStore.Core.Logging; - -namespace SmartStore.Services.Tasks -{ - /// - /// Task - /// - /// - /// Formerly Task. Had to rename to Job in order to prevent naming conflicts with System.Threading.Tasks.Task - /// - public partial class Job - { - - /// - /// Ctor for Task - /// - private Job() - { - this.Enabled = true; - } - - /// - /// Ctor for Task - /// - /// Task - public Job(ScheduleTask task) - { - this.Type = task.Type; - this.Enabled = task.Enabled; - this.StopOnError = task.StopOnError; - this.Name = task.Name; - this.LastError = task.LastError; - this.IsRunning = task.IsRunning; //task.LastStartUtc.GetValueOrDefault() > task.LastEndUtc.GetValueOrDefault(); - } - - private ITask CreateTask(ILifetimeScope scope) - { - ITask task = null; - if (this.Enabled) - { - var type2 = System.Type.GetType(this.Type); - if (type2 != null) - { - object instance; - if (!EngineContext.Current.ContainerManager.TryResolve(type2, scope, out instance)) - { - // not resolved - instance = EngineContext.Current.ContainerManager.ResolveUnregistered(type2, scope); - } - task = instance as ITask; - } - } - return task; - } - - /// - /// Executes the task - /// - /// - /// The caller is responsible for disposing the lifetime scope - /// - public void Execute(ILifetimeScope scope = null, bool throwOnError = false) - { - Execute(CancellationToken.None, scope, throwOnError); - } - - /// - /// Executes the task - /// - /// - /// The caller is responsible for disposing the lifetime scope - /// - public void Execute( - CancellationToken cancellationToken, - ILifetimeScope scope = null, - bool throwOnError = false) - { - this.IsRunning = true; - var faulted = false; - scope = scope ?? EngineContext.Current.ContainerManager.Scope(); - - try - { - var task = this.CreateTask(scope); - if (task != null) - { - this.LastStartUtc = DateTime.UtcNow; - - var scheduleTaskService = scope.Resolve(); - var scheduleTask = scheduleTaskService.GetTaskByType(this.Type); - - if (scheduleTask != null) - { - //update appropriate datetime properties - scheduleTask.LastStartUtc = this.LastStartUtc; - scheduleTaskService.UpdateTask(scheduleTask); - } - - //execute task - var ctx = new TaskExecutionContext - { - LifetimeScope = scope, - CancellationToken = cancellationToken - }; - - task.Execute(ctx); - - ctx.CancellationToken.ThrowIfCancellationRequested(); - this.LastEndUtc = this.LastSuccessUtc = DateTime.UtcNow; - this.LastError = null; - } - else - { - faulted = true; - this.LastError = "Could not create task instance"; - } - } - catch (Exception ex) - { - faulted = true; - this.Enabled = !this.StopOnError; - this.LastEndUtc = DateTime.UtcNow; - this.LastError = ex.Message.Truncate(995, "..."); - - //log error - var logger = scope.Resolve(); - logger.Error(string.Format("Error while running the '{0}' schedule task. {1}", this.Name, ex.Message), ex); - if (throwOnError) - { - throw; - } - } - finally - { - var scheduleTaskService = scope.Resolve(); - var scheduleTask = scheduleTaskService.GetTaskByType(this.Type); - - if (scheduleTask != null) - { - // update appropriate properties - scheduleTask.LastError = this.LastError; - scheduleTask.LastEndUtc = this.LastEndUtc; - if (!faulted) - { - scheduleTask.LastSuccessUtc = this.LastSuccessUtc; - } - - scheduleTaskService.UpdateTask(scheduleTask); - } - - this.IsRunning = false; - } - } - - /// - /// A value indicating whether a task is running - /// - public bool IsRunning { get; private set; } - - /// - /// Datetime of the last start - /// - public DateTime? LastStartUtc { get; private set; } - - /// - /// Datetime of the last end - /// - public DateTime? LastEndUtc { get; private set; } - - /// - /// Datetime of the last success - /// - public DateTime? LastSuccessUtc { get; private set; } - - /// - /// Message of the last error - /// - public string LastError { get; private set; } - - /// - /// A value indicating type of the task - /// - public string Type { get; private set; } - - /// - /// A value indicating whether to stop task on error - /// - public bool StopOnError { get; private set; } - - /// - /// Get the task name - /// - public string Name { get; private set; } - - /// - /// A value indicating whether the task is enabled - /// - public bool Enabled { get; set; } - } -} diff --git a/src/Libraries/SmartStore.Services/Tasks/ScheduleTaskService.cs b/src/Libraries/SmartStore.Services/Tasks/ScheduleTaskService.cs index 25f8fe6f98..83a4a97b75 100644 --- a/src/Libraries/SmartStore.Services/Tasks/ScheduleTaskService.cs +++ b/src/Libraries/SmartStore.Services/Tasks/ScheduleTaskService.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; +using System.Data.Entity.Infrastructure; using System.Linq; using SmartStore.Core.Data; using SmartStore.Core.Domain.Tasks; +using SmartStore.Core.Localization; +using SmartStore.Services.Helpers; namespace SmartStore.Services.Tasks { @@ -14,24 +17,26 @@ public partial class ScheduleTaskService : IScheduleTaskService #region Fields private readonly IRepository _taskRepository; + private readonly IDateTimeHelper _dateTimeHelper; #endregion #region Ctor - public ScheduleTaskService(IRepository taskRepository) + public ScheduleTaskService(IRepository taskRepository, IDateTimeHelper dateTimeHelper) { this._taskRepository = taskRepository; + this._dateTimeHelper = dateTimeHelper; + + T = NullLocalizer.Instance; } + public Localizer T { get; set; } + #endregion #region Methods - /// - /// Deletes a task - /// - /// Task public virtual void DeleteTask(ScheduleTask task) { if (task == null) @@ -40,11 +45,6 @@ public virtual void DeleteTask(ScheduleTask task) _taskRepository.Delete(task); } - /// - /// Gets a task - /// - /// Task identifier - /// Task public virtual ScheduleTask GetTaskById(int taskId) { if (taskId == 0) @@ -53,11 +53,6 @@ public virtual ScheduleTask GetTaskById(int taskId) return _taskRepository.GetById(taskId); } - /// - /// Gets a task by its type - /// - /// Task type - /// Task public virtual ScheduleTask GetTaskByType(string type) { try @@ -80,28 +75,63 @@ public virtual ScheduleTask GetTaskByType(string type) return null; } - /// - /// Gets all tasks - /// - /// A value indicating whether to show hidden records - /// Tasks - public virtual IList GetAllTasks(bool showHidden = false) + public virtual IList GetAllTasks(bool includeDisabled = false) { var query = _taskRepository.Table; - if (!showHidden) + if (!includeDisabled) { query = query.Where(t => t.Enabled); } - query = query.OrderBy(t => t.Seconds); + query = query.OrderByDescending(t => t.Enabled); var tasks = query.ToList(); return tasks; } - /// - /// Inserts a task - /// - /// Task + public virtual IList GetPendingTasks() + { + var now = DateTime.UtcNow; + + var query = from t in _taskRepository.Table + where t.NextRunUtc.HasValue && t.NextRunUtc <= now && t.Enabled + orderby t.NextRunUtc + select t; + + return query.ToList(); + } + + public virtual bool HasRunningTasks() + { + var query = GetRunningTasksQuery(); + return query.Any(); + } + + public virtual bool IsTaskRunning(int taskId) + { + if (taskId <= 0) + return false; + + var query = GetRunningTasksQuery(); + query.Where(t => t.Id == taskId); + return query.Any(); + } + + public virtual IList GetRunningTasks() + { + return GetRunningTasksQuery().ToList(); + } + + private IQueryable GetRunningTasksQuery() + { + var query = from t in _taskRepository.Table + where t.LastStartUtc.HasValue && t.LastStartUtc.Value > (t.LastEndUtc ?? DateTime.MinValue) + orderby t.LastStartUtc + select t; + + return query; + } + + public virtual void InsertTask(ScheduleTask task) { if (task == null) @@ -110,39 +140,114 @@ public virtual void InsertTask(ScheduleTask task) _taskRepository.Insert(task); } - /// - /// Updates the task - /// - /// Task public virtual void UpdateTask(ScheduleTask task) { if (task == null) throw new ArgumentNullException("task"); - _taskRepository.Update(task); + bool saveFailed; + bool? autoCommit = null; + + do + { + saveFailed = false; + + // ALWAYS save immediately + try + { + autoCommit = _taskRepository.AutoCommitEnabled; + _taskRepository.AutoCommitEnabled = true; + _taskRepository.Update(task); + } + catch (DbUpdateConcurrencyException ex) + { + saveFailed = true; + + var entry = ex.Entries.Single(); + var current = (ScheduleTask)entry.CurrentValues.ToObject(); // from current scope + + // When 'StopOnError' is true, the 'Enabled' property could have been be set to true on exception. + var enabledModified = entry.Property("Enabled").IsModified; + + // Save current cron expression + var cronExpression = task.CronExpression; + + // Fetch Name, CronExpression, Enabled & StopOnError from database + // (these were possibly edited thru the backend) + _taskRepository.Context.ReloadEntity(task); + + // Do we have to reschedule the task? + var cronModified = cronExpression != task.CronExpression; + + // Copy execution specific data from current to reloaded entity + task.LastEndUtc = current.LastEndUtc; + task.LastError = current.LastError; + task.LastStartUtc = current.LastStartUtc; + task.LastSuccessUtc = current.LastSuccessUtc; + task.ProgressMessage = current.ProgressMessage; + task.ProgressPercent = current.ProgressPercent; + task.NextRunUtc = current.NextRunUtc; + if (enabledModified) + { + task.Enabled = current.Enabled; + } + if (task.NextRunUtc.HasValue && cronModified) + { + // reschedule task + task.NextRunUtc = GetNextSchedule(task); + } + } + finally + { + _taskRepository.AutoCommitEnabled = autoCommit; + } + } while (saveFailed); } - /// - /// Ensures that a task is not marked as running (normalize last start and end date). - /// - /// Task identifier - /// Problem can be reproduced by inserting a news object without a language identifier. - public virtual void EnsureTaskIsNotRunning(int taskId) + public void CalculateFutureSchedules(IEnumerable tasks, bool isAppStart = false) { - try + Guard.ArgumentNotNull(() => tasks); + + using (var scope = new DbContextScope(autoCommit: false)) { - if (taskId != 0) + var now = DateTime.UtcNow; + foreach (var task in tasks) { - _taskRepository.Context.ExecuteSqlCommand("Update [dbo].[ScheduleTask] Set [LastEndUtc] = [LastStartUtc] Where Id = {0} And [LastEndUtc] < [LastStartUtc]", - true, null, taskId); + task.NextRunUtc = GetNextSchedule(task); + if (isAppStart) + { + task.ProgressPercent = null; + task.ProgressMessage = null; + if (task.LastEndUtc.GetValueOrDefault() < task.LastStartUtc) + { + task.LastEndUtc = task.LastStartUtc; + task.LastError = T("Admin.System.ScheduleTasks.AbnormalAbort"); + } + } + this.UpdateTask(task); } + + scope.Commit(); } - catch (Exception exc) + } + + public virtual DateTime? GetNextSchedule(ScheduleTask task) + { + if (task.Enabled) { - exc.Dump(); + try + { + var baseTime = DateTime.UtcNow; + var next = CronExpression.GetNextSchedule(task.CronExpression, baseTime); + return next; + } + catch { } } + + return null; } #endregion - } + + } } diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs b/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs index 074a7e619a..9b0f1aaad5 100644 --- a/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs +++ b/src/Libraries/SmartStore.Services/Tasks/TaskExecutionContext.cs @@ -3,6 +3,9 @@ using System.Linq; using System.Threading; using Autofac; +using SmartStore.Core.Async; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Tasks; namespace SmartStore.Services.Tasks { @@ -11,16 +14,91 @@ namespace SmartStore.Services.Tasks /// public class TaskExecutionContext { + private readonly IComponentContext _componentContext; + private readonly ScheduleTask _originalTask; + + internal TaskExecutionContext(IComponentContext componentContext, ScheduleTask originalTask) + { + this._componentContext = componentContext; + this._originalTask = originalTask; + this.Parameters = new Dictionary(); + } + + public T Resolve(object key = null) where T : class + { + if (key == null) + { + return _componentContext.Resolve(); + } + return _componentContext.ResolveKeyed(key); + } + + public T ResolveNamed(string name) where T : class + { + return _componentContext.ResolveNamed(name); + } + + /// + /// A cancellation token for the running task. + /// You can use ThrowIfCancellationRequested() for a hard or IsCancellationRequested for a soft break. + /// + public CancellationToken CancellationToken { get; internal set; } + + public ScheduleTask ScheduleTask { get; set; } + + public IDictionary Parameters { get; set; } + /// - /// The shared instance created - /// before the execution of the task's background thread. + /// Persists a task's progress information to the database /// - public object LifetimeScope { get; internal set; } + /// 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); + } + } /// - /// A cancellation token for the running task. - /// You can use ThrowIfCancellationRequested() for a hard or IsCancellationRequested for a soft break. + /// Persists a task's progress information to the database /// - public CancellationToken CancellationToken { get; internal set; } + /// Percentual progress. Can be null or a value between 0 and 100. + /// 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? progress, string message, bool immediately = false) + { + if (progress.HasValue) + Guard.ArgumentInRange(progress.Value, 0, 100, "progress"); + + // update cloned entity + ScheduleTask.ProgressPercent = progress; + ScheduleTask.ProgressMessage = message; + + // update attached entity + _originalTask.ProgressPercent = progress; + _originalTask.ProgressMessage = message; + + if (immediately) + { + try // dont't let this abort the task on failure + { + var dbContext = _componentContext.Resolve(); + dbContext.ChangeState(_originalTask, System.Data.Entity.EntityState.Modified); + dbContext.SaveChanges(); + } + catch { } + } + } } } diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs b/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs new file mode 100644 index 0000000000..8fd93c2956 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Tasks/TaskExecutor.cs @@ -0,0 +1,190 @@ +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; + +namespace SmartStore.Services.Tasks +{ + + public class TaskExecutor : ITaskExecutor + { + private readonly IScheduleTaskService _scheduledTaskService; + private readonly IDbContext _dbContext; + private readonly ICustomerService _customerService; + private readonly IWorkContext _workContext; + private readonly Func _taskResolver; + private readonly IComponentContext _componentContext; + + public const string CurrentCustomerIdParamName = "CurrentCustomerId"; + + public TaskExecutor( + IScheduleTaskService scheduledTaskService, + IDbContext dbContext, + ICustomerService customerService, + IWorkContext workContext, + IComponentContext componentContext, + Func taskResolver) + { + this._scheduledTaskService = scheduledTaskService; + this._dbContext = dbContext; + this._customerService = customerService; + this._workContext = workContext; + this._componentContext = componentContext; + this._taskResolver = taskResolver; + + Logger = NullLogger.Instance; + T = NullLocalizer.Instance; + } + + public ILogger Logger { get; set; } + public Localizer T { get; set; } + + public void Execute( + ScheduleTask task, + IDictionary taskParameters = null, + bool throwOnError = false) + { + if (task.IsRunning) + return; + + if (AsyncRunner.AppShutdownCancellationToken.IsCancellationRequested) + return; + + bool faulted = false; + bool canceled = false; + string lastError = null; + ITask instance = null; + string stateName = null; + + Type taskType = null; + + try + { + taskType = Type.GetType(task.Type); + + Debug.WriteLineIf(taskType == null, "Invalid task type: " + task.Type.NaIfEmpty()); + + if (taskType == null) + return; + + if (!PluginManager.IsActivePluginAssembly(taskType.Assembly)) + return; + } + catch + { + return; + } + + try + { + 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 + instance = _taskResolver(taskType); + stateName = task.Id.ToString(); + + // prepare and save entity + task.LastStartUtc = DateTime.UtcNow; + task.LastEndUtc = null; + task.NextRunUtc = null; + task.ProgressPercent = null; + task.ProgressMessage = null; + + _scheduledTaskService.UpdateTask(task); + + // create & set a composite CancellationTokenSource which also contains the global app shoutdown token + var cts = CancellationTokenSource.CreateLinkedTokenSource(AsyncRunner.AppShutdownCancellationToken, new CancellationTokenSource().Token); + AsyncState.Current.SetCancelTokenSource(cts, stateName); + + var ctx = new TaskExecutionContext(_componentContext, task) + { + ScheduleTask = task.Clone(), + CancellationToken = cts.Token, + Parameters = taskParameters ?? new Dictionary() + }; + + instance.Execute(ctx); + } + catch (Exception exception) + { + faulted = true; + 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; + } + } + finally + { + // remove from AsyncState + if (stateName.HasValue()) + { + AsyncState.Current.Remove(stateName); + } + + task.ProgressPercent = null; + task.ProgressMessage = null; + + var now = DateTime.UtcNow; + task.LastError = lastError; + task.LastEndUtc = now; + + if (faulted) + { + if ((!canceled && task.StopOnError) || instance == null) + { + task.Enabled = false; + } + } + else + { + task.LastSuccessUtc = now; + } + + if (task.Enabled) + { + task.NextRunUtc = _scheduledTaskService.GetNextSchedule(task); + } + + _scheduledTaskService.UpdateTask(task); + } + } + + private CancellationTokenSource CreateCompositeCancellationTokenSource(CancellationToken userCancellationToken) + { + return CancellationTokenSource.CreateLinkedTokenSource(AsyncRunner.AppShutdownCancellationToken, userCancellationToken); + } + } + +} diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskManager.cs b/src/Libraries/SmartStore.Services/Tasks/TaskManager.cs deleted file mode 100644 index 6a8dc82be3..0000000000 --- a/src/Libraries/SmartStore.Services/Tasks/TaskManager.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using SmartStore.Core.Events; -using SmartStore.Core.Infrastructure; -using SmartStore.Core.Plugins; - -namespace SmartStore.Services.Tasks -{ - /// - /// Represents task manager - /// - public partial class TaskManager - { - private static readonly TaskManager _taskManager = new TaskManager(); - private readonly List _taskThreads = new List(); - - private TaskManager() - { - } - - /// - /// Initializes the task manager with the property values specified in the configuration file. - /// - public void Initialize() - { - this._taskThreads.Clear(); - - var taskService = EngineContext.Current.Resolve(); - var scheduleTasks = taskService.GetAllTasks(); - - var eventPublisher = EngineContext.Current.Resolve(); - eventPublisher.Publish(new AppInitScheduledTasksEvent { - ScheduledTasks = scheduleTasks - }); - - // group by threads with the same seconds - foreach (var scheduleTaskGrouped in scheduleTasks.GroupBy(x => x.Seconds)) - { - // create a thread - var taskThread = new TaskThread(); - taskThread.Seconds = scheduleTaskGrouped.Key; - - foreach (var scheduleTask in scheduleTaskGrouped) - { - var taskType = System.Type.GetType(scheduleTask.Type); - if (taskType != null) - { - var isActiveModule = PluginManager.IsActivePluginAssembly(taskType.Assembly); - if (isActiveModule) - { - var job = new Job(scheduleTask); - taskThread.AddJob(job); - } - } - } - - if (taskThread.HasJobs) - { - this._taskThreads.Add(taskThread); - } - } - - - //one thread, one task - //foreach (var scheduleTask in scheduleTasks) - //{ - // var taskThread = new TaskThread(scheduleTask); - // this._taskThreads.Add(taskThread); - // var task = new Task(scheduleTask); - // taskThread.AddTask(task); - //} - } - - /// - /// Starts the task manager - /// - public void Start() - { - foreach (var taskThread in this._taskThreads) - { - taskThread.InitTimer(); - } - } - - /// - /// Stops the task manager - /// - public void Stop() - { - foreach (var taskThread in this._taskThreads) - { - taskThread.Dispose(); - } - } - - /// - /// Gets the task mamanger instance - /// - public static TaskManager Instance - { - get - { - return _taskManager; - } - } - - /// - /// Gets a list of task threads of this task manager - /// - public IList TaskThreads - { - get - { - return new ReadOnlyCollection(this._taskThreads); - } - } - } -} diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs b/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs new file mode 100644 index 0000000000..0e055029a3 --- /dev/null +++ b/src/Libraries/SmartStore.Services/Tasks/TaskScheduler.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Hosting; +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; + private int _sweepInterval; + private string _baseUrl; + private System.Timers.Timer _timer; + private bool _shuttingDown; + private int _errCount; + + public DefaultTaskScheduler() + { + _sweepInterval = 1; + _timer = new System.Timers.Timer(); + _timer.Elapsed += Elapsed; + + HostingEnvironment.RegisterObject(this); + } + + public int SweepIntervalMinutes + { + get { return _sweepInterval; } + set { _sweepInterval = value; } + } + + public string BaseUrl + { + // TODO: HTTPS? + get { return _baseUrl; } + set + { + CheckUrl(value); + _baseUrl = value.TrimEnd('/', '\\'); + } + } + + public void Start() + { + if (_timer.Enabled) + return; + + lock (_timer) + { + CheckUrl(_baseUrl); + _timer.Interval = GetFixedInterval(); + _timer.Start(); + } + } + + private double GetFixedInterval() + { + // Gets seconds to next sweep minute + int seconds = (_sweepInterval * 60) - DateTime.Now.Second; + return seconds * 1000; + } + + public void Stop() + { + if (!_timer.Enabled) + return; + + lock (_timer) + { + _timer.Stop(); + } + } + + public bool IsActive + { + get { return _timer.Enabled; } + } + + public CancellationTokenSource GetCancelTokenSourceFor(int scheduleTaskId) + { + var cts = AsyncState.Current.GetCancelTokenSource(scheduleTaskId.ToString()); + 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; + + 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, IDictionary taskParameters = null) + { + 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) + { + if (!System.Threading.Monitor.TryEnter(_timer)) + return; + + try + { + if (_timer.Enabled) + { + if (!_intervalFixed) + { + _timer.Interval = TimeSpan.FromMinutes(_sweepInterval).TotalMilliseconds; + _intervalFixed = true; + } + + CallEndpoint(new Uri(_baseUrl + "/Sweep")); + } + } + finally + { + System.Threading.Monitor.Exit(_timer); + } + } + + protected internal virtual void CallEndpoint(Uri uri) + { + if (_shuttingDown) + return; + + var req = WebHelper.CreateHttpRequestForSafeLocalCall(uri); + req.Method = "POST"; + req.ContentType = "text/plain"; + req.ContentLength = 0; + req.Timeout = 10000; // 10 sec. + + string authToken = CreateAuthToken(); + req.Headers.Add("X-AUTH-TOKEN", authToken); + + req.GetResponseAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + HandleException(t.Exception, uri); + _errCount++; + if (_errCount >= 10) + { + // 10 failed attempts in succession. Stop the timer! + this.Stop(); + using (var logger = new TraceLogger()) + { + logger.Information("Stopping TaskScheduler sweep timer. Too many failed requests in succession."); + } + } + } + else + { + _errCount = 0; + 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, Uri uri) + { + using (var logger = new TraceLogger()) + { + string msg = "Error while calling TaskScheduler endpoint '{0}'.".FormatInvariant(uri.OriginalString); + var wex = exception.InnerExceptions.OfType().FirstOrDefault(); + + if (wex == null) + { + logger.Error(msg, exception.InnerException); + } + else if (wex.Response == null) + { + logger.Error(msg, wex); + } + else + { + using (var response = wex.Response as HttpWebResponse) + { + if (response != null) + { + msg += " HTTP {0}, {1}".FormatCurrent((int)response.StatusCode, response.StatusDescription); + } + logger.Error(msg); + } + } + } + } + + private void CheckUrl(string url) + { + if (!url.IsWebUrl()) + { + throw Error.InvalidOperation("A valid base url is required for the background task scheduler."); + } + } + + protected override void OnDispose(bool disposing) + { + if (disposing) + { + _timer.Dispose(); + } + } + + void IRegisteredObject.Stop(bool immediate) + { + _shuttingDown = true; + HostingEnvironment.UnregisterObject(this); + } + } + +} diff --git a/src/Libraries/SmartStore.Services/Tasks/TaskThread.cs b/src/Libraries/SmartStore.Services/Tasks/TaskThread.cs deleted file mode 100644 index b165daaeec..0000000000 --- a/src/Libraries/SmartStore.Services/Tasks/TaskThread.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using SmartStore.Core.Async; -using SmartStore.Core.Domain.Tasks; - -namespace SmartStore.Services.Tasks -{ - /// - /// Represents task thread - /// - public partial class TaskThread : IDisposable - { - private Timer _timer; - private bool _disposed; - private DateTime _startedUtc; - private bool _isRunning; - private readonly Dictionary _jobs; - private int _seconds; - - internal TaskThread() - { - this._jobs = new Dictionary(); - this._seconds = 10 * 60; - } - - internal TaskThread(ScheduleTask scheduleTask) - { - this._jobs = new Dictionary(); - this._seconds = scheduleTask.Seconds; - this._isRunning = false; - } - - private void Run() - { - if (_seconds <=0) - return; - - this._startedUtc = DateTime.UtcNow; - this._isRunning = true; - - var jobs = _jobs.Values - .Select(job => AsyncRunner.Run((c, ct) => job.Execute(ct, c))) - .ToArray(); - - try - { - Task.WaitAll(jobs); - } - catch { } - - this._isRunning = false; - } - - private void TimerHandler(object state) - { - this._timer.Change(-1, -1); - this.Run(); - this._timer.Change(this.Interval, this.Interval); - } - - /// - /// Disposes the instance - /// - public void Dispose() - { - if ((this._timer != null) && !this._disposed) - { - lock (this) - { - this._timer.Dispose(); - this._timer = null; - this._disposed = true; - } - } - } - - /// - /// Inits a timer - /// - public void InitTimer() - { - if (this._timer == null) - { - this._timer = new Timer(new TimerCallback(this.TimerHandler), null, this.Interval, this.Interval); - } - } - - /// - /// Adds a job to the thread - /// - /// The task to be added - public void AddJob(Job job) - { - if (!this._jobs.ContainsKey(job.Name)) - { - this._jobs.Add(job.Name, job); - } - } - - - /// - /// Gets or sets the interval in seconds at which to run the jobs - /// - public int Seconds - { - get - { - return this._seconds; - } - internal set - { - this._seconds = value; - } - } - - /// - /// Get a datetime when thread has been started - /// - public DateTime Started - { - get - { - return this._startedUtc; - } - } - - /// - /// Get a value indicating whether thread is running - /// - public bool IsRunning - { - get - { - return this._isRunning; - } - } - - /// - /// Get a list of jobs - /// - public IList Jobs - { - get - { - var list = new List(); - foreach (var jobs in this._jobs.Values) - { - list.Add(jobs); - } - return new ReadOnlyCollection(list); - } - } - - public bool HasJobs - { - get { return _jobs.Count > 0; } - } - - /// - /// Gets the interval at which to run the jobs - /// - public int Interval - { - get - { - if (_seconds > (Int32.MaxValue / 1000)) - return Int32.MaxValue; - - return this._seconds * 1000; - } - } - } -} diff --git a/src/Libraries/SmartStore.Services/Tax/TaxService.cs b/src/Libraries/SmartStore.Services/Tax/TaxService.cs index 4bc52f11c6..056c456606 100644 --- a/src/Libraries/SmartStore.Services/Tax/TaxService.cs +++ b/src/Libraries/SmartStore.Services/Tax/TaxService.cs @@ -8,11 +8,9 @@ using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Tax; -using SmartStore.Core.Infrastructure; using SmartStore.Core.Plugins; using SmartStore.Services.Common; using SmartStore.Services.Directory; -using SmartStore.Services.Configuration; namespace SmartStore.Services.Tax { @@ -44,7 +42,6 @@ public TaxAddressKey(int customerId, bool productIsEsd) private readonly IPluginFinder _pluginFinder; private readonly IDictionary _cachedTaxRates; private readonly IDictionary _cachedTaxAddresses; - private readonly ISettingService _settingService; private readonly IProviderManager _providerManager; private readonly IGeoCountryLookup _geoCountryLookup; @@ -65,10 +62,8 @@ public TaxService( TaxSettings taxSettings, ShoppingCartSettings cartSettings, IPluginFinder pluginFinder, - ISettingService settingService, IGeoCountryLookup geoCountryLookup, - IProviderManager providerManager - ) + IProviderManager providerManager) { this._addressService = addressService; this._workContext = workContext; @@ -77,7 +72,6 @@ IProviderManager providerManager this._pluginFinder = pluginFinder; this._cachedTaxRates = new Dictionary(); this._cachedTaxAddresses = new Dictionary(); - this._settingService = settingService; this._providerManager = providerManager; this._geoCountryLookup = geoCountryLookup; } @@ -274,9 +268,7 @@ public virtual Provider LoadActiveTaxProvider() if (taxProvider == null) { taxProvider = LoadAllTaxProviders().FirstOrDefault(); - _taxSettings.ActiveTaxProviderSystemName = taxProvider.Metadata.SystemName; - _settingService.SaveSetting(_taxSettings); - } + } return taxProvider; } diff --git a/src/Libraries/SmartStore.Services/Themes/ThemeVariablesService.cs b/src/Libraries/SmartStore.Services/Themes/ThemeVariablesService.cs index 3f30e2e79d..4011840398 100644 --- a/src/Libraries/SmartStore.Services/Themes/ThemeVariablesService.cs +++ b/src/Libraries/SmartStore.Services/Themes/ThemeVariablesService.cs @@ -80,20 +80,18 @@ public virtual void DeleteThemeVariables(string themeName, int storeId) if (query.Any()) { - bool autoCommit = _rsVariables.AutoCommitEnabled; - _rsVariables.AutoCommitEnabled = false; - - query.Each(v => - { - _rsVariables.Delete(v); - _eventPublisher.EntityDeleted(v); - }); - - _cacheManager.Remove(THEMEVARS_BY_THEME_KEY.FormatInvariant(themeName, storeId)); - - _rsVariables.Context.SaveChanges(); - - _rsVariables.AutoCommitEnabled = autoCommit; + using (var scope = new DbContextScope(ctx: _rsVariables.Context, autoCommit: false)) + { + query.Each(v => + { + _rsVariables.Delete(v); + _eventPublisher.EntityDeleted(v); + }); + + _cacheManager.Remove(THEMEVARS_BY_THEME_KEY.FormatInvariant(themeName, storeId)); + + _rsVariables.Context.SaveChanges(); + } } } @@ -109,75 +107,73 @@ public virtual int SaveThemeVariables(string themeName, int storeId, IDictionary var count = 0; var infos = _themeRegistry.GetThemeManifest(themeName).Variables; - bool autoCommit = _rsVariables.AutoCommitEnabled; - _rsVariables.AutoCommitEnabled = false; - - var unsavedVars = new List(); - var savedThemeVars = _rsVariables.Table.Where(v => v.StoreId == storeId && v.Theme.Equals(themeName, StringComparison.OrdinalIgnoreCase)).ToList(); - bool touched = false; - - foreach (var v in variables) - { - ThemeVariableInfo info; - if (!infos.TryGetValue(v.Key, out info)) - { - // var not specified in metadata so don't save - // TODO: (MC) delete from db also if it exists - continue; - } - - var value = v.Value == null ? string.Empty : v.Value.ToString(); - - var savedThemeVar = savedThemeVars.FirstOrDefault(x => x.Name == v.Key); - if (savedThemeVar != null) - { - if (value.IsEmpty() || String.Equals(info.DefaultValue, value, StringComparison.CurrentCultureIgnoreCase)) - { - // it's either null or the default value, so delete - _rsVariables.Delete(savedThemeVar); - _eventPublisher.EntityDeleted(savedThemeVar); - touched = true; - count++; - } - else - { - // update entity - if (!savedThemeVar.Value.Equals(value, StringComparison.OrdinalIgnoreCase)) - { - savedThemeVar.Value = value; - _eventPublisher.EntityUpdated(savedThemeVar); - touched = true; - count++; - } - } - } - else - { - if (value.HasValue() && !String.Equals(info.DefaultValue, value, StringComparison.CurrentCultureIgnoreCase)) - { - // insert entity (only when not default value) - unsavedVars.Add(v.Key); - savedThemeVar = new ThemeVariable - { - Theme = themeName, - Name = v.Key, - Value = value, - StoreId = storeId - }; - _rsVariables.Insert(savedThemeVar); - _eventPublisher.EntityInserted(savedThemeVar); - touched = true; - count++; - } - } - } - - if (touched) - { - _rsVariables.Context.SaveChanges(); - } - - _rsVariables.AutoCommitEnabled = autoCommit; + using (var scope = new DbContextScope(ctx: _rsVariables.Context, autoCommit: false)) + { + var unsavedVars = new List(); + var savedThemeVars = _rsVariables.Table.Where(v => v.StoreId == storeId && v.Theme.Equals(themeName, StringComparison.OrdinalIgnoreCase)).ToList(); + bool touched = false; + + foreach (var v in variables) + { + ThemeVariableInfo info; + if (!infos.TryGetValue(v.Key, out info)) + { + // var not specified in metadata so don't save + // TODO: (MC) delete from db also if it exists + continue; + } + + var value = v.Value == null ? string.Empty : v.Value.ToString(); + + var savedThemeVar = savedThemeVars.FirstOrDefault(x => x.Name == v.Key); + if (savedThemeVar != null) + { + if (value.IsEmpty() || String.Equals(info.DefaultValue, value, StringComparison.CurrentCultureIgnoreCase)) + { + // it's either null or the default value, so delete + _rsVariables.Delete(savedThemeVar); + _eventPublisher.EntityDeleted(savedThemeVar); + touched = true; + count++; + } + else + { + // update entity + if (!savedThemeVar.Value.Equals(value, StringComparison.OrdinalIgnoreCase)) + { + savedThemeVar.Value = value; + _eventPublisher.EntityUpdated(savedThemeVar); + touched = true; + count++; + } + } + } + else + { + if (value.HasValue() && !String.Equals(info.DefaultValue, value, StringComparison.CurrentCultureIgnoreCase)) + { + // insert entity (only when not default value) + unsavedVars.Add(v.Key); + savedThemeVar = new ThemeVariable + { + Theme = themeName, + Name = v.Key, + Value = value, + StoreId = storeId + }; + _rsVariables.Insert(savedThemeVar); + _eventPublisher.EntityInserted(savedThemeVar); + touched = true; + count++; + } + } + } + + if (touched) + { + _rsVariables.Context.SaveChanges(); + } + } return count; } 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 1d9b39f80e..d078b22719 100644 --- a/src/Libraries/SmartStore.Services/packages.config +++ b/src/Libraries/SmartStore.Services/packages.config @@ -1,18 +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 ab15464d35..8237c4a7ad 100644 --- a/src/Plugins/SmartStore.AmazonPay/Description.txt +++ b/src/Plugins/SmartStore.AmazonPay/Description.txt @@ -1,10 +1,10 @@ FriendlyName: Pay with Amazon SystemName: SmartStore.AmazonPay -Version: 2.2.0.2 +Version: 2.6.0 Group: Payment -MinAppVersion: 2.2.0 +MinAppVersion: 2.5.0 Author: SmartStore AG DisplayOrder: 1 FileName: SmartStore.AmazonPay.dll ResourceRootKey: Plugins.Payments.AmazonPay -Url: http://community.smartstore.com/index.php?/files/file/11-amazon-payments-plugin/ \ No newline at end of file +Url: http://community.smartstore.com/marketplace/file/11-amazon-payments-plugin/ \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Events/MessageTokenEventConsumer.cs b/src/Plugins/SmartStore.AmazonPay/Events/MessageTokenEventConsumer.cs new file mode 100644 index 0000000000..78f10c09e2 --- /dev/null +++ b/src/Plugins/SmartStore.AmazonPay/Events/MessageTokenEventConsumer.cs @@ -0,0 +1,47 @@ +using System.Linq; +using SmartStore.AmazonPay.Services; +using SmartStore.Core.Domain.Messages; +using SmartStore.Core.Events; +using SmartStore.Core.Plugins; +using SmartStore.Services; +using SmartStore.Services.Messages; +using SmartStore.Services.Orders; +using SmartStore.Web.Framework; + +namespace SmartStore.AmazonPay.Events +{ + public class MessageTokenEventConsumer : IConsumer> + { + private readonly IPluginFinder _pluginFinder; + private readonly ICommonServices _services; + private readonly IOrderService _orderService; + + public MessageTokenEventConsumer( + IPluginFinder pluginFinder, + ICommonServices services, + IOrderService orderService) + { + _pluginFinder = pluginFinder; + _services = services; + _orderService = orderService; + } + + public void HandleEvent(MessageTokensAddedEvent messageTokenEvent) + { + if (!messageTokenEvent.Message.Name.IsCaseInsensitiveEqual("OrderPlaced.CustomerNotification")) + return; + + var storeId = _services.StoreContext.CurrentStore.Id; + + if (!_pluginFinder.IsPluginReady(_services.Settings, AmazonPayCore.SystemName, storeId)) + return; + + var order = _orderService.SearchOrders(storeId, _services.WorkContext.CurrentCustomer.Id, null, null, null, null, null, null, null, null, 0, 1).FirstOrDefault(); + + var isAmazonPayment = (order != null && order.PaymentMethodSystemName.IsCaseInsensitiveEqual(AmazonPayCore.SystemName)); + var tokenValue = (isAmazonPayment ? _services.Localization.GetResource("Plugins.Payments.AmazonPay.BillingAddressMessageNote") : ""); + + messageTokenEvent.Tokens.Add(new Token("SmartStore.AmazonPay.BillingAddressMessageNote", tokenValue)); + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml b/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml index 3c5dce6dd0..fbe79d7119 100644 --- a/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.AmazonPay/Localization/resources.de-de.xml @@ -8,15 +8,14 @@ - Registrieren Sie sich bei Amazon Payments.

+ 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.
-

Hinweis! Die Rechnungsadresse des Kunden erhalten Sie erst nach Bestätigung der Zahlungsautorisierung von Amazon, also i.d.R. erst kurz nach Versand der Bestellbestätigungs-Email. Um Missverständnisse zu vermeiden ist es ratsam, den Platzhalter für die Rechnungsadresse aus der Nachrichtenvorlage für die Bestellbestätigung zu entfernen.

-

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.
  • @@ -24,17 +23,20 @@ Textvorschläge:

    ]]> + + Bitte beachten Sie, dass es sich bei dieser Rechnungsadresse unter Umständen nicht um die für diese Bestellung gültige handelt! + Es wurde keine Auftrags-Referenz-ID durch Amazon übermittelt! - Zahlungsmethode "Bezahlen mit Amazon" ist für Shop "{0}" nicht verfügbar. + Die Zahlungsart "Bezahlen mit Amazon" ist für Shop "{0}" nicht verfügbar. Fehlender Checkout-Sitzungsstatus für "Bezahlen mit Amazon". Ihre Zahlung kann leider nicht bearbeitet werden. Bitte gehen Sie zurück in den Warenkorb und Durchlaufen Sie den Checkout erneut. - Ein Auftrag mit der Zahlungsmethode "Bezahlen mit Amazon" und der Kennung {0} wurde nicht gefunden. + Ein Auftrag mit der Zahlungsart "Bezahlen mit Amazon" und der Kennung {0} wurde nicht gefunden. @@ -177,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 0505e89e6b..d5163d3ef3 100644 --- a/src/Plugins/SmartStore.AmazonPay/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.AmazonPay/Localization/resources.en-us.xml @@ -8,15 +8,14 @@ - Register now at Amazon Payments.

      + 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.
      -

      Note! You get the billing address of the customer after the payment has been authorised by Amazon, so usually shortly after the order confirmation E-mail has been sent to the customer. To avoid misunderstandings, it is therefore advisable to remove the placeholder for the billing address from the message template for order confirmations.

      -

      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.
      • @@ -24,6 +23,9 @@ Text suggestions:

        ]]> + + Please note that this billing address is possibly not the valid billing address for this order! + There was no order reference ID transmitted by Amazon! @@ -66,7 +68,7 @@ Text suggestions:
          Use Sandbox - Check to use the sandbox (testing environment). + Check the box to use the sandbox (testing environment). Your Merchant ID @@ -129,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. @@ -150,7 +152,7 @@ Text suggestions:
              Payment action - Determines when to debit the customer account. + Specifies when to debit the customer account. Immediately debit @@ -165,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 @@ -177,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 @@ -216,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/Services/AmazonPayApi.cs b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayApi.cs index 6e69b3596c..59e41a1e2b 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayApi.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayApi.cs @@ -1,28 +1,28 @@ using System; +using System.Collections.Generic; +using System.Globalization; using System.IO; -using System.Web; using System.Linq; using System.Text; -using System.Globalization; -using System.Collections.Generic; +using System.Web; using OffAmazonPaymentsService; using OffAmazonPaymentsService.Model; -using SmartStore.Utilities; -using SmartStore.AmazonPay.Services; using SmartStore.AmazonPay.Extensions; +using SmartStore.AmazonPay.Services; using SmartStore.AmazonPay.Settings; using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Directory; +using SmartStore.Core.Domain.Discounts; using SmartStore.Core.Domain.Orders; -using SmartStore.Services.Payments; -using SmartStore.Services.Directory; -using SmartStore.Services.Orders; -using SmartStore.Services.Localization; -using SmartStore.Services.Common; -using SmartStore.Services.Helpers; using SmartStore.Core.Infrastructure; using SmartStore.Core.Plugins; -using SmartStore.Core.Domain.Directory; -using SmartStore.Core.Domain.Discounts; +using SmartStore.Services; +using SmartStore.Services.Common; +using SmartStore.Services.Directory; +using SmartStore.Services.Helpers; +using SmartStore.Services.Orders; +using SmartStore.Services.Payments; +using SmartStore.Utilities; namespace SmartStore.AmazonPay.Api { @@ -31,33 +31,30 @@ public class AmazonPayApi : IAmazonPayApi private readonly ICountryService _countryService; private readonly IStateProvinceService _stateProvinceService; private readonly IOrderService _orderService; - private readonly ILocalizationService _localizationService; private readonly IAddressService _addressService; private readonly IDateTimeHelper _dateTimeHelper; - private readonly ICurrencyService _currencyService; private readonly CurrencySettings _currencySettings; private readonly IOrderTotalCalculationService _orderTotalCalculationService; + private readonly ICommonServices _services; public AmazonPayApi( ICountryService countryService, IStateProvinceService stateProvinceService, IOrderService orderService, - ILocalizationService localizationService, IAddressService addressService, IDateTimeHelper dateTimeHelper, - ICurrencyService currencyService, CurrencySettings currencySettings, - IOrderTotalCalculationService orderTotalCalculationService) + IOrderTotalCalculationService orderTotalCalculationService, + ICommonServices services) { _countryService = countryService; _stateProvinceService = stateProvinceService; _orderService = orderService; - _localizationService = localizationService; _addressService = addressService; _dateTimeHelper = dateTimeHelper; - _currencyService = currencyService; _currencySettings = currencySettings; _orderTotalCalculationService = orderTotalCalculationService; + _services = services; } private string GetRandomId(string prefix) @@ -221,7 +218,7 @@ public OrderReferenceDetails SetOrderReferenceDetails(AmazonPayClient client, st if (orderTotalAmount.HasValue) { - attributes.OrderTotal = new OrderTotal() + attributes.OrderTotal = new OrderTotal { Amount = orderTotalAmount.Value.ToString("0.00", CultureInfo.InvariantCulture), CurrencyCode = currencyCode ?? "EUR" @@ -230,7 +227,7 @@ public OrderReferenceDetails SetOrderReferenceDetails(AmazonPayClient client, st if (orderGuid.HasValue()) { - attributes.SellerOrderAttributes = new SellerOrderAttributes() + attributes.SellerOrderAttributes = new SellerOrderAttributes { SellerOrderId = orderGuid, StoreName = storeName @@ -251,7 +248,7 @@ public OrderReferenceDetails SetOrderReferenceDetails(AmazonPayClient client, st return null; } - public OrderReferenceDetails SetOrderReferenceDetails(AmazonPayClient client, string orderReferenceId, Customer customer, List cart) + public OrderReferenceDetails SetOrderReferenceDetails(AmazonPayClient client, string orderReferenceId, string currencyCode, List cart) { decimal orderTotalDiscountAmountBase = decimal.Zero; Discount orderTotalAppliedDiscount = null; @@ -264,10 +261,9 @@ public OrderReferenceDetails SetOrderReferenceDetails(AmazonPayClient client, st if (shoppingCartTotalBase.HasValue) { - var currency = _currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId); - - return SetOrderReferenceDetails(client, orderReferenceId, shoppingCartTotalBase, currency.CurrencyCode); + return SetOrderReferenceDetails(client, orderReferenceId, shoppingCartTotalBase, currencyCode); } + return null; } @@ -435,17 +431,17 @@ public void Capture(AmazonPayClient client, CapturePaymentRequest capture, Captu result.NewPaymentStatus = capture.Order.PaymentStatus; var request = new CaptureRequest(); - var currency = _currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId); + var store = _services.StoreService.GetStoreById(capture.Order.StoreId); request.SellerId = client.Settings.SellerId; request.AmazonAuthorizationId = capture.Order.AuthorizationTransactionId; request.CaptureReferenceId = GetRandomId("Capture"); //request.SellerCaptureNote = client.Settings.SellerNoteCapture.Truncate(255); - request.CaptureAmount = new Price() + request.CaptureAmount = new Price { Amount = capture.Order.OrderTotal.ToString("0.00", CultureInfo.InvariantCulture), - CurrencyCode = currency.CurrencyCode + CurrencyCode = store.PrimaryStoreCurrency.CurrencyCode }; var response = client.Service.Capture(request); @@ -532,7 +528,7 @@ public string Refund(AmazonPayClient client, RefundPaymentRequest refund, Refund result.NewPaymentStatus = refund.Order.PaymentStatus; string amazonRefundId = null; - var currency = _currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId); + var store = _services.StoreService.GetStoreById(refund.Order.StoreId); var request = new RefundRequest(); request.SellerId = client.Settings.SellerId; @@ -540,10 +536,10 @@ public string Refund(AmazonPayClient client, RefundPaymentRequest refund, Refund request.RefundReferenceId = GetRandomId("Refund"); //request.SellerRefundNote = client.Settings.SellerNoteRefund.Truncate(255); - request.RefundAmount = new Price() + request.RefundAmount = new Price { Amount = refund.AmountToRefund.ToString("0.00", CultureInfo.InvariantCulture), - CurrencyCode = currency.CurrencyCode + CurrencyCode = store.PrimaryStoreCurrency.CurrencyCode }; var response = client.Service.Refund(request); @@ -629,7 +625,7 @@ public string ToInfoString(AmazonPayApiData data) try { - string[] strings = _localizationService.GetResource("Plugins.Payments.AmazonPay.MessageStrings").SplitSafe(";"); + string[] strings = _services.Localization.GetResource("Plugins.Payments.AmazonPay.MessageStrings").SplitSafe(";"); string state = data.State.Grow(data.ReasonCode, " "); if (data.ReasonDescription.HasValue()) diff --git a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayService.cs b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayService.cs index 04c34acd1c..12bbea250b 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayService.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/AmazonPayService.cs @@ -19,7 +19,6 @@ using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Directory; -using SmartStore.Core.Domain.Discounts; using SmartStore.Core.Domain.Logging; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; @@ -35,7 +34,6 @@ using SmartStore.Services.Messages; using SmartStore.Services.Orders; using SmartStore.Services.Payments; -using SmartStore.Services.Stores; using SmartStore.Services.Tasks; namespace SmartStore.AmazonPay.Services @@ -51,7 +49,6 @@ public class AmazonPayService : IAmazonPayService private readonly ICurrencyService _currencyService; private readonly CurrencySettings _currencySettings; private readonly ICustomerService _customerService; - private readonly IStoreService _storeService; private readonly IPriceFormatter _priceFormatter; private readonly OrderSettings _orderSettings; private readonly RewardPointsSettings _rewardPointsSettings; @@ -71,7 +68,6 @@ public AmazonPayService( ICurrencyService currencyService, CurrencySettings currencySettings, ICustomerService customerService, - IStoreService storeService, IPriceFormatter priceFormatter, OrderSettings orderSettings, RewardPointsSettings rewardPointsSettings, @@ -90,7 +86,6 @@ public AmazonPayService( _currencyService = currencyService; _currencySettings = currencySettings; _customerService = customerService; - _storeService = storeService; _priceFormatter = priceFormatter; _orderSettings = orderSettings; _rewardPointsSettings = rewardPointsSettings; @@ -341,7 +336,7 @@ public void SetupConfiguration(ConfigurationModel model) if (task == null) model.PollingTaskMinutes = 30; else - model.PollingTaskMinutes = (task.Seconds / 60); + model.PollingTaskMinutes = 30; // (task.Seconds / 60); } public string GetWidgetUrl() @@ -453,7 +448,7 @@ public AmazonPayViewModel ProcessPluginRequest(AmazonPayRequestType type, TempDa //model.IsOrderConfirmed = state.IsOrderConfirmed; } - var currency = _services.WorkContext.WorkingCurrency; + var currency = store.PrimaryStoreCurrency; var settings = _services.Settings.LoadSetting(store.Id); model.SellerId = settings.SellerId; @@ -534,7 +529,7 @@ public AmazonPayViewModel ProcessPluginRequest(AmazonPayRequestType type, TempDa _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedPaymentMethod, AmazonPayCore.SystemName, store.Id); var client = new AmazonPayClient(settings); - var unused = _api.SetOrderReferenceDetails(client, model.OrderReferenceId, customer, cart); + var unused = _api.SetOrderReferenceDetails(client, model.OrderReferenceId, store.PrimaryStoreCurrency.CurrencyCode, cart); // this is ugly... var paymentRequest = _httpContext.Session["OrderPaymentInfo"] as ProcessPaymentRequest; @@ -901,9 +896,9 @@ public PreProcessPaymentResult PreProcessPayment(ProcessPaymentRequest request) try { var orderGuid = request.OrderGuid.ToString(); - var store = _storeService.GetStoreById(request.StoreId); + var store = _services.StoreService.GetStoreById(request.StoreId); var customer = _customerService.GetCustomerById(request.CustomerId); - var currency = _currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId); + var currency = store.PrimaryStoreCurrency; var settings = _services.Settings.LoadSetting(store.Id); var state = _httpContext.GetAmazonPayState(_services.Localization); var client = new AmazonPayClient(settings); @@ -971,8 +966,8 @@ public ProcessPaymentResult ProcessPayment(ProcessPaymentRequest request) try { var orderGuid = request.OrderGuid.ToString(); - var store = _storeService.GetStoreById(request.StoreId); - var currency = _currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId); + var store = _services.StoreService.GetStoreById(request.StoreId); + var currency = store.PrimaryStoreCurrency; var settings = _services.Settings.LoadSetting(store.Id); var state = _httpContext.GetAmazonPayState(_services.Localization); var client = new AmazonPayClient(settings); @@ -1292,7 +1287,7 @@ public void DataPollingTaskInit() _scheduleTaskService.InsertTask(new ScheduleTask { Name = "{0} data polling".FormatWith(AmazonPayCore.SystemName), - Seconds = 30 * 60, + CronExpression = "*/30 * * * *", // Every 30 minutes Type = AmazonPayCore.DataPollingTaskType, Enabled = false, StopOnError = false, @@ -1306,7 +1301,7 @@ public void DataPollingTaskUpdate(bool enabled, int seconds) if (task != null) { task.Enabled = enabled; - task.Seconds = seconds; + //task.Seconds = seconds; _scheduleTaskService.UpdateTask(task); } diff --git a/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayApi.cs b/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayApi.cs index df245dd32f..70d72d9aa1 100644 --- a/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayApi.cs +++ b/src/Plugins/SmartStore.AmazonPay/Services/IAmazonPayApi.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; using System.Web; using OffAmazonPaymentsService.Model; -using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Orders; using SmartStore.AmazonPay.Services; using SmartStore.AmazonPay.Settings; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Orders; using SmartStore.Services.Payments; namespace SmartStore.AmazonPay.Api @@ -21,7 +21,7 @@ public partial interface IAmazonPayApi OrderReferenceDetails SetOrderReferenceDetails(AmazonPayClient client, string orderReferenceId, decimal? orderTotalAmount, string currencyCode, string orderGuid = null, string storeName = null); - OrderReferenceDetails SetOrderReferenceDetails(AmazonPayClient client, string orderReferenceId, Customer customer, List cart); + OrderReferenceDetails SetOrderReferenceDetails(AmazonPayClient client, string orderReferenceId, string currencyCode, List cart); void ConfirmOrderReference(AmazonPayClient client, string orderReferenceId); diff --git a/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj b/src/Plugins/SmartStore.AmazonPay/SmartStore.AmazonPay.csproj index ab7c222b62..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 @@ -139,6 +139,7 @@ + diff --git a/src/Plugins/SmartStore.AmazonPay/Views/AmazonPay/Configure.cshtml b/src/Plugins/SmartStore.AmazonPay/Views/AmazonPay/Configure.cshtml index f8193c3c87..3d77fb69e4 100644 --- a/src/Plugins/SmartStore.AmazonPay/Views/AmazonPay/Configure.cshtml +++ b/src/Plugins/SmartStore.AmazonPay/Views/AmazonPay/Configure.cshtml @@ -9,21 +9,19 @@ Html.AddScriptParts(true, Url.Content("~/Plugins/SmartStore.AmazonPay/Scripts/smartstore.amazonpay.js")); } - - - - - -
                    -
                    - - @Html.Raw(@T("Plugins.Payments.AmazonPay.AdminInstruction")) -
                    -
                    - - - -
                    +
                    +
                    +
                    + + @Html.Raw(@T("Plugins.Payments.AmazonPay.AdminInstruction")) +
                    +
                    +
                    + + + +
                    +
                    @Html.Action("StoreScopeConfiguration", "Setting", new { area = "Admin" }) 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/changelog.md b/src/Plugins/SmartStore.AmazonPay/changelog.md index 416414c398..c22eca67c8 100644 --- a/src/Plugins/SmartStore.AmazonPay/changelog.md +++ b/src/Plugins/SmartStore.AmazonPay/changelog.md @@ -1,48 +1,52 @@ -#Release Notes# +#Release Notes + +##Pay with Amazon 2.2.0.3 +###Improvements +* Added message token %SmartStore.AmazonPay.BillingAddressMessageNote% for billing address note in order placed customer notification ##Pay with Amazon 2.2.0.2 -###Bugfixes### +###Bugfixes * Send currency code of primary store currency (not of working currency) to payment gateway ##Pay with Amazon 2.2.0.1 ### New Features * Supports order list label for new incoming IPNs -##Pay with Amazon 1.20## -###Bugfixes### +##Pay with Amazon 1.20 +###Bugfixes * PlatformID must be .NET ID not merchant ID. PlatformID needs to be identical for all orders -##Pay with Amazon 1.19## -###Bugfixes### +##Pay with Amazon 1.19 +###Bugfixes * Declined authorization IPN did not void the payment status -##Pay with Amazon 1.18## -###Bugfixes### +##Pay with Amazon 1.18 +###Bugfixes * Order wasn't found if the capturing\refunding took place at Amazon Seller Central and the notification came through IPN -##Pay with Amazon 1.17## -###Improvements### +##Pay with Amazon 1.17 +###Improvements * Amazon payments review -##Pay with Amazon 1.16## -###Bugfixes### +##Pay with Amazon 1.16 +###Bugfixes * Reflect refunds made at amazon seller central when using data polling -##Pay with Amazon 1.15## -###Bugfixes### +##Pay with Amazon 1.15 +###Bugfixes * Multistore configuration might be lost if "All stores" are left empty -##Pay with Amazon 1.14## -###Bugfixes### +##Pay with Amazon 1.14 +###Bugfixes * Data polling did not reflect the transaction status correctly if the action took place at amazon seller central -##Pay with Amazon 1.13## -###Bugfixes### +##Pay with Amazon 1.13 +###Bugfixes * Sometimes shipping method restrictions were not applied * Shipping tab in checkout may show "Shipping address is not set" even if set -##Pay with Amazon 1.12## -###Improvements### +##Pay with Amazon 1.12 +###Improvements * Created this changelog * Changed string „Bezahlen über Amazon“ into „Bezahlen mit Amazon“ 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 ba454d5fcc..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 @@ -27,7 +27,7 @@ Enabled - Check to enable SMS provider + Check the box to enable SMS provider API ID diff --git a/src/Plugins/SmartStore.Clickatell/OrderPlacedEventConsumer.cs b/src/Plugins/SmartStore.Clickatell/OrderPlacedEventConsumer.cs index 580460d574..3ba9e1742b 100644 --- a/src/Plugins/SmartStore.Clickatell/OrderPlacedEventConsumer.cs +++ b/src/Plugins/SmartStore.Clickatell/OrderPlacedEventConsumer.cs @@ -14,7 +14,7 @@ public class OrderPlacedEventConsumer : IConsumer private readonly IPluginFinder _pluginFinder; private readonly IOrderService _orderService; private readonly IStoreContext _storeContext; - private readonly ISettingService _settingService; // codehint: sm-add + private readonly ISettingService _settingService; public OrderPlacedEventConsumer(ClickatellSettings clickatellSettings, IPluginFinder pluginFinder, @@ -26,7 +26,7 @@ public OrderPlacedEventConsumer(ClickatellSettings clickatellSettings, this._pluginFinder = pluginFinder; this._orderService = orderService; this._storeContext = storeContext; - this._settingService = settingService; // codehint: sm-add + this._settingService = settingService; } /// 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")
+ @Html.SmartLabelFor(model => model.DisplayWidgetZones) + + @Html.SettingEditorFor(model => model.DisplayWidgetZones) + @Html.ValidationMessageFor(model => model.DisplayWidgetZones) +
diff --git a/src/Plugins/SmartStore.DevTools/Views/DevTools/WidgetZone.cshtml b/src/Plugins/SmartStore.DevTools/Views/DevTools/WidgetZone.cshtml new file mode 100644 index 0000000000..222fe683e0 --- /dev/null +++ b/src/Plugins/SmartStore.DevTools/Views/DevTools/WidgetZone.cshtml @@ -0,0 +1,3 @@ + + Widget Zone + 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 4f2a281983..b076d239ac 100644 --- a/src/Plugins/SmartStore.DevTools/Web.config +++ b/src/Plugins/SmartStore.DevTools/Web.config @@ -1,131 +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 63f26d3792..325d93b6d4 100644 --- a/src/Plugins/SmartStore.DevTools/packages.config +++ b/src/Plugins/SmartStore.DevTools/packages.config @@ -1,11 +1,12 @@  - - - + + + + \ No newline at end of file diff --git a/src/Plugins/SmartStore.DiscountRules/Controllers/DiscountRulesController.cs b/src/Plugins/SmartStore.DiscountRules/Controllers/DiscountRulesController.cs index c004670e23..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 { @@ -31,13 +32,8 @@ public DiscountRulesController( this._customerService = customerService; this._countryService = countryService; this._storeService = storeService; - - T = NullLocalizer.Instance; } - public Localizer T { get; set; } - - #region Global [NonAction] 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 386027e411..5a99884991 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs +++ b/src/Plugins/SmartStore.FacebookAuth/Controllers/ExternalAuthFacebookController.cs @@ -1,75 +1,88 @@ using System.Web.Mvc; -using SmartStore.Core; using SmartStore.Core.Domain.Customers; using SmartStore.FacebookAuth.Core; using SmartStore.FacebookAuth.Models; +using SmartStore.Services; using SmartStore.Services.Authentication.External; -using SmartStore.Services.Configuration; 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 { //[UnitOfWork] public class ExternalAuthFacebookController : PluginControllerBase { - private readonly ISettingService _settingService; - private readonly FacebookExternalAuthSettings _facebookExternalAuthSettings; private readonly IOAuthProviderFacebookAuthorizer _oAuthProviderFacebookAuthorizer; private readonly IOpenAuthenticationService _openAuthenticationService; private readonly ExternalAuthenticationSettings _externalAuthenticationSettings; - private readonly IStoreContext _storeContext; - private readonly IPermissionService _permissionService; + private readonly ICommonServices _services; - public ExternalAuthFacebookController(ISettingService settingService, - FacebookExternalAuthSettings facebookExternalAuthSettings, + public ExternalAuthFacebookController( IOAuthProviderFacebookAuthorizer oAuthProviderFacebookAuthorizer, IOpenAuthenticationService openAuthenticationService, ExternalAuthenticationSettings externalAuthenticationSettings, - IStoreContext storeContext, - IPermissionService permissionService) + ICommonServices services) { - this._settingService = settingService; - this._facebookExternalAuthSettings = facebookExternalAuthSettings; this._oAuthProviderFacebookAuthorizer = oAuthProviderFacebookAuthorizer; this._openAuthenticationService = openAuthenticationService; this._externalAuthenticationSettings = externalAuthenticationSettings; - this._storeContext = storeContext; - this._permissionService = permissionService; + this._services = services; } + + private bool HasPermission(bool notify = true) + { + bool hasPermission = _services.Permissions.Authorize(StandardPermissionProvider.ManageExternalAuthenticationMethods); + + if (notify && !hasPermission) + NotifyError(_services.Localization.GetResource("Admin.AccessDenied.Description")); + + return hasPermission; + } - [AdminAuthorize] - [ChildActionOnly] + [AdminAuthorize, ChildActionOnly] public ActionResult Configure() { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageExternalAuthenticationMethods)) - return Content("Access denied"); + if (!HasPermission(false)) + return AccessDeniedPartialView(); var model = new ConfigurationModel(); - model.ClientKeyIdentifier = _facebookExternalAuthSettings.ClientKeyIdentifier; - model.ClientSecret = _facebookExternalAuthSettings.ClientSecret; + int storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); + var settings = _services.Settings.LoadSetting(storeScope); + + model.ClientKeyIdentifier = settings.ClientKeyIdentifier; + model.ClientSecret = settings.ClientSecret; + + var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); + storeDependingSettingHelper.GetOverrideKeys(settings, model, storeScope, _services.Settings); return View(model); } - [HttpPost] - [AdminAuthorize] - [ChildActionOnly] - public ActionResult Configure(ConfigurationModel model) + [HttpPost, AdminAuthorize, ChildActionOnly] + public ActionResult Configure(ConfigurationModel model, FormCollection form) { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageExternalAuthenticationMethods)) - return Content("Access denied"); + if (!HasPermission(false)) + return Configure(); if (!ModelState.IsValid) return Configure(); - - //save settings - _facebookExternalAuthSettings.ClientKeyIdentifier = model.ClientKeyIdentifier; - _facebookExternalAuthSettings.ClientSecret = model.ClientSecret; - _settingService.SaveSetting(_facebookExternalAuthSettings); - - return View(model); + + var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); + int storeScope = this.GetActiveStoreScopeConfiguration(_services.StoreService, _services.WorkContext); + var settings = _services.Settings.LoadSetting(storeScope); + + settings.ClientKeyIdentifier = model.ClientKeyIdentifier; + settings.ClientSecret = model.ClientSecret; + + storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, _services.Settings); + _services.Settings.ClearCache(); + + NotifySuccess(_services.Localization.GetResource("Admin.Common.DataSuccessfullySaved")); + + return Configure(); } [ChildActionOnly] @@ -81,7 +94,7 @@ public ActionResult PublicInfo() [NonAction] private ActionResult LoginInternal(string returnUrl, bool verifyResponse) { - var processor = _openAuthenticationService.LoadExternalAuthenticationMethodBySystemName("SmartStore.FacebookAuth", _storeContext.CurrentStore.Id); + var processor = _openAuthenticationService.LoadExternalAuthenticationMethodBySystemName(Provider.SystemName, _services.StoreContext.CurrentStore.Id); if (processor == null || !processor.IsMethodActive(_externalAuthenticationSettings)) { throw new SmartException("Facebook module cannot be loaded"); @@ -122,9 +135,13 @@ private ActionResult LoginInternal(string returnUrl, bool verifyResponse) break; } - if (result.Result != null) return result.Result; - return HttpContext.Request.IsAuthenticated ? new RedirectResult(!string.IsNullOrEmpty(returnUrl) ? returnUrl : "~/") : new RedirectResult(Url.LogOn(returnUrl)); - } + if (result.Result != null) + return result.Result; + + 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 6da9e37086..378599e6d5 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs +++ b/src/Plugins/SmartStore.FacebookAuth/Core/FacebookProviderAuthorizer.cs @@ -2,29 +2,32 @@ 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 SmartStore.Core; +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 private readonly IExternalAuthorizer _authorizer; private readonly IOpenAuthenticationService _openAuthenticationService; private readonly ExternalAuthenticationSettings _externalAuthenticationSettings; - private readonly FacebookExternalAuthSettings _facebookExternalAuthSettings; private readonly HttpContextBase _httpContext; - private readonly IWebHelper _webHelper; - private FacebookClient _facebookApplication; + private readonly ICommonServices _services; + + private FacebookOAuth2Client _facebookApplication; #endregion @@ -33,25 +36,33 @@ public class FacebookProviderAuthorizer : IOAuthProviderFacebookAuthorizer public FacebookProviderAuthorizer(IExternalAuthorizer authorizer, IOpenAuthenticationService openAuthenticationService, ExternalAuthenticationSettings externalAuthenticationSettings, - FacebookExternalAuthSettings facebookExternalAuthSettings, HttpContextBase httpContext, - IWebHelper webHelper) + ICommonServices services) { this._authorizer = authorizer; this._openAuthenticationService = openAuthenticationService; this._externalAuthenticationSettings = externalAuthenticationSettings; - this._facebookExternalAuthSettings = facebookExternalAuthSettings; this._httpContext = httpContext; - this._webHelper = webHelper; + this._services = services; } #endregion #region Utilities - private FacebookClient FacebookApplication + private FacebookOAuth2Client FacebookApplication { - get { return _facebookApplication ?? (_facebookApplication = new FacebookClient(_facebookExternalAuthSettings.ClientKeyIdentifier, _facebookExternalAuthSettings.ClientSecret)); } + get + { + if (_facebookApplication == null) + { + var settings = _services.Settings.LoadSetting(_services.StoreContext.CurrentStore.Id); + + _facebookApplication = new FacebookOAuth2Client(settings.ClientKeyIdentifier, settings.ClientSecret); + } + + return _facebookApplication; + } } private AuthorizeState VerifyAuthentication(string returnUrl) @@ -82,18 +93,50 @@ 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(); + if (authenticationResult.ExtraData.ContainsKey("name")) { var name = authenticationResult.ExtraData["name"]; @@ -122,7 +165,7 @@ private AuthorizeState RequestAuthentication(string returnUrl) private Uri GenerateLocalCallbackUri() { - string url = string.Format("{0}Plugins/SmartStore.FacebookAuth/logincallback/", _webHelper.GetStoreLocation()); + string url = string.Format("{0}Plugins/SmartStore.FacebookAuth/logincallback/", _services.WebHelper.GetStoreLocation()); return new Uri(url); } @@ -131,10 +174,14 @@ private Uri GenerateServiceLoginUrl() //code copied from DotNetOpenAuth.AspNet.Clients.FacebookClient file var builder = new UriBuilder("https://www.facebook.com/dialog/oauth"); var args = new Dictionary(); - args.Add("client_id", _facebookExternalAuthSettings.ClientKeyIdentifier); + var settings = _services.Settings.LoadSetting(_services.StoreContext.CurrentStore.Id); + + args.Add("client_id", settings.ClientKeyIdentifier); args.Add("redirect_uri", GenerateLocalCallbackUri().AbsoluteUri); args.Add("scope", "email"); + AppendQueryArgs(builder, args); + return builder.Uri; } @@ -152,6 +199,7 @@ private void AppendQueryArgs(UriBuilder builder, IEnumerable> args) { if (!args.Any>()) @@ -169,7 +217,9 @@ private string CreateQueryString(IEnumerable> args) builder.Length--; return builder.ToString(); } + private readonly string[] UriRfc3986CharsToEscape = new string[] { "!", "*", "'", "(", ")" }; + private string EscapeUriDataStringRfc3986(string value) { StringBuilder builder = new StringBuilder(Uri.EscapeDataString(value)); diff --git a/src/Plugins/SmartStore.FacebookAuth/DependencyRegistrar.cs b/src/Plugins/SmartStore.FacebookAuth/DependencyRegistrar.cs index 69f5a94064..ef4c32cf34 100644 --- a/src/Plugins/SmartStore.FacebookAuth/DependencyRegistrar.cs +++ b/src/Plugins/SmartStore.FacebookAuth/DependencyRegistrar.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq.Expressions; using Autofac; -using Autofac.Integration.Mvc; using SmartStore.Core.Infrastructure; using SmartStore.Core.Infrastructure.DependencyManagement; using SmartStore.FacebookAuth.Core; diff --git a/src/Plugins/SmartStore.FacebookAuth/Description.txt b/src/Plugins/SmartStore.FacebookAuth/Description.txt index 081bf95dd1..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 -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/FacebookExternalAuthMethod.cs b/src/Plugins/SmartStore.FacebookAuth/FacebookExternalAuthMethod.cs index 2c4c183c76..d3a0ea7b26 100644 --- a/src/Plugins/SmartStore.FacebookAuth/FacebookExternalAuthMethod.cs +++ b/src/Plugins/SmartStore.FacebookAuth/FacebookExternalAuthMethod.cs @@ -1,5 +1,6 @@ using System.Web.Routing; using SmartStore.Core.Plugins; +using SmartStore.FacebookAuth.Core; using SmartStore.Services.Authentication.External; using SmartStore.Services.Localization; @@ -11,8 +12,10 @@ namespace SmartStore.FacebookAuth public class FacebookExternalAuthMethod : BasePlugin, IExternalAuthenticationMethod, IConfigurable { #region Fields + private readonly FacebookExternalAuthSettings _facebookExternalAuthSettings; private readonly ILocalizationService _localizationService; + #endregion #region Ctor @@ -37,7 +40,7 @@ public void GetConfigurationRoute(out string actionName, out string controllerNa { actionName = "Configure"; controllerName = "ExternalAuthFacebook"; - routeValues = new RouteValueDictionary(new { Namespaces = "SmartStore.FacebookAuth.Controllers", area = "SmartStore.FacebookAuth" }); + routeValues = new RouteValueDictionary(new { Namespaces = "SmartStore.FacebookAuth.Controllers", area = Provider.SystemName }); } /// @@ -50,7 +53,7 @@ public void GetPublicInfoRoute(out string actionName, out string controllerName, { actionName = "PublicInfo"; controllerName = "ExternalAuthFacebook"; - routeValues = new RouteValueDictionary(new { Namespaces = "SmartStore.FacebookAuth.Controllers", area = "SmartStore.FacebookAuth" }); + routeValues = new RouteValueDictionary(new { Namespaces = "SmartStore.FacebookAuth.Controllers", area = Provider.SystemName }); } /// @@ -68,7 +71,6 @@ public override void Uninstall() { //locales _localizationService.DeleteLocaleStringResources(this.PluginDescriptor.ResourceRootKey); - _localizationService.DeleteLocaleStringResources("Plugins.FriendlyName.ExternalAuth.Facebook", false); base.Uninstall(); } 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 5c78e61896..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 { @@ -7,6 +7,7 @@ public class ConfigurationModel : ModelBase { [SmartResourceDisplayName("Plugins.ExternalAuth.Facebook.ClientKeyIdentifier")] public string ClientKeyIdentifier { get; set; } + [SmartResourceDisplayName("Plugins.ExternalAuth.Facebook.ClientSecret")] public string ClientSecret { get; set; } } diff --git a/src/Plugins/SmartStore.FacebookAuth/RouteProvider.cs b/src/Plugins/SmartStore.FacebookAuth/RouteProvider.cs index 1326663260..90465eb41a 100644 --- a/src/Plugins/SmartStore.FacebookAuth/RouteProvider.cs +++ b/src/Plugins/SmartStore.FacebookAuth/RouteProvider.cs @@ -1,6 +1,7 @@ using System.Web.Mvc; using System.Web.Routing; -using SmartStore.Web.Framework.Mvc.Routes; +using SmartStore.FacebookAuth.Core; +using SmartStore.Web.Framework.Routing; namespace SmartStore.FacebookAuth { @@ -13,7 +14,7 @@ public void RegisterRoutes(RouteCollection routes) new { controller = "ExternalAuthFacebook" }, new[] { "SmartStore.FacebookAuth.Controllers" } ) - .DataTokens["area"] = "SmartStore.FacebookAuth"; + .DataTokens["area"] = Provider.SystemName; } public int Priority { 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/ExternalAuthFacebook/Configure.cshtml b/src/Plugins/SmartStore.FacebookAuth/Views/ExternalAuthFacebook/Configure.cshtml index 94d69f5d8a..60dc28cea5 100644 --- a/src/Plugins/SmartStore.FacebookAuth/Views/ExternalAuthFacebook/Configure.cshtml +++ b/src/Plugins/SmartStore.FacebookAuth/Views/ExternalAuthFacebook/Configure.cshtml @@ -3,23 +3,29 @@ } @model SmartStore.FacebookAuth.Models.ConfigurationModel @using SmartStore.Web.Framework; + +
+ + @Html.Raw(T("Plugins.ExternalAuth.Facebook.AdminInstruction")) +
+ +
+ +@Html.Action("StoreScopeConfiguration", "Setting", new { area = "Admin" }) + +@Html.ValidationSummary(false) + +
+ @using (Html.BeginForm()) { - - - @@ -28,7 +34,7 @@ @Html.SmartLabelFor(model => model.ClientSecret) @@ -43,5 +49,4 @@
-
- - @Html.Raw(T("Plugins.ExternalAuth.Facebook.AdminInstruction")) -
-
@Html.SmartLabelFor(model => model.ClientKeyIdentifier) - @Html.EditorFor(model => model.ClientKeyIdentifier) + @Html.SettingEditorFor(model => model.ClientKeyIdentifier) @Html.ValidationMessageFor(model => model.ClientKeyIdentifier)
- @Html.EditorFor(model => model.ClientSecret) + @Html.SettingEditorFor(model => model.ClientSecret) @Html.ValidationMessageFor(model => model.ClientSecret)
- } \ No newline at end of file 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 3ed283c958..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 @@ -126,19 +127,6 @@ private Order GetLastOrder() return order; } - // private string GetTrackingScript() { var googleAnalyticsSettings = _settingService.LoadSetting(_storeContext.CurrentStore.Id); @@ -149,42 +137,6 @@ private string GetTrackingScript() return analyticsTrackingScript; } - // private string GetEcommerceScript(Order order) { var googleAnalyticsSettings = _settingService.LoadSetting(_storeContext.CurrentStore.Id); @@ -206,6 +158,7 @@ private string GetEcommerceScript(Order order) analyticsEcommerceScript = analyticsEcommerceScript.Replace("{CITY}", order.BillingAddress == null ? "" : FixIllegalJavaScriptChars(order.BillingAddress.City)); analyticsEcommerceScript = analyticsEcommerceScript.Replace("{STATEPROVINCE}", order.BillingAddress == null || order.BillingAddress.StateProvince == null ? "" : FixIllegalJavaScriptChars(order.BillingAddress.StateProvince.Name)); analyticsEcommerceScript = analyticsEcommerceScript.Replace("{COUNTRY}", order.BillingAddress == null || order.BillingAddress.Country == null ? "" : FixIllegalJavaScriptChars(order.BillingAddress.Country.Name)); + analyticsEcommerceScript = analyticsEcommerceScript.Replace("{CURRENCY}", order.CustomerCurrencyCode); var sb = new StringBuilder(); foreach (var item in order.OrderItems) diff --git a/src/Plugins/SmartStore.GoogleAnalytics/Description.txt b/src/Plugins/SmartStore.GoogleAnalytics/Description.txt index 4a7c23c2ff..59bf21b018 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/Description.txt +++ b/src/Plugins/SmartStore.GoogleAnalytics/Description.txt @@ -1,9 +1,9 @@ FriendlyName: Google Analytics SystemName: SmartStore.GoogleAnalytics Group: Analytics -Version: 2.2.0 -MinAppVersion: 2.2.0 +Version: 2.6.0 +MinAppVersion: 2.5.0 DisplayOrder: 1 FileName: SmartStore.GoogleAnalytics.dll ResourceRootKey: Plugins.Widgets.GoogleAnalytics -Url: http://community.smartstore.com/index.php?/files/file/25-google-analytics/ \ No newline at end of file +Url: http://community.smartstore.com/marketplace/file/25-google-analytics/ \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleAnalytics/GoogleAnalyticPlugin.cs b/src/Plugins/SmartStore.GoogleAnalytics/GoogleAnalyticPlugin.cs index fe48664ee6..185c203527 100644 --- a/src/Plugins/SmartStore.GoogleAnalytics/GoogleAnalyticPlugin.cs +++ b/src/Plugins/SmartStore.GoogleAnalytics/GoogleAnalyticPlugin.cs @@ -31,9 +31,16 @@ public GoogleAnalyticPlugin(ISettingService settingService, /// Widget zones public IList GetWidgetZones() { - return !string.IsNullOrWhiteSpace(_googleAnalyticsSettings.WidgetZone) - ? new List() { _googleAnalyticsSettings.WidgetZone } - : new List() { "head_html_tag" }; + var zones = new List() { "head_html_tag", "mobile_head_html_tag" }; + if(!string.IsNullOrWhiteSpace(_googleAnalyticsSettings.WidgetZone)) + { + zones = new List() { + _googleAnalyticsSettings.WidgetZone, + _googleAnalyticsSettings.WidgetZone == "head_html_tag" ? "mobile_head_html_tag" : "mobile_body_end_html_tag_after" + }; + } + + return zones; } /// @@ -46,7 +53,7 @@ public void GetConfigurationRoute(out string actionName, out string controllerNa { actionName = "Configure"; controllerName = "WidgetsGoogleAnalytics"; - routeValues = new RouteValueDictionary() { /*{ "Namespaces", "SmartStore.GoogleAnalytics.Controllers" },*/ { "area", "SmartStore.GoogleAnalytics" } }; + routeValues = new RouteValueDictionary() { { "area", "SmartStore.GoogleAnalytics" } }; } /// @@ -62,7 +69,6 @@ public void GetDisplayWidgetRoute(string widgetZone, object model, int storeId, controllerName = "WidgetsGoogleAnalytics"; routeValues = new RouteValueDictionary() { - //{"Namespaces", "SmartStore.GoogleAnalytics.Controllers"}, {"area", "SmartStore.GoogleAnalytics"}, {"widgetZone", widgetZone} }; @@ -76,23 +82,44 @@ public override void Install() var settings = new GoogleAnalyticsSettings() { GoogleId = "UA-0000000-0", - //TrackingScript = " ", TrackingScript = @" -", - EcommerceScript = @"_gaq.push(['_addTrans', '{ORDERID}', '{SITE}', '{TOTAL}', '{TAX}', '{SHIP}', '{CITY}', '{STATEPROVINCE}', '{COUNTRY}']); -{DETAILS} -_gaq.push(['_trackTrans']); ", - EcommerceDetailScript = @"_gaq.push(['_addItem', '{ORDERID}', '{PRODUCTSKU}', '{PRODUCTNAME}', '{CATEGORYNAME}', '{UNITPRICE}', '{QUANTITY}' ]); ", + ", + EcommerceScript = @" + ga('require', 'ecommerce'); + + ga('ecommerce:addTransaction', { + 'id': '{ORDERID}', + 'affiliation': '{SITE}', + 'revenue': '{TOTAL}', + 'shipping': '{SHIP}', + 'tax': '{TAX}', + 'currency': '{CURRENCY}' + }); + + {DETAILS} + + ga('ecommerce:send'); + ", + EcommerceDetailScript = @" + ga('ecommerce:addItem', { + 'id': '{ORDERID}', + 'name': '{PRODUCTNAME}', + 'sku': '{PRODUCTSKU}', + 'category': '{CATEGORYNAME}', + 'price': '{UNITPRICE}', + 'quantity': '{QUANTITY}' + }); + ", }; _settingService.SaveSetting(settings); 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 97b051cfbc..2dbc226f57 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/AdminMenu.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/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.GoogleMerchantCenter { @@ -13,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 = "SmartStore.GoogleMerchantCenter", area = "Admin" }) + .Action("ConfigurePlugin", "Plugin", new { systemName = GoogleMerchantCenterFeedPlugin.SystemName, area = "Admin" }) .ToItem(); - root.Append(menuItem); + pluginsNode.Prepend(menuItem); } } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Content/images/mc_logo.png b/src/Plugins/SmartStore.GoogleMerchantCenter/Content/branding.png similarity index 100% rename from src/Plugins/SmartStore.GoogleMerchantCenter/Content/images/mc_logo.png rename to src/Plugins/SmartStore.GoogleMerchantCenter/Content/branding.png diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Content/smartstore.feed.froogle.css b/src/Plugins/SmartStore.GoogleMerchantCenter/Content/smartstore.feed.froogle.css deleted file mode 100644 index 3c4b0af7b7..0000000000 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Content/smartstore.feed.froogle.css +++ /dev/null @@ -1,10 +0,0 @@ -.config-logo { - width: 274px; - height: 31px; -} -.edit-taxonomy { - min-width: 460px; -} -.google-product-search { - margin-bottom: 8px !important; -} \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Content/smartstore.gmc.css b/src/Plugins/SmartStore.GoogleMerchantCenter/Content/smartstore.gmc.css new file mode 100644 index 0000000000..4a8bfa9c72 --- /dev/null +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Content/smartstore.gmc.css @@ -0,0 +1,6 @@ +.edit-taxonomy { + min-width: 460px; +} +.gmc-product-search { + margin-bottom: 8px !important; +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedFroogleController.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedFroogleController.cs deleted file mode 100644 index 312ff89ff7..0000000000 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedFroogleController.cs +++ /dev/null @@ -1,180 +0,0 @@ -using System.Collections.Generic; -using System.Globalization; -using System.Web.Mvc; -using SmartStore.Core.Localization; -using SmartStore.GoogleMerchantCenter.Models; -using SmartStore.GoogleMerchantCenter.Services; -using SmartStore.Services.Configuration; -using SmartStore.Services.Security; -using SmartStore.Web.Framework.Controllers; -using SmartStore.Web.Framework.Plugins; -using Telerik.Web.Mvc; - -namespace SmartStore.GoogleMerchantCenter.Controllers -{ - [AdminAuthorize] - public class FeedFroogleController : PluginControllerBase - { - private readonly FroogleSettings _settings; - private readonly IGoogleFeedService _googleService; - private readonly ISettingService _settingService; - private readonly IPermissionService _permissionService; - - public FeedFroogleController( - FroogleSettings settings, - IGoogleFeedService googleService, - ISettingService settingService, - IPermissionService permissionService) - { - _settings = settings; - _googleService = googleService; - _settingService = settingService; - _permissionService = permissionService; - - T = NullLocalizer.Instance; - } - - public Localizer T { get; set; } - - private ActionResult RedirectToConfig() - { - return RedirectToAction("ConfigurePlugin", "Plugin", new { systemName = _googleService.Helper.SystemName, area = "Admin" }); - } - - public ActionResult ProductEditTab(int productId) - { - var model = new GoogleProductModel { ProductId = productId }; - var entity = _googleService.GetGoogleProductRecord(productId); - - if (entity != null) - { - model.Taxonomy = entity.Taxonomy; - model.Gender = entity.Gender; - model.AgeGroup = entity.AgeGroup; - model.Color = entity.Color; - model.Size = entity.Size; - model.Material = entity.Material; - model.Pattern = entity.Pattern; - model.Exporting = entity.Export; - } - - ViewBag.DefaultCategory = _settings.DefaultGoogleCategory; - ViewBag.DefaultGender = T("Common.Auto"); - ViewBag.DefaultAgeGroup = T("Common.Auto"); - ViewBag.DefaultColor = _settings.Color; - ViewBag.DefaultSize = _settings.Size; - ViewBag.DefaultMaterial = _settings.Material; - ViewBag.DefaultPattern = _settings.Pattern; - - var ci = CultureInfo.InvariantCulture; - - if (_settings.Gender.HasValue() && _settings.Gender != PluginHelper.NotSpecified) - { - ViewBag.DefaultGender = T("Plugins.Feed.Froogle.Gender" + ci.TextInfo.ToTitleCase(_settings.Gender)); - } - - if (_settings.AgeGroup.HasValue() && _settings.AgeGroup != PluginHelper.NotSpecified) - { - ViewBag.DefaultAgeGroup = T("Plugins.Feed.Froogle.AgeGroup" + ci.TextInfo.ToTitleCase(_settings.AgeGroup)); - } - - - ViewBag.AvailableGenders = new List - { - new SelectListItem { Value = "male", Text = T("Plugins.Feed.Froogle.GenderMale") }, - new SelectListItem { Value = "female", Text = T("Plugins.Feed.Froogle.GenderFemale") }, - new SelectListItem { Value = "unisex", Text = T("Plugins.Feed.Froogle.GenderUnisex") } - }; - - ViewBag.AvailableAgeGroups = new List - { - new SelectListItem { Value = "adult", Text = T("Plugins.Feed.Froogle.AgeGroupAdult") }, - new SelectListItem { Value = "kids", Text = T("Plugins.Feed.Froogle.AgeGroupKids") }, - }; - - var result = PartialView(model); - result.ViewData.TemplateInfo = new TemplateInfo { HtmlFieldPrefix = "CustomProperties[GMC]" }; - return result; - } - - public ActionResult GoogleCategories() - { - var categories = _googleService.GetTaxonomyList(); - return Json(categories, JsonRequestBehavior.AllowGet); - } - - public ActionResult Configure() - { - var model = new FeedFroogleModel(); - model.Copy(_googleService.Settings, true); - - if (TempData["GenerateFeedRunning"] != null) - model.IsRunning = (bool)TempData["GenerateFeedRunning"]; - - _googleService.SetupModel(model); - - return View(model); - } - - [HttpPost] - [FormValueRequired("save")] - public ActionResult Configure(FeedFroogleModel model) - { - if (!ModelState.IsValid) - return Configure(); - - model.Copy(_googleService.Settings, false); - _settingService.SaveSetting(_googleService.Settings); - - _googleService.Helper.UpdateScheduleTask(model.TaskEnabled, model.GenerateStaticFileEachMinutes * 60); - - NotifySuccess(_googleService.Helper.GetResource("ConfigSaveNote"), true); - - _googleService.SetupModel(model); - - return View(model); - } - - public ActionResult GenerateFeed() - { - if (!_permissionService.Authorize(StandardPermissionProvider.ManageScheduleTasks)) - return AccessDeniedView(); - - if (_googleService.Helper.RunScheduleTask()) - TempData["GenerateFeedRunning"] = true; - - return RedirectToConfig(); - } - - [HttpPost] - public ActionResult GenerateFeedProgress() - { - string message = _googleService.Helper.GetProgressInfo(true); - return Json(new { message = message }, JsonRequestBehavior.DenyGet); - } - - public ActionResult DeleteFiles() - { - _googleService.Helper.DeleteFeedFiles(); - - return RedirectToConfig(); - } - - [HttpPost] - public ActionResult GoogleProductEdit(int pk, string name, string value) - { - _googleService.UpdateInsert(pk, name, value); - - return this.Content(""); - } - - [HttpPost, GridAction(EnableCustomBinding = true)] - public ActionResult GoogleProductList(GridCommand command, string searchProductName, string touched) - { - return new JsonResult - { - Data = _googleService.GetGridModel(command, searchProductName, touched) - }; - } - } -} diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedGoogleMerchantCenterController.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedGoogleMerchantCenterController.cs new file mode 100644 index 0000000000..6c37461c87 --- /dev/null +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Controllers/FeedGoogleMerchantCenterController.cs @@ -0,0 +1,161 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Web.Mvc; +using SmartStore.Core; +using SmartStore.Core.Domain.Common; +using SmartStore.GoogleMerchantCenter.Models; +using SmartStore.GoogleMerchantCenter.Providers; +using SmartStore.GoogleMerchantCenter.Services; +using SmartStore.Services.DataExchange.Export; +using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Security; +using Telerik.Web.Mvc; + +namespace SmartStore.GoogleMerchantCenter.Controllers +{ + [AdminAuthorize] + public class FeedGoogleMerchantCenterController : PluginControllerBase + { + private readonly IGoogleFeedService _googleFeedService; + private readonly AdminAreaSettings _adminAreaSettings; + private readonly IExportProfileService _exportService; + + public FeedGoogleMerchantCenterController( + IGoogleFeedService googleFeedService, + AdminAreaSettings adminAreaSettings, + IExportProfileService exportService) + { + _googleFeedService = googleFeedService; + _adminAreaSettings = adminAreaSettings; + _exportService = exportService; + } + + 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.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.Export2 = true; + } + + ViewBag.DefaultCategory = ""; + ViewBag.DefaultColor = ""; + ViewBag.DefaultSize = ""; + ViewBag.DefaultMaterial = ""; + ViewBag.DefaultPattern = ""; + 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(GmcXmlExportProvider.SystemName).FirstOrDefault(); + if (profile != null) + { + var config = XmlHelper.Deserialize(profile.ProviderConfigData, typeof(ProfileConfigurationModel)) as ProfileConfigurationModel; + if (config != null) + { + ViewBag.DefaultCategory = config.DefaultGoogleCategory; + ViewBag.DefaultColor = config.Color; + ViewBag.DefaultSize = config.Size; + ViewBag.DefaultMaterial = config.Material; + ViewBag.DefaultPattern = config.Pattern; + + 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 != GmcXmlExportProvider.Unspecified) + { + ViewBag.DefaultAgeGroup = T("Plugins.Feed.Froogle.AgeGroup" + culture.TextInfo.ToTitleCase(config.AgeGroup)); + } + } + } + + ViewBag.AvailableGenders = new List + { + new SelectListItem { Value = "male", Text = T("Plugins.Feed.Froogle.GenderMale") }, + new SelectListItem { Value = "female", Text = T("Plugins.Feed.Froogle.GenderFemale") }, + new SelectListItem { Value = "unisex", Text = T("Plugins.Feed.Froogle.GenderUnisex") } + }; + + ViewBag.AvailableAgeGroups = new List + { + new SelectListItem { Value = "adult", Text = T("Plugins.Feed.Froogle.AgeGroupAdult") }, + 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; + } + + public ActionResult GoogleCategories() + { + var categories = _googleFeedService.GetTaxonomyList(); + return Json(categories, JsonRequestBehavior.AllowGet); + } + + public ActionResult Configure() + { + var model = new FeedGoogleMerchantCenterModel(); + + model.GridPageSize = _adminAreaSettings.GridPageSize; + model.AvailableGoogleCategories = _googleFeedService.GetTaxonomyList(); + model.EnergyEfficiencyClasses = T("Plugins.Feed.Froogle.EnergyEfficiencyClasses").Text.SplitSafe(","); + + return View(model); + } + + [HttpPost] + public ActionResult GoogleProductEdit(int pk, string name, string value) + { + _googleFeedService.Upsert(pk, name, value); + + return this.Content(""); + } + + [HttpPost, GridAction(EnableCustomBinding = true)] + public ActionResult GoogleProductList(GridCommand command, string searchProductName, string touched) + { + return new JsonResult + { + Data = _googleFeedService.GetGridModel(command, searchProductName, touched) + }; + } + } +} 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/DependencyRegistrar.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/DependencyRegistrar.cs index b7a1b25c54..ac6e3dd393 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/DependencyRegistrar.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/DependencyRegistrar.cs @@ -1,6 +1,5 @@ using Autofac; using Autofac.Core; -using Autofac.Integration.Mvc; using SmartStore.Core.Data; using SmartStore.Core.Infrastructure; using SmartStore.Core.Infrastructure.DependencyManagement; diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt b/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt index 5d20784e8c..6f343b6b69 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Description.txt @@ -1,10 +1,10 @@ -FriendlyName: Google Merchant Center (GMC) +FriendlyName: Google Merchant Center (GMC) feed SystemName: SmartStore.GoogleMerchantCenter Group: Marketing -Version: 2.2.0.2 -MinAppVersion: 2.2.0 +Version: 2.6.0.1 +MinAppVersion: 2.5.0 Author: SmartStore AG DisplayOrder: 1 FileName: SmartStore.GoogleMerchantCenter.dll ResourceRootKey: Plugins.Feed.Froogle -Url: http://community.smartstore.com/index.php?/files/file/37-google-merchant-center-gmc-feed/ \ No newline at end of file +Url: http://community.smartstore.com/marketplace/file/37-google-merchant-center-gmc-feed/ 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 6ab4f71335..5a8dd0168d 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Extensions/MiscExtensions.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Extensions/MiscExtensions.cs @@ -9,26 +9,28 @@ public static string XEditableLink(this HtmlHelper hlp, string fieldName, string { string displayText = null; - if (fieldName == "Gender" || fieldName == "AgeGroup" || fieldName == "Exporting") - displayText = "<#= {0}Localize #>".FormatWith(fieldName); + if (fieldName == "Gender" || fieldName == "AgeGroup" || fieldName == "Export2" || fieldName == "IsBundle" || fieldName == "IsAdult") + displayText = "<#= {0}Localize #>".FormatInvariant(fieldName); else - displayText = "<#= {0} #>".FormatWith(fieldName); + displayText = "<#= {0} #>".FormatInvariant(fieldName); string skeleton = "\" class=\"edit-link-{1}\"" + " data-pk=\"<#= ProductId #>\" data-name=\"{0}\" data-value=\"<#= {0} #>\" data-inputclass=\"edit-{1}\" data-type=\"{2}\">" + "{3}"; - return skeleton.FormatWith(fieldName, fieldName.ToLower(), type, displayText); + 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/FroogleFeedPlugin.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/FroogleFeedPlugin.cs deleted file mode 100644 index 478cd9cd2d..0000000000 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/FroogleFeedPlugin.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Data.Entity.Migrations; -using System.Web.Routing; -using SmartStore.Core.Plugins; -using SmartStore.GoogleMerchantCenter.Data.Migrations; -using SmartStore.GoogleMerchantCenter.Services; -using SmartStore.Services.Configuration; -using SmartStore.Services.Localization; - -namespace SmartStore.GoogleMerchantCenter -{ - public class FroogleFeedPlugin : BasePlugin, IConfigurable - { - private readonly IGoogleFeedService _googleService; - private readonly ISettingService _settingService; - private readonly ILocalizationService _localizationService; - - public FroogleFeedPlugin( - IGoogleFeedService googleService, - ISettingService settingService, - ILocalizationService localizationService) - { - _googleService = googleService; - _settingService = settingService; - _localizationService = localizationService; - } - - /// - /// Gets a route for provider configuration - /// - /// Action name - /// Controller name - /// Route values - public void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) - { - actionName = "Configure"; - controllerName = "FeedFroogle"; - routeValues = new RouteValueDictionary() { { "Namespaces", "SmartStore.GoogleMerchantCenter.Controllers" }, { "area", "SmartStore.GoogleMerchantCenter" } }; - } - - /// - /// Install plugin - /// - public override void Install() - { - var settings = new FroogleSettings(); - settings.CurrencyId = _googleService.Helper.CurrencyID; - - _settingService.SaveSetting(settings); - - _localizationService.ImportPluginResourcesFromXml(this.PluginDescriptor); - - _googleService.Helper.InsertScheduleTask(); - - base.Install(); - } - - /// - /// Uninstall plugin - /// - public override void Uninstall() - { - _googleService.Helper.DeleteFeedFiles(); - _googleService.Helper.DeleteScheduleTask(); - - _settingService.DeleteSetting(); - - _localizationService.DeleteLocaleStringResources(PluginDescriptor.ResourceRootKey); - - var migrator = new DbMigrator(new Configuration()); - migrator.Update(DbMigrator.InitialDatabase); - - base.Uninstall(); - } - } -} diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/FroogleSettings.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/FroogleSettings.cs deleted file mode 100644 index f65f63ecef..0000000000 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/FroogleSettings.cs +++ /dev/null @@ -1,39 +0,0 @@ -using SmartStore.Core.Configuration; -using SmartStore.Utilities; -using SmartStore.Web.Framework.Plugins; - -namespace SmartStore.GoogleMerchantCenter -{ - public class FroogleSettings : PromotionFeedSettings, ISettings - { - public FroogleSettings() - { - ProductPictureSize = 125; - StaticFileName = "google_merchant_center_{0}.xml".FormatWith(CommonHelper.GenerateRandomDigitCode(10)); - Condition = "new"; - OnlineOnly = true; - AdditionalImages = true; - SpecialPrice = true; - } - - public string DefaultGoogleCategory { get; set; } - public string Condition { get; set; } - public bool SpecialPrice { get; set; } - public string Gender { get; set; } - public string AgeGroup { get; set; } - public string Color { get; set; } - public string Size { get; set; } - public string Material { get; set; } - public string Pattern { get; set; } - public bool OnlineOnly { get; set; } - public int ExpirationDays { get; set; } - public bool ExportShipping { get; set; } - public bool ExportBasePrice { get; set; } - - public string AppendDescriptionText1 { get; set; } - public string AppendDescriptionText2 { get; set; } - public string AppendDescriptionText3 { get; set; } - public string AppendDescriptionText4 { get; set; } - public string AppendDescriptionText5 { get; set; } - } -} \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/GoogleMerchantCenterFeedPlugin.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/GoogleMerchantCenterFeedPlugin.cs new file mode 100644 index 0000000000..3808bca487 --- /dev/null +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/GoogleMerchantCenterFeedPlugin.cs @@ -0,0 +1,64 @@ +using System.Data.Entity.Migrations; +using System.Web.Routing; +using SmartStore.Core.Plugins; +using SmartStore.GoogleMerchantCenter.Data.Migrations; +using SmartStore.GoogleMerchantCenter.Services; +using SmartStore.Services; + +namespace SmartStore.GoogleMerchantCenter +{ + public class GoogleMerchantCenterFeedPlugin : BasePlugin, IConfigurable + { + private readonly IGoogleFeedService _googleFeedService; + private readonly ICommonServices _services; + + public GoogleMerchantCenterFeedPlugin( + IGoogleFeedService googleFeedService, + ICommonServices services) + { + _googleFeedService = googleFeedService; + _services = services; + } + + public static string SystemName + { + get { return "SmartStore.GoogleMerchantCenter"; } + } + + /// + /// Gets a route for provider configuration + /// + /// Action name + /// Controller name + /// Route values + public void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) + { + actionName = "Configure"; + controllerName = "FeedGoogleMerchantCenter"; + routeValues = new RouteValueDictionary() { { "Namespaces", "SmartStore.GoogleMerchantCenter.Controllers" }, { "area", SystemName } }; + } + + /// + /// Install plugin + /// + public override void Install() + { + _services.Localization.ImportPluginResourcesFromXml(this.PluginDescriptor); + + base.Install(); + } + + /// + /// Uninstall plugin + /// + public override void Uninstall() + { + _services.Localization.DeleteLocaleStringResources(PluginDescriptor.ResourceRootKey); + + var migrator = new DbMigrator(new Configuration()); + migrator.Update(DbMigrator.InitialDatabase); + + base.Uninstall(); + } + } +} diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.de-de.xml b/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.de-de.xml index b72e213a61..17b90e856c 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.de-de.xml @@ -1,128 +1,50 @@  - Google Merchant Center (GMC) Feed + Google Merchant Center Feed - + + Ermöglicht den Export von Produktdaten im Feed-Format von Google Merchant Center (GMC). + + + Google Merchant Center XML Feed + + + Ermöglicht den Export von Produktdaten im XML Feed-Format von Google Merchant Center (GMC). + + +
  • Für die Produktidentifizierung muss entweder die GTIN (z.B. als UPC, EAN etc.) oder der Hersteller samt Hersteller-Artikelnummer (MPN) hinterlegt sein. Google empfiehlt die Angabe aller drei Informationen.
  • Standard Steuer- und Versanddaten sind in den Einstellungen Ihres Google-Merchant-Center-Kontos zu hinterlegen.
  • Mehr Informationen zu benötigten Feldern finden Sie im Artikel Produkt-Feed-Spezifikationen.
  • +
  • Eine Liste mit allen gültigen Google-Produkt-Kategorie finden Sie hier.
  • ]]>
    - - - hier.]]> - - Standard-Google-Kategorie fehlt - - Währung - - - Standardwährung fürs Erstellen des Feeds. - Standard-Google-Kategorie Wird verwendet, falls auf Produktebene keine Kategorie angegeben ist. - - Allgemein - - - Produktspezifische Daten - - - Feed erstellen - - - Thumbnail-Größe Produktbild - - - Die Standardgröße (in Pixel) für das Produkt-Thumbnail. - Produkt Google-Kategorie - - Geschlecht - - - Altersgruppe - - - Farbe - - - Größe - - - Material - - - Muster - - - Feed automatisch erstellen - - - Aktivieren Sie diese Option, falls die Feed-Datei automatisch erstellt werden soll. - - - Zeitspanne (in Minuten) - - - Die Zeitspanne nach der die Feed-Datei automatisch erstellt werden soll. - - - Einstellungen wurden erfolgreich gespeichert. Starten Sie die Anwendung bitte neu, falls "Feed automatisch erstellen" geändert wurde. - - - Artikelbeschreibung - - - Legen Sie fest, welche Informationen zur Beschreibung des Artikel wie verwendet werden sollen. - - - Automatisch - - - Kurzbeschreibung - - - Detailbeschreibung - - - Produktname + Kurzbeschreibung - - - Produktname + Detailbeschreibung - - - Hersteller + Produktname + Kurzbeschreibung - - - Hersteller + Produktname + Detailbeschreibung - - - Anzuhängender Text 1 - 5 - - - Einer der fünf Textbausteine wird an die Artikelbeschreibung angehängt, falls Kurz- und Langbeschreibung leer sind. - + + Legt die dem Artikel entsprechende Google-Kategorie fest. Zwingend erforderlich. + Zusätzliche Bilder - Aktivieren Sie diese Option, falls Sie mehr als ein Produktbild exportieren möchten. Bis zu 10 sind möglich. + Legt fest, ob mehr als ein Produktbild exportiert werden soll. Bis zu 10 sind möglich. Zustand @@ -148,9 +70,6 @@ Vorrätig - - Bestellbar - Vergriffen @@ -161,19 +80,7 @@ Aktionspreis exportieren - Aktivieren Sie diese Option, falls Sie den Aktionspreis inkl. Aktionszeitraum exportieren möchten. - - - Hersteller\Marke - - - Dieser Wert wird verwendet, wenn für ein Produkt kein Hersteller bzw. Marke hinterlegt ist. - - - Eigene Artikelnummer - - - Aktivieren Sie diese Option, falls die eigene Artikelnummer als Hersteller-Artikelnummer (MPN) exportiert werden soll, sofern diese fehlt. + Legt fest, ob ein Aktionspreis inkl. Aktionszeitraum exportiert werden soll, sofern ein solcher für das jeweilige Produkt existiert. Geschlecht @@ -226,18 +133,6 @@ Muster oder grafisches Druckdesign eines Produktes. Angabe ist z.B. bei Bekleidung sinnvoll. - - Nur online zu kaufen - - - Aktivieren Sie diese Option, wenn Produkte nur online erworben werden können. Aktivieren Sie diese Option nicht, falls Produkte auch in Ihrem Ladengeschäft erhältlich sind. - - - HTML entfernen - - - Aktivieren Sie diese Option, falls für den Export alle HTML-Auszeichnungen aus der Artikelbeschreibung entfernt werden sollen. - Leer @@ -259,12 +154,6 @@ Noch nicht bearbeitet - - Shop - - - Wählen Sie den Shop, für den der Feed erstellt werden soll. - Verfällt in Tagen @@ -278,24 +167,69 @@ Versanddaten exportieren - Aktivieren Sie diese Option, wenn Sie z.B. das Gewicht des Produkt exportieren möchten. + Legt fest, ob das Gewicht eines Produktes exportiert werden soll. Grundpreis exportieren - Aktivieren Sie diese Option, wenn Sie denGrundpreis des Produkt exportieren möchten. - - - Netto- in Bruttopreise umrechnen - - - Legt fest, dass Netto- in Bruttopreise umgerechnet werden sollen. - - - Sprache - - - Legt die Sprache fest, in der sprachabhängige Werte (z.B. der Produktname) exportiert werden sollen. + 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 fa68704f19..3b75a8c306 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Localization/resources.en-us.xml @@ -1,128 +1,50 @@  - Google Merchant Center (GMC) Feed + Google Merchant Center feed + + Allows you to export product data in the feed format of Google Merchant Center (GMC). + + + Google Merchant Center XML feed + + + Allows to export product data in XML feed format of Google Merchant Center (GMC). + +
  • Either the GTIN (such as UPC, EAN, etc.) or manufacturer and manufacturer part number (MPN) are required for product identification. Google recommends that all three pieces of information be specified.
  • Specify default tax and shipping values in your Google Merchant Center account settings.
  • In order to get more info about required fields look at the following article Product feed specification.
  • +
  • You can find a list of all Google categories here.
  • ]]>
    - - - here.]]> - - Default Google category is not set - - Currency - - - The default currency that will be used to generate the feed. - Default Google category Will be used if there is no category specified on produkt level. - - General - - - Product specific data - - - Generate feed - - - Product thumbnail image size - - - The default size (pixels) for product thumbnail image. - Product Google Category - - Gender - - - Age group - - - Color - - - Size - - - Material - - - Pattern - - - Automatically generate feed - - - Check if you want the feed to be automatically generated. - - - A task period (minutes) - - - Specify a task period in minutes (generation of a new feed file). - - - Settings successfully saved. Please restart the application if "Automatically generate feed" has been changed. - - - Product description - - - Specify what information to use for the description of the product. - - - Automatic - - - Short description - - - Long description - - - Product name + short description - - - Product name + long description - - - Manufacturer + Product name + short description - - - Manufacturer + Product name + long description - - - Text 1 - 5 to be appended - - - One of the five text elements will be appended to the product description if long and short description are empty. - + + Specifies the Google category corresponding to the product. Mandatory. + Additional images - Check if you want to export more than one image. Up to 10 are possible. + Check the box if you want to export more than one image. Up to 10 are possible. Condition @@ -148,9 +70,6 @@ In stock - - Available for order - Out of stock @@ -163,18 +82,6 @@ Activate this option if you want to export special price and special price period. - - Manufacturer\Brand - - - This value is used if there's no manufacturer\brand for a product. - - - Own SKU - - - Check to export SKU as manufacturer part number (MPN) if it is missing. - Gender @@ -226,18 +133,6 @@ The pattern or graphic print featured on a product. Usefull for clothes for instance. - - Only online available - - - Check if products can only be purchased online. - - - Remove HTML - - - Check if you want to remove all HTML from the product description for the export. - Empty @@ -259,12 +154,6 @@ Not edited yet - - Store - - - Select the store that will be used to generate the feed. - Expires in days @@ -286,16 +175,61 @@ Activate this option if you want to export the base price of the product. - - Convert net into gross prices - - - Determines to convert net into gross prices. - - - Language - - - Determines which language to use to export language dependent values (for instance the product name). - + + 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/FeedFroogleModel.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Models/FeedFroogleModel.cs deleted file mode 100644 index 60ba0d7094..0000000000 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Models/FeedFroogleModel.cs +++ /dev/null @@ -1,268 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; -using System.Web.Mvc; -using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; -using Newtonsoft.Json; -using SmartStore.Web.Framework.Plugins; -using SmartStore.Core.Domain.Catalog; -using FluentValidation.Attributes; -using SmartStore.GoogleMerchantCenter.Validators; - -namespace SmartStore.GoogleMerchantCenter.Models -{ - [Validator(typeof(ConfigurationValidator))] - public class FeedFroogleModel : PromotionFeedConfigModel - { - public string GridEditUrl { get; set; } - public int GridPageSize { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.ProductPictureSize")] - public int ProductPictureSize { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Currency")] - public int CurrencyId { get; set; } - public List AvailableCurrencies { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.DefaultGoogleCategory")] - public string DefaultGoogleCategory { get; set; } - - public string[] AvailableGoogleCategories { get; set; } - public string AvailableGoogleCategoriesAsJson - { - get - { - if (AvailableGoogleCategories != null && AvailableGoogleCategories.Length > 0) - return JsonConvert.SerializeObject(AvailableGoogleCategories); - return ""; - } - } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.TaskEnabled")] - public bool TaskEnabled { get; set; } - [SmartResourceDisplayName("Plugins.Feed.Froogle.GenerateStaticFileEachMinutes")] - public int GenerateStaticFileEachMinutes { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.BuildDescription")] - public string BuildDescription { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.AppendDescriptionText")] - public string AppendDescriptionText1 { get; set; } - public string AppendDescriptionText2 { get; set; } - public string AppendDescriptionText3 { get; set; } - public string AppendDescriptionText4 { get; set; } - public string AppendDescriptionText5 { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.AdditionalImages")] - public bool AdditionalImages { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Condition")] - public string Condition { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Availability")] - public string Availability { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.SpecialPrice")] - public bool SpecialPrice { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Brand")] - public string Brand { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.UseOwnProductNo")] - public bool UseOwnProductNo { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Gender")] - public string Gender { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.AgeGroup")] - public string AgeGroup { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Color")] - public string Color { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Size")] - public string Size { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Material")] - public string Material { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Pattern")] - public string Pattern { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.OnlineOnly")] - public bool OnlineOnly { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.DescriptionToPlainText")] - public bool DescriptionToPlainText { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.SearchProductName")] - public string SearchProductName { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.SearchIsTouched")] - public string SearchIsTouched { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Store")] - public int StoreId { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.ExpirationDays")] - public int ExpirationDays { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.ExportShipping")] - public bool ExportShipping { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.ExportBasePrice")] - public bool ExportBasePrice { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.ConvertNetToGrossPrices")] - public bool ConvertNetToGrossPrices { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.LanguageId")] - public int LanguageId { get; set; } - - public void Copy(FroogleSettings settings, bool fromSettings) - { - if (fromSettings) - { - AppendDescriptionText1 = settings.AppendDescriptionText1; - AppendDescriptionText2 = settings.AppendDescriptionText2; - AppendDescriptionText3 = settings.AppendDescriptionText3; - AppendDescriptionText4 = settings.AppendDescriptionText4; - AppendDescriptionText5 = settings.AppendDescriptionText5; - ProductPictureSize = settings.ProductPictureSize; - CurrencyId = settings.CurrencyId; - DefaultGoogleCategory = settings.DefaultGoogleCategory; - BuildDescription = settings.BuildDescription; - AdditionalImages = settings.AdditionalImages; - Condition = settings.Condition; - Availability = settings.Availability; - SpecialPrice = settings.SpecialPrice; - Brand = settings.Brand; - UseOwnProductNo = settings.UseOwnProductNo; - Gender = settings.Gender; - AgeGroup = settings.AgeGroup; - Color = settings.Color; - Size = settings.Size; - Material = settings.Material; - Pattern = settings.Pattern; - OnlineOnly = settings.OnlineOnly; - DescriptionToPlainText = settings.DescriptionToPlainText; - StoreId = settings.StoreId; - ExpirationDays = settings.ExpirationDays; - ExportShipping = settings.ExportShipping; - ExportBasePrice = settings.ExportBasePrice; - ConvertNetToGrossPrices = settings.ConvertNetToGrossPrices; - LanguageId = settings.LanguageId; - } - else - { - settings.AppendDescriptionText1 = AppendDescriptionText1; - settings.AppendDescriptionText2 = AppendDescriptionText2; - settings.AppendDescriptionText3 = AppendDescriptionText3; - settings.AppendDescriptionText4 = AppendDescriptionText4; - settings.AppendDescriptionText5 = AppendDescriptionText5; - settings.ProductPictureSize = ProductPictureSize; - settings.CurrencyId = CurrencyId; - settings.DefaultGoogleCategory = DefaultGoogleCategory; - settings.BuildDescription = BuildDescription; - settings.AdditionalImages = AdditionalImages; - settings.Condition = Condition; - settings.Availability = Availability; - settings.SpecialPrice = SpecialPrice; - settings.Brand = Brand; - settings.UseOwnProductNo = UseOwnProductNo; - settings.Gender = Gender; - settings.AgeGroup = AgeGroup; - settings.Color = Color; - settings.Size = Size; - settings.Material = Material; - settings.Pattern = Pattern; - settings.OnlineOnly = OnlineOnly; - settings.DescriptionToPlainText = DescriptionToPlainText; - settings.StoreId = StoreId; - settings.ExpirationDays = ExpirationDays; - settings.ExportShipping = ExportShipping; - settings.ExportBasePrice = ExportBasePrice; - settings.ConvertNetToGrossPrices = ConvertNetToGrossPrices; - settings.LanguageId = LanguageId; - } - } - } - - - public class GoogleProductModel : ModelBase - { - public int TotalCount { get; set; } - - //this attribute is required to disable editing - [ScaffoldColumn(false)] - public int ProductId - { - get { return Id; } - set { Id = value; } - } - public int Id { get; set; } - - //this attribute is required to disable editing - [ReadOnly(true)] - [ScaffoldColumn(false)] - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.ProductName")] - public string Name { get; set; } - - public string SKU { get; set; } - public int ProductTypeId { get; set; } - public ProductType ProductType { get { return (ProductType)ProductTypeId; } } - public string ProductTypeName { get; set; } - public string ProductTypeLabelHint - { - get - { - switch (ProductType) - { - case ProductType.SimpleProduct: - return "smnet-hide"; - case ProductType.GroupedProduct: - return "success"; - case ProductType.BundledProduct: - return "info"; - default: - return ""; - } - } - } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.GoogleCategory")] - public string Taxonomy { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.Gender")] - public string Gender { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.AgeGroup")] - public string AgeGroup { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.Color")] - public string Color { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.Size")] - public string Size { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.Material")] - public string Material { get; set; } - - [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.Pattern")] - public string Pattern { get; set; } - - [SmartResourceDisplayName("Common.Export")] - public int Export { get; set; } - [SmartResourceDisplayName("Common.Export")] - public bool Exporting - { - get { return Export != 0; } - set { Export = (value ? 1 : 0); } - } - - public string GenderLocalize { get; set; } - public string AgeGroupLocalize { get; set; } - public string ExportingLocalize { get; set; } - } -} \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Models/FeedGoogleMerchantCenterModel.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Models/FeedGoogleMerchantCenterModel.cs new file mode 100644 index 0000000000..36c5f02799 --- /dev/null +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Models/FeedGoogleMerchantCenterModel.cs @@ -0,0 +1,141 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Text; +using Newtonsoft.Json; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Web.Framework; +using SmartStore.Web.Framework.Modelling; + +namespace SmartStore.GoogleMerchantCenter.Models +{ + public class FeedGoogleMerchantCenterModel + { + public int GridPageSize { get; set; } + + public string[] AvailableGoogleCategories { get; set; } + public string AvailableGoogleCategoriesAsJson + { + get + { + if (AvailableGoogleCategories != null && AvailableGoogleCategories.Length > 0) + return JsonConvert.SerializeObject(AvailableGoogleCategories); + return "[ ]"; + } + } + + public string[] EnergyEfficiencyClasses { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.SearchProductName")] + public string SearchProductName { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.SearchIsTouched")] + public string SearchIsTouched { get; set; } + } + + public class GoogleProductModel : ModelBase + { + public int TotalCount { get; set; } + + //this attribute is required to disable editing + [ScaffoldColumn(false)] + public int ProductId + { + get { return Id; } + set { Id = value; } + } + public int Id { get; set; } + + //this attribute is required to disable editing + [ReadOnly(true)] + [ScaffoldColumn(false)] + [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.ProductName")] + public string Name { get; set; } + + public string SKU { get; set; } + public int ProductTypeId { get; set; } + public ProductType ProductType { get { return (ProductType)ProductTypeId; } } + public string ProductTypeName { get; set; } + public string ProductTypeLabelHint + { + get + { + switch (ProductType) + { + case ProductType.SimpleProduct: + return "smnet-hide"; + case ProductType.GroupedProduct: + return "success"; + case ProductType.BundledProduct: + return "info"; + default: + return ""; + } + } + } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.Products.GoogleCategory")] + public string Taxonomy { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.Gender")] + public string Gender { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.AgeGroup")] + public string AgeGroup { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.Color")] + public string Color { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.Size")] + public string Size { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.Material")] + public string Material { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.Pattern")] + public string Pattern { get; set; } + + [SmartResourceDisplayName("Common.Export")] + public int Export { get; set; } + [SmartResourceDisplayName("Common.Export")] + 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 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 new file mode 100644 index 0000000000..69d81d50dd --- /dev/null +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Models/ProfileConfigurationModel.cs @@ -0,0 +1,77 @@ +using System; +using System.Xml.Serialization; +using FluentValidation.Attributes; +using Newtonsoft.Json; +using SmartStore.GoogleMerchantCenter.Validators; +using SmartStore.Web.Framework; + +namespace SmartStore.GoogleMerchantCenter.Models +{ + [Serializable] + [Validator(typeof(ProfileConfigurationValidator))] + public class ProfileConfigurationModel + { + public ProfileConfigurationModel() + { + Condition = "new"; + AdditionalImages = true; + SpecialPrice = true; + } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.DefaultGoogleCategory")] + public string DefaultGoogleCategory { get; set; } + + [XmlIgnore] + public string[] AvailableGoogleCategories { get; set; } + + [XmlIgnore] + public string AvailableGoogleCategoriesAsJson + { + get + { + if (AvailableGoogleCategories != null && AvailableGoogleCategories.Length > 0) + return JsonConvert.SerializeObject(AvailableGoogleCategories); + return ""; + } + } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.AdditionalImages")] + public bool AdditionalImages { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.Condition")] + public string Condition { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.Availability")] + public string Availability { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.SpecialPrice")] + public bool SpecialPrice { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.Gender")] + public string Gender { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.AgeGroup")] + public string AgeGroup { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.Color")] + public string Color { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.Size")] + public string Size { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.Material")] + public string Material { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.Pattern")] + public string Pattern { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.ExpirationDays")] + public int ExpirationDays { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.ExportShipping")] + public bool ExportShipping { get; set; } + + [SmartResourceDisplayName("Plugins.Feed.Froogle.ExportBasePrice")] + public bool ExportBasePrice { get; set; } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs new file mode 100644 index 0000000000..3edd31e361 --- /dev/null +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Providers/GmcXmlExportProvider.cs @@ -0,0 +1,439 @@ +using System; +using System.Linq; +using System.Xml; +using SmartStore.Core.Domain.Catalog; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Domain.Directory; +using SmartStore.Core.Localization; +using SmartStore.Core.Logging; +using SmartStore.Core.Plugins; +using SmartStore.GoogleMerchantCenter.Models; +using SmartStore.GoogleMerchantCenter.Services; +using SmartStore.Services.DataExchange.Export; +using SmartStore.Services.Directory; + +namespace SmartStore.GoogleMerchantCenter.Providers +{ + [SystemName("Feeds.GoogleMerchantCenterProductXml")] + [FriendlyName("Google Merchant Center XML product feed")] + [DisplayOrder(1)] + [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"; + + private readonly IGoogleFeedService _googleFeedService; + private readonly IMeasureService _measureService; + private readonly MeasureSettings _measureSettings; + + public GmcXmlExportProvider( + IGoogleFeedService googleFeedService, + IMeasureService measureService, + MeasureSettings measureSettings) + { + _googleFeedService = googleFeedService; + _measureService = measureService; + _measureSettings = measureSettings; + + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + private string BasePriceUnits(string value) + { + const string defaultValue = "kg"; + + if (value.IsEmpty()) + return defaultValue; + + // TODO: Product.BasePriceMeasureUnit should be localized + switch (value.ToLowerInvariant()) + { + case "mg": + case "milligramm": + case "milligram": + return "mg"; + case "g": + case "gramm": + case "gram": + return "g"; + case "kg": + case "kilogramm": + case "kilogram": + return "kg"; + + case "ml": + case "milliliter": + case "millilitre": + return "ml"; + case "cl": + case "zentiliter": + case "centilitre": + return "cl"; + case "l": + case "liter": + case "litre": + return "l"; + case "cbm": + case "kubikmeter": + case "cubic metre": + return "cbm"; + + case "cm": + case "zentimeter": + case "centimetre": + return "cm"; + case "m": + case "meter": + return "m"; + + case "qm²": + case "quadratmeter": + case "square metre": + return "sqm"; + + default: + return defaultValue; + } + } + + private bool BasePriceSupported(int baseAmount, string unit) + { + if (baseAmount == 1 || baseAmount == 10 || baseAmount == 100) + return true; + + if (baseAmount == 75 && unit == "cl") + return true; + + if ((baseAmount == 50 || baseAmount == 1000) && unit == "kg") + return true; + + return false; + } + + public static string SystemName + { + get { return "Feeds.GoogleMerchantCenterProductXml"; } + } + + public static string Unspecified + { + get { return "__nospec__"; } + } + + public override ExportConfigurationInfo ConfigurationInfo + { + get + { + return new ExportConfigurationInfo + { + PartialViewName = "~/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProfileConfiguration.cshtml", + ModelType = typeof(ProfileConfigurationModel), + Initialize = obj => + { + var model = (obj as ProfileConfigurationModel); + + model.AvailableGoogleCategories = _googleFeedService.GetTaxonomyList(); + } + }; + } + } + + public override string FileExtension + { + get { return "XML"; } + } + + protected override void Export(ExportExecuteContext context) + { + dynamic currency = context.Currency; + string measureWeightSystemKey = ""; + var dateFormat = "yyyy-MM-ddTHH:mmZ"; + + var measureWeight = _measureService.GetMeasureWeightById(_measureSettings.BaseWeightId); + + if (measureWeight != null) + measureWeightSystemKey = measureWeight.SystemKeyword; + + var config = (context.ConfigurationData as ProfileConfigurationModel) ?? new ProfileConfigurationModel(); + + using (var writer = XmlWriter.Create(context.DataStream, ExportXmlHelper.DefaultSettings)) + { + writer.WriteStartDocument(); + writer.WriteStartElement("rss"); + writer.WriteAttributeString("version", "2.0"); + writer.WriteAttributeString("xmlns", "g", null, _googleNamespace); + writer.WriteStartElement("channel"); + writer.WriteElementString("title", "{0} - Feed for Google Merchant Center".FormatInvariant((string)context.Store.Name)); + writer.WriteElementString("link", "http://base.google.com/base/"); + writer.WriteElementString("description", "Information about products"); + + while (context.Abort == DataExchangeAbortion.None && context.DataSegmenter.ReadNextSegment()) + { + 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 != DataExchangeAbortion.None) + break; + + Product entity = product.Entity; + var gmc = googleProducts.FirstOrDefault(x => x.ProductId == entity.Id); + + if (gmc != null && !gmc.Export) + continue; + + writer.WriteStartElement("item"); + + try + { + string category = (gmc == null ? null : gmc.Taxonomy); + string productType = product._CategoryPath; + string mainImageUrl = product._MainPictureUrl; + 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; + + if (category.IsEmpty()) + context.Log.Error(T("Plugins.Feed.Froogle.MissingDefaultCategory")); + + if (config.Condition.IsCaseInsensitiveEqual(Unspecified)) + { + condition = ""; + } + else if (config.Condition.HasValue()) + { + condition = config.Condition; + } + + if (config.Availability.IsCaseInsensitiveEqual(Unspecified)) + { + availability = ""; + } + else if (config.Availability.HasValue()) + { + availability = config.Availability; + } + else + { + if (entity.ManageInventoryMethod == ManageInventoryMethod.ManageStock && entity.StockQuantity <= 0) + { + if (entity.BackorderMode == BackorderMode.NoBackorders) + availability = "out of stock"; + else if (entity.BackorderMode == BackorderMode.AllowQtyBelow0 || entity.BackorderMode == BackorderMode.AllowQtyBelow0AndNotifyCustomer) + availability = (entity.AvailableForPreOrder ? "preorder" : "out of stock"); + } + } + + writer.WriteElementString("g", "id", _googleNamespace, uniqueId); + + writer.WriteStartElement("title"); + writer.WriteCData(((string)product.Name).Truncate(70).RemoveInvalidXmlChars()); + writer.WriteEndElement(); + + writer.WriteStartElement("description"); + writer.WriteCData(((string)product.FullDescription).RemoveInvalidXmlChars()); + writer.WriteEndElement(); + + writer.WriteStartElement("g", "google_product_category", _googleNamespace); + writer.WriteCData(category.RemoveInvalidXmlChars()); + writer.WriteFullEndElement(); + + if (productType.HasValue()) + { + writer.WriteStartElement("g", "product_type", _googleNamespace); + writer.WriteCData(productType.RemoveInvalidXmlChars()); + writer.WriteFullEndElement(); + } + + writer.WriteElementString("link", (string)product._DetailUrl); + + if (mainImageUrl.HasValue()) + { + writer.WriteElementString("g", "image_link", _googleNamespace, mainImageUrl); + } + + if (config.AdditionalImages) + { + var imageCount = 0; + foreach (dynamic productPicture in product.ProductPictures) + { + string pictureUrl = productPicture.Picture._ImageUrl; + if (pictureUrl.HasValue() && (mainImageUrl.IsEmpty() || !mainImageUrl.IsCaseInsensitiveEqual(pictureUrl)) && ++imageCount <= 10) + { + writer.WriteElementString("g", "additional_image_link", _googleNamespace, pictureUrl); + } + } + } + + 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) + { + writer.WriteElementString("g", "sale_price", _googleNamespace, specialPrice.Value.FormatInvariant() + " " + (string)currency.CurrencyCode); + + if (entity.SpecialPriceStartDateTimeUtc.HasValue && entity.SpecialPriceEndDateTimeUtc.HasValue) + { + var specialPriceDate = "{0}/{1}".FormatInvariant( + entity.SpecialPriceStartDateTimeUtc.Value.ToString(dateFormat), entity.SpecialPriceEndDateTimeUtc.Value.ToString(dateFormat)); + + writer.WriteElementString("g", "sale_price_effective_date", _googleNamespace, specialPriceDate); + } + + price = (product._RegularPrice as decimal?) ?? price; + } + + writer.WriteElementString("g", "price", _googleNamespace, price.FormatInvariant() + " " + (string)currency.CurrencyCode); + + writer.WriteCData("gtin", gtin, "g", _googleNamespace); + writer.WriteCData("brand", brand, "g", _googleNamespace); + writer.WriteCData("mpn", mpn, "g", _googleNamespace); + + if (config.Gender.IsCaseInsensitiveEqual(Unspecified)) + writer.WriteCData("gender", "", "g", _googleNamespace); + else + writer.WriteCData("gender", gmc != null && gmc.Gender.HasValue() ? gmc.Gender : config.Gender, "g", _googleNamespace); + + if (config.AgeGroup.IsCaseInsensitiveEqual(Unspecified)) + writer.WriteCData("age_group", "", "g", _googleNamespace); + else + writer.WriteCData("age_group", gmc != null && gmc.AgeGroup.HasValue() ? gmc.AgeGroup : config.AgeGroup, "g", _googleNamespace); + + writer.WriteCData("color", gmc != null && gmc.Color.HasValue() ? gmc.Color : config.Color, "g", _googleNamespace); + writer.WriteCData("size", gmc != null && gmc.Size.HasValue() ? gmc.Size : config.Size, "g", _googleNamespace); + writer.WriteCData("material", gmc != null && gmc.Material.HasValue() ? gmc.Material : config.Material, "g", _googleNamespace); + 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", "identifier_exists", _googleNamespace, gtin.HasValue() || brand.HasValue() || mpn.HasValue() ? "TRUE" : "FALSE"); + + if (config.ExpirationDays > 0) + { + writer.WriteElementString("g", "expiration_date", _googleNamespace, DateTime.UtcNow.AddDays(config.ExpirationDays).ToString("yyyy-MM-dd")); + } + + if (config.ExportShipping) + { + string weightInfo; + var weight = ((decimal)product.Weight).FormatInvariant(); + + if (measureWeightSystemKey.IsCaseInsensitiveEqual("gram")) + weightInfo = weight + " g"; + else if (measureWeightSystemKey.IsCaseInsensitiveEqual("lb")) + weightInfo = weight + " lb"; + else if (measureWeightSystemKey.IsCaseInsensitiveEqual("ounce")) + weightInfo = weight + " oz"; + else + weightInfo = weight + " kg"; + + writer.WriteElementString("g", "shipping_weight", _googleNamespace, weightInfo); + } + + if (config.ExportBasePrice && entity.BasePriceHasValue) + { + var measureUnit = BasePriceUnits((string)product.BasePriceMeasureUnit); + + if (BasePriceSupported(entity.BasePriceBaseAmount ?? 0, 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 exception) + { + context.RecordException(exception, entity.Id); + } + + writer.WriteEndElement(); // item + } + } + + writer.WriteEndElement(); // channel + writer.WriteEndElement(); // rss + writer.WriteEndDocument(); + } + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/RouteProvider.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/RouteProvider.cs index 57ac853189..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 { @@ -10,11 +10,12 @@ public void RegisterRoutes(RouteCollection routes) { routes.MapRoute("SmartStore.GoogleMerchantCenter", "Plugins/SmartStore.GoogleMerchantCenter/{action}", - new { controller = "FeedFroogle", action = "Configure" }, + new { controller = "FeedGoogleMerchantCenter", action = "Configure" }, new[] { "SmartStore.GoogleMerchantCenter.Controllers" } ) - .DataTokens["area"] = "SmartStore.GoogleMerchantCenter"; + .DataTokens["area"] = GoogleMerchantCenterFeedPlugin.SystemName; } + public int Priority { get diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs index c5b6812ee9..b5fab3af04 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Services/GoogleFeedService.cs @@ -1,24 +1,16 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; -using System.Web; -using System.Web.Mvc; -using System.Xml; -using Autofac; using SmartStore.Core.Data; using SmartStore.Core.Domain.Catalog; -using SmartStore.Core.Domain.Common; -using SmartStore.Core.Domain.Directory; -using SmartStore.Core.Logging; +using SmartStore.Core.Localization; +using SmartStore.Core.Plugins; using SmartStore.GoogleMerchantCenter.Domain; using SmartStore.GoogleMerchantCenter.Models; -using SmartStore.Services.Catalog; -using SmartStore.Services.Directory; -using SmartStore.Services.Localization; -using SmartStore.Services.Tasks; -using SmartStore.Web.Framework.Plugins; +using SmartStore.Services; using Telerik.Web.Mvc; namespace SmartStore.GoogleMerchantCenter.Services @@ -27,43 +19,23 @@ public partial class GoogleFeedService : IGoogleFeedService { private const string _googleNamespace = "http://base.google.com/ns/1.0"; - private readonly FeedPluginHelper _helper; private readonly IRepository _gpRepository; - private readonly IProductService _productService; - private readonly IManufacturerService _manufacturerService; - private readonly IMeasureService _measureService; - private readonly MeasureSettings _measureSettings; - private readonly IDbContext _dbContext; - private readonly AdminAreaSettings _adminAreaSettings; + private readonly ICommonServices _services; + private readonly IPluginFinder _pluginFinder; public GoogleFeedService( IRepository gpRepository, - IProductService productService, - IManufacturerService manufacturerService, - FroogleSettings settings, - IMeasureService measureService, - MeasureSettings measureSettings, - IDbContext dbContext, - AdminAreaSettings adminAreaSettings, - IComponentContext ctx) + ICommonServices services, + IPluginFinder pluginFinder) { _gpRepository = gpRepository; - _productService = productService; - _manufacturerService = manufacturerService; - Settings = settings; - _measureService = measureService; - _measureSettings = measureSettings; - _dbContext = dbContext; - _adminAreaSettings = adminAreaSettings; - - _helper = new FeedPluginHelper(ctx, "SmartStore.GoogleMerchantCenter", "SmartStore.GoogleMerchantCenter", () => - { - return Settings as PromotionFeedSettings; - }); + _services = services; + _pluginFinder = pluginFinder; + + T = NullLocalizer.Instance; } - public FroogleSettings Settings { get; set; } - public FeedPluginHelper Helper { get { return _helper; } } + public Localizer T { get; set; } public GoogleProductRecord GetGoogleProductRecord(int productId) { @@ -79,6 +51,15 @@ from gp in _gpRepository.Table return record; } + public List GetGoogleProductRecords(int[] productIds) + { + if (productIds == null || productIds.Length == 0) + return new List(); + + var lst = _gpRepository.TableUntracked.Where(x => productIds.Contains(x.ProductId)).ToList(); + return lst; + } + public void InsertGoogleProductRecord(GoogleProductRecord record) { if (record == null) @@ -103,390 +84,7 @@ public void DeleteGoogleProductRecord(GoogleProductRecord record) _gpRepository.Delete(record); } - private bool SpecialPrice(Product product, out string specialPriceDate) - { - specialPriceDate = ""; - - try - { - if (Settings.SpecialPrice && product.SpecialPrice.HasValue && product.SpecialPriceStartDateTimeUtc.HasValue && product.SpecialPriceEndDateTimeUtc.HasValue) - { - if (!(product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing)) - { - string dateFormat = "yyyy-MM-ddTHH:mmZ"; - string startDate = product.SpecialPriceStartDateTimeUtc.Value.ToString(dateFormat); - string endDate = product.SpecialPriceEndDateTimeUtc.Value.ToString(dateFormat); - - specialPriceDate = "{0}/{1}".FormatWith(startDate, endDate); - } - } - } - catch (Exception exc) - { - exc.Dump(); - } - return specialPriceDate.HasValue(); - } - private string ProductCategory(GoogleProductRecord googleProduct) - { - string productCategory = ""; - - if (googleProduct != null) - productCategory = googleProduct.Taxonomy; - - if (productCategory.IsEmpty()) - productCategory = Settings.DefaultGoogleCategory; - - return productCategory; - } - private string Condition() - { - if (Settings.Condition.IsCaseInsensitiveEqual(PluginHelper.NotSpecified)) - return ""; - - if (Settings.Condition.IsEmpty()) - return "new"; - - return Settings.Condition; - } - private string Availability(Product product) - { - if (Settings.Availability.IsCaseInsensitiveEqual(PluginHelper.NotSpecified)) - return ""; - - if (Settings.Availability.IsEmpty()) - { - if (product.ManageInventoryMethod == ManageInventoryMethod.ManageStock && product.StockQuantity <= 0) - { - switch (product.BackorderMode) - { - case BackorderMode.NoBackorders: - return "out of stock"; - case BackorderMode.AllowQtyBelow0: - case BackorderMode.AllowQtyBelow0AndNotifyCustomer: - return "available for order"; - } - } - return "in stock"; - } - return Settings.Availability; - } - private string Gender(GoogleProductRecord googleProduct) - { - if (Settings.Gender.IsCaseInsensitiveEqual(PluginHelper.NotSpecified)) - return ""; - - if (googleProduct != null && googleProduct.Gender.HasValue()) - return googleProduct.Gender; - - return Settings.Gender; - } - private string AgeGroup(GoogleProductRecord googleProduct) - { - if (Settings.AgeGroup.IsCaseInsensitiveEqual(PluginHelper.NotSpecified)) - return ""; - - if (googleProduct != null && googleProduct.AgeGroup.HasValue()) - return googleProduct.AgeGroup; - - return Settings.AgeGroup; - } - private string Color(GoogleProductRecord googleProduct) - { - if (googleProduct != null && googleProduct.Color.HasValue()) - return googleProduct.Color; - - return Settings.Color; - } - private string Size(GoogleProductRecord googleProduct) - { - if (googleProduct != null && googleProduct.Size.HasValue()) - return googleProduct.Size; - - return Settings.Size; - } - private string Material(GoogleProductRecord googleProduct) - { - if (googleProduct != null && googleProduct.Material.HasValue()) - return googleProduct.Material; - - return Settings.Material; - } - private string Pattern(GoogleProductRecord googleProduct) - { - if (googleProduct != null && googleProduct.Pattern.HasValue()) - return googleProduct.Pattern; - - return Settings.Pattern; - } - - private string ItemGroupId(GoogleProductRecord googleProduct) - { - if (googleProduct != null && googleProduct.ItemGroupId.HasValue()) - return googleProduct.ItemGroupId; - - return ""; - } - - private bool BasePriceSupported(int baseAmount, string unit) - { - if (baseAmount == 1 || baseAmount == 10 || baseAmount == 100) - return true; - - if (baseAmount == 75 && unit == "cl") - return true; - - if ((baseAmount == 50 || baseAmount == 1000) && unit == "kg") - return true; - - return false; - } - - private string BasePriceUnits(string value) - { - const string defaultValue = "kg"; - - if (value.IsEmpty()) - return defaultValue; - - // TODO: Product.BasePriceMeasureUnit should be localized - switch (value.ToLowerInvariant()) - { - case "mg": - case "milligramm": - case "milligram": - return "mg"; - case "g": - case "gramm": - case "gram": - return "g"; - case "kg": - case "kilogramm": - case "kilogram": - return "kg"; - - case "ml": - case "milliliter": - case "millilitre": - return "ml"; - case "cl": - case "zentiliter": - case "centilitre": - return "cl"; - case "l": - case "liter": - case "litre": - return "l"; - case "cbm": - case "kubikmeter": - case "cubic metre": - return "cbm"; - - case "cm": - case "zentimeter": - case "centimetre": - return "cm"; - case "m": - case "meter": - return "m"; - - case "qm": - case "quadratmeter": - case "square metre": - return "sqm"; - - default: - return defaultValue; - } - } - private void WriteItem(FeedFileCreationContext fileCreation, XmlWriter writer, Product product, Currency currency, string measureWeightSystemKey) - { - GoogleProductRecord googleProduct = null; - - try - { - googleProduct = GetGoogleProductRecord(product.Id); - - if (googleProduct != null && !googleProduct.Export) - return; - } - catch (Exception exc) - { - fileCreation.Logger.Error(exc.Message, exc); - } - - writer.WriteStartElement("item"); - - try - { - var manu = _manufacturerService.GetProductManufacturersByProductId(product.Id).FirstOrDefault(); - var mainImageUrl = Helper.GetMainProductImageUrl(fileCreation.Store, product); - var category = ProductCategory(googleProduct); - - if (category.IsEmpty()) - fileCreation.ErrorMessage = Helper.GetResource("MissingDefaultCategory"); - - string manuName = (manu != null ? manu.Manufacturer.GetLocalized(x => x.Name, Settings.LanguageId, true, false) : null); - string productName = product.GetLocalized(x => x.Name, Settings.LanguageId, true, false); - string shortDescription = product.GetLocalized(x => x.ShortDescription, Settings.LanguageId, true, false); - string fullDescription = product.GetLocalized(x => x.FullDescription, Settings.LanguageId, true, false); - - var brand = (manuName ?? Settings.Brand); - var mpn = Helper.GetManufacturerPartNumber(product); - - bool identifierExists = product.Gtin.HasValue() || brand.HasValue() || mpn.HasValue(); - - writer.WriteElementString("g", "id", _googleNamespace, product.Id.ToString()); - - writer.WriteStartElement("title"); - writer.WriteCData(productName.Truncate(70)); - writer.WriteEndElement(); - - var description = Helper.BuildProductDescription(productName, shortDescription, fullDescription, manuName, d => - { - if (fullDescription.IsEmpty() && shortDescription.IsEmpty()) - { - var rnd = new Random(); - - switch (rnd.Next(1, 5)) - { - case 1: return d.Grow(Settings.AppendDescriptionText1, " "); - case 2: return d.Grow(Settings.AppendDescriptionText2, " "); - case 3: return d.Grow(Settings.AppendDescriptionText3, " "); - case 4: return d.Grow(Settings.AppendDescriptionText4, " "); - case 5: return d.Grow(Settings.AppendDescriptionText5, " "); - } - } - return d; - }); - - writer.WriteStartElement("description"); - writer.WriteCData(description.RemoveInvalidXmlChars()); - writer.WriteEndElement(); - - writer.WriteStartElement("g", "google_product_category", _googleNamespace); - writer.WriteCData(category); - writer.WriteFullEndElement(); - - string productType = Helper.GetCategoryPath(product); - if (productType.HasValue()) - { - writer.WriteStartElement("g", "product_type", _googleNamespace); - writer.WriteCData(productType); - writer.WriteFullEndElement(); - } - - writer.WriteElementString("link", Helper.GetProductDetailUrl(fileCreation.Store, product)); - writer.WriteElementString("g", "image_link", _googleNamespace, mainImageUrl); - - foreach (string additionalImageUrl in Helper.GetAdditionalProductImages(fileCreation.Store, product, mainImageUrl)) - { - writer.WriteElementString("g", "additional_image_link", _googleNamespace, additionalImageUrl); - } - - writer.WriteElementString("g", "condition", _googleNamespace, Condition()); - writer.WriteElementString("g", "availability", _googleNamespace, Availability(product)); - - decimal price = Helper.GetProductPrice(product, currency); - string specialPriceDate; - - if (SpecialPrice(product, out specialPriceDate)) - { - writer.WriteElementString("g", "sale_price", _googleNamespace, price.FormatInvariant() + " " + currency.CurrencyCode); - writer.WriteElementString("g", "sale_price_effective_date", _googleNamespace, specialPriceDate); - - // get regular price ignoring any special price - decimal specialPrice = product.SpecialPrice.Value; - product.SpecialPrice = null; - price = Helper.GetProductPrice(product, currency); - product.SpecialPrice = specialPrice; - - _dbContext.SetToUnchanged(product); - } - - writer.WriteElementString("g", "price", _googleNamespace, price.FormatInvariant() + " " + currency.CurrencyCode); - - writer.WriteCData("gtin", product.Gtin, "g", _googleNamespace); - writer.WriteCData("brand", brand, "g", _googleNamespace); - writer.WriteCData("mpn", mpn, "g", _googleNamespace); - - writer.WriteCData("gender", Gender(googleProduct), "g", _googleNamespace); - writer.WriteCData("age_group", AgeGroup(googleProduct), "g", _googleNamespace); - writer.WriteCData("color", Color(googleProduct), "g", _googleNamespace); - writer.WriteCData("size", Size(googleProduct), "g", _googleNamespace); - writer.WriteCData("material", Material(googleProduct), "g", _googleNamespace); - writer.WriteCData("pattern", Pattern(googleProduct), "g", _googleNamespace); - writer.WriteCData("item_group_id", ItemGroupId(googleProduct), "g", _googleNamespace); - - writer.WriteElementString("g", "online_only", _googleNamespace, Settings.OnlineOnly ? "y" : "n"); - writer.WriteElementString("g", "identifier_exists", _googleNamespace, identifierExists ? "TRUE" : "FALSE"); - - if (Settings.ExpirationDays > 0) - { - writer.WriteElementString("g", "expiration_date", _googleNamespace, DateTime.UtcNow.AddDays(Settings.ExpirationDays).ToString("yyyy-MM-dd")); - } - - if (Settings.ExportShipping) - { - string weightInfo, weight = product.Weight.FormatInvariant(); - - if (measureWeightSystemKey.IsCaseInsensitiveEqual("gram")) - weightInfo = weight + " g"; - else if (measureWeightSystemKey.IsCaseInsensitiveEqual("lb")) - weightInfo = weight + " lb"; - else if (measureWeightSystemKey.IsCaseInsensitiveEqual("ounce")) - weightInfo = weight + " oz"; - else - weightInfo = weight + " kg"; - - writer.WriteElementString("g", "shipping_weight", _googleNamespace, weightInfo); - } - - if (Settings.ExportBasePrice && product.BasePriceHasValue) - { - string measureUnit = BasePriceUnits(product.BasePriceMeasureUnit); - - if (BasePriceSupported(product.BasePriceBaseAmount ?? 0, measureUnit)) - { - string basePriceMeasure = "{0} {1}".FormatWith((product.BasePriceAmount ?? decimal.Zero).FormatInvariant(), measureUnit); - string basePriceBaseMeasure = "{0} {1}".FormatWith(product.BasePriceBaseAmount, measureUnit); - - writer.WriteElementString("g", "unit_pricing_measure", _googleNamespace, basePriceMeasure); - writer.WriteElementString("g", "unit_pricing_base_measure", _googleNamespace, basePriceBaseMeasure); - } - } - } - catch (Exception exc) - { - fileCreation.Logger.Error(exc.Message, exc); - } - - writer.WriteEndElement(); // item - } - - public string[] GetTaxonomyList() - { - try - { - string fileDir = Path.Combine(Helper.Plugin.OriginalAssemblyFile.Directory.FullName, "Files"); - string fileName = "taxonomy.{0}.txt".FormatWith(Helper.Language.LanguageCulture ?? "de-DE"); - string path = Path.Combine(fileDir, fileName); - - if (!File.Exists(path)) - path = Path.Combine(fileDir, "taxonomy.en-US.txt"); - - string[] lines = File.ReadAllLines(path, Encoding.UTF8); - - return lines; - } - catch (Exception exc) - { - exc.Dump(); - } - return new string[] { }; - } - - public void UpdateInsert(int pk, string name, string value) + public void Upsert(int pk, string name, string value) { if (pk == 0 || name.IsEmpty()) return; @@ -527,9 +125,36 @@ public void UpdateInsert(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; @@ -550,12 +175,16 @@ public void UpdateInsert(int pk, string name, string value) _gpRepository.Update(product); } } + public GridModel GetGridModel(GridCommand command, string searchProductName = null, string touched = null) { var model = new GridModel(); var textInfo = CultureInfo.InvariantCulture.TextInfo; + 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)"); @@ -580,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]" + @@ -597,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]" + @@ -623,19 +252,25 @@ public GridModel GetGridModel(GridCommand command, string se data.ForEach(x => { if (x.ProductType != ProductType.SimpleProduct) - { - string key = "Admin.Catalog.Products.ProductType.{0}.Label".FormatWith(x.ProductType.ToString()); - x.ProductTypeName = Helper.GetResource(key); - } + x.ProductTypeName = T("Admin.Catalog.Products.ProductType.{0}.Label".FormatInvariant(x.ProductType.ToString())); - var googleProduct = GetGoogleProductRecord(x.Id); if (x.Gender.HasValue()) - x.GenderLocalize = Helper.GetResource("Gender" + textInfo.ToTitleCase(x.Gender)); + x.GenderLocalize = T("Plugins.Feed.Froogle.Gender" + textInfo.ToTitleCase(x.Gender)); if (x.AgeGroup.HasValue()) - x.AgeGroupLocalize = Helper.GetResource("AgeGroup" + textInfo.ToTitleCase(x.AgeGroup)); + x.AgeGroupLocalize = T("Plugins.Feed.Froogle.AgeGroup" + textInfo.ToTitleCase(x.AgeGroup)); + + x.Export2Localize = (x.Export == 0 ? no : yes); - x.ExportingLocalize = Helper.GetResource(x.Export == 0 ? "Admin.Common.No" : "Admin.Common.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; @@ -654,182 +289,31 @@ public GridModel GetGridModel(GridCommand command, string se } return model; - - #region old code - - //var searchContext = new ProductSearchContext() - //{ - // Keywords = searchProductName, - // PageIndex = command.Page - 1, - // PageSize = command.PageSize, - // VisibleIndividuallyOnly = true, - // ShowHidden = true - //}; - - //var products = _productService.SearchProducts(searchContext); - - //var data = products.Select(x => - //{ - // var gModel = new GoogleProductModel() - // { - // ProductId = x.Id, - // Name = x.Name - // }; - - // var googleProduct = GetByProductId(x.Id); - - // if (googleProduct != null) - // { - // gModel.Taxonomy = googleProduct.Taxonomy; - // gModel.Gender = googleProduct.Gender; - // gModel.AgeGroup = googleProduct.AgeGroup; - // gModel.Color = googleProduct.Color; - // gModel.Size = googleProduct.Size; - // gModel.Material = googleProduct.Material; - // gModel.Pattern = googleProduct.Pattern; - - // if (gModel.Gender.HasValue()) - // gModel.GenderLocalize = Helper.GetResource("Gender" + CultureInfo.InvariantCulture.TextInfo.ToTitleCase(gModel.Gender)); - - // if (gModel.AgeGroup.HasValue()) - // gModel.AgeGroupLocalize = Helper.GetResource("AgeGroup" + CultureInfo.InvariantCulture.TextInfo.ToTitleCase(gModel.AgeGroup)); - // } - - // return gModel; - //}) - //.ToList(); - - //var model = new GridModel() - //{ - // Data = data, - // Total = products.TotalCount - //}; - - //return model; - - #endregion old code - } - - private void CreateFeed(FeedFileCreationContext fileCreation, TaskExecutionContext taskContext) - { - var xmlSettings = new XmlWriterSettings - { - Encoding = Encoding.UTF8, - CheckCharacters = false - }; - - using (var writer = XmlWriter.Create(fileCreation.Stream, xmlSettings)) - { - try - { - fileCreation.Logger.Information("Log file - Google Merchant Center feed."); - - var searchContext = new ProductSearchContext - { - OrderBy = ProductSortingEnum.CreatedOn, - PageSize = Settings.PageSize, - StoreId = fileCreation.Store.Id, - VisibleIndividuallyOnly = true - }; - - var currency = Helper.GetUsedCurrency(Settings.CurrencyId); - var measureWeightSystemKey = _measureService.GetMeasureWeightById(_measureSettings.BaseWeightId).SystemKeyword; - - writer.WriteStartDocument(); - writer.WriteStartElement("rss"); - writer.WriteAttributeString("version", "2.0"); - writer.WriteAttributeString("xmlns", "g", null, _googleNamespace); - writer.WriteStartElement("channel"); - writer.WriteElementString("title", "{0} - Feed for Google Merchant Center".FormatWith(fileCreation.Store.Name)); - writer.WriteElementString("link", "http://base.google.com/base/"); - writer.WriteElementString("description", "Information about products"); - - for (int i = 0; i < 9999999; ++i) - { - searchContext.PageIndex = i; - - // Perf - _dbContext.DetachAll(); - - var products = _productService.SearchProducts(searchContext); - - if (fileCreation.TotalRecords == 0) - fileCreation.TotalRecords = products.TotalCount * fileCreation.StoreCount; // approx - - foreach (var product in products) - { - fileCreation.Report(); - - if (product.ProductType == ProductType.SimpleProduct || product.ProductType == ProductType.BundledProduct) - { - WriteItem(fileCreation, writer, product, currency, measureWeightSystemKey); - } - else if (product.ProductType == ProductType.GroupedProduct) - { - var associatedSearchContext = new ProductSearchContext - { - OrderBy = ProductSortingEnum.CreatedOn, - PageSize = int.MaxValue, - StoreId = fileCreation.Store.Id, - VisibleIndividuallyOnly = false, - ParentGroupedProductId = product.Id - }; - - foreach (var associatedProduct in _productService.SearchProducts(associatedSearchContext)) - { - WriteItem(fileCreation, writer, associatedProduct, currency, measureWeightSystemKey); - } - } - - if (taskContext.CancellationToken.IsCancellationRequested) - { - fileCreation.Logger.Warning("A cancellation has been requested"); - break; - } - } - - if (!products.HasNextPage || taskContext.CancellationToken.IsCancellationRequested) - break; - } - - writer.WriteEndElement(); // channel - writer.WriteEndElement(); // rss - writer.WriteEndDocument(); - - if (fileCreation.ErrorMessage.HasValue()) - fileCreation.Logger.Error(fileCreation.ErrorMessage); - } - catch (Exception exc) - { - fileCreation.Logger.Error(exc.Message, exc); - } - } } - public void CreateFeed(TaskExecutionContext context) + public string[] GetTaxonomyList() { - Helper.StartCreatingFeeds(fileCreation => + try { - CreateFeed(fileCreation, context); - return true; - }); - } + var descriptor = _pluginFinder.GetPluginDescriptorBySystemName(GoogleMerchantCenterFeedPlugin.SystemName); - public void SetupModel(FeedFroogleModel model) - { - Helper.SetupConfigModel(model, "FeedFroogle"); + var fileDir = Path.Combine(descriptor.OriginalAssemblyFile.Directory.FullName, "Files"); + var fileName = "taxonomy.{0}.txt".FormatWith(_services.WorkContext.WorkingLanguage.LanguageCulture ?? "de-DE"); + var path = Path.Combine(fileDir, fileName); - model.GenerateStaticFileEachMinutes = Helper.ScheduleTask.Seconds / 60; - model.TaskEnabled = Helper.ScheduleTask.Enabled; + if (!File.Exists(path)) + path = Path.Combine(fileDir, "taxonomy.en-US.txt"); - model.AvailableCurrencies = Helper.AvailableCurrencies(); - model.AvailableGoogleCategories = GetTaxonomyList(); + string[] lines = File.ReadAllLines(path, Encoding.UTF8); - var urlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext); - model.GridEditUrl = urlHelper.Action("GoogleProductEdit", "FeedFroogle", - new { Namespaces = "SmartStore.GoogleMerchantCenter.Controllers", area = "SmartStore.GoogleMerchantCenter" }); + return lines; + } + catch (Exception exc) + { + exc.Dump(); + } - model.GridPageSize = _adminAreaSettings.GridPageSize; + return new string[] { }; } } } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Services/IGoogleFeedService.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Services/IGoogleFeedService.cs index e5a6e1d87e..6b89e81213 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Services/IGoogleFeedService.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Services/IGoogleFeedService.cs @@ -1,29 +1,26 @@ +using System.Collections.Generic; using SmartStore.GoogleMerchantCenter.Domain; using SmartStore.GoogleMerchantCenter.Models; -using SmartStore.Services.Tasks; -using SmartStore.Web.Framework.Plugins; using Telerik.Web.Mvc; namespace SmartStore.GoogleMerchantCenter.Services { public partial interface IGoogleFeedService { - FroogleSettings Settings { get; set; } - FeedPluginHelper Helper { get; } - GoogleProductRecord GetGoogleProductRecord(int productId); + + List GetGoogleProductRecords(int[] productIds); + void InsertGoogleProductRecord(GoogleProductRecord record); + void UpdateGoogleProductRecord(GoogleProductRecord record); + void DeleteGoogleProductRecord(GoogleProductRecord record); - string[] GetTaxonomyList(); - - void UpdateInsert(int pk, string name, string value); + void Upsert(int pk, string name, string value); GridModel GetGridModel(GridCommand command, string searchProductName = null, string touched = null); - void CreateFeed(TaskExecutionContext context); - - void SetupModel(FeedFroogleModel model); + string[] GetTaxonomyList(); } } diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj b/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj index 8ad761ecef..f743627997 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/SmartStore.GoogleMerchantCenter.csproj @@ -42,6 +42,7 @@ + true @@ -81,28 +82,29 @@ 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 - ..\..\packages\EntityFramework.6.1.0\lib\net45\EntityFramework.dll + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll False - ..\..\packages\EntityFramework.6.1.0\lib\net45\EntityFramework.SqlServer.dll + ..\..\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 @@ -153,7 +155,7 @@ Properties\AssemblyVersionInfo.cs - + @@ -168,20 +170,24 @@ 201504211854125_IsActive.cs + + + 201601061649324_IsBundle.cs + - - - - + + + + - + @@ -205,7 +211,10 @@ 201403112356126_Initial.cs - + + PreserveNewest + + PreserveNewest @@ -214,22 +223,22 @@ 201504211854125_IsActive.cs + + 201601061649324_IsBundle.cs + - - Always - - - Always + + PreserveNewest PreserveNewest - Always + PreserveNewest - Always + PreserveNewest PreserveNewest @@ -257,7 +266,7 @@ - + PreserveNewest @@ -266,6 +275,11 @@ PreserveNewest + + + PreserveNewest + + 10.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/StaticFileGenerationTask.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/StaticFileGenerationTask.cs deleted file mode 100644 index 2cc1cfca3b..0000000000 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/StaticFileGenerationTask.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Autofac; -using SmartStore.GoogleMerchantCenter.Services; -using SmartStore.Services.Tasks; - -namespace SmartStore.GoogleMerchantCenter -{ - public class StaticFileGenerationTask : ITask - { - public void Execute(TaskExecutionContext context) - { - var scope = context.LifetimeScope as ILifetimeScope; - var googleService = scope.Resolve(); - - googleService.CreateFeed(context); - } - } -} \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Validators/ConfigurationValidator.cs b/src/Plugins/SmartStore.GoogleMerchantCenter/Validators/ProfileConfigurationValidator.cs similarity index 68% rename from src/Plugins/SmartStore.GoogleMerchantCenter/Validators/ConfigurationValidator.cs rename to src/Plugins/SmartStore.GoogleMerchantCenter/Validators/ProfileConfigurationValidator.cs index 169214a4cc..9799ada57f 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Validators/ConfigurationValidator.cs +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Validators/ProfileConfigurationValidator.cs @@ -4,9 +4,9 @@ namespace SmartStore.GoogleMerchantCenter.Validators { - public class ConfigurationValidator : AbstractValidator + public class ProfileConfigurationValidator : AbstractValidator { - public ConfigurationValidator(ILocalizationService localize) + public ProfileConfigurationValidator(ILocalizationService localize) { RuleFor(x => x.ExpirationDays).InclusiveBetween(0, 29) .WithMessage(localize.GetResource("Plugins.Feed.Froogle.ExpirationDays.Validate")); diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedFroogle/Configure.cshtml b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedFroogle/Configure.cshtml deleted file mode 100644 index 3720c9df24..0000000000 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedFroogle/Configure.cshtml +++ /dev/null @@ -1,620 +0,0 @@ -@model FeedFroogleModel -@using SmartStore.GoogleMerchantCenter; -@using SmartStore.GoogleMerchantCenter.Models; -@using SmartStore.Web.Framework; -@using SmartStore.Web.Framework.Plugins; -@using Telerik.Web.Mvc.UI; -@using SmartStore.Web.Framework.UI; - -@{ - Layout = ""; - - Html.AddCssFileParts(true, - "~/Content/x-editable/bootstrap-editable.css", - "~/Plugins/SmartStore.GoogleMerchantCenter/Content/smartstore.feed.froogle.css"); - - Html.AppendScriptParts(true, "~/Content/x-editable/bootstrap-editable.js"); -} - - - - - -@(Html.SmartStore().TabStrip().Name("googlebase-configure").Items(x => { - x.Add().Text(T("Plugins.Feed.Froogle.General").Text).Content(@TabGeneral()).Selected(true); - x.Add().Text(T("Plugins.Feed.Froogle.ProductData").Text).Content(@TabProductData()); -})) - -@helper TabGeneral() -{ - - -
    - - - - - @if (Model.GeneratedFiles.Count > 0 && !Model.IsRunning) - { - - - - - } - else if (Model.IsRunning) - { - - - - - } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - @Html.Raw(@T("Plugins.Feed.Froogle.AdminInstruction")) -
    -
    - @Html.SmartLabelFor(m => m.GeneratedFiles) - - @foreach (var group in Model.GeneratedFiles.GroupBy(x => x.StoreId)) - { - var firstFile = group.First(); -

    - @(firstFile.StoreName): - @foreach (var file in group) - { -
    @file.FileUrl - } -
    @T("Admin.Configuration.ActivityLog"), - @T("Common.UpdatedOn"): @(firstFile.LastWriteTime) -

    - } -
      - - @Model.ProcessInfo -
      - @if (Model.IsRunning) - { - -  @T("Admin.Common.Update") - - } - else - { - -  @T("Plugins.Feed.Froogle.Generate") - - - if (Model.GeneratedFiles.Count > 0) - { - -  @T("Admin.Common.Delete") - - } - } -
    -
    -
    - @Html.SmartLabelFor(m => m.DefaultGoogleCategory) - - -
    - @Html.SmartLabelFor(m => m.ProductPictureSize) - - @Html.EditorFor(m => m.ProductPictureSize) - @Html.ValidationMessageFor(m => m.ProductPictureSize) -
    - @Html.SmartLabelFor(m => m.AdditionalImages) - - @Html.EditorFor(m => m.AdditionalImages) - @Html.ValidationMessageFor(m => m.AdditionalImages) -
    -
    -
    - @Html.SmartLabelFor(model => model.StoreId) - - @Html.DropDownListFor(model => model.StoreId, Model.AvailableStores) - @Html.ValidationMessageFor(model => model.StoreId) -
    - @Html.SmartLabelFor(model => model.LanguageId) - - @Html.DropDownListFor(model => model.LanguageId, Model.AvailableLanguages) - @Html.ValidationMessageFor(model => model.LanguageId) -
    - @Html.SmartLabelFor(m => m.BuildDescription) - - @Html.DropDownList("BuildDescription", new List { - new SelectListItem { Text = Model.Helper.GetResource("Automatic"), Value = "" }, - new SelectListItem { Text = Model.Helper.GetResource("Common.Unspecified"), Value = PluginHelper.NotSpecified }, - new SelectListItem { Text = Model.Helper.GetResource("DescShort"), Value = "short" }, - new SelectListItem { Text = Model.Helper.GetResource("DescLong"), Value = "long" }, - new SelectListItem { Text = Model.Helper.GetResource("DescTitleAndShort"), Value = "titleAndShort" }, - new SelectListItem { Text = Model.Helper.GetResource("DescTitleAndLong"), Value = "titleAndLong" }, - new SelectListItem { Text = Model.Helper.GetResource("DescManuAndTitleAndShort"), Value = "manuAndTitleAndShort" }, - new SelectListItem { Text = Model.Helper.GetResource("DescManuAndTitleAndLong"), Value = "manuAndTitleAndLong" } - }) - @Html.ValidationMessageFor(m => m.BuildDescription) -
    - @Html.SmartLabelFor(m => m.DescriptionToPlainText) - - @Html.EditorFor(m => m.DescriptionToPlainText) - @Html.ValidationMessageFor(m => m.DescriptionToPlainText) -
    - @Html.SmartLabelFor(m => m.AppendDescriptionText1) - - @Html.EditorFor(m => m.AppendDescriptionText1) - @Html.ValidationMessageFor(m => m.AppendDescriptionText1) -
    -   - - @Html.EditorFor(m => m.AppendDescriptionText2) - @Html.ValidationMessageFor(m => m.AppendDescriptionText2) -
    -   - - @Html.EditorFor(m => m.AppendDescriptionText3) - @Html.ValidationMessageFor(m => m.AppendDescriptionText3) -
    -   - - @Html.EditorFor(m => m.AppendDescriptionText4) - @Html.ValidationMessageFor(m => m.AppendDescriptionText4) -
    -   - - @Html.EditorFor(m => m.AppendDescriptionText5) - @Html.ValidationMessageFor(m => m.AppendDescriptionText5) -
    -
    -
    - @Html.SmartLabelFor(m => m.Condition) - - @Html.DropDownList("Condition", new List { - new SelectListItem { Text = Model.Helper.GetResource("Automatic"), Value = "" }, - new SelectListItem { Text = Model.Helper.GetResource("Common.Unspecified"), Value = PluginHelper.NotSpecified }, - new SelectListItem { Text = Model.Helper.GetResource("ConditionNew"), Value = "new" }, - new SelectListItem { Text = Model.Helper.GetResource("ConditionUsed"), Value = "used" }, - new SelectListItem { Text = Model.Helper.GetResource("ConditionRefurbished"), Value = "refurbished" } - }) - @Html.ValidationMessageFor(m => m.Condition) -
    - @Html.SmartLabelFor(m => m.Availability) - - @Html.DropDownList("Availability", new List { - new SelectListItem { Text = Model.Helper.GetResource("Automatic"), Value = "" }, - new SelectListItem { Text = Model.Helper.GetResource("Common.Unspecified"), Value = PluginHelper.NotSpecified }, - new SelectListItem { Text = Model.Helper.GetResource("AvailabilityInStock"), Value = "in stock" }, - new SelectListItem { Text = Model.Helper.GetResource("AvailabilityAvailableForOrder"), Value = "available for order" }, - new SelectListItem { Text = Model.Helper.GetResource("AvailabilityOutOfStock"), Value = "out of stock" }, - new SelectListItem { Text = Model.Helper.GetResource("AvailabilityPreorder"), Value = "preorder" } - }) - @Html.ValidationMessageFor(m => m.Availability) -
    - @Html.SmartLabelFor(m => m.Gender) - - @Html.DropDownList("Gender", new List { - new SelectListItem { Text = Model.Helper.GetResource("Automatic"), Value = "" }, - new SelectListItem { Text = Model.Helper.GetResource("Common.Unspecified"), Value = PluginHelper.NotSpecified }, - new SelectListItem { Text = Model.Helper.GetResource("GenderMale"), Value = "male" }, - new SelectListItem { Text = Model.Helper.GetResource("GenderFemale"), Value = "female" }, - new SelectListItem { Text = Model.Helper.GetResource("GenderUnisex"), Value = "unisex" } - }) - @Html.ValidationMessageFor(m => m.Gender) -
    - @Html.SmartLabelFor(m => m.AgeGroup) - - @Html.DropDownList("AgeGroup", new List { - new SelectListItem { Text = Model.Helper.GetResource("Automatic"), Value = "" }, - new SelectListItem { Text = Model.Helper.GetResource("Common.Unspecified"), Value = PluginHelper.NotSpecified }, - new SelectListItem { Text = Model.Helper.GetResource("AgeGroupAdult"), Value = "adult" }, - new SelectListItem { Text = Model.Helper.GetResource("AgeGroupKids"), Value = "kids" } - }) - @Html.ValidationMessageFor(m => m.AgeGroup) -
    - @Html.SmartLabelFor(m => m.Brand) - - @Html.EditorFor(m => m.Brand) - @Html.ValidationMessageFor(m => m.Brand) -
    - @Html.SmartLabelFor(m => m.Color) - - @Html.EditorFor(m => m.Color) - @Html.ValidationMessageFor(m => m.Color) -
    - @Html.SmartLabelFor(m => m.Size) - - @Html.EditorFor(m => m.Size) - @Html.ValidationMessageFor(m => m.Size) -
    - @Html.SmartLabelFor(m => m.Material) - - @Html.EditorFor(m => m.Material) - @Html.ValidationMessageFor(m => m.Material) -
    - @Html.SmartLabelFor(m => m.Pattern) - - @Html.EditorFor(m => m.Pattern) - @Html.ValidationMessageFor(m => m.Pattern) -
    -
    -
    - @Html.SmartLabelFor(m => m.CurrencyId) - - @Html.DropDownListFor(m => m.CurrencyId, Model.AvailableCurrencies) - @Html.ValidationMessageFor(m => m.CurrencyId) -
    - @Html.SmartLabelFor(m => m.ExpirationDays) - - @Html.EditorFor(m => m.ExpirationDays) - @Html.ValidationMessageFor(m => m.ExpirationDays) -
    - @Html.SmartLabelFor(m => m.SpecialPrice) - - @Html.EditorFor(m => m.SpecialPrice) - @Html.ValidationMessageFor(m => m.SpecialPrice) -
    - @Html.SmartLabelFor(m => m.ExportShipping) - - @Html.EditorFor(m => m.ExportShipping) - @Html.ValidationMessageFor(m => m.ExportShipping) -
    - @Html.SmartLabelFor(m => m.ExportBasePrice) - - @Html.EditorFor(m => m.ExportBasePrice) - @Html.ValidationMessageFor(m => m.ExportBasePrice) -
    - @Html.SmartLabelFor(m => m.UseOwnProductNo) - - @Html.EditorFor(m => m.UseOwnProductNo) - @Html.ValidationMessageFor(m => m.UseOwnProductNo) -
    - @Html.SmartLabelFor(m => m.OnlineOnly) - - @Html.EditorFor(m => m.OnlineOnly) - @Html.ValidationMessageFor(m => m.OnlineOnly) -
    - @Html.SmartLabelFor(m => m.ConvertNetToGrossPrices) - - @Html.EditorFor(m => m.ConvertNetToGrossPrices) - @Html.ValidationMessageFor(m => m.ConvertNetToGrossPrices) -
    - @Html.SmartLabelFor(m => m.TaskEnabled) - - @Html.EditorFor(m => m.TaskEnabled) - @Html.ValidationMessageFor(m => m.TaskEnabled) -
    - @Html.SmartLabelFor(m => m.GenerateStaticFileEachMinutes) - - @Html.EditorFor(m => m.GenerateStaticFileEachMinutes) - @Html.ValidationMessageFor(m => m.GenerateStaticFileEachMinutes) -
      - -
    -
    -} - -@helper TabProductData() -{ -
    - - @Html.Raw(@T("Plugins.Feed.Froogle.GridEditNote")) -
    - - - - - - - - -
    - - - - - - - - - - - - - - - - -
    - @(Html.Telerik().Grid() - .Name("froogleproducts-grid") - .DataKeys(keys => - { - keys.Add(x => x.ProductId).RouteKey("ProductId"); - }) - .Columns(c => - { - c.Bound(x => x.ProductId).ReadOnly().Visible(false); - c.Bound(x => x.Name) - .ReadOnly().Visible(true).Width(420) - .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")); - }) - .ClientEvents(e => - { - e.OnDataBound("OnGridDataBound"); - e.OnDataBinding("OnGridDataBinding"); - }) - .DataBinding(dataBinding => - { - dataBinding.Ajax().Select("GoogleProductList", "FeedFroogle"); - }) - .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) - .PreserveGridState() - .EnableCustomBinding(true) - ) -
    - - -} diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedFroogle/ProductEditTab.cshtml b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedFroogle/ProductEditTab.cshtml deleted file mode 100644 index fa3273da09..0000000000 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedFroogle/ProductEditTab.cshtml +++ /dev/null @@ -1,111 +0,0 @@ -@model SmartStore.GoogleMerchantCenter.Models.GoogleProductModel - -@{ - Layout = ""; -} - - - -@* VERY IMPORTANT for proper model binding *@ -@Html.Hidden("__Type__", Model.GetType().AssemblyQualifiedName) - -@Html.HiddenFor(m => m.ProductId) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - @Html.SmartLabelFor(m => m.Exporting) - - @Html.EditorFor(m => m.Exporting) - @Html.ValidationMessageFor(m => m.Exporting) -
    - @Html.SmartLabelFor(m => m.Taxonomy) - - @Html.TextBoxFor(m => m.Taxonomy, new { data_provide = "typeahead", placeholder = ViewBag.DefaultCategory, style = "width: 97%", autocomplete = "off" }) - @Html.ValidationMessageFor(m => m.Taxonomy) -
    - @Html.SmartLabelFor(m => m.Gender) - - @Html.DropDownListFor(m => m.Gender, (IEnumerable)ViewBag.AvailableGenders, (string)ViewBag.DefaultGender) - @Html.ValidationMessageFor(m => m.Gender) -
    - @Html.SmartLabelFor(m => m.AgeGroup) - - @Html.DropDownListFor(m => m.AgeGroup, (IEnumerable)ViewBag.AvailableAgeGroups, (string)ViewBag.DefaultAgeGroup) - @Html.ValidationMessageFor(m => m.AgeGroup) -
    - @Html.SmartLabelFor(m => m.Color) - - @Html.TextBoxFor(m => m.Color, new { placeholder = ViewBag.DefaultColor }) - @Html.ValidationMessageFor(m => m.Color) -
    - @Html.SmartLabelFor(m => m.Size) - - @Html.TextBoxFor(m => m.Size, new { placeholder = ViewBag.DefaultSize }) - @Html.ValidationMessageFor(m => m.Size) -
    - @Html.SmartLabelFor(m => m.Material) - - @Html.TextBoxFor(m => m.Material, new { placeholder = ViewBag.DefaultMaterial }) - @Html.ValidationMessageFor(m => m.Material) -
    - @Html.SmartLabelFor(m => m.Pattern) - - @Html.TextBoxFor(m => m.Pattern, new { placeholder = ViewBag.DefaultPattern }) - @Html.ValidationMessageFor(m => m.Pattern) -
    \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml new file mode 100644 index 0000000000..d91ec16571 --- /dev/null +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/Configure.cshtml @@ -0,0 +1,272 @@ +@model FeedGoogleMerchantCenterModel +@using SmartStore.GoogleMerchantCenter; +@using SmartStore.GoogleMerchantCenter.Models; +@using SmartStore.GoogleMerchantCenter.Providers; +@using SmartStore.Web.Framework; +@using Telerik.Web.Mvc.UI; +@using SmartStore.Web.Framework.UI; + +@{ + Layout = ""; + + Html.AddCssFileParts(true, "~/Content/x-editable/bootstrap-editable.css"); + Html.AddCssFileParts(true, "~/Plugins/SmartStore.GoogleMerchantCenter/Content/smartstore.gmc.css"); + Html.AppendScriptParts(true, "~/Content/x-editable/bootstrap-editable.js"); +} + +
    +
    +
    + + @Html.Raw(@T("Plugins.Feed.Froogle.AdminInstruction")) +
    +
    + @Html.Action("InfoProfile", "Export", new { systemName = GmcXmlExportProvider.SystemName, returnUrl = Request.RawUrl, area = "admin" }) +
    +
    +
    + + Google Merchant Center + +
    +
    + +

     

    + +
    + + + + + + + +
    + + + + + + + + + + + + + + + + +
    + @(Html.Telerik().Grid() + .Name("gmc-products-grid") + .DataKeys(keys => + { + keys.Add(x => x.ProductId).RouteKey("ProductId"); + }) + .Columns(c => + { + c.Bound(x => x.ProductId) + .ReadOnly() + .Visible(false); + c.Bound(x => x.Name) + .ReadOnly().Visible(true) + .Template(x => @Html.LabeledProductName(x.ProductId, x.Name, x.ProductTypeName, x.ProductTypeLabelHint)) + .ClientTemplate(@Html.LabeledProductName("ProductId", "Name")); + c.Bound(x => x.SKU) + .ReadOnly() + .Visible(true); + c.Bound(x => x.Export2) + .ClientTemplate(Html.XEditableLink("Export2", "select2")); + c.Bound(x => x.Taxonomy) + .ClientTemplate(Html.XEditableLink("Taxonomy", "typeahead")); + c.Bound(x => x.Gender) + .ClientTemplate(Html.XEditableLink("Gender", "select2")); + c.Bound(x => x.AgeGroup) + .ClientTemplate(Html.XEditableLink("AgeGroup", "select2")); + c.Bound(x => x.IsAdult) + .ClientTemplate(Html.XEditableLink("IsAdult", "select2")); + c.Bound(x => x.Color) + .ClientTemplate(Html.XEditableLink("Color", "text")); + c.Bound(x => x.Size) + .ClientTemplate(Html.XEditableLink("Size", "text")); + c.Bound(x => x.Material) + .ClientTemplate(Html.XEditableLink("Material", "text")); + c.Bound(x => x.Pattern) + .ClientTemplate(Html.XEditableLink("Pattern", "text")); + c.Bound(x => x.Multipack2) + .ClientTemplate(Html.XEditableLink("Multipack2", "text")); + c.Bound(x => x.IsBundle) + .ClientTemplate(Html.XEditableLink("IsBundle", "select2")); + c.Bound(x => x.EnergyEfficiencyClass) + .ClientTemplate(Html.XEditableLink("EnergyEfficiencyClass", "select2")); + c.Bound(x => x.CustomLabel0) + .ClientTemplate(Html.XEditableLink("CustomLabel0", "text")); + c.Bound(x => x.CustomLabel1) + .ClientTemplate(Html.XEditableLink("CustomLabel1", "text")); + c.Bound(x => x.CustomLabel2) + .ClientTemplate(Html.XEditableLink("CustomLabel2", "text")); + c.Bound(x => x.CustomLabel3) + .ClientTemplate(Html.XEditableLink("CustomLabel3", "text")); + c.Bound(x => x.CustomLabel4) + .ClientTemplate(Html.XEditableLink("CustomLabel4", "text")); + }) + .ClientEvents(e => + { + e.OnDataBound("OnGridDataBound"); + e.OnDataBinding("OnGridDataBinding"); + e.OnError("OnGridError"); + }) + .DataBinding(dataBinding => + { + dataBinding.Ajax().Select("GoogleProductList", "FeedGoogleMerchantCenter"); + }) + .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) + .PreserveGridState() + .EnableCustomBinding(true) + ) +
    +
    + + \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProductEditTab.cshtml b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProductEditTab.cshtml new file mode 100644 index 0000000000..c20ef9acd7 --- /dev/null +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProductEditTab.cshtml @@ -0,0 +1,192 @@ +@model SmartStore.GoogleMerchantCenter.Models.GoogleProductModel + +@{ + Layout = ""; +} + + + +@* VERY IMPORTANT for proper model binding *@ +@Html.Hidden("__Type__", Model.GetType().AssemblyQualifiedName) + +@Html.HiddenFor(m => m.ProductId) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + @Html.SmartLabelFor(m => m.Export2) + + @Html.EditorFor(m => m.Export2) + @Html.ValidationMessageFor(m => m.Export2) +
    + @Html.SmartLabelFor(m => m.Taxonomy) + + @Html.TextBoxFor(m => m.Taxonomy, new { data_provide = "typeahead", placeholder = ViewBag.DefaultCategory, style = "width: 97%", autocomplete = "off" }) + @Html.ValidationMessageFor(m => m.Taxonomy) +
    + @Html.SmartLabelFor(m => m.Gender) + + @Html.DropDownListFor(m => m.Gender, (IEnumerable)ViewBag.AvailableGenders, (string)ViewBag.DefaultGender) + @Html.ValidationMessageFor(m => m.Gender) +
    + @Html.SmartLabelFor(m => m.AgeGroup) + + @Html.DropDownListFor(m => m.AgeGroup, (IEnumerable)ViewBag.AvailableAgeGroups, (string)ViewBag.DefaultAgeGroup) + @Html.ValidationMessageFor(m => m.AgeGroup) +
    + @Html.SmartLabelFor(m => m.IsAdult) + + @Html.EditorFor(m => m.IsAdult, new { placeholder = ViewBag.DefaultIsAdult }) + @Html.ValidationMessageFor(m => m.IsAdult) +
    + @Html.SmartLabelFor(m => m.Color) + + @Html.TextBoxFor(m => m.Color, new { placeholder = ViewBag.DefaultColor }) + @Html.ValidationMessageFor(m => m.Color) +
    + @Html.SmartLabelFor(m => m.Size) + + @Html.TextBoxFor(m => m.Size, new { placeholder = ViewBag.DefaultSize }) + @Html.ValidationMessageFor(m => m.Size) +
    + @Html.SmartLabelFor(m => m.Material) + + @Html.TextBoxFor(m => m.Material, new { placeholder = ViewBag.DefaultMaterial }) + @Html.ValidationMessageFor(m => m.Material) +
    + @Html.SmartLabelFor(m => m.Pattern) + + @Html.TextBoxFor(m => m.Pattern, new { placeholder = ViewBag.DefaultPattern }) + @Html.ValidationMessageFor(m => m.Pattern) +
    + @Html.SmartLabelFor(m => m.Multipack2) + + @Html.EditorFor(m => m.Multipack2, new { placeholder = ViewBag.DefaultMultipack2 }) + @Html.ValidationMessageFor(m => m.Multipack2) +
    + @Html.SmartLabelFor(m => m.IsBundle) + + @Html.EditorFor(m => m.IsBundle, new { placeholder = ViewBag.DefaultIsBundle }) + @Html.ValidationMessageFor(m => m.IsBundle) +
    + @Html.SmartLabelFor(m => m.EnergyEfficiencyClass) + + @Html.DropDownListFor(m => m.EnergyEfficiencyClass, (IEnumerable)ViewBag.AvailableEnergyEfficiencyClasses, (string)ViewBag.DefaultEnergyEfficiencyClass) + @Html.ValidationMessageFor(m => m.EnergyEfficiencyClass) +
    + @Html.SmartLabelFor(m => m.CustomLabel0) + + @Html.EditorFor(m => m.CustomLabel0, new { placeholder = ViewBag.DefaultCustomLabel }) + @Html.ValidationMessageFor(m => m.CustomLabel0) +
    + @Html.SmartLabelFor(m => m.CustomLabel1) + + @Html.EditorFor(m => m.CustomLabel1, new { placeholder = ViewBag.DefaultCustomLabel }) + @Html.ValidationMessageFor(m => m.CustomLabel1) +
    + @Html.SmartLabelFor(m => m.CustomLabel2) + + @Html.EditorFor(m => m.CustomLabel2, new { placeholder = ViewBag.DefaultCustomLabel }) + @Html.ValidationMessageFor(m => m.CustomLabel2) +
    + @Html.SmartLabelFor(m => m.CustomLabel3) + + @Html.EditorFor(m => m.CustomLabel3, new { placeholder = ViewBag.DefaultCustomLabel }) + @Html.ValidationMessageFor(m => m.CustomLabel3) +
    + @Html.SmartLabelFor(m => m.CustomLabel4) + + @Html.EditorFor(m => m.CustomLabel4, new { placeholder = ViewBag.DefaultCustomLabel }) + @Html.ValidationMessageFor(m => m.CustomLabel4) +
    \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProfileConfiguration.cshtml b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProfileConfiguration.cshtml new file mode 100644 index 0000000000..5f41005fb9 --- /dev/null +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProfileConfiguration.cshtml @@ -0,0 +1,163 @@ +@using SmartStore.GoogleMerchantCenter.Providers +@using SmartStore.GoogleMerchantCenter.Models; +@model ProfileConfigurationModel +@{ + Layout = null; +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + @Html.SmartLabelFor(m => m.DefaultGoogleCategory) + + @Html.TextBoxFor(m => m.DefaultGoogleCategory, new { data_provide = "typeahead", placeholder = Model.DefaultGoogleCategory, style = "width: 98%", autocomplete = "off", + data_items = 18, data_source = Model.AvailableGoogleCategoriesAsJson }) + @Html.ValidationMessageFor(m => m.DefaultGoogleCategory) +
    + @Html.SmartLabelFor(m => m.ExpirationDays) + + @Html.EditorFor(m => m.ExpirationDays) + @Html.ValidationMessageFor(m => m.ExpirationDays) +
    + @Html.SmartLabelFor(m => m.Condition) + + @Html.DropDownList("Condition", new List + { + new SelectListItem { Text = T("Common.Auto"), Value = "" }, + new SelectListItem { Text = T("Common.Unspecified"), Value = GmcXmlExportProvider.Unspecified }, + new SelectListItem { Text = T("Plugins.Feed.Froogle.ConditionNew"), Value = "new" }, + new SelectListItem { Text = T("Plugins.Feed.Froogle.ConditionUsed"), Value = "used" }, + new SelectListItem { Text = T("Plugins.Feed.Froogle.ConditionRefurbished"), Value = "refurbished" } + }) + @Html.ValidationMessageFor(m => m.Condition) +
    + @Html.SmartLabelFor(m => m.Availability) + + @Html.DropDownList("Availability", new List + { + new SelectListItem { Text = T("Common.Auto"), Value = "" }, + new SelectListItem { Text = T("Common.Unspecified"), Value = GmcXmlExportProvider.Unspecified }, + new SelectListItem { Text = T("Plugins.Feed.Froogle.AvailabilityInStock"), Value = "in stock" }, + new SelectListItem { Text = T("Plugins.Feed.Froogle.AvailabilityOutOfStock"), Value = "out of stock" }, + new SelectListItem { Text = T("Plugins.Feed.Froogle.AvailabilityPreorder"), Value = "preorder" } + }) + @Html.ValidationMessageFor(m => m.Availability) +
    + @Html.SmartLabelFor(m => m.Gender) + + @Html.DropDownList("Gender", new List + { + new SelectListItem { Text = T("Common.Auto"), Value = "" }, + new SelectListItem { Text = T("Common.Unspecified"), Value = GmcXmlExportProvider.Unspecified }, + new SelectListItem { Text = T("Plugins.Feed.Froogle.GenderMale"), Value = "male" }, + new SelectListItem { Text = T("Plugins.Feed.Froogle.GenderFemale"), Value = "female" }, + new SelectListItem { Text = T("Plugins.Feed.Froogle.GenderUnisex"), Value = "unisex" } + }) + @Html.ValidationMessageFor(m => m.Gender) +
    + @Html.SmartLabelFor(m => m.AgeGroup) + + @Html.DropDownList("AgeGroup", new List + { + new SelectListItem { Text = T("Common.Auto"), Value = "" }, + new SelectListItem { Text = T("Common.Unspecified"), Value = GmcXmlExportProvider.Unspecified }, + new SelectListItem { Text = T("Plugins.Feed.Froogle.AgeGroupAdult"), Value = "adult" }, + new SelectListItem { Text = T("Plugins.Feed.Froogle.AgeGroupKids"), Value = "kids" } + }) + @Html.ValidationMessageFor(m => m.AgeGroup) +
    + @Html.SmartLabelFor(m => m.Color) + + @Html.EditorFor(m => m.Color) + @Html.ValidationMessageFor(m => m.Color) +
    + @Html.SmartLabelFor(m => m.Size) + + @Html.EditorFor(m => m.Size) + @Html.ValidationMessageFor(m => m.Size) +
    + @Html.SmartLabelFor(m => m.Material) + + @Html.EditorFor(m => m.Material) + @Html.ValidationMessageFor(m => m.Material) +
    + @Html.SmartLabelFor(m => m.Pattern) + + @Html.EditorFor(m => m.Pattern) + @Html.ValidationMessageFor(m => m.Pattern) +
    + @Html.SmartLabelFor(m => m.AdditionalImages) + + @Html.EditorFor(m => m.AdditionalImages) + @Html.ValidationMessageFor(m => m.AdditionalImages) +
    + @Html.SmartLabelFor(m => m.SpecialPrice) + + @Html.EditorFor(m => m.SpecialPrice) + @Html.ValidationMessageFor(m => m.SpecialPrice) +
    + @Html.SmartLabelFor(m => m.ExportShipping) + + @Html.EditorFor(m => m.ExportShipping) + @Html.ValidationMessageFor(m => m.ExportShipping) +
    + @Html.SmartLabelFor(m => m.ExportBasePrice) + + @Html.EditorFor(m => m.ExportBasePrice) + @Html.ValidationMessageFor(m => m.ExportBasePrice) +
    \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/Web.config b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/Web.config index 31dc8df754..e9d36a3c51 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/Views/Web.config +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/Views/Web.config @@ -14,7 +14,7 @@ - + diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/changelog.md b/src/Plugins/SmartStore.GoogleMerchantCenter/changelog.md index 550a335079..2c1f7000cf 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/changelog.md +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/changelog.md @@ -1,13 +1,39 @@ -#Release Notes# +#Release Notes -##Google Merchant Center (GMC) 2.2.0.2## +##Google Merchant Center (GMC) 2.6.0.1 + ###Bugfixes + * Id should be unique when exporting attribute combinations as products + * No special price exported when the special price period was not specified + +##Google Merchant Center (GMC) 2.5.0.1 +###Bugfixes +* GMC feed does not generate the sale price if the sale price is set for a future date + +##Google Merchant Center (GMC) 2.2.0.5 +###New Features +* Supports GMC fields: multipack, bundle, adult, energy efficiency class and custom label (0 to 4) +* Export of availability date +###Improvements +* Removed "online_only" because it's not part of the GMC feed specification anymore + +##Google Merchant Center (GMC) 2.2.0.4 +###Improvements +* Supports new export framework +###Bugfixes +* Availability value "available for order" deprecated + +##Google Merchant Center (GMC) 2.2.0.3 +###Bugfixes +* Include\exclude option in product tab should initially be activated + +##Google Merchant Center (GMC) 2.2.0.2 ###Improvements * Supporting of paged google-product data query for SQL-Server Compact Edition -##Google Merchant Center (GMC) 2.2.0.1## +##Google Merchant Center (GMC) 2.2.0.1 ###New Features * #582 Option to include\exclude a product -##Google Merchant Center (GMC) 2.2.0## +##Google Merchant Center (GMC) 2.2.0 ###Improvements * Paged product query to reduce memory payload \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/packages.config b/src/Plugins/SmartStore.GoogleMerchantCenter/packages.config index 098fe232fa..3527aa9a5c 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/packages.config +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/packages.config @@ -1,12 +1,12 @@  - - - - + + + + - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.GoogleMerchantCenter/web.config b/src/Plugins/SmartStore.GoogleMerchantCenter/web.config index 46b8ba77d4..ba87d0f098 100644 --- a/src/Plugins/SmartStore.GoogleMerchantCenter/web.config +++ b/src/Plugins/SmartStore.GoogleMerchantCenter/web.config @@ -1,117 +1,117 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - \ No newline at end of file + diff --git a/src/Plugins/SmartStore.OfflinePayment/Controllers/OfflinePaymentController.cs b/src/Plugins/SmartStore.OfflinePayment/Controllers/OfflinePaymentController.cs index 468940bc74..14ef7c989e 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Controllers/OfflinePaymentController.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Controllers/OfflinePaymentController.cs @@ -14,6 +14,7 @@ using SmartStore.Services.Stores; using SmartStore.Web.Framework; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.Settings; namespace SmartStore.OfflinePayment.Controllers @@ -33,14 +34,22 @@ public OfflinePaymentController( this._services = services; this._storeService = storeService; this._ctx = ctx; - - T = NullLocalizer.Instance; } - public Localizer T { get; set; } - #region Global + private List GetTransactModes() + { + var list = new List + { + new SelectListItem { Text = T("Enums.SmartStore.Core.Domain.Payments.PaymentStatus.Pending"), Value = ((int)TransactMode.Pending).ToString() }, + new SelectListItem { Text = T("Enums.SmartStore.Core.Domain.Payments.PaymentStatus.Authorized"), Value = ((int)TransactMode.Authorize).ToString() }, + new SelectListItem { Text = T("Enums.SmartStore.Core.Domain.Payments.PaymentStatus.Paid"), Value = ((int)TransactMode.Paid).ToString() } + }; + + return list; + } + [NonAction] private TModel ConfigureGet(Action fn = null) where TModel : ConfigurationModelBase, new() @@ -195,6 +204,10 @@ public override ProcessPaymentRequest GetPaymentInfo(FormCollection form) paymentInfo.DirectDebitCountry = form["DirectDebitCountry"]; paymentInfo.DirectDebitIban = form["DirectDebitIban"]; } + else if (type == "PurchaseOrderNumber") + { + paymentInfo.PurchaseOrderNumber = form["PurchaseOrderNumber"]; + } } return paymentInfo; @@ -233,6 +246,10 @@ public override string GetPaymentSummary(FormCollection form) return number.Mask(8); } } + else if (type == "PurchaseOrderNumber") + { + return form["PurchaseOrderNumber"]; + } } return null; @@ -409,7 +426,17 @@ public ActionResult ManualConfigure() var model = ConfigureGet((m, s) => { m.TransactMode = s.TransactMode; - m.TransactModeValues = s.TransactMode.ToSelectList(); + m.TransactModeValues = GetTransactModes(); + m.ExcludedCreditCards = s.ExcludedCreditCards.SplitSafe(","); + + m.AvailableCreditCards = ManualProvider.CreditCardTypes + .Select(x => new SelectListItem + { + Text = x.Text, + Value = x.Value, + Selected = m.ExcludedCreditCards.Contains(x.Value) + }) + .ToList(); }); return View(model); @@ -424,8 +451,7 @@ public ActionResult ManualConfigure(ManualConfigurationModel model, FormCollecti ConfigurePost(model, form, s => { s.TransactMode = model.TransactMode; - - model.TransactModeValues = s.TransactMode.ToSelectList(); + s.ExcludedCreditCards = string.Join(",", model.ExcludedCreditCards ?? new string[0]); }); return ManualConfigure(); @@ -433,50 +459,35 @@ public ActionResult ManualConfigure(ManualConfigurationModel model, FormCollecti public ActionResult ManualPaymentInfo() { - var model = PaymentInfoGet(); - - // CC types - model.CreditCardTypes.Add(new SelectListItem() - { - Text = "Visa", - Value = "Visa", - }); - model.CreditCardTypes.Add(new SelectListItem() - { - Text = "Master card", - Value = "MasterCard", - }); - model.CreditCardTypes.Add(new SelectListItem() + var model = PaymentInfoGet((m, s) => { - Text = "Discover", - Value = "Discover", - }); - model.CreditCardTypes.Add(new SelectListItem() - { - Text = "Amex", - Value = "Amex", + var excludedCreditCards = s.ExcludedCreditCards.SplitSafe(","); + + foreach (var creditCard in ManualProvider.CreditCardTypes) + { + if (!excludedCreditCards.Any(x => x.IsCaseInsensitiveEqual(creditCard.Value))) + { + m.CreditCardTypes.Add(new SelectListItem + { + Text = creditCard.Text, + Value = creditCard.Value + }); + } + } }); // years for (int i = 0; i < 15; i++) { string year = Convert.ToString(DateTime.Now.Year + i); - model.ExpireYears.Add(new SelectListItem() - { - Text = year, - Value = year, - }); + model.ExpireYears.Add(new SelectListItem { Text = year, Value = year }); } // months for (int i = 1; i <= 12; i++) { string text = (i < 10) ? "0" + i.ToString() : i.ToString(); - model.ExpireMonths.Add(new SelectListItem() - { - Text = text, - Value = i.ToString(), - }); + model.ExpireMonths.Add(new SelectListItem { Text = text, Value = i.ToString() }); } // set postback values @@ -484,6 +495,7 @@ public ActionResult ManualPaymentInfo() model.CardholderName = form["CardholderName"]; model.CardNumber = form["CardNumber"]; model.CardCode = form["CardCode"]; + var selectedCcType = model.CreditCardTypes.Where(x => x.Value.Equals(form["CreditCardType"], StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); if (selectedCcType != null) selectedCcType.Selected = true; @@ -499,5 +511,38 @@ public ActionResult ManualPaymentInfo() #endregion - } + #region PurchaseOrderNumber + + [AdminAuthorize] + [ChildActionOnly] + public ActionResult PurchaseOrderNumberConfigure() + { + var model = ConfigureGet(); + + return View("GenericConfigure", model); + } + + [HttpPost, AdminAuthorize, ChildActionOnly, ValidateInput(false)] + public ActionResult PurchaseOrderNumberConfigure(PurchaseOrderNumberConfigurationModel model, FormCollection form) + { + if (!ModelState.IsValid) + return InvoiceConfigure(); + + ConfigurePost(model, form); + + return PurchaseOrderNumberConfigure(); + } + + public ActionResult PurchaseOrderNumberPaymentInfo() + { + var model = PaymentInfoGet(); + + var form = this.GetPaymentData(); + model.PurchaseOrderNumber = form["PurchaseOrderNumber"]; + + return PartialView("PurchaseOrderNumberPaymentInfo", model); + } + + #endregion + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/Description.txt b/src/Plugins/SmartStore.OfflinePayment/Description.txt index 869d27cb75..db6f0241f9 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Description.txt +++ b/src/Plugins/SmartStore.OfflinePayment/Description.txt @@ -2,8 +2,8 @@ Description: Contains common offline payment methods like Direct Debit, Invoice, Prepayment etc. Group: Payment SystemName: SmartStore.OfflinePayment -Version: 2.2.0 -MinAppVersion: 2.2.0 +Version: 2.6.0 +MinAppVersion: 2.5.0 DisplayOrder: 0 FileName: SmartStore.OfflinePayment.dll ResourceRootKey: Plugins.SmartStore.OfflinePayment diff --git a/src/Plugins/SmartStore.OfflinePayment/Localization/resources.de-de.xml b/src/Plugins/SmartStore.OfflinePayment/Localization/resources.de-de.xml index 2effbe1ecb..a9f2235ba1 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.OfflinePayment/Localization/resources.de-de.xml @@ -25,7 +25,7 @@ Zusätzliche prozentuale Gebühr zum Gesamtbetrag. Ein fester Wert wird verwendet, falls diese Option nicht aktiviert ist. - + Nachnahme @@ -122,10 +122,16 @@ - Markiere die Zahlung nach dem Abschluß der Bestellung als + Zahlungsstatus nach Bestellabschluss - Bestimmen Sie den Transaktionsmodus. + Legt den Zahlungsstatus nach Bestellabschluss fest. + + + Auszuschließende Kreditkarten + + + Kreditkarten mit denen Kunden nicht zahlen dürfen.
    @@ -211,5 +217,21 @@
    + + + Bestellnummer + + + + + + + + + + Bestellnummer + + +
    \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/Localization/resources.en-us.xml b/src/Plugins/SmartStore.OfflinePayment/Localization/resources.en-us.xml index 7f21d92f8a..af3d0cc477 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.OfflinePayment/Localization/resources.en-us.xml @@ -33,7 +33,7 @@ @@ -54,7 +54,7 @@ @@ -96,7 +96,7 @@ @@ -115,10 +115,16 @@
    - After checkout mark payment as + Payment status after order completion - Specify transaction mode. + Specifies the payment status after order completion. + + + Excluded credit cards + + + Credit cards that customers may not pay with.
    @@ -137,7 +143,7 @@ @@ -207,5 +213,21 @@ + + + Purchase order + + + + + + + + + + PO Number + + +
    \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/Models/ConfigurationModel.cs b/src/Plugins/SmartStore.OfflinePayment/Models/ConfigurationModel.cs index 5a2b5690b5..ba59935fdb 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Models/ConfigurationModel.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Models/ConfigurationModel.cs @@ -2,7 +2,7 @@ using System.Web.Mvc; using SmartStore.OfflinePayment.Settings; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.OfflinePayment.Models { @@ -35,7 +35,11 @@ public class ManualConfigurationModel : ConfigurationModelBase { [SmartResourceDisplayName("Plugins.Payments.Manual.Fields.TransactMode")] public TransactMode TransactMode { get; set; } - public SelectList TransactModeValues { get; set; } + public List TransactModeValues { get; set; } + + [SmartResourceDisplayName("Plugins.Payments.Manual.ExcludedCreditCards")] + public string[] ExcludedCreditCards { get; set; } + public List AvailableCreditCards { get; set; } } public class PayInStoreConfigurationModel : ConfigurationModelBase @@ -45,4 +49,8 @@ public class PayInStoreConfigurationModel : ConfigurationModelBase public class PrepaymentConfigurationModel : ConfigurationModelBase { } + + public class PurchaseOrderNumberConfigurationModel : ConfigurationModelBase + { + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/Models/PaymentInfoModel.cs b/src/Plugins/SmartStore.OfflinePayment/Models/PaymentInfoModel.cs index a48704604d..b13dd98de0 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Models/PaymentInfoModel.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Models/PaymentInfoModel.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.OfflinePayment.Models { @@ -97,4 +97,10 @@ public class PrepaymentPaymentInfoModel : PaymentInfoModelBase { } + public class PurchaseOrderNumberPaymentInfoModel : PaymentInfoModelBase + { + [SmartResourceDisplayName("Plugins.Payment.PurchaseOrder.PurchaseOrderNumber")] + [AllowHtml] + public string PurchaseOrderNumber { get; set; } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/Plugin.cs b/src/Plugins/SmartStore.OfflinePayment/Plugin.cs index 1fd3c082ee..f496f22698 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Plugin.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Plugin.cs @@ -41,12 +41,18 @@ public override void Install() }); settings.SaveSetting(new ManualPaymentSettings { + DescriptionText = "@Plugins.Payments.Manual.PaymentInfoDescription", TransactMode = TransactMode.Pending }); settings.SaveSetting(new DirectDebitPaymentSettings { DescriptionText = "@Plugins.Payments.DirectDebit.PaymentInfoDescription" }); + settings.SaveSetting(new PurchaseOrderNumberPaymentSettings + { + DescriptionText = "@Plugins.Payments.PurchaseOrderNumber.PaymentInfoDescription", + TransactMode = TransactMode.Pending + }); // add resources loc.ImportPluginResourcesFromXml(this.PluginDescriptor); diff --git a/src/Plugins/SmartStore.OfflinePayment/Providers/ManualProvider.cs b/src/Plugins/SmartStore.OfflinePayment/Providers/ManualProvider.cs index edbfbb52d1..4f4331c2eb 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Providers/ManualProvider.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Providers/ManualProvider.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.Web.Mvc; using SmartStore.Core.Domain.Payments; using SmartStore.Core.Plugins; using SmartStore.OfflinePayment.Settings; @@ -10,12 +12,28 @@ namespace SmartStore.OfflinePayment [DisplayOrder(1)] public class ManualProvider : OfflinePaymentProviderBase, IConfigurable { + public static List CreditCardTypes + { + get + { + var creditCardTypes = new List + { + new SelectListItem { Text = "Visa", Value = "Visa" }, + new SelectListItem { Text = "Master Card", Value = "MasterCard" }, + new SelectListItem { Text = "Discover", Value = "Discover" }, + new SelectListItem { Text = "Amex", Value = "Amex" } + }; + return creditCardTypes; + } + } + public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest processPaymentRequest) { var result = new ProcessPaymentResult(); var settings = CommonServices.Settings.LoadSetting(processPaymentRequest.StoreId); result.AllowStoringCreditCardNumber = true; + switch (settings.TransactMode) { case TransactMode.Pending: @@ -24,14 +42,12 @@ public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest proces case TransactMode.Authorize: result.NewPaymentStatus = PaymentStatus.Authorized; break; - case TransactMode.AuthorizeAndCapture: + case TransactMode.Paid: result.NewPaymentStatus = PaymentStatus.Paid; break; default: - { - result.AddError(T("Common.Payment.TranactionTypeNotSupported")); - return result; - } + result.AddError(T("Common.Payment.TranactionTypeNotSupported")); + return result; } return result; @@ -51,7 +67,7 @@ public override ProcessPaymentResult ProcessRecurringPayment(ProcessPaymentReque case TransactMode.Authorize: result.NewPaymentStatus = PaymentStatus.Authorized; break; - case TransactMode.AuthorizeAndCapture: + case TransactMode.Paid: result.NewPaymentStatus = PaymentStatus.Paid; break; default: diff --git a/src/Plugins/SmartStore.OfflinePayment/Providers/PurchaseOrderNumberProvider.cs b/src/Plugins/SmartStore.OfflinePayment/Providers/PurchaseOrderNumberProvider.cs new file mode 100644 index 0000000000..530dfd1d3a --- /dev/null +++ b/src/Plugins/SmartStore.OfflinePayment/Providers/PurchaseOrderNumberProvider.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Web.Routing; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Payments; +using SmartStore.Core.Plugins; +using SmartStore.OfflinePayment.Settings; +using SmartStore.Services.Configuration; +using SmartStore.Services.Localization; +using SmartStore.Services.Payments; + +namespace SmartStore.OfflinePayment +{ + [SystemName("SmartStore.PurchaseOrderNumber")] + [FriendlyName("Purchase Order Number")] + [DisplayOrder(10)] + public class PurchaseOrderNumberProvider : OfflinePaymentProviderBase, IConfigurable + { + private readonly ISettingService _settingService; + private readonly ILocalizationService _localizationService; + + public PurchaseOrderNumberProvider(ISettingService settingService, ILocalizationService localizationService) + { + _settingService = settingService; + _localizationService = localizationService; + } + + public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest processPaymentRequest) + { + var result = new ProcessPaymentResult(); + var settings = CommonServices.Settings.LoadSetting(processPaymentRequest.StoreId); + + result.AllowStoringCreditCardNumber = true; + switch (settings.TransactMode) + { + case TransactMode.Pending: + result.NewPaymentStatus = PaymentStatus.Pending; + break; + case TransactMode.Authorize: + result.NewPaymentStatus = PaymentStatus.Authorized; + break; + case TransactMode.Paid: + result.NewPaymentStatus = PaymentStatus.Paid; + break; + default: + { + result.AddError(T("Common.Payment.TranactionTypeNotSupported")); + return result; + } + } + + return result; + } + + public override bool RequiresInteraction + { + get + { + return true; + } + } + + protected override string GetActionPrefix() + { + return "PurchaseOrderNumber"; + } + + } +} diff --git a/src/Plugins/SmartStore.OfflinePayment/RouteProvider.cs b/src/Plugins/SmartStore.OfflinePayment/RouteProvider.cs index c0cf43ddd5..70a8e63318 100644 --- a/src/Plugins/SmartStore.OfflinePayment/RouteProvider.cs +++ b/src/Plugins/SmartStore.OfflinePayment/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.OfflinePayment { diff --git a/src/Plugins/SmartStore.OfflinePayment/Settings/OfflinePaymentSettings.cs b/src/Plugins/SmartStore.OfflinePayment/Settings/OfflinePaymentSettings.cs index 7344b3ca56..52fbe9bc18 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Settings/OfflinePaymentSettings.cs +++ b/src/Plugins/SmartStore.OfflinePayment/Settings/OfflinePaymentSettings.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Generic; using SmartStore.Core.Configuration; namespace SmartStore.OfflinePayment.Settings @@ -27,8 +25,14 @@ public class InvoicePaymentSettings : PaymentSettingsBase, ISettings public class ManualPaymentSettings : PaymentSettingsBase, ISettings { public TransactMode TransactMode { get; set; } + public string ExcludedCreditCards { get; set; } } + public class PurchaseOrderNumberPaymentSettings : PaymentSettingsBase, ISettings + { + public TransactMode TransactMode { get; set; } + } + public class PayInStorePaymentSettings : PaymentSettingsBase, ISettings { } @@ -40,19 +44,21 @@ public class PrepaymentPaymentSettings : PaymentSettingsBase, ISettings /// /// Represents manual payment processor transaction mode /// - public enum TransactMode : int + public enum TransactMode { /// /// Pending /// Pending = 0, + /// /// Authorize /// Authorize = 1, + /// - /// Authorize and capture + /// Paid /// - AuthorizeAndCapture = 2 + Paid = 2 } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/SmartStore.OfflinePayment.csproj b/src/Plugins/SmartStore.OfflinePayment/SmartStore.OfflinePayment.csproj index 015c3b66c5..9083d62e99 100644 --- a/src/Plugins/SmartStore.OfflinePayment/SmartStore.OfflinePayment.csproj +++ b/src/Plugins/SmartStore.OfflinePayment/SmartStore.OfflinePayment.csproj @@ -42,6 +42,7 @@ + true @@ -81,19 +82,18 @@ MinimumRecommendedRules.ruleset - - ..\..\packages\Autofac.3.4.1\lib\net40\Autofac.dll - False + + ..\..\packages\Autofac.3.5.2\lib\net40\Autofac.dll - - ..\..\packages\FluentValidation.5.0.0.1\lib\Net40\FluentValidation.dll - False + + ..\..\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 @@ -145,6 +145,7 @@ + @@ -214,6 +215,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + diff --git a/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/ManualConfigure.cshtml b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/ManualConfigure.cshtml index a8bbca8460..89604eb07f 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/ManualConfigure.cshtml +++ b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/ManualConfigure.cshtml @@ -1,6 +1,5 @@ @model SmartStore.OfflinePayment.Models.ManualConfigurationModel @using SmartStore.Web.Framework; - @{ Layout = ""; } @@ -16,9 +15,19 @@
    @Html.SettingOverrideCheckbox(model => model.TransactMode) - @Html.DropDownList("TransactMode", Model.TransactModeValues) + @Html.DropDownListFor(model => model.TransactMode, Model.TransactModeValues)
    + @Html.SmartLabelFor(model => model.ExcludedCreditCards) + + @Html.SettingOverrideCheckbox(model => model.ExcludedCreditCards) + @Html.ListBoxFor(x => x.ExcludedCreditCards, new MultiSelectList(Model.AvailableCreditCards, "Value", "Text"), new { multiple = "multiple" }) + @Html.ValidationMessageFor(model => model.ExcludedCreditCards) +
    @Html.SmartLabelFor(model => model.AdditionalFee) diff --git a/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/ManualPaymentInfo.Mobile.cshtml b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/ManualPaymentInfo.Mobile.cshtml index 49fa227e1e..dc1a947aab 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/ManualPaymentInfo.Mobile.cshtml +++ b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/ManualPaymentInfo.Mobile.cshtml @@ -7,7 +7,7 @@ @Html.Hidden("OfflinePaymentMethodType", "Manual") @Html.SmartLabelFor(model => model.CreditCardTypes, false) -@Html.DropDownListFor(model => model.CreditCardType, Model.CreditCardTypes) +@Html.DropDownListFor(model => model.CreditCardType, Model.CreditCardTypes, new { data_native_menu = "false" }) @Html.SmartLabelFor(model => model.CardholderName, false) @Html.TextBoxFor(model => model.CardholderName, new { autocomplete = "off" }) @Html.ValidationMessageFor(model => model.CardholderName) @@ -17,9 +17,9 @@ @Html.SmartLabelFor(model => model.ExpireMonth, false)
    - @Html.DropDownListFor(model => model.ExpireMonth, Model.ExpireMonths) + @Html.DropDownListFor(model => model.ExpireMonth, Model.ExpireMonths, new { data_native_menu = "false" }) / - @Html.DropDownListFor(model => model.ExpireYear, Model.ExpireYears) + @Html.DropDownListFor(model => model.ExpireYear, Model.ExpireYears, new { data_native_menu = "false" })
    @Html.SmartLabelFor(model => model.CardCode, false) diff --git a/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/PurchaseOrderNumberPaymentInfo.Mobile.cshtml b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/PurchaseOrderNumberPaymentInfo.Mobile.cshtml new file mode 100644 index 0000000000..87191ec5e0 --- /dev/null +++ b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/PurchaseOrderNumberPaymentInfo.Mobile.cshtml @@ -0,0 +1,10 @@ +@{ + Layout = ""; +} +@using SmartStore.Web.Framework; +@using SmartStore.OfflinePayment.Models; +@model PurchaseOrderNumberPaymentInfoModel + +@Html.SmartLabelFor(model => model.PurchaseOrderNumber, false) +@Html.TextBoxFor(model => model.PurchaseOrderNumber, new { autocomplete = "off" }) +@Html.ValidationMessageFor(model => model.PurchaseOrderNumber) diff --git a/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/PurchaseOrderNumberPaymentInfo.cshtml b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/PurchaseOrderNumberPaymentInfo.cshtml new file mode 100644 index 0000000000..0d21481f2d --- /dev/null +++ b/src/Plugins/SmartStore.OfflinePayment/Views/OfflinePayment/PurchaseOrderNumberPaymentInfo.cshtml @@ -0,0 +1,20 @@ +@using SmartStore.Web.Framework; +@using SmartStore.OfflinePayment.Models; + +@model PurchaseOrderNumberPaymentInfoModel + +@{ + Layout = ""; +} + +@Html.Hidden("OfflinePaymentMethodType", "PurchaseOrderNumber") + +
    +
    + @Html.LabelFor(model => model.PurchaseOrderNumber, new { @class="control-label required" }) +
    + @Html.TextBoxFor(model => model.PurchaseOrderNumber, new { style = "width: 165px;", autocomplete = "off" }) + @Html.ValidationMessageFor(model => model.PurchaseOrderNumber) +
    +
    +
    \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/Views/Web.config b/src/Plugins/SmartStore.OfflinePayment/Views/Web.config index 31dc8df754..e9d36a3c51 100644 --- a/src/Plugins/SmartStore.OfflinePayment/Views/Web.config +++ b/src/Plugins/SmartStore.OfflinePayment/Views/Web.config @@ -14,7 +14,7 @@ - + diff --git a/src/Plugins/SmartStore.OfflinePayment/changelog.md b/src/Plugins/SmartStore.OfflinePayment/changelog.md index b577e2d27c..a7d6c374d2 100644 --- a/src/Plugins/SmartStore.OfflinePayment/changelog.md +++ b/src/Plugins/SmartStore.OfflinePayment/changelog.md @@ -1,5 +1,9 @@ -#Release Notes# +#Release Notes -##Offline Payment Methods 1.1## -###Improvements### +##Offline Payment Methods 2.5.0 +###New Features +* Option to exclude credit card types + +##Offline Payment Methods 1.1 +###Improvements * Multistore configuration \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/packages.config b/src/Plugins/SmartStore.OfflinePayment/packages.config index d24ebf5f1f..5669aa5ceb 100644 --- a/src/Plugins/SmartStore.OfflinePayment/packages.config +++ b/src/Plugins/SmartStore.OfflinePayment/packages.config @@ -1,10 +1,10 @@  - - + + - + \ No newline at end of file diff --git a/src/Plugins/SmartStore.OfflinePayment/web.config b/src/Plugins/SmartStore.OfflinePayment/web.config index 46b8ba77d4..ba87d0f098 100644 --- a/src/Plugins/SmartStore.OfflinePayment/web.config +++ b/src/Plugins/SmartStore.OfflinePayment/web.config @@ -1,117 +1,117 @@ - + - - + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - \ No newline at end of file + diff --git a/src/Plugins/SmartStore.PayPal/Content/favicon.png b/src/Plugins/SmartStore.PayPal/Content/favicon.png new file mode 100644 index 0000000000..aa29944a07 Binary files /dev/null and b/src/Plugins/SmartStore.PayPal/Content/favicon.png differ diff --git a/src/Plugins/SmartStore.PayPal/Content/images/logo200x53.png b/src/Plugins/SmartStore.PayPal/Content/images/logo200x53.png deleted file mode 100644 index a23fcb7a0b..0000000000 Binary files a/src/Plugins/SmartStore.PayPal/Content/images/logo200x53.png and /dev/null differ diff --git a/src/Plugins/SmartStore.PayPal/Content/smartstore.paypal.css b/src/Plugins/SmartStore.PayPal/Content/smartstore.paypal.css index eb06a6cd69..06e549454c 100644 --- a/src/Plugins/SmartStore.PayPal/Content/smartstore.paypal.css +++ b/src/Plugins/SmartStore.PayPal/Content/smartstore.paypal.css @@ -1,7 +1,7 @@ #paypal-checkout .selection-text { text-align: right; font-weight: bold; - padding: 12px 15px 5px 0; + padding: 12px 15px 0 0; } #paypal-checkout .form-horizontal { @@ -11,19 +11,9 @@ #paypal-checkout #paypal-express-button { cursor: pointer; } -.configure-paypal-direct .logo, -.configure-paypal-express .logo, -.configure-paypal-standard .logo -.paypal-standard-public .logo{ - width: 200px; - height: 53px; -} .paypal-standard-public { margin-bottom: 10px; } -.button-save { - padding-top: 10px; -} .form-horizontal.paypal-direct #s2id_CreditCardType{ min-width: 180px; } diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalControllerBase.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalControllerBase.cs new file mode 100644 index 0000000000..06d2c94600 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalControllerBase.cs @@ -0,0 +1,325 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Web; +using System.Web.Mvc; +using SmartStore.Core.Configuration; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Payments; +using SmartStore.Core.Logging; +using SmartStore.PayPal.Settings; +using SmartStore.Services.Orders; +using SmartStore.Services.Payments; +using SmartStore.Web.Framework.Controllers; + +namespace SmartStore.PayPal.Controllers +{ + public abstract class PayPalControllerBase : PaymentControllerBase where TSetting : PayPalSettingsBase, ISettings, new() + { + public PayPalControllerBase( + string systemName, + IPaymentService paymentService, + IOrderService orderService, + IOrderProcessingService orderProcessingService) + { + SystemName = systemName; + PaymentService = paymentService; + OrderService = orderService; + OrderProcessingService = orderProcessingService; + } + + protected string SystemName { get; private set; } + protected IPaymentService PaymentService { get; private set; } + protected IOrderService OrderService { get; private set; } + protected IOrderProcessingService OrderProcessingService { get; private set; } + + protected PaymentStatus GetPaymentStatus(string paymentStatus, string pendingReason, decimal payPalTotal, decimal orderTotal) + { + var result = PaymentStatus.Pending; + + if (paymentStatus == null) + paymentStatus = string.Empty; + + if (pendingReason == null) + pendingReason = string.Empty; + + switch (paymentStatus.ToLowerInvariant()) + { + case "pending": + switch (pendingReason.ToLowerInvariant()) + { + case "authorization": + result = PaymentStatus.Authorized; + break; + default: + result = PaymentStatus.Pending; + break; + } + break; + case "processed": + case "completed": + case "canceled_reversal": + result = PaymentStatus.Paid; + break; + case "denied": + case "expired": + case "failed": + case "voided": + result = PaymentStatus.Voided; + break; + case "reversed": + result = PaymentStatus.Refunded; + break; + case "refunded": + if ((Math.Abs(orderTotal) - Math.Abs(payPalTotal)) > decimal.Zero) + result = PaymentStatus.PartiallyRefunded; + else + result = PaymentStatus.Refunded; + break; + default: + break; + } + return result; + } + + protected bool VerifyIPN(PayPalSettingsBase settings, string formString, out Dictionary values) + { + // settings: multistore context not possible here. we need the custom value to determine what store it is. + + var request = settings.GetPayPalWebRequest(); + request.Method = "POST"; + request.ContentType = "application/x-www-form-urlencoded"; + request.UserAgent = Request.UserAgent; + + var formContent = string.Format("{0}&cmd=_notify-validate", formString); + request.ContentLength = formContent.Length; + + using (var sw = new StreamWriter(request.GetRequestStream(), Encoding.ASCII)) + { + sw.Write(formContent); + } + + string response = null; + using (var sr = new StreamReader(request.GetResponse().GetResponseStream())) + { + response = HttpUtility.UrlDecode(sr.ReadToEnd()); + } + + var success = response.Trim().Equals("VERIFIED", StringComparison.OrdinalIgnoreCase); + + values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var item in formString.SplitSafe("&")) + { + var line = HttpUtility.UrlDecode(item).TrimSafe(); + var equalIndex = line.IndexOf('='); + + if (equalIndex >= 0) + values.Add(line.Substring(0, equalIndex), line.Substring(equalIndex + 1)); + } + + return success; + } + + [ValidateInput(false)] + public ActionResult IPNHandler() + { + if (!PaymentService.IsPaymentMethodActive(SystemName, Services.StoreContext.CurrentStore.Id)) + throw new SmartException(T("Plugins.Payments.PayPal.NoModuleLoading")); + + var settings = Services.Settings.LoadSetting(); + byte[] param = Request.BinaryRead(Request.ContentLength); + //var strRequest = Encoding.ASCII.GetString(param); + var strRequest = Encoding.UTF8.GetString(param); + Dictionary values; + var sb = new StringBuilder(); + + if (VerifyIPN(settings, strRequest, out values)) + { + #region values + + decimal total = decimal.Zero; + try + { + total = decimal.Parse(values["mc_gross"], new CultureInfo("en-US")); + } + catch { } + + string payer_status = string.Empty; + values.TryGetValue("payer_status", out payer_status); + string payment_status = string.Empty; + values.TryGetValue("payment_status", out payment_status); + string pending_reason = string.Empty; + values.TryGetValue("pending_reason", out pending_reason); + string mc_currency = string.Empty; + values.TryGetValue("mc_currency", out mc_currency); + string txn_id = string.Empty; + values.TryGetValue("txn_id", out txn_id); + string txn_type = string.Empty; + values.TryGetValue("txn_type", out txn_type); + string rp_invoice_id = string.Empty; + values.TryGetValue("rp_invoice_id", out rp_invoice_id); + string payment_type = string.Empty; + values.TryGetValue("payment_type", out payment_type); + string payer_id = string.Empty; + values.TryGetValue("payer_id", out payer_id); + string receiver_id = string.Empty; + values.TryGetValue("receiver_id", out receiver_id); + string invoice = string.Empty; + values.TryGetValue("invoice", out invoice); + string payment_fee = string.Empty; + values.TryGetValue("payment_fee", out payment_fee); + + #endregion + + sb.AppendLine("PayPal IPN:"); + foreach (KeyValuePair kvp in values.Where(x => x.Value.HasValue())) + { + sb.AppendLine(kvp.Key + ": " + kvp.Value); + } + + switch (txn_type) + { + case "recurring_payment_profile_created": + //do nothing here + break; + case "recurring_payment": + #region Recurring payment + { + Guid orderNumberGuid = Guid.Empty; + try + { + orderNumberGuid = new Guid(rp_invoice_id); + } + catch { } + + var initialOrder = OrderService.GetOrderByGuid(orderNumberGuid); + if (initialOrder != null) + { + var newPaymentStatus = GetPaymentStatus(payment_status, pending_reason, total, initialOrder.OrderTotal); + var recurringPayments = OrderService.SearchRecurringPayments(0, 0, initialOrder.Id, null); + + foreach (var rp in recurringPayments) + { + switch (newPaymentStatus) + { + case PaymentStatus.Authorized: + case PaymentStatus.Paid: + { + var recurringPaymentHistory = rp.RecurringPaymentHistory; + if (recurringPaymentHistory.Count == 0) + { + //first payment + var rph = new RecurringPaymentHistory + { + RecurringPaymentId = rp.Id, + OrderId = initialOrder.Id, + CreatedOnUtc = DateTime.UtcNow + }; + rp.RecurringPaymentHistory.Add(rph); + OrderService.UpdateRecurringPayment(rp); + } + else + { + //next payments + OrderProcessingService.ProcessNextRecurringPayment(rp); + } + } + break; + } + } + + Logger.Information(T("Plugins.Payments.PayPal.IpnRecurringPaymentInfo"), new SmartException(sb.ToString())); + } + else + { + Logger.Error(T("Plugins.Payments.PayPal.IpnOrderNotFound"), new SmartException(sb.ToString())); + } + } + #endregion + break; + default: + #region Standard payment + { + var orderNumber = ""; + var orderNumberGuid = Guid.Empty; + values.TryGetValue("custom", out orderNumber); + + try + { + orderNumberGuid = new Guid(orderNumber); + } + catch { } + + var order = OrderService.GetOrderByGuid(orderNumberGuid); + if (order != null) + { + order.HasNewPaymentNotification = true; + + OrderService.AddOrderNote(order, sb.ToString()); + + if (settings.IpnChangesPaymentStatus) + { + var newPaymentStatus = GetPaymentStatus(payment_status, pending_reason, total, order.OrderTotal); + + switch (newPaymentStatus) + { + case PaymentStatus.Pending: + break; + case PaymentStatus.Authorized: + if (OrderProcessingService.CanMarkOrderAsAuthorized(order)) + { + OrderProcessingService.MarkAsAuthorized(order); + } + break; + case PaymentStatus.Paid: + if (OrderProcessingService.CanMarkOrderAsPaid(order)) + { + OrderProcessingService.MarkOrderAsPaid(order); + } + break; + case PaymentStatus.Refunded: + if (OrderProcessingService.CanRefundOffline(order)) + { + OrderProcessingService.RefundOffline(order); + } + break; + case PaymentStatus.PartiallyRefunded: + if (OrderProcessingService.CanPartiallyRefundOffline(order, Math.Abs(total))) + { + OrderProcessingService.PartiallyRefundOffline(order, Math.Abs(total)); + } + break; + case PaymentStatus.Voided: + if (OrderProcessingService.CanVoidOffline(order)) + { + OrderProcessingService.VoidOffline(order); + } + break; + default: + break; + } + } + } + else + { + Logger.Error(T("Plugins.Payments.PayPal.IpnOrderNotFound"), new SmartException(sb.ToString())); + } + } + #endregion + break; + } + } + else + { + Logger.Error(T("Plugins.Payments.PayPal.IpnFailed"), new SmartException(strRequest)); + } + + //nothing should be rendered to visitor + return Content(""); + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalDirectController.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalDirectController.cs index d0a5d8434d..abdf2df189 100644 --- a/src/Plugins/SmartStore.PayPal/Controllers/PayPalDirectController.cs +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalDirectController.cs @@ -1,14 +1,8 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; using System.Linq; -using System.Text; using System.Web.Mvc; -using Autofac; -using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; -using SmartStore.Core.Logging; using SmartStore.PayPal.Models; using SmartStore.PayPal.Services; using SmartStore.PayPal.Settings; @@ -16,59 +10,57 @@ using SmartStore.Services; using SmartStore.Services.Orders; using SmartStore.Services.Payments; -using SmartStore.Services.Stores; using SmartStore.Web.Framework.Controllers; -using SmartStore.Web.Framework.Plugins; +using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.Settings; namespace SmartStore.PayPal.Controllers { - public class PayPalDirectController : PaymentControllerBase + public class PayPalDirectController : PayPalControllerBase { - private readonly PluginHelper _helper; - private readonly IPaymentService _paymentService; - private readonly IOrderService _orderService; - private readonly IOrderProcessingService _orderProcessingService; private readonly ICommonServices _services; - private readonly IStoreService _storeService; public PayPalDirectController( - IPaymentService paymentService, IOrderService orderService, + IPaymentService paymentService, + IOrderService orderService, IOrderProcessingService orderProcessingService, PaymentSettings paymentSettings, - IComponentContext ctx, ICommonServices services, - IStoreService storeService) + ICommonServices services) : base( + PayPalDirectProvider.SystemName, + paymentService, + orderService, + orderProcessingService) { - _paymentService = paymentService; - _orderService = orderService; - _orderProcessingService = orderProcessingService; _services = services; - _storeService = storeService; - _helper = new PluginHelper(ctx, "SmartStore.PayPal", "Plugins.Payments.PayPalDirect"); } private SelectList TransactModeValues(TransactMode selected) { - return new SelectList(new List() + return new SelectList(new List { - new { ID = (int)TransactMode.Authorize, Name = _helper.GetResource("ModeAuth") }, - new { ID = (int)TransactMode.AuthorizeAndCapture, Name = _helper.GetResource("ModeAuthAndCapture") } - }, "ID", "Name", (int)selected); + new { ID = (int)TransactMode.Authorize, Name = T("Plugins.Payments.PayPalDirect.ModeAuth") }, + new { ID = (int)TransactMode.AuthorizeAndCapture, Name = T("Plugins.Payments.PayPalDirect.ModeAuthAndCapture") } + }, + "ID", "Name", (int)selected); } [AdminAuthorize, ChildActionOnly] public ActionResult Configure() { var model = new PayPalDirectConfigurationModel(); - int storeScope = this.GetActiveStoreScopeConfiguration(_storeService, _services.WorkContext); - var settings = _services.Settings.LoadSetting(storeScope); + int storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); model.Copy(settings, true); - model.TransactModeValues = TransactModeValues(settings.TransactMode); + model.TransactModeValues = TransactModeValues(settings.TransactMode); - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - storeDependingSettingHelper.GetOverrideKeys(settings, model, storeScope, _services.Settings); + model.AvailableSecurityProtocols = PayPalService.GetSecurityProtocols() + .Select(x => new SelectListItem { Value = ((int)x.Key).ToString(), Text = x.Value }) + .ToList(); + + var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); + storeDependingSettingHelper.GetOverrideKeys(settings, model, storeScope, Services.Settings); return View(model); } @@ -82,18 +74,18 @@ public ActionResult Configure(PayPalDirectConfigurationModel model, FormCollecti ModelState.Clear(); var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - int storeScope = this.GetActiveStoreScopeConfiguration(_storeService, _services.WorkContext); - var settings = _services.Settings.LoadSetting(storeScope); + int storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); model.Copy(settings, false); - storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, _services.Settings); + storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, Services.Settings); // multistore context not possible, see IPN handling - _services.Settings.SaveSetting(settings, x => x.UseSandbox, 0, false); + Services.Settings.SaveSetting(settings, x => x.UseSandbox, 0, false); - _services.Settings.ClearCache(); - NotifySuccess(_services.Localization.GetResource("Plugins.Payments.PayPal.ConfigSaveNote")); + Services.Settings.ClearCache(); + NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); return Configure(); } @@ -103,22 +95,22 @@ public ActionResult PaymentInfo() var model = new PayPalDirectPaymentInfoModel(); //CC types - model.CreditCardTypes.Add(new SelectListItem() + model.CreditCardTypes.Add(new SelectListItem { Text = "Visa", Value = "Visa", }); - model.CreditCardTypes.Add(new SelectListItem() + model.CreditCardTypes.Add(new SelectListItem { Text = "Master card", Value = "MasterCard", }); - model.CreditCardTypes.Add(new SelectListItem() + model.CreditCardTypes.Add(new SelectListItem { Text = "Discover", Value = "Discover", }); - model.CreditCardTypes.Add(new SelectListItem() + model.CreditCardTypes.Add(new SelectListItem { Text = "Amex", Value = "Amex", @@ -128,7 +120,7 @@ public ActionResult PaymentInfo() for (int i = 0; i < 15; i++) { string year = Convert.ToString(DateTime.Now.Year + i); - model.ExpireYears.Add(new SelectListItem() + model.ExpireYears.Add(new SelectListItem { Text = year, Value = year, @@ -139,7 +131,7 @@ public ActionResult PaymentInfo() for (int i = 1; i <= 12; i++) { string text = (i < 10) ? "0" + i.ToString() : i.ToString(); - model.ExpireMonths.Add(new SelectListItem() + model.ExpireMonths.Add(new SelectListItem { Text = text, Value = i.ToString(), @@ -151,6 +143,7 @@ public ActionResult PaymentInfo() model.CardholderName = form["CardholderName"]; model.CardNumber = form["CardNumber"]; model.CardCode = form["CardCode"]; + var selectedCcType = model.CreditCardTypes.Where(x => x.Value.Equals(form["CreditCardType"], StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); if (selectedCcType != null) selectedCcType.Selected = true; @@ -168,10 +161,9 @@ public ActionResult PaymentInfo() public override IList ValidatePaymentForm(FormCollection form) { var warnings = new List(); + var validator = new PaymentInfoValidator(Services.Localization); - //validate - var validator = new PaymentInfoValidator(_services.Localization); - var model = new PayPalDirectPaymentInfoModel() + var model = new PayPalDirectPaymentInfoModel { CardholderName = form["CardholderName"], CardNumber = form["CardNumber"], @@ -181,9 +173,10 @@ public override IList ValidatePaymentForm(FormCollection form) }; var validationResult = validator.Validate(model); - if (!validationResult.IsValid) - foreach (var error in validationResult.Errors) - warnings.Add(error.ErrorMessage); + if (!validationResult.IsValid) + { + validationResult.Errors.Each(x => warnings.Add(x.ErrorMessage)); + } return warnings; } @@ -191,12 +184,14 @@ public override IList ValidatePaymentForm(FormCollection form) public override ProcessPaymentRequest GetPaymentInfo(FormCollection form) { var paymentInfo = new ProcessPaymentRequest(); + paymentInfo.CreditCardType = form["CreditCardType"]; paymentInfo.CreditCardName = form["CardholderName"]; paymentInfo.CreditCardNumber = form["CardNumber"]; paymentInfo.CreditCardExpireMonth = int.Parse(form["ExpireMonth"]); paymentInfo.CreditCardExpireYear = int.Parse(form["ExpireYear"]); paymentInfo.CreditCardCvv2 = form["CardCode"]; + return paymentInfo; } @@ -204,221 +199,12 @@ public override ProcessPaymentRequest GetPaymentInfo(FormCollection form) public override string GetPaymentSummary(FormCollection form) { var number = form["CardNumber"]; - return "{0}, {1}, {2}".FormatCurrent( + + return "{0}, {1}, {2}".FormatInvariant( form["CreditCardType"], form["CardholderName"], number.Mask(4) ); } - - [ValidateInput(false)] - public ActionResult IPNHandler() - { - Debug.WriteLine("PayPal Direct IPN: {0}".FormatWith(Request.ContentLength)); - - byte[] param = Request.BinaryRead(Request.ContentLength); - string strRequest = Encoding.ASCII.GetString(param); - Dictionary values; - - var provider = _paymentService.LoadPaymentMethodBySystemName("Payments.PayPalDirect", true); - var processor = provider != null ? provider.Value as PayPalDirectProvider : null; - if (processor == null) - throw new SmartException(_helper.GetResource("NoModuleLoading")); - - if (processor.VerifyIPN(strRequest, out values)) - { - #region values - decimal total = decimal.Zero; - try - { - total = decimal.Parse(values["mc_gross"], new CultureInfo("en-US")); - } - catch { } - - string payer_status = string.Empty; - values.TryGetValue("payer_status", out payer_status); - string payment_status = string.Empty; - values.TryGetValue("payment_status", out payment_status); - string pending_reason = string.Empty; - values.TryGetValue("pending_reason", out pending_reason); - string mc_currency = string.Empty; - values.TryGetValue("mc_currency", out mc_currency); - string txn_id = string.Empty; - values.TryGetValue("txn_id", out txn_id); - string txn_type = string.Empty; - values.TryGetValue("txn_type", out txn_type); - string rp_invoice_id = string.Empty; - values.TryGetValue("rp_invoice_id", out rp_invoice_id); - string payment_type = string.Empty; - values.TryGetValue("payment_type", out payment_type); - string payer_id = string.Empty; - values.TryGetValue("payer_id", out payer_id); - string receiver_id = string.Empty; - values.TryGetValue("receiver_id", out receiver_id); - string invoice = string.Empty; - values.TryGetValue("invoice", out invoice); - string payment_fee = string.Empty; - values.TryGetValue("payment_fee", out payment_fee); - - #endregion - - var sb = new StringBuilder(); - sb.AppendLine("PayPal IPN:"); - foreach (KeyValuePair kvp in values) - { - sb.AppendLine(kvp.Key + ": " + kvp.Value); - } - - var newPaymentStatus = PayPalHelper.GetPaymentStatus(payment_status, pending_reason); - sb.AppendLine("{0}: {1}".FormatWith(_helper.GetResource("NewPaymentStatus"), newPaymentStatus)); - - switch (txn_type) - { - case "recurring_payment_profile_created": - //do nothing here - break; - case "recurring_payment": - #region Recurring payment - { - Guid orderNumberGuid = Guid.Empty; - try - { - orderNumberGuid = new Guid(rp_invoice_id); - } - catch - { - } - - var initialOrder = _orderService.GetOrderByGuid(orderNumberGuid); - if (initialOrder != null) - { - var recurringPayments = _orderService.SearchRecurringPayments(0, 0, initialOrder.Id, null); - foreach (var rp in recurringPayments) - { - switch (newPaymentStatus) - { - case PaymentStatus.Authorized: - case PaymentStatus.Paid: - { - var recurringPaymentHistory = rp.RecurringPaymentHistory; - if (recurringPaymentHistory.Count == 0) - { - //first payment - var rph = new RecurringPaymentHistory() - { - RecurringPaymentId = rp.Id, - OrderId = initialOrder.Id, - CreatedOnUtc = DateTime.UtcNow - }; - rp.RecurringPaymentHistory.Add(rph); - _orderService.UpdateRecurringPayment(rp); - } - else - { - //next payments - _orderProcessingService.ProcessNextRecurringPayment(rp); - } - } - break; - } - } - - //this.OrderService.InsertOrderNote(newOrder.OrderId, sb.ToString(), DateTime.UtcNow); - Logger.Information(_helper.GetResource("IpnLogInfo"), new SmartException(sb.ToString())); - } - else - { - Logger.Error(_helper.GetResource("IpnOrderNotFound"), new SmartException(sb.ToString())); - } - } - #endregion - break; - default: - #region Standard payment - { - string orderNumber = string.Empty; - values.TryGetValue("custom", out orderNumber); - Guid orderNumberGuid = Guid.Empty; - try - { - orderNumberGuid = new Guid(orderNumber); - } - catch - { - } - - var order = _orderService.GetOrderByGuid(orderNumberGuid); - if (order != null) - { - //order note - order.HasNewPaymentNotification = true; - - order.OrderNotes.Add(new OrderNote - { - Note = sb.ToString(), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); - - switch (newPaymentStatus) - { - case PaymentStatus.Pending: - { - } - break; - case PaymentStatus.Authorized: - { - if (_orderProcessingService.CanMarkOrderAsAuthorized(order)) - { - _orderProcessingService.MarkAsAuthorized(order); - } - } - break; - case PaymentStatus.Paid: - { - if (_orderProcessingService.CanMarkOrderAsPaid(order)) - { - _orderProcessingService.MarkOrderAsPaid(order); - } - } - break; - case PaymentStatus.Refunded: - { - if (_orderProcessingService.CanRefundOffline(order)) - { - _orderProcessingService.RefundOffline(order); - } - } - break; - case PaymentStatus.Voided: - { - if (_orderProcessingService.CanVoidOffline(order)) - { - _orderProcessingService.VoidOffline(order); - } - } - break; - default: - break; - } - } - else - { - Logger.Error(_helper.GetResource("IpnOrderNotFound"), new SmartException(sb.ToString())); - } - } - #endregion - break; - } - } - else - { - Logger.Error(_helper.GetResource("IpnFailed"), new SmartException(strRequest)); - } - - //nothing should be rendered to visitor - return Content(""); - } } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalExpressController.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalExpressController.cs index a1e1b10db2..776d1a210a 100644 --- a/src/Plugins/SmartStore.PayPal/Controllers/PayPalExpressController.cs +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalExpressController.cs @@ -1,114 +1,121 @@ using System; using System.Collections.Generic; -using System.Globalization; +using System.Linq; +using System.Net; using System.Text; using System.Web.Mvc; -using Autofac; using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Discounts; using SmartStore.Core.Domain.Logging; using SmartStore.Core.Domain.Orders; -using SmartStore.Core.Domain.Payments; using SmartStore.Core.Domain.Shipping; -using SmartStore.Core.Localization; -using SmartStore.Core.Logging; using SmartStore.PayPal.Models; using SmartStore.PayPal.PayPalSvc; using SmartStore.PayPal.Services; using SmartStore.PayPal.Settings; using SmartStore.PayPal.Validators; -using SmartStore.Services; using SmartStore.Services.Common; using SmartStore.Services.Customers; using SmartStore.Services.Directory; -using SmartStore.Services.Localization; using SmartStore.Services.Orders; using SmartStore.Services.Payments; -using SmartStore.Services.Stores; using SmartStore.Web.Framework.Controllers; -using SmartStore.Web.Framework.Plugins; +using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.Settings; namespace SmartStore.PayPal.Controllers { - public class PayPalExpressController : PaymentControllerBase + public class PayPalExpressController : PayPalControllerBase { - private readonly PluginHelper _helper; - private readonly IPaymentService _paymentService; - private readonly IOrderService _orderService; - private readonly IOrderProcessingService _orderProcessingService; - private readonly ILogger _logger; - private readonly PaymentSettings _paymentSettings; - private readonly ILocalizationService _localizationService; private readonly OrderSettings _orderSettings; private readonly ICurrencyService _currencyService; - private readonly CurrencySettings _currencySettings; private readonly IOrderTotalCalculationService _orderTotalCalculationService; private readonly ICustomerService _customerService; private readonly IGenericAttributeService _genericAttributeService; - private readonly ICommonServices _services; - private readonly IStoreService _storeService; public PayPalExpressController( - IPaymentService paymentService, IOrderService orderService, + IPaymentService paymentService, + IOrderService orderService, IOrderProcessingService orderProcessingService, - ILogger logger, - PaymentSettings paymentSettings, ILocalizationService localizationService, OrderSettings orderSettings, - ICurrencyService currencyService, CurrencySettings currencySettings, - IOrderTotalCalculationService orderTotalCalculationService, ICustomerService customerService, - IGenericAttributeService genericAttributeService, - IComponentContext ctx, ICommonServices services, - IStoreService storeService) + ICurrencyService currencyService, + IOrderTotalCalculationService orderTotalCalculationService, + ICustomerService customerService, + IGenericAttributeService genericAttributeService) : base( + PayPalExpressProvider.SystemName, + paymentService, + orderService, + orderProcessingService) { - _paymentService = paymentService; - _orderService = orderService; - _orderProcessingService = orderProcessingService; - _logger = logger; - _paymentSettings = paymentSettings; - _localizationService = localizationService; _orderSettings = orderSettings; _currencyService = currencyService; - _currencySettings = currencySettings; _orderTotalCalculationService = orderTotalCalculationService; _customerService = customerService; _genericAttributeService = genericAttributeService; - _services = services; - _storeService = storeService; - - _helper = new PluginHelper(ctx, "SmartStore.PayPal", "Plugins.Payments.PayPalExpress"); - - T = NullLocalizer.Instance; } - public Localizer T + private SelectList TransactModeValues(TransactMode selected) { - get; - set; + return new SelectList(new List + { + new { ID = (int)TransactMode.Authorize, Name = T("Plugins.Payments.PayPalExpress.ModeAuth") }, + new { ID = (int)TransactMode.AuthorizeAndCapture, Name = T("Plugins.Payments.PayPalExpress.ModeAuthAndCapture") } + }, + "ID", "Name", (int)selected); } - public SelectList TransactModeValues(TransactMode selected) + private string GetCheckoutButtonUrl(PayPalExpressPaymentSettings settings) { - return new SelectList(new List() { - new { ID = (int)TransactMode.Authorize, Name = _helper.GetResource("ModeAuth") }, - new { ID = (int)TransactMode.AuthorizeAndCapture, Name = _helper.GetResource("ModeAuthAndCapture") } - }, "ID", "Name", (int)selected); + const string expressCheckoutButton = "https://www.paypalobjects.com/{0}/i/btn/btn_xpressCheckout.gif"; + + HttpWebResponse response = null; + var culture = Services.WorkContext.WorkingLanguage.LanguageCulture; + + if (settings.SecurityProtocol.HasValue) + { + ServicePointManager.SecurityProtocol = settings.SecurityProtocol.Value; + } + + var buttonUrl = expressCheckoutButton.FormatInvariant(culture.Replace("-", "_")); + var request = (HttpWebRequest)WebRequest.Create(buttonUrl); + request.Method = "HEAD"; + + try + { + response = (HttpWebResponse)request.GetResponse(); + return buttonUrl; + } + catch (WebException) + { + /* A WebException will be thrown if the status of the response is not `200 OK` */ + return expressCheckoutButton.FormatInvariant("en_US"); + } + finally + { + if (response != null) + { + response.Close(); + } + } } [AdminAuthorize, ChildActionOnly] public ActionResult Configure() { var model = new PayPalExpressConfigurationModel(); - int storeScope = this.GetActiveStoreScopeConfiguration(_storeService, _services.WorkContext); - var settings = _services.Settings.LoadSetting(storeScope); + int storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); model.Copy(settings, true); model.TransactModeValues = TransactModeValues(settings.TransactMode); - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - storeDependingSettingHelper.GetOverrideKeys(settings, model, storeScope, _services.Settings); + model.AvailableSecurityProtocols = PayPalService.GetSecurityProtocols() + .Select(x => new SelectListItem { Value = ((int)x.Key).ToString(), Text = x.Value }) + .ToList(); + + var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); + storeDependingSettingHelper.GetOverrideKeys(settings, model, storeScope, Services.Settings); return View(model); } @@ -122,264 +129,68 @@ public ActionResult Configure(PayPalExpressConfigurationModel model, FormCollect ModelState.Clear(); var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - int storeScope = this.GetActiveStoreScopeConfiguration(_storeService, _services.WorkContext); - var settings = _services.Settings.LoadSetting(storeScope); + int storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); model.Copy(settings, false); - storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, _services.Settings); + storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, Services.Settings); // multistore context not possible, see IPN handling - _services.Settings.SaveSetting(settings, x => x.UseSandbox, 0, false); + Services.Settings.SaveSetting(settings, x => x.UseSandbox, 0, false); - _services.Settings.ClearCache(); - NotifySuccess(_services.Localization.GetResource("Plugins.Payments.PayPal.ConfigSaveNote")); + Services.Settings.ClearCache(); + NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); return Configure(); } public ActionResult PaymentInfo() { - var model = new PayPalExpressPaymentInfoModel(); - model.CurrentPageIsBasket = PayPalHelper.CurrentPageIsBasket(this.ControllerContext.ParentActionViewContext.RequestContext.RouteData); + model.CurrentPageIsBasket = ControllerContext.ParentActionViewContext.RequestContext.RouteData.IsRouteEqual("ShoppingCart", "Cart"); if (model.CurrentPageIsBasket) { - var culture = _services.WorkContext.WorkingLanguage.LanguageCulture; - var buttonUrl = "https://www.paypalobjects.com/{0}/i/btn/btn_xpressCheckout.gif".FormatWith(culture.Replace("-", "_")); - model.SubmitButtonImageUrl = PayPalHelper.CheckIfButtonExists(buttonUrl); + var settings = Services.Settings.LoadSetting(Services.StoreContext.CurrentStore.Id); + + model.SubmitButtonImageUrl = GetCheckoutButtonUrl(settings); } return PartialView(model); } - [ValidateInput(false)] - public ActionResult IPNHandler() + public ActionResult MiniShoppingCart() { - byte[] param = Request.BinaryRead(Request.ContentLength); - string strRequest = Encoding.ASCII.GetString(param); - Dictionary values; + var settings = Services.Settings.LoadSetting(Services.StoreContext.CurrentStore.Id); - var provider = _paymentService.LoadPaymentMethodBySystemName("Payments.PayPalExpress", true); - var processor = provider != null ? provider.Value as PayPalExpress : null; - if (processor == null) - throw new SmartException(T("PayPal Express module cannot be loaded")); - - if (processor.VerifyIPN(strRequest, out values)) + if (settings.ShowButtonInMiniShoppingCart) { - #region values - decimal total = decimal.Zero; - try - { - total = decimal.Parse(values["mc_gross"], new CultureInfo("en-US")); - } - catch { } - - string payer_status = string.Empty; - values.TryGetValue("payer_status", out payer_status); - string payment_status = string.Empty; - values.TryGetValue("payment_status", out payment_status); - string pending_reason = string.Empty; - values.TryGetValue("pending_reason", out pending_reason); - string mc_currency = string.Empty; - values.TryGetValue("mc_currency", out mc_currency); - string txn_id = string.Empty; - values.TryGetValue("txn_id", out txn_id); - string txn_type = string.Empty; - values.TryGetValue("txn_type", out txn_type); - string rp_invoice_id = string.Empty; - values.TryGetValue("rp_invoice_id", out rp_invoice_id); - string payment_type = string.Empty; - values.TryGetValue("payment_type", out payment_type); - string payer_id = string.Empty; - values.TryGetValue("payer_id", out payer_id); - string receiver_id = string.Empty; - values.TryGetValue("receiver_id", out receiver_id); - string invoice = string.Empty; - values.TryGetValue("invoice", out invoice); - string payment_fee = string.Empty; - values.TryGetValue("payment_fee", out payment_fee); - - #endregion - - var sb = new StringBuilder(); - sb.AppendLine("Paypal IPN:"); - foreach (KeyValuePair kvp in values) - { - sb.AppendLine(kvp.Key + ": " + kvp.Value); - } - - var newPaymentStatus = PayPalHelper.GetPaymentStatus(payment_status, pending_reason); - sb.AppendLine("New payment status: " + newPaymentStatus); + var model = new PayPalExpressPaymentInfoModel(); + model.SubmitButtonImageUrl = GetCheckoutButtonUrl(settings); - switch (txn_type) - { - case "recurring_payment_profile_created": - //do nothing here - break; - case "recurring_payment": - #region Recurring payment - { - Guid orderNumberGuid = Guid.Empty; - try - { - orderNumberGuid = new Guid(rp_invoice_id); - } - catch - { - } - - var initialOrder = _orderService.GetOrderByGuid(orderNumberGuid); - if (initialOrder != null) - { - var recurringPayments = _orderService.SearchRecurringPayments(0, 0, initialOrder.Id, null); - foreach (var rp in recurringPayments) - { - switch (newPaymentStatus) - { - case PaymentStatus.Authorized: - case PaymentStatus.Paid: - { - var recurringPaymentHistory = rp.RecurringPaymentHistory; - if (recurringPaymentHistory.Count == 0) - { - //first payment - var rph = new RecurringPaymentHistory() - { - RecurringPaymentId = rp.Id, - OrderId = initialOrder.Id, - CreatedOnUtc = DateTime.UtcNow - }; - rp.RecurringPaymentHistory.Add(rph); - _orderService.UpdateRecurringPayment(rp); - } - else - { - //next payments - _orderProcessingService.ProcessNextRecurringPayment(rp); - //UNDONE change new order status according to newPaymentStatus - //UNDONE refund/void is not supported - } - } - break; - } - } - - _logger.Information("PayPal IPN. Recurring info", new SmartException(sb.ToString())); - } - else - { - _logger.Error("PayPal IPN. Order is not found", new SmartException(sb.ToString())); - } - } - #endregion - break; - default: - #region Standard payment - { - string orderNumber = string.Empty; - values.TryGetValue("custom", out orderNumber); - Guid orderNumberGuid = Guid.Empty; - try - { - orderNumberGuid = new Guid(orderNumber); - } - catch - { - } - - var order = _orderService.GetOrderByGuid(orderNumberGuid); - if (order != null) - { - //order note - order.HasNewPaymentNotification = true; - - order.OrderNotes.Add(new OrderNote - { - Note = sb.ToString(), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); - - switch (newPaymentStatus) - { - case PaymentStatus.Pending: - { - } - break; - case PaymentStatus.Authorized: - { - if (_orderProcessingService.CanMarkOrderAsAuthorized(order)) - { - _orderProcessingService.MarkAsAuthorized(order); - } - } - break; - case PaymentStatus.Paid: - { - if (_orderProcessingService.CanMarkOrderAsPaid(order)) - { - _orderProcessingService.MarkOrderAsPaid(order); - } - } - break; - case PaymentStatus.Refunded: - { - if (_orderProcessingService.CanRefundOffline(order)) - { - _orderProcessingService.RefundOffline(order); - } - } - break; - case PaymentStatus.Voided: - { - if (_orderProcessingService.CanVoidOffline(order)) - { - _orderProcessingService.VoidOffline(order); - } - } - break; - default: - break; - } - } - else - { - _logger.Error("PayPal IPN. Order is not found", new SmartException(sb.ToString())); - } - } - #endregion - break; - } - } - else - { - _logger.Error("PayPal IPN failed.", new SmartException(strRequest)); + return PartialView(model); } - //nothing should be rendered to visitor - return Content(""); + return new EmptyResult(); } - public ActionResult SubmitButton() { try { //user validation - if ((_services.WorkContext.CurrentCustomer.IsGuest() && !_orderSettings.AnonymousCheckoutAllowed)) + if ((Services.WorkContext.CurrentCustomer.IsGuest() && !_orderSettings.AnonymousCheckoutAllowed)) return RedirectToRoute("Login"); - var settings = _services.Settings.LoadSetting(_services.StoreContext.CurrentStore.Id); - var cart = _services.WorkContext.CurrentCustomer.GetCartItems(ShoppingCartType.ShoppingCart, _services.StoreContext.CurrentStore.Id); + var store = Services.StoreContext.CurrentStore; + var customer = Services.WorkContext.CurrentCustomer; + var settings = Services.Settings.LoadSetting(store.Id); + var cart = Services.WorkContext.CurrentCustomer.GetCartItems(ShoppingCartType.ShoppingCart, store.Id); if (cart.Count == 0) return RedirectToRoute("ShoppingCart"); - var currency = _currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId).CurrencyCode; - if (String.IsNullOrEmpty(settings.ApiAccountName)) throw new ApplicationException("PayPal API Account Name is not set"); if (String.IsNullOrEmpty(settings.ApiAccountPassword)) @@ -387,32 +198,28 @@ public ActionResult SubmitButton() if (String.IsNullOrEmpty(settings.Signature)) throw new ApplicationException("PayPal API Signature is not set"); - var provider = _paymentService.LoadPaymentMethodBySystemName("Payments.PayPalExpress", true); - var processor = provider != null ? provider.Value as PayPalExpress : null; + var provider = PaymentService.LoadPaymentMethodBySystemName(PayPalExpressProvider.SystemName, true); + var processor = provider != null ? provider.Value as PayPalExpressProvider : null; if (processor == null) throw new SmartException("PayPal Express Checkout module cannot be loaded"); var processPaymentRequest = new PayPalProcessPaymentRequest(); - processPaymentRequest.StoreId = _services.StoreContext.CurrentStore.Id; + processPaymentRequest.StoreId = store.Id; //Get sub-total and discounts that apply to sub-total 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); //order total decimal resultTemp = decimal.Zero; resultTemp += subTotalWithDiscountBase; - // get customer - int customerId = Convert.ToInt32(_services.WorkContext.CurrentCustomer.Id.ToString()); - var customer = _customerService.GetCustomerById(customerId); - //Get discounts that apply to Total Discount appliedDiscount = null; var discountAmount = _orderTotalCalculationService.GetOrderTotalDiscount(customer, resultTemp, out appliedDiscount); @@ -429,20 +236,20 @@ public ActionResult SubmitButton() decimal tempDiscount = discountAmount + orderSubTotalDiscountAmountBase; - resultTemp = _currencyService.ConvertFromPrimaryStoreCurrency(resultTemp, _services.WorkContext.WorkingCurrency); + resultTemp = _currencyService.ConvertFromPrimaryStoreCurrency(resultTemp, Services.WorkContext.WorkingCurrency); if (tempDiscount > decimal.Zero) { - tempDiscount = _currencyService.ConvertFromPrimaryStoreCurrency(tempDiscount, _services.WorkContext.WorkingCurrency); + tempDiscount = _currencyService.ConvertFromPrimaryStoreCurrency(tempDiscount, Services.WorkContext.WorkingCurrency); } - processPaymentRequest.PaymentMethodSystemName = "Payments.PayPalExpress"; + processPaymentRequest.PaymentMethodSystemName = PayPalExpressProvider.SystemName; processPaymentRequest.OrderTotal = resultTemp; processPaymentRequest.Discount = tempDiscount; processPaymentRequest.IsRecurringPayment = false; //var selectedPaymentMethodSystemName = _workContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.SelectedPaymentMethod, _storeContext.CurrentStore.Id); - processPaymentRequest.CustomerId = _services.WorkContext.CurrentCustomer.Id; + processPaymentRequest.CustomerId = Services.WorkContext.CurrentCustomer.Id; this.Session["OrderPaymentInfo"] = processPaymentRequest; var resp = processor.SetExpressCheckout(processPaymentRequest, cart); @@ -451,15 +258,12 @@ public ActionResult SubmitButton() { processPaymentRequest.PaypalToken = resp.Token; processPaymentRequest.OrderGuid = new Guid(); - processPaymentRequest.IsShippingMethodSet = PayPalHelper.CurrentPageIsBasket(this.RouteData); + processPaymentRequest.IsShippingMethodSet = ControllerContext.RouteData.IsRouteEqual("ShoppingCart", "Cart"); this.Session["OrderPaymentInfo"] = processPaymentRequest; - _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedPaymentMethod, "Payments.PayPalExpress", - _services.StoreContext.CurrentStore.Id); + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedPaymentMethod, PayPalExpressProvider.SystemName, store.Id); - var result = new RedirectResult(String.Format( - PayPalHelper.GetPaypalUrl(settings) + - "?cmd=_express-checkout&useraction=commit&token={0}", resp.Token)); + var result = new RedirectResult(String.Format(settings.GetPayPalUrl() + "?cmd=_express-checkout&useraction=commit&token={0}", resp.Token)); return result; } @@ -471,7 +275,7 @@ public ActionResult SubmitButton() error.AppendLine(String.Format("{0} | {1} | {2}", errormsg.ErrorCode, errormsg.ShortMessage, errormsg.LongMessage)); } - _logger.InsertLog(LogLevel.Error, resp.Errors[0].ShortMessage, resp.Errors[0].LongMessage, _services.WorkContext.CurrentCustomer); + Logger.InsertLog(LogLevel.Error, resp.Errors[0].ShortMessage, resp.Errors[0].LongMessage, customer); NotifyError(error.ToString(), false); @@ -480,7 +284,7 @@ public ActionResult SubmitButton() } catch (Exception ex) { - _logger.InsertLog(LogLevel.Error, ex.Message, ex.StackTrace, _services.WorkContext.CurrentCustomer); + Logger.InsertLog(LogLevel.Error, ex.Message, ex.StackTrace, Services.WorkContext.CurrentCustomer); NotifyError(ex.Message, false); @@ -491,8 +295,8 @@ public ActionResult SubmitButton() public ActionResult GetDetails(string token) { - var provider = _paymentService.LoadPaymentMethodBySystemName("Payments.PayPalExpress", true); - var processor = provider != null ? provider.Value as PayPalExpress : null; + var provider = PaymentService.LoadPaymentMethodBySystemName("Payments.PayPalExpress", true); + var processor = provider != null ? provider.Value as PayPalExpressProvider : null; if (processor == null) throw new SmartException("PayPal Express module cannot be loaded"); @@ -503,13 +307,14 @@ public ActionResult GetDetails(string token) var paymentInfo = this.Session["OrderPaymentInfo"] as ProcessPaymentRequest; paymentInfo = processor.SetCheckoutDetails(paymentInfo, resp.GetExpressCheckoutDetailsResponseDetails); this.Session["OrderPaymentInfo"] = paymentInfo; + + var store = Services.StoreContext.CurrentStore; var customer = _customerService.GetCustomerById(paymentInfo.CustomerId); - _services.WorkContext.CurrentCustomer = customer; - _customerService.UpdateCustomer(_services.WorkContext.CurrentCustomer); + Services.WorkContext.CurrentCustomer = customer; + _customerService.UpdateCustomer(Services.WorkContext.CurrentCustomer); - var selectedShippingOption = _services.WorkContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.SelectedShippingOption, - _services.StoreContext.CurrentStore.Id); + var selectedShippingOption = Services.WorkContext.CurrentCustomer.GetAttribute(SystemCustomerAttributeNames.SelectedShippingOption, store.Id); if (selectedShippingOption != null) { return RedirectToAction("Confirm", "Checkout", new { area = "" }); @@ -517,8 +322,7 @@ public ActionResult GetDetails(string token) else { //paymentInfo.RequiresPaymentWorkflow = false; - _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedPaymentMethod, paymentInfo.PaymentMethodSystemName, - _services.StoreContext.CurrentStore.Id); + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedPaymentMethod, paymentInfo.PaymentMethodSystemName, store.Id); _customerService.UpdateCustomer(customer); return RedirectToAction("BillingAddress", "Checkout", new { area = "" }); @@ -532,7 +336,7 @@ public ActionResult GetDetails(string token) error.AppendLine(String.Format("{0} | {1} | {2}", errormsg.ErrorCode, errormsg.ShortMessage, errormsg.LongMessage)); } - _logger.InsertLog(LogLevel.Error, resp.Errors[0].ShortMessage, resp.Errors[0].LongMessage, _services.WorkContext.CurrentCustomer); + Logger.InsertLog(LogLevel.Error, resp.Errors[0].ShortMessage, resp.Errors[0].LongMessage, Services.WorkContext.CurrentCustomer); NotifyError(error.ToString(), false); @@ -552,16 +356,18 @@ public override IList ValidatePaymentForm(FormCollection form) { var warnings = new List(); - //validate - var validator = new PayPalExpressPaymentInfoValidator(_localizationService); - var model = new PayPalExpressPaymentInfoModel() - { + var validator = new PayPalExpressPaymentInfoValidator(Services.Localization); + var model = new PayPalExpressPaymentInfoModel(); - }; var validationResult = validator.Validate(model); + if (!validationResult.IsValid) + { foreach (var error in validationResult.Errors) + { warnings.Add(error.ErrorMessage); + } + } return warnings; } diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalPlusController.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalPlusController.cs new file mode 100644 index 0000000000..7a835f3d19 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalPlusController.cs @@ -0,0 +1,408 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Web; +using System.Web.Mvc; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Html; +using SmartStore.Core.Plugins; +using SmartStore.PayPal.Models; +using SmartStore.PayPal.Services; +using SmartStore.PayPal.Settings; +using SmartStore.PayPal.Validators; +using SmartStore.Services.Catalog; +using SmartStore.Services.Common; +using SmartStore.Services.Customers; +using SmartStore.Services.Directory; +using SmartStore.Services.Localization; +using SmartStore.Services.Payments; +using SmartStore.Services.Tax; +using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Plugins; +using SmartStore.Web.Framework.Security; +using SmartStore.Web.Framework.Settings; + +namespace SmartStore.PayPal.Controllers +{ + public class PayPalPlusController : PayPalRestApiControllerBase + { + private readonly HttpContextBase _httpContext; + private readonly PluginMediator _pluginMediator; + private readonly IGenericAttributeService _genericAttributeService; + private readonly IPaymentService _paymentService; + private readonly ITaxService _taxService; + private readonly ICurrencyService _currencyService; + private readonly IPriceFormatter _priceFormatter; + + public PayPalPlusController( + HttpContextBase httpContext, + PluginMediator pluginMediator, + IPayPalService payPalService, + IGenericAttributeService genericAttributeService, + IPaymentService paymentService, + ITaxService taxService, + ICurrencyService currencyService, + IPriceFormatter priceFormatter) : base( + PayPalPlusProvider.SystemName, + payPalService) + { + _httpContext = httpContext; + _pluginMediator = pluginMediator; + _genericAttributeService = genericAttributeService; + _paymentService = paymentService; + _taxService = taxService; + _currencyService = currencyService; + _priceFormatter = priceFormatter; + } + + private string GetPaymentMethodName(Provider provider) + { + if (provider != null) + { + var name = _pluginMediator.GetLocalizedFriendlyName(provider.Metadata); + + if (name.IsEmpty()) + name = provider.Metadata.FriendlyName; + + if (name.IsEmpty()) + name = provider.Metadata.SystemName; + + return name; + } + return ""; + } + + private string GetPaymentFee(Provider provider, List cart) + { + var paymentMethodAdditionalFee = provider.Value.GetAdditionalHandlingFee(cart); + var rateBase = _taxService.GetPaymentMethodAdditionalFee(paymentMethodAdditionalFee, Services.WorkContext.CurrentCustomer); + var rate = _currencyService.ConvertFromPrimaryStoreCurrency(rateBase, Services.WorkContext.WorkingCurrency); + + if (rate != decimal.Zero) + { + return _priceFormatter.FormatPaymentMethodAdditionalFee(rate, true); + } + return ""; + } + + private PayPalPlusCheckoutModel.ThirdPartyPaymentMethod GetThirdPartyPaymentMethodModel( + Provider provider, + PayPalPlusPaymentSettings settings, + Store store) + { + var model = new PayPalPlusCheckoutModel.ThirdPartyPaymentMethod(); + model.MethodName = GetPaymentMethodName(provider); + model.RedirectUrl = Url.Action("CheckoutReturn", "PayPalPlus", new { area = Plugin.SystemName, systemName = provider.Metadata.SystemName }, store.SslEnabled ? "https" : "http"); + + try + { + if (settings.DisplayPaymentMethodDescription) + { + // not the short description, the full description is intended for frontend + var paymentMethod = _paymentService.GetPaymentMethodBySystemName(provider.Metadata.SystemName); + if (paymentMethod != null) + { + var description = paymentMethod.GetLocalized(x => x.FullDescription); + if (description.HasValue()) + { + description = HtmlUtils.ConvertHtmlToPlainText(description); + description = HtmlUtils.StripTags(HttpUtility.HtmlDecode(description)); + + if (description.HasValue()) + model.Description = description.EncodeJsString(); + } + } + } + } + catch { } + + try + { + if (settings.DisplayPaymentMethodLogo && provider.Metadata.PluginDescriptor != null && store.SslEnabled) + { + var brandImageUrl = _pluginMediator.GetBrandImageUrl(provider.Metadata); + if (brandImageUrl.HasValue()) + { + if (brandImageUrl.StartsWith("~")) + brandImageUrl = brandImageUrl.Substring(1); + + var uri = new UriBuilder(Uri.UriSchemeHttps, Request.Url.Host, -1, brandImageUrl); + model.ImageUrl = uri.ToString(); + } + } + } + catch { } + + return model; + } + + [NonAction] + public override IList ValidatePaymentForm(FormCollection form) + { + var warnings = new List(); + return warnings; + } + + [NonAction] + public override ProcessPaymentRequest GetPaymentInfo(FormCollection form) + { + var paymentInfo = new ProcessPaymentRequest(); + return paymentInfo; + } + + [AdminAuthorize, ChildActionOnly] + public ActionResult Configure() + { + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); + + var model = new PayPalPlusConfigurationModel + { + ConfigGroups = T("Plugins.SmartStore.PayPal.ConfigGroups").Text.SplitSafe(";") + }; + + model.AvailableSecurityProtocols = PayPal.Services.PayPalService.GetSecurityProtocols() + .Select(x => new SelectListItem { Value = ((int)x.Key).ToString(), Text = x.Value }) + .ToList(); + + // it's better to also offer inactive methods here but filter them out in frontend + var methods = _paymentService.LoadAllPaymentMethods(storeScope); + + model.AvailableThirdPartyPaymentMethods = methods + .Where(x => + x.Metadata.PluginDescriptor.SystemName != Plugin.SystemName && + !x.Value.RequiresInteraction && + (x.Metadata.PluginDescriptor.SystemName == "SmartStore.OfflinePayment" || x.Value.PaymentMethodType == PaymentMethodType.Redirection)) + .Select(x => new SelectListItem { Value = x.Metadata.SystemName, Text = GetPaymentMethodName(x) }) + .ToList(); + + + model.Copy(settings, true); + + var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); + storeDependingSettingHelper.GetOverrideKeys(settings, model, storeScope, Services.Settings); + + return View(model); + } + + [HttpPost, AdminAuthorize, ChildActionOnly] + public ActionResult Configure(PayPalPlusConfigurationModel model, FormCollection form) + { + var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); + var oldClientId = settings.ClientId; + var oldSecret = settings.Secret; + var oldProfileId = settings.ExperienceProfileId; + + var validator = new PayPalPlusConfigValidator(Services.Localization, x => + { + return storeScope == 0 || storeDependingSettingHelper.IsOverrideChecked(settings, x, form); + }); + + validator.Validate(model, ModelState); + + if (!ModelState.IsValid) + return Configure(); + + ModelState.Clear(); + + model.Copy(settings, false); + + // credentials changed: reset profile and webhook id to avoid errors + if (!oldClientId.IsCaseInsensitiveEqual(settings.ClientId) || !oldSecret.IsCaseInsensitiveEqual(settings.Secret)) + { + if (oldProfileId.IsCaseInsensitiveEqual(settings.ExperienceProfileId)) + settings.ExperienceProfileId = null; + + settings.WebhookId = null; + } + + storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, Services.Settings); + + Services.Settings.SaveSetting(settings, x => x.UseSandbox, 0, false); + + Services.Settings.ClearCache(); + NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); + + return Configure(); + } + + public ActionResult PaymentInfo() + { + return new EmptyResult(); + } + + public ActionResult PaymentWall() + { + var sb = new StringBuilder(); + var store = Services.StoreContext.CurrentStore; + var customer = Services.WorkContext.CurrentCustomer; + var language = Services.WorkContext.WorkingLanguage; + var settings = Services.Settings.LoadSetting(store.Id); + var cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, store.Id); + + var pppMethod = _paymentService.GetPaymentMethodBySystemName(PayPalPlusProvider.SystemName); + var pppProvider = _paymentService.LoadPaymentMethodBySystemName(PayPalPlusProvider.SystemName, false, store.Id); + + var methods = _paymentService.LoadActivePaymentMethods(customer, cart, store.Id, null, false); + var session = _httpContext.GetPayPalSessionData(); + + var model = new PayPalPlusCheckoutModel(); + model.ThirdPartyPaymentMethods = new List(); + model.UseSandbox = settings.UseSandbox; + model.LanguageCulture = (language.LanguageCulture ?? "de_DE").Replace("-", "_"); + model.PayPalPlusPseudoMessageFlag = TempData["PayPalPlusPseudoMessageFlag"] as string; + model.PayPalFee = GetPaymentFee(pppProvider, cart); + model.HasAnyFees = model.PayPalFee.HasValue(); + + if (pppMethod != null) + { + model.FullDescription = pppMethod.GetLocalized(x => x.FullDescription, language.Id); + } + + if (customer.BillingAddress != null && customer.BillingAddress.Country != null) + { + model.BillingAddressCountryCode = customer.BillingAddress.Country.TwoLetterIsoCode; + } + + foreach (var systemName in settings.ThirdPartyPaymentMethods) + { + var provider = methods.FirstOrDefault(x => x.Metadata.SystemName == systemName); + if (provider != null) + { + var methodModel = GetThirdPartyPaymentMethodModel(provider, settings, store); + model.ThirdPartyPaymentMethods.Add(methodModel); + + var fee = GetPaymentFee(provider, cart); + if (fee.HasValue()) + model.HasAnyFees = true; + if (sb.Length > 0) + sb.Append(", "); + sb.AppendFormat("['{0}','{1}']", methodModel.MethodName.Replace("'", ""), fee); + } + } + + model.ThirdPartyFees = sb.ToString(); + + // we must create a new paypal payment each time the payment wall is rendered because otherwise patch payment can fail + // with "Item amount must add up to specified amount subtotal (or total if amount details not specified)". + session.PaymentId = null; + session.ApprovalUrl = null; + + var result = PayPalService.EnsureAccessToken(session, settings); + if (result.Success) + { + var protocol = (store.SslEnabled ? "https" : "http"); + var returnUrl = Url.Action("CheckoutReturn", "PayPalPlus", new { area = Plugin.SystemName }, protocol); + var cancelUrl = Url.Action("CheckoutCancel", "PayPalPlus", new { area = Plugin.SystemName }, protocol); + + result = PayPalService.CreatePayment(settings, session, cart, PayPalPlusProvider.SystemName, returnUrl, cancelUrl); + if (result.Success && result.Json != null) + { + foreach (var link in result.Json.links) + { + if (((string)link.rel).IsCaseInsensitiveEqual("approval_url")) + { + session.PaymentId = result.Id; + session.ApprovalUrl = link.href; + break; + } + } + } + else + { + model.ErrorMessage = result.ErrorMessage; + } + } + else + { + model.ErrorMessage = result.ErrorMessage; + } + + model.ApprovalUrl = session.ApprovalUrl; + + return View(model); + } + + [HttpPost] + public ActionResult PatchShipping() + { + var store = Services.StoreContext.CurrentStore; + var customer = Services.WorkContext.CurrentCustomer; + var settings = Services.Settings.LoadSetting(store.Id); + var cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, store.Id); + var session = HttpContext.GetPayPalSessionData(); + + var apiResult = PayPalService.PatchShipping(settings, session, cart, PayPalPlusProvider.SystemName); + + return new JsonResult { Data = new { success = apiResult.Success, error = apiResult.ErrorMessage } }; + } + + public ActionResult CheckoutCompleted() + { + var instruct = _httpContext.Session[PayPalPlusProvider.CheckoutCompletedKey] as string; + + if (instruct.HasValue()) + { + return Content(instruct); + } + + return new EmptyResult(); + } + + [ValidateInput(false)] + public ActionResult CheckoutReturn(string systemName, string paymentId, string PayerID) + { + // Request.QueryString: + // paymentId: PAY-0TC88803RP094490KK4KM6AI, token: EC-5P379249AL999154U, PayerID: 5L9K773HHJLPN + + var customer = Services.WorkContext.CurrentCustomer; + var store = Services.StoreContext.CurrentStore; + var settings = Services.Settings.LoadSetting(store.Id); + var session = _httpContext.GetPayPalSessionData(); + + if (systemName.IsEmpty()) + systemName = PayPalPlusProvider.SystemName; + + if (paymentId.HasValue() && session.PaymentId.IsEmpty()) + session.PaymentId = paymentId; + + session.PayerId = PayerID; + + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedPaymentMethod, systemName, store.Id); + + var paymentRequest = _httpContext.Session["OrderPaymentInfo"] as ProcessPaymentRequest; + if (paymentRequest == null) + { + _httpContext.Session["OrderPaymentInfo"] = new ProcessPaymentRequest + { + PaymentMethodSystemName = systemName + }; + } + + return RedirectToAction("Confirm", "Checkout", new { area = "" }); + } + + [ValidateInput(false)] + public ActionResult CheckoutCancel() + { + // Request.QueryString: + // token: EC-6JM38216F6718012L, ppp_msg: 1 + + // undocumented + var pseudoMessageFlag = Request.QueryString["ppp_msg"] as string; + + if (pseudoMessageFlag.HasValue()) + { + TempData["PayPalPlusPseudoMessageFlag"] = pseudoMessageFlag; + } + + // back to where he came from + return RedirectToAction("PaymentMethod", "Checkout", new { area = "" }); + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalRestApiControllerBase.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalRestApiControllerBase.cs new file mode 100644 index 0000000000..8d5818e5d5 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalRestApiControllerBase.cs @@ -0,0 +1,177 @@ +using System; +using System.IO; +using System.Net; +using System.Web.Mvc; +using SmartStore.Core.Configuration; +using SmartStore.PayPal.Services; +using SmartStore.PayPal.Settings; +using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Security; + +namespace SmartStore.PayPal.Controllers +{ + public abstract class PayPalRestApiControllerBase : PaymentControllerBase where TSetting : PayPalApiSettingsBase, ISettings, new() + { + public PayPalRestApiControllerBase( + string systemName, + IPayPalService payPalService) + { + SystemName = systemName; + PayPalService = payPalService; + } + + private string GetControllerName() + { + return GetType().Name.Replace("Controller", ""); + } + + protected string SystemName { get; private set; } + protected IPayPalService PayPalService { get; private set; } + + [AdminAuthorize] + public ActionResult UpsertExperienceProfile() + { + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); + + var store = Services.StoreService.GetStoreById(storeScope == 0 ? Services.StoreContext.CurrentStore.Id : storeScope); + var session = new PayPalSessionData(); + + var result = PayPalService.EnsureAccessToken(session, settings); + if (result.Success) + { + result = PayPalService.UpsertCheckoutExperience(settings, session, store); + if (result.Success && result.Id.HasValue()) + { + settings.ExperienceProfileId = result.Id; + Services.Settings.SaveSetting(settings, x => x.ExperienceProfileId, storeScope, false); + Services.Settings.ClearCache(); + } + } + + if (result.Success) + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); + else + NotifyError(result.ErrorMessage); + + return RedirectToAction("ConfigureProvider", "Plugin", new { area = "admin", systemName = SystemName }); + } + + [AdminAuthorize] + public ActionResult DeleteExperienceProfile() + { + var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); + var session = new PayPalSessionData(); + + var result = PayPalService.EnsureAccessToken(session, settings); + if (result.Success) + { + result = PayPalService.DeleteCheckoutExperience(settings, session); + if (result.Success) + { + settings.ExperienceProfileId = null; + Services.Settings.SaveSetting(settings, x => x.ExperienceProfileId, storeScope, false); + Services.Settings.ClearCache(); + } + } + + if (result.Success) + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); + else + NotifyError(result.ErrorMessage); + + return RedirectToAction("ConfigureProvider", "Plugin", new { area = "admin", systemName = SystemName }); + } + + [AdminAuthorize] + public ActionResult CreateWebhook() + { + var settings = Services.Settings.LoadSetting(); + var session = new PayPalSessionData(); + + if (settings.WebhookId.HasValue()) + { + var unused = PayPalService.DeleteWebhook(settings, session); + + Services.Settings.SaveSetting(settings, x => x.WebhookId, 0, false); + } + + var url = Url.Action("Webhook", GetControllerName(), new { area = Plugin.SystemName }, "https"); + + var result = PayPalService.EnsureAccessToken(session, settings); + if (result.Success) + { + result = PayPalService.CreateWebhook(settings, session, url); + if (result.Success) + { + settings.WebhookId = result.Id; + Services.Settings.SaveSetting(settings, x => x.WebhookId, 0, false); + } + } + + Services.Settings.ClearCache(); + + if (result.Success) + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); + else + NotifyError(result.ErrorMessage); + + return RedirectToAction("ConfigureProvider", "Plugin", new { area = "admin", systemName = SystemName }); + } + + [AdminAuthorize] + public ActionResult DeleteWebhook() + { + var settings = Services.Settings.LoadSetting(); + var session = new PayPalSessionData(); + + if (settings.WebhookId.HasValue()) + { + var result = PayPalService.EnsureAccessToken(session, settings); + if (result.Success) + { + result = PayPalService.DeleteWebhook(settings, session); + if (result.Success) + { + settings.WebhookId = null; + Services.Settings.SaveSetting(settings, x => x.WebhookId, 0, false); + Services.Settings.ClearCache(); + } + } + + if (result.Success) + NotifySuccess(T("Admin.Common.TaskSuccessfullyProcessed")); + else + NotifyError(result.ErrorMessage); + } + + return RedirectToAction("ConfigureProvider", "Plugin", new { area = "admin", systemName = SystemName }); + } + + [ValidateInput(false)] + public ActionResult Webhook() + { + HttpStatusCode result = HttpStatusCode.OK; + + try + { + string json = null; + using (var reader = new StreamReader(Request.InputStream)) + { + json = reader.ReadToEnd(); + } + + var settings = Services.Settings.LoadSetting(); + + result = PayPalService.ProcessWebhook(settings, Request.Headers, json, PayPalPlusProvider.SystemName); + } + catch (Exception exception) + { + PayPalService.LogError(exception, isWarning: true); + } + + return new HttpStatusCodeResult(result); + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Controllers/PayPalStandardController.cs b/src/Plugins/SmartStore.PayPal/Controllers/PayPalStandardController.cs index 10aa50046e..e007d86dfc 100644 --- a/src/Plugins/SmartStore.PayPal/Controllers/PayPalStandardController.cs +++ b/src/Plugins/SmartStore.PayPal/Controllers/PayPalStandardController.cs @@ -1,73 +1,50 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.Linq; -using System.Text; using System.Web.Mvc; -using SmartStore.Core; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; using SmartStore.Core.Logging; using SmartStore.PayPal.Models; +using SmartStore.PayPal.Services; using SmartStore.PayPal.Settings; -using SmartStore.Services; -using SmartStore.Services.Localization; using SmartStore.Services.Orders; using SmartStore.Services.Payments; -using SmartStore.Services.Stores; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.Settings; namespace SmartStore.PayPal.Controllers { - public class PayPalStandardController : PaymentControllerBase + public class PayPalStandardController : PayPalControllerBase { - private readonly IPaymentService _paymentService; - private readonly IOrderService _orderService; - private readonly IOrderProcessingService _orderProcessingService; - private readonly IStoreContext _storeContext; - private readonly IWorkContext _workContext; - private readonly IWebHelper _webHelper; - private readonly PaymentSettings _paymentSettings; - private readonly ILocalizationService _localizationService; - private readonly ICommonServices _services; - private readonly IStoreService _storeService; - public PayPalStandardController( - IPaymentService paymentService, IOrderService orderService, - IOrderProcessingService orderProcessingService, - IStoreContext storeContext, - IWorkContext workContext, - IWebHelper webHelper, - PaymentSettings paymentSettings, - ILocalizationService localizationService, - ICommonServices services, - IStoreService storeService) + IPaymentService paymentService, + IOrderService orderService, + IOrderProcessingService orderProcessingService) : base( + PayPalStandardProvider.SystemName, + paymentService, + orderService, + orderProcessingService) { - _paymentService = paymentService; - _orderService = orderService; - _orderProcessingService = orderProcessingService; - _storeContext = storeContext; - _workContext = workContext; - _webHelper = webHelper; - _paymentSettings = paymentSettings; - _localizationService = localizationService; - _services = services; - _storeService = storeService; } [AdminAuthorize, ChildActionOnly] public ActionResult Configure() { var model = new PayPalStandardConfigurationModel(); - int storeScope = this.GetActiveStoreScopeConfiguration(_storeService, _services.WorkContext); - var settings = _services.Settings.LoadSetting(storeScope); + int storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); model.Copy(settings, true); - var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - storeDependingSettingHelper.GetOverrideKeys(settings, model, storeScope, _services.Settings); + model.AvailableSecurityProtocols = PayPalService.GetSecurityProtocols() + .Select(x => new SelectListItem { Value = ((int)x.Key).ToString(), Text = x.Value }) + .ToList(); + + var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); + storeDependingSettingHelper.GetOverrideKeys(settings, model, storeScope, Services.Settings); return View(model); } @@ -81,18 +58,18 @@ public ActionResult Configure(PayPalStandardConfigurationModel model, FormCollec ModelState.Clear(); var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData); - int storeScope = this.GetActiveStoreScopeConfiguration(_storeService, _services.WorkContext); - var settings = _services.Settings.LoadSetting(storeScope); + int storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext); + var settings = Services.Settings.LoadSetting(storeScope); model.Copy(settings, false); - storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, _services.Settings); + storeDependingSettingHelper.UpdateSettings(settings, form, storeScope, Services.Settings); // multistore context not possible, see IPN handling - _services.Settings.SaveSetting(settings, x => x.UseSandbox, 0, false); + Services.Settings.SaveSetting(settings, x => x.UseSandbox, 0, false); - _services.Settings.ClearCache(); - NotifySuccess(_services.Localization.GetResource("Plugins.Payments.PayPal.ConfigSaveNote")); + Services.Settings.ClearCache(); + NotifySuccess(T("Admin.Common.DataSuccessfullySaved")); return Configure(); } @@ -119,38 +96,42 @@ public override ProcessPaymentRequest GetPaymentInfo(FormCollection form) [ValidateInput(false)] public ActionResult PDTHandler(FormCollection form) { - string tx = _webHelper.QueryString("tx"); Dictionary values; + var tx = Services.WebHelper.QueryString("tx"); + var utcNow = DateTime.UtcNow; + var orderNumberGuid = Guid.Empty; + var orderNumber = string.Empty; + var total = decimal.Zero; string response; - var provider = _paymentService.LoadPaymentMethodBySystemName("Payments.PayPalStandard", true); - var processor = provider != null ? provider.Value as PayPalStandardProvider : null; + var provider = PaymentService.LoadPaymentMethodBySystemName(SystemName, true); + var processor = (provider != null ? provider.Value as PayPalStandardProvider : null); if (processor == null) - throw new SmartException(_localizationService.GetResource("Plugins.Payments.PayPalStandard.NoModuleLoading")); + throw new SmartException(T("Plugins.Payments.PayPal.NoModuleLoading")); - var settings = _services.Settings.LoadSetting(); + var settings = Services.Settings.LoadSetting(); if (processor.GetPDTDetails(tx, settings, out values, out response)) { - string orderNumber = string.Empty; values.TryGetValue("custom", out orderNumber); - Guid orderNumberGuid = Guid.Empty; + try { orderNumberGuid = new Guid(orderNumber); } catch { } - Order order = _orderService.GetOrderByGuid(orderNumberGuid); + + var order = OrderService.GetOrderByGuid(orderNumberGuid); + if (order != null) { - decimal total = decimal.Zero; try { total = decimal.Parse(values["mc_gross"], new CultureInfo("en-US")); } catch (Exception exc) { - Logger.Error(_localizationService.GetResource("Plugins.Payments.PayPalStandard.FailedGetGross"), exc); + Logger.Error(T("Plugins.Payments.PayPalStandard.FailedGetGross"), exc); } string payer_status = string.Empty; @@ -174,33 +155,54 @@ public ActionResult PDTHandler(FormCollection form) string payment_fee = string.Empty; values.TryGetValue("payment_fee", out payment_fee); - string paymentNote = _localizationService.GetResource("Plugins.Payments.PayPalStandard.PaymentNote").FormatWith( + var paymentNote = T("Plugins.Payments.PayPalStandard.PaymentNote", total, mc_currency, payer_status, payment_status, pending_reason, txn_id, payment_type, payer_id, receiver_id, invoice, payment_fee); - //order note - order.OrderNotes.Add(new OrderNote() - { - Note = paymentNote, - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); - - //validate order total - if (settings.PdtValidateOrderTotal && !Math.Round(total, 2).Equals(Math.Round(order.OrderTotal, 2))) + OrderService.AddOrderNote(order, paymentNote); + + // validate order total... you may get differences if settings.PassProductNamesAndTotals is true + if (settings.PdtValidateOrderTotal) { - Logger.Error(_localizationService.GetResource("Plugins.Payments.PayPalStandard.UnequalTotalOrder").FormatWith(total, order.OrderTotal)); + var roundedTotal = Math.Round(total, 2); + var roundedOrderTotal = Math.Round(order.OrderTotal, 2); + var roundedDifference = Math.Abs(roundedTotal - roundedOrderTotal); + + if (!roundedTotal.Equals(roundedOrderTotal)) + { + var message = T("Plugins.Payments.PayPalStandard.UnequalTotalOrder", + total, roundedOrderTotal.FormatInvariant(), order.OrderTotal, roundedDifference.FormatInvariant()); - return RedirectToAction("Index", "Home", new { area = "" }); + if (settings.PdtValidateOnlyWarn) + { + OrderService.AddOrderNote(order, message); + } + else + { + Logger.Error(message); + + return RedirectToAction("Index", "Home", new { area = "" }); + } + } } - //mark order as paid - if (_orderProcessingService.CanMarkOrderAsPaid(order)) + // mark order as paid + var newPaymentStatus = GetPaymentStatus(payment_status, pending_reason, total, order.OrderTotal); + + if (newPaymentStatus == PaymentStatus.Paid) { - order.AuthorizationTransactionId = txn_id; - _orderService.UpdateOrder(order); + // note, order can be marked as paid through IPN + if (order.AuthorizationTransactionId.IsEmpty()) + { + order.AuthorizationTransactionId = order.CaptureTransactionId = txn_id; + order.AuthorizationTransactionResult = order.CaptureTransactionResult = "Success"; - _orderProcessingService.MarkOrderAsPaid(order); + OrderService.UpdateOrder(order); + } + + if (OrderProcessingService.CanMarkOrderAsPaid(order)) + { + OrderProcessingService.MarkOrderAsPaid(order); + } } } @@ -208,289 +210,23 @@ public ActionResult PDTHandler(FormCollection form) } else { - string orderNumber = string.Empty; - values.TryGetValue("custom", out orderNumber); - Guid orderNumberGuid = Guid.Empty; try { + values.TryGetValue("custom", out orderNumber); orderNumberGuid = new Guid(orderNumber); - } - catch { } - Order order = _orderService.GetOrderByGuid(orderNumberGuid); - if (order != null) - { - //order note - order.OrderNotes.Add(new OrderNote() - { - Note = "{0} {1}".FormatWith(_localizationService.GetResource("Plugins.Payments.PayPalStandard.PdtFailed"), response), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); - } - return RedirectToAction("Index", "Home", new { area = "" }); - } - } - - [ValidateInput(false)] - public ActionResult IPNHandler() - { - Debug.WriteLine("PayPal Standard IPN: {0}".FormatWith(Request.ContentLength)); - - byte[] param = Request.BinaryRead(Request.ContentLength); - string strRequest = Encoding.ASCII.GetString(param); - Dictionary values; - var provider = _paymentService.LoadPaymentMethodBySystemName("Payments.PayPalStandard", true); - var processor = provider != null ? provider.Value as PayPalStandardProvider : null; - if (processor == null) - throw new SmartException(_localizationService.GetResource("Plugins.Payments.PayPalStandard.NoModuleLoading")); - - if (processor.VerifyIPN(strRequest, out values)) - { - #region values - decimal total = decimal.Zero; - try - { - total = decimal.Parse(values["mc_gross"], new CultureInfo("en-US")); + var order = OrderService.GetOrderByGuid(orderNumberGuid); + OrderService.AddOrderNote(order, "{0} {1}".FormatInvariant(T("Plugins.Payments.PayPalStandard.PdtFailed"), response)); } catch { } - string payer_status = string.Empty; - values.TryGetValue("payer_status", out payer_status); - string payment_status = string.Empty; - values.TryGetValue("payment_status", out payment_status); - string pending_reason = string.Empty; - values.TryGetValue("pending_reason", out pending_reason); - string mc_currency = string.Empty; - values.TryGetValue("mc_currency", out mc_currency); - string txn_id = string.Empty; - values.TryGetValue("txn_id", out txn_id); - string txn_type = string.Empty; - values.TryGetValue("txn_type", out txn_type); - string rp_invoice_id = string.Empty; - values.TryGetValue("rp_invoice_id", out rp_invoice_id); - string payment_type = string.Empty; - values.TryGetValue("payment_type", out payment_type); - string payer_id = string.Empty; - values.TryGetValue("payer_id", out payer_id); - string receiver_id = string.Empty; - values.TryGetValue("receiver_id", out receiver_id); - string invoice = string.Empty; - values.TryGetValue("invoice", out invoice); - string payment_fee = string.Empty; - values.TryGetValue("payment_fee", out payment_fee); - - #endregion - - var sb = new StringBuilder(); - sb.AppendLine("PayPal IPN:"); - foreach (KeyValuePair kvp in values) - { - sb.AppendLine(kvp.Key + ": " + kvp.Value); - } - - var newPaymentStatus = GetPaymentStatus(payment_status, pending_reason); - sb.AppendLine("{0}: {1}".FormatWith(_localizationService.GetResource("Plugins.Payments.PayPalStandard.NewPaymentStatus"), newPaymentStatus)); - - switch (txn_type) - { - case "recurring_payment_profile_created": - //do nothing here - break; - case "recurring_payment": - #region Recurring payment - { - Guid orderNumberGuid = Guid.Empty; - try - { - orderNumberGuid = new Guid(rp_invoice_id); - } - catch { } - - var initialOrder = _orderService.GetOrderByGuid(orderNumberGuid); - if (initialOrder != null) - { - var recurringPayments = _orderService.SearchRecurringPayments(0, 0, initialOrder.Id, null); - foreach (var rp in recurringPayments) - { - switch (newPaymentStatus) - { - case PaymentStatus.Authorized: - case PaymentStatus.Paid: { - var recurringPaymentHistory = rp.RecurringPaymentHistory; - if (recurringPaymentHistory.Count == 0) - { - //first payment - var rph = new RecurringPaymentHistory() - { - RecurringPaymentId = rp.Id, - OrderId = initialOrder.Id, - CreatedOnUtc = DateTime.UtcNow - }; - rp.RecurringPaymentHistory.Add(rph); - _orderService.UpdateRecurringPayment(rp); - } - else - { - //next payments - _orderProcessingService.ProcessNextRecurringPayment(rp); - } - } - break; - } - } - - //this.OrderService.InsertOrderNote(newOrder.OrderId, sb.ToString(), DateTime.UtcNow); - Logger.Information(_localizationService.GetResource("Plugins.Payments.PayPalStandard.IpnLogInfo"), new SmartException(sb.ToString())); - } - else - { - Logger.Error(_localizationService.GetResource("Plugins.Payments.PayPalStandard.IpnOrderNotFound"), new SmartException(sb.ToString())); - } - } - #endregion - break; - default: - #region Standard payment - { - string orderNumber = string.Empty; - values.TryGetValue("custom", out orderNumber); - Guid orderNumberGuid = Guid.Empty; - try - { - orderNumberGuid = new Guid(orderNumber); - } - catch { } - - var order = _orderService.GetOrderByGuid(orderNumberGuid); - if (order != null) - { - //order note - order.HasNewPaymentNotification = true; - - order.OrderNotes.Add(new OrderNote - { - Note = sb.ToString(), - DisplayToCustomer = false, - CreatedOnUtc = DateTime.UtcNow - }); - _orderService.UpdateOrder(order); - - switch (newPaymentStatus) - { - case PaymentStatus.Pending: - { - } - break; - case PaymentStatus.Authorized: { - if (_orderProcessingService.CanMarkOrderAsAuthorized(order)) - { - _orderProcessingService.MarkAsAuthorized(order); - } - } - break; - case PaymentStatus.Paid: - { - if (_orderProcessingService.CanMarkOrderAsPaid(order)) - { - - order.AuthorizationTransactionId = txn_id; - _orderService.UpdateOrder(order); - - _orderProcessingService.MarkOrderAsPaid(order); - } - } - break; - case PaymentStatus.Refunded: { - if (_orderProcessingService.CanRefundOffline(order)) - { - _orderProcessingService.RefundOffline(order); - } - } - break; - case PaymentStatus.Voided: { - if (_orderProcessingService.CanVoidOffline(order)) - { - _orderProcessingService.VoidOffline(order); - } - } - break; - default: - break; - } - } - else - { - Logger.Error(_localizationService.GetResource("Plugins.Payments.PayPalStandard.IpnOrderNotFound"), new SmartException(sb.ToString())); - } - } - #endregion - break; - } - } - else - { - Logger.Error(_localizationService.GetResource("Plugins.Payments.PayPalStandard.IpnFailed"), new SmartException(strRequest)); + return RedirectToAction("Index", "Home", new { area = "" }); } - - //nothing should be rendered to visitor - return Content(""); } - /// - /// Gets a payment status - /// - /// PayPal payment status - /// PayPal pending reason - /// Payment status - public PaymentStatus GetPaymentStatus(string paymentStatus, string pendingReason) - { - var result = PaymentStatus.Pending; - - if (paymentStatus == null) - paymentStatus = string.Empty; - - if (pendingReason == null) - pendingReason = string.Empty; - - switch (paymentStatus.ToLowerInvariant()) - { - case "pending": - switch (pendingReason.ToLowerInvariant()) - { - case "authorization": - result = PaymentStatus.Authorized; - break; - default: - result = PaymentStatus.Pending; - break; - } - break; - case "processed": - case "completed": - case "canceled_reversal": - result = PaymentStatus.Paid; - break; - case "denied": - case "expired": - case "failed": - case "voided": - result = PaymentStatus.Voided; - break; - case "refunded": - case "reversed": - result = PaymentStatus.Refunded; - break; - default: - break; - } - return result; - } - public ActionResult CancelOrder(FormCollection form) { - var order = _orderService.SearchOrders(_storeContext.CurrentStore.Id, _workContext.CurrentCustomer.Id, null, null, null, null, null, null, null, null, 0, 1) + var order = OrderService.SearchOrders(Services.StoreContext.CurrentStore.Id, Services.WorkContext.CurrentCustomer.Id, null, null, null, null, null, null, null, null, 0, 1) .FirstOrDefault(); if (order != null) diff --git a/src/Plugins/SmartStore.PayPal/DependencyRegistrar.cs b/src/Plugins/SmartStore.PayPal/DependencyRegistrar.cs index 0285470f18..95e7e8f1c3 100644 --- a/src/Plugins/SmartStore.PayPal/DependencyRegistrar.cs +++ b/src/Plugins/SmartStore.PayPal/DependencyRegistrar.cs @@ -2,9 +2,9 @@ using Autofac.Integration.Mvc; using SmartStore.Core.Infrastructure; using SmartStore.Core.Infrastructure.DependencyManagement; -using SmartStore.Core.Plugins; -using SmartStore.Web.Controllers; using SmartStore.PayPal.Filters; +using SmartStore.PayPal.Services; +using SmartStore.Web.Controllers; namespace SmartStore.PayPal { @@ -12,9 +12,21 @@ public class DependencyRegistrar : IDependencyRegistrar { public virtual void Register(ContainerBuilder builder, ITypeFinder typeFinder, bool isActiveModule) { + builder.RegisterType().As().InstancePerRequest(); + if (isActiveModule) { builder.RegisterType().AsActionFilterFor(x => x.PaymentMethod()).InstancePerRequest(); + + builder.RegisterType().AsActionFilterFor(x => x.FlyoutShoppingCart()).InstancePerRequest(); + + builder.RegisterType() + .AsActionFilterFor(x => x.PaymentMethod()) + .InstancePerRequest(); + + builder.RegisterType() + .AsActionFilterFor(x => x.Completed()) + .InstancePerRequest(); } } diff --git a/src/Plugins/SmartStore.PayPal/Description.txt b/src/Plugins/SmartStore.PayPal/Description.txt index 84e4594446..b96ae172bb 100644 --- a/src/Plugins/SmartStore.PayPal/Description.txt +++ b/src/Plugins/SmartStore.PayPal/Description.txt @@ -1,10 +1,10 @@ FriendlyName: PayPal -Description: Provides the PayPal payment methods PayPal Express, PayPal Standard and PayPal Direct. +Description: Provides the PayPal payment methods PayPal Standard, PayPal Direct, PayPal Express and PayPal PLUS. SystemName: SmartStore.PayPal Group: Payment -Version: 2.2.0.1 -MinAppVersion: 2.2.0 +Version: 2.6.0.5 +MinAppVersion: 2.5.0 DisplayOrder: 1 FileName: SmartStore.PayPal.dll ResourceRootKey: Plugins.SmartStore.PayPal -Url: http://community.smartstore.com/index.php?/files/file/29-paypal-payment-plugins/ \ No newline at end of file +Url: http://community.smartstore.com/marketplace/file/29-paypal-payment-plugins/ \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Extensions/MiscExtensions.cs b/src/Plugins/SmartStore.PayPal/Extensions/MiscExtensions.cs new file mode 100644 index 0000000000..352d70d951 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Extensions/MiscExtensions.cs @@ -0,0 +1,40 @@ +using System.Net; +using System.Web; +using SmartStore.PayPal.Services; +using SmartStore.PayPal.Settings; +using SmartStore.Services.Orders; + +namespace SmartStore.PayPal +{ + internal static class MiscExtensions + { + public static string GetPayPalUrl(this PayPalSettingsBase settings) + { + return settings.UseSandbox ? + "https://www.sandbox.paypal.com/cgi-bin/webscr" : + "https://www.paypal.com/cgi-bin/webscr"; + } + + public static HttpWebRequest GetPayPalWebRequest(this PayPalSettingsBase settings) + { + if (settings.SecurityProtocol.HasValue) + { + ServicePointManager.SecurityProtocol = settings.SecurityProtocol.Value; + } + + var request = (HttpWebRequest)WebRequest.Create(GetPayPalUrl(settings)); + return request; + } + + public static PayPalSessionData GetPayPalSessionData(this HttpContextBase httpContext, CheckoutState state = null) + { + if (state == null) + state = httpContext.GetCheckoutState(); + + if (!state.CustomProperties.ContainsKey(PayPalPlusProvider.SystemName)) + state.CustomProperties.Add(PayPalPlusProvider.SystemName, new PayPalSessionData()); + + return state.CustomProperties[PayPalPlusProvider.SystemName] as PayPalSessionData; + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Filters/PayPalExpressCheckoutFilter.cs b/src/Plugins/SmartStore.PayPal/Filters/PayPalExpressCheckoutFilter.cs index 8536350363..317fae97a0 100644 --- a/src/Plugins/SmartStore.PayPal/Filters/PayPalExpressCheckoutFilter.cs +++ b/src/Plugins/SmartStore.PayPal/Filters/PayPalExpressCheckoutFilter.cs @@ -6,7 +6,6 @@ using SmartStore.Core.Domain.Customers; using SmartStore.Services; using SmartStore.Services.Common; -using SmartStore.Services.Customers; using SmartStore.Services.Payments; namespace SmartStore.PayPal.Filters @@ -15,19 +14,18 @@ public class PayPalExpressCheckoutFilter : IActionFilter { private static readonly string[] s_interceptableActions = new string[] { "PaymentMethod" }; - private readonly IGenericAttributeService _genericAttributeService; - private readonly HttpContextBase _httpContext; - private readonly ICommonServices _services; - private readonly ICustomerService _customerService; + private readonly IGenericAttributeService _genericAttributeService; + private readonly HttpContextBase _httpContext; + private readonly ICommonServices _services; - public PayPalExpressCheckoutFilter(IGenericAttributeService genericAttributeService, - HttpContextBase httpContext, ICommonServices services, - ICustomerService customerService) + public PayPalExpressCheckoutFilter( + IGenericAttributeService genericAttributeService, + HttpContextBase httpContext, + ICommonServices services) { - _genericAttributeService = genericAttributeService; - _httpContext = httpContext; - _services = services; - _customerService = customerService; + _genericAttributeService = genericAttributeService; + _httpContext = httpContext; + _services = services; } private static bool IsInterceptableAction(string actionName) @@ -37,36 +35,35 @@ private static bool IsInterceptableAction(string actionName) public void OnActionExecuting(ActionExecutingContext filterContext) { - if (filterContext == null || filterContext.ActionDescriptor == null || filterContext.HttpContext == null || filterContext.HttpContext.Request == null) - return; + if (filterContext == null || filterContext.ActionDescriptor == null || filterContext.HttpContext == null || filterContext.HttpContext.Request == null) + return; - string actionName = filterContext.ActionDescriptor.ActionName; + var attr = Convert.ToBoolean(filterContext.HttpContext.GetCheckoutState().CustomProperties["PayPalExpressButtonUsed"]); - var store = _services.StoreContext.CurrentStore; - var customer = _services.WorkContext.CurrentCustomer; - - var attr = Convert.ToBoolean(filterContext.HttpContext.GetCheckoutState().CustomProperties["PayPalExpressButtonUsed"]); + //verify paypalexpressprovider was used + if (attr == true) + { + var store = _services.StoreContext.CurrentStore; + var customer = _services.WorkContext.CurrentCustomer; - //verify paypalexpressprovider was used - if (attr == true) { - _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedPaymentMethod, "Payments.PayPalExpress", store.Id); + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.SelectedPaymentMethod, PayPalExpressProvider.SystemName, store.Id); - var paymentRequest = _httpContext.Session["OrderPaymentInfo"] as ProcessPaymentRequest; - if (paymentRequest == null) - { - _httpContext.Session["OrderPaymentInfo"] = new ProcessPaymentRequest(); - } + var paymentRequest = _httpContext.Session["OrderPaymentInfo"] as ProcessPaymentRequest; + if (paymentRequest == null) + { + _httpContext.Session["OrderPaymentInfo"] = new ProcessPaymentRequest(); + } - //delete property for backward navigation - _httpContext.GetCheckoutState().CustomProperties.Remove("PayPalExpressButtonUsed"); + //delete property for backward navigation + _httpContext.GetCheckoutState().CustomProperties.Remove("PayPalExpressButtonUsed"); - filterContext.Result = new RedirectToRouteResult( - new RouteValueDictionary { - { "Controller", "Checkout" }, - { "Action", "Confirm" }, - { "area", null } - }); - } + filterContext.Result = new RedirectToRouteResult( + new RouteValueDictionary { + { "Controller", "Checkout" }, + { "Action", "Confirm" }, + { "area", null } + }); + } } public void OnActionExecuted(ActionExecutedContext filterContext) diff --git a/src/Plugins/SmartStore.PayPal/Filters/PayPalExpressWidgetZoneFilter.cs b/src/Plugins/SmartStore.PayPal/Filters/PayPalExpressWidgetZoneFilter.cs new file mode 100644 index 0000000000..f5eaa49d36 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Filters/PayPalExpressWidgetZoneFilter.cs @@ -0,0 +1,69 @@ +using System; +using System.Web.Mvc; +using SmartStore.PayPal.Settings; +using SmartStore.Services; +using SmartStore.Services.Payments; +using SmartStore.Web.Framework.UI; +using SmartStore.Web.Models.ShoppingCart; + +namespace SmartStore.PayPal.Filters +{ + public class PayPalExpressWidgetZoneFilter : IActionFilter, IResultFilter + { + private readonly Lazy _widgetProvider; + private readonly Lazy _paymentService; + private readonly Lazy _services; + private readonly Lazy _payPalExpressSettings; + + public PayPalExpressWidgetZoneFilter( + Lazy widgetProvider, + Lazy paymentService, + Lazy services, + Lazy payPalExpressSettings) + { + _widgetProvider = widgetProvider; + _paymentService = paymentService; + _services = services; + _payPalExpressSettings = payPalExpressSettings; + } + + public void OnActionExecuting(ActionExecutingContext filterContext) + { + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { + } + + public void OnResultExecuting(ResultExecutingContext filterContext) + { + if (filterContext.IsChildAction) + return; + + // should only run on a full view rendering result + var result = filterContext.Result as ViewResultBase; + if (result == null) + return; + + var controller = filterContext.RouteData.Values["controller"] as string; + var action = filterContext.RouteData.Values["action"] as string; + + if (action.IsCaseInsensitiveEqual("FlyoutShoppingCart") && controller.IsCaseInsensitiveEqual("ShoppingCart")) + { + var model = filterContext.Controller.ViewData.Model as MiniShoppingCartModel; + + if (model != null && model.DisplayCheckoutButton && _payPalExpressSettings.Value.ShowButtonInMiniShoppingCart) + { + if (_paymentService.Value.IsPaymentMethodActive(PayPalExpressProvider.SystemName, _services.Value.StoreContext.CurrentStore.Id)) + { + _widgetProvider.Value.RegisterAction("mini_shopping_cart_bottom", "MiniShoppingCart", "PayPalExpress", new { area = "SmartStore.PayPal" }); + } + } + } + } + + public void OnResultExecuted(ResultExecutedContext filterContext) + { + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Filters/PayPalPlusCheckoutFilter.cs b/src/Plugins/SmartStore.PayPal/Filters/PayPalPlusCheckoutFilter.cs new file mode 100644 index 0000000000..7c47079830 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Filters/PayPalPlusCheckoutFilter.cs @@ -0,0 +1,49 @@ +using System; +using System.Web.Mvc; +using System.Web.Routing; +using SmartStore.Core.Domain.Customers; +using SmartStore.Services; +using SmartStore.Services.Common; +using SmartStore.Services.Payments; + +namespace SmartStore.PayPal.Filters +{ + public class PayPalPlusCheckoutFilter : IActionFilter + { + private readonly ICommonServices _services; + private readonly IPaymentService _paymentService; + private readonly Lazy _genericAttributeService; + + public PayPalPlusCheckoutFilter( + ICommonServices services, + IPaymentService paymentService, + Lazy genericAttributeService) + { + _services = services; + _paymentService = paymentService; + _genericAttributeService = genericAttributeService; + } + + public void OnActionExecuting(ActionExecutingContext filterContext) + { + if (filterContext == null || filterContext.ActionDescriptor == null || filterContext.HttpContext == null || filterContext.HttpContext.Request == null) + return; + + var store = _services.StoreContext.CurrentStore; + + if (!_paymentService.IsPaymentMethodActive(PayPalPlusProvider.SystemName, store.Id)) + return; + + _genericAttributeService.Value.SaveAttribute(_services.WorkContext.CurrentCustomer, SystemCustomerAttributeNames.SelectedPaymentMethod, PayPalPlusProvider.SystemName, store.Id); + + var routeValues = new RouteValueDictionary(new { action = "PaymentWall", controller = "PayPalPlus" }); + + filterContext.Result = new RedirectToRouteResult("SmartStore.PayPalPlus", routeValues); + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { + + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Filters/PayPalPlusWidgetZoneFilter.cs b/src/Plugins/SmartStore.PayPal/Filters/PayPalPlusWidgetZoneFilter.cs new file mode 100644 index 0000000000..cbe12c7aaf --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Filters/PayPalPlusWidgetZoneFilter.cs @@ -0,0 +1,58 @@ +using System; +using System.Web; +using System.Web.Mvc; +using SmartStore.Web.Framework.UI; + +namespace SmartStore.PayPal.Filters +{ + public class PayPalPlusWidgetZoneFilter : IActionFilter, IResultFilter + { + private readonly Lazy _httpContext; + private readonly Lazy _widgetProvider; + + public PayPalPlusWidgetZoneFilter( + Lazy httpContext, + Lazy widgetProvider) + { + _httpContext = httpContext; + _widgetProvider = widgetProvider; + } + + public void OnActionExecuting(ActionExecutingContext filterContext) + { + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { + } + + public void OnResultExecuting(ResultExecutingContext filterContext) + { + if (filterContext.IsChildAction) + return; + + // should only run on a full view rendering result + var result = filterContext.Result as ViewResultBase; + if (result == null) + return; + + var controller = filterContext.RouteData.Values["controller"] as string; + var action = filterContext.RouteData.Values["action"] as string; + + if (action.IsCaseInsensitiveEqual("Completed") && controller.IsCaseInsensitiveEqual("Checkout")) + { + var instruct = _httpContext.Value.Session[PayPalPlusProvider.CheckoutCompletedKey] as string; + + if (instruct.HasValue()) + { + _widgetProvider.Value.RegisterAction("checkout_completed_top", "CheckoutCompleted", "PayPalPlus", new { area = Plugin.SystemName }); + _widgetProvider.Value.RegisterAction("mobile_checkout_completed_top", "CheckoutCompleted", "PayPalPlus", new { area = Plugin.SystemName }); + } + } + } + + public void OnResultExecuted(ResultExecutedContext filterContext) + { + } + } +} diff --git a/src/Plugins/SmartStore.PayPal/Localization/resources.de-de.xml b/src/Plugins/SmartStore.PayPal/Localization/resources.de-de.xml index 0b796c2b89..0568ab1572 100644 --- a/src/Plugins/SmartStore.PayPal/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.PayPal/Localization/resources.de-de.xml @@ -1,29 +1,100 @@  - PayPal Zahlungsmethoden + PayPal Zahlungsarten - Stellt die PayPal-Zahlungsmethoden PayPal Express, PayPal Standard und PayPal Direct zur Verfügung. + Stellt die PayPal-Zahlungsarten PayPal Standard, PayPal Direct, PayPal Express und PayPal PLUS zur Verfügung. + + API Zugang;Datenaustausch;Sonstiges + + + {0}]]> + + + Mitteilung;Ereignis;Ereignis-ID;Status;Betrag;Zahlungs-ID + + + Sonstiges + + + {0} hat die Forderung gegen Sie im Rahmen eines laufenden Factoringvertrages an die PayPal (Europe) S.àr.l. et Cie, S.C.A. abgetreten. Zahlungen mit schuldbefreiender Wirkung können nur an die PayPal (Europe) S.àr.l. et Cie, S.C.A. geleistet werden. + + + Bitte überweisen Sie den fälligen Betrag auf folgendes Konto. + + + Referenz;Bankleitzahl;Bank;BIC;IBAN;Kontoinhaber;Kontonummer;Betrag;Zahlung fällig am;Details + + + Mitteilung wurde ignoriert, da {0} Aufträge mit der Payment-ID {1} gefunden wurden. + + + Währung {0} entspricht nicht der Leitwährung des Shops {1}. + + + Keine Weiterleitungs-URL von PayPal erhalten. + + + + Client-ID + + + Legt die API Client-ID fest. + + + Secret + + + Legt die API Secret fest. + + + Bitte geben Sie Ihre API Zugangsdaten ein. + + + Experience Profil-ID + + + Legt die Experience Profil-ID fest. Das Profil beinhaltet globale Daten wie z.B. Shop-Name und -Logo. Ein Profil braucht nur einmalig angelegt werden. Über den Button können Sie ein neues Profil erstellen oder ein vorhandenes aktualisieren. + + + Experience Profil-ID + + + Webhook-ID + + + Legt die Webhook-ID fest. Über einen Webhook sendet PayPal Statusänderungen von Zahlungen an Ihren Shop, wodurch der Zahlungsstatus Ihrer Aufträge automatisch aktualisert werden kann. + + - - Ihre Einstellungen wurden erfolgreich gespeichert. - Sandbox benutzen Aktiviert die Nutzung der PayPal Sandbox (Testumgebung). + + Zahlungsstatus über IPN ändern + + + Legt fest, ob durch eingehende IPNs der Zahlunsgstatus eines Auftrags angepasst werden soll. + Transaktionsmodus Bestimmen Sie den Transaktionsmodus. + + Sicherheitsprotokoll + + + Legt das mit der PayPal-API zu verwendende Sicherheitsprotokoll fest. + API Benutzername @@ -54,11 +125,34 @@ Zusätzliche prozentuale Gebühr zum Gesamtbetrag. Es wird ein fester Wert verwendet, falls diese Option nicht aktiviert ist. + + Das Zahlungsmodul Modul konnte nicht geladen werden. + + + PayPal IPN. Wiederkehrende Zahlung. + + + PayPal IPN. Bestellung konnte nicht gefunden werden. + + + PayPal IPN. Benachrichtigung konnte nicht abgerufen werden. + + + Teilerstattung von {0} + + + Zahlartgebühren + + + Rabatt + + + Angewendeter Geschenkgutschein + - - - + + PayPal Express @@ -78,29 +172,14 @@
    1. In Ihr Premier- oder Business-Konto einloggen.
    2. Auf den Register Mein Profil klicken.
    3. +
    4. Unter Sprach-Kodierung > Weitere Einstellungen wählen Sie bitte UTF-8.
    5. Sofortige Zahlungsbestätigung klicken.
    6. Einstellungen für sofortige Zahlungsbestätigungen wählen klicken.
    7. -
    8. Bei Benachrichtigungs-URL die URL Ihres IPN-Handlers (http://www.yourStore.com/Plugins/PayPalExpress/IPNHandler) eingeben.
    9. +
    10. Bei Benachrichtigungs-URL die URL Ihres IPN-Handlers (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalExpress/IPNHandler) eingeben.
    11. Speichern klicken. Danach sollten Sie eine Nachricht über die erfolgreiche Aktivierung von IPN erhalten.
    ]]> -
    - - - PayPal-Express-Modul konnte nicht geladen werden. - - - Neuer Zahlungsstatus - - - PayPal Express IPN. Wiederkehrende Information. - - - PayPal Express IPN. Bestellung konnte nicht gefunden werden. - - - PayPal Express IPN. Benachrichtigung konnte nicht abgerufen werden. - +
    Wiederkehrende Zahlung @@ -113,28 +192,26 @@ Autorisierung sofort, Abbuchung später - oder - - - Checkout-Button auf der Warenkorbseite aktivieren + + Button im Miniwarenkorb anzeigen - - Aktiviert den Checkout-Button auf der Warenkorbseite. + + Legt fest, ob der Checkout-Button im Miniwarenkorb angezeigt werden soll. - Bestätigte Emailadresse + Bestätigte E-Mail-Adresse - Wählen Sie ob sich bei der verwendetet Emailadresse, um eine von PayPal bestätigte Emailadresse handeln muss. + Legt fest, ob es sich bei der verwendeten E-Mail-Adresse, um eine von PayPal bestätigte Adresse handeln muss. Anzeige der Lieferadresse unterdrücken - Bestimmen Sie ob auf der PayPal Seite die Lieferadresse angezeigt werden soll. + Legt fest, ob auf der PayPal Seite die Lieferadresse angezeigt werden soll. Zeitlimit für API-Callback @@ -155,7 +232,6 @@ PayPal Direct - @@ -173,28 +249,14 @@
    1. In Ihr Premier- oder Business-Konto einloggen.
    2. Auf den Register Mein Profil klicken.
    3. +
    4. Unter Sprach-Kodierung > Weitere Einstellungen wählen Sie bitte UTF-8.
    5. Sofortige Zahlungsbestätigung klicken.
    6. Einstellungen für sofortige Zahlungsbestätigungen wählen klicken.
    7. -
    8. Bei Benachrichtigungs-URL die URL Ihres IPN-Handlers (http://www.yourStore.com/Plugins/PaymentPayPalDirect/IPNHandler) eingeben.
    9. +
    10. Bei Benachrichtigungs-URL die URL Ihres IPN-Handlers (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalDirect/IPNHandler) eingeben.
    11. Speichern klicken. Danach sollten Sie eine Nachricht über die erfolgreiche Aktivierung von IPN erhalten.
    ]]>
    - - PayPal-Direct-Modul konnte nicht geladen werden. - - - Neuer Zahlungsstatus - - - PayPal Direct IPN. Wiederkehrende Information. - - - PayPal Direct IPN. Bestellung konnte nicht gefunden werden. - - - PayPal Direct IPN. Benachrichtigung konnte nicht abgerufen werden. - Wiederkehrende Zahlung @@ -213,12 +275,11 @@ PayPal Standard - - Stellen Sie bitte sicher, dass PayPal die Primärwährung Ihres Shops unterstützt, falls Sie dieses Gateway benutzen!

    + Stellen Sie bitte sicher, dass PayPal die Primärwährung Ihres Shops unterstützt, falls Sie dieses Gateway benutzen.

    Sie müssen PDT (Payment Data Transfer) und die automatische Rückleitung in Ihrem PayPal-Profil aktivieren, um PDT nutzen zu können. Ferner benötigen Sie einen PDT-Identitäts-Token, der für jede PDT-Kommunikation mit PayPal erforderlich ist. Folgen Sie den Schritten, um Ihr Konto für PDT zu konfigurieren:

    @@ -227,7 +288,7 @@
  • Klicken Sie auf den Register Mein Profil.
  • Klicken Sie unter Verkäufer/Händler auf Website-Einstellungen.
  • Aktivieren Sie Automatische Rückleitung.
  • -
  • Als Rückleitungs-URL geben Sie bitte die Seite Ihres Shops ein, die die Transaktions-ID von PayPal nach einer Kundenzahlung empfängt (http://www.yourStore.com/Plugins/PaymentPayPalStandard/PDTHandler).
  • +
  • Als Rückleitungs-URL geben Sie bitte die Seite Ihres Shops ein, die die Transaktions-ID von PayPal nach einer Kundenzahlung empfängt (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalStandard/PDTHandler).
  • Aktivieren Sie Übertragung der Zahlungsdaten.
  • Klicken Sie auf Speichern.
  • Klicken Sie unter Verkäufer/Händler auf Website-Einstellungen.
  • @@ -241,9 +302,10 @@
    1. In Ihr Premier- oder Business-Konto einloggen.
    2. Auf den Register Mein Profil klicken.
    3. -
    4. Sofortige Zahlungsbestätigung klicken.
    5. +
    6. Unter Sprach-Kodierung > Weitere Einstellungen wählen Sie bitte UTF-8.
    7. +
    8. Zurück zu Mein Profil und auf Sofortige Zahlungsbestätigung klicken.
    9. Einstellungen für sofortige Zahlungsbestätigungen wählen klicken.
    10. -
    11. Bei Benachrichtigungs-URL die URL Ihres IPN-Handlers (http://www.yourStore.com/Plugins/PaymentPayPalStandard/IPNHandler) eingeben.
    12. +
    13. Bei Benachrichtigungs-URL die URL Ihres IPN-Handlers (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalStandard/IPNHandler) eingeben.
    14. Speichern klicken. Danach sollten Sie eine Nachricht über die erfolgreiche Aktivierung von IPN erhalten.
    @@ -271,23 +333,17 @@ Aktivieren Sie diese Option, falls der durch PayPal übermittelte Gesamtbetrag überprüft werden soll. - - Zusätzliche Gebühren - - - Zusätzliche Gebühren, die dem Kunden berechnet werden sollen. - - - Zusätzliche Gebühren (prozentual) + + Nur warnen - - Zusätzliche prozentuale Gebühr zum Gesamtbetrag. Es wird ein fester Wert verwendet, falls diese Option nicht aktiviert ist. + + Legt fest, ob im Falle einer Nichtübereinstimmung des Gesamtbetrags lediglich eine Auftragsnotiz angelegt werden soll. Ansonsten wird ein Fehler erzeugt und der Zahlungsstatus wird nicht aktualisiert. - Produktbezeichnung und Gesamtbetrag an PayPal übermitteln + Produktbezeichnungen und Einzelpreise übermitteln - Aktivieren Sie diese Option, falls die Produktbezeichnung und der Gesamtbetrag an PayPal übermitteln werden sollen. + Aktivieren Sie diese Option, falls Produktbezeichnungen und Einzelpreise an PayPal übermittelt werden sollen. IPN aktivieren @@ -304,58 +360,94 @@ URL zum IPN-Handler festlegen. - - PayPal Standard Modul konnte nicht geladen werden. - PayPal Standard PDT. Fehler beim Abruf von mc_gross. - PayPal Standard PDT. Der übermittelte Gesamtbetrag {0} entspricht nicht dem des Shops {1}. + PayPal Standard PDT. Der übermittelte Gesamtbetrag {0} entspricht nicht dem des Shops {1} (ungerundet {2}). Die Differenz beträgt {3}. PayPal Standard PDT ist fehlgeschlagen. - - Neuer Zahlungsstatus - - - PayPal Standard IPN. Wiederkehrende Information. - - - PayPal Standard IPN. Bestellung konnte nicht gefunden werden. - - - PayPal Standard IPN. Benachrichtigung konnte nicht abgerufen werden. - Unvollständige Zugangsdaten für die PayPal API. Bitte geben Sie die erforderlichen Zugangsdaten im Konfigurationsbereich ein. Versandgebühren - - Zahlungsgebühren - Umsatzsteuer +Total: {0} +Währung: {1} +Zahlenderstatus: {2} +Zahlungsstatus: {3} +Ausstehend (Grund): {4} +Transaktions-ID: {5} +Zahlungstyp: {6} +Zahlender-ID: {7} +Empfänger-ID: {8} +Rechnung: {9} +Zahlungsgebühr: {10}]]> + + +
    +
    + + + PayPal PLUS + + + + + + PayPal PLUS bietet die Möglichkeit per PayPal, Kreditkarte, Lastschriftverfahren und per Rechnung zu bezahlen. +Die Bezahlung auf Rechnung steht möglicherweise nicht allen Händlern zur Verfügung. PayPal PLUS unterstützt nur den Transaktionstyp der sofortigen Buchung und ist derzeit nur in Deutschland verfügbar.

    +

    PayPal PLUS ersetzt die Zahlartenliste im Kassenbereich durch die PayPal Payment Wall. Sie können der Payment Wall jedoch bis zu 5 weitere Offline- bzw. Weiterleitungszahlarten hinzufügen.

    +

    So richten Sie PayPal PLUS ein: +

      +
    1. Legen Sie unter My Apps & Credentials eine neue REST API Anwendung an. +Unter Manage your applications finden Sie weitere Informationen zum Thema PayPal Anwendungen (Englisch).
    2. +
    3. Klicken Sie auf den Namen der Anwendung und tragen Sie Client-ID und Secret weiter unten auf dieser Seite ein. Speichern.
    4. +
    5. Optional: Klicken Sie unten bei Experience Profil-ID auf Hinzufügen, um ein Profil zu erstellen. Ihre Shop-Daten (Name, Logo etc.) werden Käufern dadurch beim Bezahlen auf den PayPal-Seiten angezeigt.
    6. +
    7. Optional: Klicken Sie unten bei Webhook-ID auf Hinzufügen, um einen Webhook zu erstellen. Über einen Webhook empfängt der Shop PayPal-Mitteilungen und aktualisiert u.U. den Zahlungsstatus Ihrer Aufträge.
    8. +

    ]]>
    + + Wir können Ihnen leider keine PayPal PLUS Zahlungsart anbieten. Bitte versuchen Sie er später erneut. + + + Entschuldigung, das hat gerade nicht funktioniert. Bitte versuchen Sie es erneut oder wählen Sie eine andere Zahlungsmethode. + + + .]]> + + + Weitere Zahlarten + + + Sie können bis zu 5 weitere Zahlarten festlegen. Diese werden zusammen mit den PayPal PLUS Zahlarten im Kassenbereich angeboten. + + + Es können maximal 5 weitere Zahlarten festgelegt werden. + + + Logo der Zahlarten anzeigen + + + Legt fest, ob für die weiteren Zahlarten deren Logo angezeigt werden soll. SSL erforderlich. + + + Beschreibung der Zahlarten anzeigen + + + Legt fest, ob für die weiteren Zahlarten deren Beschreibung angezeigt werden soll. +
    -
    +
    +
    \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Localization/resources.en-us.xml b/src/Plugins/SmartStore.PayPal/Localization/resources.en-us.xml index 529e7c2e12..700b806aa7 100644 --- a/src/Plugins/SmartStore.PayPal/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.PayPal/Localization/resources.en-us.xml @@ -4,7 +4,152 @@ PayPal Payment Methods - Provides the PayPal payment methods PayPal Express, PayPal Standard and PayPal Direct. + Provides the PayPal payment methods PayPal Standard, PayPal Direct, PayPal Express and PayPal PLUS. + + + + API access;Data exchange;Miscellaneous + + + {0}]]> + + + Message;Event;Event-ID;State;Amount;Payment-ID + + + Miscellaneous + + + The execution of the payment has been declined by PayPal. + + + {0} hat die Forderung gegen Sie im Rahmen eines laufenden Factoringvertrages an die PayPal (Europe) S.àr.l. et Cie, S.C.A. abgetreten. Zahlungen mit schuldbefreiender Wirkung können nur an die PayPal (Europe) S.àr.l. et Cie, S.C.A. geleistet werden. + + + Please transfer the due amount to the following bank account. + + + Reference;Bank routing number;Bank;BIC;IBAN;Account holder;Account number;Amount;Payment due date;Details + + + Message has been ignored because {0} orders with payment ID {1} found. + + + Currency {0} does not equal primary store currency {1}. + + + No redirect URL received from PayPal. + + + + Client ID + + + Specifies the API client ID. + + + Secret + + + Specifies the API secret. + + + Please enter your API credentials. + + + Experience profile ID + + + Specifies the experience profile ID. The profile contains global data like shop name and logo. A profile needs to be created only once. Using the button, you can create a new profile or update an existing one. + + + Webhook ID + + + Specifies the webhook ID. PayPal sends payment status changes via a webhook to your shop, whereby the payment status of your orders can be automatically synced. + + + + + + Use Sandbox + + + Check the box to enable Sandbox (testing environment). + + + IPN may change payment status + + + Specifies whether received IPNs should change the payment status of an order. + + + Transaction mode + + + Specify the payment transaction mode. + + + Security protocol + + + Specifies the security protocol to use with the PayPal API. + + + API Account Name + + + Specify the API account name. + + + API Account Password + + + Specify the API account password. + + + Signature + + + Enter the signature. + + + Additional fee + + + Enter additional fee to charge your customers. + + + Additional fee. Use percentage. + + + Specifies whether to apply a percentage additional fee to the order total. A fixed value is used if not enabled. + + + The payment module cannot be loaded. + + + PayPal IPN. Recurring payment. + + + PayPal IPN. Order cannot be found. + + + PayPal IPN. Failed to retrieve notification. + + + Partial refund of {0} + + + Payment method fee + + + Discount + + + Giftcard Applied + + @@ -28,72 +173,14 @@
    1. Log in to your Premier or Business account.
    2. Click the Profile subtab.
    3. +
    4. Click Language Encoding and More options and select UTF-8.
    5. Click Instant Payment Notification in the Selling Preferences column.
    6. Click the Edit IPN Settings button to update your settings.
    7. -
    8. Select Receive IPN messages (enabled) and enter the URL of your IPN handler (http://www.yourStore.com/Plugins/PayPalExpress/IPNHandler).
    9. +
    10. Select Receive IPN messages (enabled) and enter the URL of your IPN handler (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalExpress/IPNHandler).
    11. Click Save, and you should get a message that you have successfully activated IPN.
    ]]> -
    - - - Use Sandbox - - - Check to enable Sandbox (testing environment). - - - Transaction mode - - - Specify transaction mode. - - - API Account Name - - - Specify API account name. - - - API Account Password - - - Specify API account password. - - - Signature - - - Specify signature. - - - Additional fee - - - Enter additional fee to charge your customers. - - - Additional fee. Use percentage - - - Determines whether to apply a percentage additional fee to the order total. A fixed value is used if not enabled. - - - - PayPal Express module cannot be loaded. - - - New payment status - - - PayPal Express IPN. Recurring info. - - - PayPal Express IPN. Order cannot be found. - - - PayPal Express IPN. Failed to retrieve notification. - + recurring payment @@ -106,16 +193,14 @@ Authorize immediately, debit later - or - - - Activate checkout button in shopping cart + + Show button in mini shopping cart - - Activates the checkout button in the shopping cart. + + Soecifies to show the checkout button in the mini shopping cart. Confirmed email address @@ -147,7 +232,6 @@ PayPal Direct - @@ -165,70 +249,14 @@
    1. Log in to your Premier or Business account.
    2. Click the Profile subtab.
    3. +
    4. Click Language Encoding and More options and select UTF-8.
    5. Click Instant Payment Notification in the Selling Preferences column.
    6. Click the Edit IPN Settings button to update your settings.
    7. -
    8. Select Receive IPN messages (enabled) and enter the URL of your IPN handler (http://www.yourStore.com/Plugins/PaymentPayPalDirect/IPNHandler).
    9. +
    10. Select Receive IPN messages (enabled) and enter the URL of your IPN handler (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalDirect/IPNHandler).
    11. Click Save, and you should get a message that you have successfully activated IPN.
    ]]>
    - - Use Sandbox - - - Check to enable Sandbox (testing environment). - - - Transaction mode - - - Specify transaction mode. - - - API Account Name - - - Specify API account name. - - - API Account Password - - - Specify API account password. - - - Signature - - - Specify signature. - - - Additional fee - - - Enter additional fee to charge your customers. - - - Additional fee. Use percentage - - - Determines whether to apply a percentage additional fee to the order total. A fixed value is used if not enabled. - - - PayPal Direct module cannot be loaded. - - - New payment status - - - PayPal Direct IPN. Recurring info. - - - PayPal Direct IPN. Order cannot be found. - - - PayPal Direct IPN. Failed to retrieve notification. - recurring payment @@ -257,10 +285,10 @@ you send to PayPal. Follow these steps to configure your account for PDT:

    1. Log in to your PayPal account.
    2. -
    3. Click the Profile subtab.
    4. +
    5. Click the Profile subtab.
    6. Click Website Payment Preferences in the Seller Preferences column.
    7. Under Auto Return for Website Payments, click the On radio button.
    8. -
    9. For the Return URL, enter the URL on your site that will receive the transaction ID posted by PayPal after a customer payment (http://www.yourStore.com/Plugins/PaymentPayPalStandard/PDTHandler).
    10. +
    11. For the Return URL, enter the URL on your site that will receive the transaction ID posted by PayPal after a customer payment (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalStandard/PDTHandler).
    12. Under Payment Data Transfer, click the On radio button.
    13. Click Save.
    14. Click Website Payment Preferences in the Seller Preferences column.
    15. @@ -273,10 +301,11 @@ The second way is to configure your paypal account to activate this service; follow these steps:
      1. Log in to your Premier or Business account.
      2. -
      3. Click the Profile subtab.
      4. -
      5. Click Instant Payment Notification in the Selling Preferences column.
      6. +
      7. Click the Profile subtab.
      8. +
      9. Click Language Encoding and More options and select UTF-8.
      10. +
      11. Back to Profile click Instant Payment Notification in the Selling Preferences column.
      12. Click the Edit IPN Settings button to update your settings.
      13. -
      14. Select Receive IPN messages (Enabled) and enter the URL of your IPN handler (http://www.yourStore.com/Plugins/PaymentPayPalStandard/IPNHandler).
      15. +
      16. Select Receive IPN messages (Enabled) and enter the URL of your IPN handler (https://www.yourStore.com/Plugins/SmartStore.PayPal/PayPalStandard/IPNHandler).
      17. Click Save, and you should get a message that you have successfully activated IPN.
      @@ -286,12 +315,6 @@ After confirmation of the order, you will be redirected straight to PayPal. Please provide your data for online banking. - - Use Sandbox - - - Check to enable Sandbox (testing environment). - Business Email @@ -308,31 +331,25 @@ Validate order total - Check if the PDT handler should validate PayPal's order totals. - - - Additional fee - - - Enter additional fee to charge your customers. + Check the box if the PDT handler should validate PayPal's order totals. - - Additional fee. Use percentage + + Only warn - - Determines whether to apply a percentage additional fee to the order total. If not enabled, a fixed value is used. + + Specifies whether to add an order note if the order total is unequal. Otherwise an error will be logged and the payment status is not updated. - Pass product names and order totals to PayPal + Transmit product names and unit prices - Check if product names and order totals should be passed to PayPal. + Check the box if product names and unit prices should be transmitted to PayPal. Enable IPN - Check if IPN (Instant Payment Notification) is enabled. + Check the box if IPN (Instant Payment Notification) is enabled. Leave blank to use the default IPN handler URL. @@ -343,58 +360,94 @@ Specify IPN Handler. - - PayPal Standard module cannot be loaded. - PayPal Standard PDT. Error getting mc_gross. - PayPal Standard PDT. Returned order total {0} doesn't equal order total {1}. + PayPal Standard PDT. Returned order total {0} doesn't equal order total {1} (not rounded {2}). The difference is {3}. PayPal Standard PDT failed. - - New payment status - - - PayPal Standard IPN. Recurring info. - - - PayPal Standard IPN. Order cannot be found. - - - PayPal Standard IPN. Failed to retrieve notification. - Incomplete credentials for the PayPal API. Please enter the required data in the configuration area. Shipping fee - - Payment method fee - Sales tax +Total: {0} +Currency: {1} +Payer status: {2} +Payment status: {3} +Pending reason: {4} +Transaction ID: {5} +Payment type: {6} +Payer ID: {7} +Receiver ID: {8} +Invoice: {9} +Payment fee: {10}]]> + + + PayPal PLUS + + + + + + PayPal PLUS is a solution where PayPal offers PayPal, Credit Card, Direct Debit (ELV) and pay upon invoice as individual payment options on the payment selection page. +Pay upon invoice may not be available for all merchants. PayPal PLUS only supports the transaction type of instant settlement and is only available in Germany at the moment.

      +

      PayPal PLUS replaces the list of payment methods in checkout by the PayPal payment wall. You can add up to 5 offline or redirection payment methods to the payment wall.

      +

      How to set up PayPal PLUS: +

        +
      1. Create a new REST API application under My Apps & Credentials. +Go to Manage your applications to get more information about PayPal applications.
      2. +
      3. Click the name of the application and enter Client-ID and Secret below on this page. Save.
      4. +
      5. Optional: Click Add next to Experience profile ID to create a new profile. Your shop data (name, logo etc.) can now be displayed to customers when paying on PayPal pages.
      6. +
      7. Optional: Click Add next to Webhook-ID to create a webhook. Through a webhook the shop receives PayPal messages und possibly updates the payment state of your orders.
      8. +

      ]]> +
      +
      + + Unfortunately we cannot offer you a PayPal PLUS payment method. Please try again later. + + + I'm sorry, that just didn't work. Please try again or select another payment method. + + + .]]> + + + More payment methods + + + You can set up to 5 more payment methods. These are offered along with the PayPal PLUS payment methods during the checkout process. + + + Maximum of 5 third party payment methods can be specified. + + + Display logo of other payment methods + + + Specifies whether to display the logo for other payment methods. SSL required. + + + Display description of other payment methods + + + Specifies whether to display the description for other payment methods. + +
      +
      + \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Models/ApiConfigurationModels.cs b/src/Plugins/SmartStore.PayPal/Models/ApiConfigurationModels.cs index 91ac993572..f81f6c2d7f 100644 --- a/src/Plugins/SmartStore.PayPal/Models/ApiConfigurationModels.cs +++ b/src/Plugins/SmartStore.PayPal/Models/ApiConfigurationModels.cs @@ -1,23 +1,32 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Net; using System.Web.Mvc; using SmartStore.PayPal.Settings; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.PayPal.Models { - public abstract class ApiConfigurationModel: ModelBase + public abstract class ApiConfigurationModel: ModelBase { public string[] ConfigGroups { get; set; } - [SmartResourceDisplayName("Plugins.Payments.PayPal.UseSandbox")] + [SmartResourceDisplayName("Plugins.Payments.PayPal.UseSandbox")] public bool UseSandbox { get; set; } + [SmartResourceDisplayName("Plugins.Payments.PayPal.IpnChangesPaymentStatus")] + public bool IpnChangesPaymentStatus { get; set; } + [SmartResourceDisplayName("Plugins.Payments.PayPal.TransactMode")] public int TransactMode { get; set; } public SelectList TransactModeValues { get; set; } + [SmartResourceDisplayName("Plugins.Payments.PayPal.SecurityProtocol")] + public SecurityProtocolType? SecurityProtocol { get; set; } + public List AvailableSecurityProtocols { get; set; } + [SmartResourceDisplayName("Plugins.Payments.PayPal.ApiAccountName")] public string ApiAccountName { get; set; } @@ -28,6 +37,18 @@ public abstract class ApiConfigurationModel: ModelBase [SmartResourceDisplayName("Plugins.Payments.PayPal.Signature")] public string Signature { get; set; } + [SmartResourceDisplayName("Plugins.SmartStore.PayPal.ClientId")] + public string ClientId { get; set; } + + [SmartResourceDisplayName("Plugins.SmartStore.PayPal.Secret")] + public string Secret { get; set; } + + [SmartResourceDisplayName("Plugins.SmartStore.PayPal.ExperienceProfileId")] + public string ExperienceProfileId { get; set; } + + [SmartResourceDisplayName("Plugins.SmartStore.PayPal.WebhookId")] + public string WebhookId { get; set; } + [SmartResourceDisplayName("Plugins.Payments.PayPal.AdditionalFee")] public decimal AdditionalFee { get; set; } @@ -41,7 +62,9 @@ public void Copy(PayPalDirectPaymentSettings settings, bool fromSettings) { if (fromSettings) { - UseSandbox = settings.UseSandbox; + SecurityProtocol = settings.SecurityProtocol; + UseSandbox = settings.UseSandbox; + IpnChangesPaymentStatus = settings.IpnChangesPaymentStatus; TransactMode = Convert.ToInt32(settings.TransactMode); ApiAccountName = settings.ApiAccountName; ApiAccountPassword = settings.ApiAccountPassword; @@ -51,7 +74,9 @@ public void Copy(PayPalDirectPaymentSettings settings, bool fromSettings) } else { - settings.UseSandbox = UseSandbox; + settings.SecurityProtocol = SecurityProtocol; + settings.UseSandbox = UseSandbox; + settings.IpnChangesPaymentStatus = IpnChangesPaymentStatus; settings.TransactMode = (TransactMode)TransactMode; settings.ApiAccountName = ApiAccountName; settings.ApiAccountPassword = ApiAccountPassword; @@ -64,10 +89,10 @@ public void Copy(PayPalDirectPaymentSettings settings, bool fromSettings) public class PayPalExpressConfigurationModel : ApiConfigurationModel { - [SmartResourceDisplayName("Plugins.Payments.PayPalExpress.Fields.DisplayCheckoutButton")] - public bool DisplayCheckoutButton { get; set; } + [SmartResourceDisplayName("Plugins.Payments.PayPalExpress.Fields.ShowButtonInMiniShoppingCart")] + public bool ShowButtonInMiniShoppingCart { get; set; } - [SmartResourceDisplayName("Plugins.Payments.PayPalExpress.Fields.ConfirmedShipment")] + [SmartResourceDisplayName("Plugins.Payments.PayPalExpress.Fields.ConfirmedShipment")] public bool ConfirmedShipment { get; set; } [SmartResourceDisplayName("Plugins.Payments.PayPalExpress.Fields.NoShipmentAddress")] @@ -83,14 +108,16 @@ public void Copy(PayPalExpressPaymentSettings settings, bool fromSettings) { if (fromSettings) { - UseSandbox = settings.UseSandbox; - TransactMode = Convert.ToInt32(settings.TransactMode); - ApiAccountName = settings.ApiAccountName; + SecurityProtocol = settings.SecurityProtocol; + UseSandbox = settings.UseSandbox; + IpnChangesPaymentStatus = settings.IpnChangesPaymentStatus; + TransactMode = Convert.ToInt32(settings.TransactMode); + ApiAccountName = settings.ApiAccountName; ApiAccountPassword = settings.ApiAccountPassword; Signature = settings.Signature; AdditionalFee = settings.AdditionalFee; AdditionalFeePercentage = settings.AdditionalFeePercentage; - DisplayCheckoutButton = settings.DisplayCheckoutButton; + ShowButtonInMiniShoppingCart = settings.ShowButtonInMiniShoppingCart; ConfirmedShipment = settings.ConfirmedShipment; NoShipmentAddress = settings.NoShipmentAddress; CallbackTimeout = settings.CallbackTimeout; @@ -98,22 +125,72 @@ public void Copy(PayPalExpressPaymentSettings settings, bool fromSettings) } else { - settings.UseSandbox = UseSandbox; + settings.SecurityProtocol = SecurityProtocol; + settings.UseSandbox = UseSandbox; + settings.IpnChangesPaymentStatus = IpnChangesPaymentStatus; settings.TransactMode = (TransactMode)TransactMode; - settings.ApiAccountName = ApiAccountName; + settings.ApiAccountName = ApiAccountName; settings.ApiAccountPassword = ApiAccountPassword; settings.Signature = Signature; settings.AdditionalFee = AdditionalFee; settings.AdditionalFeePercentage = AdditionalFeePercentage; - settings.DisplayCheckoutButton = DisplayCheckoutButton; + settings.ShowButtonInMiniShoppingCart = ShowButtonInMiniShoppingCart; settings.ConfirmedShipment = ConfirmedShipment; settings.NoShipmentAddress = NoShipmentAddress; settings.CallbackTimeout = CallbackTimeout; settings.DefaultShippingPrice = DefaultShippingPrice; } } - } + public class PayPalPlusConfigurationModel : ApiConfigurationModel + { + [SmartResourceDisplayName("Plugins.Payments.PayPalPlus.ThirdPartyPaymentMethods")] + public List ThirdPartyPaymentMethods { get; set; } + public List AvailableThirdPartyPaymentMethods { get; set; } + + [SmartResourceDisplayName("Plugins.Payments.PayPalPlus.DisplayPaymentMethodLogo")] + public bool DisplayPaymentMethodLogo { get; set; } + + [SmartResourceDisplayName("Plugins.Payments.PayPalPlus.DisplayPaymentMethodDescription")] + public bool DisplayPaymentMethodDescription { get; set; } + + + public void Copy(PayPalPlusPaymentSettings settings, bool fromSettings) + { + if (fromSettings) + { + SecurityProtocol = settings.SecurityProtocol; + UseSandbox = settings.UseSandbox; + TransactMode = (int)Settings.TransactMode.AuthorizeAndCapture; + AdditionalFee = settings.AdditionalFee; + AdditionalFeePercentage = settings.AdditionalFeePercentage; + + ClientId = settings.ClientId; + Secret = settings.Secret; + ExperienceProfileId = settings.ExperienceProfileId; + WebhookId = settings.WebhookId; + ThirdPartyPaymentMethods = settings.ThirdPartyPaymentMethods; + DisplayPaymentMethodLogo = settings.DisplayPaymentMethodLogo; + DisplayPaymentMethodDescription = settings.DisplayPaymentMethodDescription; + } + else + { + settings.SecurityProtocol = SecurityProtocol; + settings.UseSandbox = UseSandbox; + settings.TransactMode = Settings.TransactMode.AuthorizeAndCapture; + settings.AdditionalFee = AdditionalFee; + settings.AdditionalFeePercentage = AdditionalFeePercentage; + + settings.ClientId = ClientId; + settings.Secret = Secret; + settings.ExperienceProfileId = ExperienceProfileId; + settings.WebhookId = WebhookId; + settings.ThirdPartyPaymentMethods = ThirdPartyPaymentMethods; + settings.DisplayPaymentMethodLogo = DisplayPaymentMethodLogo; + settings.DisplayPaymentMethodDescription = DisplayPaymentMethodDescription; + } + } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Models/PayPalDirectPaymentInfoModel.cs b/src/Plugins/SmartStore.PayPal/Models/PayPalDirectPaymentInfoModel.cs index 86922f5a0d..7f2d0cfbd3 100644 --- a/src/Plugins/SmartStore.PayPal/Models/PayPalDirectPaymentInfoModel.cs +++ b/src/Plugins/SmartStore.PayPal/Models/PayPalDirectPaymentInfoModel.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.PayPal.Models { diff --git a/src/Plugins/SmartStore.PayPal/Models/PayPalExpressPaymentInfoModel.cs b/src/Plugins/SmartStore.PayPal/Models/PayPalExpressPaymentInfoModel.cs index 10b3ce2669..a485049605 100644 --- a/src/Plugins/SmartStore.PayPal/Models/PayPalExpressPaymentInfoModel.cs +++ b/src/Plugins/SmartStore.PayPal/Models/PayPalExpressPaymentInfoModel.cs @@ -1,4 +1,4 @@ -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.PayPal.Models { diff --git a/src/Plugins/SmartStore.PayPal/Models/PayPalPlusCheckoutModel.cs b/src/Plugins/SmartStore.PayPal/Models/PayPalPlusCheckoutModel.cs new file mode 100644 index 0000000000..280d76ec24 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Models/PayPalPlusCheckoutModel.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using SmartStore.Web.Framework.Modelling; + +namespace SmartStore.PayPal.Models +{ + public class PayPalPlusCheckoutModel : ModelBase + { + public bool UseSandbox { get; set; } + public string BillingAddressCountryCode { get; set; } + public string LanguageCulture { get; set; } + public string ApprovalUrl { get; set; } + public string ErrorMessage { get; set; } + public string PayPalPlusPseudoMessageFlag { get; set; } + public string FullDescription { get; set; } + public string PayPalFee { get; set; } + public string ThirdPartyFees { get; set; } + public bool HasAnyFees { get; set; } + + public List ThirdPartyPaymentMethods { get; set; } + + public class ThirdPartyPaymentMethod + { + public string RedirectUrl { get; set; } + public string MethodName { get; set; } + public string ImageUrl { get; set; } + public string Description { get; set; } + public string PaymentFee { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Models/PayPalStandardConfigurationModel.cs b/src/Plugins/SmartStore.PayPal/Models/PayPalStandardConfigurationModel.cs index 76f329c55c..9adc991fc6 100644 --- a/src/Plugins/SmartStore.PayPal/Models/PayPalStandardConfigurationModel.cs +++ b/src/Plugins/SmartStore.PayPal/Models/PayPalStandardConfigurationModel.cs @@ -1,14 +1,24 @@ -using SmartStore.PayPal.Settings; +using System.Collections.Generic; +using System.Net; +using System.Web.Mvc; +using SmartStore.PayPal.Settings; using SmartStore.Web.Framework; -using SmartStore.Web.Framework.Mvc; +using SmartStore.Web.Framework.Modelling; namespace SmartStore.PayPal.Models { public class PayPalStandardConfigurationModel : ModelBase { - [SmartResourceDisplayName("Plugins.Payments.PayPal.UseSandbox")] + [SmartResourceDisplayName("Plugins.Payments.PayPal.SecurityProtocol")] + public SecurityProtocolType? SecurityProtocol { get; set; } + public List AvailableSecurityProtocols { get; set; } + + [SmartResourceDisplayName("Plugins.Payments.PayPal.UseSandbox")] public bool UseSandbox { get; set; } + [SmartResourceDisplayName("Plugins.Payments.PayPal.IpnChangesPaymentStatus")] + public bool IpnChangesPaymentStatus { get; set; } + [SmartResourceDisplayName("Plugins.Payments.PayPalStandard.Fields.BusinessEmail")] public string BusinessEmail { get; set; } @@ -18,10 +28,13 @@ public class PayPalStandardConfigurationModel : ModelBase [SmartResourceDisplayName("Plugins.Payments.PayPalStandard.Fields.PDTValidateOrderTotal")] public bool PdtValidateOrderTotal { get; set; } - [SmartResourceDisplayName("Plugins.Payments.PayPalStandard.Fields.AdditionalFee")] + [SmartResourceDisplayName("Plugins.Payments.PayPalStandard.Fields.PdtValidateOnlyWarn")] + public bool PdtValidateOnlyWarn { get; set; } + + [SmartResourceDisplayName("Plugins.Payments.PayPal.AdditionalFee")] public decimal AdditionalFee { get; set; } - [SmartResourceDisplayName("Plugins.Payments.PayPalStandard.Fields.AdditionalFeePercentage")] + [SmartResourceDisplayName("Plugins.Payments.PayPal.AdditionalFeePercentage")] public bool AdditionalFeePercentage { get; set; } [SmartResourceDisplayName("Plugins.Payments.PayPalStandard.Fields.PassProductNamesAndTotals")] @@ -33,14 +46,17 @@ public class PayPalStandardConfigurationModel : ModelBase [SmartResourceDisplayName("Plugins.Payments.PayPalStandard.Fields.IpnUrl")] public string IpnUrl { get; set; } - public void Copy(PayPalStandardPaymentSettings settings, bool fromSettings) + public void Copy(PayPalStandardPaymentSettings settings, bool fromSettings) { if (fromSettings) { + SecurityProtocol = settings.SecurityProtocol; UseSandbox = settings.UseSandbox; + IpnChangesPaymentStatus = settings.IpnChangesPaymentStatus; BusinessEmail = settings.BusinessEmail; PdtToken = settings.PdtToken; PdtValidateOrderTotal = settings.PdtValidateOrderTotal; + PdtValidateOnlyWarn = settings.PdtValidateOnlyWarn; AdditionalFee = settings.AdditionalFee; AdditionalFeePercentage = settings.AdditionalFeePercentage; PassProductNamesAndTotals = settings.PassProductNamesAndTotals; @@ -49,17 +65,19 @@ public void Copy(PayPalStandardPaymentSettings settings, bool fromSettings) } else { + settings.SecurityProtocol = SecurityProtocol; settings.UseSandbox = UseSandbox; + settings.IpnChangesPaymentStatus = IpnChangesPaymentStatus; settings.BusinessEmail = BusinessEmail; settings.PdtToken = PdtToken; settings.PdtValidateOrderTotal = PdtValidateOrderTotal; + settings.PdtValidateOnlyWarn = PdtValidateOnlyWarn; settings.AdditionalFee = AdditionalFee; settings.AdditionalFeePercentage = AdditionalFeePercentage; settings.PassProductNamesAndTotals = PassProductNamesAndTotals; settings.EnableIpn = EnableIpn; settings.IpnUrl = IpnUrl; } - } } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Plugin.cs b/src/Plugins/SmartStore.PayPal/Plugin.cs index 4ef737565d..472a7f2680 100644 --- a/src/Plugins/SmartStore.PayPal/Plugin.cs +++ b/src/Plugins/SmartStore.PayPal/Plugin.cs @@ -1,4 +1,6 @@ -using SmartStore.Core.Plugins; +using System; +using SmartStore.Core.Plugins; +using SmartStore.PayPal.Services; using SmartStore.PayPal.Settings; using SmartStore.Services.Configuration; using SmartStore.Services.Localization; @@ -9,13 +11,21 @@ public class Plugin : BasePlugin { private readonly ISettingService _settingService; private readonly ILocalizationService _localizationService; + private readonly Lazy _payPalService; public Plugin( ISettingService settingService, - ILocalizationService localizationService) + ILocalizationService localizationService, + Lazy payPalService) { _settingService = settingService; _localizationService = localizationService; + _payPalService = payPalService; + } + + public static string SystemName + { + get { return "SmartStore.PayPal"; } } public override void Install() @@ -23,6 +33,7 @@ public override void Install() _settingService.SaveSetting(new PayPalExpressPaymentSettings()); _settingService.SaveSetting(new PayPalDirectPaymentSettings()); _settingService.SaveSetting(new PayPalStandardPaymentSettings()); + _settingService.SaveSetting(new PayPalPlusPaymentSettings()); _localizationService.ImportPluginResourcesFromXml(this.PluginDescriptor); @@ -31,9 +42,30 @@ public override void Install() public override void Uninstall() { + try + { + var settings = _settingService.LoadSetting(); + if (settings.WebhookId.HasValue()) + { + var session = new PayPalSessionData(); + var result = _payPalService.Value.EnsureAccessToken(session, settings); + + if (result.Success) + result = _payPalService.Value.DeleteWebhook(settings, session); + + if (!result.Success) + _payPalService.Value.LogError(null, result.ErrorMessage); + } + } + catch (Exception exception) + { + _payPalService.Value.LogError(exception); + } + _settingService.DeleteSetting(); _settingService.DeleteSetting(); _settingService.DeleteSetting(); + _settingService.DeleteSetting(); _localizationService.DeleteLocaleStringResources(PluginDescriptor.ResourceRootKey); diff --git a/src/Plugins/SmartStore.PayPal/Providers/PayPalDirectProvider.cs b/src/Plugins/SmartStore.PayPal/Providers/PayPalDirectProvider.cs index 1227e36887..96a41d0cfa 100644 --- a/src/Plugins/SmartStore.PayPal/Providers/PayPalDirectProvider.cs +++ b/src/Plugins/SmartStore.PayPal/Providers/PayPalDirectProvider.cs @@ -1,6 +1,5 @@ using System; using System.Globalization; -using Autofac; using SmartStore.Core; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Directory; @@ -8,46 +7,78 @@ using SmartStore.Core.Plugins; using SmartStore.PayPal.Controllers; using SmartStore.PayPal.PayPalSvc; -using SmartStore.PayPal.Services; using SmartStore.PayPal.Settings; -using SmartStore.Services.Configuration; using SmartStore.Services.Customers; -using SmartStore.Services.Directory; using SmartStore.Services.Payments; namespace SmartStore.PayPal { - /// - /// PayPalDirect provider - /// - [SystemName("Payments.PayPalDirect")] + [SystemName("Payments.PayPalDirect")] [FriendlyName("PayPal Direct")] [DisplayOrder(1)] public class PayPalDirectProvider : PayPalProviderBase { - #region Fields - - private readonly ICurrencyService _currencyService; private readonly ICustomerService _customerService; - private readonly CurrencySettings _currencySettings; - #endregion + public PayPalDirectProvider( + ICustomerService customerService) + { + _customerService = customerService; + } - #region Ctor + public static string SystemName { get { return "Payments.PayPalDirect"; } } - public PayPalDirectProvider( ICurrencyService currencyService, - ICustomerService customerService, - CurrencySettings currencySettings, - IComponentContext ctx) + public override bool RequiresInteraction { - _currencyService = currencyService; - _customerService = customerService; - _currencySettings = currencySettings; + get + { + return true; + } } - #endregion + public override RecurringPaymentType RecurringPaymentType + { + get + { + return RecurringPaymentType.Automatic; + } + } - #region Methods + public override PaymentMethodType PaymentMethodType + { + get + { + return PaymentMethodType.Standard; + } + } + + private CreditCardTypeType GetCreditCardType(string creditCardType) + { + var creditCardTypeType = (CreditCardTypeType)Enum.Parse(typeof(CreditCardTypeType), creditCardType); + return creditCardTypeType; + } + + private CountryCodeType GetCountryCodeType(Country country) + { + CountryCodeType payerCountry = CountryCodeType.US; + try + { + payerCountry = (CountryCodeType)Enum.Parse(typeof(CountryCodeType), country.TwoLetterIsoCode); + } + catch { } + + return payerCountry; + } + + protected override string GetControllerName() + { + return "PayPalDirect"; + } + + public override Type GetControllerType() + { + return typeof(PayPalDirectController); + } protected override string GetResourceRootKey() { @@ -63,16 +94,17 @@ public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest proces { var result = new ProcessPaymentResult(); + var store = Services.StoreService.GetStoreById(processPaymentRequest.StoreId); var customer = _customerService.GetCustomerById(processPaymentRequest.CustomerId); - var settings = CommonServices.Settings.LoadSetting(processPaymentRequest.StoreId); + var settings = Services.Settings.LoadSetting(processPaymentRequest.StoreId); var req = new DoDirectPaymentReq(); req.DoDirectPaymentRequest = new DoDirectPaymentRequestType(); - req.DoDirectPaymentRequest.Version = PayPalHelper.GetApiVersion(); + req.DoDirectPaymentRequest.Version = ApiVersion; var details = new DoDirectPaymentRequestDetailsType(); req.DoDirectPaymentRequest.DoDirectPaymentRequestDetails = details; - details.IPAddress = CommonServices.WebHelper.GetCurrentIpAddress(); + details.IPAddress = Services.WebHelper.GetCurrentIpAddress(); if (details.IPAddress.IsEmpty()) details.IPAddress = "127.0.0.1"; @@ -85,14 +117,14 @@ public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest proces //credit card details.CreditCard = new CreditCardDetailsType(); details.CreditCard.CreditCardNumber = processPaymentRequest.CreditCardNumber; - details.CreditCard.CreditCardType = PayPalHelper.GetPaypalCreditCardType(processPaymentRequest.CreditCardType); + details.CreditCard.CreditCardType = GetCreditCardType(processPaymentRequest.CreditCardType); details.CreditCard.ExpMonthSpecified = true; details.CreditCard.ExpMonth = processPaymentRequest.CreditCardExpireMonth; details.CreditCard.ExpYearSpecified = true; details.CreditCard.ExpYear = processPaymentRequest.CreditCardExpireYear; details.CreditCard.CVV2 = processPaymentRequest.CreditCardCvv2; details.CreditCard.CardOwner = new PayerInfoType(); - details.CreditCard.CardOwner.PayerCountry = PayPalHelper.GetPaypalCountryCodeType(customer.BillingAddress.Country); + details.CreditCard.CardOwner.PayerCountry = GetCountryCodeType(customer.BillingAddress.Country); details.CreditCard.CreditCardTypeSpecified = true; //billing address details.CreditCard.CardOwner.Address = new AddressType(); @@ -104,14 +136,16 @@ public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest proces details.CreditCard.CardOwner.Address.StateOrProvince = customer.BillingAddress.StateProvince.Abbreviation; else details.CreditCard.CardOwner.Address.StateOrProvince = "CA"; - details.CreditCard.CardOwner.Address.Country = PayPalHelper.GetPaypalCountryCodeType(customer.BillingAddress.Country); + details.CreditCard.CardOwner.Address.Country = GetCountryCodeType(customer.BillingAddress.Country); details.CreditCard.CardOwner.Address.PostalCode = customer.BillingAddress.ZipPostalCode; details.CreditCard.CardOwner.Payer = customer.BillingAddress.Email; details.CreditCard.CardOwner.PayerName = new PersonNameType(); details.CreditCard.CardOwner.PayerName.FirstName = customer.BillingAddress.FirstName; details.CreditCard.CardOwner.PayerName.LastName = customer.BillingAddress.LastName; - //order totals - var payPalCurrency = PayPalHelper.GetPaypalCurrency(_currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId)); + + //order totals + var payPalCurrency = GetApiCurrency(store.PrimaryStoreCurrency); + details.PaymentDetails = new PaymentDetailsType(); details.PaymentDetails.OrderTotal = new BasicAmountType(); details.PaymentDetails.OrderTotal.Value = Math.Round(processPaymentRequest.OrderTotal, 2).ToString("N", new CultureInfo("en-us")); @@ -136,14 +170,13 @@ public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest proces } //send request - using (var service = new PayPalAPIAASoapBinding()) + using (var service = GetApiAaService(settings)) { - service.Url = PayPalHelper.GetPaypalServiceUrl(settings); - service.RequesterCredentials = PayPalHelper.GetPaypalApiCredentials(settings); - DoDirectPaymentResponseType response = service.DoDirectPayment(req); + var response = service.DoDirectPayment(req); + + var error = ""; + var success = IsSuccess(response, out error); - string error = ""; - bool success = PayPalHelper.CheckSuccess(Helper, response, out error); if (success) { result.AvsResult = response.AVSCode; @@ -180,25 +213,26 @@ public override ProcessPaymentResult ProcessRecurringPayment(ProcessPaymentReque { var result = new ProcessPaymentResult(); + var store = Services.StoreService.GetStoreById(processPaymentRequest.StoreId); var customer = _customerService.GetCustomerById(processPaymentRequest.CustomerId); - var settings = CommonServices.Settings.LoadSetting(processPaymentRequest.StoreId); + var settings = Services.Settings.LoadSetting(processPaymentRequest.StoreId); var req = new CreateRecurringPaymentsProfileReq(); req.CreateRecurringPaymentsProfileRequest = new CreateRecurringPaymentsProfileRequestType(); - req.CreateRecurringPaymentsProfileRequest.Version = PayPalHelper.GetApiVersion(); + req.CreateRecurringPaymentsProfileRequest.Version = ApiVersion; var details = new CreateRecurringPaymentsProfileRequestDetailsType(); req.CreateRecurringPaymentsProfileRequest.CreateRecurringPaymentsProfileRequestDetails = details; details.CreditCard = new CreditCardDetailsType(); details.CreditCard.CreditCardNumber = processPaymentRequest.CreditCardNumber; - details.CreditCard.CreditCardType = PayPalHelper.GetPaypalCreditCardType(processPaymentRequest.CreditCardType); + details.CreditCard.CreditCardType = GetCreditCardType(processPaymentRequest.CreditCardType); details.CreditCard.ExpMonthSpecified = true; details.CreditCard.ExpMonth = processPaymentRequest.CreditCardExpireMonth; details.CreditCard.ExpYearSpecified = true; details.CreditCard.ExpYear = processPaymentRequest.CreditCardExpireYear; details.CreditCard.CVV2 = processPaymentRequest.CreditCardCvv2; details.CreditCard.CardOwner = new PayerInfoType(); - details.CreditCard.CardOwner.PayerCountry = PayPalHelper.GetPaypalCountryCodeType(customer.BillingAddress.Country); + details.CreditCard.CardOwner.PayerCountry = GetCountryCodeType(customer.BillingAddress.Country); details.CreditCard.CreditCardTypeSpecified = true; details.CreditCard.CardOwner.Address = new AddressType(); @@ -210,7 +244,7 @@ public override ProcessPaymentResult ProcessRecurringPayment(ProcessPaymentReque details.CreditCard.CardOwner.Address.StateOrProvince = customer.BillingAddress.StateProvince.Abbreviation; else details.CreditCard.CardOwner.Address.StateOrProvince = "CA"; - details.CreditCard.CardOwner.Address.Country = PayPalHelper.GetPaypalCountryCodeType(customer.BillingAddress.Country); + details.CreditCard.CardOwner.Address.Country = GetCountryCodeType(customer.BillingAddress.Country); details.CreditCard.CardOwner.Address.PostalCode = customer.BillingAddress.ZipPostalCode; details.CreditCard.CardOwner.Payer = customer.BillingAddress.Email; details.CreditCard.CardOwner.PayerName = new PersonNameType(); @@ -224,11 +258,11 @@ public override ProcessPaymentResult ProcessRecurringPayment(ProcessPaymentReque //schedule details.ScheduleDetails = new ScheduleDetailsType(); - details.ScheduleDetails.Description = Helper.GetResource("RecurringPayment"); + details.ScheduleDetails.Description = T("Plugins.Payments.PayPalDirect.RecurringPayment"); details.ScheduleDetails.PaymentPeriod = new BillingPeriodDetailsType(); details.ScheduleDetails.PaymentPeriod.Amount = new BasicAmountType(); details.ScheduleDetails.PaymentPeriod.Amount.Value = Math.Round(processPaymentRequest.OrderTotal, 2).ToString("N", new CultureInfo("en-us")); - details.ScheduleDetails.PaymentPeriod.Amount.currencyID = PayPalHelper.GetPaypalCurrency(_currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId)); + details.ScheduleDetails.PaymentPeriod.Amount.currencyID = GetApiCurrency(store.PrimaryStoreCurrency); details.ScheduleDetails.PaymentPeriod.BillingFrequency = processPaymentRequest.RecurringCycleLength; switch (processPaymentRequest.RecurringCyclePeriod) { @@ -245,19 +279,18 @@ public override ProcessPaymentResult ProcessRecurringPayment(ProcessPaymentReque details.ScheduleDetails.PaymentPeriod.BillingPeriod = BillingPeriodType.Year; break; default: - throw new SmartException(Helper.GetResource("NotSupportedPeriod")); + throw new SmartException(T("Plugins.Payments.PayPalDirect.NotSupportedPeriod")); } details.ScheduleDetails.PaymentPeriod.TotalBillingCycles = processPaymentRequest.RecurringTotalCycles; details.ScheduleDetails.PaymentPeriod.TotalBillingCyclesSpecified = true; - using (var service = new PayPalAPIAASoapBinding()) + using (var service = GetApiAaService(settings)) { - service.Url = PayPalHelper.GetPaypalServiceUrl(settings); - service.RequesterCredentials = PayPalHelper.GetPaypalApiCredentials(settings); - CreateRecurringPaymentsProfileResponseType response = service.CreateRecurringPaymentsProfile(req); + var response = service.CreateRecurringPaymentsProfile(req); + + var error = ""; + var success = IsSuccess(response, out error); - string error = ""; - bool success = PayPalHelper.CheckSuccess(Helper, response, out error); if (success) { result.NewPaymentStatus = PaymentStatus.Pending; @@ -274,51 +307,5 @@ public override ProcessPaymentResult ProcessRecurringPayment(ProcessPaymentReque return result; } - - protected override string GetControllerName() - { - return "PayPalDirect"; - } - - public override Type GetControllerType() - { - return typeof(PayPalDirectController); - } - - #endregion - - #region Properties - - public override bool RequiresInteraction - { - get - { - return true; - } - } - - /// - /// Gets a recurring payment type of payment method - /// - public override RecurringPaymentType RecurringPaymentType - { - get - { - return RecurringPaymentType.Automatic; - } - } - - /// - /// Gets a payment method type - /// - public override PaymentMethodType PaymentMethodType - { - get - { - return PaymentMethodType.Standard; - } - } - - #endregion } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Providers/PayPalExpressProvider.cs b/src/Plugins/SmartStore.PayPal/Providers/PayPalExpressProvider.cs index d46643b8f7..98c4e8e223 100644 --- a/src/Plugins/SmartStore.PayPal/Providers/PayPalExpressProvider.cs +++ b/src/Plugins/SmartStore.PayPal/Providers/PayPalExpressProvider.cs @@ -5,49 +5,47 @@ using System.Web; using SmartStore.Core; using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; using SmartStore.Core.Domain.Shipping; +using SmartStore.Core.Domain.Tax; using SmartStore.Core.Infrastructure; using SmartStore.Core.Plugins; using SmartStore.PayPal.Controllers; using SmartStore.PayPal.PayPalSvc; -using SmartStore.PayPal.Services; using SmartStore.PayPal.Settings; -using SmartStore.Services; using SmartStore.Services.Catalog; using SmartStore.Services.Common; -using SmartStore.Services.Configuration; using SmartStore.Services.Customers; using SmartStore.Services.Directory; using SmartStore.Services.Orders; using SmartStore.Services.Payments; using SmartStore.Services.Shipping; +using SmartStore.Services.Tax; namespace SmartStore.PayPal { - [SystemName("Payments.PayPalExpress")] + [SystemName("Payments.PayPalExpress")] [FriendlyName("PayPal Express")] [DisplayOrder(0)] - public partial class PayPalExpress : PayPalProviderBase + public partial class PayPalExpressProvider : PayPalProviderBase { private readonly ICurrencyService _currencyService; - private readonly CurrencySettings _currencySettings; private readonly IPriceCalculationService _priceCalculationService; - private readonly IGenericAttributeService _genericAttributeService; + private readonly ITaxService _taxService; + private readonly IGenericAttributeService _genericAttributeService; private readonly IStateProvinceService _stateProvinceService; private readonly IGiftCardService _giftCardService; private readonly IShippingService _shippingService; private readonly ICustomerService _customerService; private readonly ICountryService _countryService; private readonly HttpContextBase _httpContext; - - public PayPalExpress( + + public PayPalExpressProvider( ICurrencyService currencyService, - CurrencySettings currencySettings, IPriceCalculationService priceCalculationService, - IGenericAttributeService genericAttributeService, + ITaxService taxService, + IGenericAttributeService genericAttributeService, IStateProvinceService stateProvinceService, IGiftCardService giftCardService, IShippingService shippingService, @@ -56,8 +54,8 @@ public PayPalExpress( HttpContextBase httpContext) { _currencyService = currencyService; - _currencySettings = currencySettings; _priceCalculationService = priceCalculationService; + _taxService = taxService; _genericAttributeService = genericAttributeService; _stateProvinceService = stateProvinceService; _giftCardService = giftCardService; @@ -67,6 +65,28 @@ public PayPalExpress( _httpContext = httpContext; } + public static string SystemName { get { return "Payments.PayPalExpress"; } } + + public override PaymentMethodType PaymentMethodType + { + get + { + return PaymentMethodType.StandardAndButton; + } + } + + private PaymentActionCodeType GetPaymentAction(PayPalExpressPaymentSettings settings) + { + if (settings.TransactMode == TransactMode.Authorize) + { + return PaymentActionCodeType.Authorization; + } + else + { + return PaymentActionCodeType.Sale; + } + } + protected override string GetResourceRootKey() { return "Plugins.Payments.PayPalExpress"; @@ -80,78 +100,39 @@ protected override string GetResourceRootKey() public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest processPaymentRequest) { var result = new ProcessPaymentResult(); - var doPayment = DoExpressCheckoutPayment(processPaymentRequest); - var settings = CommonServices.Settings.LoadSetting(processPaymentRequest.StoreId); + var settings = Services.Settings.LoadSetting(processPaymentRequest.StoreId); + var doPayment = DoExpressCheckoutPayment(processPaymentRequest); + if (doPayment.Ack == AckCodeType.Success) { - if (PayPalHelper.GetPaymentAction(settings) == PaymentActionCodeType.Authorization) + if (GetPaymentAction(settings) == PaymentActionCodeType.Authorization) { - result.NewPaymentStatus = PaymentStatus.Authorized; - } - else + result.AuthorizationTransactionId = doPayment.DoExpressCheckoutPaymentResponseDetails.PaymentInfo.FirstOrDefault().TransactionID; + result.AuthorizationTransactionResult = doPayment.Ack.ToString(); + + result.NewPaymentStatus = PaymentStatus.Authorized; + } + else { - result.NewPaymentStatus = PaymentStatus.Paid; + result.CaptureTransactionId = doPayment.DoExpressCheckoutPaymentResponseDetails.PaymentInfo.FirstOrDefault().TransactionID; + result.CaptureTransactionResult = doPayment.Ack.ToString(); + + result.NewPaymentStatus = PaymentStatus.Paid; } - result.AuthorizationTransactionId = processPaymentRequest.PaypalToken; - result.CaptureTransactionId = doPayment.DoExpressCheckoutPaymentResponseDetails.PaymentInfo.FirstOrDefault().TransactionID; - result.CaptureTransactionResult = doPayment.Ack.ToString(); - } + + //result.AuthorizationTransactionId = processPaymentRequest.PaypalToken; + //result.CaptureTransactionId = doPayment.DoExpressCheckoutPaymentResponseDetails.PaymentInfo.FirstOrDefault().TransactionID; + //result.CaptureTransactionResult = doPayment.Ack.ToString(); + } else { result.NewPaymentStatus = PaymentStatus.Pending; - } - - return result; - } - - /// - /// Post process payment (used by payment gateways that require redirecting to a third-party URL) - /// - /// Payment info required for an order processing - public override void PostProcessPayment(PostProcessPaymentRequest postProcessPaymentRequest) - { - //TODO: - //handle Giropay - //if(!String.IsNullOrEmpty(postProcessPaymentRequest.GiroPayUrl)) - // return re + result.Errors.Each(x => result.AddError(x)); + } - } - - public override CapturePaymentResult Capture(CapturePaymentRequest capturePaymentRequest) - { - var result = new DoCaptureResponseType(); - var settings = CommonServices.Settings.LoadSetting(capturePaymentRequest.Order.StoreId); - - // build the request - var req = new DoCaptureReq - { - DoCaptureRequest = new DoCaptureRequestType() - }; - - //execute request - using (var service = new PayPalAPIAASoapBinding()) - { - service.Url = PayPalHelper.GetPaypalServiceUrl(settings); - service.RequesterCredentials = PayPalHelper.GetPaypalApiCredentials(settings); - result = service.DoCapture(req); - } - - var capturePaymentResult = new CapturePaymentResult(); - - if (result.Ack == AckCodeType.Success) - { - capturePaymentResult.CaptureTransactionId = result.DoCaptureResponseDetails.PaymentInfo.TransactionID; - capturePaymentResult.CaptureTransactionResult = "Success"; - } - else - { - capturePaymentResult.CaptureTransactionResult = "Error"; - capturePaymentResult.Errors.Add(result.Errors.FirstOrDefault().LongMessage); - } - - return capturePaymentResult; + return result; } /// @@ -176,87 +157,89 @@ public override Type GetControllerType() return typeof(PayPalExpressController); } - public override PaymentMethodType PaymentMethodType + public SetExpressCheckoutResponseType SetExpressCheckout(PayPalProcessPaymentRequest processPaymentRequest, IList cart) { - get - { - return PaymentMethodType.StandardAndButton; - } - } - - public SetExpressCheckoutResponseType SetExpressCheckout(PayPalProcessPaymentRequest processPaymentRequest, - IList cart) - { - var result = new SetExpressCheckoutResponseType(); - var currentStore = CommonServices.StoreContext.CurrentStore; - var settings = CommonServices.Settings.LoadSetting(processPaymentRequest.StoreId); - - var req = new SetExpressCheckoutReq + var result = new SetExpressCheckoutResponseType(); + var store = Services.StoreService.GetStoreById(processPaymentRequest.StoreId); + var customer = Services.WorkContext.CurrentCustomer; + var settings = Services.Settings.LoadSetting(processPaymentRequest.StoreId); + var payPalCurrency = GetApiCurrency(store.PrimaryStoreCurrency); + var excludingTax = (Services.WorkContext.GetTaxDisplayTypeFor(customer, store.Id) == TaxDisplayType.ExcludingTax); + + var req = new SetExpressCheckoutReq { SetExpressCheckoutRequest = new SetExpressCheckoutRequestType { - Version = PayPalHelper.GetApiVersion(), + Version = ApiVersion, SetExpressCheckoutRequestDetails = new SetExpressCheckoutRequestDetailsType() } }; var details = new SetExpressCheckoutRequestDetailsType { - PaymentAction = PayPalHelper.GetPaymentAction(settings), + PaymentAction = GetPaymentAction(settings), PaymentActionSpecified = true, - CancelURL = CommonServices.WebHelper.GetStoreLocation(currentStore.SslEnabled) + "cart", - ReturnURL = CommonServices.WebHelper.GetStoreLocation(currentStore.SslEnabled) + "Plugins/SmartStore.PayPal/PayPalExpress/GetDetails", + CancelURL = Services.WebHelper.GetStoreLocation(store.SslEnabled) + "cart", + ReturnURL = Services.WebHelper.GetStoreLocation(store.SslEnabled) + "Plugins/SmartStore.PayPal/PayPalExpress/GetDetails", //CallbackURL = _webHelper.GetStoreLocation(currentStore.SslEnabled) + "Plugins/SmartStore.PayPal/PayPalExpress/ShippingOptions?CustomerID=" + _workContext.CurrentCustomer.Id.ToString(), //CallbackTimeout = _payPalExpressPaymentSettings.CallbackTimeout.ToString() ReqConfirmShipping = settings.ConfirmedShipment.ToString(), NoShipping = settings.NoShipmentAddress.ToString() }; - // populate cart - decimal itemTotal = decimal.Zero; + // populate cart + var taxRate = decimal.Zero; + var unitPriceTaxRate = decimal.Zero; + var itemTotal = decimal.Zero; var cartItems = new List(); - foreach (OrganizedShoppingCartItem item in cart) + + foreach (var item in cart) { - decimal shoppingCartUnitPriceWithDiscountBase = _priceCalculationService.GetUnitPrice(item, true); - decimal shoppingCartUnitPriceWithDiscount = _currencyService.ConvertFromPrimaryStoreCurrency(shoppingCartUnitPriceWithDiscountBase, CommonServices.WorkContext.WorkingCurrency); - decimal priceIncludingTier = shoppingCartUnitPriceWithDiscount; + var product = item.Item.Product; + var unitPrice = _priceCalculationService.GetUnitPrice(item, true); + var shoppingCartUnitPriceWithDiscount = excludingTax + ? _taxService.GetProductPrice(product, unitPrice, false, customer, out taxRate) + : _taxService.GetProductPrice(product, unitPrice, true, customer, out unitPriceTaxRate); - cartItems.Add(new PaymentDetailsItemType() + cartItems.Add(new PaymentDetailsItemType { - Name = item.Item.Product.Name, - Number = item.Item.Product.Sku, + Name = product.Name, + Number = product.Sku, Quantity = item.Item.Quantity.ToString(), - Amount = new BasicAmountType() // this is the per item cost + // this is the per item cost + Amount = new BasicAmountType { - currencyID = PayPalHelper.GetPaypalCurrency(_currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId)), - Value = (priceIncludingTier).ToString("N", new CultureInfo("en-us")) + currencyID = payPalCurrency, + Value = shoppingCartUnitPriceWithDiscount.FormatInvariant() } }); - itemTotal += (item.Item.Quantity * priceIncludingTier); + + itemTotal += (item.Item.Quantity * shoppingCartUnitPriceWithDiscount); }; // additional handling fee var additionalHandlingFee = GetAdditionalHandlingFee(cart); - cartItems.Add(new PaymentDetailsItemType() + cartItems.Add(new PaymentDetailsItemType { - Name = "Zahlartgebhren", + Name = T("Plugins.Payments.PayPal.PaymentMethodFee").Text, Quantity = "1", Amount = new BasicAmountType() { - currencyID = PayPalHelper.GetPaypalCurrency(_currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId)), - Value = (additionalHandlingFee).ToString("N", new CultureInfo("en-us")) + currencyID = payPalCurrency, + Value = additionalHandlingFee.FormatInvariant() } }); + itemTotal += GetAdditionalHandlingFee(cart); //shipping - decimal shippingTotal = decimal.Zero; + var shippingTotal = decimal.Zero; if (cart.RequiresShipping()) { decimal? shoppingCartShippingBase = OrderTotalCalculationService.GetShoppingCartShippingTotal(cart); if (shoppingCartShippingBase.HasValue && shoppingCartShippingBase > 0) { - shippingTotal = _currencyService.ConvertFromPrimaryStoreCurrency(shoppingCartShippingBase.Value, CommonServices.WorkContext.WorkingCurrency); + shippingTotal = _currencyService.ConvertFromPrimaryStoreCurrency(shoppingCartShippingBase.Value, Services.WorkContext.WorkingCurrency); } else { @@ -285,28 +268,23 @@ public SetExpressCheckoutResponseType SetExpressCheckout(PayPalProcessPaymentReq //decimal shoppingCartTax = _currencyService.ConvertFromPrimaryStoreCurrency(shoppingCartTaxBase, CommonServices.WorkContext.WorkingCurrency); // discount - decimal discount = -processPaymentRequest.Discount; - + var discount = -processPaymentRequest.Discount; if (discount != 0) { - cartItems.Add(new PaymentDetailsItemType() + cartItems.Add(new PaymentDetailsItemType { - Name = "Threadrock Discount", + Name = T("Plugins.Payments.PayPal.ThreadrockDiscount").Text, Quantity = "1", - Amount = new BasicAmountType() // this is the total discount + Amount = new BasicAmountType // this is the total discount { - currencyID = PayPalHelper.GetPaypalCurrency(_currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId)), - Value = discount.ToString("N", new CultureInfo("en-us")) + currencyID = payPalCurrency, + Value = discount.FormatInvariant() } }); itemTotal += discount; } - // get customer - int customerId = Convert.ToInt32(CommonServices.WorkContext.CurrentCustomer.Id.ToString()); - var customer = _customerService.GetCustomerById(customerId); - if (!cart.IsRecurring()) { //we don't apply gift cards for recurring products @@ -326,14 +304,14 @@ public SetExpressCheckoutResponseType SetExpressCheckout(PayPalProcessPaymentReq decimal amountToSubtract = -amountCanBeUsed; - cartItems.Add(new PaymentDetailsItemType() + cartItems.Add(new PaymentDetailsItemType { - Name = "Giftcard Applied", + Name = T("Plugins.Payments.PayPal.GiftcardApplied").Text, Quantity = "1", - Amount = new BasicAmountType() + Amount = new BasicAmountType { - currencyID = PayPalHelper.GetPaypalCurrency(_currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId)), - Value = amountToSubtract.ToString("N", new CultureInfo("en-us")) + currencyID = payPalCurrency, + Value = amountToSubtract.FormatInvariant() } }); @@ -349,13 +327,13 @@ public SetExpressCheckoutResponseType SetExpressCheckout(PayPalProcessPaymentReq { ItemTotal = new BasicAmountType { - Value = Math.Round(itemTotal, 2).ToString("N", new CultureInfo("en-us")), - currencyID = PayPalHelper.GetPaypalCurrency(_currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId)) + Value = Math.Round(itemTotal, 2).FormatInvariant(), + currencyID = payPalCurrency }, ShippingTotal = new BasicAmountType { - Value = Math.Round(shippingTotal, 2).ToString("N", new CultureInfo("en-us")), - currencyID = PayPalHelper.GetPaypalCurrency(_currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId)) + Value = Math.Round(shippingTotal, 2).FormatInvariant(), + currencyID = payPalCurrency }, //TaxTotal = new BasicAmountType //{ @@ -364,12 +342,12 @@ public SetExpressCheckoutResponseType SetExpressCheckout(PayPalProcessPaymentReq //}, OrderTotal = new BasicAmountType { - Value = Math.Round(itemTotal + shippingTotal, 2).ToString("N", new CultureInfo("en-us")), - currencyID = PayPalHelper.GetPaypalCurrency(_currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId)) + Value = Math.Round(itemTotal + shippingTotal, 2).FormatInvariant(), + currencyID = payPalCurrency }, Custom = processPaymentRequest.OrderGuid.ToString(), ButtonSource = SmartStoreVersion.CurrentFullVersion, - PaymentAction = PayPalHelper.GetPaymentAction(settings), + PaymentAction = GetPaymentAction(settings), PaymentDetailsItem = cartItems.ToArray() }; details.PaymentDetails = new[] { paymentDetails }; @@ -379,14 +357,11 @@ public SetExpressCheckoutResponseType SetExpressCheckout(PayPalProcessPaymentReq req.SetExpressCheckoutRequest.SetExpressCheckoutRequestDetails.Custom = processPaymentRequest.OrderGuid.ToString(); req.SetExpressCheckoutRequest.SetExpressCheckoutRequestDetails = details; - using (var service = new PayPalAPIAASoapBinding()) + using (var service = GetApiAaService(settings)) { - service.Url = PayPalHelper.GetPaypalServiceUrl(settings); - service.RequesterCredentials = PayPalHelper.GetPaypalApiCredentials(settings); result = service.SetExpressCheckout(req); } - _httpContext.GetCheckoutState().CustomProperties.Add("PayPalExpressButtonUsed", true); return result; } @@ -394,19 +369,17 @@ public SetExpressCheckoutResponseType SetExpressCheckout(PayPalProcessPaymentReq public GetExpressCheckoutDetailsResponseType GetExpressCheckoutDetails(string token) { var result = new GetExpressCheckoutDetailsResponseType(); - var settings = CommonServices.Settings.LoadSetting(CommonServices.StoreContext.CurrentStore.Id); + var settings = Services.Settings.LoadSetting(Services.StoreContext.CurrentStore.Id); - using (var service = new PayPalAPIAASoapBinding()) + using (var service = GetApiAaService(settings)) { var req = new GetExpressCheckoutDetailsReq(); req.GetExpressCheckoutDetailsRequest = new GetExpressCheckoutDetailsRequestType { Token = token, - Version = PayPalHelper.GetApiVersion() + Version = ApiVersion }; - service.Url = PayPalHelper.GetPaypalServiceUrl(settings); - service.RequesterCredentials = PayPalHelper.GetPaypalApiCredentials(settings); result = service.GetExpressCheckoutDetails(req); } return result; @@ -414,14 +387,14 @@ public GetExpressCheckoutDetailsResponseType GetExpressCheckoutDetails(string to public ProcessPaymentRequest SetCheckoutDetails(ProcessPaymentRequest processPaymentRequest, GetExpressCheckoutDetailsResponseDetailsType checkoutDetails) { - int customerId = Convert.ToInt32(CommonServices.WorkContext.CurrentCustomer.Id.ToString()); + int customerId = Convert.ToInt32(Services.WorkContext.CurrentCustomer.Id.ToString()); var customer = _customerService.GetCustomerById(customerId); - var settings = CommonServices.Settings.LoadSetting(CommonServices.StoreContext.CurrentStore.Id); + var settings = Services.Settings.LoadSetting(Services.StoreContext.CurrentStore.Id); - CommonServices.WorkContext.CurrentCustomer = customer; + Services.WorkContext.CurrentCustomer = customer; //var cart = customer.ShoppingCartItems.Where(sci => sci.ShoppingCartType == ShoppingCartType.ShoppingCart).ToList(); - var cart = CommonServices.WorkContext.CurrentCustomer.GetCartItems(ShoppingCartType.ShoppingCart, CommonServices.StoreContext.CurrentStore.Id); + var cart = Services.WorkContext.CurrentCustomer.GetCartItems(ShoppingCartType.ShoppingCart, Services.StoreContext.CurrentStore.Id); // get/update billing address string billingFirstName = checkoutDetails.PayerInfo.PayerName.FirstName; @@ -441,7 +414,7 @@ public ProcessPaymentRequest SetCheckoutDetails(ProcessPaymentRequest processPay if (billingCountry != null) billingCountryId = billingCountry.Id; - var billingAddress = customer.Addresses.ToList().FindAddress( + var billingAddress = customer.Addresses.FindAddress( billingFirstName, billingLastName, billingPhoneNumber, billingEmail, string.Empty, string.Empty, billingAddress1, billingAddress2, billingCity, billingStateProvinceId, billingZipPostalCode, billingCountryId); @@ -498,7 +471,7 @@ public ProcessPaymentRequest SetCheckoutDetails(ProcessPaymentRequest processPay if (shippingCountry != null) shippingCountryId = shippingCountry.Id; - var shippingAddress = customer.Addresses.ToList().FindAddress( + var shippingAddress = customer.Addresses.FindAddress( shippingFirstName, shippingLastName, shippingPhoneNumber, shippingEmail, string.Empty, string.Empty, shippingAddress1, shippingAddress2, shippingCity, @@ -541,7 +514,7 @@ public ProcessPaymentRequest SetCheckoutDetails(ProcessPaymentRequest processPay if (checkoutDetails.UserSelectedOptions.ShippingOptionName.Contains(shippingOption.Name) && checkoutDetails.UserSelectedOptions.ShippingOptionName.Contains(shippingOption.Description)) { - _genericAttributeService.SaveAttribute(CommonServices.WorkContext.CurrentCustomer, SystemCustomerAttributeNames.SelectedShippingOption, shippingOption); + _genericAttributeService.SaveAttribute(Services.WorkContext.CurrentCustomer, SystemCustomerAttributeNames.SelectedShippingOption, shippingOption); isShippingSet = true; break; } @@ -569,7 +542,8 @@ public ProcessPaymentRequest SetCheckoutDetails(ProcessPaymentRequest processPay public DoExpressCheckoutPaymentResponseType DoExpressCheckoutPayment(ProcessPaymentRequest processPaymentRequest) { var result = new DoExpressCheckoutPaymentResponseType(); - var settings = CommonServices.Settings.LoadSetting(processPaymentRequest.StoreId); + var store = Services.StoreService.GetStoreById(processPaymentRequest.StoreId); + var settings = Services.Settings.LoadSetting(processPaymentRequest.StoreId); // populate payment details var paymentDetails = new PaymentDetailsType @@ -577,7 +551,7 @@ public DoExpressCheckoutPaymentResponseType DoExpressCheckoutPayment(ProcessPaym OrderTotal = new BasicAmountType { Value = Math.Round(processPaymentRequest.OrderTotal, 2).ToString("N", new CultureInfo("en-us")), - currencyID = PayPalHelper.GetPaypalCurrency(_currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId)) + currencyID = GetApiCurrency(store.PrimaryStoreCurrency) }, Custom = processPaymentRequest.OrderGuid.ToString(), ButtonSource = SmartStoreVersion.CurrentFullVersion @@ -588,12 +562,12 @@ public DoExpressCheckoutPaymentResponseType DoExpressCheckoutPayment(ProcessPaym { DoExpressCheckoutPaymentRequest = new DoExpressCheckoutPaymentRequestType { - Version = PayPalHelper.GetApiVersion(), + Version = ApiVersion, DoExpressCheckoutPaymentRequestDetails = new DoExpressCheckoutPaymentRequestDetailsType { Token = processPaymentRequest.PaypalToken, PayerID = processPaymentRequest.PaypalPayerId, - PaymentAction = PayPalHelper.GetPaymentAction(settings), + PaymentAction = GetPaymentAction(settings), PaymentActionSpecified = true, PaymentDetails = new PaymentDetailsType[] { @@ -604,14 +578,20 @@ public DoExpressCheckoutPaymentResponseType DoExpressCheckoutPayment(ProcessPaym }; //execute request - using (var service = new PayPalAPIAASoapBinding()) + using (var service = GetApiAaService(settings)) { - service.Url = PayPalHelper.GetPaypalServiceUrl(settings); - service.RequesterCredentials = PayPalHelper.GetPaypalApiCredentials(settings); result = service.DoExpressCheckoutPayment(req); } return result; } - } + + + public class PayPalProcessPaymentRequest : ProcessPaymentRequest + { + /// + /// Gets or sets an order Discount Amount + /// + public decimal Discount { get; set; } + } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Providers/PayPalPlusProvider.cs b/src/Plugins/SmartStore.PayPal/Providers/PayPalPlusProvider.cs new file mode 100644 index 0000000000..45e251d2d0 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Providers/PayPalPlusProvider.cs @@ -0,0 +1,32 @@ +using System; +using SmartStore.Core.Plugins; +using SmartStore.PayPal.Controllers; +using SmartStore.PayPal.Settings; +using SmartStore.Services.Payments; + +namespace SmartStore.PayPal +{ + [SystemName("Payments.PayPalPlus")] + [FriendlyName("PayPal PLUS")] + [DisplayOrder(1)] + public partial class PayPalPlusProvider : PayPalRestApiProviderBase + { + public static string SystemName + { + get { return "Payments.PayPalPlus"; } + } + + public override PaymentMethodType PaymentMethodType + { + get + { + return PaymentMethodType.StandardAndRedirection; + } + } + + public override Type GetControllerType() + { + return typeof(PayPalPlusController); + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Providers/PayPalProviderBase.cs b/src/Plugins/SmartStore.PayPal/Providers/PayPalProviderBase.cs index 687d179faf..1c65bd39dd 100644 --- a/src/Plugins/SmartStore.PayPal/Providers/PayPalProviderBase.cs +++ b/src/Plugins/SmartStore.PayPal/Providers/PayPalProviderBase.cs @@ -1,113 +1,169 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Net; using System.Text; -using System.Web; using System.Web.Routing; -using Autofac; using SmartStore.Core.Configuration; +using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; using SmartStore.Core.Logging; using SmartStore.Core.Plugins; using SmartStore.PayPal.PayPalSvc; -using SmartStore.PayPal.Services; using SmartStore.PayPal.Settings; using SmartStore.Services; using SmartStore.Services.Orders; using SmartStore.Services.Payments; -using SmartStore.Web.Framework.Plugins; namespace SmartStore.PayPal { - public abstract class PayPalProviderBase : PaymentMethodBase, IConfigurable where TSetting : PayPalApiSettingsBase, ISettings, new() + public abstract class PayPalProviderBase : PaymentMethodBase, IConfigurable where TSetting : PayPalApiSettingsBase, ISettings, new() { protected PayPalProviderBase() { Logger = NullLogger.Instance; } + public static string ApiVersion + { + get { return "109"; } + } + public ILogger Logger { get; set; } + public ICommonServices Services { get; set; } + public IOrderService OrderService { get; set; } + public IOrderTotalCalculationService OrderTotalCalculationService { get; set; } - public ICommonServices CommonServices { get; set; } + public override bool SupportCapture + { + get { return true; } + } - public IOrderService OrderService { get; set; } + public override bool SupportPartiallyRefund + { + get { return true; } + } - public IOrderTotalCalculationService OrderTotalCalculationService { get; set; } + public override bool SupportRefund + { + get { return true; } + } - public IComponentContext ComponentContext { get; set; } + public override bool SupportVoid + { + get { return true; } + } protected abstract string GetResourceRootKey(); - private PluginHelper _helper; - public PluginHelper Helper + protected PayPalAPIAASoapBinding GetApiAaService(TSetting settings) { - get + if (settings.SecurityProtocol.HasValue) { - if (_helper == null) - { - _helper = new PluginHelper(this.ComponentContext, "SmartStore.PayPal", GetResourceRootKey()); - } - return _helper; + ServicePointManager.SecurityProtocol = settings.SecurityProtocol.Value; } + + var service = new PayPalAPIAASoapBinding(); + + service.Url = settings.UseSandbox ? "https://api-3t.sandbox.paypal.com/2.0/" : "https://api-3t.paypal.com/2.0/"; + + service.RequesterCredentials = GetApiCredentials(settings); + + return service; } - /// - /// Verifies IPN - /// - /// Form string - /// Values - /// Result - public bool VerifyIPN(string formString, out Dictionary values) - { - // settings: multistore context not possible here. we need the custom value to determine what store it is. - var settings = CommonServices.Settings.LoadSetting(); - var req = (HttpWebRequest)WebRequest.Create(PayPalHelper.GetPaypalUrl(settings)); + protected PayPalAPISoapBinding GetApiService(TSetting settings) + { + if (settings.SecurityProtocol.HasValue) + { + ServicePointManager.SecurityProtocol = settings.SecurityProtocol.Value; + } - req.Method = "POST"; - req.ContentType = "application/x-www-form-urlencoded"; - req.UserAgent = HttpContext.Current.Request.UserAgent; + var service = new PayPalAPISoapBinding(); - string formContent = string.Format("{0}&cmd=_notify-validate", formString); - req.ContentLength = formContent.Length; + service.Url = settings.UseSandbox ? "https://api-3t.sandbox.paypal.com/2.0/" : "https://api-3t.paypal.com/2.0/"; - using (var sw = new StreamWriter(req.GetRequestStream(), Encoding.ASCII)) - { - sw.Write(formContent); - } + service.RequesterCredentials = GetApiCredentials(settings); - string response = null; - using (var sr = new StreamReader(req.GetResponse().GetResponseStream())) - { - response = HttpUtility.UrlDecode(sr.ReadToEnd()); - } - bool success = response.Trim().Equals("VERIFIED", StringComparison.OrdinalIgnoreCase); + return service; + } - values = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (string l in formString.Split('&')) - { - string line = HttpUtility.UrlDecode(l).Trim(); - int equalPox = line.IndexOf('='); - if (equalPox >= 0) - values.Add(line.Substring(0, equalPox), line.Substring(equalPox + 1)); - } + protected CustomSecurityHeaderType GetApiCredentials(PayPalApiSettingsBase settings) + { + var customSecurityHeaderType = new CustomSecurityHeaderType(); - return success; - } + customSecurityHeaderType.Credentials = new UserIdPasswordType(); + customSecurityHeaderType.Credentials.Username = settings.ApiAccountName; + customSecurityHeaderType.Credentials.Password = settings.ApiAccountPassword; + customSecurityHeaderType.Credentials.Signature = settings.Signature; + customSecurityHeaderType.Credentials.Subject = ""; - /// - /// Gets additional handling fee - /// - /// Shoping cart - /// Additional handling fee - public override decimal GetAdditionalHandlingFee(IList cart) + return customSecurityHeaderType; + } + + protected CurrencyCodeType GetApiCurrency(Currency currency) + { + var currencyCodeType = CurrencyCodeType.USD; + try + { + currencyCodeType = (CurrencyCodeType)Enum.Parse(typeof(CurrencyCodeType), currency.CurrencyCode, true); + } + catch { } + + return currencyCodeType; + } + + protected bool IsSuccess(AbstractResponseType abstractResponse, out string errorMsg) + { + var success = false; + var sb = new StringBuilder(); + + switch (abstractResponse.Ack) + { + case AckCodeType.Success: + case AckCodeType.SuccessWithWarning: + success = true; + break; + default: + break; + } + + if (null != abstractResponse.Errors) + { + foreach (ErrorType errorType in abstractResponse.Errors) + { + if (errorType.ShortMessage.IsEmpty()) + continue; + + if (sb.Length > 0) + sb.Append(Environment.NewLine); + + sb.Append("{0}: {1}".FormatInvariant(Services.Localization.GetResource("Admin.System.Log.Fields.ShortMessage"), errorType.ShortMessage)); + sb.AppendLine(" ({0}).".FormatInvariant(errorType.ErrorCode)); + + if (errorType.LongMessage.HasValue() && errorType.LongMessage != errorType.ShortMessage) + sb.AppendLine("{0}: {1}".FormatInvariant(Services.Localization.GetResource("Admin.System.Log.Fields.FullMessage"), errorType.LongMessage)); + } + } + + errorMsg = sb.ToString(); + return success; + } + + protected abstract string GetControllerName(); + + /// + /// Gets additional handling fee + /// + /// Shoping cart + /// Additional handling fee + public override decimal GetAdditionalHandlingFee(IList cart) { var result = decimal.Zero; try { - var settings = CommonServices.Settings.LoadSetting(); + var settings = Services.Settings.LoadSetting(); result = this.CalculateAdditionalFee(OrderTotalCalculationService, cart, settings.AdditionalFee, settings.AdditionalFeePercentage); } @@ -124,31 +180,32 @@ public override decimal GetAdditionalHandlingFee(IListCapture payment result public override CapturePaymentResult Capture(CapturePaymentRequest capturePaymentRequest) { - var result = new CapturePaymentResult() + var result = new CapturePaymentResult { NewPaymentStatus = capturePaymentRequest.Order.PaymentStatus }; - var settings = CommonServices.Settings.LoadSetting(capturePaymentRequest.Order.StoreId); - string authorizationId = capturePaymentRequest.Order.AuthorizationTransactionId; + var settings = Services.Settings.LoadSetting(capturePaymentRequest.Order.StoreId); + var currencyCode = Services.WorkContext.WorkingCurrency.CurrencyCode ?? "EUR"; + + var authorizationId = capturePaymentRequest.Order.AuthorizationTransactionId; var req = new DoCaptureReq(); req.DoCaptureRequest = new DoCaptureRequestType(); - req.DoCaptureRequest.Version = PayPalHelper.GetApiVersion(); + req.DoCaptureRequest.Version = ApiVersion; req.DoCaptureRequest.AuthorizationID = authorizationId; req.DoCaptureRequest.Amount = new BasicAmountType(); req.DoCaptureRequest.Amount.Value = Math.Round(capturePaymentRequest.Order.OrderTotal, 2).ToString("N", new CultureInfo("en-us")); - req.DoCaptureRequest.Amount.currencyID = (CurrencyCodeType)Enum.Parse(typeof(CurrencyCodeType), Helper.CurrencyCode, true); + req.DoCaptureRequest.Amount.currencyID = (CurrencyCodeType)Enum.Parse(typeof(CurrencyCodeType), currencyCode, true); req.DoCaptureRequest.CompleteType = CompleteCodeType.Complete; - using (var service = new PayPalAPIAASoapBinding()) + using (var service = GetApiAaService(settings)) { - service.Url = PayPalHelper.GetPaypalServiceUrl(settings); - service.RequesterCredentials = PayPalHelper.GetPaypalApiCredentials(settings); - DoCaptureResponseType response = service.DoCapture(req); + var response = service.DoCapture(req); + + var error = ""; + var success = IsSuccess(response, out error); - string error = ""; - bool success = PayPalHelper.CheckSuccess(_helper, response, out error); if (success) { result.NewPaymentStatus = PaymentStatus.Paid; @@ -163,40 +220,64 @@ public override CapturePaymentResult Capture(CapturePaymentRequest capturePaymen return result; } - /// - /// Handles refund - /// - /// RefundPaymentRequest - /// RefundPaymentResult public override RefundPaymentResult Refund(RefundPaymentRequest request) { - var result = new RefundPaymentResult() + // "Transaction refused (10009). You can not refund this type of transaction.": + // merchant must accept the payment in his PayPal account + var result = new RefundPaymentResult { NewPaymentStatus = request.Order.PaymentStatus }; - var settings = CommonServices.Settings.LoadSetting(request.Order.StoreId); - string transactionId = request.Order.CaptureTransactionId; + var settings = Services.Settings.LoadSetting(request.Order.StoreId); - var req = new RefundTransactionReq(); + var transactionId = request.Order.CaptureTransactionId; + + var req = new RefundTransactionReq(); req.RefundTransactionRequest = new RefundTransactionRequestType(); - //NOTE: Specify amount in partial refund - req.RefundTransactionRequest.RefundType = RefundType.Full; + + if (request.IsPartialRefund) + { + var store = Services.StoreService.GetStoreById(request.Order.StoreId); + var currencyCode = store.PrimaryStoreCurrency.CurrencyCode; + + req.RefundTransactionRequest.RefundType = RefundType.Partial; + + req.RefundTransactionRequest.Amount = new BasicAmountType(); + req.RefundTransactionRequest.Amount.Value = Math.Round(request.AmountToRefund, 2).ToString("N", new CultureInfo("en-us")); + req.RefundTransactionRequest.Amount.currencyID = (CurrencyCodeType)Enum.Parse(typeof(CurrencyCodeType), currencyCode, true); + + // see https://developer.paypal.com/docs/classic/express-checkout/digital-goods/ECDGIssuingRefunds/ + // https://developer.paypal.com/docs/classic/api/merchant/RefundTransaction_API_Operation_NVP/ + var memo = Services.Localization.GetResource("Plugins.SmartStore.PayPal.PartialRefundMemo", 0, false, "", true); + if (memo.HasValue()) + { + req.RefundTransactionRequest.Memo = memo.FormatInvariant(req.RefundTransactionRequest.Amount.Value); + } + } + else + { + req.RefundTransactionRequest.RefundType = RefundType.Full; + } + req.RefundTransactionRequest.RefundTypeSpecified = true; - req.RefundTransactionRequest.Version = PayPalHelper.GetApiVersion(); + req.RefundTransactionRequest.Version = ApiVersion; req.RefundTransactionRequest.TransactionID = transactionId; - using (var service = new PayPalAPISoapBinding()) + using (var service = GetApiService(settings)) { - service.Url = PayPalHelper.GetPaypalServiceUrl(settings); - service.RequesterCredentials = PayPalHelper.GetPaypalApiCredentials(settings); - RefundTransactionResponseType response = service.RefundTransaction(req); + var response = service.RefundTransaction(req); + + var error = ""; + var Success = IsSuccess(response, out error); - string error = string.Empty; - bool Success = PayPalHelper.CheckSuccess(_helper, response, out error); if (Success) { - result.NewPaymentStatus = PaymentStatus.Refunded; + if (request.IsPartialRefund) + result.NewPaymentStatus = PaymentStatus.PartiallyRefunded; + else + result.NewPaymentStatus = PaymentStatus.Refunded; + //cancelPaymentResult.RefundTransactionID = response.RefundTransactionID; } else @@ -215,31 +296,30 @@ public override RefundPaymentResult Refund(RefundPaymentRequest request) /// Result public override VoidPaymentResult Void(VoidPaymentRequest request) { - var result = new VoidPaymentResult() + var result = new VoidPaymentResult { NewPaymentStatus = request.Order.PaymentStatus }; - string transactionId = request.Order.AuthorizationTransactionId; - var settings = CommonServices.Settings.LoadSetting(request.Order.StoreId); + var settings = Services.Settings.LoadSetting(request.Order.StoreId); + + var transactionId = request.Order.AuthorizationTransactionId; - if (String.IsNullOrEmpty(transactionId)) + if (transactionId.IsEmpty()) transactionId = request.Order.CaptureTransactionId; var req = new DoVoidReq(); req.DoVoidRequest = new DoVoidRequestType(); - req.DoVoidRequest.Version = PayPalHelper.GetApiVersion(); + req.DoVoidRequest.Version = ApiVersion; req.DoVoidRequest.AuthorizationID = transactionId; - - using (var service = new PayPalAPIAASoapBinding()) + using (var service = GetApiAaService(settings)) { - service.Url = PayPalHelper.GetPaypalServiceUrl(settings); - service.RequesterCredentials = PayPalHelper.GetPaypalApiCredentials(settings); - DoVoidResponseType response = service.DoVoid(req); + var response = service.DoVoid(req); + + var error = ""; + var success = IsSuccess(response, out error); - string error = ""; - bool success = PayPalHelper.CheckSuccess(_helper, response, out error); if (success) { result.NewPaymentStatus = PaymentStatus.Voided; @@ -262,11 +342,11 @@ public override CancelRecurringPaymentResult CancelRecurringPayment(CancelRecurr { var result = new CancelRecurringPaymentResult(); var order = request.Order; - var settings = CommonServices.Settings.LoadSetting(order.StoreId); + var settings = Services.Settings.LoadSetting(order.StoreId); var req = new ManageRecurringPaymentsProfileStatusReq(); req.ManageRecurringPaymentsProfileStatusRequest = new ManageRecurringPaymentsProfileStatusRequestType(); - req.ManageRecurringPaymentsProfileStatusRequest.Version = PayPalHelper.GetApiVersion(); + req.ManageRecurringPaymentsProfileStatusRequest.Version = ApiVersion; var details = new ManageRecurringPaymentsProfileStatusRequestDetailsType(); req.ManageRecurringPaymentsProfileStatusRequest.ManageRecurringPaymentsProfileStatusRequestDetails = details; @@ -274,14 +354,12 @@ public override CancelRecurringPaymentResult CancelRecurringPayment(CancelRecurr //Recurring payments profile ID returned in the CreateRecurringPaymentsProfile response details.ProfileID = order.SubscriptionTransactionId; - using (var service = new PayPalAPIAASoapBinding()) + using (var service = GetApiAaService(settings)) { - service.Url = PayPalHelper.GetPaypalServiceUrl(settings); - service.RequesterCredentials = PayPalHelper.GetPaypalApiCredentials(settings); var response = service.ManageRecurringPaymentsProfileStatus(req); string error = ""; - if (!PayPalHelper.CheckSuccess(_helper, response, out error)) + if (!IsSuccess(response, out error)) { result.AddError(error); } @@ -315,28 +393,6 @@ public override void GetPaymentInfoRoute(out string actionName, out string contr controllerName = GetControllerName(); routeValues = new RouteValueDictionary() { { "area", "SmartStore.PayPal" } }; } - - protected abstract string GetControllerName(); - - public override bool SupportCapture - { - get { return true; } - } - - public override bool SupportPartiallyRefund - { - get { return false; } - } - - public override bool SupportRefund - { - get { return true; } - } - - public override bool SupportVoid - { - get { return true; } - } } } diff --git a/src/Plugins/SmartStore.PayPal/Providers/PayPalRestApiProviderBase.cs b/src/Plugins/SmartStore.PayPal/Providers/PayPalRestApiProviderBase.cs new file mode 100644 index 0000000000..2330c9d336 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Providers/PayPalRestApiProviderBase.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.Web; +using System.Web.Routing; +using SmartStore.Core.Configuration; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Payments; +using SmartStore.Core.Logging; +using SmartStore.Core.Plugins; +using SmartStore.PayPal.Services; +using SmartStore.PayPal.Settings; +using SmartStore.Services; +using SmartStore.Services.Orders; +using SmartStore.Services.Payments; + +namespace SmartStore.PayPal +{ + public abstract class PayPalRestApiProviderBase : PaymentMethodBase, IConfigurable where TSetting : PayPalApiSettingsBase, ISettings, new() + { + protected PayPalRestApiProviderBase() + { + Logger = NullLogger.Instance; + } + + public ILogger Logger { get; set; } + public HttpContextBase HttpContext { get; set; } + public ICommonServices Services { get; set; } + public IOrderService OrderService { get; set; } + public IOrderTotalCalculationService OrderTotalCalculationService { get; set; } + public IPayPalService PayPalService { get; set; } + + protected string GetControllerName() + { + return GetControllerType().Name.EmptyNull().Replace("Controller", ""); + } + + public static string CheckoutCompletedKey + { + get { return "PayPalCheckoutCompleted"; } + } + + public override bool SupportCapture + { + get { return true; } + } + + public override bool SupportPartiallyRefund + { + get { return true; } + } + + public override bool SupportRefund + { + get { return true; } + } + + public override bool SupportVoid + { + get { return true; } + } + + public override decimal GetAdditionalHandlingFee(IList cart) + { + var result = decimal.Zero; + try + { + var settings = Services.Settings.LoadSetting(); + + result = this.CalculateAdditionalFee(OrderTotalCalculationService, cart, settings.AdditionalFee, settings.AdditionalFeePercentage); + } + catch (Exception) + { + } + return result; + } + + public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest processPaymentRequest) + { + var result = new ProcessPaymentResult + { + NewPaymentStatus = PaymentStatus.Pending + }; + + HttpContext.Session.SafeRemove(CheckoutCompletedKey); + + var settings = Services.Settings.LoadSetting(processPaymentRequest.StoreId); + var session = HttpContext.GetPayPalSessionData(); + + processPaymentRequest.OrderGuid = session.OrderGuid; + + var apiResult = PayPalService.ExecutePayment(settings, session); + + if (apiResult.Success && apiResult.Json != null) + { + var state = (string)apiResult.Json.state; + string reasonCode = null; + dynamic relatedObject = null; + + if (!state.IsCaseInsensitiveEqual("failed")) + { + // the payment id is required to find the order during webhook message processing + result.AuthorizationTransactionCode = apiResult.Id; + + // intent: "sale" for immediate payment, "authorize" for pre-authorized payments and "order" for an order. + // info required cause API has different endpoints for different intents. + var intent = (string)apiResult.Json.intent; + + if (intent.IsCaseInsensitiveEqual("sale")) + { + relatedObject = apiResult.Json.transactions[0].related_resources[0].sale; + + session.PaymentInstruction = PayPalService.ParsePaymentInstruction(apiResult.Json.payment_instruction) as PayPalPaymentInstruction; + } + else + { + relatedObject = apiResult.Json.transactions[0].related_resources[0].authorization; + } + + if (relatedObject != null) + { + state = (string)relatedObject.state; + reasonCode = (string)relatedObject.reason_code; + + // see PayPalService.Refund() + result.AuthorizationTransactionResult = "{0} ({1})".FormatInvariant(state.NaIfEmpty(), intent.NaIfEmpty()); + result.AuthorizationTransactionId = (string)relatedObject.id; + + result.NewPaymentStatus = PayPalService.GetPaymentStatus(state, reasonCode, PaymentStatus.Authorized); + + if (result.NewPaymentStatus == PaymentStatus.Paid) + { + result.CaptureTransactionResult = result.AuthorizationTransactionResult; + result.CaptureTransactionId = result.AuthorizationTransactionId; + } + } + } + else + { + var failureReason = (string)apiResult.Json.failure_reason; + + result.Errors.Add(T("Plugins.SmartStore.PayPal.PaymentExecuteFailed").Text.Grow(failureReason, " ")); + } + } + + if (!apiResult.Success) + result.Errors.Add(apiResult.ErrorMessage); + + return result; + } + + public override void PostProcessPayment(PostProcessPaymentRequest postProcessPaymentRequest) + { + var instruction = PayPalService.CreatePaymentInstruction(HttpContext.GetPayPalSessionData().PaymentInstruction); + + if (instruction.HasValue()) + { + HttpContext.Session[CheckoutCompletedKey] = instruction; + + OrderService.AddOrderNote(postProcessPaymentRequest.Order, instruction, true); + } + } + + public override CapturePaymentResult Capture(CapturePaymentRequest capturePaymentRequest) + { + var result = new CapturePaymentResult + { + NewPaymentStatus = capturePaymentRequest.Order.PaymentStatus + }; + + var settings = Services.Settings.LoadSetting(capturePaymentRequest.Order.StoreId); + var session = new PayPalSessionData(); + + var apiResult = PayPalService.EnsureAccessToken(session, settings); + if (apiResult.Success) + { + apiResult = PayPalService.Capture(settings, session, capturePaymentRequest); + + if (apiResult.Success) + result.NewPaymentStatus = PaymentStatus.Paid; + } + + if (!apiResult.Success) + result.Errors.Add(apiResult.ErrorMessage); + + return result; + } + + public override RefundPaymentResult Refund(RefundPaymentRequest refundPaymentRequest) + { + var result = new RefundPaymentResult + { + NewPaymentStatus = refundPaymentRequest.Order.PaymentStatus + }; + + var settings = Services.Settings.LoadSetting(refundPaymentRequest.Order.StoreId); + var session = new PayPalSessionData(); + + var apiResult = PayPalService.EnsureAccessToken(session, settings); + if (apiResult.Success) + { + apiResult = PayPalService.Refund(settings, session, refundPaymentRequest); + if (apiResult.Success) + { + if (refundPaymentRequest.IsPartialRefund) + result.NewPaymentStatus = PaymentStatus.PartiallyRefunded; + else + result.NewPaymentStatus = PaymentStatus.Refunded; + } + } + + if (!apiResult.Success) + result.Errors.Add(apiResult.ErrorMessage); + + return result; + } + + public override VoidPaymentResult Void(VoidPaymentRequest voidPaymentRequest) + { + var result = new VoidPaymentResult + { + NewPaymentStatus = voidPaymentRequest.Order.PaymentStatus + }; + + var settings = Services.Settings.LoadSetting(voidPaymentRequest.Order.StoreId); + var session = new PayPalSessionData(); + + var apiResult = PayPalService.EnsureAccessToken(session, settings); + if (apiResult.Success) + { + apiResult = PayPalService.Void(settings, session, voidPaymentRequest); + if (apiResult.Success) + { + result.NewPaymentStatus = PaymentStatus.Voided; + } + } + + if (!apiResult.Success) + result.Errors.Add(apiResult.ErrorMessage); + + return result; + } + + public override void GetConfigurationRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) + { + actionName = "Configure"; + controllerName = GetControllerName(); + routeValues = new RouteValueDictionary { { "area", "SmartStore.PayPal" } }; + } + + public override void GetPaymentInfoRoute(out string actionName, out string controllerName, out RouteValueDictionary routeValues) + { + actionName = "PaymentInfo"; + controllerName = GetControllerName(); + routeValues = new RouteValueDictionary { { "area", "SmartStore.PayPal" } }; + } + } +} + diff --git a/src/Plugins/SmartStore.PayPal/Providers/PayPalStandardProvider.cs b/src/Plugins/SmartStore.PayPal/Providers/PayPalStandardProvider.cs index f2e2331ce6..4632649153 100644 --- a/src/Plugins/SmartStore.PayPal/Providers/PayPalStandardProvider.cs +++ b/src/Plugins/SmartStore.PayPal/Providers/PayPalStandardProvider.cs @@ -4,58 +4,53 @@ using System.Globalization; using System.IO; using System.Linq; -using System.Net; using System.Text; using System.Web; using System.Web.Routing; using SmartStore.Core.Domain.Common; -using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Logging; using SmartStore.Core.Plugins; using SmartStore.PayPal.Controllers; -using SmartStore.PayPal.Services; using SmartStore.PayPal.Settings; using SmartStore.Services; -using SmartStore.Services.Directory; using SmartStore.Services.Localization; using SmartStore.Services.Orders; using SmartStore.Services.Payments; namespace SmartStore.PayPal { - /// - /// PayPalStandard provider - /// - [SystemName("Payments.PayPalStandard")] + [SystemName("Payments.PayPalStandard")] [FriendlyName("PayPal Standard")] [DisplayOrder(2)] public partial class PayPalStandardProvider : PaymentPluginBase, IConfigurable { - private readonly ICurrencyService _currencyService; - private readonly CurrencySettings _currencySettings; private readonly IOrderTotalCalculationService _orderTotalCalculationService; - private readonly HttpContextBase _httpContext; - private readonly ICommonServices _commonServices; + private readonly ICommonServices _services; private readonly ILogger _logger; - public PayPalStandardProvider(ICurrencyService currencyService, - HttpContextBase httpContext, - CurrencySettings currencySettings, + public PayPalStandardProvider( IOrderTotalCalculationService orderTotalCalculationService, - ICommonServices commonServices, + ICommonServices services, ILogger logger) { - _currencyService = currencyService; - _currencySettings = currencySettings; _orderTotalCalculationService = orderTotalCalculationService; - _httpContext = httpContext; - _commonServices = commonServices; + _services = services; _logger = logger; } + public static string SystemName { get { return "Payments.PayPalStandard"; } } + + public override PaymentMethodType PaymentMethodType + { + get + { + return PaymentMethodType.Redirection; + } + } + /// /// Process a payment /// @@ -66,7 +61,7 @@ public override ProcessPaymentResult ProcessPayment(ProcessPaymentRequest proces var result = new ProcessPaymentResult(); result.NewPaymentStatus = PaymentStatus.Pending; - var settings = _commonServices.Settings.LoadSetting(processPaymentRequest.StoreId); + var settings = _services.Settings.LoadSetting(processPaymentRequest.StoreId); if (settings.BusinessEmail.IsEmpty() || settings.PdtToken.IsEmpty()) { @@ -85,10 +80,11 @@ public override void PostProcessPayment(PostProcessPaymentRequest postProcessPay if (postProcessPaymentRequest.Order.PaymentStatus == PaymentStatus.Paid) return; - var settings = _commonServices.Settings.LoadSetting(postProcessPaymentRequest.Order.StoreId); + var store = _services.StoreService.GetStoreById(postProcessPaymentRequest.Order.StoreId); + var settings = _services.Settings.LoadSetting(postProcessPaymentRequest.Order.StoreId); var builder = new StringBuilder(); - builder.Append(PayPalHelper.GetPaypalUrl(settings)); + builder.Append(settings.GetPayPalUrl()); string orderNumber = postProcessPaymentRequest.Order.GetOrderNumber(); string cmd = (settings.PassProductNamesAndTotals ? "_cart" : "_xclick"); @@ -200,11 +196,11 @@ public override void PostProcessPayment(PostProcessPaymentRequest postProcessPay if (cartTotal > postProcessPaymentRequest.Order.OrderTotal) { - /* Take the difference between what the order total is and what it should be and use that as the "discount". - * The difference equals the amount of the gift card and/or reward points used. - */ + // Take the difference between what the order total is and what it should be and use that as the "discount". + // The difference equals the amount of the gift card and/or reward points used. decimal discountTotal = cartTotal - postProcessPaymentRequest.Order.OrderTotal; discountTotal = Math.Round(discountTotal, 2); + //gift card or rewared point amount applied to cart in SmartStore.NET - shows in Paypal as "discount" builder.AppendFormat("&discount_amount_cart={0}", discountTotal.ToString("0.00", CultureInfo.InvariantCulture)); } @@ -220,7 +216,7 @@ public override void PostProcessPayment(PostProcessPaymentRequest postProcessPay builder.AppendFormat("&custom={0}", postProcessPaymentRequest.Order.OrderGuid); builder.AppendFormat("&charset={0}", "utf-8"); - builder.Append(string.Format("&no_note=1¤cy_code={0}", HttpUtility.UrlEncode(_currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId).CurrencyCode))); + builder.Append(string.Format("&no_note=1¤cy_code={0}", HttpUtility.UrlEncode(store.PrimaryStoreCurrency.CurrencyCode))); builder.AppendFormat("&invoice={0}", HttpUtility.UrlEncode(orderNumber)); builder.AppendFormat("&rm=2", new object[0]); @@ -239,8 +235,8 @@ public override void PostProcessPayment(PostProcessPaymentRequest postProcessPay builder.AppendFormat("&no_shipping=1", new object[0]); } - string returnUrl = _commonServices.WebHelper.GetStoreLocation(false) + "Plugins/PaymentPayPalStandard/PDTHandler"; - string cancelReturnUrl = _commonServices.WebHelper.GetStoreLocation(false) + "Plugins/PaymentPayPalStandard/CancelOrder"; + var returnUrl = _services.WebHelper.GetStoreLocation(store.SslEnabled) + "Plugins/SmartStore.PayPal/PayPalStandard/PDTHandler"; + var cancelReturnUrl = _services.WebHelper.GetStoreLocation(store.SslEnabled) + "Plugins/SmartStore.PayPal/PayPalStandard/CancelOrder"; builder.AppendFormat("&return={0}&cancel_return={1}", HttpUtility.UrlEncode(returnUrl), HttpUtility.UrlEncode(cancelReturnUrl)); //Instant Payment Notification (server to server message) @@ -248,7 +244,7 @@ public override void PostProcessPayment(PostProcessPaymentRequest postProcessPay { string ipnUrl; if (String.IsNullOrWhiteSpace(settings.IpnUrl)) - ipnUrl = _commonServices.WebHelper.GetStoreLocation(false) + "Plugins/PaymentPayPalStandard/IPNHandler"; + ipnUrl = _services.WebHelper.GetStoreLocation(store.SslEnabled) + "Plugins/SmartStore.PayPal/PayPalStandard/IPNHandler"; else ipnUrl = settings.IpnUrl; builder.AppendFormat("¬ify_url={0}", ipnUrl); @@ -285,7 +281,7 @@ public override void PostProcessPayment(PostProcessPaymentRequest postProcessPay builder.AppendFormat("&zip={0}", HttpUtility.UrlEncode(address.ZipPostalCode)); builder.AppendFormat("&email={0}", HttpUtility.UrlEncode(address.Email)); - _httpContext.Response.Redirect(builder.ToString()); + postProcessPaymentRequest.RedirectUrl = builder.ToString(); } /// @@ -315,7 +311,7 @@ public override decimal GetAdditionalHandlingFee(IList(_commonServices.StoreContext.CurrentStore.Id); + var settings = _services.Settings.LoadSetting(_services.StoreContext.CurrentStore.Id); result = this.CalculateAdditionalFee(_orderTotalCalculationService, cart, settings.AdditionalFee, settings.AdditionalFeePercentage); } @@ -325,7 +321,6 @@ public override decimal GetAdditionalHandlingFee(IList /// Gets PDT details /// @@ -335,22 +330,29 @@ public override decimal GetAdditionalHandlingFee(IListResult public bool GetPDTDetails(string tx, PayPalStandardPaymentSettings settings, out Dictionary values, out string response) { - var req = (HttpWebRequest)WebRequest.Create(PayPalHelper.GetPaypalUrl(settings)); - req.Method = "POST"; - req.ContentType = "application/x-www-form-urlencoded"; + var request = settings.GetPayPalWebRequest(); + request.Method = "POST"; + request.ContentType = "application/x-www-form-urlencoded"; - string formContent = string.Format("cmd=_notify-synch&at={0}&tx={1}", settings.PdtToken, tx); - req.ContentLength = formContent.Length; + var formContent = string.Format("cmd=_notify-synch&at={0}&tx={1}", settings.PdtToken, tx); + request.ContentLength = formContent.Length; - using (var sw = new StreamWriter(req.GetRequestStream(), Encoding.ASCII)) - sw.Write(formContent); + using (var sw = new StreamWriter(request.GetRequestStream(), Encoding.ASCII)) + { + sw.Write(formContent); + } response = null; - using (var sr = new StreamReader(req.GetResponse().GetResponseStream())) - response = HttpUtility.UrlDecode(sr.ReadToEnd()); + using (var sr = new StreamReader(request.GetResponse().GetResponseStream())) + { + response = HttpUtility.UrlDecode(sr.ReadToEnd()); + } values = new Dictionary(StringComparer.OrdinalIgnoreCase); - bool firstLine = true, success = false; + + var firstLine = true; + var success = false; + foreach (string l in response.Split('\n')) { string line = l.Trim(); @@ -408,10 +410,10 @@ public List GetLineItems(PostProcessPaymentRequest postProcessPa var order = postProcessPaymentRequest.Order; var lst = new List(); - // order items - foreach (var orderItem in order.OrderItems) + // order items... checkout attributes are included in order total + foreach (var orderItem in order.OrderItems) { - var item = new PayPalLineItem() + var item = new PayPalLineItem { Type = PayPalItemType.CartItem, Name = orderItem.Product.GetLocalized(x => x.Name), @@ -423,30 +425,10 @@ public List GetLineItems(PostProcessPaymentRequest postProcessPa cartTotal += orderItem.PriceExclTax; } - // checkout attributes.... are included in order total - //foreach (var caValue in checkoutAttributeValues) - //{ - // var attributePrice = _taxService.GetCheckoutAttributePrice(caValue, false, order.Customer); - - // if (attributePrice > decimal.Zero && caValue.CheckoutAttribute != null) - // { - // var item = new PayPalLineItem() - // { - // Type = PayPalItemType.CheckoutAttribute, - // Name = caValue.CheckoutAttribute.GetLocalized(x => x.Name), - // Quantity = 1, - // Amount = attributePrice - // }; - // lst.Add(item); - - // cartTotal += attributePrice; - // } - //} - // shipping if (order.OrderShippingExclTax > decimal.Zero) { - var item = new PayPalLineItem() + var item = new PayPalLineItem { Type = PayPalItemType.Shipping, Name = T("Plugins.Payments.PayPalStandard.ShippingFee").Text, @@ -461,10 +443,10 @@ public List GetLineItems(PostProcessPaymentRequest postProcessPa // payment fee if (order.PaymentMethodAdditionalFeeExclTax > decimal.Zero) { - var item = new PayPalLineItem() + var item = new PayPalLineItem { Type = PayPalItemType.PaymentFee, - Name = T("Plugins.Payments.PayPalStandard.PaymentMethodFee").Text, + Name = T("Plugins.Payments.PayPal.PaymentMethodFee").Text, Quantity = 1, Amount = order.PaymentMethodAdditionalFeeExclTax }; @@ -476,7 +458,7 @@ public List GetLineItems(PostProcessPaymentRequest postProcessPa // tax if (order.OrderTax > decimal.Zero) { - var item = new PayPalLineItem() + var item = new PayPalLineItem { Type = PayPalItemType.Tax, Name = T("Plugins.Payments.PayPalStandard.SalesTax").Text, @@ -511,16 +493,18 @@ public void AdjustLineItemAmounts(List paypalItems, PostProcessP return; decimal totalSmartStore = Math.Round(postProcessPaymentRequest.Order.OrderSubtotalExclTax, 2); - decimal totalPayPal = decimal.Zero; - decimal delta, portion, rest; + decimal totalPayPal = decimal.Zero; + decimal delta, portion, rest; + + // calculate what PayPal calculates + cartItems.Each(x => totalPayPal += (x.AmountRounded * x.Quantity)); + totalPayPal = Math.Round(totalPayPal, 2, MidpointRounding.AwayFromZero); - // calculate what PayPal calculates - cartItems.Each(x => totalPayPal += (x.AmountRounded * x.Quantity)); - totalPayPal = Math.Round(totalPayPal, 2, MidpointRounding.AwayFromZero); + // calculate difference + delta = Math.Round(totalSmartStore - totalPayPal, 2); + //"SM: {0}, PP: {1}, delta: {2}".FormatInvariant(totalSmartStore, totalPayPal, delta).Dump(); - // calculate difference - delta = Math.Round(totalSmartStore - totalPayPal, 2); - if (delta == decimal.Zero) + if (delta == decimal.Zero) return; // prepare lines... only lines with quantity = 1 are adjustable. if there is no one, create one. @@ -551,56 +535,12 @@ public void AdjustLineItemAmounts(List paypalItems, PostProcessP restItem.Amount = restItem.Amount + rest; } - //"SM: {0}, PP: {1}, delta: {2} (portion: {3}, rest: {4})".FormatWith(totalSmartStore, totalPayPal, delta, portion, rest).Dump(); - } - catch (Exception exc) - { - _logger.Error(exc.Message, exc); - } - } - - - /// - /// Verifies IPN - /// - /// Form string - /// Values - /// Result - public bool VerifyIPN(string formString, out Dictionary values) - { - // settings: multistore context not possible here. we need the custom value to determine what store it is. - var settings = _commonServices.Settings.LoadSetting(); - - var req = (HttpWebRequest)WebRequest.Create(PayPalHelper.GetPaypalUrl(settings)); - req.Method = "POST"; - req.ContentType = "application/x-www-form-urlencoded"; - req.UserAgent = HttpContext.Current.Request.UserAgent; - - string formContent = string.Format("{0}&cmd=_notify-validate", formString); - req.ContentLength = formContent.Length; - - using (var sw = new StreamWriter(req.GetRequestStream(), Encoding.ASCII)) - { - sw.Write(formContent); - } - - string response = null; - using (var sr = new StreamReader(req.GetResponse().GetResponseStream())) - { - response = HttpUtility.UrlDecode(sr.ReadToEnd()); - } - bool success = response.Trim().Equals("VERIFIED", StringComparison.OrdinalIgnoreCase); - - values = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (string l in formString.Split('&')) + //"SM: {0}, PP: {1}, delta: {2} (portion: {3}, rest: {4})".FormatInvariant(totalSmartStore, totalPayPal, delta, portion, rest).Dump(); + } + catch (Exception exception) { - string line = HttpUtility.UrlDecode(l).Trim(); - int equalPox = line.IndexOf('='); - if (equalPox >= 0) - values.Add(line.Substring(0, equalPox), line.Substring(equalPox + 1)); + _logger.Error(exception.Message, exception); } - - return success; } /// @@ -628,17 +568,47 @@ public override void GetPaymentInfoRoute(out string actionName, out string contr controllerName = "PayPalStandard"; routeValues = new RouteValueDictionary() { { "area", "SmartStore.PayPal" } }; } + } - #region Properties - public override PaymentMethodType PaymentMethodType + public class PayPalLineItem : ICloneable + { + public PayPalItemType Type { get; set; } + public string Name { get; set; } + public int Quantity { get; set; } + public decimal Amount { get; set; } + + public decimal AmountRounded { get { - return PaymentMethodType.Redirection; + return Math.Round(Amount, 2); } } - #endregion + public PayPalLineItem Clone() + { + var item = new PayPalLineItem + { + Type = this.Type, + Name = this.Name, + Quantity = this.Quantity, + Amount = this.Amount + }; + return item; + } + + object ICloneable.Clone() + { + return this.Clone(); + } + } + + public enum PayPalItemType + { + CartItem = 0, + Shipping, + PaymentFee, + Tax } } diff --git a/src/Plugins/SmartStore.PayPal/RouteProvider.cs b/src/Plugins/SmartStore.PayPal/RouteProvider.cs index b6c0707bee..74e44ce093 100644 --- a/src/Plugins/SmartStore.PayPal/RouteProvider.cs +++ b/src/Plugins/SmartStore.PayPal/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.PayPal { @@ -29,8 +29,17 @@ public void RegisterRoutes(RouteCollection routes) ) .DataTokens["area"] = "SmartStore.PayPal"; - //Legacay Routes - routes.MapRoute("SmartStore.PayPalExpress.IPN", + routes.MapRoute("SmartStore.PayPalPlus", + "Plugins/SmartStore.PayPal/{controller}/{action}", + new { controller = "PayPalPlus", action = "Index" }, + new[] { "SmartStore.PayPal.Controllers" } + ) + .DataTokens["area"] = Plugin.SystemName; + + + + //Legacay Routes + routes.MapRoute("SmartStore.PayPalExpress.IPN", "Plugins/PaymentPayPalExpress/IPNHandler", new { controller = "PayPalExpress", action = "IPNHandler" }, new[] { "SmartStore.PayPal.Controllers" } diff --git a/src/Plugins/SmartStore.PayPal/Services/IPayPalService.cs b/src/Plugins/SmartStore.PayPal/Services/IPayPalService.cs new file mode 100644 index 0000000000..2f4b94c1c9 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Services/IPayPalService.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Net; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Payments; +using SmartStore.Core.Domain.Stores; +using SmartStore.PayPal.Settings; +using SmartStore.Services.Payments; + +namespace SmartStore.PayPal.Services +{ + public interface IPayPalService + { + void AddOrderNote(PayPalSettingsBase settings, Order order, string anyString, bool isIpn = false); + + void LogError(Exception exception, string shortMessage = null, string fullMessage = null, bool notify = false, IList errors = null, bool isWarning = false); + + PayPalPaymentInstruction ParsePaymentInstruction(dynamic json); + + string CreatePaymentInstruction(PayPalPaymentInstruction instruct); + + PaymentStatus GetPaymentStatus(string state, string reasonCode, PaymentStatus defaultStatus); + + PayPalResponse CallApi(string method, string path, string accessToken, PayPalApiSettingsBase settings, string data); + + PayPalResponse EnsureAccessToken(PayPalSessionData session, PayPalApiSettingsBase settings); + + PayPalResponse GetPayment(PayPalApiSettingsBase settings, PayPalSessionData session); + + PayPalResponse CreatePayment( + PayPalApiSettingsBase settings, + PayPalSessionData session, + List cart, + string providerSystemName, + string returnUrl, + string cancelUrl); + + PayPalResponse PatchShipping( + PayPalApiSettingsBase settings, + PayPalSessionData session, + List cart, + string providerSystemName); + + PayPalResponse ExecutePayment(PayPalApiSettingsBase settings, PayPalSessionData session); + + PayPalResponse Refund(PayPalApiSettingsBase settings, PayPalSessionData session, RefundPaymentRequest request); + + PayPalResponse Capture(PayPalApiSettingsBase settings, PayPalSessionData session, CapturePaymentRequest request); + + PayPalResponse Void(PayPalApiSettingsBase settings, PayPalSessionData session, VoidPaymentRequest request); + + PayPalResponse UpsertCheckoutExperience(PayPalApiSettingsBase settings, PayPalSessionData session, Store store); + + PayPalResponse DeleteCheckoutExperience(PayPalApiSettingsBase settings, PayPalSessionData session); + + PayPalResponse CreateWebhook(PayPalApiSettingsBase settings, PayPalSessionData session, string url); + + PayPalResponse DeleteWebhook(PayPalApiSettingsBase settings, PayPalSessionData session); + + HttpStatusCode ProcessWebhook( + PayPalApiSettingsBase settings, + NameValueCollection headers, + string rawJson, + string providerSystemName); + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Services/PayPalEnums.cs b/src/Plugins/SmartStore.PayPal/Services/PayPalEnums.cs new file mode 100644 index 0000000000..d5bba1b72c --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Services/PayPalEnums.cs @@ -0,0 +1,27 @@ + +namespace SmartStore.PayPal.Services +{ + public enum PayPalPaymentInstructionItem + { + Reference = 0, + BankRoutingNumber, + Bank, + Bic, + Iban, + AccountHolder, + AccountNumber, + Amount, + PaymentDueDate, + Details + } + + public enum PayPalMessage + { + Message = 0, + Event, + EventId, + State, + Amount, + PaymentId + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Services/PayPalHelper.cs b/src/Plugins/SmartStore.PayPal/Services/PayPalHelper.cs deleted file mode 100644 index fd1c3a32de..0000000000 --- a/src/Plugins/SmartStore.PayPal/Services/PayPalHelper.cs +++ /dev/null @@ -1,244 +0,0 @@ -using System; -using System.Net; -using System.Text; -using System.Web.Routing; -using SmartStore.Core.Domain.Directory; -using SmartStore.Core.Domain.Payments; -using SmartStore.PayPal.PayPalSvc; -using SmartStore.PayPal.Settings; -using SmartStore.Web.Framework.Plugins; - -namespace SmartStore.PayPal.Services -{ - /// - /// Represents paypal helper - /// - public static class PayPalHelper - { - /// - /// Gets a payment status - /// - /// PayPal payment status - /// PayPal pending reason - /// Payment status - public static PaymentStatus GetPaymentStatus(string paymentStatus, string pendingReason) - { - var result = PaymentStatus.Pending; - - if (paymentStatus == null) - paymentStatus = string.Empty; - - if (pendingReason == null) - pendingReason = string.Empty; - - switch (paymentStatus.ToLowerInvariant()) - { - case "pending": - switch (pendingReason.ToLowerInvariant()) - { - case "authorization": - result = PaymentStatus.Authorized; - break; - default: - result = PaymentStatus.Pending; - break; - } - break; - case "processed": - case "completed": - case "canceled_reversal": - result = PaymentStatus.Paid; - break; - case "denied": - case "expired": - case "failed": - case "voided": - result = PaymentStatus.Voided; - break; - case "refunded": - case "reversed": - result = PaymentStatus.Refunded; - break; - default: - break; - } - return result; - } - - /// - /// Checks response - /// - /// response - /// Error message if exists - /// True - response OK; otherwise, false - public static bool CheckSuccess(PluginHelper helper, AbstractResponseType abstractResponse, out string errorMsg) - { - bool success = false; - StringBuilder sb = new StringBuilder(); - switch (abstractResponse.Ack) - { - case AckCodeType.Success: - case AckCodeType.SuccessWithWarning: - success = true; - break; - default: - break; - } - if (null != abstractResponse.Errors) - { - foreach (ErrorType errorType in abstractResponse.Errors) - { - if (sb.Length <= 0) - { - sb.Append(Environment.NewLine); - } - sb.AppendLine("{0}: {1}".FormatWith(helper.GetResource("Admin.System.Log.Fields.FullMessage"), errorType.LongMessage)); - sb.AppendLine("{0}: {1}".FormatWith(helper.GetResource("Admin.System.Log.Fields.ShortMessage"), errorType.ShortMessage)); - sb.Append("Code: ").Append(errorType.ErrorCode).Append(Environment.NewLine); - } - } - errorMsg = sb.ToString(); - return success; - } - - /// - /// Get Paypal currency code - /// - /// Currency - /// Paypal currency code - public static CurrencyCodeType GetPaypalCurrency(Currency currency) - { - CurrencyCodeType currencyCodeType = CurrencyCodeType.USD; - try - { - currencyCodeType = (CurrencyCodeType)Enum.Parse(typeof(CurrencyCodeType), currency.CurrencyCode, true); - } - catch - { - } - return currencyCodeType; - } - - public static string CheckIfButtonExists(string buttonUrl) - { - - HttpWebResponse response = null; - var request = (HttpWebRequest)WebRequest.Create(buttonUrl); - request.Method = "HEAD"; - - try - { - response = (HttpWebResponse)request.GetResponse(); - return buttonUrl; - } - catch (WebException) - { - /* A WebException will be thrown if the status of the response is not `200 OK` */ - return "https://www.paypalobjects.com/en_US/i/btn/btn_xpressCheckout.gif"; - } - finally - { - if (response != null) - { - response.Close(); - } - } - - - } - - public static bool CurrentPageIsBasket(RouteData routeData) - { - return routeData.GetRequiredString("controller").IsCaseInsensitiveEqual("ShoppingCart") - && routeData.GetRequiredString("action").IsCaseInsensitiveEqual("Cart"); - } - - //TODO: join the following two methods, with help of payment method type - - /// - /// Gets Paypal URL - /// - /// - public static string GetPaypalUrl(PayPalSettingsBase settings) - { - return settings.UseSandbox ? - "https://www.sandbox.paypal.com/cgi-bin/webscr" : - "https://www.paypal.com/cgi-bin/webscr"; - } - - /// - /// Gets Paypal URL - /// - /// - public static string GetPaypalServiceUrl(PayPalSettingsBase settings) - { - return settings.UseSandbox ? - "https://api-3t.sandbox.paypal.com/2.0/" : - "https://api-3t.paypal.com/2.0/"; - } - - public static string GetApiVersion() - { - return "109"; - } - - /// - /// Gets API credentials - /// - /// - public static CustomSecurityHeaderType GetPaypalApiCredentials(PayPalApiSettingsBase settings) - { - CustomSecurityHeaderType customSecurityHeaderType = new CustomSecurityHeaderType(); - - customSecurityHeaderType.Credentials = new UserIdPasswordType(); - customSecurityHeaderType.Credentials.Username = settings.ApiAccountName; - customSecurityHeaderType.Credentials.Password = settings.ApiAccountPassword; - customSecurityHeaderType.Credentials.Signature = settings.Signature; - customSecurityHeaderType.Credentials.Subject = ""; - - return customSecurityHeaderType; - } - /// - /// Get Paypal country code - /// - /// Country - /// Paypal country code - public static CountryCodeType GetPaypalCountryCodeType(Country country) - { - CountryCodeType payerCountry = CountryCodeType.US; - try - { - payerCountry = (CountryCodeType)Enum.Parse(typeof(CountryCodeType), country.TwoLetterIsoCode); - } - catch - { - } - return payerCountry; - } - - /// - /// Get Paypal credit card type - /// - /// Credit card type - /// Paypal credit card type - public static CreditCardTypeType GetPaypalCreditCardType(string creditCardType) - { - var creditCardTypeType = (CreditCardTypeType)Enum.Parse(typeof(CreditCardTypeType), creditCardType); - return creditCardTypeType; - } - - public static PaymentActionCodeType GetPaymentAction(PayPalExpressPaymentSettings payPalExpressPaymentSettings) - { - if (payPalExpressPaymentSettings.TransactMode == TransactMode.Authorize) - { - return PaymentActionCodeType.Authorization; - } - else - { - return PaymentActionCodeType.Sale; - } - } - - } -} - diff --git a/src/Plugins/SmartStore.PayPal/Services/PayPalProcessPaymentRequest.cs b/src/Plugins/SmartStore.PayPal/Services/PayPalProcessPaymentRequest.cs deleted file mode 100644 index 22984ffeac..0000000000 --- a/src/Plugins/SmartStore.PayPal/Services/PayPalProcessPaymentRequest.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using SmartStore.Services.Payments; - -namespace SmartStore.PayPal.Services -{ - public class PayPalProcessPaymentRequest : ProcessPaymentRequest - { - /// - /// Gets or sets an order Discount Amount - /// - public decimal Discount { get; set; } - } -} diff --git a/src/Plugins/SmartStore.PayPal/Services/PayPalService.cs b/src/Plugins/SmartStore.PayPal/Services/PayPalService.cs new file mode 100644 index 0000000000..62e53f01d5 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Services/PayPalService.cs @@ -0,0 +1,1177 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Web; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SmartStore.Core.Data; +using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Customers; +using SmartStore.Core.Domain.Discounts; +using SmartStore.Core.Domain.Logging; +using SmartStore.Core.Domain.Orders; +using SmartStore.Core.Domain.Payments; +using SmartStore.Core.Domain.Stores; +using SmartStore.Core.Domain.Tax; +using SmartStore.Core.Localization; +using SmartStore.Core.Logging; +using SmartStore.PayPal.Settings; +using SmartStore.Services; +using SmartStore.Services.Catalog; +using SmartStore.Services.Common; +using SmartStore.Services.Customers; +using SmartStore.Services.Directory; +using SmartStore.Services.Localization; +using SmartStore.Services.Media; +using SmartStore.Services.Orders; +using SmartStore.Services.Payments; +using SmartStore.Services.Tax; + +namespace SmartStore.PayPal.Services +{ + public class PayPalService : IPayPalService + { + private readonly Lazy> _orderRepository; + private readonly ICommonServices _services; + private readonly IOrderService _orderService; + private readonly IOrderProcessingService _orderProcessingService; + private readonly IOrderTotalCalculationService _orderTotalCalculationService; + private readonly IPaymentService _paymentService; + private readonly IPriceCalculationService _priceCalculationService; + private readonly ITaxService _taxService; + private readonly ICurrencyService _currencyService; + private readonly Lazy _pictureService; + private readonly Lazy _companyInfoSettings; + + public PayPalService( + Lazy> orderRepository, + ICommonServices services, + IOrderService orderService, + IOrderProcessingService orderProcessingService, + IOrderTotalCalculationService orderTotalCalculationService, + IPaymentService paymentService, + IPriceCalculationService priceCalculationService, + ITaxService taxService, + ICurrencyService currencyService, + Lazy pictureService, + Lazy companyInfoSettings) + { + _orderRepository = orderRepository; + _services = services; + _orderService = orderService; + _orderProcessingService = orderProcessingService; + _orderTotalCalculationService = orderTotalCalculationService; + _paymentService = paymentService; + _priceCalculationService = priceCalculationService; + _taxService = taxService; + _currencyService = currencyService; + _pictureService = pictureService; + _companyInfoSettings = companyInfoSettings; + + T = NullLocalizer.Instance; + Logger = NullLogger.Instance; + } + + public Localizer T { get; set; } + public ILogger Logger { get; set; } + + private Dictionary CreateAddress(Address addr, bool addRecipientName) + { + var dic = new Dictionary(); + + dic.Add("line1", addr.Address1.Truncate(100)); + + if (addr.Address2.HasValue()) + { + dic.Add("line2", addr.Address2.Truncate(100)); + } + + dic.Add("city", addr.City.Truncate(50)); + + if (addr.CountryId != 0 && addr.Country != null) + { + dic.Add("country_code", addr.Country.TwoLetterIsoCode); + } + + dic.Add("postal_code", addr.ZipPostalCode.Truncate(20)); + + if (addr.StateProvinceId != 0 && addr.StateProvince != null) + { + dic.Add("state", addr.StateProvince.Abbreviation.Truncate(100)); + } + + if (addRecipientName) + { + dic.Add("recipient_name", addr.GetFullName().Truncate(50)); + } + + return dic; + } + + private Dictionary CreateAmount( + Store store, + Customer customer, + List cart, + string providerSystemName, + List> items) + { + var amount = new Dictionary(); + var amountDetails = new Dictionary(); + var language = _services.WorkContext.WorkingLanguage; + var currency = _services.WorkContext.WorkingCurrency; + var currencyCode = store.PrimaryStoreCurrency.CurrencyCode; + var includingTax = (_services.WorkContext.GetTaxDisplayTypeFor(customer, store.Id) == TaxDisplayType.IncludingTax); + + Discount orderAppliedDiscount; + List appliedGiftCards; + int redeemedRewardPoints = 0; + decimal redeemedRewardPointsAmount; + decimal orderDiscountInclTax; + decimal totalOrderItems = decimal.Zero; + var taxTotal = decimal.Zero; + + var shipping = Math.Round(_orderTotalCalculationService.GetShoppingCartShippingTotal(cart) ?? decimal.Zero, 2); + + var additionalHandlingFee = _paymentService.GetAdditionalHandlingFee(cart, providerSystemName); + var paymentFeeBase = _taxService.GetPaymentMethodAdditionalFee(additionalHandlingFee, customer); + var paymentFee = Math.Round(_currencyService.ConvertFromPrimaryStoreCurrency(paymentFeeBase, currency), 2); + + var total = Math.Round(_orderTotalCalculationService.GetShoppingCartTotal(cart, out orderDiscountInclTax, out orderAppliedDiscount, out appliedGiftCards, + out redeemedRewardPoints, out redeemedRewardPointsAmount) ?? decimal.Zero, 2); + + // line items + foreach (var item in cart) + { + decimal unitPriceTaxRate = decimal.Zero; + decimal unitPrice = _priceCalculationService.GetUnitPrice(item, true); + decimal productPrice = _taxService.GetProductPrice(item.Item.Product, unitPrice, includingTax, customer, out unitPriceTaxRate); + + if (items != null && productPrice != decimal.Zero) + { + var line = new Dictionary(); + line.Add("quantity", item.Item.Quantity); + line.Add("name", item.Item.Product.GetLocalized(x => x.Name, language.Id, true, false).Truncate(127)); + line.Add("price", productPrice.FormatInvariant()); + line.Add("currency", currencyCode); + line.Add("sku", item.Item.Product.Sku.Truncate(50)); + items.Add(line); + } + + totalOrderItems += (Math.Round(productPrice, 2) * item.Item.Quantity); + } + + if (items != null && paymentFee != decimal.Zero) + { + var line = new Dictionary(); + line.Add("quantity", "1"); + line.Add("name", T("Order.PaymentMethodAdditionalFee").Text.Truncate(127)); + line.Add("price", paymentFee.FormatInvariant()); + line.Add("currency", currencyCode); + items.Add(line); + + totalOrderItems += Math.Round(paymentFee, 2); + } + + if (!includingTax) + { + // "To avoid rounding errors we recommend not submitting tax amounts on line item basis. + // Calculated tax amounts for the entire shopping basket may be submitted in the amount objects. + // In this case the item amounts will be treated as amounts excluding tax. + // In a B2C scenario, where taxes are included, no taxes should be submitted to PayPal." + + SortedDictionary taxRates = null; + taxTotal = Math.Round(_orderTotalCalculationService.GetTaxTotal(cart, out taxRates), 2); + + amountDetails.Add("tax", taxTotal.FormatInvariant()); + } + + var itemsPlusMisc = (totalOrderItems + taxTotal + shipping); + + if (total != itemsPlusMisc) + { + var otherAmount = Math.Round(total - itemsPlusMisc, 2); + totalOrderItems += otherAmount; + + if (items != null && otherAmount != decimal.Zero) + { + // e.g. discount applied to cart total + var line = new Dictionary(); + line.Add("quantity", "1"); + line.Add("name", T("Plugins.SmartStore.PayPal.Other").Text.Truncate(127)); + line.Add("price", otherAmount.FormatInvariant()); + line.Add("currency", currencyCode); + items.Add(line); + } + } + + // fill amount object + amountDetails.Add("shipping", shipping.FormatInvariant()); + amountDetails.Add("subtotal", totalOrderItems.FormatInvariant()); + + //if (paymentFee != decimal.Zero) + //{ + // amountDetails.Add("handling_fee", paymentFee.FormatInvariant()); + //} + + amount.Add("total", total.FormatInvariant()); + amount.Add("currency", currencyCode); + amount.Add("details", amountDetails); + + return amount; + } + + private string ToInfoString(dynamic json) + { + var sb = new StringBuilder(); + + try + { + string[] strings = T("Plugins.SmartStore.PayPal.MessageStrings").Text.SplitSafe(";"); + var message = (string)json.summary; + var eventType = (string)json.event_type; + var eventId = (string)json.id; + string state = null; + string amount = null; + string paymentId = null; + + if (json.resource != null) + { + state = (string)json.resource.state; + paymentId = (string)json.resource.parent_payment; + + if (json.resource.amount != null) + amount = string.Concat((string)json.resource.amount.total, " ", (string)json.resource.amount.currency); + } + + sb.AppendLine("{0}: {1}".FormatInvariant(strings.SafeGet((int)PayPalMessage.Message), message.NaIfEmpty())); + sb.AppendLine("{0}: {1}".FormatInvariant(strings.SafeGet((int)PayPalMessage.Event), eventType.NaIfEmpty())); + sb.AppendLine("{0}: {1}".FormatInvariant(strings.SafeGet((int)PayPalMessage.EventId), eventId.NaIfEmpty())); + sb.AppendLine("{0}: {1}".FormatInvariant(strings.SafeGet((int)PayPalMessage.PaymentId), paymentId.NaIfEmpty())); + sb.AppendLine("{0}: {1}".FormatInvariant(strings.SafeGet((int)PayPalMessage.State), state.NaIfEmpty())); + sb.AppendLine("{0}: {1}".FormatInvariant(strings.SafeGet((int)PayPalMessage.Amount), amount.NaIfEmpty())); + } + catch { } + + return sb.ToString(); + } + + public static string GetApiUrl(bool sandbox) + { + return sandbox ? "https://api.sandbox.paypal.com" : "https://api.paypal.com"; + } + + public static Dictionary GetSecurityProtocols() + { + var dic = new Dictionary(); + + foreach (SecurityProtocolType protocol in Enum.GetValues(typeof(SecurityProtocolType))) + { + string friendlyName = null; + switch (protocol) + { + case SecurityProtocolType.Ssl3: + friendlyName = "SSL 3.0"; + break; + case SecurityProtocolType.Tls: + friendlyName = "TLS 1.0"; + break; + case SecurityProtocolType.Tls11: + friendlyName = "TLS 1.1"; + break; + case SecurityProtocolType.Tls12: + friendlyName = "TLS 1.2"; + break; + default: + friendlyName = protocol.ToString().ToUpper(); + break; + } + + dic.Add(protocol, friendlyName); + } + return dic; + } + + public void AddOrderNote(PayPalSettingsBase settings, Order order, string anyString, bool isIpn = false) + { + try + { + if (order == null || anyString.IsEmpty() || (settings != null && !settings.AddOrderNotes)) + return; + + string[] orderNoteStrings = T("Plugins.SmartStore.PayPal.OrderNoteStrings").Text.SplitSafe(";"); + var faviconUrl = "{0}Plugins/{1}/Content/favicon.png".FormatInvariant(_services.WebHelper.GetStoreLocation(false), Plugin.SystemName); + + var sb = new StringBuilder(); + sb.AppendFormat("", faviconUrl); + + var note = orderNoteStrings.SafeGet(0).FormatInvariant(anyString); + + sb.AppendFormat("{0}", note); + + if (isIpn) + order.HasNewPaymentNotification = true; + + _orderService.AddOrderNote(order, sb.ToString()); + } + catch { } + } + + public void LogError(Exception exception, string shortMessage = null, string fullMessage = null, bool notify = false, IList errors = null, bool isWarning = false) + { + try + { + if (exception != null) + { + shortMessage = exception.ToAllMessages(); + fullMessage = exception.ToString(); + } + + if (shortMessage.HasValue()) + { + shortMessage = "PayPal. " + shortMessage; + Logger.InsertLog(isWarning ? LogLevel.Warning : LogLevel.Error, shortMessage, fullMessage.EmptyNull()); + + if (notify) + { + if (isWarning) + _services.Notifier.Warning(new LocalizedString(shortMessage)); + else + _services.Notifier.Error(new LocalizedString(shortMessage)); + } + } + } + catch (Exception) { } + + if (errors != null && shortMessage.HasValue()) + { + errors.Add(shortMessage); + } + } + + public PayPalPaymentInstruction ParsePaymentInstruction(dynamic json) + { + if (json == null) + return null; + + DateTime dt; + var result = new PayPalPaymentInstruction(); + + try + { + result.ReferenceNumber = (string)json.reference_number; + result.Type = (string)json.instruction_type; + result.Note = (string)json.note; + + if (DateTime.TryParse((string)json.payment_due_date, CultureInfo.InvariantCulture, DateTimeStyles.None, out dt)) + { + result.DueDate = dt; + } + + if (json.amount != null) + { + result.AmountCurrencyCode = (string)json.amount.currency; + result.Amount = decimal.Parse((string)json.amount.value, CultureInfo.InvariantCulture); + } + + var rbi = json.recipient_banking_instruction; + + if (rbi != null) + { + result.RecipientBanking = new PayPalPaymentInstruction.RecipientBankingInstruction(); + result.RecipientBanking.BankName = (string)rbi.bank_name; + result.RecipientBanking.AccountHolderName = (string)rbi.account_holder_name; + result.RecipientBanking.AccountNumber = (string)rbi.account_number; + result.RecipientBanking.RoutingNumber = (string)rbi.routing_number; + result.RecipientBanking.Iban = (string)rbi.international_bank_account_number; + result.RecipientBanking.Bic = (string)rbi.bank_identifier_code; + } + + if (json.links != null) + { + result.Link = (string)json.links[0].href; + } + } + catch { } + + return result; + } + + public string CreatePaymentInstruction(PayPalPaymentInstruction instruct) + { + if (instruct == null || instruct.RecipientBanking == null) + return null; + + if (!instruct.IsManualBankTransfer && !instruct.IsPayUponInvoice) + return null; + + var sb = new StringBuilder("
      "); + var paragraphTemplate = "
      {0}
      "; + var rowTemplate = "{0}: {1}
      "; + var instructStrings = T("Plugins.SmartStore.PayPal.PaymentInstructionStrings").Text.SplitSafe(";"); + + if (instruct.IsManualBankTransfer) + { + sb.AppendFormat(paragraphTemplate, T("Plugins.SmartStore.PayPal.ManualBankTransferNote")); + + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.Reference), instruct.ReferenceNumber); + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.AccountNumber), instruct.RecipientBanking.AccountNumber); + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.AccountHolder), instruct.RecipientBanking.AccountHolderName); + + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.Bank), instruct.RecipientBanking.BankName); + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.Iban), instruct.RecipientBanking.Iban); + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.Bic), instruct.RecipientBanking.Bic); + } + else if (instruct.IsPayUponInvoice) + { + string amount = null; + var culture = new CultureInfo(_services.WorkContext.WorkingLanguage.LanguageCulture ?? "de-DE"); + + try + { + var currency = _currencyService.GetCurrencyByCode(instruct.AmountCurrencyCode); + var format = (currency != null && currency.CustomFormatting.HasValue() ? currency.CustomFormatting : "C"); + + amount = instruct.Amount.ToString(format, culture); + } + catch { } + + if (amount.IsEmpty()) + { + amount = string.Concat(instruct.Amount.ToString("N"), " ", instruct.AmountCurrencyCode); + } + + var intro = T("Plugins.SmartStore.PayPal.PayUponInvoiceLegalNote", _companyInfoSettings.Value.CompanyName.NaIfEmpty()); + + // /v1/payments/payment//payment-instruction not working anymore. Serves 401 unauthorized. + //if (instruct.Link.HasValue()) + //{ + // intro = "{0} {2}.".FormatInvariant(intro, instruct.Link, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.Details)); + //} + + sb.AppendFormat(paragraphTemplate, intro); + + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.Bank), instruct.RecipientBanking.BankName); + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.AccountHolder), instruct.RecipientBanking.AccountHolderName); + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.Iban), instruct.RecipientBanking.Iban); + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.Bic), instruct.RecipientBanking.Bic); + sb.Append("
      "); + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.Amount), amount); + if (instruct.DueDate.HasValue) + { + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.PaymentDueDate), instruct.DueDate.Value.ToString("d", culture)); + } + sb.AppendFormat(rowTemplate, instructStrings.SafeGet((int)PayPalPaymentInstructionItem.Reference), instruct.ReferenceNumber); + } + + sb.Append("
      "); + + return sb.ToString(); + } + + public PaymentStatus GetPaymentStatus(string state, string reasonCode, PaymentStatus defaultStatus) + { + var result = defaultStatus; + + if (state == null) + state = string.Empty; + + if (reasonCode == null) + reasonCode = string.Empty; + + switch (state.ToLowerInvariant()) + { + case "authorized": + result = PaymentStatus.Authorized; + break; + case "pending": + switch (reasonCode.ToLowerInvariant()) + { + case "authorization": + result = PaymentStatus.Authorized; + break; + default: + result = PaymentStatus.Pending; + break; + } + break; + case "completed": + case "captured": + case "partially_captured": + case "canceled_reversal": + result = PaymentStatus.Paid; + break; + case "denied": + case "expired": + case "failed": + case "voided": + result = PaymentStatus.Voided; + break; + case "reversed": + case "refunded": + result = PaymentStatus.Refunded; + break; + case "partially_refunded": + result = PaymentStatus.PartiallyRefunded; + break; + } + + return result; + } + + public PayPalResponse CallApi(string method, string path, string accessToken, PayPalApiSettingsBase settings, string data) + { + var isJson = (data.HasValue() && (data.StartsWith("{") || data.StartsWith("["))); + var encoding = (isJson ? Encoding.UTF8 : Encoding.ASCII); + var result = new PayPalResponse(); + HttpWebResponse webResponse = null; + + var url = GetApiUrl(settings.UseSandbox) + path.EnsureStartsWith("/"); + + if (method.IsCaseInsensitiveEqual("GET") && data.HasValue()) + url = url.EnsureEndsWith("?") + data; + + if (settings.SecurityProtocol.HasValue) + ServicePointManager.SecurityProtocol = settings.SecurityProtocol.Value; + + var request = (HttpWebRequest)WebRequest.Create(url); + request.Method = method; + request.Accept = "application/json"; + request.ContentType = (isJson ? "application/json" : "application/x-www-form-urlencoded"); + + try + { + if (HttpContext.Current != null && HttpContext.Current.Request != null) + request.UserAgent = HttpContext.Current.Request.UserAgent; + else + request.UserAgent = Plugin.SystemName; + } + catch { } + + if (path.EmptyNull().EndsWith("/token")) + { + // see https://github.com/paypal/sdk-core-dotnet/blob/master/Source/SDK/OAuthTokenCredential.cs + byte[] credentials = Encoding.UTF8.GetBytes("{0}:{1}".FormatInvariant(settings.ClientId, settings.Secret)); + + request.Headers.Add("Authorization", "Basic " + Convert.ToBase64String(credentials)); + } + else + { + request.Headers["Authorization"] = "Bearer " + accessToken.EmptyNull(); + } + + request.Headers["PayPal-Partner-Attribution-Id"] = "SmartStoreAG_Cart_PayPalPlus"; + + if (data.HasValue() && (method.IsCaseInsensitiveEqual("POST") || method.IsCaseInsensitiveEqual("PUT") || method.IsCaseInsensitiveEqual("PATCH"))) + { + byte[] bytes = encoding.GetBytes(data); + + request.ContentLength = bytes.Length; + + using (var stream = request.GetRequestStream()) + { + stream.Write(bytes, 0, bytes.Length); + } + } + else + { + request.ContentLength = 0; + } + + try + { + webResponse = request.GetResponse() as HttpWebResponse; + result.Success = ((int)webResponse.StatusCode < 400); + } + catch (WebException wexc) + { + result.Success = false; + result.ErrorMessage = wexc.ToString(); + webResponse = wexc.Response as HttpWebResponse; + } + catch (Exception exception) + { + result.Success = false; + result.ErrorMessage = exception.ToString(); + LogError(exception); + } + + try + { + if (webResponse != null) + { + using (var reader = new StreamReader(webResponse.GetResponseStream(), Encoding.UTF8)) + { + var rawResponse = reader.ReadToEnd(); + if (rawResponse.HasValue()) + { + try + { + if (rawResponse.StartsWith("[")) + result.Json = JArray.Parse(rawResponse); + else + result.Json = JObject.Parse(rawResponse); + + if (result.Json != null) + { + if (!result.Success) + { + var name = (string)result.Json.name; + var message = (string)result.Json.message; + + if (name.IsEmpty()) + name = (string)result.Json.error; + + if (message.IsEmpty()) + message = (string)result.Json.error_description; + + result.ErrorMessage = "{0} ({1}).".FormatInvariant(message.NaIfEmpty(), name.NaIfEmpty()); + } + } + } + catch + { + if (!result.Success) + result.ErrorMessage = rawResponse; + } + } + } + + if (!result.Success) + { + if (result.ErrorMessage.IsEmpty()) + result.ErrorMessage = webResponse.StatusDescription; + + LogError(null, result.ErrorMessage, string.Concat(data.NaIfEmpty(), "\r\n\r\n", result.Json == null ? "" : result.Json.ToString()), false); + } + } + } + catch (Exception exception) + { + LogError(exception); + } + finally + { + if (webResponse != null) + { + webResponse.Close(); + webResponse.Dispose(); + } + } + + return result; + } + + public PayPalResponse EnsureAccessToken(PayPalSessionData session, PayPalApiSettingsBase settings) + { + if (session.AccessToken.IsEmpty() || DateTime.UtcNow >= session.TokenExpiration) + { + var result = CallApi("POST", "/v1/oauth2/token", null, settings, "grant_type=client_credentials"); + + if (result.Success) + { + session.AccessToken = (string)result.Json.access_token; + + var expireSeconds = ((string)result.Json.expires_in).ToInt(5 * 60); + + session.TokenExpiration = DateTime.UtcNow.AddSeconds(expireSeconds); + } + else + { + return result; + } + } + + return new PayPalResponse + { + Success = session.AccessToken.HasValue() + }; + } + + public PayPalResponse GetPayment(PayPalApiSettingsBase settings, PayPalSessionData session) + { + var result = CallApi("GET", "/v1/payments/payment/" + session.PaymentId, session.AccessToken, settings, null); + + if (result.Success && result.Json != null) + { + result.Id = (string)result.Json.id; + } + + return result; + } + + public PayPalResponse CreatePayment( + PayPalApiSettingsBase settings, + PayPalSessionData session, + List cart, + string providerSystemName, + string returnUrl, + string cancelUrl) + { + var store = _services.StoreContext.CurrentStore; + var customer = _services.WorkContext.CurrentCustomer; + + //var dateOfBirth = customer.GetAttribute(SystemCustomerAttributeNames.DateOfBirth); + + var data = new Dictionary(); + var redirectUrls = new Dictionary(); + var payer = new Dictionary(); + //var payerInfo = new Dictionary(); + var transaction = new Dictionary(); + var items = new List>(); + var itemList = new Dictionary(); + + // "PayPal PLUS only supports transaction type “Sale” (instant settlement)" + if (providerSystemName == PayPalPlusProvider.SystemName) + data.Add("intent", "sale"); + else + data.Add("intent", settings.TransactMode == TransactMode.AuthorizeAndCapture ? "sale" : "authorize"); + + if (settings.ExperienceProfileId.HasValue()) + data.Add("experience_profile_id", settings.ExperienceProfileId); + + // redirect urls + if (returnUrl.HasValue()) + redirectUrls.Add("return_url", returnUrl); + + if (cancelUrl.HasValue()) + redirectUrls.Add("cancel_url", cancelUrl); + + if (redirectUrls.Any()) + data.Add("redirect_urls", redirectUrls); + + // payer, payer_info + // paypal review: do not transmit + //if (dateOfBirth.HasValue) + //{ + // payerInfo.Add("birth_date", dateOfBirth.Value.ToString("yyyy-MM-dd")); + //} + //if (customer.BillingAddress != null) + //{ + // payerInfo.Add("billing_address", CreateAddress(customer.BillingAddress, false)); + //} + + payer.Add("payment_method", "paypal"); + //payer.Add("payer_info", payerInfo); + data.Add("payer", payer); + + var amount = CreateAmount(store, customer, cart, providerSystemName, items); + + itemList.Add("items", items); + + transaction.Add("amount", amount); + transaction.Add("item_list", itemList); + transaction.Add("invoice_number", session.OrderGuid.ToString()); + + data.Add("transactions", new List> { transaction }); + + var result = CallApi("POST", "/v1/payments/payment", session.AccessToken, settings, JsonConvert.SerializeObject(data)); + + if (result.Success && result.Json != null) + { + result.Id = (string)result.Json.id; + } + + //Logger.InsertLog(LogLevel.Information, "PayPal PLUS", JsonConvert.SerializeObject(data, Formatting.Indented) + "\r\n\r\n" + (result.Json != null ? result.Json.ToString() : "")); + + return result; + } + + public PayPalResponse PatchShipping( + PayPalApiSettingsBase settings, + PayPalSessionData session, + List cart, + string providerSystemName) + { + var data = new List>(); + var amountTotal = new Dictionary(); + + var store = _services.StoreContext.CurrentStore; + var customer = _services.WorkContext.CurrentCustomer; + + if (customer.ShippingAddress != null) + { + var shippingAddress = new Dictionary(); + shippingAddress.Add("op", "add"); + shippingAddress.Add("path", "/transactions/0/item_list/shipping_address"); + shippingAddress.Add("value", CreateAddress(customer.ShippingAddress, true)); + data.Add(shippingAddress); + } + + // update of whole amount object required. patching single amount values not possible (MALFORMED_REQUEST). + var amount = CreateAmount(store, customer, cart, providerSystemName, null); + + amountTotal.Add("op", "replace"); + amountTotal.Add("path", "/transactions/0/amount"); + amountTotal.Add("value", amount); + data.Add(amountTotal); + + var result = CallApi("PATCH", "/v1/payments/payment/{0}".FormatInvariant(session.PaymentId), session.AccessToken, settings, JsonConvert.SerializeObject(data)); + + //Logger.InsertLog(LogLevel.Information, "PayPal PLUS", JsonConvert.SerializeObject(data, Formatting.Indented) + "\r\n\r\n" + (result.Json != null ? result.Json.ToString() : "")); + + return result; + } + + public PayPalResponse ExecutePayment(PayPalApiSettingsBase settings, PayPalSessionData session) + { + var data = new Dictionary(); + data.Add("payer_id", session.PayerId); + + var result = CallApi("POST", "/v1/payments/payment/{0}/execute".FormatInvariant(session.PaymentId), session.AccessToken, settings, JsonConvert.SerializeObject(data)); + + if (result.Success && result.Json != null) + { + result.Id = (string)result.Json.id; + + //Logger.InsertLog(LogLevel.Information, "PayPal PLUS", JsonConvert.SerializeObject(data, Formatting.Indented) + "\r\n\r\n" + result.Json.ToString()); + } + + return result; + } + + public PayPalResponse Refund(PayPalApiSettingsBase settings, PayPalSessionData session, RefundPaymentRequest request) + { + var data = new Dictionary(); + var store = _services.StoreService.GetStoreById(request.Order.StoreId); + var isSale = request.Order.AuthorizationTransactionResult.Contains("(sale)"); + + var path = "/v1/payments/{0}/{1}/refund".FormatInvariant(isSale ? "sale" : "capture", request.Order.CaptureTransactionId); + + var amount = new Dictionary(); + amount.Add("total", request.AmountToRefund.FormatInvariant()); + amount.Add("currency", store.PrimaryStoreCurrency.CurrencyCode); + + data.Add("amount", amount); + + var result = CallApi("POST", path, session.AccessToken, settings, data.Any() ? JsonConvert.SerializeObject(data) : null); + + if (result.Success && result.Json != null) + { + result.Id = (string)result.Json.id; + } + + //Logger.InsertLog(LogLevel.Information, "PayPal Refund", JsonConvert.SerializeObject(data, Formatting.Indented) + "\r\n\r\n" + (result.Json != null ? result.Json.ToString() : "")); + + return result; + } + + public PayPalResponse Capture(PayPalApiSettingsBase settings, PayPalSessionData session, CapturePaymentRequest request) + { + var data = new Dictionary(); + //var isAuthorize = request.Order.AuthorizationTransactionCode.IsCaseInsensitiveEqual("authorize"); + + var path = "/v1/payments/authorization/{0}/capture".FormatInvariant(request.Order.AuthorizationTransactionId); + + var store = _services.StoreService.GetStoreById(request.Order.StoreId); + + var amount = new Dictionary(); + amount.Add("total", request.Order.OrderTotal.FormatInvariant()); + amount.Add("currency", store.PrimaryStoreCurrency.CurrencyCode); + + data.Add("amount", amount); + + var result = CallApi("POST", path, session.AccessToken, settings, JsonConvert.SerializeObject(data)); + + if (result.Success && result.Json != null) + { + result.Id = (string)result.Json.id; + } + + return result; + } + + public PayPalResponse Void(PayPalApiSettingsBase settings, PayPalSessionData session, VoidPaymentRequest request) + { + var path = "/v1/payments/authorization/{0}/void".FormatInvariant(request.Order.AuthorizationTransactionId); + + var result = CallApi("POST", path, session.AccessToken, settings, null); + + if (result.Success && result.Json != null) + { + result.Id = (string)result.Json.id; + } + + return result; + } + + public PayPalResponse UpsertCheckoutExperience(PayPalApiSettingsBase settings, PayPalSessionData session, Store store) + { + PayPalResponse result; + var name = store.Name; + var logo = _pictureService.Value.GetPictureById(store.LogoPictureId); + var path = "/v1/payment-experience/web-profiles"; + + var data = new Dictionary(); + var presentation = new Dictionary(); + var inpuFields = new Dictionary(); + + // find existing profile id, only one profile per profile name possible + if (settings.ExperienceProfileId.IsEmpty()) + { + result = CallApi("GET", path, session.AccessToken, settings, null); + if (result.Success && result.Json != null) + { + foreach (var profile in result.Json) + { + var profileName = (string)profile.name; + if (profileName.IsCaseInsensitiveEqual(name)) + { + settings.ExperienceProfileId = (string)profile.id; + break; + } + } + } + } + + presentation.Add("brand_name", name); + presentation.Add("locale_code", _services.WorkContext.WorkingLanguage.UniqueSeoCode.EmptyNull().ToUpper()); + + if (logo != null) + presentation.Add("logo_image", _pictureService.Value.GetPictureUrl(logo, showDefaultPicture: false, storeLocation: store.Url)); + + inpuFields.Add("allow_note", false); + inpuFields.Add("no_shipping", 0); + inpuFields.Add("address_override", 1); + + data.Add("name", name); + data.Add("presentation", presentation); + data.Add("input_fields", inpuFields); + + if (settings.ExperienceProfileId.HasValue()) + path = string.Concat(path, "/", HttpUtility.UrlPathEncode(settings.ExperienceProfileId)); + + result = CallApi(settings.ExperienceProfileId.HasValue() ? "PUT" : "POST", path, session.AccessToken, settings, JsonConvert.SerializeObject(data)); + + if (result.Success) + { + if (result.Json != null) + result.Id = (string)result.Json.id; + else + result.Id = settings.ExperienceProfileId; + } + + return result; + } + + public PayPalResponse DeleteCheckoutExperience(PayPalApiSettingsBase settings, PayPalSessionData session) + { + var result = CallApi("DELETE", "/v1/payment-experience/web-profiles/" + settings.ExperienceProfileId, session.AccessToken, settings, null); + + if (result.Success && result.Json != null) + { + result.Id = (string)result.Json.id; + } + + return result; + } + + public PayPalResponse CreateWebhook(PayPalApiSettingsBase settings, PayPalSessionData session, string url) + { + var data = new Dictionary(); + var events = new List>(); + + events.Add(new Dictionary { { "name", "PAYMENT.AUTHORIZATION.VOIDED" } }); + events.Add(new Dictionary { { "name", "PAYMENT.CAPTURE.COMPLETED" } }); + events.Add(new Dictionary { { "name", "PAYMENT.CAPTURE.DENIED" } }); + events.Add(new Dictionary { { "name", "PAYMENT.CAPTURE.PENDING" } }); + events.Add(new Dictionary { { "name", "PAYMENT.CAPTURE.REFUNDED" } }); + events.Add(new Dictionary { { "name", "PAYMENT.CAPTURE.REVERSED" } }); + events.Add(new Dictionary { { "name", "PAYMENT.SALE.COMPLETED" } }); + events.Add(new Dictionary { { "name", "PAYMENT.SALE.DENIED" } }); + events.Add(new Dictionary { { "name", "PAYMENT.SALE.PENDING" } }); + events.Add(new Dictionary { { "name", "PAYMENT.SALE.REFUNDED" } }); + events.Add(new Dictionary { { "name", "PAYMENT.SALE.REVERSED" } }); + + data.Add("url", url); + data.Add("event_types", events); + + var result = CallApi("POST", "/v1/notifications/webhooks", session.AccessToken, settings, JsonConvert.SerializeObject(data)); + + if (result.Success && result.Json != null) + { + result.Id = (string)result.Json.id; + } + + return result; + } + + public PayPalResponse DeleteWebhook(PayPalApiSettingsBase settings, PayPalSessionData session) + { + var result = CallApi("DELETE", "/v1/notifications/webhooks/" + settings.WebhookId, session.AccessToken, settings, null); + + if (result.Success && result.Json != null) + { + result.Id = (string)result.Json.id; + } + + return result; + } + + /// return 503 (HttpStatusCode.ServiceUnavailable) to ask paypal to resend it at later time again + public HttpStatusCode ProcessWebhook( + PayPalApiSettingsBase settings, + NameValueCollection headers, + string rawJson, + string providerSystemName) + { + if (rawJson.IsEmpty()) + return HttpStatusCode.OK; + + dynamic json = JObject.Parse(rawJson); + var eventType = (string)json.event_type; + + //foreach (var key in headers.AllKeys)"{0}: {1}".FormatInvariant(key, headers[key]).Dump(); + //string data = JsonConvert.SerializeObject(json, Formatting.Indented);data.Dump(); + + + // validating against PayPal SDK failing using sandbox, so better we do not use it: + //var apiContext = new global::PayPal.Api.APIContext + //{ + // AccessToken = "I do not have one here", + // Config = new Dictionary + // { + // { "mode", settings.UseSandbox ? "sandbox" : "live" }, + // { "clientId", settings.ClientId }, + // { "clientSecret", settings.Secret }, + // { "webhook.id", setting.WebhookId }, + // } + //}; + //var result = global::PayPal.Api.WebhookEvent.ValidateReceivedEvent(apiContext, headers, rawJson, webhookId); + //} + + var paymentId = (string)json.resource.parent_payment; + if (paymentId.IsEmpty()) + { + LogError(null, T("Plugins.SmartStore.PayPal.FoundOrderForPayment", 0, "".NaIfEmpty()), JsonConvert.SerializeObject(json, Formatting.Indented), isWarning: true); + return HttpStatusCode.OK; + } + + var orders = _orderRepository.Value.Table + .Where(x => x.PaymentMethodSystemName == providerSystemName && x.AuthorizationTransactionCode == paymentId) + .ToList(); + + if (orders.Count != 1) + { + LogError(null, T("Plugins.SmartStore.PayPal.FoundOrderForPayment", orders.Count, paymentId), JsonConvert.SerializeObject(json, Formatting.Indented), isWarning: true); + return HttpStatusCode.OK; + } + + var order = orders.First(); + var store = _services.StoreService.GetStoreById(order.StoreId); + + var total = decimal.Zero; + var currency = (string)json.resource.amount.currency; + var primaryCurrency = store.PrimaryStoreCurrency.CurrencyCode; + + if (!primaryCurrency.IsCaseInsensitiveEqual(currency)) + { + LogError(null, T("Plugins.SmartStore.PayPal.CurrencyNotEqual", currency.NaIfEmpty(), primaryCurrency), JsonConvert.SerializeObject(json, Formatting.Indented), isWarning: true); + return HttpStatusCode.OK; + } + + eventType = eventType.Substring(eventType.LastIndexOf('.') + 1); + + var newPaymentStatus = GetPaymentStatus(eventType, "authorization", order.PaymentStatus); + + var isValidTotal = decimal.TryParse((string)json.resource.amount.total, NumberStyles.Currency, CultureInfo.InvariantCulture, out total); + + if (newPaymentStatus == PaymentStatus.Refunded && (Math.Abs(order.OrderTotal) - Math.Abs(total)) > decimal.Zero) + { + newPaymentStatus = PaymentStatus.PartiallyRefunded; + } + + switch (newPaymentStatus) + { + case PaymentStatus.Pending: + break; + case PaymentStatus.Authorized: + if (_orderProcessingService.CanMarkOrderAsAuthorized(order)) + _orderProcessingService.MarkAsAuthorized(order); + break; + case PaymentStatus.Paid: + if (_orderProcessingService.CanMarkOrderAsPaid(order)) + _orderProcessingService.MarkOrderAsPaid(order); + break; + case PaymentStatus.Refunded: + if (_orderProcessingService.CanRefundOffline(order)) + _orderProcessingService.RefundOffline(order); + break; + case PaymentStatus.PartiallyRefunded: + if (_orderProcessingService.CanPartiallyRefundOffline(order, Math.Abs(total))) + _orderProcessingService.PartiallyRefundOffline(order, Math.Abs(total)); + break; + case PaymentStatus.Voided: + if (_orderProcessingService.CanVoidOffline(order)) + _orderProcessingService.VoidOffline(order); + break; + } + + AddOrderNote(settings, order, (string)ToInfoString(json), true); + + return HttpStatusCode.OK; + } + } + + + public class PayPalResponse + { + public bool Success { get; set; } + public dynamic Json { get; set; } + public string ErrorMessage { get; set; } + public string Id { get; set; } + } + + public class PayPalSessionData + { + public PayPalSessionData() + { + OrderGuid = Guid.NewGuid(); + } + + public string AccessToken { get; set; } + public DateTime TokenExpiration { get; set; } + public string PaymentId { get; set; } + public string PayerId { get; set; } + public string ApprovalUrl { get; set; } + public Guid OrderGuid { get; private set; } + public PayPalPaymentInstruction PaymentInstruction { get; set; } + } + + public class PayPalPaymentInstruction + { + public string ReferenceNumber { get; set; } + public string Type { get; set; } + public decimal Amount { get; set; } + public string AmountCurrencyCode { get; set; } + public DateTime? DueDate { get; set; } + public string Note { get; set; } + public string Link { get; set; } + + public RecipientBankingInstruction RecipientBanking { get; set; } + + public bool IsManualBankTransfer + { + get { return Type.IsCaseInsensitiveEqual("MANUAL_BANK_TRANSFER"); } + } + + public bool IsPayUponInvoice + { + get { return Type.IsCaseInsensitiveEqual("PAY_UPON_INVOICE"); } + } + + public class RecipientBankingInstruction + { + public string BankName { get; set; } + public string AccountHolderName { get; set; } + public string AccountNumber { get; set; } + public string RoutingNumber { get; set; } + public string Iban { get; set; } + public string Bic { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Services/PayPalStandardCore.cs b/src/Plugins/SmartStore.PayPal/Services/PayPalStandardCore.cs deleted file mode 100644 index eb7d70877d..0000000000 --- a/src/Plugins/SmartStore.PayPal/Services/PayPalStandardCore.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Globalization; - -namespace SmartStore.PayPal.Services -{ - public class PayPalLineItem : ICloneable - { - public PayPalItemType Type { get; set; } - public string Name { get; set; } - public int Quantity { get; set; } - public decimal Amount { get; set; } - - public decimal AmountRounded - { - get - { - return Math.Round(Amount, 2); - } - } - - public PayPalLineItem Clone() - { - var item = new PayPalLineItem() - { - Type = this.Type, - Name = this.Name, - Quantity = this.Quantity, - Amount = this.Amount - }; - return item; - } - - object ICloneable.Clone() - { - return this.Clone(); - } - } - - - public enum PayPalItemType : int - { - CartItem = 0, - CheckoutAttribute, - Shipping, - PaymentFee, - Tax - } -} diff --git a/src/Plugins/SmartStore.PayPal/Settings/PayPalSettings.cs b/src/Plugins/SmartStore.PayPal/Settings/PayPalSettings.cs index 9e3adea8da..8109422c68 100644 --- a/src/Plugins/SmartStore.PayPal/Settings/PayPalSettings.cs +++ b/src/Plugins/SmartStore.PayPal/Settings/PayPalSettings.cs @@ -1,20 +1,37 @@ +using System.Collections.Generic; +using System.Net; using SmartStore.Core.Configuration; -using SmartStore.PayPal; +using SmartStore.PayPal.Services; namespace SmartStore.PayPal.Settings { public abstract class PayPalSettingsBase { - public bool UseSandbox { get; set; } - /// - /// Gets or sets a value indicating whether to "additional fee" is specified as percentage. true - percentage, false - fixed value. - /// - public bool AdditionalFeePercentage { get; set; } - /// - /// Additional fee - /// + public PayPalSettingsBase() + { + SecurityProtocol = SecurityProtocolType.Tls12; + IpnChangesPaymentStatus = true; + AddOrderNotes = true; + } + + public SecurityProtocolType? SecurityProtocol { get; set; } + + public bool UseSandbox { get; set; } + + public bool AddOrderNotes { get; set; } + + /// + /// Gets or sets a value indicating whether to "additional fee" is specified as percentage. true - percentage, false - fixed value. + /// + public bool AdditionalFeePercentage { get; set; } + public decimal AdditionalFee { get; set; } - } + + /// + /// Gets or sets a value indicating whether an IPN should change the payment status + /// + public bool IpnChangesPaymentStatus { get; set; } + } public abstract class PayPalApiSettingsBase : PayPalSettingsBase { @@ -22,14 +39,35 @@ public abstract class PayPalApiSettingsBase : PayPalSettingsBase public string ApiAccountName { get; set; } public string ApiAccountPassword { get; set; } public string Signature { get; set; } + + /// + /// PayPal client id + /// + public string ClientId { get; set; } + + /// + /// PayPal secret + /// + public string Secret { get; set; } + + /// + /// PayPal experience profile id + /// + public string ExperienceProfileId { get; set; } + + /// + /// PayPal webhook id + /// + public string WebhookId { get; set; } } + public class PayPalDirectPaymentSettings : PayPalApiSettingsBase, ISettings { public PayPalDirectPaymentSettings() { + UseSandbox = true; TransactMode = TransactMode.Authorize; - UseSandbox = true; } } @@ -44,12 +82,17 @@ public PayPalExpressPaymentSettings() /// /// Determines whether the checkout button is displayed beneath the cart /// - public bool DisplayCheckoutButton { get; set; } + //public bool DisplayCheckoutButton { get; set; } - /// - /// Determines whether the shipment address has to be confirmed by PayPal - /// - public bool ConfirmedShipment { get; set; } + /// + /// Specifies whether to display the checkout button in mini shopping cart + /// + public bool ShowButtonInMiniShoppingCart { get; set; } + + /// + /// Determines whether the shipment address has to be confirmed by PayPal + /// + public bool ConfirmedShipment { get; set; } /// /// Determines whether the shipment address is transmitted to PayPal @@ -67,12 +110,34 @@ public PayPalExpressPaymentSettings() public decimal DefaultShippingPrice { get; set; } } - public class PayPalStandardPaymentSettings : PayPalSettingsBase, ISettings + public class PayPalPlusPaymentSettings : PayPalApiSettingsBase, ISettings + { + public PayPalPlusPaymentSettings() + { + UseSandbox = true; + } + + /// + /// Specifies other payment methods to be offered in payment wall + /// + public List ThirdPartyPaymentMethods { get; set; } + + /// + /// Specifies whether to display the logo of a third party payment method + /// + public bool DisplayPaymentMethodLogo { get; set; } + + /// + /// Specifies whether to display the description of a third party payment method + /// + public bool DisplayPaymentMethodDescription { get; set; } + } + + public class PayPalStandardPaymentSettings : PayPalSettingsBase, ISettings { public PayPalStandardPaymentSettings() { UseSandbox = true; - PdtValidateOrderTotal = true; EnableIpn = true; } @@ -80,22 +145,18 @@ public PayPalStandardPaymentSettings() public string PdtToken { get; set; } public bool PassProductNamesAndTotals { get; set; } public bool PdtValidateOrderTotal { get; set; } + public bool PdtValidateOnlyWarn { get; set; } public bool EnableIpn { get; set; } public string IpnUrl { get; set; } } - /// - /// Represents payment processor transaction mode - /// - public enum TransactMode : int + + /// + /// Represents payment processor transaction mode + /// + public enum TransactMode { - /// - /// Authorize - /// Authorize = 1, - /// - /// Authorize and capture - /// AuthorizeAndCapture = 2 } } diff --git a/src/Plugins/SmartStore.PayPal/SmartStore.PayPal.csproj b/src/Plugins/SmartStore.PayPal/SmartStore.PayPal.csproj index 763d76c4f8..e82c4a4097 100644 --- a/src/Plugins/SmartStore.PayPal/SmartStore.PayPal.csproj +++ b/src/Plugins/SmartStore.PayPal/SmartStore.PayPal.csproj @@ -34,6 +34,7 @@ + true @@ -55,18 +56,22 @@ false - - ..\..\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\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.8.0.3\lib\net45\Newtonsoft.Json.dll + False + @@ -113,23 +118,33 @@ Properties\AssemblyVersionInfo.cs + + + + + + + + + + - - + + + - @@ -138,6 +153,7 @@ Settings.settings + @@ -169,6 +185,9 @@ + + PreserveNewest + PreserveNewest @@ -180,9 +199,6 @@ PreserveNewest - - Always - Always @@ -265,6 +281,21 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + Designer diff --git a/src/Plugins/SmartStore.PayPal/Validators/PayPalPlusConfigValidator.cs b/src/Plugins/SmartStore.PayPal/Validators/PayPalPlusConfigValidator.cs new file mode 100644 index 0000000000..b7355b4299 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Validators/PayPalPlusConfigValidator.cs @@ -0,0 +1,33 @@ +using System; +using FluentValidation; +using SmartStore.PayPal.Models; +using SmartStore.Services.Localization; +using SmartStore.Web.Framework.Validators; + +namespace SmartStore.PayPal.Validators +{ + public class PayPalPlusConfigValidator : SmartValidatorBase + { + public PayPalPlusConfigValidator(ILocalizationService localize, Func addRule) + { + if (addRule("ClientId")) + { + RuleFor(x => x.ClientId).NotEmpty() + .WithMessage(localize.GetResource("Plugins.SmartStore.PayPal.ValidateClientIdAndSecret")); + } + + if (addRule("Secret")) + { + RuleFor(x => x.Secret).NotEmpty() + .WithMessage(localize.GetResource("Plugins.SmartStore.PayPal.ValidateClientIdAndSecret")); + } + + if (addRule("ThirdPartyPaymentMethods")) + { + RuleFor(x => x.ThirdPartyPaymentMethods) + .Must(x => x == null || x.Count <= 5) + .WithMessage(localize.GetResource("Plugins.Payments.PayPalPlus.ValidateThirdPartyPaymentMethods")); + } + } + } +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalDirect/Configure.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalDirect/Configure.cshtml index febf02c057..128ae981b6 100644 --- a/src/Plugins/SmartStore.PayPal/Views/PayPalDirect/Configure.cshtml +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalDirect/Configure.cshtml @@ -6,21 +6,19 @@ Html.AddCssFileParts(true, "~/Plugins/SmartStore.PayPal/Content/smartstore.paypal.css"); } - - - - - -
      -
      - - @Html.Raw(T("Plugins.Payments.PayPalDirect.AdminInstruction")) -
      -
      - - - -
      +
      +
      +
      + + @Html.Raw(T("Plugins.Payments.PayPalDirect.AdminInstruction")) +
      +
      +
      + + PayPal + +
      +
      @Html.Action("StoreScopeConfiguration", "Setting", new { area = "Admin" }) @@ -36,6 +34,14 @@ @Html.DropDownListFor(model => model.TransactMode, Model.TransactModeValues)
    + @Html.SmartLabelFor(model => model.SecurityProtocol) + + @Html.DropDownListFor(model => model.SecurityProtocol, Model.AvailableSecurityProtocols, T("Common.Unspecified")) +
    @Html.SmartLabelFor(model => model.UseSandbox) @@ -73,6 +79,15 @@ @Html.ValidationMessageFor(model => model.Signature)
    + @Html.SmartLabelFor(model => model.IpnChangesPaymentStatus) + + @Html.SettingEditorFor(model => model.IpnChangesPaymentStatus) + @Html.ValidationMessageFor(model => model.IpnChangesPaymentStatus) +
    @Html.SmartLabelFor(model => model.AdditionalFee) diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalDirect/PaymentInfo.Mobile.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalDirect/PaymentInfo.Mobile.cshtml index 66dd7a45af..aeb9300a95 100644 --- a/src/Plugins/SmartStore.PayPal/Views/PayPalDirect/PaymentInfo.Mobile.cshtml +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalDirect/PaymentInfo.Mobile.cshtml @@ -6,7 +6,7 @@ @using SmartStore.Web.Framework; @Html.SmartLabelFor(model => model.CreditCardTypes, false) -@Html.DropDownListFor(model => model.CreditCardType, Model.CreditCardTypes) +@Html.DropDownListFor(model => model.CreditCardType, Model.CreditCardTypes, new { data_native_menu = "false" }) @Html.SmartLabelFor(model => model.CardholderName, false) @Html.TextBoxFor(model => model.CardholderName, new { autocomplete = "off" }) @Html.ValidationMessageFor(model => model.CardholderName) @@ -16,8 +16,8 @@ @Html.SmartLabelFor(model => model.ExpireMonth, false)
    - @Html.DropDownListFor(model => model.ExpireMonth, Model.ExpireMonths) - @Html.DropDownListFor(model => model.ExpireYear, Model.ExpireYears) + @Html.DropDownListFor(model => model.ExpireMonth, Model.ExpireMonths, new { data_native_menu = "false" }) + @Html.DropDownListFor(model => model.ExpireYear, Model.ExpireYears, new { data_native_menu = "false" })
    @Html.SmartLabelFor(model => model.CardCode, false) diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalExpress/Configure.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalExpress/Configure.cshtml index 3ab559894a..5738f2df4a 100644 --- a/src/Plugins/SmartStore.PayPal/Views/PayPalExpress/Configure.cshtml +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalExpress/Configure.cshtml @@ -7,21 +7,19 @@ Html.AddCssFileParts(true, "~/Plugins/SmartStore.PayPal/Content/smartstore.paypal.css"); } - - - - - -
    -
    - - @Html.Raw(T("Plugins.Payments.PayPalExpress.AdminInstruction")) -
    -
    - - - -
    +
    +
    +
    + + @Html.Raw(T("Plugins.Payments.PayPalExpress.AdminInstruction")) +
    +
    +
    + + PayPal + +
    +
    @Html.Action("StoreScopeConfiguration", "Setting", new { area = "Admin" }) @@ -37,6 +35,14 @@ @Html.DropDownListFor(model => model.TransactMode, Model.TransactModeValues)
    + @Html.SmartLabelFor(model => model.SecurityProtocol) + + @Html.DropDownListFor(model => model.SecurityProtocol, Model.AvailableSecurityProtocols, T("Common.Unspecified")) +
    @Html.SmartLabelFor(model => model.UseSandbox) @@ -74,43 +80,33 @@ @Html.ValidationMessageFor(model => model.Signature)
    - @Html.SmartLabelFor(model => model.AdditionalFee) - - @Html.SettingEditorFor(model => model.AdditionalFee) - @Html.ValidationMessageFor(model => model.AdditionalFee) -
    - @Html.SmartLabelFor(model => model.AdditionalFeePercentage) - - @Html.SettingEditorFor(model => model.AdditionalFeePercentage) - @Html.ValidationMessageFor(model => model.AdditionalFeePercentage) -
    - @Html.SmartLabelFor(model => model.DisplayCheckoutButton) - - @Html.SettingEditorFor(model => model.DisplayCheckoutButton) - @Html.ValidationMessageFor(model => model.DisplayCheckoutButton) -
    - @Html.SmartLabelFor(model => model.ConfirmedShipment) - - @Html.SettingEditorFor(model => model.ConfirmedShipment) - @Html.ValidationMessageFor(model => model.ConfirmedShipment) -
    + @Html.SmartLabelFor(model => model.CallbackTimeout) + + @Html.SettingEditorFor(model => model.CallbackTimeout) + @Html.ValidationMessageFor(model => model.CallbackTimeout) +
    + @Html.SmartLabelFor(model => model.IpnChangesPaymentStatus) + + @Html.SettingEditorFor(model => model.IpnChangesPaymentStatus) + @Html.ValidationMessageFor(model => model.IpnChangesPaymentStatus) +
    + @Html.SmartLabelFor(model => model.ShowButtonInMiniShoppingCart) + + @Html.SettingEditorFor(model => model.ShowButtonInMiniShoppingCart) + @Html.ValidationMessageFor(model => model.ShowButtonInMiniShoppingCart) +
    @Html.SmartLabelFor(model => model.NoShipmentAddress) @@ -120,15 +116,15 @@ @Html.ValidationMessageFor(model => model.NoShipmentAddress)
    - @Html.SmartLabelFor(model => model.CallbackTimeout) - - @Html.SettingEditorFor(model => model.CallbackTimeout) - @Html.ValidationMessageFor(model => model.CallbackTimeout) -
    + @Html.SmartLabelFor(model => model.ConfirmedShipment) + + @Html.SettingEditorFor(model => model.ConfirmedShipment) + @Html.ValidationMessageFor(model => model.ConfirmedShipment) +
    @Html.SmartLabelFor(model => model.DefaultShippingPrice) @@ -138,9 +134,29 @@ @Html.ValidationMessageFor(model => model.DefaultShippingPrice)
    + @Html.SmartLabelFor(model => model.AdditionalFee) + + @Html.SettingEditorFor(model => model.AdditionalFee) + @Html.ValidationMessageFor(model => model.AdditionalFee) +
    + @Html.SmartLabelFor(model => model.AdditionalFeePercentage) + + @Html.SettingEditorFor(model => model.AdditionalFeePercentage) + @Html.ValidationMessageFor(model => model.AdditionalFeePercentage) +
      + +   + diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalExpress/MiniShoppingCart.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalExpress/MiniShoppingCart.cshtml new file mode 100644 index 0000000000..3296cbd446 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalExpress/MiniShoppingCart.cshtml @@ -0,0 +1,10 @@ +@model SmartStore.PayPal.Models.PayPalExpressPaymentInfoModel +
    +
    + @T("Plugins.Payments.PayPalExpress.SelectionText") +
    +
    + + +
    +
    \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/Configure.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/Configure.cshtml new file mode 100644 index 0000000000..129922ca27 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/Configure.cshtml @@ -0,0 +1,207 @@ +@using SmartStore.PayPal; +@using SmartStore.PayPal.Models; +@using SmartStore.Web.Framework; +@using SmartStore.Web.Framework.UI; +@model PayPalPlusConfigurationModel +@{ + Layout = ""; + Html.AddCssFileParts(true, "~/Plugins/SmartStore.PayPal/Content/smartstore.paypal.css"); + + var hasCredentials = (Model.ClientId.HasValue() && Model.Secret.HasValue()); +} + +
    +
    +
    + + @Html.Raw(T("Plugins.Payments.PayPalPlus.AdminInstruction")) +
    +
    +
    + + PayPal + +
    +
    + +@Html.Action("StoreScopeConfiguration", "Setting", new { area = "Admin" }) + +@Html.ValidationSummary(false) + +@using (Html.BeginForm()) +{ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    + @Model.ConfigGroups.SafeGet(0) +
    +
    +
    + @Html.SmartLabelFor(model => model.UseSandbox) + + @Html.EditorFor(model => model.UseSandbox) + @Html.ValidationMessageFor(model => model.UseSandbox) +
    + @Html.SmartLabelFor(model => model.ClientId) + + @Html.SettingOverrideCheckbox(model => model.ClientId) + @Html.TextBoxFor(model => model.ClientId, new { @class = "control-xlarge" }) + @Html.ValidationMessageFor(model => model.ClientId) +
    + @Html.SmartLabelFor(model => model.Secret) + + @Html.SettingOverrideCheckbox(model => model.Secret) + @Html.TextBoxFor(model => model.Secret, new { @class = "control-xlarge" }) + @Html.ValidationMessageFor(model => model.Secret) +
    +
    +
    + @Model.ConfigGroups.SafeGet(1) +
    +
    +
    + @Html.SmartLabelFor(model => model.SecurityProtocol) + + @Html.DropDownListFor(model => model.SecurityProtocol, Model.AvailableSecurityProtocols, T("Common.Unspecified")) +
    + @Html.SmartLabelFor(model => model.ExperienceProfileId) + + @Html.SettingEditorFor(model => model.ExperienceProfileId) + + +   + @T(Model.ExperienceProfileId.HasValue() ? "Common.Refresh" : "Common.AddNew") + + + @if (Model.ExperienceProfileId.HasValue()) + { + +  @T("Admin.Common.Delete") + + } + + @Html.ValidationMessageFor(model => model.ExperienceProfileId) +
    + @Html.SmartLabelFor(model => model.WebhookId) + + @* IPNs and webhook messages have no store context, so multistore configuration not possible here *@ + @Html.EditorFor(model => model.WebhookId) + + @if (Model.WebhookId.HasValue()) + { + +  @T("Admin.Common.Delete") + + } + else + { + +  @T("Common.AddNew") + + } + + @Html.ValidationMessageFor(model => model.WebhookId) +
    +
    +
    + @Model.ConfigGroups.SafeGet(2) +
    +
    +
    + @Html.SmartLabelFor(model => model.ThirdPartyPaymentMethods) + + @Html.SettingOverrideCheckbox(model => model.ThirdPartyPaymentMethods) + @Html.ListBoxFor(x => x.ThirdPartyPaymentMethods, + new MultiSelectList(Model.AvailableThirdPartyPaymentMethods, "Value", "Text"), + new { multiple = "multiple", @class = "control-xlarge" }) + @Html.ValidationMessageFor(model => model.ThirdPartyPaymentMethods) +
    + @Html.SmartLabelFor(model => model.DisplayPaymentMethodLogo) + + @Html.SettingEditorFor(model => model.DisplayPaymentMethodLogo) + @Html.ValidationMessageFor(model => model.DisplayPaymentMethodLogo) +
    + @Html.SmartLabelFor(model => model.DisplayPaymentMethodDescription) + + @Html.SettingEditorFor(model => model.DisplayPaymentMethodDescription) + @Html.ValidationMessageFor(model => model.DisplayPaymentMethodDescription) +
    + @Html.SmartLabelFor(model => model.AdditionalFee) + + @Html.SettingEditorFor(model => model.AdditionalFee) + @Html.ValidationMessageFor(model => model.AdditionalFee) +
    + @Html.SmartLabelFor(model => model.AdditionalFeePercentage) + + @Html.SettingEditorFor(model => model.AdditionalFeePercentage) + @Html.ValidationMessageFor(model => model.AdditionalFeePercentage) +
    +   + + +
    +} \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWall.Mobile.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWall.Mobile.cshtml new file mode 100644 index 0000000000..266ae25887 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWall.Mobile.cshtml @@ -0,0 +1,66 @@ +@using SmartStore.Web.Models.Checkout; +@using SmartStore.PayPal.Models; +@model PayPalPlusCheckoutModel +@{ + Layout = "~/Views/Shared/_Root.cshtml"; + Html.AddTitleParts(T("PageTitle.Checkout").Text); +} + +@Html.Partial("PaymentWallScripting") + +
    +
    +

    @T("Checkout.SelectPaymentMethod")

    +
    +
    + @Html.Widget("mobile_checkout_payment_method_top") + + @if (Model.ErrorMessage.HasValue()) + { +
    +
      +
    • @Html.Raw(T("Plugins.Payments.PayPalPlus.MethodUnavailable"))
    • +
    • + @if (!Model.ApprovalUrl.HasValue()) + { + @T("Plugins.SmartStore.PayPal.NoApprovalUrlReturned") + } + @Model.ErrorMessage +
    • +
    +
    + } + else if (Model.PayPalPlusPseudoMessageFlag.HasValue() && Model.PayPalPlusPseudoMessageFlag == "1") + { +
    +
      +
    • @Html.Raw(T("Plugins.Payments.PayPalPlus.SorryFailure"))
    • +
    +
    + } + + @if (Model.FullDescription.HasValue()) + { +
    + @Html.Raw(Model.FullDescription) +
    + } + + @if (Model.HasAnyFees) + { + + } + +
    + +
    + +
    + +
    + + @Html.Widget("mobile_checkout_payment_method_bottom") +
    +
    \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWall.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWall.cshtml new file mode 100644 index 0000000000..461b02a3ab --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWall.cshtml @@ -0,0 +1,69 @@ +@using SmartStore.Web.Models.Checkout; +@using SmartStore.PayPal.Models; +@model PayPalPlusCheckoutModel +@{ + Layout = "~/Views/Shared/_Checkout.cshtml"; + Html.AddTitleParts(T("PageTitle.Checkout").Text); +} +@section orderProgress +{ + @Html.Action("CheckoutProgress", "Checkout", new { step = CheckoutProgressStep.Payment }) +} + +@Html.Partial("PaymentWallScripting") + +
    +
    +

    @T("Checkout.SelectPaymentMethod")

    +
    +
    + @Html.Widget("checkout_payment_method_top") + + @if (Model.ErrorMessage.HasValue()) + { +
    + @Html.Raw(T("Plugins.Payments.PayPalPlus.MethodUnavailable")) +
    + @if (!Model.ApprovalUrl.HasValue()) + { + @T("Plugins.SmartStore.PayPal.NoApprovalUrlReturned") + } + @Model.ErrorMessage +
    + } + else if (Model.PayPalPlusPseudoMessageFlag.HasValue() && Model.PayPalPlusPseudoMessageFlag == "1") + { +
    + @Html.Raw(T("Plugins.Payments.PayPalPlus.SorryFailure")) +
    + } + + @if (Model.FullDescription.HasValue()) + { +
    + @Html.Raw(Model.FullDescription) +
    + } + + @if (Model.HasAnyFees) + { + + } + +
    + +
    + +  @T("Common.Back") + + + +
    + + @Html.Widget("checkout_payment_method_bottom") +
    +
    \ No newline at end of file diff --git a/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWallScripting.cshtml b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWallScripting.cshtml new file mode 100644 index 0000000000..c1f2cdf6f0 --- /dev/null +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalPlus/PaymentWallScripting.cshtml @@ -0,0 +1,117 @@ +@using SmartStore.PayPal +@using SmartStore.PayPal.Models +@model PayPalPlusCheckoutModel + + \ 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 561f35a72d..5846c1ed43 100644 --- a/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/Configure.cshtml +++ b/src/Plugins/SmartStore.PayPal/Views/PayPalStandard/Configure.cshtml @@ -5,27 +5,33 @@ Layout = ""; } - - - - - -
    -
    - - @Html.Raw(T("Plugins.Payments.PayPalStandard.AdminInstruction")) -
    -
    - - - -
    +
    +
    +
    + + @Html.Raw(T("Plugins.Payments.PayPalStandard.AdminInstruction")) +
    +
    +
    + + PayPal + +
    +
    @Html.Action("StoreScopeConfiguration", "Setting", new { area = "Admin" }) @using (Html.BeginForm()) { + + + + + + + + - + + + + + @@ -108,7 +132,7 @@ @Html.ValidationMessageFor(model => model.IpnUrl) - + + +
    + @Html.SmartLabelFor(model => model.SecurityProtocol) + + @Html.DropDownListFor(model => model.SecurityProtocol, Model.AvailableSecurityProtocols, T("Common.Unspecified")) +
    @Html.SmartLabelFor(model => model.UseSandbox) @@ -63,6 +69,15 @@ @Html.ValidationMessageFor(model => model.PdtValidateOrderTotal)
    + @Html.SmartLabelFor(model => model.PdtValidateOnlyWarn) + + @Html.SettingEditorFor(model => model.PdtValidateOnlyWarn) + @Html.ValidationMessageFor(model => model.PdtValidateOnlyWarn) +
    @Html.SmartLabelFor(model => model.AdditionalFee) @@ -99,7 +114,16 @@ @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)
    @T("Plugins.Payments.PayPalStandard.Fields.EnableIpn.Hint2") @@ -132,7 +156,11 @@ $(document).ready(function () { $("#@Html.FieldIdFor(model => model.EnableIpn)").change(function () { - $('.ipn-url').toggle($(this).is(':checked')); + $('.ipn-handling').toggle($(this).is(':checked')); + }).trigger('change'); + + $("#@Html.FieldIdFor(model => model.PdtValidateOrderTotal)").change(function () { + $('#PdtValidateOnlyWarnContainer').toggle($(this).is(':checked')); }).trigger('change'); }); 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 cf618f7d59..e074479a1d 100644 --- a/src/Plugins/SmartStore.PayPal/changelog.md +++ b/src/Plugins/SmartStore.PayPal/changelog.md @@ -1,13 +1,56 @@ -#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 +* Option to add order note when order total validation fails + +##PayPal 2.2.0.2 +###Improvements +* 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## -###Bugfixes### +##Paypal 1.22 +###Bugfixes * PayPal Standard provider now using shipping rather than billing address if shipping is required -##Paypal 1.21## -###Improvements### +##Paypal 1.21 +###Improvements * Multistore configuration \ No newline at end of file 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 c751879157..5c5e03f8ab 100644 --- a/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs +++ b/src/Plugins/SmartStore.Shipping/Controllers/ByTotalController.cs @@ -1,14 +1,13 @@ using System.Web.Mvc; using SmartStore.Core.Domain.Common; -using SmartStore.Core.Domain.Directory; +using SmartStore.Services; +using SmartStore.Services.Directory; +using SmartStore.Services.Shipping; using SmartStore.Shipping.Domain; using SmartStore.Shipping.Models; using SmartStore.Shipping.Services; -using SmartStore.Services.Configuration; -using SmartStore.Services.Directory; -using SmartStore.Services.Shipping; -using SmartStore.Services.Stores; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Security; using Telerik.Web.Mvc; namespace SmartStore.Shipping.Controllers @@ -17,34 +16,26 @@ namespace SmartStore.Shipping.Controllers public class ByTotalController : PluginControllerBase { private readonly IShippingService _shippingService; - private readonly IStoreService _storeService; - private readonly ISettingService _settingService; private readonly IShippingByTotalService _shippingByTotalService; private readonly ShippingByTotalSettings _shippingByTotalSettings; private readonly ICountryService _countryService; - private readonly ICurrencyService _currencyService; - private readonly CurrencySettings _currencySettings; private readonly AdminAreaSettings _adminAreaSettings; + private readonly ICommonServices _services; - public ByTotalController(IShippingService shippingService, - IStoreService storeService, - ISettingService settingService, + public ByTotalController( + IShippingService shippingService, IShippingByTotalService shippingByTotalService, ShippingByTotalSettings shippingByTotalSettings, ICountryService countryService, - ICurrencyService currencyService, - CurrencySettings currencySettings, - AdminAreaSettings adminAreaSettings) + AdminAreaSettings adminAreaSettings, + ICommonServices services) { this._shippingService = shippingService; - this._storeService = storeService; - this._settingService = settingService; this._shippingByTotalService = shippingByTotalService; this._shippingByTotalSettings = shippingByTotalSettings; this._countryService = countryService; - this._currencyService = currencyService; - this._currencySettings = currencySettings; this._adminAreaSettings = adminAreaSettings; + this._services = services; } public ActionResult Configure() @@ -52,33 +43,35 @@ 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(); + var allStores = _services.StoreService.GetAllStores(); + foreach (var sm in shippingMethods) { - model.AvailableShippingMethods.Add(new SelectListItem() { Text = sm.Name, Value = sm.Id.ToString() }); + model.AvailableShippingMethods.Add(new SelectListItem { Text = sm.Name, Value = sm.Id.ToString() }); } //stores - model.AvailableStores.Add(new SelectListItem() { Text = "*", Value = "0" }); - foreach (var store in _storeService.GetAllStores()) + model.AvailableStores.Add(new SelectListItem { Text = "*", Value = "0" }); + foreach (var store in allStores) { - model.AvailableStores.Add(new SelectListItem() { Text = store.Name, Value = store.Id.ToString() }); + model.AvailableStores.Add(new SelectListItem { Text = store.Name, Value = store.Id.ToString() }); } - //model.AvailableCountries.Add(new SelectListItem() { Text = "*", Value = "0" }); + //model.AvailableCountries.Add(new SelectListItem { Text = "*", Value = "0" }); var countries = _countryService.GetAllCountries(true); foreach (var c in countries) { - model.AvailableCountries.Add(new SelectListItem() { Text = c.Name, Value = c.Id.ToString() }); + model.AvailableCountries.Add(new SelectListItem { Text = c.Name, Value = c.Id.ToString() }); } model.LimitMethodsToCreated = _shippingByTotalSettings.LimitMethodsToCreated; model.SmallQuantityThreshold = _shippingByTotalSettings.SmallQuantityThreshold; model.SmallQuantitySurcharge = _shippingByTotalSettings.SmallQuantitySurcharge; - model.PrimaryStoreCurrencyCode = _currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId).CurrencyCode; + model.PrimaryStoreCurrencyCode = _services.StoreContext.CurrentStore.PrimaryStoreCurrency.CurrencyCode; model.GridPageSize = _adminAreaSettings.GridPageSize; return View(model); @@ -107,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; @@ -119,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); @@ -166,7 +161,7 @@ public ActionResult SaveGeneralSettings(ByTotalListModel model) _shippingByTotalSettings.SmallQuantityThreshold = model.SmallQuantityThreshold; _shippingByTotalSettings.SmallQuantitySurcharge = model.SmallQuantitySurcharge; - _settingService.SaveSetting(_shippingByTotalSettings); + _services.Settings.SaveSetting(_shippingByTotalSettings); return Json(new { Result = true }); } 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 e4b70d8120..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 -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/Localization/resources.de-de.xml b/src/Plugins/SmartStore.Shipping/Localization/resources.de-de.xml index 66322e9293..259ba4ecd4 100644 --- a/src/Plugins/SmartStore.Shipping/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.Shipping/Localization/resources.de-de.xml @@ -35,7 +35,7 @@ PLZ-(Bereich) - PLZ-(Bereich) des Kunden, entweder als spezifischer Wert oder als Muster (z.B. 4000-49999 für das PLZ-Gebiet 4). In einem Muster lassen sich auch Wildcards verwenden, wie Stern (*) oder Fragezeichen (?). Lassen Sie das Feld leer, wenn die Gebühr unabhängig von PLZ für alle Kunden im definierten (Bundes)land gelten soll. + PLZ-(Bereich) des Kunden, entweder als spezifische Werte (getrennt durch Komma) oder als Muster (z.B. 4000-49999 für das PLZ-Gebiet 4). In einem Muster lassen sich auch Wildcards verwenden, wie Stern (*) oder Fragezeichen (?). Sie können auch mehrere Wildcards angeben (durch Komma getrennt). Lassen Sie das Feld leer, wenn die Gebühr unabhängig von PLZ für alle Kunden im definierten (Bundes)land gelten soll. Versandart diff --git a/src/Plugins/SmartStore.Shipping/Localization/resources.en-us.xml b/src/Plugins/SmartStore.Shipping/Localization/resources.en-us.xml index c44b4c48fd..e229ab4d46 100644 --- a/src/Plugins/SmartStore.Shipping/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.Shipping/Localization/resources.en-us.xml @@ -36,7 +36,7 @@ Zip-(Range) - Zip/postal code (range), either as specific value or range pattern (e.g. 4000-4999). You can also define wildcard chars like * or ?. If zip is empty, then this shipping rate will apply to all customers from the given country or state / province, regardless of the zip code. + Zip/postal code (range), either as specific values (comma devided) or range pattern (e.g. 4000-4999). You can also define wildcard chars like * or ?. If zip is empty, then this shipping rate will apply to all customers from the given country or state / province, regardless of the zip code. You can also enter multiple ranges (comma devided) Shipping method @@ -60,7 +60,7 @@ Use percentage - Check to use 'charge percentage' value. + Check the box to use 'charge percentage' value. Charge % 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 a4eb6d3def..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; } @@ -187,7 +190,7 @@ public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest get decimal sqThreshold = _shippingByTotalSettings.SmallQuantityThreshold; decimal sqSurcharge = _shippingByTotalSettings.SmallQuantitySurcharge; - var shippingMethods = _shippingService.GetAllShippingMethods(countryId); + var shippingMethods = _shippingService.GetAllShippingMethods(getShippingOptionRequest); foreach (var shippingMethod in shippingMethods) { decimal? rate = GetRate(subTotal, shippingMethod.Id, storeId, countryId, stateProvinceId, zip); @@ -200,6 +203,7 @@ public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest get } var shippingOption = new ShippingOption(); + shippingOption.ShippingMethodId = shippingMethod.Id; shippingOption.Name = shippingMethod.Name; shippingOption.Description = shippingMethod.Description; shippingOption.Rate = rate.Value; diff --git a/src/Plugins/SmartStore.Shipping/Providers/FixedRateProvider.cs b/src/Plugins/SmartStore.Shipping/Providers/FixedRateProvider.cs index fc73a0c460..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,15 +54,15 @@ 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; } - int? restrictByCountryId = (getShippingOptionRequest.ShippingAddress != null && getShippingOptionRequest.ShippingAddress.Country != null) ? (int?)getShippingOptionRequest.ShippingAddress.Country.Id : null; - var shippingMethods = this._shippingService.GetAllShippingMethods(restrictByCountryId); + var shippingMethods = this._shippingService.GetAllShippingMethods(getShippingOptionRequest); foreach (var shippingMethod in shippingMethods) { var shippingOption = new ShippingOption(); + shippingOption.ShippingMethodId = shippingMethod.Id; shippingOption.Name = shippingMethod.GetLocalized(x => x.Name); shippingOption.Description = shippingMethod.GetLocalized(x => x.Description); shippingOption.Rate = GetRate(shippingMethod.Id); @@ -79,8 +82,7 @@ public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest get if (getShippingOptionRequest == null) throw new ArgumentNullException("getShippingOptionRequest"); - int? restrictByCountryId = (getShippingOptionRequest.ShippingAddress != null && getShippingOptionRequest.ShippingAddress.Country != null) ? (int?)getShippingOptionRequest.ShippingAddress.Country.Id : null; - var shippingMethods = this._shippingService.GetAllShippingMethods(restrictByCountryId); + var shippingMethods = this._shippingService.GetAllShippingMethods(getShippingOptionRequest); var rates = new List(); foreach (var shippingMethod in shippingMethods) 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/Services/ShippingByTotalService.cs b/src/Plugins/SmartStore.Shipping/Services/ShippingByTotalService.cs index b20c154be6..a24143413c 100644 --- a/src/Plugins/SmartStore.Shipping/Services/ShippingByTotalService.cs +++ b/src/Plugins/SmartStore.Shipping/Services/ShippingByTotalService.cs @@ -212,16 +212,26 @@ private bool ZipMatches(string zip, string pattern) { return true; // catch all } - + + var patterns = pattern.Contains(",") + ? pattern.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()) + : new string[] { pattern }; + try { - var wildcard = new Wildcard(pattern); - return wildcard.IsMatch(zip); + foreach (var entry in patterns) + { + var wildcard = new Wildcard(entry); + if (wildcard.IsMatch(zip)) + return true; + } } catch { return zip.IsCaseInsensitiveEqual(pattern); } + + return false; } /// diff --git a/src/Plugins/SmartStore.Shipping/SmartStore.Shipping.csproj b/src/Plugins/SmartStore.Shipping/SmartStore.Shipping.csproj index 59f1bdcdf0..3a9b8863dd 100644 --- a/src/Plugins/SmartStore.Shipping/SmartStore.Shipping.csproj +++ b/src/Plugins/SmartStore.Shipping/SmartStore.Shipping.csproj @@ -42,6 +42,7 @@ + true @@ -81,19 +82,19 @@ 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 - ..\..\packages\EntityFramework.6.1.0\lib\net45\EntityFramework.dll + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll False - ..\..\packages\EntityFramework.6.1.0\lib\net45\EntityFramework.SqlServer.dll + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll diff --git a/src/Plugins/SmartStore.Shipping/Views/ByTotal/Configure.cshtml b/src/Plugins/SmartStore.Shipping/Views/ByTotal/Configure.cshtml index 779738d741..ae05d59b64 100644 --- a/src/Plugins/SmartStore.Shipping/Views/ByTotal/Configure.cshtml +++ b/src/Plugins/SmartStore.Shipping/Views/ByTotal/Configure.cshtml @@ -146,7 +146,7 @@ data: $(this.form).serialize(), dataType: 'json', success: function (data) { - alert('Saved'); + EventBroker.publish('message', { title: '@T("Admin.Common.DataEditSuccess")', type: 'success' }); }, error: function (xhr, ajaxOptions, thrownError) { alert('Failed to add record.'); 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 7605330a41..1a1ea4e95b 100644 --- a/src/Plugins/SmartStore.Shipping/packages.config +++ b/src/Plugins/SmartStore.Shipping/packages.config @@ -1,8 +1,8 @@  - - - + + + 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 786862a0d6..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 { @@ -27,11 +28,12 @@ public class ByWeightShippingComputationMethod : BasePlugin, IShippingRateComput private readonly ShippingByWeightObjectContext _objectContext; private readonly ILocalizationService _localizationService; private readonly IPriceFormatter _priceFormatter; - private readonly ICommonServices _commonServices; + private readonly ICommonServices _services; #endregion #region Ctor + public ByWeightShippingComputationMethod(IShippingService shippingService, IStoreContext storeContext, IShippingByWeightService shippingByWeightService, @@ -40,7 +42,7 @@ public ByWeightShippingComputationMethod(IShippingService shippingService, ShippingByWeightObjectContext objectContext, ILocalizationService localizationService, IPriceFormatter priceFormatter, - ICommonServices commonServices) + ICommonServices services) { this._shippingService = shippingService; this._storeContext = storeContext; @@ -50,13 +52,18 @@ public ByWeightShippingComputationMethod(IShippingService shippingService, this._objectContext = objectContext; this._localizationService = localizationService; this._priceFormatter = priceFormatter; - this._commonServices = commonServices; - } - #endregion + this._services = services; + + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } - #region Utilities + #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(countryId); + var shippingMethods = _shippingService.GetAllShippingMethods(request); foreach (var shippingMethod in shippingMethods) { var record = _shippingByWeightService.FindRecord(shippingMethod.Id, storeId, countryId, weight, zip); @@ -152,6 +159,7 @@ public GetShippingOptionResponse GetShippingOptions(GetShippingOptionRequest get if (rate.HasValue) { var shippingOption = new ShippingOption(); + shippingOption.ShippingMethodId = shippingMethod.Id; shippingOption.Name = shippingMethod.GetLocalized(x => x.Name); if (record != null && record.SmallQuantityThreshold > subTotal) diff --git a/src/Plugins/SmartStore.ShippingByWeight/Controllers/ShippingByWeightController.cs b/src/Plugins/SmartStore.ShippingByWeight/Controllers/ShippingByWeightController.cs index 5431abad39..383d77dfa2 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Controllers/ShippingByWeightController.cs +++ b/src/Plugins/SmartStore.ShippingByWeight/Controllers/ShippingByWeightController.cs @@ -1,17 +1,15 @@ -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Web.Mvc; +using System.Web.Mvc; using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Directory; +using SmartStore.Services; +using SmartStore.Services.Directory; +using SmartStore.Services.Shipping; using SmartStore.ShippingByWeight.Domain; using SmartStore.ShippingByWeight.Models; using SmartStore.ShippingByWeight.Services; -using SmartStore.Services.Configuration; -using SmartStore.Services.Directory; -using SmartStore.Services.Shipping; -using SmartStore.Services.Stores; using SmartStore.Web.Framework.Controllers; +using SmartStore.Web.Framework.Filters; +using SmartStore.Web.Framework.Security; using Telerik.Web.Mvc; namespace SmartStore.ShippingByWeight.Controllers @@ -21,61 +19,67 @@ namespace SmartStore.ShippingByWeight.Controllers public class ShippingByWeightController : PluginControllerBase { private readonly IShippingService _shippingService; - private readonly IStoreService _storeService; private readonly ICountryService _countryService; private readonly ShippingByWeightSettings _shippingByWeightSettings; private readonly IShippingByWeightService _shippingByWeightService; - private readonly ISettingService _settingService; - - private readonly ICurrencyService _currencyService; - private readonly CurrencySettings _currencySettings; private readonly IMeasureService _measureService; private readonly MeasureSettings _measureSettings; private readonly AdminAreaSettings _adminAreaSettings; - - public ShippingByWeightController(IShippingService shippingService, - IStoreService storeService, ICountryService countryService, ShippingByWeightSettings shippingByWeightSettings, - IShippingByWeightService shippingByWeightService, ISettingService settingService, - ICurrencyService currencyService, CurrencySettings currencySettings, - IMeasureService measureService, MeasureSettings measureSettings, - AdminAreaSettings adminAreaSettings) + private readonly ICommonServices _services; + + public ShippingByWeightController( + IShippingService shippingService, + ICountryService countryService, + ShippingByWeightSettings shippingByWeightSettings, + IShippingByWeightService shippingByWeightService, + IMeasureService measureService, + MeasureSettings measureSettings, + AdminAreaSettings adminAreaSettings, + ICommonServices services) { this._shippingService = shippingService; - this._storeService = storeService; this._countryService = countryService; this._shippingByWeightSettings = shippingByWeightSettings; this._shippingByWeightService = shippingByWeightService; - this._settingService = settingService; - - this._currencyService = currencyService; - this._currencySettings = currencySettings; this._measureService = measureService; this._measureSettings = measureSettings; this._adminAreaSettings = adminAreaSettings; + this._services = services; } 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(); - foreach (var sm in shippingMethods) - model.AvailableShippingMethods.Add(new SelectListItem() { Text = sm.Name, Value = sm.Id.ToString() }); + var countries = _countryService.GetAllCountries(true); + var allStores = _services.StoreService.GetAllStores(); + + foreach (var sm in shippingMethods) + { + model.AvailableShippingMethods.Add(new SelectListItem { Text = sm.Name, Value = sm.Id.ToString() }); + } //stores - model.AvailableStores.Add(new SelectListItem() { Text = "*", Value = "0" }); - foreach (var store in _storeService.GetAllStores()) - model.AvailableStores.Add(new SelectListItem() { Text = store.Name, Value = store.Id.ToString() }); - - model.AvailableCountries.Add(new SelectListItem() { Text = "*", Value = "0" }); - var countries = _countryService.GetAllCountries(true); - foreach (var c in countries) - model.AvailableCountries.Add(new SelectListItem() { Text = c.Name, Value = c.Id.ToString() }); + model.AvailableStores.Add(new SelectListItem { Text = "*", Value = "0" }); + foreach (var store in allStores) + { + model.AvailableStores.Add(new SelectListItem { Text = store.Name, Value = store.Id.ToString() }); + } + + model.AvailableCountries.Add(new SelectListItem { Text = "*", Value = "0" }); + foreach (var c in countries) + { + model.AvailableCountries.Add(new SelectListItem { Text = c.Name, Value = c.Id.ToString() }); + } + model.LimitMethodsToCreated = _shippingByWeightSettings.LimitMethodsToCreated; model.CalculatePerWeightUnit = _shippingByWeightSettings.CalculatePerWeightUnit; - model.PrimaryStoreCurrencyCode = _currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId).CurrencyCode; + model.PrimaryStoreCurrencyCode = _services.StoreContext.CurrentStore.PrimaryStoreCurrency.CurrencyCode; model.BaseWeightIn = _measureService.GetMeasureWeightById(_measureSettings.BaseWeightId).Name; model.GridPageSize = _adminAreaSettings.GridPageSize; @@ -162,7 +166,7 @@ public ActionResult SaveGeneralSettings(ShippingByWeightListModel model) //save settings _shippingByWeightSettings.LimitMethodsToCreated = model.LimitMethodsToCreated; _shippingByWeightSettings.CalculatePerWeightUnit = model.CalculatePerWeightUnit; - _settingService.SaveSetting(_shippingByWeightSettings); + _services.Settings.SaveSetting(_shippingByWeightSettings); return Configure(); } diff --git a/src/Plugins/SmartStore.ShippingByWeight/Data/ShippingByWeightRecordMap.cs b/src/Plugins/SmartStore.ShippingByWeight/Data/ShippingByWeightRecordMap.cs index a42e0fe8bf..8f906b6226 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Data/ShippingByWeightRecordMap.cs +++ b/src/Plugins/SmartStore.ShippingByWeight/Data/ShippingByWeightRecordMap.cs @@ -9,7 +9,7 @@ public ShippingByWeightRecordMap() { this.ToTable("ShippingByWeight"); this.HasKey(x => x.Id); - this.Property(x => x.Zip).IsOptional().HasMaxLength(400); + this.Property(x => x.Zip).IsOptional(); } } } \ No newline at end of file diff --git a/src/Plugins/SmartStore.ShippingByWeight/Description.txt b/src/Plugins/SmartStore.ShippingByWeight/Description.txt index 9dd67c0819..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 -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/Localization/resources.de-de.xml b/src/Plugins/SmartStore.ShippingByWeight/Localization/resources.de-de.xml index 22792b8ccf..1cb41d9656 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Localization/resources.de-de.xml +++ b/src/Plugins/SmartStore.ShippingByWeight/Localization/resources.de-de.xml @@ -13,7 +13,7 @@ PLZ-(Bereich) - PLZ-(Bereich) des Kunden, entweder als spezifischer Wert oder als Muster (z.B. 4000-49999 für das PLZ-Gebiet 4). In einem Muster lassen sich auch Wildcards verwenden, wie Stern (*) oder Fragezeichen (?). Lassen Sie das Feld leer, wenn die Gebühr unabhängig von PLZ für alle Kunden im definierten (Bundes)land gelten soll. + PLZ-(Bereich) des Kunden, entweder als spezifische Werte (getrennt durch Komma) oder als Muster (z.B. 4000-49999 für das PLZ-Gebiet 4). In einem Muster lassen sich auch Wildcards verwenden, wie Stern (*) oder Fragezeichen (?). Sie können auch mehrere Wildcards angeben (durch Komma getrennt). Lassen Sie das Feld leer, wenn die Gebühr unabhängig von PLZ für alle Kunden im definierten (Bundes)land gelten soll. Versandart diff --git a/src/Plugins/SmartStore.ShippingByWeight/Localization/resources.en-us.xml b/src/Plugins/SmartStore.ShippingByWeight/Localization/resources.en-us.xml index a08218ae97..c168a3ef5f 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/Localization/resources.en-us.xml +++ b/src/Plugins/SmartStore.ShippingByWeight/Localization/resources.en-us.xml @@ -12,7 +12,7 @@ Zip-(Range) - Zip/postal code (range), either as specific value or range pattern (e.g. 4000-4999). You can also define wildcard chars like * or ?. If zip is empty, then this shipping rate will apply to all customers from the given country or state / province, regardless of the zip code. + Zip/postal code (range), either as specific values (comma devided) or range pattern (e.g. 4000-4999). You can also define wildcard chars like * or ?. If zip is empty, then this shipping rate will apply to all customers from the given country or state / province, regardless of the zip code. You can also enter multiple ranges (comma devided) Shipping method @@ -36,7 +36,7 @@ Use percentage - Check to use 'charge percentage' value. + Check the box to use 'charge percentage' value. Charge percentage (of subtotal) 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 8e9cfc5430..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, @@ -201,16 +201,26 @@ private bool ZipMatches(string zip, string pattern) { return true; // catch all } + + var patterns = pattern.Contains(",") + ? pattern.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()) + : new string[] { pattern }; try { - var wildcard = new Wildcard(pattern); - return wildcard.IsMatch(zip); + foreach (var entry in patterns) + { + var wildcard = new Wildcard(entry); + if (wildcard.IsMatch(zip)) + return true; + } } catch { return zip.IsCaseInsensitiveEqual(pattern); } + + return false; } #endregion diff --git a/src/Plugins/SmartStore.ShippingByWeight/SmartStore.ShippingByWeight.csproj b/src/Plugins/SmartStore.ShippingByWeight/SmartStore.ShippingByWeight.csproj index 422aa5517d..990c0f2f61 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/SmartStore.ShippingByWeight.csproj +++ b/src/Plugins/SmartStore.ShippingByWeight/SmartStore.ShippingByWeight.csproj @@ -42,6 +42,7 @@ + true @@ -81,22 +82,19 @@ 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 - ..\..\packages\EntityFramework.6.1.0\lib\net45\EntityFramework.dll - False + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll False - ..\..\packages\EntityFramework.6.1.0\lib\net45\EntityFramework.SqlServer.dll - False + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll diff --git a/src/Plugins/SmartStore.ShippingByWeight/Views/ShippingByWeight/Configure.cshtml b/src/Plugins/SmartStore.ShippingByWeight/Views/ShippingByWeight/Configure.cshtml index 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 7605330a41..1a1ea4e95b 100644 --- a/src/Plugins/SmartStore.ShippingByWeight/packages.config +++ b/src/Plugins/SmartStore.ShippingByWeight/packages.config @@ -1,8 +1,8 @@  - - - + + + 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 4c9be02213..3f4f325f97 100644 --- a/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj +++ b/src/Plugins/SmartStore.Tax/SmartStore.Tax.csproj @@ -42,6 +42,7 @@ + true @@ -81,23 +82,23 @@ MinimumRecommendedRules.ruleset - - ..\..\packages\Autofac.3.4.1\lib\net40\Autofac.dll - False + + ..\..\packages\Autofac.3.5.2\lib\net40\Autofac.dll - - ..\..\packages\EntityFramework.6.1.0\lib\net45\EntityFramework.dll - False + + False + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll - - ..\..\packages\EntityFramework.6.1.0\lib\net45\EntityFramework.SqlServer.dll - False + + False + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.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 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 fa6b3ec07d..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 232860f3c8..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,13 +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(), - MediaType = file.Headers.ContentType.MediaType.ToUnquoted(), - ContentDisposition = file.Headers.ContentDisposition.Parameters - }; + var image = new UploadImage(file.Headers); if (image.FileName.IsEmpty()) image.FileName = entity.Name; @@ -113,14 +142,13 @@ 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) { var seoName = _pictureService.Value.GetPictureSeName(Path.GetFileNameWithoutExtension(image.FileName)); - var newPicture = _pictureService.Value.InsertPicture(pictureBinary, image.MediaType, seoName, true, false); + var newPicture = _pictureService.Value.InsertPicture(pictureBinary, image.MediaType, seoName, true, false, false); if (newPicture != null) { @@ -155,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/AddressesController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/AddressesController.cs index 49785de800..3b7e734583 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/AddressesController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/AddressesController.cs @@ -73,14 +73,16 @@ public SingleResult
    GetAddress(int key) // navigation properties - public Country GetCountry(int key) + [WebApiQueryable] + public SingleResult GetCountry(int key) { - return GetExpandedProperty(key, x => x.Country); + return GetRelatedEntity(key, x => x.Country); } - public StateProvince GetStateProvince(int key) + [WebApiQueryable] + public SingleResult GetStateProvince(int key) { - return GetExpandedProperty(key, x => x.StateProvince); + return GetRelatedEntity(key, x => x.StateProvince); } } } diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/CategoriesController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/CategoriesController.cs index aa72f64388..9d252befa9 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/CategoriesController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/CategoriesController.cs @@ -67,9 +67,7 @@ public SingleResult GetCategory(int key) [WebApiQueryable] public IQueryable GetAppliedDiscounts(int key) { - var entity = GetExpandedEntity>(key, x => x.AppliedDiscounts); - - return entity.AppliedDiscounts.AsQueryable(); + return GetRelatedCollection(key, x => x.AppliedDiscounts); } } } diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/CountriesController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/CountriesController.cs index cf78613bd4..15f58522a4 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/CountriesController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/CountriesController.cs @@ -36,9 +36,7 @@ public SingleResult GetCountry(int key) [WebApiQueryable] public IQueryable GetStateProvinces(int key) { - var entity = GetExpandedEntity>(key, x => x.StateProvinces); - - return entity.StateProvinces.AsQueryable(); + return GetRelatedCollection(key, x => x.StateProvinces); } } } diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/CustomersController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/CustomersController.cs index 317c3e09da..c811dd60cf 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/CustomersController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/CustomersController.cs @@ -44,14 +44,16 @@ public SingleResult GetCustomer(int key) // navigation properties - public Address GetBillingAddress(int key) + [WebApiQueryable] + public SingleResult
    GetBillingAddress(int key) { - return GetExpandedProperty
    (key, x => x.BillingAddress); + return GetRelatedEntity(key, x => x.BillingAddress); } - public Address GetShippingAddress(int key) + [WebApiQueryable] + public SingleResult
    GetShippingAddress(int key) { - return GetExpandedProperty
    (key, x => x.ShippingAddress); + return GetRelatedEntity(key, x => x.ShippingAddress); } //public Language GetLanguage(int key) @@ -67,25 +69,19 @@ public Address GetShippingAddress(int key) [WebApiQueryable] public IQueryable GetOrders(int key) { - var entity = GetExpandedEntity>(key, x => x.Orders); - - return entity.Orders.AsQueryable(); + return GetRelatedCollection(key, x => x.Orders); } [WebApiQueryable] public IQueryable GetReturnRequests(int key) { - var entity = GetExpandedEntity>(key, x => x.ReturnRequests); - - return entity.ReturnRequests.AsQueryable(); + return GetRelatedCollection(key, x => x.ReturnRequests); } [WebApiQueryable] public IQueryable
    GetAddresses(int key) { - var entity = GetExpandedEntity>(key, x => x.Addresses); - - return entity.Addresses.AsQueryable(); + return GetRelatedCollection(key, x => x.Addresses); } // actions diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/DiscountsController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/DiscountsController.cs index de6badcc25..eeb0d3d448 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/DiscountsController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/DiscountsController.cs @@ -37,9 +37,7 @@ public SingleResult GetDiscount(int key) [WebApiQueryable] public IQueryable GetAppliedToCategories(int key) { - var entity = GetExpandedEntity>(key, x => x.AppliedToCategories); - - return entity.AppliedToCategories.AsQueryable(); + return GetRelatedCollection(key, x => x.AppliedToCategories); } } } diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/LocalizedPropertysController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/LocalizedPropertysController.cs index 8aa403ce14..9f9c52edca 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/LocalizedPropertysController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/LocalizedPropertysController.cs @@ -31,9 +31,10 @@ public SingleResult GetLocalizedProperty(int key) // navigation properties - public Language GetLanguage(int key) + [WebApiQueryable] + public SingleResult GetLanguage(int key) { - return GetExpandedProperty(key, x => x.Language); + return GetRelatedEntity(key, x => x.Language); } } } diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/OrderItemsController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/OrderItemsController.cs index bdbe03a781..2e608f15bc 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/OrderItemsController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/OrderItemsController.cs @@ -41,14 +41,16 @@ public SingleResult GetOrderItem(int key) // navigation properties - public Order GetOrder(int key) + [WebApiQueryable] + public SingleResult GetOrder(int key) { - return GetExpandedProperty(key, x => x.Order); + return GetRelatedEntity(key, x => x.Order); } - public Product GetProduct(int key) + [WebApiQueryable] + public SingleResult GetProduct(int key) { - return GetExpandedProperty(key, x => x.Product); + return GetRelatedEntity(key, x => x.Product); } } } diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/OrderNotesController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/OrderNotesController.cs index 99bc1502c3..354204fb0e 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/OrderNotesController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/OrderNotesController.cs @@ -23,9 +23,10 @@ public SingleResult GetOrderNote(int key) // navigation properties - public Order GetOrder(int key) + [WebApiQueryable] + public SingleResult GetOrder(int key) { - return GetExpandedProperty(key, x => x.Order); + return GetRelatedEntity(key, x => x.Order); } } } diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/OrdersController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/OrdersController.cs index f86745f368..f0f1c0eb9b 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/OrdersController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/OrdersController.cs @@ -55,67 +55,66 @@ public SingleResult GetOrder(int key) // navigation properties - public Customer GetCustomer(int key) + [WebApiQueryable] + public SingleResult GetCustomer(int key) { - return GetExpandedProperty(key, x => x.Customer); + return GetRelatedEntity(key, x => x.Customer); } - public Address GetBillingAddress(int key) + [WebApiQueryable] + public SingleResult
    GetBillingAddress(int key) { - return GetExpandedProperty
    (key, x => x.BillingAddress); + return GetRelatedEntity(key, x => x.BillingAddress); } - public Address GetShippingAddress(int key) + [WebApiQueryable] + public SingleResult
    GetShippingAddress(int key) { - return GetExpandedProperty
    (key, x => x.ShippingAddress); + return GetRelatedEntity(key, x => x.ShippingAddress); } [WebApiQueryable] public IQueryable GetOrderNotes(int key) { - var entity = GetExpandedEntity>(key, x => x.OrderNotes); //var entity = GetEntityByKeyNotNull(key); // if ProxyCreationEnabled = true - - return entity.OrderNotes.AsQueryable(); + return GetRelatedCollection(key, x => x.OrderNotes); } [WebApiQueryable] public IQueryable GetShipments(int key) { - var entity = GetExpandedEntity>(key, x => x.Shipments); - - return entity.Shipments.AsQueryable(); + return GetRelatedCollection(key, x => x.Shipments); } [WebApiQueryable] public IQueryable GetOrderItems(int key) { - var entity = GetExpandedEntity>(key, x => x.OrderItems); - - return entity.OrderItems.AsQueryable(); + return GetRelatedCollection(key, x => x.OrderItems); } // 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(() => { @@ -123,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(() => { @@ -144,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/PaymentMethodsController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/PaymentMethodsController.cs new file mode 100644 index 0000000000..ccc87cfad6 --- /dev/null +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/PaymentMethodsController.cs @@ -0,0 +1,35 @@ +using System.Web.Http; +using SmartStore.Core.Domain.Payments; +using SmartStore.Services.Payments; +using SmartStore.Web.Framework.WebApi; +using SmartStore.Web.Framework.WebApi.OData; +using SmartStore.Web.Framework.WebApi.Security; + +namespace SmartStore.WebApi.Controllers.OData +{ + [WebApiAuthenticate(Permission = "ManagePaymentMethods")] + public class PaymentMethodsController : WebApiEntityController + { + protected override void Insert(PaymentMethod entity) + { + Service.InsertPaymentMethod(entity); + } + protected override void Update(PaymentMethod entity) + { + Service.UpdatePaymentMethod(entity); + } + protected override void Delete(PaymentMethod entity) + { + Service.DeletePaymentMethod(entity); + } + + [WebApiQueryable] + public SingleResult GetShippingMethod(int key) + { + return GetSingleResult(key); + } + + // navigation properties + + } +} diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/PicturesController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/PicturesController.cs index 26e4025c27..9e23c2959f 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/PicturesController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/PicturesController.cs @@ -37,9 +37,7 @@ public SingleResult GetPicture(int key) [WebApiQueryable] public IQueryable GetProductPictures(int key) { - var entity = GetExpandedEntity>(key, x => x.ProductPictures); - - return entity.ProductPictures.AsQueryable(); + return GetRelatedCollection(key, x => x.ProductPictures); } } } diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductBundleItemsController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductBundleItemsController.cs index 3d86242624..a24d207821 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductBundleItemsController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductBundleItemsController.cs @@ -27,18 +27,20 @@ protected override void Delete(ProductBundleItem entity) public SingleResult GetProductBundleItem(int key) { return GetSingleResult(key); - } + } // navigation properties - public Product GetProduct(int key) + [WebApiQueryable] + public SingleResult GetProduct(int key) { - return GetExpandedProperty(key, x => x.Product); + return GetRelatedEntity(key, x => x.Product); } - public Product GetBundleProduct(int key) + [WebApiQueryable] + public SingleResult GetBundleProduct(int key) { - return GetExpandedProperty(key, x => x.BundleProduct); + return GetRelatedEntity(key, x => x.BundleProduct); } } } diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductVariantAttributeCombinationsController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductVariantAttributeCombinationsController.cs index a7e393e205..822b0d4d62 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductVariantAttributeCombinationsController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductVariantAttributeCombinationsController.cs @@ -32,9 +32,10 @@ public SingleResult GetProductVariantAttribu // navigation properties - public DeliveryTime GetDeliveryTime(int key) + [WebApiQueryable] + public SingleResult GetDeliveryTime(int key) { - return GetExpandedProperty(key, x => x.DeliveryTime); + return GetRelatedEntity(key, x => x.DeliveryTime); } } } diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductVariantAttributeValuesController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductVariantAttributeValuesController.cs index b2f6a9e3ff..ad1f54efe7 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductVariantAttributeValuesController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductVariantAttributeValuesController.cs @@ -31,9 +31,10 @@ public SingleResult GetProductVariantAttributeValu // navigation properties - public ProductVariantAttribute GetProductVariantAttribute(int key) + [WebApiQueryable] + public SingleResult GetProductVariantAttribute(int key) { - return GetExpandedProperty(key, x => x.ProductVariantAttribute); + return GetRelatedEntity(key, x => x.ProductVariantAttribute); } } } diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductVariantAttributesController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductVariantAttributesController.cs index 9218f73e6d..487f636560 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductVariantAttributesController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductVariantAttributesController.cs @@ -33,17 +33,16 @@ public SingleResult GetProductVariantAttribute(int key) // navigation properties - public ProductAttribute GetProductAttribute(int key) + [WebApiQueryable] + public SingleResult GetProductAttribute(int key) { - return GetExpandedProperty(key, x => x.ProductAttribute); + return GetRelatedEntity(key, x => x.ProductAttribute); } [WebApiQueryable] public IQueryable GetProductVariantAttributeValues(int key) { - var entity = GetExpandedEntity>(key, x => x.ProductVariantAttributeValues); - - return entity.ProductVariantAttributeValues.AsQueryable(); + return GetRelatedCollection(key, x => x.ProductVariantAttributeValues); } } } diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductsController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductsController.cs index e0a6d49da2..df8dfd389d 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductsController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/ProductsController.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; +using System.Net.Http; using System.Web.Http; using System.Web.Http.OData; +using System.Data.Entity; using SmartStore.Core; using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Directory; @@ -24,17 +27,23 @@ public class ProductsController : WebApiEntityController _priceCalculationService; private readonly Lazy _urlRecordService; private readonly Lazy _productAttributeService; + private readonly Lazy _categoryService; + private readonly Lazy _manufacturerService; public ProductsController( Lazy workContext, Lazy priceCalculationService, Lazy urlRecordService, - Lazy productAttributeService) + Lazy productAttributeService, + Lazy categoryService, + Lazy manufacturerService) { _workContext = workContext; _priceCalculationService = priceCalculationService; _urlRecordService = urlRecordService; _productAttributeService = productAttributeService; + _categoryService = categoryService; + _manufacturerService = manufacturerService; } protected override IQueryable GetEntitySet() @@ -79,99 +88,136 @@ public SingleResult GetProduct(int key) // navigation properties - public DeliveryTime GetDeliveryTime(int key) + public HttpResponseMessage NavigationProductCategories(int key, int relatedKey) { - return GetExpandedProperty(key, x => x.DeliveryTime); + var productCategories = _categoryService.Value.GetProductCategoriesByProductId(key, true); + var productCategory = productCategories.FirstOrDefault(x => x.CategoryId == relatedKey); + + if (Request.Method == HttpMethod.Post) + { + if (productCategory == null) + { + productCategory = new ProductCategory { ProductId = key, CategoryId = relatedKey }; + + _categoryService.Value.InsertProductCategory(productCategory); + + return Request.CreateResponse(HttpStatusCode.Created, productCategory); + } + } + else if (Request.Method == HttpMethod.Delete) + { + if (productCategory != null) + _categoryService.Value.DeleteProductCategory(productCategory); + + return Request.CreateResponse(HttpStatusCode.NoContent); + } + + return Request.CreateResponseForEntity(productCategory, relatedKey); } - public QuantityUnit GetQuantityUnit(int key) + public HttpResponseMessage NavigationProductManufacturers(int key, int relatedKey) { - return GetExpandedProperty(key, x => x.QuantityUnit); + var productManufacturers = _manufacturerService.Value.GetProductManufacturersByProductId(key, true); + var productManufacturer = productManufacturers.FirstOrDefault(x => x.ManufacturerId == relatedKey); + + if (Request.Method == HttpMethod.Post) + { + if (productManufacturer == null) + { + productManufacturer = new ProductManufacturer { ProductId = key, ManufacturerId = relatedKey }; + + _manufacturerService.Value.InsertProductManufacturer(productManufacturer); + + return Request.CreateResponse(HttpStatusCode.Created, productManufacturer); + } + } + else if (Request.Method == HttpMethod.Delete) + { + if (productManufacturer != null) + _manufacturerService.Value.DeleteProductManufacturer(productManufacturer); + + return Request.CreateResponse(HttpStatusCode.NoContent); + } + + return Request.CreateResponseForEntity(productManufacturer, relatedKey); } - public Download GetSampleDownload(int key) + [WebApiQueryable] + public SingleResult GetDeliveryTime(int key) { - return GetExpandedProperty(key, x => x.SampleDownload); + return GetRelatedEntity(key, x => x.DeliveryTime); } [WebApiQueryable] - public IQueryable GetProductCategories(int key) + public SingleResult GetQuantityUnit(int key) { - var entity = GetExpandedEntity>(key, x => x.ProductCategories); + return GetRelatedEntity(key, x => x.QuantityUnit); + } - return entity.ProductCategories.AsQueryable(); + [WebApiQueryable] + public SingleResult GetSampleDownload(int key) + { + return GetRelatedEntity(key, x => x.SampleDownload); } [WebApiQueryable] - public IQueryable GetProductManufacturers(int key) + public IQueryable GetProductCategories(int key) { - var entity = GetExpandedEntity>(key, x => x.ProductManufacturers); + return GetRelatedCollection(key, x => x.ProductCategories); + } - return entity.ProductManufacturers.AsQueryable(); + [WebApiQueryable] + public IQueryable GetProductManufacturers(int key) + { + return GetRelatedCollection(key, x => x.ProductManufacturers); } [WebApiQueryable] public IQueryable GetProductPictures(int key) { - var entity = GetExpandedEntity>(key, x => x.ProductPictures); - - return entity.ProductPictures.AsQueryable(); + return GetRelatedCollection(key, x => x.ProductPictures); } [WebApiQueryable] public IQueryable GetProductSpecificationAttributes(int key) { - var entity = GetExpandedEntity>(key, x => x.ProductSpecificationAttributes); - - return entity.ProductSpecificationAttributes.AsQueryable(); + return GetRelatedCollection(key, x => x.ProductSpecificationAttributes); } [WebApiQueryable] public IQueryable GetProductTags(int key) { - var entity = GetExpandedEntity>(key, x => x.ProductTags); - - return entity.ProductTags.AsQueryable(); + return GetRelatedCollection(key, x => x.ProductTags); } [WebApiQueryable] public IQueryable GetTierPrices(int key) { - var entity = GetExpandedEntity>(key, x => x.TierPrices); - - return entity.TierPrices.AsQueryable(); + return GetRelatedCollection(key, x => x.TierPrices); } [WebApiQueryable] public IQueryable GetAppliedDiscounts(int key) { - var entity = GetExpandedEntity>(key, x => x.AppliedDiscounts); - - return entity.AppliedDiscounts.AsQueryable(); + return GetRelatedCollection(key, x => x.AppliedDiscounts); } [WebApiQueryable] public IQueryable GetProductVariantAttributes(int key) { - var entity = GetExpandedEntity>(key, x => x.ProductVariantAttributes); - - return entity.ProductVariantAttributes.AsQueryable(); + return GetRelatedCollection(key, x => x.ProductVariantAttributes); } [WebApiQueryable] public IQueryable GetProductVariantAttributeCombinations(int key) { - var entity = GetExpandedEntity>(key, x => x.ProductVariantAttributeCombinations); - - return entity.ProductVariantAttributeCombinations.AsQueryable(); + return GetRelatedCollection(key, x => x.ProductVariantAttributeCombinations); } [WebApiQueryable] public IQueryable GetProductBundleItems(int key) { - var entity = GetExpandedEntity>(key, x => x.ProductBundleItems); - - return entity.ProductBundleItems.AsQueryable(); + return GetRelatedCollection(key, x => x.ProductBundleItems); } // actions @@ -188,8 +234,9 @@ public IQueryable GetProductBundleItems(int key) { if (entity.ProductType == ProductType.GroupedProduct) { - var searchContext = new ProductSearchContext() + var searchContext = new ProductSearchContext { + OrderBy = ProductSortingEnum.Position, Query = this.GetExpandedEntitySet(requiredProperties), ParentGroupedProductId = entity.Id, PageSize = int.MaxValue, @@ -199,17 +246,17 @@ public IQueryable GetProductBundleItems(int key) Product lowestPriceProduct; var associatedProducts = Service.PrepareProductSearchQuery(searchContext); - result = _priceCalculationService.Value.GetLowestPrice(entity, associatedProducts, out lowestPriceProduct); + result = _priceCalculationService.Value.GetLowestPrice(entity, null, associatedProducts, out lowestPriceProduct); } else { bool displayFromMessage; - result = _priceCalculationService.Value.GetLowestPrice(entity, out displayFromMessage); + result = _priceCalculationService.Value.GetLowestPrice(entity, null, out displayFromMessage); } } else { - result = _priceCalculationService.Value.GetPreselectedPrice(entity); + result = _priceCalculationService.Value.GetPreselectedPrice(entity, null); } return null; }); @@ -345,22 +392,5 @@ public IQueryable ManageAttributes(int key, ODataAction return entity.ProductVariantAttributes.AsQueryable(); } - - - //[HttpGet, WebApiQueryable] - //public IQueryable GetRelatedProducts(int key) - //{ - // if (!ModelState.IsValid) - // throw this.ExceptionInvalidModelState(); - - // var repository = EngineContext.Current.Resolve>(); - - // var query = - // from x in repository.Table - // where x.ProductId1 == key - // select x; - - // return query; - //} } } diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/ReturnRequestsController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/ReturnRequestsController.cs index 949b9b067b..fbcdd389b0 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/ReturnRequestsController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/ReturnRequestsController.cs @@ -24,9 +24,10 @@ public SingleResult GetReturnRequest(int key) // navigation properties - public Customer GetCustomer(int key) + [WebApiQueryable] + public SingleResult GetCustomer(int key) { - return GetExpandedProperty(key, x => x.Customer); + return GetRelatedEntity(key, x => x.Customer); } } } 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/OData/ShippingMethodsController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/ShippingMethodsController.cs index 0f4f6aef90..ab13899ff9 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/ShippingMethodsController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/ShippingMethodsController.cs @@ -37,9 +37,7 @@ public SingleResult GetShippingMethod(int key) [WebApiQueryable] public IQueryable GetRestrictedCountries(int key) { - var entity = GetExpandedEntity>(key, x => x.RestrictedCountries); - - return entity.RestrictedCountries.AsQueryable(); + return GetRelatedCollection(key, x => x.RestrictedCountries); } } } diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/SpecificationAttributeOptionsController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/SpecificationAttributeOptionsController.cs index f4e66b5ddd..e377bb9fff 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/SpecificationAttributeOptionsController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/SpecificationAttributeOptionsController.cs @@ -33,17 +33,16 @@ public SingleResult GetSpecificationAttributeOptio // navigation properties - public SpecificationAttribute GetSpecificationAttribute(int key) + [WebApiQueryable] + public SingleResult GetSpecificationAttribute(int key) { - return GetExpandedProperty(key, x => x.SpecificationAttribute); + return GetRelatedEntity(key, x => x.SpecificationAttribute); } [WebApiQueryable] public IQueryable GetProductSpecificationAttributes(int key) { - var entity = GetExpandedEntity>(key, x => x.ProductSpecificationAttributes); - - return entity.ProductSpecificationAttributes.AsQueryable(); + return GetRelatedCollection(key, x => x.ProductSpecificationAttributes); } } } diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/SpecificationAttributesController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/SpecificationAttributesController.cs index 7e9c084a9d..3990c280d8 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/SpecificationAttributesController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/SpecificationAttributesController.cs @@ -36,9 +36,7 @@ public SingleResult GetSpecificationAttribute(int key) [WebApiQueryable] public IQueryable GetSpecificationAttributeOptions(int key) { - var entity = GetExpandedEntity>(key, x => x.SpecificationAttributeOptions); - - return entity.SpecificationAttributeOptions.AsQueryable(); + return GetRelatedCollection(key, x => x.SpecificationAttributeOptions); } } } diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/StateProvincesController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/StateProvincesController.cs index 1c26bdacb6..f01dccfec4 100644 --- a/src/Plugins/SmartStore.WebApi/Controllers/OData/StateProvincesController.cs +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/StateProvincesController.cs @@ -31,9 +31,10 @@ public SingleResult GetStateProvince(int key) // navigation properties - public Country GetCountry(int key) + [WebApiQueryable] + public SingleResult GetCountry(int key) { - return GetExpandedProperty(key, x => x.Country); + return GetRelatedEntity(key, x => x.Country); } } } diff --git a/src/Plugins/SmartStore.WebApi/Controllers/OData/SyncMappingsController.cs b/src/Plugins/SmartStore.WebApi/Controllers/OData/SyncMappingsController.cs new file mode 100644 index 0000000000..bccd10dcaa --- /dev/null +++ b/src/Plugins/SmartStore.WebApi/Controllers/OData/SyncMappingsController.cs @@ -0,0 +1,33 @@ +using System.Web.Http; +using SmartStore.Core.Domain.DataExchange; +using SmartStore.Core.Domain.Stores; +using SmartStore.Services.DataExchange; +using SmartStore.Web.Framework.WebApi; +using SmartStore.Web.Framework.WebApi.OData; +using SmartStore.Web.Framework.WebApi.Security; + +namespace SmartStore.WebApi.Controllers.OData +{ + [WebApiAuthenticate(Permission = "ManageMaintenance")] // TODO: ManageMaintenance... really? + public class SyncMappingsController : WebApiEntityController + { + protected override void Insert(SyncMapping entity) + { + Service.InsertSyncMapping(entity); + } + protected override void Update(SyncMapping entity) + { + Service.UpdateSyncMapping(entity); + } + protected override void Delete(SyncMapping entity) + { + Service.DeleteSyncMapping(entity); + } + + [WebApiQueryable] + public SingleResult GetSyncMapping(int key) + { + return GetSingleResult(key); + } + } +} diff --git a/src/Plugins/SmartStore.WebApi/Controllers/WebApiController.cs b/src/Plugins/SmartStore.WebApi/Controllers/WebApiController.cs index 30c9c98295..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; @@ -19,36 +21,36 @@ public class WebApiController : PluginControllerBase private readonly WebApiSettings _webApiSettings; private readonly IWebApiPluginService _webApiPluginService; private readonly AdminAreaSettings _adminAreaSettings; - private readonly ICommonServices _commonServices; + private readonly ICommonServices _services; public WebApiController( WebApiSettings settings, IWebApiPluginService webApiPluginService, AdminAreaSettings adminAreaSettings, - ICommonServices commonServices) + ICommonServices services) { _webApiSettings = settings; _webApiPluginService = webApiPluginService; _adminAreaSettings = adminAreaSettings; - _commonServices = commonServices; + _services = services; } private bool HasPermission(bool notify = true) { - bool hasPermission = _commonServices.Permissions.Authorize(WebApiPermissionProvider.ManageWebApi); + bool hasPermission = _services.Permissions.Authorize(WebApiPermissionProvider.ManageWebApi); if (notify && !hasPermission) - NotifyError(_commonServices.Localization.GetResource("Admin.AccessDenied.Description")); + NotifyError(_services.Localization.GetResource("Admin.AccessDenied.Description")); return hasPermission; } private void AddButtonText() { - ViewData["ButtonTextEnable"] = _commonServices.Localization.GetResource("Plugins.Api.WebApi.Activate"); - ViewData["ButtonTextDisable"] = _commonServices.Localization.GetResource("Plugins.Api.WebApi.Deactivate"); - ViewData["ButtonTextRemoveKeys"] = _commonServices.Localization.GetResource("Plugins.Api.WebApi.RemoveKeys"); - ViewData["ButtonTextCreateKeys"] = _commonServices.Localization.GetResource("Plugins.Api.WebApi.CreateKeys"); + ViewData["ButtonTextEnable"] = _services.Localization.GetResource("Plugins.Api.WebApi.Activate"); + ViewData["ButtonTextDisable"] = _services.Localization.GetResource("Plugins.Api.WebApi.Deactivate"); + ViewData["ButtonTextRemoveKeys"] = _services.Localization.GetResource("Plugins.Api.WebApi.RemoveKeys"); + ViewData["ButtonTextCreateKeys"] = _services.Localization.GetResource("Plugins.Api.WebApi.CreateKeys"); } public ActionResult Configure() @@ -84,7 +86,7 @@ public ActionResult SaveGeneralSettings(WebApiConfigModel model) return AccessDeniedPartialView(); model.Copy(_webApiSettings, false); - _commonServices.Settings.SaveSetting(_webApiSettings); + _services.Settings.SaveSetting(_webApiSettings); WebApiCachingControllingData.Remove(); @@ -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 53b6ca55d7..646ac2fd0d 100644 --- a/src/Plugins/SmartStore.WebApi/Description.txt +++ b/src/Plugins/SmartStore.WebApi/Description.txt @@ -1,10 +1,10 @@ FriendlyName: SmartStore.NET Web Api SystemName: SmartStore.WebApi -Version: 2.2.0.2 +Version: 2.6.0 Group: Api -MinAppVersion: 2.2.0 +MinAppVersion: 2.5.0 Author: SmartStore AG DisplayOrder: 1 FileName: SmartStore.WebApi.dll ResourceRootKey: Plugins.Api.WebApi -Url: http://community.smartstore.com/index.php?/files/file/27-smartstorenet-web-api/ \ No newline at end of file +Url: http://community.smartstore.com/marketplace/file/27-smartstorenet-web-api/ \ No newline at end of file diff --git a/src/Plugins/SmartStore.WebApi/Localization/resources.de-de.xml b/src/Plugins/SmartStore.WebApi/Localization/resources.de-de.xml index 0159961db4..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,22 @@ 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 + + + 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 4b7f67b5bf..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 @@ -35,6 +35,18 @@ Each unauthorized access will be saved in the log list. + + No timestamp validation + + + 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 0688b8f792..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 @@ -15,7 +15,13 @@ public class WebApiConfigModel : ModelBase [SmartResourceDisplayName("Plugins.Api.WebApi.ValidMinutePeriod")] public int ValidMinutePeriod { get; set; } - [SmartResourceDisplayName("Plugins.Api.WebApi.LogUnauthorized")] + [SmartResourceDisplayName("Plugins.Api.WebApi.NoRequestTimestampValidation")] + public bool NoRequestTimestampValidation { get; set; } + + [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; } @@ -25,11 +31,15 @@ public void Copy(WebApiSettings settings, bool fromSettings) if (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 114a7f15ce..73e315200d 100644 --- a/src/Plugins/SmartStore.WebApi/SmartStore.WebApi.csproj +++ b/src/Plugins/SmartStore.WebApi/SmartStore.WebApi.csproj @@ -49,6 +49,7 @@ + true @@ -77,20 +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.4.1.1\lib\net45\AutoMapper.dll - - ..\..\packages\AutoMapper.3.2.1\lib\net40\AutoMapper.Net4.dll + + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll + + + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll False @@ -103,11 +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 @@ -204,6 +211,9 @@ + + + @@ -221,7 +231,9 @@ + + @@ -311,7 +323,6 @@ - +
    +
    + @Html.Partial("_LastRun", task) +
    +
    +
    + @Html.Partial("_NextRun", task) +
    +
    +
    + @T("Common.Edit") + + @T("Admin.System.ScheduleTasks.RunNow") + + + @T("Common.Cancel") + +
    + + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/MinimalTask.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/MinimalTask.cshtml new file mode 100644 index 0000000000..530a08f62e --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/MinimalTask.cshtml @@ -0,0 +1,63 @@ +@model ScheduleTaskModel +@{ + Layout = null; + var widgetId = "minimal-task-widget-" + Model.Id; + var returnUrl = (string)ViewBag.ReturnUrl; + var hasPermission = ViewBag.HasPermission == true; + var cancellable = ViewBag.Cancellable == true; + var reloadPage = ViewBag.ReloadPage == true; +} +
    +
    + @Html.Partial("_MinimalTaskWidget", Model) +
    +
    + @if (cancellable && hasPermission) + { + + @T("Common.Cancel") + + } +
    + + + + + + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/_LastRun.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/_LastRun.cshtml new file mode 100644 index 0000000000..6626a3e711 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/_LastRun.cshtml @@ -0,0 +1,30 @@ +@model ScheduleTaskModel +@{ + Layout = null; +} + +@if (!Model.IsRunning && !Model.LastEnd.HasValue) +{ +
    @T("Common.Never")
    +} +else if (Model.LastStart.HasValue) +{ +
    @Model.LastStart.Value.ToString("g")
    +
    @Model.LastStartPretty
    + if (Model.Duration.HasValue()) + { +
    @T("Common.Duration"): @Model.Duration
    + } + if (Model.LastError.HasValue()) + { +
    @T("Common.Error"): @Model.LastError
    + if (Model.LastSuccess.HasValue && Model.LastSuccess != Model.LastEnd) + { +
    @T("Admin.System.ScheduleTasks.LastSuccess"): @Model.LastSuccess.Value.ToString("g")
    + } + } +} + + + + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/_MinimalTaskWidget.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/_MinimalTaskWidget.cshtml new file mode 100644 index 0000000000..8c93b494dc --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/_MinimalTaskWidget.cshtml @@ -0,0 +1,58 @@ +@model ScheduleTaskModel +@{ + Layout = null; + var returnUrl = (string)ViewBag.ReturnUrl; + var hasPermission = ViewBag.HasPermission == true; +} +@(Model.Enabled ? T("Common.Scheduled") : T("Common.Unscheduled")) +@if (Model.Enabled) +{ + @T("Common.Rule"): + @(Model.CronDescription ?? Model.CronExpression) + if (Model.NextRun.HasValue) + { + - @T("Admin.System.ScheduleTasks.NextRun"): + @Model.NextRunPretty + } +} +else +{ + @T("Admin.System.ScheduleTasks.LastStart"): + @(Model.LastStart.HasValue ? Model.LastStartPretty : T("Common.Never").Text) +} +@if (hasPermission) +{ + +} + + + + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/_NextRun.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/_NextRun.cshtml new file mode 100644 index 0000000000..8bdd1b5575 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/ScheduleTask/_NextRun.cshtml @@ -0,0 +1,25 @@ +@model ScheduleTaskModel +@{ + Layout = null; +} + +@if (Model.NextRun.HasValue) +{ + if (Model.IsOverdue) + { + @Model.NextRunPretty + } + else + { +
    @Model.NextRun.Value.ToString("g")
    +
    @Model.NextRunPretty
    + } +} +else +{ +
    @T("Common.Never")
    +} + + + + diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Security/Permissions.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Security/Permissions.cshtml index 5703d75a62..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 { @@ -33,12 +39,12 @@ - @T("Admin.Configuration.ACL.Permission") + @T("Admin.Configuration.ACL.Permission") @foreach (var cr in Model.AvailableCustomerRoles) { - @cr.Name + @cr.Name } @@ -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/Blog.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Blog.cshtml index dc64352a48..973aa0703f 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Blog.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Blog.cshtml @@ -64,6 +64,15 @@ @Html.ValidationMessageFor(model => model.NumberOfTags) + + + @Html.SmartLabelFor(model => model.MaxAgeInDays) + + + @Html.SettingEditorFor(model => model.MaxAgeInDays) + @Html.ValidationMessageFor(model => model.MaxAgeInDays) + + @Html.SmartLabelFor(model => model.ShowHeaderRssUrl) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml index a55ae23614..56664aafbb 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/Catalog.cshtml @@ -23,13 +23,17 @@ $("#@Html.FieldIdFor(model => model.RecentlyViewedProductsEnabled)").click(toggleRecentlyViewedProducts); $("#@Html.FieldIdFor(model => model.RecentlyAddedProductsEnabled)").click(toggleRecentlyAddedProducts); $("#@Html.FieldIdFor(model => model.ShowBestsellersOnHomepage)").click(toggleBestsellersOnHomepage); + $("#@Html.FieldIdFor(model => model.ShowManufacturersOnHomepage)").click(toggleManufacturersOnHomepage); $("#@Html.FieldIdFor(model => model.ProductsAlsoPurchasedEnabled)").click(toggleProductsAlsoPurchasedNumber); $("#@Html.FieldIdFor(model => model.ProductsByTagAllowCustomersToSelectPageSize)").click(toggleTagPageSize); $("#@Html.FieldIdFor(model => model.ProductSearchAllowCustomersToSelectPageSize)").click(toggleSearchPageSize); $("#@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(); @@ -42,7 +46,7 @@ toggleProductSearchAutoComplete(); toggleCompareProducts(); toggleHtmlTextCollapsedHeight(); - toggleFilterEnabled(); + toggleManufacturersOnHomepage(); }); function toggleShowCategoryProductNumberIncludingSubcategories() { @@ -90,6 +94,17 @@ } } + function toggleManufacturersOnHomepage() { + if ($('#@Html.FieldIdFor(model => model.ShowManufacturersOnHomepage)').is(':checked')) { + $('#pnlShowManufacturerPictures').show(); + $('#pnlManufacturersBlockItemsToDisplay').show(); + } + else { + $('#pnlShowManufacturerPictures').hide(); + $('#pnlManufacturersBlockItemsToDisplay').hide(); + } + } + function toggleProductsAlsoPurchasedNumber() { if ($('#@Html.FieldIdFor(model => model.ProductsAlsoPurchasedEnabled)').is(':checked')) { $('#pnlProductsAlsoPurchasedNumber').show(); @@ -144,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") @@ -163,8 +168,8 @@ { x.Add().Text(T("Admin.Configuration.Settings.Catalog.MiscSettings").Text).Content(@TabMiscSettings()).Selected(true); x.Add().Text(T("Admin.Configuration.Settings.Catalog.ProductListSettings").Text).Content(@TabProductListSettings()); - x.Add().Text(T("Admin.Configuration.Settings.Catalog.UserSettings").Text).Content(@TabUserSettings()); x.Add().Text(T("Admin.Configuration.Settings.Catalog.ProductDetailSettings").Text).Content(@TabProductDetailSettings()); + x.Add().Text(T("Admin.Configuration.Settings.Catalog.UserSettings").Text).Content(@TabUserSettings()); x.Add().Text(T("Admin.Configuration.Settings.Catalog.ProductSearchSettings").Text).Content(@TabProductSearchSettings()); })) @@ -244,8 +249,7 @@ @Html.ValidationMessageFor(model => model.IgnoreFeaturedProducts) - - + @Html.SmartLabelFor(model => model.CompareProductsEnabled) @@ -290,6 +294,62 @@ @Html.ValidationMessageFor(model => model.NumberOfBestsellersOnHomepage) + + + + @Html.SmartLabelFor(model => model.ShowManufacturersOnHomepage) + + + @Html.SettingEditorFor(model => model.ShowManufacturersOnHomepage) + @Html.ValidationMessageFor(model => model.ShowManufacturersOnHomepage) + + + + + @Html.SmartLabelFor(model => model.ManufacturersBlockItemsToDisplay) + + + @Html.SettingEditorFor(model => model.ManufacturersBlockItemsToDisplay) + @Html.ValidationMessageFor(model => model.ManufacturersBlockItemsToDisplay) + + + + + @Html.SmartLabelFor(model => model.ShowManufacturerPictures) + + + @Html.SettingEditorFor(model => model.ShowManufacturerPictures) + @Html.ValidationMessageFor(model => model.ShowManufacturerPictures) + + + + + @Html.SmartLabelFor(model => model.HideManufacturerDefaultPictures) + + + @Html.SettingEditorFor(model => model.HideManufacturerDefaultPictures) + @Html.ValidationMessageFor(model => model.HideManufacturerDefaultPictures) + + + + + @Html.SmartLabelFor(model => model.HideCategoryDefaultPictures) + + + @Html.SettingEditorFor(model => model.HideCategoryDefaultPictures) + @Html.ValidationMessageFor(model => model.HideCategoryDefaultPictures) + + + + + @Html.SmartLabelFor(model => model.HideProductDefaultPictures) + + + @Html.SettingEditorFor(model => model.HideProductDefaultPictures) + @Html.ValidationMessageFor(model => model.HideProductDefaultPictures) + + + @Html.SmartLabelFor(model => model.EnableHtmlTextCollapser) @@ -322,237 +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.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) -
    -
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    @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() { - +
    + + + + + + + + + + +
    @Html.SmartLabelFor(model => model.ShowProductReviewsInProductLists) @@ -630,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 76180000af..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) @@ -703,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) @@ -771,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 4fcee50d11..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() { @@ -95,6 +98,47 @@ @Html.ValidationMessageFor(model => model.CustomerSettings.CheckUsernameAvailabilityEnabled) + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + +
    @Html.SmartLabelFor(model => model.SearchPageProductsPerPage) @@ -810,7 +930,16 @@
    + + @Html.SmartLabelFor(model => model.SearchDescriptions) + + @Html.SettingEditorFor(model => model.SearchDescriptions) + @Html.ValidationMessageFor(model => model.SearchDescriptions) +

    + @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) @@ -105,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) @@ -177,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) @@ -214,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) @@ -176,6 +196,68 @@ @Html.ValidationMessageFor(model => model.SeoSettings.CanonicalHostNameRule)
    + @Html.SmartLabelFor(model => model.SeoSettings.ExtraRobotsDisallows) + + @Html.TextAreaFor(model => model.SeoSettings.ExtraRobotsDisallows, new { @class = "input-large", style = "height:250px" }) + @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() @@ -355,10 +437,33 @@ @Html.SmartLabelFor(model => model.PdfSettings.LogoPictureId)
    - @Html.SettingEditorFor(model => model.PdfSettings.LogoPictureId, "#pdf-logo-picture") + @Html.SettingEditorFor(model => model.PdfSettings.LogoPictureId, "#pdf-logo-picture", new { transientUpload = true }) @Html.ValidationMessageFor(model => model.PdfSettings.LogoPictureId)
    +
    +
    + @Html.SmartLabelFor(model => model.PdfSettings.AttachOrderPdfToOrderPlacedEmail) + + @Html.SettingEditorFor(model => model.PdfSettings.AttachOrderPdfToOrderPlacedEmail) + @Html.ValidationMessageFor(model => model.PdfSettings.AttachOrderPdfToOrderPlacedEmail) +
    + @Html.SmartLabelFor(model => model.PdfSettings.AttachOrderPdfToOrderCompletedEmail) + + @Html.SettingEditorFor(model => model.PdfSettings.AttachOrderPdfToOrderCompletedEmail) + @Html.ValidationMessageFor(model => model.PdfSettings.AttachOrderPdfToOrderCompletedEmail) +
    } @helper TabLocalizationSettings() @@ -433,9 +538,9 @@ - -
    - + +
    + @@ -551,9 +656,9 @@ - -
    - + +
    + @@ -582,9 +687,6 @@ @Html.ValidationMessageFor(model => model.CompanyInformationSettings.TaxNumber) - - - } @@ -629,9 +731,9 @@ - -
    - + +
    + @@ -670,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/News.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Setting/News.cshtml index cf0d619047..cd65a40c30 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Setting/News.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Setting/News.cshtml @@ -73,6 +73,15 @@ @Html.ValidationMessageFor(model => model.NewsArchivePageSize) + + + @Html.SmartLabelFor(model => model.MaxAgeInDays) + + + @Html.SettingEditorFor(model => model.MaxAgeInDays) + @Html.ValidationMessageFor(model => model.MaxAgeInDays) + + @Html.SmartLabelFor(model => model.ShowHeaderRssUrl) 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/Download.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Download.cshtml index d530182896..04de1aee37 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Download.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Download.cshtml @@ -1,186 +1,149 @@ -@model int +@model int? @using SmartStore.Core; @using SmartStore.Web.Framework.UI; @using SmartStore.Utilities; + +@functions { + bool? _minimalMode = null; + string _fieldName = null; + + private bool MinimalMode + { + get + { + if (!_minimalMode.HasValue) + { + _minimalMode = ViewData.ContainsKey("minimalMode") ? ViewData["minimalMode"].Convert() : false; + } + return _minimalMode.Value; + } + } + + private string FieldName + { + get + { + if (_fieldName == null) + { + _fieldName = ViewData.ContainsKey("fieldName") ? ViewData["fieldName"].Convert() : ViewData.TemplateInfo.GetFullHtmlFieldName(string.Empty); + } + return _fieldName; + } + } +} + @{ - - //other variables - var randomNumber = CommonHelper.GenerateRandomInteger(); - var clientId = "download" + randomNumber; + var clientId = "download-editor-" + CommonHelper.GenerateRandomInteger(); var downloadService = EngineContext.Current.Resolve(); - var download = downloadService.GetDownloadById(Model); - + var download = downloadService.GetDownloadById(Model.GetValueOrDefault()); + var initiallyShowUrlPanel = false; + var hasFile = false; + var downloadUrl = ""; + if (download != null) { + downloadUrl = Url.Action("DownloadFile", "Download", new { downloadId = download.Id }); + initiallyShowUrlPanel = !MinimalMode && download.UseDownloadUrl; + hasFile = !download.UseDownloadUrl; + } + Html.AddScriptParts("~/bundles/fileupload"); Html.AddCssFileParts("~/css/fileupload"); + Html.AddScriptParts(true, "~/Administration/Scripts/smartstore.download.js"); } - -
    @(download != null ? download.Filename : "")
    - -
    - - - - - - - - - - - - - - - - - -
    - - - checked="checked" - } - /> -
    - @T("Admin.Download.DownloadURL"): - - value="@(download.DownloadUrl)" - } - /> - - -
    - @T("Admin.Download.UploadFile"): - -
    - -
    - - @Html.HiddenFor(x => x, new { @class = "hidden"} ) - - - - - - @T("Common.Fileuploader.Upload") - - - - - - -
    - -
    -
    -
    - -
     
    -
    - -
    - - -
    - -
    - -
    -
    +
    + + + + + @if (!MinimalMode) + { + + } + else if (hasFile) + { + + + + } + + +
    + + +
    + + @if (hasFile) + { + + @download.Filename@download.Extension + + } + + + @if (hasFile) + { + + } + else + { + @T("Common.Fileuploader.Upload") + } + + + + + + +
    + +
    +
    +
    + +
     
    +
    +
    + + + @if (!MinimalMode) + { +
    +
    + @{ var value = download != null ? download.DownloadUrl : ""; } + + + + +
    +
    + } + +
    + \ No newline at end of file diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Picture.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Picture.cshtml index d60f7b8d8a..fc793a9bcb 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Picture.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/Picture.cshtml @@ -2,9 +2,23 @@ @using SmartStore.Core; @using SmartStore.Web.Framework.UI; @using SmartStore.Utilities; + +@functions { + private bool TransientUpload + { + get + { + if (ViewData.ContainsKey("transientUpload")) + { + var x = ViewData["transientUpload"].Convert(); + return x; + } + return false; + } + } +} + @{ - - //other variables var random = CommonHelper.GenerateRandomInteger(); var clientId = "picture" + random; var pictureService = EngineContext.Current.Resolve(); @@ -15,7 +29,7 @@ Html.AddCssFileParts("~/css/fileupload"); } -
    +
    @Html.HiddenFor(x => x, new { @class = "hidden"} ) @@ -59,7 +73,7 @@ elRemove = el.find('.remove'); $('#@clientId').fileupload({ - url: '@(Url.Content("~/Admin/Picture/AsyncUpload"))', + url: el.data('upload-url'), dataType: 'json', //acceptFileTypes: /^image\/(gif|jpeg|jpg|png)$/, acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, 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 bab1deb472..feba06977d 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/WidgetZone.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shared/EditorTemplates/WidgetZone.cshtml @@ -11,7 +11,7 @@ } } - + - -
    - - - - - @foreach (var sm in Model.AvailableShippingMethods) - { - - } - - - - @foreach (var c in Model.AvailableCountries) - { - - - @foreach (var sm in Model.AvailableShippingMethods) - { - var restricted = Model.Restricted.ContainsKey(c.Id) && Model.Restricted[c.Id][sm.Id]; - - } - - } - -
    - @T("Admin.Configuration.Shipping.Restrictions.Country") - - - -
    - @c.Name - - -
    -
    - } - - - -} diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Shipping/_CreateOrUpdateMethod.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Shipping/_CreateOrUpdateMethod.cshtml index dbd129ab13..8d63b6e67b 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Shipping/_CreateOrUpdateMethod.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Shipping/_CreateOrUpdateMethod.cshtml @@ -1,72 +1,130 @@ @model ShippingMethodModel @using Telerik.Web.Mvc.UI; + @Html.ValidationSummary(true) @Html.HiddenFor(model => model.Id) -@(Html.LocalizedEditor("shipping-method-localized", - @ - - - - - - - - - - - -
    - @Html.SmartLabelFor(model => model.Locales[item].Name) - - @Html.EditorFor(model => Model.Locales[item].Name) - @Html.ValidationMessageFor(model => model.Locales[item].Name) -
    - @Html.SmartLabelFor(model => model.Locales[item].Description) - - @Html.TextAreaFor(model => model.Locales[item].Description) - @Html.ValidationMessageFor(model => model.Locales[item].Description) -
    - @Html.HiddenFor(model => model.Locales[item].LanguageId) -
    - , - @ - - - - - - - - -
    - @Html.SmartLabelFor(model => model.Name) - - @Html.EditorFor(model => model.Name) - @Html.ValidationMessageFor(model => model.Name) -
    - @Html.SmartLabelFor(model => model.Description) - - @Html.TextAreaFor(model => model.Description) - @Html.ValidationMessageFor(model => model.Description) -
    - )) - - - - - - - - - -
    - @Html.SmartLabelFor(model => model.DisplayOrder) - - @Html.EditorFor(model => model.DisplayOrder) - @Html.ValidationMessageFor(model => model.DisplayOrder) -
    - @Html.SmartLabelFor(model => model.IgnoreCharges) - - @Html.EditorFor(model => model.IgnoreCharges) - @Html.ValidationMessageFor(model => model.IgnoreCharges) -
    + +@Html.SmartStore().TabStrip().Name("shipping-method-edit").Items(x => +{ + x.Add().Text(T("Admin.Common.General").Text).Content(TabGeneral()).Selected(true); + x.Add().Text(T("Admin.Common.Restrictions").Text).Content(TabRestrictions()); + + EngineContext.Current.Resolve().Publish(new TabStripCreated(x, "shipping-method-edit", this.Html, this.Model)); +}) + +@helper TabGeneral() +{ + @(Html.LocalizedEditor("shipping-method-localized", + @ + + + + + + + + + + + +
    + @Html.SmartLabelFor(model => model.Locales[item].Name) + + @Html.EditorFor(model => Model.Locales[item].Name) + @Html.ValidationMessageFor(model => model.Locales[item].Name) +
    + @Html.SmartLabelFor(model => model.Locales[item].Description) + + @Html.EditorFor(model => model.Locales[item].Description, Html.RichEditorFlavor()) + @Html.ValidationMessageFor(model => model.Locales[item].Description) +
    + @Html.HiddenFor(model => model.Locales[item].LanguageId) +
    + , + @ + + + + + + + + +
    + @Html.SmartLabelFor(model => model.Name) + + @Html.EditorFor(model => model.Name) + @Html.ValidationMessageFor(model => model.Name) +
    + @Html.SmartLabelFor(model => model.Description) + + @Html.EditorFor(model => model.Description, Html.RichEditorFlavor()) + @Html.ValidationMessageFor(model => model.Description) +
    + )) + + + + + + + + + + +
    + @Html.SmartLabelFor(model => model.DisplayOrder) + + @Html.EditorFor(model => model.DisplayOrder) + @Html.ValidationMessageFor(model => model.DisplayOrder) +
    + @Html.SmartLabelFor(model => model.IgnoreCharges) + + @Html.EditorFor(model => model.IgnoreCharges) + @Html.ValidationMessageFor(model => model.IgnoreCharges) +
    +} + +@helper TabRestrictions() +{ + if (Model.Id == 0) + { +
    + @T("Admin.Configuration.Restriction.SaveBeforeEdit") +
    + } + else if (Model.FilterConfigurationUrls.Count == 0) + { +
    + @T("Admin.Configuration.Shipping.Methods.RestrictionNote") +
    + } + +
    +
    +
    +} + + \ No newline at end of file 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/Store/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Store/List.cshtml index ff0728c169..b3832779c4 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Store/List.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Store/List.cshtml @@ -23,13 +23,17 @@ .ClientTemplate("\"><#= Name #>"); columns.Bound(x => x.Url) .ClientTemplate("\" target=\"_blank\"><#= Url #>"); + columns.Bound(x => x.Hosts) + .Encoded(false); + columns.Bound(x => x.PrimaryStoreCurrencyName); + columns.Bound(x => x.PrimaryExchangeRateCurrencyName); columns.Bound(x => x.ContentDeliveryNetwork) .ClientTemplate("\" target=\"_blank\"><#= ContentDeliveryNetwork #>"); - columns.Bound(x => x.Hosts); columns.Bound(x => x.SslEnabled) .Template(item => @Html.SymbolForBool(item.SslEnabled)) .ClientTemplate(@Html.SymbolForBool("SslEnabled")) - .Centered(); + .Centered() + .Width(80); columns.Bound(x => x.DisplayOrder) .Centered(); }) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/Store/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Store/_CreateOrUpdate.cshtml index aac7dff4f0..4b3855f9ab 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Store/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Store/_CreateOrUpdate.cshtml @@ -27,7 +27,7 @@ @Html.SmartLabelFor(model => model.LogoPictureId) - @Html.EditorFor(model => model.LogoPictureId, "#store-logo-picture") + @Html.EditorFor(model => model.LogoPictureId, "#store-logo-picture", new { transientUpload = true }) @Html.ValidationMessageFor(model => model.LogoPictureId) @@ -49,6 +49,15 @@ @Html.ValidationMessageFor(model => model.Url) + + + @Html.SmartLabelFor(model => model.Hosts) + + + @Html.TextBoxFor(model => model.Hosts, new { @class = "input-large" }) + @Html.ValidationMessageFor(model => model.Hosts) + + @Html.SmartLabelFor(model => model.ContentDeliveryNetwork) @@ -58,6 +67,24 @@ @Html.ValidationMessageFor(model => model.ContentDeliveryNetwork) + + + @Html.SmartLabelFor(model => model.PrimaryStoreCurrencyId) + + + @Html.DropDownListFor(model => model.PrimaryStoreCurrencyId, Model.AvailableCurrencies) + @Html.ValidationMessageFor(model => model.PrimaryStoreCurrencyId) + + + + + @Html.SmartLabelFor(model => model.PrimaryExchangeRateCurrencyId) + + + @Html.DropDownListFor(model => model.PrimaryExchangeRateCurrencyId, Model.AvailableCurrencies) + @Html.ValidationMessageFor(model => model.PrimaryExchangeRateCurrencyId) + + @Html.SmartLabelFor(model => model.SslEnabled) @@ -76,15 +103,6 @@ @Html.ValidationMessageFor(model => model.SecureUrl) - - - @Html.SmartLabelFor(model => model.Hosts) - - - @Html.TextBoxFor(model => model.Hosts, new { @class = "input-large" }) - @Html.ValidationMessageFor(model => model.Hosts) - - @Html.SmartLabelFor(model => model.HtmlBodyId) 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/Topic/_CreateOrUpdate.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Topic/_CreateOrUpdate.cshtml index e1e0e9610d..f18c32777f 100644 --- a/src/Presentation/SmartStore.Web/Administration/Views/Topic/_CreateOrUpdate.cshtml +++ b/src/Presentation/SmartStore.Web/Administration/Views/Topic/_CreateOrUpdate.cshtml @@ -177,7 +177,7 @@ @Html.SmartLabelFor(model => model.TitleTag) - @Html.DropDownListFor(model => model.TitleTag, Model.AvailableTitleTags) + @Html.DropDownListFor(model => model.TitleTag, Model.AvailableTitleTags, new { @class = "autowidth", data_select_min_results_for_search = 100 }) @Html.ValidationMessageFor(model => model.TitleTag) diff --git a/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/Edit.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/Edit.cshtml new file mode 100644 index 0000000000..4914d5c1ea --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/Edit.cshtml @@ -0,0 +1,24 @@ +@model UrlRecordModel +@using SmartStore.Admin.Models.UrlRecord; +@{ + var title = string.Concat(T("Admin.Common.Edit"), " ", T("Admin.System.SeNames.Name")); + ViewBag.Title = title; +} +@using (Html.BeginForm()) +{ +
    +
    + @title - @Model.Slug @Html.ActionLink("(" + T("Admin.Common.BackToList") + ")", "List") +
    +
    + +  @T("Admin.Common.Entity") + + + + +
    +
    + @Html.Partial("_CreateOrUpdate", Model) +} +@Html.DeleteConfirmation("urlrecord-delete") diff --git a/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/List.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/List.cshtml new file mode 100644 index 0000000000..fa2a560a7d --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/List.cshtml @@ -0,0 +1,247 @@ +@model UrlRecordListModel +@using SmartStore.Admin.Models.UrlRecord; +@using Telerik.Web.Mvc.UI +@{ + ViewBag.Title = T("Admin.System.SeNames").Text; +} + +@using (Html.BeginForm()) +{ +
    +
    + + @T("Admin.System.SeNames") +
    +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + @Html.SmartLabelFor(model => model.SeName) + + @Html.EditorFor(model => Model.SeName) +
    + @Html.SmartLabelFor(model => model.EntityName) + + @Html.EditorFor(model => Model.EntityName) +
    + @Html.SmartLabelFor(model => model.EntityId) + + @Html.EditorFor(model => Model.EntityId) +
    + @Html.SmartLabelFor(model => model.IsActive) + + @Html.EditorFor(model => Model.IsActive) +
    + @Html.SmartLabelFor(x => x.LanguageId) + + @Html.DropDownListFor(x => x.LanguageId, Model.AvailableLanguages, T("Common.Unspecified")) +
    +   + + +
    + +

    + + + + + +
    + @(Html.Telerik().Grid() + .Name("urlrecord-grid") + .Columns(columns => + { + columns.Bound(x => x.Id) + .ClientTemplate("") + .Title("") + .Width(50) + .HtmlAttributes(new { style = "text-align:center" }) + .HeaderHtmlAttributes(new { style = "text-align:center" }); + columns.Bound(x => x.Id) + .Width(100) + .Centered(); + columns.Bound(x => x.Slug) + .ClientTemplate("<#= Slug #>"); + columns.Bound(x => x.SlugsPerEntity) + .Centered() + .Width(180) + .ClientTemplate("<#= SlugsPerEntity #>"); + columns.Bound(x => x.EntityName) + .Width(200); + columns.Bound(x => x.EntityId) + .Width(140) + .Centered() + .ClientTemplate("<#= EntityId #>"); + columns.Bound(x => x.IsActive) + .Template(item => @Html.SymbolForBool(item.IsActive)) + .ClientTemplate(@Html.SymbolForBool("IsActive")) + .Width(100) + .Centered(); + columns.Bound(x => x.Language) + .Width(200); + }) + .Pageable(settings => settings.PageSize(Model.GridPageSize).Position(GridPagerPosition.Both)) + .DataBinding(dataBinding => dataBinding.Ajax().Select("List", "UrlRecord")) + .ClientEvents(events => events.OnDataBinding("onDataBinding").OnDataBound("onDataBound").OnRowDataBound("onRowDataBound")) + .EnableCustomBinding(true)) +
    +} + + \ No newline at end of file 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 new file mode 100644 index 0000000000..ba361be39e --- /dev/null +++ b/src/Presentation/SmartStore.Web/Administration/Views/UrlRecord/_CreateOrUpdate.cshtml @@ -0,0 +1,63 @@ +@model UrlRecordModel +@using SmartStore.Admin.Models.UrlRecord; +@using Telerik.Web.Mvc.UI; + +@Html.ValidationSummary() +@Html.HiddenFor(model => model.Id) + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + @Html.SmartLabelFor(model => model.Id) + + @Html.DisplayFor(model => model.Id) +
    + @Html.SmartLabelFor(model => model.IsActive) + + @Html.EditorFor(model => model.IsActive) + @Html.ValidationMessageFor(model => model.IsActive) +
    + @Html.SmartLabelFor(model => model.Slug) + + @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.EntityName) + + @Html.EditorFor(model => model.EntityName) + @Html.ValidationMessageFor(model => model.EntityName) +
    + @Html.SmartLabelFor(model => model.EntityId) + + @Html.EditorFor(model => model.EntityId) + @Html.ValidationMessageFor(model => model.EntityId) +
    + @Html.SmartLabelFor(x => x.LanguageId) + + @Html.DropDownListFor(x => x.LanguageId, Model.AvailableLanguages) + @Html.ValidationMessageFor(x => x.LanguageId) +
    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/Views/Widget/WidgetsByZone.cshtml b/src/Presentation/SmartStore.Web/Administration/Views/Widget/WidgetsByZone.cshtml deleted file mode 100644 index faa065e000..0000000000 --- a/src/Presentation/SmartStore.Web/Administration/Views/Widget/WidgetsByZone.cshtml +++ /dev/null @@ -1,4 +0,0 @@ -@model List -@foreach (var widget in Model) { - @Html.Action(widget.ActionName, widget.ControllerName, widget.RouteValues) -} \ No newline at end of file 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 a9d0066006..7e1b1648ae 100644 --- a/src/Presentation/SmartStore.Web/Administration/packages.config +++ b/src/Presentation/SmartStore.Web/Administration/packages.config @@ -1,10 +1,11 @@  - - - - + + + + + @@ -14,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 0035df8219..a1501d7653 100644 --- a/src/Presentation/SmartStore.Web/Administration/sitemap.config +++ b/src/Presentation/SmartStore.Web/Administration/sitemap.config @@ -94,6 +94,7 @@ + @@ -107,7 +108,6 @@ - @@ -128,6 +128,10 @@ + + + + @@ -137,13 +141,12 @@ - + - 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 b492367ecf..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 @@ -240,6 +243,9 @@ Land + + Kundennummer + Geburtsdatum @@ -486,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. @@ -594,6 +609,9 @@ Hersteller ('{0}') gelöscht + + Auftrag {0} gelöscht + Produkt ('{0}') gelöscht @@ -783,6 +801,9 @@ Telefonnummer wird benötigt + + * Eingabefelder mit Sternchen sind Pflichfelder und müssen ausgefüllt werden. + Bundesland/Region @@ -1359,6 +1380,9 @@ Die Warengruppe wurde gelöscht. + + Andere Beschreibung anzeigen + Rabatte @@ -1405,10 +1429,10 @@ Gelöscht - Beschreibung + Obere Beschreibung - Beschreibung der Warengruppe + Beschreibung der Warengruppe, die auf der Warengruppenseite oberhalb der Produkte angezeigt wird. Reihenfolge @@ -1467,6 +1491,9 @@ Legt die Vater-Warengruppe fest. Lassen Sie das Feld leer, um eine Warengruppe erster Ebene zu erzeugen. + + Übergeordnete Warengruppe + Bild @@ -1924,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. @@ -1970,7 +2000,7 @@ Checkout-Selling - Hinzufügen + Checkout-Selling-Produkt hinzufügen Produkt @@ -2026,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 @@ -2090,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. @@ -2197,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 @@ -2215,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 @@ -2269,6 +2317,9 @@ Die Länge des Produktes. + + Niedrigster Attributkombinationspreis + Aktion bei Erreichen des Meldebestandes @@ -2338,6 +2389,12 @@ Produktname ist erforderlich + + Summe nicht genehmigter Bewertungen + + + Summe nicht genehmigter Rezensionen + Benachrichtigt den Administrator, wenn die Mindestmenge unterschritten wird. @@ -2560,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 @@ -2825,7 +2894,7 @@ Cross-Selling - Hinzufügen + Cross-Selling-Produkt hinzufügen Reihenfolge @@ -2834,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 @@ -2932,6 +3001,15 @@ Auf Aktualisierung prüfen + + Unbekannter Fehler beim Paket-Download. Bitte versuchen Sie es später erneut. + + + AutoUpdate möglich + + + <p>Dieses Update kann automatisch installiert werden. Hierfür lädt SmartStore.NET ein Installationspaket auf Ihren Webserver herunter, führt die Installation durch und startet die Anwendung neu. Vor der Installation wird der Verzeichnisinhalt Ihres Shops gesichert, mit Ausnahme der Ordner <i>App_Data</i> und <i>Media</i> sowie der SQL Server Datenbank. </p><p>Klicken Sie die Schaltfläche <b>Jetzt aktualisieren</b>, um das Paket downzuloaden und zu installieren. Alternativ hierzu können Sie weiter unten das Paket auf Ihren lokalen PC downloaden und die Installation zu einem späteren Zeitpunkt manuell durchführen.</p> + Aktuelle Version @@ -2947,23 +3025,14 @@ Update verfügbar - - Ihre Version - Jetzt aktualisieren - - AutoUpdate möglich - - - Unbekannter Fehler beim Paket-Download. Bitte versuchen Sie es später erneut. - - - <p>Dieses Update kann automatisch installiert werden. Hierfür lädt SmartStore.NET ein Installationspaket auf Ihren Webserver herunter, führt die Installation durch und startet die Anwendung neu. Vor der Installation wird der Verzeichnisinhalt Ihres Shops gesichert, mit Ausnahme der Ordner <i>App_Data</i> und <i>Media</i> sowie der SQL Server Datenbank. </p><p>Klicken Sie die Schaltfläche <b>Jetzt aktualisieren</b>, um das Paket downzuloaden und zu installieren. Alternativ hierzu können Sie weiter unten das Paket auf Ihren lokalen PC downloaden und die Installation zu einem späteren Zeitpunkt manuell durchführen.</p> + + Ihre Version - Über + Über SmartStore.NET Aktionen @@ -3028,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. @@ -3046,35 +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 - - Excel-Datei + + Fehler + + + Fehler beim Senden einer E-Mail + + + 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 @@ -3091,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 - - Nur Ausgewählte als XML exportieren + + {0} Dateien wurden gelöscht + + + Die Datei muss vom Typ {0} sein. + + + {0} Verzeichnisse wurden gelöscht + + + FTP-Status {0} ({1}). Allgemein @@ -3130,41 +3235,17 @@ Verbergen - - Von CSV-Datei importieren - - - Von Excel importieren - - - Aktiv seit: {0}. - - - Import abbrechen - - - Import wurde abgebrochen + + HTTP-Status {0} ({1}). - - Soll der aktive Importvorgang wirklich abgebrochen werden? Bislang importierte Produkte werden nicht gelöscht. + + Ignorieren - - Vollständigen Bericht runterladen... + + Importdatei - - Der Import läuft jetzt im Hintergrund . Sie können den Fortschritt bzw. das Ergebnis des letzten Importvorganges jederzeit im Import-Dialog einsehen. - - - <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 @@ -3172,6 +3253,9 @@ Info + + Letzte Ausführung + Lizenzieren @@ -3184,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 @@ -3205,6 +3322,9 @@ Sie müssen die Anwendung neu starten, damit die Änderungen wirksam werden. + + Einschränkungen + Speichern @@ -3214,6 +3334,12 @@ Suchen + + Ausgewählte + + + Jetzt senden + Suchmaschinen (SEO) @@ -3223,6 +3349,12 @@ Anzeigen + + Werte für Überspringen und Begrenzen müssen größer oder gleich 0 sein. + + + Ausgelassen + Standard @@ -3253,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. @@ -3274,6 +3418,9 @@ Bitte warten, die Verarbeitung läuft... + + Warnungen + Falsche E-Mail @@ -3286,6 +3433,9 @@ Zugriffsrechte + + Es sind keine Kundengruppen definiert + Zugriffsrecht @@ -3304,9 +3454,6 @@ Filtert Ereignisse nach Ereignistypen. - - Ereignistyp - Nachricht @@ -3331,6 +3478,15 @@ Kunden-E-Mail + + Filtert Ergebnisse nach E-Mail-Adresse der Kunden. + + + Kundensystemkonto + + + Filtert Ergebnisse nach Kundenystemkonten. + Aktivitätstypen @@ -3346,6 +3502,24 @@ Die Ereignistypen wurden erfolgreich bearbeitet. + + Diese Konfiguration für Kindelemente übernehmen + + + Diese Funktion übernimmt die Zugriffsrecht-Konfiguration dieser Warengruppe für alle Unterwarengruppen und Produkte.<br/> + Bitte beachten Sie, dass die Änderungen der Zugriffsrechte zunächst gespeichert werden müssen, <br /> + bevor diese für Unterkategorien und Produkte übernommen werden können. <br /> + <b>Vorsicht:</b> Bitte beachten Sie, <b>dass vorhandene Zugriffsrechte überschrieben bzw. gelöscht werden</b>. + + + Diese Konfiguration für Kindelemente übernehmen + + + Diese Funktion übernimmt die Shop-Konfiguration dieser Warengruppe für alle Unterwarengruppen und Produkte.<br/> + Bitte beachten Sie, dass die Änderungen an der Store-Konfiguration zunächst gespeichert werden müssen, <br /> + bevor diese für Unterkategorien und Produkte übernommen werden können. <br /> + <b>Vorsicht:</b> Bitte beachten Sie, <b>dass vorhandene Store-Konfiguration überschrieben bzw. gelöscht werden</b>. + Content Slider @@ -3490,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. @@ -3578,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 @@ -3631,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 @@ -3698,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 @@ -3721,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 @@ -3889,6 +4069,9 @@ Die E-Mail wurde erfolgreich versandt. + + Test der E-Mail-Funktion. + Das E-Mail-Konto wurde erfolgreich bearbeitet. @@ -3929,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 @@ -3974,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 @@ -4115,7 +4298,7 @@ Zahlung - Das Plugin erlaubt keine Aktivierung dieser Zahlungsmethode. + Das Plugin erlaubt keine Aktivierung dieser Zahlungsart. Zahlungsarten @@ -4127,10 +4310,10 @@ Konfiguration - Wiederkehrender Zahlungstyp + Wiederkehrende Zahlungen - Unterstützt Capture + Buchung möglich Teilerstattung möglich @@ -4139,7 +4322,22 @@ Erstattung möglich - Kann storniert werden + 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 @@ -4270,6 +4468,9 @@ Das Plugin wurde deinstalliert. + + Beim Aufruf eines Plugins ist ein unbekannter Fehler aufgetreten. Details entnehmen Sie bitte der folgenden Meldung. + Verpackungseinheiten @@ -4300,6 +4501,9 @@ Regionale Einstellungen + + Sie müssen zunächst speichern, bevor Sie Einschränkungen festlegen können. + Einstellungen @@ -4339,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 @@ -4361,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 @@ -4420,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 @@ -4474,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 @@ -4549,6 +4783,12 @@ Anzahl der angezeigten Produkt-Tags + + Preisanzeige + + + Legt fest, ob bzw. welcher Typ von Preis in Produktlisten angezeigt werden soll. + Produktdetail @@ -4666,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 @@ -4756,6 +5002,24 @@ Legt fest, ob die Hersteller-Produktnummer im Shop angezeigt werden soll. + + Zeige Herstellerbilder auf der Homepage + + + 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 + + + Bestimmt ob Hersteller auf der Homepage angezeigt werden. + Produktbilder bei der Autovervollständigung anzeigen @@ -4804,6 +5068,18 @@ 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 + + + Legt fest, ob und wo Unterwarengruppen auf einer Warengruppenseite angezeigt werden sollen. + SKU Suche unterdrücken @@ -4978,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 @@ -4996,6 +5278,24 @@ Legt die maximale Länge des angezeigten Benutzernamens fest. + + Kundennummer speichern + + + 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 @@ -5026,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 @@ -5095,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 @@ -5173,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 @@ -5308,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 @@ -5650,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 @@ -5704,6 +6064,12 @@ Lokalisierung + + Meta Robots + + + Legt fest, ob und wie Suchmaschinen die Seiten Ihres Shops indexieren. + Seiten-Titel-Anpassung @@ -5758,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 @@ -5812,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 @@ -5872,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 @@ -5950,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 @@ -5992,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... @@ -6002,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 … @@ -6046,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 @@ -6151,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 @@ -6265,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 @@ -6278,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 @@ -6301,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 @@ -6355,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 @@ -6565,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. @@ -6580,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 @@ -6661,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 @@ -6668,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 @@ -6766,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. @@ -7039,6 +7486,12 @@ Ein Name für die Forengruppe ist erforderlich + + URL-Alias + + + Legt einen Suchmaschinen-freundlichen Seitennamen für das Forum fest. 'Tolles Forum' resultiert bspw. in '~/tolles-forum'. Standard ist der Name des Forums. + Das Forum wurde erfolgreich aktualisiert @@ -7084,6 +7537,12 @@ Ein Name für die Forengruppe ist erforderlich + + URL-Alias + + + Legt einen Suchmaschinen-freundlichen Seitennamen für die Forengruppe fest. 'Tolle Forengruppe' resultiert bspw. in '~/tolle-forengruppe'. Standard ist der Name der Forengruppe. + Die Forengruppe wurde erfolgreich aktualisert. @@ -7111,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 @@ -7144,6 +7621,12 @@ Der Name der Vorlage (Schreibgeschützt) + + Nur manuell senden + + + Legt fest, ob E-Mails, die von dieser Nachrichtenvorlage abgeleitet sind, ausschließlich manuell gesendet werden sollen. + Betreff @@ -7585,6 +8068,9 @@ Zurück zur Gesamtliste + + Die Kundengruppe "{0}" wurde nicht gefunden. + Die Kundengruppe wurde gelöscht. @@ -7768,6 +8254,9 @@ Erstellt am + + Kunden GUID + Kundengruppen @@ -7825,6 +8314,9 @@ IP-Adresse, mit der sich der Kunde zuletzt im Shop aktiv war. + + Ist Systemkonto + Mehrwertsteuerfrei @@ -7837,6 +8329,9 @@ Zeigt an, wann der Kunde zuletzt im Shop aktiv war. + + Letztes Login-Datum + Nachname @@ -7858,6 +8353,9 @@ Passwort des Kunden. + + Passwort Salt + Telefon @@ -8212,25 +8710,748 @@ 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. - + + Trennzeichen + + + Legt das zu verwendende Trennzeichen für die Felder fest. + + + Geben Sie bitte ein gültiges Trennzeichen ein. + + + Inneres Anführungszeichen + + + Legt das innere Anführungszeichen (Escaping) fest. + + + Geben Sie bitte ein gültiges, inneres Anführungszeichen (Escaping) ein. + + + Trennzeichen und inneres Anführungszeichen können in CSV Dateien nicht gleich sein. + + + Anführungszeichen + + + Legt das zu verwendende Anführungszeichen fest. + + + Geben Sie bitte ein gültiges Anführungszeichen ein. + + + Alle Felder in Anführungszeichen + + + Legt fest, ob die Werte aller Felder in Anführungszeichen gestellt werden sollen. + + + Trennzeichen und Anführungszeichen können in CSV Dateien nicht gleich sein. + + + Mehrzeilen erlaubt + + + Legt fest, ob mehrzeilige Feldwerte unterstützt werden. + + + Überflüssige Leerzeichen entfernen + + + Legt fest, ob Leerzeichen am Anfang und am Ende eines Feldwertes entfernt werden sollen. + + + Stapelgröße + + + 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. + + + Ein System-Exportprofil kann nicht gelöscht werden. + + + Nach erfolgreicher Veröffentlichung aufräumen + + + Legt fest, ob nicht mehr benötigte Dateien gelöscht werden sollen, nachdem alle Veröffentlichungen erfolgreich waren. + + + Einstellungen übernehmen von + + + 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 @@ -8386,8 +9607,11 @@ Hilfe + + Dokumentation + - Community-Forum + Community SmartStore.NET ist ein Fork der ASP.NET Open-Source E-Commerce-Lösung {0}. @@ -8434,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 @@ -8441,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 @@ -8465,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 @@ -8474,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} @@ -8483,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 @@ -8548,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 @@ -8555,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 @@ -8585,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 @@ -8759,10 +9995,10 @@ Setzt den Zahlungsstatus auf 'Bezahlt' ohne dabei den Zahlungsanbieter zu kontaktieren. - Auftrags-GUID + Bestellreferenznummer - Interne Referenz für den Auftrag. + Die interne Bestellreferenznummer. Im Gegensatz zur Auftragsnummer existiert diese bereits im Kassenbereich, d.h. vor der eigentlichen Erstelllung des Auftrags. Auftragsnummer @@ -8858,10 +10094,10 @@ Setzt den Zahlungsstatus auf 'Teilweise erstattet' samt Erstattungsbetrag, ohne dabei den Zahlungsanbieter zu kontaktieren. - Zahlungsmethode + Zahlungsart - Die Zahlungsmethode für diese Transaktion + Die Zahlungsart für diese Transaktion Die Zahlungsgebühr (netto) für diesen Auftrag @@ -9100,6 +10336,12 @@ Notiz + + Neue IPN + + + In den Auftragsnotizen ist eine neue Benachrichtigung vom Zahlungsanbieter eingetroffen. + Auftrag als PDF @@ -9502,6 +10744,9 @@ Plugins verwalten + + Bitte beachten Sie, dass der tatsächliche Grundpreis von verschiedenen Faktoren abhängig ist und erst im Shop zuverlässig berechnet werden kann. + Promotion Feeds @@ -9751,8 +10996,8 @@ E-Mail-Adresse erforderlich - - Es wurden {0} E-Mail(s) importiert und {1} aktualisiert. + + Abonnement GUID Bereitstellendes Plugin @@ -10279,6 +11524,9 @@ Fügen Sie die SQL-Abfrage hier ein: + + Die SQL Anweisung wurde erfolgreich ausgeführt. + Ausführen @@ -10291,6 +11539,9 @@ Zurück zur Übersicht + + E-Mail Anhang konnte nicht herunterladen: Daten nicht verfügbar. + Die E-Mail wurde gelöscht @@ -10300,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 @@ -10360,6 +11623,12 @@ Priorität eingeben + + Nur manuell senden + + + Legt fest, ob die E-Mail ausschließlich manuell gesendet werden soll. + Gesendet um @@ -10399,6 +11668,9 @@ Der Name des Empfängers. + + Anzahl Anhänge + Enddatum @@ -10421,7 +11693,7 @@ Lade nur noch nicht gesendete E-Mails - Zeige E-Mails nur aus der Warteschlange. + Lade nur noch nicht gesendete E-Mails. Maximale Sendeversuche @@ -10429,6 +11701,12 @@ Gibt die Anzahl der Zustellversuche an. + + Lade nur manuell zu sendende E-Mails + + + Lade nur manuell zu sendende E-Mails. + Anfangsdatum @@ -10445,82 +11723,169 @@ Wiederholen - Die Nachricht wird erneut gesendet + Die Nachricht wurde erfolgreich neu eingereiht. 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 - - Standard + + Legt die Sprache des SEO Namens fest. + + + Standard + + + SEO Name + + + Legt den SEO Namen fest. + + + Namen pro Objekt + + + Die Anzahl der SEO Namen pro Objekt. + + + Geplante Aufgabe - - Name + + PDF-Konvertierer - - Der zu findende Name. + + Suchmaschine Systeminformation @@ -10555,6 +11920,12 @@ Der Name des Daten-Providers. + + Aufräumen + + + Der Arbeitsspeicher wurde erfolgreich aufgeräumt. + HTTP_HOST @@ -10591,6 +11962,9 @@ Zeitzone des Servers + + Benutzter Speicher (RAM) + GMT/UTC @@ -10600,6 +11974,12 @@ Warnungen + + Zugriffsverweigerung durch anonyme Anfrage bei {0}. + + + Zugriffsverweigerung durch Kunde #{0} '{1}' bei {2}. + Die Standard-Maßeinheit wurde nicht festgelegt. @@ -10618,6 +11998,9 @@ Die Standard-Gewichtseinheit wurde festgelegt. + + Bitte nur Ziffern eingeben. + Alle Verzeichnisberechtigungen sind OK. @@ -10642,11 +12025,20 @@ '{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 gibt keine aktiven Zahlungsmethoden. + Es existieren keine aktiven Zahlungsarten. - Die Zahlungsmethoden sind OK. + Die Zahlungsarten sind OK. Die Hauptwährung für den Shop wurde nicht festgelegt. @@ -10657,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. @@ -10942,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. @@ -11104,6 +12520,9 @@ Zur Kasse + + Ein anonymer Checkout ist nicht zulässig. + Rechnungsanschrift @@ -11128,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. @@ -11150,7 +12578,7 @@ Weiter - Keine Zahlungsmethoden verfügbar. + Es sind keine Zahlungsarten verfügbar. Ihre Bestellung ist angekommen @@ -11168,7 +12596,7 @@ Zahlungsinformation - Zahlungsmethode + Zahlungsarten Bestelldetails @@ -11227,6 +12655,9 @@ Übermittele Auftragsinformationen + + Newsletter abonnieren + Geschäftsbedingungen (AGB) @@ -11311,6 +12742,9 @@ Abgebrochen + + Der API-Aufruf zur Prüfung eines CAPTCHAs ist fehlgeschlagen. + Produkt bearbeiten @@ -11332,6 +12766,15 @@ Fortsetzen + + In die Zwischenablage kopieren + + + Kopieren ist fehlgeschlagen. + + + Kopiert! + Anzahl @@ -11344,6 +12787,9 @@ Querverweise + + Die von Ihnen gewählte Kundennummer existiert bereits. Bitte geben Sie eine andere Kundennummer an. + Datum @@ -11362,33 +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 @@ -11416,12 +12913,21 @@ Dateien für den Upload hier ablegen + + URL eingeben + Fehlgeschlagen Datei hochladen + + Filter + + + Versandkostenfrei + Anzeigename @@ -11437,6 +12943,9 @@ Startseite + + Bild + Importieren @@ -11446,12 +12955,21 @@ Aktiv + + Sprache + Liste + + Lade + Lade nächsten Schritt… + + Diese Funktion steht für Gäste nicht zur Verfügung. + Verschiedenes @@ -11461,6 +12979,9 @@ Mehr Info + + Mein + Navigation @@ -11476,6 +12997,12 @@ Nein, abbrechen + + Es sind keine Dateien vorhanden. + + + Es wurde keine Datei hochgeladen. + Der Vorgang wurde aus Sicherheitsgründen nicht ausgeführt. @@ -11485,6 +13012,9 @@ Benachrichtigung + + Nicht auswählbar + Aus @@ -11500,6 +13030,12 @@ Optional + + Optionen + + + Aufteilung + Capture wird nicht unterstützt @@ -11524,6 +13060,24 @@ Artikel + + Profil + + + Projektion + + + Provider + + + öffentlich + + + Veröffentlicht + + + Veröffentlichung + Frage @@ -11536,9 +13090,21 @@ Entfernen + + Ersetzen + + + Die Anfrage konnte nicht ausgeführt werden.<br />Controller: {0}, Action: {1}, Grund: {2}. + + + Regel + Speichern + + Geplant + Suchen @@ -11572,6 +13138,12 @@ Anzeigen + + Alle anzeigen + + + Mehr anzeigen + Verkleinern @@ -11584,11 +13156,23 @@ Systemname + + Nicht verfügbar + undefiniert + + Unbekannt + + + Unveröffentlicht + + + Ungeplant + - nicht spezifiziert + Nicht spezifiziert Geändert am @@ -11605,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 @@ -11659,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. @@ -11668,6 +13267,9 @@ Währungen + + Der Kunde existiert nicht. + Gast @@ -11689,6 +13291,9 @@ Produkt + + Das Produkt besitzt keine Nutzungsvereinbarung. + Ich bin einverstanden. @@ -11698,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 @@ -11764,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 @@ -11815,6 +13435,15 @@ Jahre + + Über der Produktliste + + + Am Seitenende + + + Nicht anzeigen + Benutze CONTAINS und AND mit prefix_term @@ -11845,6 +13474,27 @@ Benutzernamen anzeigen + + Automatisch vergeben + + + Deaktiviert + + + Aktiviert + + + Anzeigen + + + Immer editierbar + + + Editierbar falls leer + + + Nicht anzeigen + Lesbar @@ -11854,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 @@ -11938,6 +13684,24 @@ Warnung + + Aktiviert anzeigen + + + Deaktiviert anzeigen + + + Nicht anzeigen + + + Aktiviert anzeigen + + + Deaktiviert anzeigen + + + Nicht anzeigen + Abgebrochen @@ -12227,9 +13991,6 @@ Forenthemen mit den neuesten Beiträgen - - {0} - Forum: {1} - Forumname @@ -12470,6 +14231,9 @@ Informationen + + Die Installationssprache '{0}' ist nicht registriert. + Bitte geben Sie eine gültige Kreditkartennummer ein. @@ -12719,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 @@ -12737,6 +14537,12 @@ Geschenkgutschein ({0}) + + Für die wiederkehrende Zahlung existiert kein Ausgangsauftrag. + + + Keine wiederkehrenden Produkte. + Notizen @@ -12746,6 +14552,9 @@ Notiz + + Der Auftrag {0} wurde nicht gefunden. + Auftragsnummer @@ -12762,11 +14571,14 @@ Gesamtsumme - Zahlungsmethode + Zahlungsart Zahlungsgebühr + + bestellung-{0}.pdf + Telefon @@ -12878,6 +14690,9 @@ Versandanschrift + + Die Lieferanschrift fehlt. + Versandart @@ -13028,6 +14843,9 @@ Wunschliste per E-Mail an einen Freund senden + + Das Datum der nächsten Zahlung kann nicht ermittelt werden. + Kartencode @@ -13052,6 +14870,9 @@ Falsche Kartennummer + + Die Zahlungsart konnte nicht geladen werden. + Gültig bis @@ -13061,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 @@ -13178,12 +15017,6 @@ Gewicht - - Gruppierte Produkte - - - Produktset besteht aus - E-Mail @@ -13199,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 @@ -13247,6 +15062,9 @@ Datenschutzerklärung + + Private Nachrichten sind deaktiviert. + Posteingang @@ -13370,11 +15188,17 @@ {0} am Lager. + + Nicht im Sortiment + Nicht am Lager - Grundpreis: {0} pro {1} + Inhalt: {0} {1} ({2} / {3} {1}) + + + {0} {1} ({2} / {3} {1}) Home @@ -13575,7 +15399,10 @@ Dieses Produkt ist ausverkauft. - Dieses Produkt ist ausverkauft. + Keine Bundle-Bestandteile vorhanden + + + Das Produkt {0} wurde nicht gefunden. Preis: @@ -13655,6 +15482,9 @@ Menge + + Die Produktvariante {0} wurde nicht gefunden. + Gewicht @@ -13700,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 @@ -13790,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. @@ -13904,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 @@ -14036,6 +15884,9 @@ Die eingegebene Nummer des Geschenkgutscheines konnte nicht angewendet werden. + + Der Warenkorb ist deaktiviert. + Gesamt @@ -14148,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 @@ -14229,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. @@ -14252,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> @@ -14621,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 @@ -14651,9 +16550,33 @@ Vor einem Jahr + + Sekunde + + + Sek. + + + Sekunden + + + Sek. + Vor {0} Sekunden + + Woche + + + Wochen + + + Jahr + + + Jahre + Vor {0} Jahren @@ -14714,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 9e3f271712..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 @@ -201504232202590_AutoUpdateRes \ 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 45d1081077..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 @@ -240,6 +243,9 @@ Country + + Customer number + Date of birth @@ -486,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 @@ -594,6 +609,9 @@ Deleted a manufacturer ('{0}') + + Deleted order {0} + Deleted a product ('{0}') @@ -783,6 +801,9 @@ Phone number is required. + + * Input elements with asterisk are required and have to be filled out. + State / province @@ -1395,6 +1416,9 @@ The category has been deleted successfully. + + Show other description + Discounts @@ -1444,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 @@ -1506,6 +1530,9 @@ Select a parent category for this category. Leave this field empty to make this the root level category + + Parent category + Picture @@ -1522,7 +1549,7 @@ Published - Check to publish this category (visible in store). Uncheck to unpublish (category not available in store). + Check the box to publish this category (visible in store). Uncheck to unpublish (category not available in store). URL alias @@ -1534,7 +1561,7 @@ Show on home page - Check if you want to show a category on home page. + Check the box if you want to show a category on home page. Subject to ACL @@ -1702,7 +1729,7 @@ Published - Check to publish this manufacturer (visible in store). Uncheck to unpublish (manufacturer not available in store). + Check the box to publish this manufacturer (visible in store). Uncheck to unpublish (manufacturer not available in store). URL alias @@ -1984,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. @@ -2015,7 +2045,7 @@ Copy images - Check to copy the images. + Check the box the box to copy the images. New product name @@ -2033,7 +2063,7 @@ Cross-sells - Add new cross-sell product + Add checkout-selling product Product @@ -2042,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. @@ -2087,7 +2117,7 @@ Allow customer reviews - Check to allow customers to review this product. + Check the box to allow customers to review this product. Allowed quantities @@ -2095,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 @@ -2105,7 +2141,7 @@ Automatically add these products to the cart - Check to automatically add these products to the cart. + Check the box to automatically add these products to the cart. Available end date @@ -2117,7 +2153,7 @@ Available for pre-order - Check if this item is available for Pre-Order. It also displays "Pre-order" button instead of "Add to cart". + Check the box if this item is available for Pre-Order. It also displays "Pre-order" button instead of "Add to cart". Available start date @@ -2159,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. @@ -2192,7 +2228,7 @@ Call for price - Check to show "Call for Pricing" or "Call for quote" instead of price. + Check the box to show "Call for Pricing" or "Call for quote" instead of price. Customer enters price @@ -2210,25 +2246,25 @@ Disable buy button - Check to disable the buy button for this product. This may be necessary for products that are "available upon request". + Check the box to disable the buy button for this product. This may be necessary for products that are "available upon request". Disable wishlist button - Check to disable the wishlist button for this product. + Check the box to disable the wishlist button for this product. Display stock availability - Check to display stock availability. When enabled, customers will see stock availability. + Check the box to display stock availability. When enabled, customers will see stock availability. Display stock quantity - Check to display stock quantity. When enabled, customers will see stock quantity. + Check the box to display stock quantity. When enabled, customers will see stock quantity. Download file @@ -2266,17 +2302,23 @@ 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 if this product has a sample download file that can be downloaded before checkout. + Check the box if this product has a sample download file that can be downloaded before checkout. + + + Has tier prices Has user agreement - Check if the product has a user agreement. + Check the box if the product has a user agreement. Height @@ -2284,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 @@ -2294,7 +2342,7 @@ Downloadable product - Check if this product variant is a downloadable product. When a customer purchases a download product, they can download the item direct from your store by viewing their completed order. + Check the box if this product variant is a downloadable product. When a customer purchases a download product, they can download the item direct from your store by viewing their completed order. Is Electronic Service @@ -2306,19 +2354,19 @@ Free shipping - Check if this product comes with FREE shipping. + Check the box if this product comes with FREE shipping. Is gift card - Check if it is a gift card. + Check the box if this product is a gift card. Recurring product - Check if this product is a recurring product. + Check the box if this product is a recurring product. Shipping enabled @@ -2338,6 +2386,9 @@ The length of the product. + + Lowest attribute combination price + Low stock activity @@ -2407,6 +2458,12 @@ Please provide a name. + + Not approved rating sum + + + Not approved total reviews + Notify admin for quantity below @@ -2474,7 +2531,7 @@ Published - Check to publish this product (visible in store). Uncheck to unpublish (product not available in store). + Check the box to publish this product (visible in store). Uncheck to unpublish (product not available in store). Quantity unit @@ -2507,7 +2564,7 @@ Require other products are added to the cart - Check if this product requires that other products are added to the cart. + Check the box if this product requires that other products are added to the cart. Sample download file @@ -2531,7 +2588,7 @@ Show on home page - Check to display this product on your store's home page. Recommended for your most popular products. + Check the box to display this product on your store's home page. Recommended for your most popular products. SKU @@ -2591,7 +2648,7 @@ Visible individually - Check it if you want this product to be visible in catalog or search results. You can use this field (just uncheck) to hide associated products from catalog and make them accessible only from a parent "grouped" product details page. + Check the box it if you want this product to be visible in catalog or search results. You can use this field (just uncheck) to hide associated products from catalog and make them accessible only from a parent "grouped" product details page. Weight @@ -2632,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 @@ -2771,7 +2840,7 @@ Pictures - Check the images that shows this attribute combination + Check the box of the images that show this attribute combination # @@ -2903,7 +2972,7 @@ Related products - Add new related product + Add cross-selling product Display order @@ -2915,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 @@ -2945,7 +3014,7 @@ Show on product page - Check to display the attribute in the public product detail page + Check the box to display the attribute in the public product detail page Attribute @@ -3025,6 +3094,15 @@ Check for update + + Unknown error during package download. Please try again later. + + + AutoUpdate possible + + + <p>This update can be installed automatically. For this SmartStore.NET downloads an installation package to your webserver, executes it and restarts the application. Before the installation your shop directory is backed up, except the folders <i>App_Data</i> and <i>Media</i>, as well as the SQL Server database file. </p><p>Click the <b>Update now</b> button to download and install the package. As an alternative to this, you can download the package to your local PC further below and perform the installation at a later time manually.</p> + Current version @@ -3040,26 +3118,17 @@ Update available - - Your version - Update now - - AutoUpdate possible - - - Unknown error during package download. Please try again later. - - - <p>This update can be installed automatically. For this SmartStore.NET downloads an installation package to your webserver, executes it and restarts the application. Before the installation your shop directory is backed up, except the folders <i>App_Data</i> and <i>Media</i>, as well as the SQL Server database file. </p><p>Click the <b>Update now</b> button to download and install the package. As an alternative to this, you can download the package to your local PC further below and perform the installation at a later time manually.</p> + + Your version Actions - About + About SmartStore.NET Actions @@ -3124,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. @@ -3142,35 +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 - - Excel file + + Errors + + + Error sending an email + + + PDF Export Please wait while the export is being executed + + Export all + No data to export. - - Export to CSV + + Export selected Export to Excel @@ -3187,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}. - - Export to XML (selected) + + {0} folders were deleted + + + FTP status {0} ({1}). General @@ -3226,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. + + HTTP status {0} ({1}). - - <b>Last import</b>: {0}{1}. + + Ignore - - No report available + + Import file - - {0} of {1} rows processed. - - - {0} new, {1} updated - with {2} warning(s) und {3} error(s). + + Import files Please wait while the import is being executed @@ -3268,6 +3349,9 @@ Info + + Last run + License @@ -3280,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 @@ -3301,6 +3418,9 @@ You must restart the application for the changes to take effect. + + Restrictions + Save @@ -3310,6 +3430,12 @@ Search + + Selected + + + Send now + Search engines (SEO) @@ -3319,6 +3445,12 @@ Show + + Values for skip and limit must be greater than or equal to 0. + + + Skipped + Standard @@ -3349,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 @@ -3370,6 +3514,9 @@ Please wait while processing is running... + + Warnings + Wrong email @@ -3382,6 +3529,9 @@ Access control list + + No customer roles defined + Permission name @@ -3403,9 +3553,6 @@ The Activity Log Type. - - Activity Log Type - Message @@ -3430,6 +3577,15 @@ Customer Email + + Filters results by customer email address. + + + Customer system account + + + Filters results by customer system accounts. + Activity Types @@ -3445,6 +3601,24 @@ The types have been updated successfully. + + Transfer this ACL configuration to children + + + This function assigns the ACL configuration of this category to all subcategories and products included in this category.<br /> + Please keep in mind you have to save changes in the ACL configuration <br/> + before you can assign them to all subcategories and products. <br/> + <b>Attention:</b> Please keep in mind that <b>existing ACL records will be deleted</b> + + + Transfer this store configuration to children + + + This function assigns the store configuration of this category to all subcategories and products included in this category.<br /> + Please keep in mind you have to save changes in the store configuration <br/> + before you can assign them to all subcategories and products. <br/> + <b>Attention:</b> Please keep in mind that <b>existing store mappings will be deleted</b> + Content Slider @@ -3595,6 +3769,9 @@ back to country list + + The country cannot be deleted because it has associated addresses. + The country has been deleted successfully. @@ -3686,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 @@ -3742,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 @@ -3812,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 @@ -3835,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 @@ -3959,7 +4139,7 @@ SSL - Check to use Secure Sockets Layer (SSL) to encrypt the SMTP connection. + Check the box to use Secure Sockets Layer (SSL) to encrypt the SMTP connection. Host @@ -3998,7 +4178,7 @@ Use default credentials - Check to use default credentials for the connection + Check the box to use default credentials for the connection User @@ -4015,6 +4195,9 @@ Email has been successfully sent. + + Testing email functionality. + The email account has been updated successfully. @@ -4061,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 @@ -4094,7 +4277,7 @@ Right-to-left - Check to enable right-to-left support for this language. The active theme should support RTL (have appropriate CSS style file). And it affects only public store. + Check the box to enable right-to-left support for this language. The active theme should support RTL (have appropriate CSS style file). And it affects only public store. Unique SEO code @@ -4106,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 @@ -4271,19 +4454,34 @@ Display order - Recurring support + Recurring payments Supports capture - Partial refund + Supports partial refund - Refund + Supports refund - Void + 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 @@ -4423,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 @@ -4453,6 +4654,9 @@ Regional Settings + + You need to save before you can specify restrictions. + Settings @@ -4487,19 +4691,25 @@ Allow not registered users to leave comments - Check to allow not registered users to leave comments. + Check the box to allow not registered users to leave comments. Blog enabled - Check to enable the blog in your store. + 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 - Check to notify store owner about new blog comments. + Check the box to notify store owner about new blog comments. Number of tags (cloud) @@ -4517,7 +4727,7 @@ Display blog RSS feed link in the browser address bar - Check 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 @@ -4526,31 +4736,31 @@ Allow anonymous users to email a friend - Check if you want to allow anonymous users to email a friend. + Check the box if you want to allow anonymous users to email a friend. Allow anonymous users to write product reviews - Check to allow anonymous users to write product reviews. + Check the box to allow anonymous users to write product reviews. Allow product sorting - Check to enable product sorting option on category/manufacturer details page. + Check the box to enable product sorting option on category/manufacturer details page. Allow view mode changing - Check to enable the option to change view mode on category/manufacturer details page. + Check the box to enable the option to change view mode on category/manufacturer details page. 'Ask question' enabled - Check to allow customers to send an inquiry concerning a product + Check the box to allow customers to send an inquiry concerning a product Base price for bundle items @@ -4562,20 +4772,26 @@ Category breadcrumb enabled - Check to show category breadcrumb. + Check the box the box to show category breadcrumb. 'Compare Products' enabled - Check to allow customers to use the 'Compare Products' option in your store + 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 @@ -4598,13 +4814,13 @@ 'Email a friend' enabled - Check to allow customers to use the 'Email a friend' option in your store + Check the box to allow customers to use the 'Email a friend' option in your store Enable dynamic price update - Check if you want to enable dynamic price update on product details page in case a product has product attributes with price adjustments. + Check the box if you want to enable dynamic price update on product details page in case a product has product attributes with price adjustments. Truncate long texts @@ -4628,7 +4844,25 @@ Hide buy-button in product lists - Check to hide the buy-button in product lists. + 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 @@ -4640,13 +4874,13 @@ Ignore discounts (sitewide) - Check to ignore discounts (sitewide). It can significantly improve performance. + Check the box to ignore discounts (sitewide). It can significantly improve performance. Ignore featured products (sitewide) - Check to ignore featured products (sitewide). It can significantly improve performance. + Check the box to ignore featured products (sitewide). It can significantly improve performance. Show featured products in lists @@ -4658,13 +4892,13 @@ Include full description in compare products - Check to display product full description on the compare products page. + Check the box to display product full description on the compare products page. Include short description in compare products - Check to display product short description on the compare products page. + Check the box to display product short description on the compare products page. Label product as "new" for max. [x] days @@ -4691,7 +4925,7 @@ Notify about new product reviews - Check to notify store owner about new product reviews. + Check the box to notify store owner about new product reviews. Number of best sellers on home page @@ -4705,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 @@ -4715,13 +4955,13 @@ Product reviews must be approved - Check if product reviews must be approved by administrator. + Check the box if product reviews must be approved by administrator. 'Products also purchased' enabled - Check to allow customers to view a list of products purchased by other customers who purchased the above + Check the box to allow customers to view a list of products purchased by other customers who purchased the above Number of also purchased products to display @@ -4757,7 +4997,7 @@ Search autocomplete enabled - Check to enabled autocomplete in the search box. + Check the box to enabled autocomplete in the search box. Number of 'autocomplete' products to display @@ -4790,7 +5030,7 @@ 'Recently added products' enabled - Check to allow customers to use the 'Recently added products' feature in your store + Check the box to allow customers to use the 'Recently added products' feature in your store Number of 'Recently added products' @@ -4814,7 +5054,7 @@ 'Recently viewed products' enabled - Check to allow customers to use the 'Recently viewed products' feature in your store + Check the box to allow customers to use the 'Recently viewed products' feature in your store Number of 'Recently viewed products' @@ -4822,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 @@ -4838,19 +5084,19 @@ Show best sellers on home page - Check to show best sellers on home page. + Check the box to show best sellers on home page. Show the number of distinct products besides each category - Check to show the number of products besides each category (category navigation block). + Check the box to show the number of products besides each category (category navigation block). Include subcategories (number of distinct products) - Check to including when showing the number of products besides each category. + Check the box to include subcategories when showing the number of products besides each category. Show color squares in product lists @@ -4880,7 +5126,7 @@ Display dimensions - Check to display dimensions. When enabled, customers will see the dimensions of the product. + Check the box to display dimensions. When enabled, customers will see the dimensions of the product. Show discount sign @@ -4892,7 +5138,7 @@ Show GTIN - Check to show GTIN in public store. + Check the box to show GTIN in public store. Show image of linked product @@ -4910,7 +5156,25 @@ Show manufacturer part number - Check to show manufacturer part numbers in public store. + Check the box to show manufacturer part numbers in public store. + + + Show manufacturer pictures on homepage + + + 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 + + + Specifies whether manufacturers will be displayed on the homepage. Show product images in autocomplete box @@ -4934,19 +5198,19 @@ Include products from subcategories - Check if you want a category details page to include products from subcategories. + Check the box if you want a category details page to include products from subcategories. Show SKU - Check to show product SKU in public store. + Check the box to show product SKU in public store. Show a share button - Check to show share button on product details page. + Check the box to show share button on product details page. Show variant combination price adjustments @@ -4958,13 +5222,25 @@ Display weight - Check to display the weight. When enabled, customers will see the weight of the product. + 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 + + + Indicates whether and where to show subcategories on a category page. Suppress SKU search - Check to disable the searching of product SKUs. This setting can increase the performance of searching. + Check the box to disable the searching of product SKUs. This setting can increase the performance of searching. Customers @@ -4985,7 +5261,7 @@ 'City' required - Check if 'City' is required. + Check the box if 'City' is required. 'Company' enabled @@ -4997,7 +5273,7 @@ 'Company' required - Check if 'Company' is required. + Check the box if 'Company' is required. 'Country' enabled @@ -5018,7 +5294,7 @@ 'Fax number' required - Check if 'Fax number' is required. + Check the box if 'Fax number' is required. 'Phone number' enabled @@ -5030,7 +5306,7 @@ 'Phone number' required - Check if 'Phone number' is required. + Check the box if 'Phone number' is required. 'State/province' enabled @@ -5048,7 +5324,7 @@ 'Street address 2' required - Check if 'Street address 2' is required. + Check the box if 'Street address 2' is required. 'Street address' enabled @@ -5060,7 +5336,7 @@ 'Street address' required - Check if 'Street address' is required. + Check the box if 'Street address' is required. 'Zip / postal code' enabled @@ -5072,13 +5348,13 @@ 'Zip / postal code' required - Check if 'Zip / postal code' is required. + Check the box if 'Zip / postal code' is required. Allow customers to select time zone - Check to allow customers to select time zone. If checked, then time zone can be selected on the public store (account page). If not, then default time zone will be used. + Check the box to allow customers to select a time zone. If checked, then the time zone can be selected on the public store (account page). If not, then the default time zone will be used. Allow customers to upload avatars @@ -5114,7 +5390,7 @@ 'City' required - Check if 'City' is required. + Check the box if 'City' is required. 'Company' enabled @@ -5126,7 +5402,7 @@ 'Company' required - Check if 'Company' is required. + Check the box if 'Company' is required. 'Country' enabled @@ -5134,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 @@ -5152,6 +5434,24 @@ Determines the maximum length of the displayed customer name. + + Save customer number + + + 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 @@ -5182,11 +5482,23 @@ 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 - Check to enable auto registration when using external authentication. + Check the box to enable auto registration when using external authentication. External authentication settings @@ -5201,7 +5513,7 @@ 'Fax number' required - Check if 'Fax number' is required + Check the box if 'Fax number' is required 'Gender' enabled @@ -5213,19 +5525,19 @@ Hide 'Back in stock subscriptions' tab - Check to hide 'Back in stock subscriptions' tab on 'My account' page + Check the box to hide 'Back in stock subscriptions' tab on 'My account' page Hide 'Downloadable products' tab - Check to hide 'Downloadable products' tab on 'My account' page + Check the box to hide 'Downloadable products' tab on 'My account' page Hide newsletter box - Check if you want to hide the newsletter subscription box. + Check the box if you want to hide the newsletter subscription box. 'Newsletter' enabled @@ -5249,7 +5561,13 @@ 'Phone number' required - Check if 'Phone number' is required. + 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 @@ -5285,7 +5603,7 @@ 'Street address 2' required - Check if 'Street address 2' is required. + Check the box if 'Street address 2' is required. 'Street address' enabled @@ -5297,19 +5615,19 @@ 'Street address' required - Check if 'Street address' is required. + Check the box if 'Street address' is required. 'Usernames' enabled - Check to use usernames for login/registration instead of emails. WARNING: not recommended to change in production environment. + Check the box to use usernames for login/registration instead of E-Mails. WARNING: not recommended to change in production environment. Registration method - Determines customer registration method. Standard - mode where visitors can register and no approval is required. Email Validation - mode where user must respond to validation email that is sent to them before they are activated. Admin Approval - mode where visitors can register but admin approval is required. Disabled - mode where registration is disabled + Determines customer registration method. Standard - mode where visitors can register and no approval is required. E-mail Validation - mode where user must respond to validation email that is sent to them before they are activated. Admin Approval - mode where visitors can register but admin approval is required. Disabled - mode where registration is disabled Validate customer email address @@ -5327,7 +5645,25 @@ 'Zip / postal code' required - Check if 'Zip / postal code' is required. + 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 @@ -5360,7 +5696,7 @@ Allow customers to manage forum subscriptions - Check if you want to allow customers to manage forum subscriptions + Check the box if you want to allow customers to manage forum subscriptions Allow guests to create posts @@ -5402,7 +5738,7 @@ Forums enabled - Check to enable forums. + Check the box to enable forums. Notify about private messages @@ -5473,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 @@ -5528,7 +5882,7 @@ CAPTCHA enabled - Check to enable CAPTCHA + Check the box to enable CAPTCHA Captcha is enabled but the appropriate keys are not entered. @@ -5537,55 +5891,55 @@ Show on 'ask question' page - Check to show CAPTCHA on 'ask question' page + Check the box the box to show CAPTCHA on 'ask question' page Show on blog page (comments) - Check to show CAPTCHA on blog page when writing a comment. + Check the box to show CAPTCHA on blog page when writing a comment. Show on contact us page - Check to show CAPTCHA on contact us page. + Check the box to show CAPTCHA on contact us page. Show on 'email product to a friend' page - Check to show CAPTCHA on 'email product to a friend' page. + Check the box to show CAPTCHA on 'email product to a friend' page. Show on 'email wishlist to a friend' page - Check to show CAPTCHA on 'email wishlist to a friend' page. + Check the box to show CAPTCHA on 'email wishlist to a friend' page. Show on login page - Check to show CAPTCHA on login page. + Check the box to show CAPTCHA on login page. Show on news page (comments) - Check to show CAPTCHA on news page when writing a comment. + Check the box to show CAPTCHA on news page when writing a comment. Show on product reviews page - Check to show CAPTCHA on product reviews page when writing a review. + Check the box to show CAPTCHA on product reviews page when writing a review. Show on registration page - Check to show CAPTCHA on registration page. + Check the box to show CAPTCHA on registration page. Company @@ -5771,7 +6125,7 @@ Convert non-western chars - Check to take out the accent marks in the letters of SEO names while keeping the letter. + Check the box to take out the accent marks in the letters of SEO names while keeping the letter. Default language redirect behaviour @@ -5818,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 @@ -5872,6 +6232,12 @@ Localization settings + + Meta robots + + + Specifies if and how search engines indexing the pages of your store. + Page title SEO adjustment @@ -5888,7 +6254,7 @@ Enabled - Check to enabled PDF. + Check the box to enabled PDF. Use Letter page size @@ -5926,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 @@ -5969,22 +6341,28 @@ Store closed - Check to close the store. Uncheck to re-open. + Check the box to close the store. Uncheck to re-open. Allow an admin to view the closed store - Check to allow a user with admin access to view the store while it is set to closed. + Check the box to allow a user with admin access to view the store while it is set to closed. Store information + + Check string + + + Enter any string to check the SEO name creation. Changed settings must be saved before. + Use images for language selection - Check if you want to use images for language selection. + Check the box if you want to use images for language selection. Media settings @@ -6040,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 @@ -6104,13 +6488,13 @@ Allow not registered users to leave comments - Check to allow not registered users to leave comments. + Check the box to allow not registered users to leave comments. News enabled - Check to enable the news in your store. + Check the box to enable the news in your store. Number of items to display @@ -6118,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 @@ -6128,19 +6518,19 @@ Notify about new news comments - Check to notify store owner about new news comments. + Check the box to notify store owner about new news comments. Display news RSS feed link in the browser address bar - Check to enable the news RSS feed link in customers browser address bar + Check the box to enable the news RSS feed link in customers browser address bar Show on home page - Check to display your news items on your store home page. + Check the box to display your news items on your store home page. No setting could be loaded with the specified ID. @@ -6152,7 +6542,7 @@ Anonymous checkout allowed - Check to enable anonymous checkout (customers are not required to login/register when purchasing products) + Check the box to enable anonymous checkout (customers are not required to login/register when purchasing products) Disable "Order completed" page @@ -6160,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 @@ -6182,7 +6578,7 @@ Is re-order allowed - Check if you want to allow customers to make re-orders. + Check the box if you want to allow customers to make re-orders. Min order sub-total amount @@ -6214,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 @@ -6239,7 +6641,7 @@ Enable Returns System - Check if you want to allow customers to submit return requests for items they've previously purchased. + Check the box if you want to allow customers to submit return requests for items they've previously purchased. Return request settings @@ -6272,7 +6674,7 @@ Enabled - Check if you want to enable the Reward Points Program. + Check the box if you want to enable the Reward Points Program. Exchange rate @@ -6319,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 @@ -6326,25 +6734,25 @@ Display shipment events - Check if you want your customers to see shipment events on their shipment details pages (if supported by your shipping rate computation method). + Check the box if you want your customers to see shipment events on their shipment details pages (if supported by your shipping rate computation method). Estimate shipping enabled - Check to allow customers to estimate shipping on shopping cart page + Check the box to allow customers to estimate shipping on shopping cart page Free shipping over 'X' - Check to enable free shipping for all orders over 'X'. Set the value for X below. + Check the box to enable free shipping for all orders over 'X'. Set the value for X below. Calculate 'X' including tax - Check to calculate 'X' value including tax; otherwise excluding tax. + Check the box to calculate 'X' value including tax; otherwise excluding tax. Value of 'X' @@ -6365,13 +6773,13 @@ Allow guests to email their wishlists - Check to allow guests to email their wishlists to friends. + Check the box to allow guests to email their wishlists to friends. Allow 'out of stock' items to be added to wishlist - Check to allow 'out of stock' products to be added to wishlist. + Check the box to allow 'out of stock' products to be added to wishlist. Shopping cart @@ -6401,7 +6809,7 @@ Allow customers to email their wishlists - Check to allow customers to email their wishlists to friends. + Check the box to allow customers to email their wishlists to friends. Maximum shopping cart items @@ -6419,7 +6827,7 @@ Show mini-shopping cart - Check to enable mini-shopping cart. + Check the box to enable mini-shopping cart. Mini-shopping cart product number @@ -6431,7 +6839,16 @@ Move items from wishlist to cart - Check to move products from wishlist to the cart when clicking "Add to cart" button. Otherwise, they are copied. + 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 @@ -6446,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 @@ -6467,13 +6884,19 @@ Show discount box - Check if you want the discount coupon box to be displayed on shopping cart page + 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 - Check if you want the gift card coupon box to be displayed on shopping cart page + Check the box if you want the gift card coupon box to be displayed on shopping cart page Show quantity of linked product @@ -6523,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 @@ -6566,19 +7004,19 @@ Allow VAT exemption - Check if this store will exempt eligible VAT-registered customers from VAT. + Check the box if this store will exempt eligible VAT-registered customers from VAT. Notify admin when a new VAT number is submitted - Check if you want to receive a notification (email) when a new VAT number is submitted. + Check the box if you want to receive a notification (email) when a new VAT number is submitted. EU VAT enabled - Check to enable EU VAT (the European Union Value Added Tax) + Check the box to enable EU VAT (the European Union Value Added Tax) Your shop country @@ -6590,7 +7028,7 @@ Use web service - Check if you want to use the EU web service to validate VAT numbers. WARNING: If this option is enabled, then DO NOT disable country form field available during registration (public store). + Check the box if you want to use the EU web service to validate VAT numbers. WARNING: If this option is enabled, then DO NOT disable country form field available during registration (public store). Hide tax in order summary @@ -6650,25 +7088,25 @@ Show legal information in footer. - Check to show legal information in footer. + Check the box to show legal information in footer. Show legal information in product detail page. - Check to show legal information in product detail page. + Check the box to show legal information in product detail page. Show legal information in product grid. - Check to show legal information in product grid. + Check the box to show legal information in product grid. Show legal information in product list. - Check to show legal information in product list. + Check the box to show legal information in product list. Tax based on @@ -6736,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. @@ -6754,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 @@ -6839,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 @@ -6846,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 if your store will be SSL secured. + Specifies whether the store should be SSL secured. Store logo @@ -6947,6 +7390,9 @@ Name + + Theme requires no configuration + Variables were successfully updated. @@ -6975,7 +7421,7 @@ Allow customers to select a theme - Check to allow customers to select a store theme. + Check the box to allow customers to select a store theme. Enable asset bundling @@ -6999,7 +7445,7 @@ Mobile devices supported - Check to enable mobile devices support. + Check the box to enable mobile devices support. Save theme choice in cookie @@ -7235,6 +7681,12 @@ Forum group name is required. + + URL-Alias + + + Set a search engine friendly page name e.g. 'the-best-forum' to make your page URL 'http://www.yourStore.com/the-best-forum'. Leave empty to generate it automatically based on the name of the forum. + The forum has been updated successfully. @@ -7283,6 +7735,12 @@ Forum Group Name is required. + + URL-Alias + + + Set a search engine friendly page name e.g. 'the-best-forumgroup' to make your page URL 'http://www.yourStore.com/the-best-forumgroup'. Leave empty to generate it automatically based on the name of the forum group. + The forum group has been updated successfully. @@ -7313,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 @@ -7346,6 +7822,12 @@ The name of this template (read only). + + Only send manually + + + Indicates whether emails derived from this message template should only be send manually. + Subject @@ -7545,7 +8027,7 @@ Allow guests to vote - Check to allow guests to vote. + Check the box to allow guests to vote. Display order @@ -7584,7 +8066,7 @@ Show on home page - Check if you want to show poll on home page. + Check the box if you want to show poll on home page. Start date @@ -7635,13 +8117,13 @@ Include in sitemap - Check to include this topic in the sitemap. + Check the box to include this topic in the sitemap. Password protected - Check if this topic is password protected + Check the box if this topic is password protected Meta description @@ -7817,6 +8299,9 @@ back to customer role list + + The customer role "{0}" cannot be found. + The customer role has been deleted successfully. @@ -7833,13 +8318,13 @@ System customer roles can't be disabled. - Check to make this role active. + Check the box to make this role active. Free shipping - Check to allow customers in this role to get free shipping on their orders. + Check the box to allow customers in this role to get free shipping on their orders. Is system role @@ -7872,7 +8357,7 @@ Tax exempt - Check to allow customers in this role to make tax free purchases. + Check the box to allow customers in this role to make tax free purchases. The customer role has been updated successfully. @@ -8009,6 +8494,9 @@ The date the customer record is created. + + Customer GUID + Customer roles @@ -8066,6 +8554,9 @@ The last used IP address. + + Is system account + Is tax exempt @@ -8078,6 +8569,9 @@ The last activity date. + + Last login date + Last name @@ -8099,6 +8593,9 @@ The customer's password. + + Password salt + Phone @@ -8471,29 +8968,752 @@ 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. + + + Please enter a valid delimiter. + + + Inner quote character + + + Specifies the inner quote character used for escaping. + + + Please enter a valid inner quote character (escaping). + + + Delimiter and inner quote character cannot be equal in CSV files. + + + Quote character + + + Specifies the quotation character. + + + Please enter a valid quote character. + + + Quote all fields + + + Specifies whether to set quotation marks around all field values. + + + Delimiter and quote character cannot be equal in CSV files. + + + Supports multilines + + + Specifies whether field values with multilines are supported. + + + Trim values + + + Specifies whether to remove space characters at start and end of a field value. + + + Batch size + + + Specifies the maximum number of records per export file. 0 is the default and means that all the records are exported in one file. + + + Cannot delete a system export profile. + + + Clean up after successful deployment + + + Specifies whether to delete unneeded files after all deployments succeeded. + + + Apply settings from + + + Specifies an export profile from which to apply the settings. + + + This is an automatic notification of store "{0}" about a recent data export. + + + 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 @@ -8657,8 +9877,11 @@ Help + + Documentation + - Community forums + Community SmartStore.NET is a fork of the ASP.NET open source e-commerce solution {0}. @@ -8708,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 @@ -8715,7 +9944,7 @@ Order has been captured - Error capturing order #{0}. Error: {1} + Unable to capture payment for order {0}. Order has been deleted @@ -8739,7 +9968,7 @@ Order has been partially refunded. Amount = {0} - Unable to partially refund order. {0} + Unable to partially refund order {0}. Order placed @@ -8748,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} @@ -8757,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 @@ -8828,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 @@ -9042,10 +10277,10 @@ Sets the payment status to 'paid' without contacting the payment provider. - Order GUID + Order reference number - Internal reference for order. + The internal order reference number. In contrast to the order number it already exists during checkout that is before order creation. Order Number @@ -9389,6 +10624,12 @@ Note + + New IPN + + + A new notification from the payment provider has arrived in the order notes. + Order as PDF @@ -9800,6 +11041,9 @@ Manage plugins + + Please keep in mind, the base price is depending on several factors and will therefore only be calculated reliable in the front end. + Promotion feeds @@ -10058,8 +11302,8 @@ Email is required. - - {0} email(s) were imported and {1} updated. + + Subscription GUID Providing plugin @@ -10607,6 +11851,9 @@ Insert your SQL query here + + The SQL command was executed successfully. + Execute @@ -10619,6 +11866,9 @@ back to message queue + + Could not download e-mail attachment: no data. + The queued email has been deleted successfully. @@ -10628,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 @@ -10691,6 +11953,12 @@ Enter priority. + + Only send manually + + + Indicates whether the email should only be send manually. + Sent on @@ -10733,6 +12001,9 @@ End date + + Number of attachments + End date @@ -10755,7 +12026,7 @@ Load not sent emails only - Only load emails into queue that have not yet been sent. + Load not sent emails only. Maximum send attempts @@ -10763,6 +12034,12 @@ The maximum number of attempts to send a message. + + Load emails manually send only + + + Load emails manually send only. + Start date @@ -10784,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 @@ -10889,6 +12253,12 @@ The name of the data provider. + + Collect + + + The memory has been successfully cleaned up. + HTTP_HOST @@ -10925,6 +12295,9 @@ Server time zone + + Used memory (RAM) + Greenwich Mean Time (GMT/UTC) @@ -10934,6 +12307,12 @@ Warnings + + Access denied to anonymous request on {0}. + + + Access denied to user #{0} '{1}' on {2}. + Default dimension is not set @@ -10958,6 +12337,9 @@ Default weight is set + + Please enter digits only. + All directory permissions are OK @@ -10991,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 @@ -11015,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 @@ -11318,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. @@ -11480,6 +12895,9 @@ Checkout + + An anonymous checkout is not allowed. + Billing address @@ -11504,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). @@ -11603,6 +13030,9 @@ Submitting order information... + + Subscribed to newsletter + Terms of service @@ -11687,6 +13117,9 @@ Cancelled + + The API call to verify a CAPTCHA has failed. + Edit product @@ -11708,6 +13141,15 @@ Continue + + Copy to clipboard + + + Failed to copy. + + + Copied! + Count @@ -11720,6 +13162,9 @@ Cross references + + Customer number already exists, please choose another. + Date @@ -11738,33 +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 @@ -11792,12 +13288,21 @@ Drop files here to upload + + Enter URL + Failed Upload a file + + Filter + + + Free shipping + Friendly name @@ -11813,6 +13318,9 @@ Home + + Image + Import @@ -11822,12 +13330,21 @@ Is active + + Language + List + + Loading + Loading next step... + + This function is not available for guests. + Miscellaneous @@ -11837,6 +13354,9 @@ More info + + My + Navigation @@ -11852,6 +13372,12 @@ No, cancel + + There are no files available. + + + There was no file uploaded. + The operation was not carried out for security reasons. @@ -11861,6 +13387,9 @@ Alert + + Not selectable + Off @@ -11876,6 +13405,12 @@ Optional + + Options + + + Partition + Capture method not supported @@ -11900,6 +13435,24 @@ Product + + Profile + + + Projection + + + Provider + + + public + + + Published + + + Publishing + Question @@ -11912,9 +13465,21 @@ Remove + + Replace + + + The request could not be processed.<br />Controller: {0}, Action: {1}, Reason: {2}. + + + Rule + Save + + Scheduled + Search @@ -11948,6 +13513,12 @@ Show + + Show all + + + Show more + Shrink @@ -11960,9 +13531,21 @@ System name + + Unavailable + Undefined + + Unknown + + + Unpublished + + + Unscheduled + Unspecified @@ -11981,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 @@ -12035,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. @@ -12044,6 +13642,9 @@ Currencies + + The customer does not exist. + Guest @@ -12065,6 +13666,9 @@ Product + + The product has no user agreement. + I agree @@ -12074,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 @@ -12137,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 @@ -12188,6 +13807,15 @@ Years + + Above product list + + + At bottom of page + + + Do not display + Using CONTAINS and AND with prefix_term @@ -12218,6 +13846,27 @@ Show usernames + + Automatically assigned + + + Disabled + + + Enabled + + + Display + + + Always editable + + + Editable if empty + + + Do not display + Clear @@ -12227,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 @@ -12311,6 +14056,24 @@ Warning + + Show activated + + + Show deactivated + + + Do not show + + + Show activated + + + Show deactivated + + + Do not show + Cancelled @@ -12577,9 +14340,6 @@ Forum's topics with newest posts. - - {0} - Forum: {1} - Forum Name @@ -12820,6 +14580,9 @@ Information + + The install language '{0}' is not registered. + Please enter a valid credit card number. @@ -13069,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 @@ -13087,6 +14886,12 @@ Gift card ({0}) + + No initial order exists for the recurring payment. + + + There are no recurring products. + Note(s) @@ -13096,6 +14901,9 @@ Note + + The order {0} was not found. + Order # @@ -13117,6 +14925,9 @@ Payment method additional fee + + order-{0}.pdf + Phone @@ -13228,6 +15039,9 @@ Shipping Address + + The shipping address is missing. + Shipping Method @@ -13378,6 +15192,9 @@ Email wishlist to a friend + + The next payment date could not be calculated. + Card code @@ -13402,6 +15219,9 @@ Wrong card number + + The payment method could not be loaded. + Valid until @@ -13411,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 @@ -13528,12 +15366,6 @@ Weight - - Associated products - - - Bundled items - Mail @@ -13549,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. @@ -13597,6 +15411,9 @@ Privacy Notice + + Private messages are disabled. + Inbox @@ -13720,11 +15537,17 @@ {0} in stock + + Not in assortment + Out of stock - base price: {0} per {1} + Content: {0} {1} ({2} / {3} {1}) + + + {0} {1} ({2} / {3} {1}) Top @@ -13925,7 +15748,10 @@ This product is sold out. - This product is sold out. + No bundle items available + + + The product {0} was not found. Price: @@ -14002,6 +15828,9 @@ Quantity + + The product variant {0} was not found. + Weight @@ -14047,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 @@ -14137,6 +15969,9 @@ You cannot vote for your own review + + The product review {0} was not found. + Only registered users can write reviews @@ -14251,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 @@ -14383,6 +16230,9 @@ The coupon code you entered couldn't be applied to your order + + The shoping cart is disabled. + Total @@ -14495,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 @@ -14576,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 @@ -14599,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> @@ -14968,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 @@ -14998,9 +16896,33 @@ one year ago + + Second + + + sec + + + Seconds + + + sec + {0} seconds ago + + Week + + + Weeks + + + Year + + + Years + {0} years ago @@ -15061,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 9e3f271712..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 @@ -201504232202590_AutoUpdateRes \ 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/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 4100e82a92..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) @@ -386,7 +389,8 @@ th label { vertical-align: text-top; } -.label-smnet { +.label-smnet, +.label-inline { margin-right: 5px; } .label-smnet-hide { @@ -417,51 +421,6 @@ th label { } -/*.ui-menu { - padding: 5px 0; - margin: 2px 0 0; // override default ul - background-color: @dropdownBackground; - border: 1px solid #ccc; // Fallback for IE7-8 - border: 1px solid @dropdownBorder; - *border-right-width: 2px; - *border-bottom-width: 2px; - border-top: none; - .border-radius(6px); - .box-shadow(0 5px 10px rgba(0,0,0,.2)); - -webkit-background-clip: padding-box; - -moz-background-clip: padding; - background-clip: padding-box; -} - -.ui-autocomplete { - border-color: @inputBorder; // goes nice with textboxes - .box-shadow(0 5px 10px rgba(0,0,0,.12)); - border-top: none !important; -} -.ui-autocomplete.ui-corner-all { - .border-radius(0 0 @inputBorderRadius @inputBorderRadius); -} - -.ui-autocomplete-loading { - background-image: url('images/throbber-progress-black-16.gif'); - background-repeat: no-repeat; - background-position: 98% center; -} - -.ui-autocomplete .ui-menu-item a { - .border-radius(0); - padding: 3px 16px; - font-family: @baseFontFamily; - font-weight: normal; - color: @dropdownLinkColor; - .small(); - - &:hover, - &.ui-state-hover { - color: @dropdownLinkColorHover !important; - background: @dropdownLinkBackgroundHover !important; - } -}*/ // MVC UI adaptation // ------------------------- @@ -513,4 +472,99 @@ th label { .note-editable { background-color: #fff; +} + + +// NICER MODAL FADE +// ------------------------------ + +.modal.fade { + top: 50%; + .transition(~'opacity 0.12s linear, transform 0.12s ease-out'); + .transform(~'translate(0, -40px) scale(0.9, 0.9)'); +} + +.modal.fade.in { + .transform(none); +} + +.modal-backdrop, +.modal-backdrop.fade.in { + 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/dropdowns.less b/src/Presentation/SmartStore.Web/Content/bootstrap/dropdowns.less index 2e744829c6..e30e708e3d 100644 --- a/src/Presentation/SmartStore.Web/Content/bootstrap/dropdowns.less +++ b/src/Presentation/SmartStore.Web/Content/bootstrap/dropdowns.less @@ -59,7 +59,6 @@ .box-shadow(0 2px 6px rgba(0,0,0,.15)); -webkit-background-clip: padding-box; - -moz-background-clip: padding; background-clip: padding-box; .dropdown-menu { 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/media.less b/src/Presentation/SmartStore.Web/Content/bootstrap/media.less index 1decab71de..8b89ef39df 100644 --- a/src/Presentation/SmartStore.Web/Content/bootstrap/media.less +++ b/src/Presentation/SmartStore.Web/Content/bootstrap/media.less @@ -38,7 +38,7 @@ // ------------------------- .media .pull-left { - margin-right: 10px; + margin-right: 25px; } .media .pull-right { margin-left: 10px; diff --git a/src/Presentation/SmartStore.Web/Content/bootstrap/mixins.less b/src/Presentation/SmartStore.Web/Content/bootstrap/mixins.less index 60e1b07a23..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,32 +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; - -moz-background-clip: @clip; - background-clip: @clip; + background-clip: @clip; } // Background sizing .background-size(@size) { - -webkit-background-size: @size; - -moz-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 @@ -353,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; } @@ -392,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; @@ -429,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/bootstrap/popovers.less b/src/Presentation/SmartStore.Web/Content/bootstrap/popovers.less index a4c4bb0e07..70da281fdd 100644 --- a/src/Presentation/SmartStore.Web/Content/bootstrap/popovers.less +++ b/src/Presentation/SmartStore.Web/Content/bootstrap/popovers.less @@ -13,7 +13,6 @@ padding: 1px; background-color: @popoverBackground; -webkit-background-clip: padding-box; - -moz-background-clip: padding; background-clip: padding-box; border: 1px solid #ccc; border: 1px solid rgba(0,0,0,.2); 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/image-gallery/css/blueimp-gallery-custom.css b/src/Presentation/SmartStore.Web/Content/image-gallery/css/blueimp-gallery-custom.css index 20597a70b8..bc6725bd86 100644 --- a/src/Presentation/SmartStore.Web/Content/image-gallery/css/blueimp-gallery-custom.css +++ b/src/Presentation/SmartStore.Web/Content/image-gallery/css/blueimp-gallery-custom.css @@ -1,8 +1,61 @@  +.smartgallery-overlay, +.blueimp-gallery { + -webkit-transition: opacity 0.2s linear, transform 0.2s ease-out; + transition: opacity 0.2s linear, transform 0.2s ease-out; +} + +.smartgallery-overlay { + position: fixed; + z-index: 999998; + background: #000; + left: 0; + top: 0; + width: 100%; + height: 100%; + opacity: 0; +} +.smartgallery-overlay.in { + opacity: 0.3; +} + + +.blueimp-gallery { + left: 40px; + right: 40px; + top: 40px; + bottom: 40px; + background: #fff; + border: 1px solid rgba(0,0,0, 0.25); + border-radius: 4px; + border-top-right-radius: 0; + -webkit-box-shadow: 0px 4px 20px rgba(0,0,0, 0.35); + box-shadow: 0px 4px 20px rgba(0,0,0, 0.35); + -webkit-transform: translate(0, -75px) scale(0.9, 0.9); + -moz-transform: translate(0, -75px) scale(0.9, 0.9); + -ms-transform: translate(0, -75px) scale(0.9, 0.9); + transform: translate(0, -75px) scale(0.9, 0.9); +} + +.blueimp-gallery-display { + -webkit-transform: none; + -moz-transform: none; + -ms-transform: none; + transform: none; +} + + +.blueimp-gallery > .prev, +.blueimp-gallery > .next, +.blueimp-gallery > .close { + -webkit-transition: all 0.1s ease-in-out; + transition: all 0.1s ease-in-out; +} + .blueimp-gallery > .prev, .blueimp-gallery > .next { border: none; - border-radius: none; + border-radius: 0; text-shadow: none; font-family: initial; background: transparent; @@ -12,25 +65,36 @@ color: #bbb; opacity: 1; } +.blueimp-gallery > .prev { left: 0; } +.blueimp-gallery > .next { right: 0; } + .blueimp-gallery > .prev:hover, .blueimp-gallery > .next:hover { - color: #08c; + color: #888; + background-color: #f5f5f5; +} +.blueimp-gallery > .prev:active, +.blueimp-gallery > .next:active { + background-color: #eaeaea; } .blueimp-gallery > .close { text-shadow: none; opacity: 1 !important; - color: #aaa; padding: 8px 12px; - padding-top: 4px; + padding-top: 2px; border-bottom-left-radius: 6px; - background-color: #fff; + background-color: #c5c5c5; + color: #fff; margin: 0; top: 0; right: 0; } .blueimp-gallery > .close:hover { - color: #333; + background-color: #ee5f5b; +} +.blueimp-gallery > .close:active { + background-color: #ea3d38; } .blueimp-gallery > .indicator { @@ -39,6 +103,7 @@ right: 0; bottom: 0; background-color: #fff; + background-color: rgba(255,255,255, 0.5); opacity: 0.95; padding: 10px 0; -webkit-transition: all 0.4s ease-in-out; @@ -82,3 +147,5 @@ height: 25px; } } + + 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/jquery.pnotify.default.css b/src/Presentation/SmartStore.Web/Content/jquery.pnotify.default.css index 36a63c876f..8e7e891425 100644 --- a/src/Presentation/SmartStore.Web/Content/jquery.pnotify.default.css +++ b/src/Presentation/SmartStore.Web/Content/jquery.pnotify.default.css @@ -57,11 +57,7 @@ margin: 0; width: 70px; border-top: none; padding: 0; - -webkit-border-top-left-radius: 0; - -moz-border-top-left-radius: 0; border-top-left-radius: 0; - -webkit-border-top-right-radius: 0; - -moz-border-top-right-radius: 0; border-top-right-radius: 0; /* Ensures history container is above notices. */ z-index: 10000; @@ -144,6 +140,9 @@ margin: 0; .ui-pnotify .ui-pnotify-text { padding-top: 10px; padding-bottom: 10px; + max-height: 500px; + overflow: auto; + position: relative; } .ui-pnotify-closer, 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 a1aa99e822..bae872f9e9 100644 --- a/src/Presentation/SmartStore.Web/Controllers/BlogController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/BlogController.cs @@ -6,23 +6,24 @@ 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; @@ -47,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; @@ -70,6 +72,7 @@ public BlogController(IBlogService blogService, ICacheManager cacheManager, ICustomerActivityService customerActivityService, IStoreMappingService storeMappingService, + ILanguageService languageService, MediaSettings mediaSettings, BlogSettings blogSettings, LocalizationSettings localizationSettings, @@ -88,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; @@ -117,7 +121,7 @@ protected void PrepareBlogPostModel(BlogPostModel model, BlogPost blogPost, bool model.Title = blogPost.Title; model.Body = blogPost.Body; model.AllowComments = blogPost.AllowComments; - model.AvatarPictureSize = _mediaSettings.AvatarPictureSize; // codehint: sm-add + model.AvatarPictureSize = _mediaSettings.AvatarPictureSize; model.CreatedOn = _dateTimeHelper.ConvertToUserTime(blogPost.CreatedOnUtc, DateTimeKind.Utc); model.Tags = blogPost.ParseTags().ToList(); model.NumberOfComments = blogPost.ApprovedCommentCount; @@ -228,28 +232,43 @@ public ActionResult BlogByMonth(BlogPagingFilteringModel command) return View("List", model); } + [Compress] public ActionResult ListRss(int languageId) { - var feed = new SyndicationFeed( - string.Format("{0}: Blog", _storeContext.CurrentStore.Name), - "Blog", - new Uri(_webHelper.GetStoreLocation(false)), - "BlogRSS", - DateTime.UtcNow); + 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); - if (!_blogSettings.Enabled) - return new RssActionResult() { Feed = feed }; + var title = "{0} - Blog".FormatInvariant(_storeContext.CurrentStore.Name); - var items = new List(); - var blogPosts = _blogService.GetAllBlogPosts(_storeContext.CurrentStore.Id, languageId, - null, null, 0, int.MaxValue); - foreach (var blogPost in blogPosts) - { - string blogPostUrl = Url.RouteUrl("BlogPost", new { SeName = blogPost.GetSeName(blogPost.LanguageId, ensureTwoPublishedLanguages: false) }, "http"); - items.Add(new SyndicationItem(blogPost.Title, blogPost.Body, new Uri(blogPostUrl), String.Format("Blog:{0}", blogPost.Id), blogPost.CreatedOnUtc)); - } - feed.Items = items; - return new RssActionResult() { Feed = feed }; + 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 }; } public ActionResult BlogPost(int blogPostId) @@ -320,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, @@ -337,10 +353,8 @@ public ActionResult BlogCommentAdd(int blogPostId, BlogPostModel model, bool cap requestContext: this.ControllerContext.RequestContext, includeImplicitMvcValues: true /*helps fill in the nulls above*/ ); - return Redirect(url); - // codehint: sm-delete - //return RedirectToRoute("BlogPost", new { SeName = blogPost.GetSeName(blogPost.LanguageId, ensureTwoPublishedLanguages: false) }); + return Redirect(url); } //If we got this far, something failed, redisplay form @@ -449,11 +463,12 @@ public ActionResult RssHeaderLink() if (!_blogSettings.Enabled || !_blogSettings.ShowHeaderRssUrl) return Content(""); - string link = string.Format("", + string link = string.Format("", Url.RouteUrl("BlogRSS", new { languageId = _workContext.WorkingLanguage.Id }, _webHelper.IsCurrentConnectionSecured() ? "https" : "http"), _storeContext.CurrentStore.Name); return Content(link); } + #endregion } } diff --git a/src/Presentation/SmartStore.Web/Controllers/BoardsController.cs b/src/Presentation/SmartStore.Web/Controllers/BoardsController.cs index d9dcc8baaa..ed096936d8 100644 --- a/src/Presentation/SmartStore.Web/Controllers/BoardsController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/BoardsController.cs @@ -16,8 +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.Filters; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Framework.Security; using SmartStore.Web.Models.Boards; @@ -263,48 +266,46 @@ public ActionResult ActiveDiscussions(int forumId = 0) return View(model); } + [Compress] public ActionResult ActiveDiscussionsRss(int forumId = 0) { if (!_forumSettings.ForumsEnabled) - { return HttpNotFound(); - } - if (!_forumSettings.ActiveDiscussionsFeedEnabled) - { - return HttpNotFound(); - } + 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); - int topicLimit = _forumSettings.ActiveDiscussionsFeedCount; - var topics = _forumService.GetActiveTopics(forumId, topicLimit); - string url = Url.Action("ActiveDiscussionsRSS", null, (object)null, "http"); + var title = "{0} - {1}".FormatInvariant(_storeContext.CurrentStore.Name, T("Forum.ActiveDiscussionsFeedTitle")); - var feedTitle = _localizationService.GetResource("Forum.ActiveDiscussionsFeedTitle"); - var feedDescription = _localizationService.GetResource("Forum.ActiveDiscussionsFeedDescription"); + var feed = new SmartSyndicationFeed(new Uri(discussionLink), title, T("Forum.ActiveDiscussionsFeedDescription")); - var feed = new SyndicationFeed( - string.Format(feedTitle, _storeContext.CurrentStore.Name), - feedDescription, - new Uri(url), - "ActiveDiscussionsRSS", - DateTime.UtcNow); + feed.AddNamespaces(false); + feed.Init(selfLink, language); - var items = new List(); + if (!_forumSettings.ActiveDiscussionsFeedEnabled) + return new RssActionResult { Feed = feed }; - var viewsText = _localizationService.GetResource("Forum.Views"); - var repliesText = _localizationService.GetResource("Forum.Replies"); + var items = new List(); + var topics = _forumService.GetActiveTopics(forumId, _forumSettings.ActiveDiscussionsFeedCount); - foreach (var topic in topics) - { - string topicUrl = Url.RouteUrl("TopicSlug", new { id = topic.Id, slug = topic.GetSeName() }, "http"); - string content = String.Format("{2}: {0}, {3}: {1}", topic.NumReplies.ToString(), topic.Views.ToString(), repliesText, viewsText); + var viewsText = T("Forum.Views"); + var repliesText = T("Forum.Replies"); - items.Add(new SyndicationItem(topic.Subject, content, new Uri(topicUrl), - String.Format("Topic:{0}", topic.Id), (topic.LastPostTime ?? topic.UpdatedOnUtc))); - } - feed.Items = items; + 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); - return new RssActionResult() { Feed = feed }; + items.Add(item); + } + + feed.Items = items; + + return new RssActionResult { Feed = feed }; } public ActionResult ForumGroup(int id) @@ -354,7 +355,7 @@ public ActionResult Forum(int id, int page = 1) if (forumSubscription != null) { model.WatchForumText = _localizationService.GetResource("Forum.UnwatchForum"); - model.WatchForumSubscribed = true; // codehint: sm-add + model.WatchForumSubscribed = true; } } @@ -379,58 +380,51 @@ public ActionResult Forum(int id, int page = 1) return RedirectToRoute("Boards"); } + [Compress] public ActionResult ForumRss(int id) { if (!_forumSettings.ForumsEnabled) - { return HttpNotFound(); - } - if (!_forumSettings.ForumFeedsEnabled) - { - return HttpNotFound(); - } + 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); - int topicLimit = _forumSettings.ForumFeedCount; - var forum = _forumService.GetForumById(id); + var feed = new SmartSyndicationFeed(new Uri(forumLink), _storeContext.CurrentStore.Name, T("Forum.ForumFeedDescription")); - if (forum != null) - { - //Order by newest topic posts & limit the number of topics to return - var topics = _forumService.GetAllTopics(forum.Id, 0, string.Empty, ForumSearchType.All, 0, 0, topicLimit); + feed.AddNamespaces(false); + feed.Init(selfLink, language); - string url = Url.Action("ForumRSS", null, new { id = forum.Id }, "http"); + if (!_forumSettings.ForumFeedsEnabled) + return new RssActionResult { Feed = feed }; - var feedTitle = _localizationService.GetResource("Forum.ForumFeedTitle"); - var feedDescription = _localizationService.GetResource("Forum.ForumFeedDescription"); + var forum = _forumService.GetForumById(id); - var feed = new SyndicationFeed( - string.Format(feedTitle, _storeContext.CurrentStore.Name, forum.GetLocalized(x => x.Name)), - feedDescription, - new Uri(url), - string.Format("ForumRSS:{0}", forum.Id), - DateTime.UtcNow); + if (forum == null) + return new RssActionResult { Feed = feed }; - var items = new List(); + feed.Title = new TextSyndicationContent("{0} - {1}".FormatInvariant(_storeContext.CurrentStore.Name, forum.GetLocalized(x => x.Name, language.Id))); - var viewsText = _localizationService.GetResource("Forum.Views"); - var repliesText = _localizationService.GetResource("Forum.Replies"); + var items = new List(); + var topics = _forumService.GetAllTopics(id, 0, string.Empty, ForumSearchType.All, 0, 0, _forumSettings.ForumFeedCount); - foreach (var topic in topics) - { - string topicUrl = Url.RouteUrl("TopicSlug", new { id = topic.Id, slug = topic.GetSeName() }, "http"); - string content = string.Format("{2}: {0}, {3}: {1}", topic.NumReplies.ToString(), topic.Views.ToString(), repliesText, viewsText); + var viewsText = T("Forum.Views"); + var repliesText = T("Forum.Replies"); - items.Add(new SyndicationItem(topic.Subject, content, new Uri(topicUrl), String.Format("Topic:{0}", topic.Id), - (topic.LastPostTime ?? topic.UpdatedOnUtc))); - } + 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); - feed.Items = items; + var item = feed.CreateItem(topic.Subject, synopsis, topicUrl, topic.LastPostTime ?? topic.UpdatedOnUtc); - return new RssActionResult() { Feed = feed }; - } + items.Add(item); + } + + feed.Items = items; - return new RssActionResult() { Feed = new SyndicationFeed() }; + return new RssActionResult { Feed = feed }; } [HttpPost] diff --git a/src/Presentation/SmartStore.Web/Controllers/CatalogController.cs b/src/Presentation/SmartStore.Web/Controllers/CatalogController.cs index 191bdc64fc..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,7 +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.Filters; +using SmartStore.Web.Framework.Modelling; using SmartStore.Web.Framework.Security; using SmartStore.Web.Framework.UI; using SmartStore.Web.Infrastructure.Cache; @@ -30,7 +31,7 @@ namespace SmartStore.Web.Controllers { - public partial class CatalogController : PublicControllerBase + public partial class CatalogController : PublicControllerBase { #region Fields @@ -105,22 +106,10 @@ public CatalogController( this._catalogSettings = catalogSettings; this._helper = helper; - - T = NullLocalizer.Instance; } #endregion - #region Properties - - public Localizer T - { - get; - set; - } - - #endregion - #region Categories [RequireHttpsByConfigAttribute(SslRequirement.No)] @@ -144,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; @@ -157,6 +149,11 @@ public ActionResult Category(int categoryId, CatalogPagingFilteringModel command command.ViewMode = category.DefaultViewMode; } + if (command.OrderBy == (int)ProductSortingEnum.Initial) + { + command.OrderBy = (int)_catalogSettings.DefaultSortOrder; + } + var model = category.ToModel(); _helper.PreparePagingFilteringModel(model.PagingFilteringContext, command, new PageSizeContext @@ -193,13 +190,13 @@ public ActionResult Category(int categoryId, CatalogPagingFilteringModel command var customerRolesIds = _services.WorkContext.CurrentCustomer.CustomerRoles.Where(x => x.Active).Select(x => x.Id).ToList(); - //subcategories + // subcategories model.SubCategories = _categoryService .GetAllCategoriesByParentCategoryId(categoryId) .Select(x => { var subCatName = x.GetLocalized(y => y.Name); - var subCatModel = new CategoryModel.SubCategoryModel() + var subCatModel = new CategoryModel.SubCategoryModel { Id = x.Id, Name = subCatName, @@ -216,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) }; @@ -288,6 +285,7 @@ public ActionResult Category(int categoryId, CatalogPagingFilteringModel command products, prepareColorAttributes: true, prepareManufacturers: command.ViewMode.IsCaseInsensitiveEqual("list")).ToList(); + model.PagingFilteringContext.LoadPagedList(products); } else @@ -368,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(""); @@ -408,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) }; @@ -445,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; @@ -457,7 +458,12 @@ public ActionResult Manufacturer(int manufacturerId, CatalogPagingFilteringModel var model = manufacturer.ToModel(); // prepare picture model - model.PictureModel = this.PrepareManufacturerPictureModel(manufacturer, model.Name); + model.PictureModel = _helper.PrepareManufacturerPictureModel(manufacturer, model.Name); + + if (command.OrderBy == (int)ProductSortingEnum.Initial) + { + command.OrderBy = (int)_catalogSettings.DefaultSortOrder; + } _helper.PreparePagingFilteringModel(model.PagingFilteringContext, command, new PageSizeContext { @@ -561,72 +567,53 @@ 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(); // prepare picture model - modelMan.PictureModel = this.PrepareManufacturerPictureModel(manufacturer, modelMan.Name); + modelMan.PictureModel = _helper.PrepareManufacturerPictureModel(manufacturer, modelMan.Name); model.Add(modelMan); } return View(model); } - private PictureModel PrepareManufacturerPictureModel(Manufacturer manufacturer, string localizedName) - { - var model = new PictureModel(); - - int pictureSize = _mediaSettings.ManufacturerThumbPictureSize; - var manufacturerPictureCacheKey = string.Format(ModelCacheEventConsumer.MANUFACTURER_PICTURE_MODEL_KEY, - manufacturer.Id, - pictureSize, - true, - _services.WorkContext.WorkingLanguage.Id, - _services.WebHelper.IsCurrentConnectionSecured(), - _services.StoreContext.CurrentStore.Id); - - model = _services.Cache.Get(manufacturerPictureCacheKey, () => - { - var pictureModel = new PictureModel() - { - PictureId = manufacturer.PictureId.GetValueOrDefault(), - FullSizeImageUrl = _pictureService.GetPictureUrl(manufacturer.PictureId.GetValueOrDefault()), - ImageUrl = _pictureService.GetPictureUrl(manufacturer.PictureId.GetValueOrDefault(), pictureSize), - Title = string.Format(T("Media.Manufacturer.ImageLinkTitleFormat"), localizedName), - AlternateText = string.Format(T("Media.Manufacturer.ImageAlternateTextFormat"), localizedName) - }; - return pictureModel; - }); - - return model; - } - [ChildActionOnly] public ActionResult ManufacturerNavigation(int currentManufacturerId) { - if (_catalogSettings.ManufacturersBlockItemsToDisplay == 0) + 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 + TotalManufacturers = manufacturers.Count, + DisplayManufacturers = _catalogSettings.ShowManufacturersOnHomepage, + DisplayImages = _catalogSettings.ShowManufacturerPictures }; foreach (var manufacturer in manufacturers.Take(_catalogSettings.ManufacturersBlockItemsToDisplay)) { - var modelMan = new ManufacturerBriefInfoModel() + var modelMan = new ManufacturerBriefInfoModel { Id = manufacturer.Id, Name = manufacturer.GetLocalized(x => x.Name), SeName = manufacturer.GetSeName(), + PictureUrl = _pictureService.GetPictureUrl(manufacturer.PictureId.GetValueOrDefault(), _mediaSettings.ManufacturerThumbPictureSize, + !_catalogSettings.HideManufacturerDefaultPictures), IsActive = currentManufacturer != null && currentManufacturer.Id == manufacturer.Id, }; model.Manufacturers.Add(modelMan); @@ -750,6 +737,11 @@ public ActionResult ProductsByTag(int productTagId, CatalogPagingFilteringModel TagName = productTag.GetLocalized(y => y.Name) }; + if (command.OrderBy == (int)ProductSortingEnum.Initial) + { + command.OrderBy = (int)_catalogSettings.DefaultSortOrder; + } + _helper.PreparePagingFilteringModel(model.PagingFilteringContext, command, new PageSizeContext { AllowCustomersToSelectPageSize = _catalogSettings.ProductsByTagAllowCustomersToSelectPageSize, @@ -866,39 +858,67 @@ public ActionResult RecentlyAddedProducts(CatalogPagingFilteringModel command) return View(model); } + [Compress] public ActionResult RecentlyAddedProductsRss() { - var feed = new SyndicationFeed( - string.Format("{0}: {1}", _services.StoreContext.CurrentStore.Name, T("RSS.RecentlyAddedProducts")), - T("RSS.InformationAboutProducts"), - new Uri(_services.WebHelper.GetStoreLocation(false)), - "RecentlyAddedProductsRSS", - DateTime.UtcNow); + 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 }; + 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 ctx = new ProductSearchContext(); - ctx.LanguageId = _services.WorkContext.WorkingLanguage.Id; - ctx.OrderBy = ProductSortingEnum.CreatedOn; - ctx.PageSize = _catalogSettings.RecentlyAddedProductsNumber; - ctx.StoreId = _services.StoreContext.CurrentStoreIdIfMultiStoreMode; - ctx.VisibleIndividuallyOnly = true; - - var products = _productService.SearchProducts(ctx); + 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 (!String.IsNullOrEmpty(productUrl)) + if (productUrl.HasValue()) { - items.Add(new SyndicationItem(product.GetLocalized(x => x.Name), product.GetLocalized(x => x.ShortDescription), new Uri(productUrl), String.Format("RecentlyAddedProduct:{0}", product.Id), product.CreatedOnUtc)); + 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 }; + + return new RssActionResult { Feed = feed }; } #endregion @@ -1000,10 +1020,13 @@ public ActionResult CompareProducts() IncludeShortDescriptionInCompareProducts = _catalogSettings.IncludeShortDescriptionInCompareProducts, IncludeFullDescriptionInCompareProducts = _catalogSettings.IncludeFullDescriptionInCompareProducts, }; + var products = _compareProductsService.GetComparedProducts(); - _helper.PrepareProductOverviewModels(products, prepareSpecificationAttributes: true) + + _helper.PrepareProductOverviewModels(products, prepareSpecificationAttributes: true, prepareFullDescription: true, isCompareList: true) .ToList() .ForEach(model.Products.Add); + return View(model); } @@ -1047,8 +1070,10 @@ public ActionResult FlyoutCompare() IncludeShortDescriptionInCompareProducts = _catalogSettings.IncludeShortDescriptionInCompareProducts, IncludeFullDescriptionInCompareProducts = _catalogSettings.IncludeFullDescriptionInCompareProducts, }; + var products = _compareProductsService.GetComparedProducts(); - _helper.PrepareProductOverviewModels(products, prepareSpecificationAttributes: true) + + _helper.PrepareProductOverviewModels(products, prepareSpecificationAttributes: true, isCompareList: true) .ToList() .ForEach(model.Products.Add); @@ -1067,16 +1092,22 @@ 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; if (command.PageNumber <= 0) command.PageNumber = 1; + if (command.OrderBy == (int)ProductSortingEnum.Initial) + command.OrderBy = (int)_catalogSettings.DefaultSortOrder; + _helper.PreparePagingFilteringModel(model.PagingFilteringContext, command, new PageSizeContext { AllowCustomersToSelectPageSize = _catalogSettings.ProductSearchAllowCustomersToSelectPageSize, @@ -1084,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); @@ -1103,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 @@ -1117,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) { @@ -1142,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 @@ -1163,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); } @@ -1183,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; @@ -1204,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); @@ -1233,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 dceb88c312..ebc506887c 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CatalogHelper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using System.Web; @@ -11,7 +12,7 @@ using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Media; using SmartStore.Core.Domain.Tax; -using SmartStore.Core.Localization; +using SmartStore.Core.Localization; using SmartStore.Core.Logging; using SmartStore.Services; using SmartStore.Services.Catalog; @@ -20,10 +21,11 @@ using SmartStore.Services.Directory; using SmartStore.Services.Helpers; using SmartStore.Services.Localization; -using SmartStore.Services.Media; +using SmartStore.Services.Media; 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; @@ -58,7 +60,6 @@ public class CatalogHelper private readonly CatalogSettings _catalogSettings; private readonly CustomerSettings _customerSettings; private readonly CaptchaSettings _captchaSettings; - private readonly CurrencySettings _currencySettings; private readonly TaxSettings _taxSettings; private readonly IMeasureService _measureService; private readonly IQuantityUnitService _quantityUnitService; @@ -66,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; @@ -91,7 +93,6 @@ public CatalogHelper( MediaSettings mediaSettings, CatalogSettings catalogSettings, CustomerSettings customerSettings, - CurrencySettings currencySettings, CaptchaSettings captchaSettings, IMeasureService measureService, IQuantityUnitService quantityUnitService, @@ -100,6 +101,7 @@ public CatalogHelper( IDeliveryTimeService deliveryTimeService, ISettingService settingService, Lazy _menuPublisher, + Lazy topicService, HttpRequestBase httpRequest, UrlHelper urlHelper) { @@ -131,8 +133,8 @@ public CatalogHelper( this._catalogSettings = catalogSettings; this._customerSettings = customerSettings; this._captchaSettings = captchaSettings; - this._currencySettings = currencySettings; this._menuPublisher = _menuPublisher; + this._topicService = topicService; this._httpRequest = httpRequest; this._urlHelper = urlHelper; @@ -143,8 +145,13 @@ public CatalogHelper( public ILogger Logger { get; set; } - public ProductDetailsModel PrepareProductDetailsPageModel(Product product, bool isAssociatedProduct = false, - ProductBundleItemData productBundleItem = null, IList productBundleItems = null, FormCollection selectedAttributes = null) + public ProductDetailsModel PrepareProductDetailsPageModel( + Product product, + bool isAssociatedProduct = false, + ProductBundleItemData productBundleItem = null, + IList productBundleItems = null, + NameValueCollection selectedAttributes = null, + NameValueCollection queryData = null) { if (product == null) throw new ArgumentNullException("product"); @@ -162,7 +169,7 @@ public ProductDetailsModel PrepareProductDetailsPageModel(Product product, bool 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")), @@ -179,6 +186,30 @@ public ProductDetailsModel PrepareProductDetailsPageModel(Product product, bool 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 && @@ -203,12 +234,12 @@ public ProductDetailsModel PrepareProductDetailsPageModel(Product product, bool IList bundleItems = null; ProductVariantAttributeCombination combination = null; - var combinationImageIds = new List(); if (product.ProductType == ProductType.GroupedProduct && !isAssociatedProduct) // associated products { - var searchContext = new ProductSearchContext() + var searchContext = new ProductSearchContext { + OrderBy = ProductSortingEnum.Position, StoreId = _services.StoreContext.CurrentStore.Id, ParentGroupedProductId = product.Id, PageSize = int.MaxValue, @@ -227,7 +258,19 @@ public ProductDetailsModel PrepareProductDetailsPageModel(Product product, bool foreach (var itemData in bundleItems.Where(x => x.Item.Product.CanBeBundleItem())) { var item = itemData.Item; - var bundledProductModel = PrepareProductDetailsPageModel(item.Product, false, itemData); + var bundleItemAttributes = new NameValueCollection(); + + if (selectedAttributes != null) + { + 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); bundledProductModel.BundleItem.Id = item.Id; bundledProductModel.BundleItem.Quantity = item.Quantity; @@ -235,11 +278,11 @@ public ProductDetailsModel PrepareProductDetailsPageModel(Product product, bool bundledProductModel.BundleItem.Visible = item.Visible; bundledProductModel.BundleItem.IsBundleItemPricing = item.BundleProduct.BundlePerItemPricing; - string bundleItemName = item.GetLocalized(x => x.Name); + var bundleItemName = item.GetLocalized(x => x.Name); if (bundleItemName.HasValue()) bundledProductModel.Name = bundleItemName; - string bundleItemShortDescription = item.GetLocalized(x => x.ShortDescription); + var bundleItemShortDescription = item.GetLocalized(x => x.ShortDescription); if (bundleItemShortDescription.HasValue()) bundledProductModel.ShortDescription = bundleItemShortDescription; @@ -249,17 +292,18 @@ public ProductDetailsModel PrepareProductDetailsPageModel(Product product, bool model = PrepareProductDetailModel(model, product, isAssociatedProduct, productBundleItem, bundleItems, selectedAttributes); + IList combinationPictureIds = null; + if (productBundleItem == null) { - model.Combinations.GetAllCombinationImageIds(combinationImageIds); - - if (combination == null && model.CombinationSelected != null) - combination = model.CombinationSelected; + combinationPictureIds = _productAttributeService.GetAllProductVariantAttributeCombinationPictureIds(product.Id); + if (combination == null && model.SelectedCombination != null) + combination = model.SelectedCombination; } // pictures var pictures = _pictureService.GetPicturesByProductId(product.Id); - PrepareProductDetailsPictureModel(model.DetailsPictureModel, pictures, model.Name, combinationImageIds, isAssociatedProduct, productBundleItem, combination); + PrepareProductDetailsPictureModel(model.DetailsPictureModel, pictures, model.Name, combinationPictureIds, isAssociatedProduct, productBundleItem, combination); return model; } @@ -304,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 @@ -317,8 +361,14 @@ private PictureModel CreatePictureModel(ProductDetailsPictureModel model, Pictur return result; } - public void PrepareProductDetailsPictureModel(ProductDetailsPictureModel model, IList pictures, string name, List allCombinationImageIds, - bool isAssociatedProduct, ProductBundleItemData bundleItem = null, ProductVariantAttributeCombination combination = null) + public void PrepareProductDetailsPictureModel( + ProductDetailsPictureModel model, + IList pictures, + string name, + IList allCombinationImageIds, + bool isAssociatedProduct, + ProductBundleItemData bundleItem = null, + ProductVariantAttributeCombination combination = null) { model.Name = name; model.DefaultPictureZoomEnabled = _mediaSettings.DefaultPictureZoomEnabled; @@ -355,7 +405,8 @@ public void PrepareProductDetailsPictureModel(ProductDetailsPictureModel model, else { // images not belonging to any combination... - foreach (var picture in pictures.Where(p => !allCombinationImageIds.Contains(p.Id))) + allCombinationImageIds = allCombinationImageIds ?? new List(); + foreach (var picture in pictures.Where(p => !allCombinationImageIds.Contains(p.Id))) { model.PictureModels.Add(CreatePictureModel(model, picture, _mediaSettings.ProductDetailsPictureSize)); } @@ -386,14 +437,18 @@ public void PrepareProductDetailsPictureModel(ProductDetailsPictureModel model, // default picture if (defaultPicture == null) { - model.DefaultPictureModel = new PictureModel() + 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 { @@ -408,7 +463,7 @@ public ProductDetailsModel PrepareProductDetailModel( bool isAssociatedProduct = false, ProductBundleItemData productBundleItem = null, IList productBundleItems = null, - FormCollection selectedAttributes = null, + NameValueCollection selectedAttributes = null, int selectedQuantity = 1) { if (product == null) @@ -418,7 +473,11 @@ public ProductDetailsModel PrepareProductDetailModel( throw new ArgumentNullException("model"); if (selectedAttributes == null) - selectedAttributes = new FormCollection(); + 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; @@ -446,9 +505,7 @@ public ProductDetailsModel PrepareProductDetailModel( { foreach (var attribute in variantAttributes) { - int preSelectedValueId = 0; - - var pvaModel = new ProductDetailsModel.ProductVariantAttributeModel() + var pvaModel = new ProductDetailsModel.ProductVariantAttributeModel { Id = attribute.Id, ProductId = attribute.ProductId, @@ -471,78 +528,98 @@ public ProductDetailsModel PrepareProductDetailModel( pvaModel.BeginYear = match.Groups[1].Value.ToInt(); pvaModel.EndYear = match.Groups[2].Value.ToInt(); } - } - if (attribute.ShouldHaveValues()) + 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) { - var pvaValues = _productAttributeService.GetProductVariantAttributeValues(attribute.Id); - - foreach (var pvaValue in pvaValues) + if (hasSelectedAttributes) { - ProductBundleItemAttributeFilter attributeFilter = null; + var attributeKey = "product_attribute_{0}_{1}_{2}_{3}".FormatInvariant(product.Id, bundleItemId, attribute.ProductAttributeId, attribute.Id); + pvaModel.TextValue = selectedAttributes[attributeKey]; + } + } - if (productBundleItem.FilterOut(pvaValue, out attributeFilter)) - continue; + var preSelectedValueId = 0; + var pvaValues = (attribute.ShouldHaveValues() ? _productAttributeService.GetProductVariantAttributeValues(attribute.Id) : new List()); - if (preSelectedValueId == 0 && attributeFilter != null && attributeFilter.IsPreSelected) - preSelectedValueId = attributeFilter.AttributeValueId; + foreach (var pvaValue in pvaValues) + { + ProductBundleItemAttributeFilter attributeFilter = null; - var linkedProduct = _productService.GetProductById(pvaValue.LinkedProductId); + if (productBundleItem.FilterOut(pvaValue, out attributeFilter)) + continue; - var pvaValueModel = new ProductDetailsModel.ProductVariantAttributeValueModel(); - pvaValueModel.Id = pvaValue.Id; - pvaValueModel.Name = pvaValue.GetLocalized(x => x.Name); - pvaValueModel.Alias = pvaValue.Alias; - pvaValueModel.ColorSquaresRgb = pvaValue.ColorSquaresRgb; //used with "Color squares" attribute type - pvaValueModel.IsPreSelected = pvaValue.IsPreSelected; + if (preSelectedValueId == 0 && attributeFilter != null && attributeFilter.IsPreSelected) + preSelectedValueId = attributeFilter.AttributeValueId; - if (linkedProduct != null && linkedProduct.VisibleIndividually) - pvaValueModel.SeName = linkedProduct.GetSeName(); + var linkedProduct = _productService.GetProductById(pvaValue.LinkedProductId); - if (hasSelectedAttributes) - pvaValueModel.IsPreSelected = false; // explicitly selected always discards pre-selected by merchant + var pvaValueModel = new ProductDetailsModel.ProductVariantAttributeValueModel(); + pvaValueModel.Id = pvaValue.Id; + pvaValueModel.Name = pvaValue.GetLocalized(x => x.Name); + pvaValueModel.Alias = pvaValue.Alias; + pvaValueModel.ColorSquaresRgb = pvaValue.ColorSquaresRgb; //used with "Color squares" attribute type + pvaValueModel.IsPreSelected = pvaValue.IsPreSelected; - // display price if allowed - if (displayPrices && !isBundlePricing) - { - 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); - - if (priceAdjustmentBase > decimal.Zero) - pvaValueModel.PriceAdjustment = "+" + _priceFormatter.FormatPrice(priceAdjustment, true, false); - else if (priceAdjustmentBase < decimal.Zero) - pvaValueModel.PriceAdjustment = "-" + _priceFormatter.FormatPrice(-priceAdjustment, true, false); + if (linkedProduct != null && linkedProduct.VisibleIndividually) + pvaValueModel.SeName = linkedProduct.GetSeName(); - if (pvaValueModel.IsPreSelected) - { - preSelectedPriceAdjustmentBase = decimal.Add(preSelectedPriceAdjustmentBase, priceAdjustmentBase); - preSelectedWeightAdjustment = decimal.Add(preSelectedWeightAdjustment, pvaValue.WeightAdjustment); - } + if (hasSelectedAttributes) + pvaValueModel.IsPreSelected = false; // explicitly selected always discards pre-selected by merchant - if (_catalogSettings.ShowLinkedAttributeValueQuantity && pvaValue.ValueType == ProductVariantAttributeValueType.ProductLinkage) - { - pvaValueModel.QuantityInfo = pvaValue.Quantity; - } + // display price if allowed + if (displayPrices && !isBundlePricing) + { + decimal taxRate = decimal.Zero; + decimal attributeValuePriceAdjustment = _priceCalculationService.GetProductVariantAttributeValuePriceAdjustment(pvaValue); + decimal priceAdjustmentBase = _taxService.GetProductPrice(product, attributeValuePriceAdjustment, out taxRate); + decimal priceAdjustment = _currencyService.ConvertFromPrimaryStoreCurrency(priceAdjustmentBase, currency); - pvaValueModel.PriceAdjustmentValue = priceAdjustment; - } + if (priceAdjustmentBase > decimal.Zero) + pvaValueModel.PriceAdjustment = "+" + _priceFormatter.FormatPrice(priceAdjustment, true, false); + else if (priceAdjustmentBase < decimal.Zero) + pvaValueModel.PriceAdjustment = "-" + _priceFormatter.FormatPrice(-priceAdjustment, true, false); - if (!_catalogSettings.ShowVariantCombinationPriceAdjustment) + if (pvaValueModel.IsPreSelected) { - pvaValueModel.PriceAdjustment = ""; + preSelectedPriceAdjustmentBase = decimal.Add(preSelectedPriceAdjustmentBase, priceAdjustmentBase); + preSelectedWeightAdjustment = decimal.Add(preSelectedWeightAdjustment, pvaValue.WeightAdjustment); } - if (_catalogSettings.ShowLinkedAttributeValueImage && pvaValue.ValueType == ProductVariantAttributeValueType.ProductLinkage) + if (_catalogSettings.ShowLinkedAttributeValueQuantity && pvaValue.ValueType == ProductVariantAttributeValueType.ProductLinkage) { - var linkagePicture = _pictureService.GetPicturesByProductId(pvaValue.LinkedProductId, 1).FirstOrDefault(); - if (linkagePicture != null) - pvaValueModel.ImageUrl = _pictureService.GetPictureUrl(linkagePicture, _mediaSettings.AutoCompleteSearchThumbPictureSize, false); + pvaValueModel.QuantityInfo = pvaValue.Quantity; } - pvaModel.Values.Add(pvaValueModel); + pvaValueModel.PriceAdjustmentValue = priceAdjustment; } + + if (!_catalogSettings.ShowVariantCombinationPriceAdjustment) + { + pvaValueModel.PriceAdjustment = ""; + } + + if (_catalogSettings.ShowLinkedAttributeValueImage && pvaValue.ValueType == ProductVariantAttributeValueType.ProductLinkage) + { + var linkagePicture = _pictureService.GetPicturesByProductId(pvaValue.LinkedProductId, 1).FirstOrDefault(); + if (linkagePicture != null) + pvaValueModel.ImageUrl = _pictureService.GetPictureUrl(linkagePicture, _mediaSettings.VariantValueThumbPictureSize, false); + } + + pvaModel.Values.Add(pvaValueModel); } // we need selected attributes to get initially displayed combination images @@ -555,14 +632,25 @@ public ProductDetailsModel PrepareProductDetailModel( pvaModel.Values.Each(x => x.IsPreSelected = false); if ((defaultValue = pvaModel.Values.FirstOrDefault(v => v.Id == preSelectedValueId)) != null) + { defaultValue.IsPreSelected = true; + selectedAttributes.AddProductAttribute(attribute.ProductAttributeId, attribute.Id, defaultValue.Id, product.Id, bundleItemId); + } } if (defaultValue == null) - defaultValue = pvaModel.Values.FirstOrDefault(v => v.IsPreSelected); + { + foreach (var value in pvaModel.Values.Where(x => x.IsPreSelected)) + { + selectedAttributes.AddProductAttribute(attribute.ProductAttributeId, attribute.Id, value.Id, product.Id, bundleItemId); + } + } + + //if (defaultValue == null) + // defaultValue = pvaModel.Values.FirstOrDefault(v => v.IsPreSelected); - if (defaultValue != null) - selectedAttributes.AddProductAttribute(attribute.ProductAttributeId, attribute.Id, defaultValue.Id, product.Id, bundleItemId); + //if (defaultValue != null) + // selectedAttributes.AddProductAttribute(attribute.ProductAttributeId, attribute.Id, defaultValue.Id, product.Id, bundleItemId); } model.ProductVariantAttributes.Add(pvaModel); @@ -575,8 +663,6 @@ public ProductDetailsModel PrepareProductDetailModel( if (!isBundle) { - model.Combinations = _productAttributeService.GetAllProductVariantAttributeCombinations(product.Id); - if (selectedAttributes.Count > 0) { // merge with combination data if there's a match @@ -589,20 +675,19 @@ 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); } - model.CombinationSelected = model.Combinations - .FirstOrDefault(x => _productAttributeParser.AreProductAttributesEqual(x.AttributesXml, attributeXml)); + model.SelectedCombination = _productAttributeParser.FindProductVariantAttributeCombination(product.Id, attributeXml); - if (model.CombinationSelected != null && model.CombinationSelected.IsActive == false) + if (model.SelectedCombination != null && model.SelectedCombination.IsActive == false) { model.IsAvailable = false; model.StockAvailability = T("Products.Availability.IsNotActive"); } - product.MergeWithCombination(model.CombinationSelected); + product.MergeWithCombination(model.SelectedCombination); // mark explicitly selected as pre-selected foreach (var attribute in model.ProductVariantAttributes) @@ -628,8 +713,9 @@ public ProductDetailsModel PrepareProductDetailModel( { // cases where stock inventory is not functional. determined by what ShoppingCartService.GetStandardWarnings and ProductService.AdjustInventory is not handling. model.IsAvailable = true; - model.StockAvailability = product.ProductVariantAttributeCombinations.Count == 0 ? product.FormatStockMessage(_localizationService) : ""; - } + var hasAttributeCombinations = _services.DbContext.QueryForCollection(product, (Product p) => p.ProductVariantAttributeCombinations).Any(); + model.StockAvailability = !hasAttributeCombinations ? product.FormatStockMessage(_localizationService) : ""; + } else if (model.IsAvailable) { model.IsAvailable = product.IsAvailableByStock(); @@ -653,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); + 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; @@ -664,39 +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 = ""; + 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.IsTaxExempt && !product.IsShipEnabled) - model.LegalInfo += taxInfo + " " + defaultTaxRate; - - if(product.IsShipEnabled) + if (!product.IsShipEnabled || (addShippingPrice == 0 && product.IsFreeShipping)) { - model.LegalInfo = T("Tax.LegalInfoProductDetail", + model.LegalInfo += "{0} {1}, {2}".FormatInvariant( product.IsTaxExempt ? "" : taxInfo, product.IsTaxExempt ? "" : defaultTaxRate, - additionalShippingCosts, - shippingInfoLink); + T("Common.FreeShipping")); + } + else + { + 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) @@ -755,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 @@ -809,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) { @@ -835,17 +934,35 @@ public ProductDetailsModel PrepareProductDetailModel( model.ProductPrice.PriceValue = finalPriceWithoutDiscount; model.ProductPrice.PriceWithDiscountValue = finalPriceWithDiscount; - model.BasePriceInfo = product.GetBasePriceInfo(_localizationService, _priceFormatter, attributesTotalPriceBase); + model.BasePriceInfo = product.GetBasePriceInfo( + _localizationService, + _priceFormatter, + _currencyService, + _taxService, + _priceCalculationService, + currency, + attributesTotalPriceBase); if (!string.IsNullOrWhiteSpace(model.ProductPrice.OldPrice) || !string.IsNullOrWhiteSpace(model.ProductPrice.PriceWithDiscount)) { model.ProductPrice.NoteWithoutDiscount = T(isBundle && product.BundlePerItemPricing ? "Products.Bundle.PriceWithoutDiscount.Note" : "Products.Price"); } - if (isBundle && product.BundlePerItemPricing && !string.IsNullOrWhiteSpace(model.ProductPrice.PriceWithDiscount)) + if ((isBundle && product.BundlePerItemPricing && !string.IsNullOrWhiteSpace(model.ProductPrice.PriceWithDiscount)) || product.HasTierPrices) { - model.ProductPrice.NoteWithDiscount = T("Products.Bundle.PriceWithDiscount.Note"); - model.BasePriceInfo = product.GetBasePriceInfo(_localizationService, _priceFormatter, (product.Price - finalPriceWithDiscount) * (-1)); + if (!product.HasTierPrices) + { + model.ProductPrice.NoteWithDiscount = T("Products.Bundle.PriceWithDiscount.Note"); + } + + model.BasePriceInfo = product.GetBasePriceInfo( + _localizationService, + _priceFormatter, + _currencyService, + _taxService, + _priceCalculationService, + currency, + (product.Price - finalPriceWithDiscount) * (-1)); } } } @@ -880,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() @@ -907,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 @@ -983,20 +1102,27 @@ public IEnumerable PrepareProductOverviewModels( bool prepareSpecificationAttributes = false, bool forceRedirectionAfterAddingToCart = false, bool prepareColorAttributes = false, - bool prepareManufacturers = false) + bool prepareManufacturers = false, + bool isCompact = false, + bool prepareFullDescription = false, + bool isCompareList = false) { if (products == null) throw new ArgumentNullException("products"); // PERF!! + var currentStore = _services.StoreContext.CurrentStore; + var currentCustomer = _services.WorkContext.CurrentCustomer; + var workingCurrency = _services.WorkContext.WorkingCurrency; var displayPrices = _services.Permissions.Authorize(StandardPermissionProvider.DisplayPrices); var enableShoppingCart = _services.Permissions.Authorize(StandardPermissionProvider.EnableShoppingCart); var enableWishlist = _services.Permissions.Authorize(StandardPermissionProvider.EnableWishlist); - var currentCustomer = _services.WorkContext.CurrentCustomer; - var taxDisplayType = _services.WorkContext.GetTaxDisplayTypeFor(currentCustomer, _services.StoreContext.CurrentStore.Id); - string taxInfo = T(taxDisplayType == TaxDisplayType.IncludingTax ? "Tax.InclVAT" : "Tax.ExclVAT"); - string shippingInfoLink = _urlHelper.RouteUrl("Topic", new { SystemName = "shippinginfo" }); + var taxDisplayType = _services.WorkContext.GetTaxDisplayTypeFor(currentCustomer, currentStore.Id); 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") }, @@ -1004,25 +1130,44 @@ 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(); foreach (var product in products) { - var minPriceProduct = product; + var contextProduct = product; + var finalPrice = decimal.Zero; var model = new ProductOverviewModel { Id = product.Id, Name = product.GetLocalized(x => x.Name).EmptyNull(), ShortDescription = product.GetLocalized(x => x.ShortDescription), - FullDescription = product.GetLocalized(x => x.FullDescription), SeName = product.GetSeName() }; + if (prepareFullDescription) + { + model.FullDescription = product.GetLocalized(x => x.FullDescription); + } + // price if (preparePriceModel) { @@ -1034,167 +1179,155 @@ public IEnumerable PrepareProductOverviewModels( ShowDiscountSign = _catalogSettings.ShowDiscountSign }; - switch (product.ProductType) + if (product.ProductType == ProductType.GroupedProduct) { - case ProductType.GroupedProduct: - { - #region Grouped product + #region Grouped product - var searchContext = new ProductSearchContext - { - StoreId = _services.StoreContext.CurrentStore.Id, - ParentGroupedProductId = product.Id, - PageSize = int.MaxValue, - VisibleIndividuallyOnly = false - }; + priceModel.DisableBuyButton = true; + priceModel.DisableWishListButton = true; + priceModel.AvailableForPreOrder = false; - var associatedProducts = _productService.SearchProducts(searchContext); + var searchContext = new ProductSearchContext + { + OrderBy = ProductSortingEnum.Position, + StoreId = currentStore.Id, + ParentGroupedProductId = product.Id, + PageSize = int.MaxValue, + VisibleIndividuallyOnly = false + }; + + var associatedProducts = _productService.SearchProducts(searchContext); + + if (associatedProducts.Count > 0) + { + contextProduct = associatedProducts.OrderBy(x => x.DisplayOrder).First(); - if (associatedProducts.Count <= 0) + if (displayPrices && _catalogSettings.PriceDisplayType != PriceDisplayType.Hide) + { + decimal? displayPrice = null; + bool displayFromMessage = false; + + if (_catalogSettings.PriceDisplayType == PriceDisplayType.PreSelectedPrice) { - priceModel.OldPrice = null; - priceModel.Price = null; - priceModel.DisableBuyButton = true; - priceModel.DisableWishListButton = true; - priceModel.AvailableForPreOrder = false; + displayPrice = _priceCalculationService.GetPreselectedPrice(contextProduct, cargoData); + } + else if (_catalogSettings.PriceDisplayType == PriceDisplayType.PriceWithoutDiscountsAndAttributes) + { + displayPrice = _priceCalculationService.GetFinalPrice(contextProduct, null, currentCustomer, decimal.Zero, false, 1, null, cargoData); } else { - priceModel.DisableBuyButton = true; - priceModel.DisableWishListButton = true; - priceModel.AvailableForPreOrder = false; + displayFromMessage = true; + displayPrice = _priceCalculationService.GetLowestPrice(product, cargoData, associatedProducts, out contextProduct); + } - if (displayPrices) + if (contextProduct != null && !contextProduct.CustomerEntersPrice) + { + if (contextProduct.CallForPrice) { - decimal? minPossiblePrice = _priceCalculationService.GetLowestPrice(product, associatedProducts, out minPriceProduct); - - if (minPriceProduct != null && !minPriceProduct.CustomerEntersPrice) - { - if (minPriceProduct.CallForPrice) - { - priceModel.OldPrice = null; - priceModel.Price = res["Products.CallForPrice"]; - } - else if (minPossiblePrice.HasValue) - { - //calculate prices - decimal taxRate = decimal.Zero; - decimal oldPriceBase = _taxService.GetProductPrice(minPriceProduct, minPriceProduct.OldPrice, out taxRate); - decimal finalPriceBase = _taxService.GetProductPrice(minPriceProduct, minPossiblePrice.Value, out taxRate); - decimal finalPrice = _currencyService.ConvertFromPrimaryStoreCurrency(finalPriceBase, _services.WorkContext.WorkingCurrency); - - priceModel.OldPrice = null; - priceModel.Price = String.Format(res["Products.PriceRangeFrom"], _priceFormatter.FormatPrice(finalPrice)); - priceModel.HasDiscount = finalPriceBase != oldPriceBase && oldPriceBase != decimal.Zero; - } - else - { - //Actually it's not possible (we presume that minimalPrice always has a value) - //We never should get here - Debug.WriteLine(string.Format("Cannot calculate minPrice for product #{0}", product.Id)); - } - } + priceModel.OldPrice = null; + priceModel.Price = res["Products.CallForPrice"]; } - else + else if (displayPrice.HasValue) { - //hide prices + //calculate prices + decimal taxRate = decimal.Zero; + decimal oldPriceBase = _taxService.GetProductPrice(contextProduct, contextProduct.OldPrice, out taxRate); + decimal finalPriceBase = _taxService.GetProductPrice(contextProduct, displayPrice.Value, out taxRate); + finalPrice = _currencyService.ConvertFromPrimaryStoreCurrency(finalPriceBase, workingCurrency); + priceModel.OldPrice = null; - priceModel.Price = null; + + if (displayFromMessage) + priceModel.Price = String.Format(res["Products.PriceRangeFrom"], _priceFormatter.FormatPrice(finalPrice)); + else + priceModel.Price = _priceFormatter.FormatPrice(finalPrice); + + priceModel.HasDiscount = (finalPriceBase != oldPriceBase && oldPriceBase != decimal.Zero); + } + else + { + // Actually it's not possible (we presume that displayPrice always has a value). We never should get here + Debug.WriteLine(string.Format("Cannot calculate displayPrice for product #{0}", product.Id)); } } + } + } + + #endregion + } + else + { + #region Simple product + + //add to cart button + priceModel.DisableBuyButton = product.DisableBuyButton || !enableShoppingCart || !displayPrices; + + //add to wishlist button + priceModel.DisableWishListButton = product.DisableWishlistButton || !enableWishlist || !displayPrices; - #endregion + //pre-order + priceModel.AvailableForPreOrder = product.AvailableForPreOrder; + + //prices + if (displayPrices && _catalogSettings.PriceDisplayType != PriceDisplayType.Hide && !product.CustomerEntersPrice) + { + if (product.CallForPrice) + { + //call for price + priceModel.OldPrice = null; + priceModel.Price = res["Products.CallForPrice"]; } - break; - case ProductType.SimpleProduct: - default: + else { - #region Simple product + //calculate prices + bool displayFromMessage = false; + decimal displayPrice = decimal.Zero; + + if (_catalogSettings.PriceDisplayType == PriceDisplayType.PreSelectedPrice) + { + displayPrice = _priceCalculationService.GetPreselectedPrice(product, cargoData); + } + else if (_catalogSettings.PriceDisplayType == PriceDisplayType.PriceWithoutDiscountsAndAttributes) + { + displayPrice = _priceCalculationService.GetFinalPrice(product, null, currentCustomer, decimal.Zero, false, 1, null, cargoData); + } + else + { + displayPrice = _priceCalculationService.GetLowestPrice(product, cargoData, out displayFromMessage); + } - //add to cart button - priceModel.DisableBuyButton = product.DisableBuyButton || !enableShoppingCart || !displayPrices; + decimal taxRate = decimal.Zero; + decimal oldPriceBase = _taxService.GetProductPrice(product, product.OldPrice, out taxRate); + decimal finalPriceBase = _taxService.GetProductPrice(product, displayPrice, out taxRate); - //add to wishlist button - priceModel.DisableWishListButton = product.DisableWishlistButton || !enableWishlist || !displayPrices; + decimal oldPrice = _currencyService.ConvertFromPrimaryStoreCurrency(oldPriceBase, workingCurrency); + finalPrice = _currencyService.ConvertFromPrimaryStoreCurrency(finalPriceBase, workingCurrency); - //pre-order - priceModel.AvailableForPreOrder = product.AvailableForPreOrder; + priceModel.HasDiscount = (finalPriceBase != oldPriceBase && oldPriceBase != decimal.Zero); - //prices - if (displayPrices) + if (displayFromMessage) { - if (!product.CustomerEntersPrice) - { - if (product.CallForPrice) - { - //call for price - priceModel.OldPrice = null; - priceModel.Price = res["Products.CallForPrice"]; - } - else - { - //calculate prices - bool isBundlePerItemPricing = (product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing); - - bool displayFromMessage = false; - decimal minPossiblePrice = _priceCalculationService.GetLowestPrice(product, out displayFromMessage); - - decimal taxRate = decimal.Zero; - decimal oldPriceBase = _taxService.GetProductPrice(product, product.OldPrice, out taxRate); - decimal finalPriceBase = _taxService.GetProductPrice(product, minPossiblePrice, out taxRate); - - decimal oldPrice = _currencyService.ConvertFromPrimaryStoreCurrency(oldPriceBase, _services.WorkContext.WorkingCurrency); - decimal finalPrice = _currencyService.ConvertFromPrimaryStoreCurrency(finalPriceBase, _services.WorkContext.WorkingCurrency); - - priceModel.HasDiscount = (finalPriceBase != oldPriceBase && oldPriceBase != decimal.Zero); - - // check tier prices - if (product.HasTierPrices && !isBundlePerItemPricing && !displayFromMessage) - { - var tierPrices = new List(); - - tierPrices.AddRange(product.TierPrices - .OrderBy(tp => tp.Quantity) - .FilterByStore(_services.StoreContext.CurrentStore.Id) - .FilterForCustomer(_services.WorkContext.CurrentCustomer) - .ToList() - .RemoveDuplicatedQuantities()); - - // When there is just one tier (with qty 1), there are no actual savings in the list. - displayFromMessage = (tierPrices.Count > 0 && !(tierPrices.Count == 1 && tierPrices[0].Quantity <= 1)); - } - - if (displayFromMessage) - { - priceModel.OldPrice = null; - priceModel.Price = String.Format(res["Products.PriceRangeFrom"], _priceFormatter.FormatPrice(finalPrice)); - } - else - { - if (priceModel.HasDiscount) - { - priceModel.OldPrice = _priceFormatter.FormatPrice(oldPrice); - priceModel.Price = _priceFormatter.FormatPrice(finalPrice); - } - else - { - priceModel.OldPrice = null; - priceModel.Price = _priceFormatter.FormatPrice(finalPrice); - } - } - } - } + priceModel.OldPrice = null; + priceModel.Price = String.Format(res["Products.PriceRangeFrom"], _priceFormatter.FormatPrice(finalPrice)); } else { - //hide prices - priceModel.OldPrice = null; - priceModel.Price = null; + if (priceModel.HasDiscount) + { + priceModel.OldPrice = _priceFormatter.FormatPrice(oldPrice); + priceModel.Price = _priceFormatter.FormatPrice(finalPrice); + } + else + { + priceModel.OldPrice = null; + priceModel.Price = _priceFormatter.FormatPrice(finalPrice); + } } - - #endregion } - break; + } + + #endregion } model.ProductPrice = priceModel; @@ -1203,6 +1336,35 @@ public IEnumerable PrepareProductOverviewModels( #endregion } + // color squares + if (prepareColorAttributes && _catalogSettings.ShowColorSquaresInLists) + { + #region Prepare color attributes + + var attributes = cargoData.Attributes.Load(contextProduct.Id); + var colorAttribute = attributes.FirstOrDefault(x => x.AttributeControlType == AttributeControlType.ColorSquares); + + if (colorAttribute != null) + { + var colorValues = + from a in colorAttribute.ProductVariantAttributeValues.Take(50) + where (a.ColorSquaresRgb.HasValue() && !a.ColorSquaresRgb.IsCaseInsensitiveEqual("transparent")) + select new ProductOverviewModel.ColorAttributeModel + { + Color = a.ColorSquaresRgb, + Alias = a.Alias, + FriendlyName = a.GetLocalized(l => l.Name) + }; + + if (colorValues.Any()) + { + model.ColorAttributes.AddRange(colorValues.Distinct()); + } + } + + #endregion + } + // picture if (preparePictureModel) { @@ -1213,15 +1375,15 @@ public IEnumerable PrepareProductOverviewModels( //prepare picture model var defaultProductPictureCacheKey = string.Format(ModelCacheEventConsumer.PRODUCT_DEFAULTPICTURE_MODEL_KEY, product.Id, pictureSize, true, - _services.WorkContext.WorkingLanguage.Id, _services.WebHelper.IsCurrentConnectionSecured(), _services.StoreContext.CurrentStore.Id); + _services.WorkContext.WorkingLanguage.Id, _services.WebHelper.IsCurrentConnectionSecured(), currentStore.Id); model.DefaultPictureModel = _services.Cache.Get(defaultProductPictureCacheKey, () => { 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) }; @@ -1236,60 +1398,30 @@ public IEnumerable PrepareProductOverviewModels( { model.SpecificationAttributeModels = PrepareProductSpecificationModel(product); } - - // available colors - if (prepareColorAttributes && _catalogSettings.ShowColorSquaresInLists) - { - #region Prepare color attributes - - // get the FIRST color type attribute - var colorAttr = _productAttributeService.GetProductVariantAttributesByProductId(minPriceProduct.Id) - .FirstOrDefault(x => x.AttributeControlType == AttributeControlType.ColorSquares); - - if (colorAttr != null) - { - var colorValues = - from a in colorAttr.ProductVariantAttributeValues.Take(50) - where (a.ColorSquaresRgb.HasValue() && !a.ColorSquaresRgb.IsCaseInsensitiveEqual("transparent")) - select new ProductOverviewModel.ColorAttributeModel - { - Color = a.ColorSquaresRgb, - Alias = a.Alias, - FriendlyName = a.GetLocalized(l => l.Name) - }; - - if (colorValues.Any()) - { - model.ColorAttributes.AddRange(colorValues.Distinct()); - } - } - - #endregion - } - model.ProductMinPriceId = minPriceProduct.Id; + model.MinPriceProductId = contextProduct.Id; model.ShowSku = _catalogSettings.ShowProductSku; model.ShowWeight = _catalogSettings.ShowWeight; model.ShowDimensions = _catalogSettings.ShowDimensions; - model.Sku = minPriceProduct.Sku; + model.Sku = contextProduct.Sku; model.Dimensions = res["Products.DimensionsValue"].Text.FormatCurrent( - minPriceProduct.Width.ToString("F2"), - minPriceProduct.Height.ToString("F2"), - minPriceProduct.Length.ToString("F2") + contextProduct.Width.ToString("F2"), + contextProduct.Height.ToString("F2"), + contextProduct.Length.ToString("F2") ); 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; model.ShowDeliveryTimes = _catalogSettings.ShowDeliveryTimesInProductLists; model.InvisibleDeliveryTime = (product.ProductType == ProductType.GroupedProduct); - model.IsShipEnabled = minPriceProduct.IsShipEnabled; - model.DisplayDeliveryTimeAccordingToStock = minPriceProduct.DisplayDeliveryTimeAccordingToStock(_catalogSettings); - model.StockAvailablity = minPriceProduct.FormatStockMessage(_localizationService); + model.IsShipEnabled = contextProduct.IsShipEnabled; + model.DisplayDeliveryTimeAccordingToStock = contextProduct.DisplayDeliveryTimeAccordingToStock(_catalogSettings); + model.StockAvailablity = contextProduct.FormatStockMessage(_localizationService); model.DisplayBasePrice = _catalogSettings.ShowBasePriceInProductLists; model.CompareEnabled = _catalogSettings.CompareProductsEnabled; @@ -1297,7 +1429,7 @@ from a in colorAttr.ProductVariantAttributeValues.Take(50) if (model.ShowDeliveryTimes) { - var deliveryTime = _deliveryTimeService.GetDeliveryTime(minPriceProduct); + var deliveryTime = _deliveryTimeService.GetDeliveryTime(contextProduct); if (deliveryTime != null) { model.DeliveryTimeName = deliveryTime.GetLocalized(x => x.Name); @@ -1307,36 +1439,38 @@ from a in colorAttr.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 = minPriceProduct.GetBasePriceInfo(_localizationService, _priceFormatter); + model.BasePriceInfo = contextProduct.GetBasePriceInfo(finalPrice, _localizationService, _priceFormatter, workingCurrency); } - var addShippingPrice = _currencyService.ConvertCurrency( - minPriceProduct.AdditionalShippingCharge, - _currencyService.GetCurrencyById(_currencySettings.PrimaryStoreCurrencyId), _services.WorkContext.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 (minPriceProduct.Weight > 0) + if (contextProduct.Weight > 0) { - model.Weight = "{0} {1}".FormatCurrent(minPriceProduct.Weight.ToString("F2"), _measureService.GetMeasureWeightById(_measureSettings.BaseWeightId).Name); + model.Weight = "{0} {1}".FormatCurrent(contextProduct.Weight.ToString("F2"), _measureService.GetMeasureWeightById(_measureSettings.BaseWeightId).Name); } // 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); } + return models; } @@ -1515,7 +1649,7 @@ public void PreparePagingFilteringModel(PagingFilteringModel model, PagingFilter foreach (ProductSortingEnum enumValue in Enum.GetValues(typeof(ProductSortingEnum))) { - if (enumValue == ProductSortingEnum.CreatedOnAsc) + if (enumValue == ProductSortingEnum.CreatedOnAsc || enumValue == ProductSortingEnum.Initial) { // TODO: (MC) das von uns eingeführte "CreatedOnAsc" schmeiß ich // jetzt deshalb aus der UI raus, weil wir diese Sortier-Option @@ -1639,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(); @@ -1664,16 +1799,9 @@ public List PrepareManufacturersOverviewModel( }; - Picture pic = manufacturer.Picture; - if (pic != null) + if (_catalogSettings.ShowManufacturerPicturesInProductDetail) { - item.PictureModel = new PictureModel - { - PictureId = pic.Id, - Title = T("Media.Product.ImageLinkTitleFormat", manufacturer.Name), - AlternateText = T("Media.Product.ImageAlternateTextFormat", manufacturer.Name), - ImageUrl = _pictureService.GetPictureUrl(pic), - }; + item.PictureModel = PrepareManufacturerPictureModel(manufacturer, manufacturer.GetLocalized(x => x.Name)); } cachedModels.Add(item.Id, item); @@ -1685,6 +1813,35 @@ public List PrepareManufacturersOverviewModel( return model; } + public PictureModel PrepareManufacturerPictureModel(Manufacturer manufacturer, string localizedName) + { + var model = new PictureModel(); + + var pictureSize = _mediaSettings.ManufacturerThumbPictureSize; + var manufacturerPictureCacheKey = string.Format(ModelCacheEventConsumer.MANUFACTURER_PICTURE_MODEL_KEY, + manufacturer.Id, + pictureSize, + !_catalogSettings.HideManufacturerDefaultPictures, + _services.WorkContext.WorkingLanguage.Id, + _services.WebHelper.IsCurrentConnectionSecured(), + _services.StoreContext.CurrentStore.Id); + + model = _services.Cache.Get(manufacturerPictureCacheKey, () => + { + var pictureModel = new PictureModel + { + PictureId = manufacturer.PictureId.GetValueOrDefault(), + //FullSizeImageUrl = _pictureService.GetPictureUrl(manufacturer.PictureId.GetValueOrDefault()), + 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) + }; + return pictureModel; + }); + + return model; + } + } #region Nested Classes diff --git a/src/Presentation/SmartStore.Web/Controllers/CheckoutController.cs b/src/Presentation/SmartStore.Web/Controllers/CheckoutController.cs index 8b1e37d65e..5047764d52 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CheckoutController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CheckoutController.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Web; using System.Web.Mvc; -using System.Web.Routing; using SmartStore.Core; using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; @@ -11,24 +10,24 @@ using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Payments; using SmartStore.Core.Domain.Shipping; -using SmartStore.Core.Plugins; +using SmartStore.Core.Html; +using SmartStore.Core.Logging; using SmartStore.Services.Catalog; using SmartStore.Services.Common; +using SmartStore.Services.Configuration; using SmartStore.Services.Customers; using SmartStore.Services.Directory; using SmartStore.Services.Localization; -using SmartStore.Core.Logging; using SmartStore.Services.Orders; using SmartStore.Services.Payments; 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; using SmartStore.Web.Models.Common; -using SmartStore.Services.Configuration; -using SmartStore.Web.Framework.Plugins; -using SmartStore.Web.Models.ShoppingCart; namespace SmartStore.Web.Controllers { @@ -205,6 +204,7 @@ protected CheckoutShippingMethodModel PrepareShippingMethodModel(IList !String.IsNullOrEmpty(so.Name) && so.Name.Equals(selectedShippingOption.Name, StringComparison.InvariantCultureIgnoreCase) && !String.IsNullOrEmpty(so.ShippingRateComputationMethodSystemName) && so.ShippingRateComputationMethodSystemName.Equals(selectedShippingOption.ShippingRateComputationMethodSystemName, StringComparison.InvariantCultureIgnoreCase)); + if (shippingOptionToSelect != null) shippingOptionToSelect.Selected = true; } @@ -268,6 +269,7 @@ protected CheckoutPaymentMethodModel PreparePaymentMethodModel(IList decimal.Zero) { model.DisplayRewardPoints = true; @@ -276,18 +278,22 @@ protected CheckoutPaymentMethodModel PreparePaymentMethodModel(IList pm.Value.PaymentMethodType == PaymentMethodType.Standard || pm.Value.PaymentMethodType == PaymentMethodType.Redirection || - pm.Value.PaymentMethodType == PaymentMethodType.StandardAndRedirection) + .LoadActivePaymentMethods(_workContext.CurrentCustomer, cart, _storeContext.CurrentStore.Id, paymentTypes) .ToList(); + var allPaymentMethods = _paymentService.GetAllPaymentMethods(); + foreach (var pm in boundPaymentMethods) { if (cart.IsRecurring() && pm.Value.RecurringPaymentType == RecurringPaymentType.NotSupported) continue; + + var paymentMethod = allPaymentMethods.FirstOrDefault(x => x.PaymentMethodSystemName.IsCaseInsensitiveEqual(pm.Metadata.SystemName)); - var pmModel = new CheckoutPaymentMethodModel.PaymentMethodModel() + var pmModel = new CheckoutPaymentMethodModel.PaymentMethodModel { Name = _pluginMediator.GetLocalizedFriendlyName(pm.Metadata), Description = _pluginMediator.GetLocalizedDescription(pm.Metadata), @@ -295,6 +301,11 @@ protected CheckoutPaymentMethodModel PreparePaymentMethodModel(IList x.FullDescription, _workContext.WorkingLanguage.Id); + } pmModel.BrandUrl = _pluginMediator.GetBrandImageUrl(pm.Metadata); @@ -349,6 +360,7 @@ protected CheckoutConfirmModel PrepareConfirmOrderModel(IList(_workContext.CurrentCustomer, SystemCustomerAttributeNames.SelectedShippingOption, null, _storeContext.CurrentStore.Id); return RedirectToAction("PaymentMethod"); - } - + } //model var model = PrepareShippingMethodModel(cart); @@ -619,9 +630,11 @@ public ActionResult SelectShippingMethod(string shippingoption) //parse selected method if (String.IsNullOrEmpty(shippingoption)) return ShippingMethod(); - var splittedOption = shippingoption.Split(new string[] { "___" }, StringSplitOptions.RemoveEmptyEntries); + + var splittedOption = shippingoption.Split(new string[] { "___" }, StringSplitOptions.RemoveEmptyEntries); if (splittedOption.Length != 2) return ShippingMethod(); + string selectedName = splittedOption[0]; string shippingRateComputationMethodSystemName = splittedOption[1]; @@ -643,8 +656,8 @@ public ActionResult SelectShippingMethod(string shippingoption) .ToList(); } - var shippingOption = shippingOptions - .Find(so => !String.IsNullOrEmpty(so.Name) && so.Name.Equals(selectedName, StringComparison.InvariantCultureIgnoreCase)); + var shippingOption = shippingOptions.Find(so => !String.IsNullOrEmpty(so.Name) && so.Name.Equals(selectedName, StringComparison.InvariantCultureIgnoreCase)); + if (shippingOption == null) return ShippingMethod(); @@ -681,7 +694,7 @@ public ActionResult PaymentMethod() _genericAttributeService.SaveAttribute( _workContext.CurrentCustomer, SystemCustomerAttributeNames.SelectedPaymentMethod, - (!isPaymentWorkflowRequired || !model.PaymentMethods.Any()) ? null : model.PaymentMethods[0].PaymentMethodSystemName, + !model.PaymentMethods.Any() ? null : model.PaymentMethods[0].PaymentMethodSystemName, _storeContext.CurrentStore.Id); _httpContext.GetCheckoutState().IsPaymentSelectionSkipped = true; @@ -783,94 +796,96 @@ public ActionResult Confirm() [ValidateInput(false)] public ActionResult ConfirmOrder(FormCollection form) { - //validation - var cart = _workContext.CurrentCustomer.GetCartItems(ShoppingCartType.ShoppingCart, _storeContext.CurrentStore.Id); + //validation + var storeId = _storeContext.CurrentStore.Id; + var customer = _workContext.CurrentCustomer; + var cart = customer.GetCartItems(ShoppingCartType.ShoppingCart, storeId); if (cart.Count == 0) return RedirectToRoute("ShoppingCart"); - if ((_workContext.CurrentCustomer.IsGuest() && !_orderSettings.AnonymousCheckoutAllowed)) + if ((customer.IsGuest() && !_orderSettings.AnonymousCheckoutAllowed)) return new HttpUnauthorizedResult(); - - //model var model = new CheckoutConfirmModel(); - try - { - bool isPaymentPaymentWorkflowRequired = IsPaymentWorkflowRequired(cart); + PlaceOrderResult placeOrderResult = null; + PostProcessPaymentRequest postProcessPaymentRequest = null; + try + { var processPaymentRequest = _httpContext.Session["OrderPaymentInfo"] as ProcessPaymentRequest; if (processPaymentRequest == null) { //Check whether payment workflow is required - if (isPaymentPaymentWorkflowRequired) + if (IsPaymentWorkflowRequired(cart)) return RedirectToAction("PaymentMethod"); processPaymentRequest = new ProcessPaymentRequest(); } //prevent 2 orders being placed within an X seconds time frame - if (!IsMinimumOrderPlacementIntervalValid(_workContext.CurrentCustomer)) - throw new Exception(_localizationService.GetResource("Checkout.MinOrderPlacementInterval")); + if (!IsMinimumOrderPlacementIntervalValid(customer)) + throw new Exception(T("Checkout.MinOrderPlacementInterval")); //place order - processPaymentRequest.StoreId = _storeContext.CurrentStore.Id; - processPaymentRequest.CustomerId = _workContext.CurrentCustomer.Id; - processPaymentRequest.PaymentMethodSystemName = _workContext.CurrentCustomer.GetAttribute( - 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); + placeOrderResult = _orderProcessingService.PlaceOrder(processPaymentRequest, placeOrderExtraData); - if (placeOrderResult.Success) + if (!placeOrderResult.Success) { - if (isPaymentPaymentWorkflowRequired) - { - var postProcessPaymentRequest = new PostProcessPaymentRequest() - { - Order = placeOrderResult.PlacedOrder - }; - _paymentService.PostProcessPayment(postProcessPaymentRequest); - } - - _httpContext.Session["PaymentData"] = null; - _httpContext.Session["OrderPaymentInfo"] = null; - _httpContext.RemoveCheckoutState(); - - if (_webHelper.IsRequestBeingRedirected || _webHelper.IsPostBeingDone) - { - //redirection or POST has been done in PostProcessPayment - return Content("Redirected"); - } - else - { - //if no redirection has been done (to a third-party payment page) - //theoretically it's not possible - return RedirectToAction("Completed"); - } - } - else - { - foreach (var error in placeOrderResult.Errors) - model.Warnings.Add(error); + 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); + } } - //If we got this far, something failed, redisplay form + if (placeOrderResult == null || !placeOrderResult.Success || model.Warnings.Any()) + { + return View(model); + } - //if (model.Warnings.Count > 0) - // TempData["ConfirmOrderWarnings"] = model.Warnings; + try + { + postProcessPaymentRequest = new PostProcessPaymentRequest + { + Order = placeOrderResult.PlacedOrder + }; - //return RedirectToRoute("CheckoutConfirm"); - return View(model); - } + _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 ba7a63234c..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,15 +150,11 @@ public CommonController( this._themeSettings = themeSettings; this._pageAssetsBuilder = pageAssetsBuilder; this._pictureService = pictureService; - this._services = services; - - T = NullLocalizer.Instance; + this._manufacturerService = manufacturerService; + this._categoryService = categoryService; + this._productService = productService; } - public Localizer T { get; set; } - - #endregion - #region Utilities [NonAction] @@ -154,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 @@ -165,7 +179,7 @@ protected LanguageSelectorModel PrepareLanguageSelectorModel() var workingLanguage = _services.WorkContext.WorkingLanguage; - var model = new LanguageSelectorModel() + var model = new LanguageSelectorModel { CurrentLanguageId = workingLanguage.Id, AvailableLanguages = availableLanguages, @@ -176,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) { @@ -196,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() @@ -230,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; @@ -244,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() { @@ -363,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) { @@ -383,10 +376,9 @@ public ActionResult SetLanguage(int langid, string returnUrl = "") returnUrl = helper.GetAbsolutePath(); } - return Redirect(returnUrl); + return RedirectToReferrer(returnUrl); } - //currency [ChildActionOnly] public ActionResult CurrencySelector() { @@ -397,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) @@ -510,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) @@ -546,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), @@ -559,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(); @@ -570,7 +564,7 @@ public ActionResult ShopBar() if (_catalogSettings.CompareProductsEnabled) { - model.CompareItems = EngineContext.Current.Resolve().GetComparedProductsCount(); + model.CompareItems = _compareProductsService.Value.GetComparedProductsCount(); } return PartialView(model); @@ -579,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) @@ -601,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, @@ -609,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()) { @@ -616,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); - } - } + 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; - var socialSettings = EngineContext.Current.Resolve(); - - 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); } @@ -647,6 +644,7 @@ public ActionResult Footer() [ChildActionOnly] public ActionResult Menu() { + var store = _services.StoreContext.CurrentStore; var customer = _services.WorkContext.CurrentCustomer; var model = new MenuModel @@ -661,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); } @@ -670,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, @@ -681,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] @@ -726,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] @@ -774,15 +784,16 @@ public ActionResult Favicon() ///
    /// True - use desktop version; false - use version for mobile devices /// Action result + [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] @@ -805,55 +816,56 @@ public ActionResult RobotsTextFile() { "/bin/", "/Content/files/", - "/Content/files/exportimport/", - "/country/getstatesbycountryid", - "/install", - "/product/setreviewhelpfulness", + "/Content/files/ExportImport/", + "/Exchange/", + "/Country/GetStatesByCountryId", + "/Install", + "/Product/SetReviewHelpfulness", }; var localizableDisallowPaths = new List() { - "/boards/forumwatch", - "/boards/postedit", - "/boards/postdelete", - "/boards/postcreate", - "/boards/topicedit", - "/boards/topicdelete", - "/boards/topiccreate", - "/boards/topicmove", - "/boards/topicwatch", - "/cart", - "/checkout", - "/product/clearcomparelist", - "/compareproducts", - "/customer/avatar", - "/customer/activation", - "/customer/addresses", - "/customer/backinstocksubscriptions", - "/customer/changepassword", - "/customer/checkusernameavailability", - "/customer/downloadableproducts", - "/customer/forumsubscriptions", - "/customer/deleteforumsubscriptions", - "/customer/info", - "/customer/orders", - "/customer/returnrequests", - "/customer/rewardpoints", - "/privatemessages", - "/newsletter/subscriptionactivation", - "/onepagecheckout", - "/order", - "/passwordrecovery", - "/poll/vote", - "/privatemessages", - "/returnrequest", - "/newsletter/subscribe", - "/topic/authenticate", - "/wishlist", - "/product/askquestion", - "/product/emailafriend", - "/search", - "/config", - "/settings" + "/Boards/ForumWatch", + "/Boards/PostEdit", + "/Boards/PostDelete", + "/Boards/PostCreate", + "/Boards/TopicEdit", + "/Boards/TopicDelete", + "/Boards/TopicCreate", + "/Boards/TopicMove", + "/Boards/TopicWatch", + "/Cart", + "/Checkout", + "/Product/ClearCompareList", + "/CompareProducts", + "/Customer/Avatar", + "/Customer/Activation", + "/Customer/Addresses", + "/Customer/BackInStockSubscriptions", + "/Customer/ChangePassword", + "/Customer/CheckUsernameAvailability", + "/Customer/DownloadableProducts", + "/Customer/ForumSubscriptions", + "/Customer/DeleteForumSubscriptions", + "/Customer/Info", + "/Customer/Orders", + "/Customer/ReturnRequests", + "/Customer/RewardPoints", + "/PrivateMessages", + "/Newsletter/SubscriptionActivation", + "/Order", + "/PasswordRecovery", + "/Poll/Vote", + "/ReturnRequest", + "/Newsletter/Subscribe", + "/Topic/Authenticate", + "/Wishlist", + "/Product/AskQuestion", + "/Product/EmailAFriend", + "/Search", + "/Config", + "/Settings", + "/Login", + "/Register" }; @@ -864,7 +876,8 @@ public ActionResult RobotsTextFile() sb.AppendFormat("Sitemap: {0}", Url.RouteUrl("SitemapSEO", (object)null, _securitySettings.Value.ForceSslForAllPages ? "https" : "http")); sb.AppendLine(); - var disallows = disallowPaths.Concat(localizableDisallowPaths); + var disallows = disallowPaths.Concat(localizableDisallowPaths); + if (_localizationSettings.SeoFriendlyUrlsForLanguagesEnabled) { // URLs are localizable. Append SEO code @@ -879,6 +892,9 @@ public ActionResult RobotsTextFile() // append extra disallows disallows = disallows.Concat(seoSettings.ExtraRobotsDisallows.Select(x => x.Trim())); + // Append all lowercase variants (at least Google is case sensitive) + disallows = disallows.Concat(GetLowerCaseVariants(disallows)); + foreach (var disallow in disallows) { sb.AppendFormat("Disallow: {0}", disallow); @@ -890,6 +906,21 @@ public ActionResult RobotsTextFile() return null; } + private IEnumerable GetLowerCaseVariants(IEnumerable disallows) + { + var other = new List(); + foreach (var item in disallows) + { + var lower = item.ToLower(); + if (lower != item) + { + other.Add(lower); + } + } + + return other; + } + public ActionResult GenericUrl() { // seems that no entity was found @@ -993,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 4f61b36436..a72b4fc872 100644 --- a/src/Presentation/SmartStore.Web/Controllers/CustomerController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/CustomerController.cs @@ -4,15 +4,16 @@ using System.Web; using System.Web.Mvc; using SmartStore.Core; +using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Common; using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Forums; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Media; using SmartStore.Core.Domain.Messages; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Tax; +using SmartStore.Core.Logging; using SmartStore.Services.Authentication; using SmartStore.Services.Authentication.External; using SmartStore.Services.Catalog; @@ -27,18 +28,18 @@ using SmartStore.Services.Orders; using SmartStore.Services.Seo; 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; using SmartStore.Web.Models.Common; using SmartStore.Web.Models.Customer; -using SmartStore.Core.Logging; -using SmartStore.Web.Framework.Plugins; -using SmartStore.Utilities; namespace SmartStore.Web.Controllers { - public partial class CustomerController : PublicControllerBase + public partial class CustomerController : PublicControllerBase { #region Fields @@ -75,6 +76,7 @@ public partial class CustomerController : PublicControllerBase private readonly IDownloadService _downloadService; private readonly IWebHelper _webHelper; private readonly ICustomerActivityService _customerActivityService; + private readonly IProductAttributeParser _productAttributeParser; private readonly MediaSettings _mediaSettings; private readonly IWorkflowMessageService _workflowMessageService; @@ -107,7 +109,9 @@ public CustomerController(IAuthenticationService authenticationService, IOpenAuthenticationService openAuthenticationService, IBackInStockSubscriptionService backInStockSubscriptionService, IDownloadService downloadService, IWebHelper webHelper, - ICustomerActivityService customerActivityService, MediaSettings mediaSettings, + ICustomerActivityService customerActivityService, + IProductAttributeParser productAttributeParser, + MediaSettings mediaSettings, IWorkflowMessageService workflowMessageService, LocalizationSettings localizationSettings, CaptchaSettings captchaSettings, ExternalAuthenticationSettings externalAuthenticationSettings, PluginMediator pluginMediator) @@ -145,6 +149,7 @@ public CustomerController(IAuthenticationService authenticationService, this._downloadService = downloadService; this._webHelper = webHelper; this._customerActivityService = customerActivityService; + this._productAttributeParser = productAttributeParser; this._mediaSettings = mediaSettings; this._workflowMessageService = workflowMessageService; @@ -241,6 +246,7 @@ protected void PrepareCustomerInfoModel(CustomerInfoModel model, Customer custom model.StateProvinceId = customer.GetAttribute(SystemCustomerAttributeNames.StateProvinceId); model.Phone = customer.GetAttribute(SystemCustomerAttributeNames.Phone); model.Fax = customer.GetAttribute(SystemCustomerAttributeNames.Fax); + model.CustomerNumber = customer.GetAttribute(SystemCustomerAttributeNames.CustomerNumber); //newsletter var newsletter = _newsLetterSubscriptionService.GetNewsLetterSubscriptionByEmail(customer.Email, _storeContext.CurrentStore.Id); @@ -311,6 +317,19 @@ protected void PrepareCustomerInfoModel(CustomerInfoModel model, Customer custom model.AllowUsersToChangeUsernames = _customerSettings.AllowUsersToChangeUsernames; model.CheckUsernameAvailabilityEnabled = _customerSettings.CheckUsernameAvailabilityEnabled; model.SignatureEnabled = _forumSettings.ForumsEnabled && _forumSettings.SignaturesEnabled; + model.DisplayCustomerNumber = _customerSettings.CustomerNumberMethod != CustomerNumberMethod.Disabled + && _customerSettings.CustomerNumberVisibility != CustomerNumberVisibility.None; + + if (_customerSettings.CustomerNumberMethod != CustomerNumberMethod.Disabled + && (_customerSettings.CustomerNumberVisibility == CustomerNumberVisibility.Editable + || (_customerSettings.CustomerNumberVisibility == CustomerNumberVisibility.EditableIfEmpty && String.IsNullOrEmpty(model.CustomerNumber)))) + { + model.CustomerNumberEnabled = true; + } + else + { + model.CustomerNumberEnabled = false; + } //external authentication foreach (var ear in _openAuthenticationService.GetExternalIdentifiersFor(customer)) @@ -333,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); - 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(), @@ -439,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 { @@ -468,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; @@ -528,15 +554,15 @@ public ActionResult Register(RegisterModel model, string returnUrl, bool captcha if (_workContext.CurrentCustomer.IsRegistered()) { - //Already registered customer. + // Already registered customer. _authenticationService.SignOut(); - //Save a new record + // Save a new record _workContext.CurrentCustomer = _customerService.InsertGuestCustomer(); } var customer = _workContext.CurrentCustomer; - //validate CAPTCHA + // validate CAPTCHA if (_captchaSettings.Enabled && _captchaSettings.ShowOnRegistrationPage && !captchaValid) { ModelState.AddModelError("", _localizationService.GetResource("Common.WrongCaptcha")); @@ -555,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); @@ -571,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); @@ -609,11 +635,13 @@ public ActionResult Register(RegisterModel model, string returnUrl, bool captcha _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.Phone, model.Phone); if (_customerSettings.FaxEnabled) _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.Fax, model.Fax); + 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) { @@ -632,7 +660,7 @@ public ActionResult Register(RegisterModel model, string returnUrl, bool captcha { if (model.Newsletter) { - _newsLetterSubscriptionService.InsertNewsLetterSubscription(new NewsLetterSubscription() + _newsLetterSubscriptionService.InsertNewsLetterSubscription(new NewsLetterSubscription { NewsLetterSubscriptionGuid = Guid.NewGuid(), Email = model.Email, @@ -692,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: @@ -705,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)) @@ -731,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; @@ -801,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); } @@ -874,14 +900,14 @@ public ActionResult AccountActivation(string token, string email) { var customer = _customerService.GetCustomerByEmail(email); if (customer == null) - return RedirectToRoute("HomePage"); + return RedirectToHomePageWithError("Email"); var cToken = customer.GetAttribute(SystemCustomerAttributeNames.AccountActivationToken); if (String.IsNullOrEmpty(cToken)) - return RedirectToRoute("HomePage"); + return RedirectToHomePageWithError("Token"); if (!cToken.Equals(token, StringComparison.InvariantCultureIgnoreCase)) - return RedirectToRoute("HomePage"); + return RedirectToHomePageWithError("Token"); //activate user account customer.Active = true; @@ -889,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); } @@ -952,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)) { @@ -1002,9 +1027,28 @@ public ActionResult Info(CustomerInfoModel model) //form fields if (_customerSettings.GenderEnabled) + { _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.Gender, model.Gender); + } + + if (_customerSettings.CustomerNumberMethod != CustomerNumberMethod.Disabled) + { + var customerNumbers = _genericAttributeService.GetAttributes(SystemCustomerAttributeNames.CustomerNumber, "customer"); + var currentCustomerNumber = customer.GetAttribute(SystemCustomerAttributeNames.CustomerNumber); + + if (model.CustomerNumber != currentCustomerNumber && customerNumbers.Where(x => x.Value == model.CustomerNumber).Any()) + { + NotifyError("Common.CustomerNumberAlreadyExists"); + } + else + { + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.CustomerNumber, model.CustomerNumber); + } + } + _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.FirstName, model.FirstName); _genericAttributeService.SaveAttribute(customer, SystemCustomerAttributeNames.LastName, model.LastName); + if (_customerSettings.DateOfBirthEnabled) { DateTime? dateOfBirth = null; @@ -1037,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"); } @@ -1242,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); } @@ -1261,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) @@ -1276,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 @@ -1311,8 +1332,9 @@ public ActionResult ReturnRequests() if (orderItem != null) { var product = orderItem.Product; + var attributeQueryData = new List>(); - var itemModel = new CustomerReturnRequestsModel.ReturnRequestModel() + var itemModel = new CustomerReturnRequestsModel.ReturnRequestModel { Id = returnRequest.Id, ReturnRequestStatus = returnRequest.ReturnRequestStatus.GetLocalizedEnum(_localizationService, _workContext), @@ -1323,8 +1345,22 @@ public ActionResult ReturnRequests() ReturnAction = returnRequest.RequestedAction, ReturnReason = returnRequest.ReasonForReturn, Comments = returnRequest.CustomerComments, - CreatedOn = _dateTimeHelper.ConvertToUserTime(returnRequest.CreatedOnUtc, DateTimeKind.Utc), + CreatedOn = _dateTimeHelper.ConvertToUserTime(returnRequest.CreatedOnUtc, DateTimeKind.Utc) }; + + if (orderItem.Product.ProductType != ProductType.BundledProduct) + { + _productAttributeParser.DeserializeQueryData(attributeQueryData, orderItem.AttributesXml, orderItem.ProductId); + } + else if (orderItem.Product.BundlePerItemPricing && orderItem.BundleData.HasValue()) + { + var bundleData = orderItem.GetBundleData(); + + bundleData.ForEach(x => _productAttributeParser.DeserializeQueryData(attributeQueryData, x.AttributesXml, x.ProductId, x.BundleItemId)); + } + + itemModel.ProductUrl = _productAttributeParser.GetProductUrlWithAttributes(attributeQueryData, itemModel.ProductSeName); + model.Items.Add(itemModel); } } @@ -1347,11 +1383,12 @@ public ActionResult DownloadableProducts() var model = new CustomerDownloadableProductsModel(); model.NavigationModel = GetCustomerNavigationModel(customer); model.NavigationModel.SelectedTab = CustomerNavigationEnum.DownloadableProducts; - var items = _orderService.GetAllOrderItems(null, customer.Id, null, null, - null, null, null, true); + + var items = _orderService.GetAllOrderItems(null, customer.Id, null, null, null, null, null, true); + foreach (var item in items) { - var itemModel = new CustomerDownloadableProductsModel.DownloadableProductsModel() + var itemModel = new CustomerDownloadableProductsModel.DownloadableProductsModel { OrderItemGuid = item.OrderItemGuid, OrderId = item.OrderId, @@ -1361,6 +1398,9 @@ public ActionResult DownloadableProducts() ProductAttributes = item.AttributeDescription, ProductId = item.ProductId }; + + itemModel.ProductUrl = _productAttributeParser.GetProductUrlWithAttributes(item.AttributesXml, item.ProductId, itemModel.ProductSeName); + model.Items.Add(itemModel); if (_downloadService.IsDownloadAllowed(item)) @@ -1380,11 +1420,11 @@ public ActionResult UserAgreement(Guid id /* orderItemId */) var orderItem = _orderService.GetOrderItemByGuid(id); if (orderItem == null) - return RedirectToRoute("HomePage"); + return RedirectToHomePageWithError("Guid"); var product = orderItem.Product; if (product == null || !product.HasUserAgreement) - return RedirectToRoute("HomePage"); + return RedirectToHomePageWithError("Product"); var model = new UserAgreementModel(); model.UserAgreementText = product.UserAgreementText; @@ -1528,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.GetPictureBits(); - if (customerAvatar != null) - customerAvatar = _pictureService.UpdatePicture(customerAvatar.Id, customerPictureBinary, uploadedFile.ContentType, null, true); - else - customerAvatar = _pictureService.InsertPicture(customerPictureBinary, uploadedFile.ContentType, null, true); - } + 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))); + + byte[] customerPictureBinary = uploadedFile.InputStream.ToByteArray(); - int customerAvatarId = 0; - if (customerAvatar != null) - customerAvatarId = customerAvatar.Id; + 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( - customer.GetAttribute(SystemCustomerAttributeNames.AvatarPictureId), - _mediaSettings.AvatarPictureSize, - false); - return View(model); + model.AvatarUrl = _pictureService.GetPictureUrl(customerAvatarId, _mediaSettings.AvatarPictureSize, false); + + return View(model); } catch (Exception exc) { @@ -1559,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); } @@ -1587,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"); @@ -1636,14 +1677,14 @@ public ActionResult PasswordRecoveryConfirm(string token, string email) { var customer = _customerService.GetCustomerByEmail(email); if (customer == null ) - return RedirectToRoute("HomePage"); + return RedirectToHomePageWithError("Email"); var cPrt = customer.GetAttribute(SystemCustomerAttributeNames.PasswordRecoveryToken); if (String.IsNullOrEmpty(cPrt)) - return RedirectToRoute("HomePage"); + return RedirectToHomePageWithError("Token"); if (!cPrt.Equals(token, StringComparison.InvariantCultureIgnoreCase)) - return RedirectToRoute("HomePage"); + return RedirectToHomePageWithError("Token"); var model = new PasswordRecoveryConfirmModel(); return View(model); @@ -1655,14 +1696,14 @@ public ActionResult PasswordRecoveryConfirmPOST(string token, string email, Pass { var customer = _customerService.GetCustomerByEmail(email); if (customer == null) - return RedirectToRoute("HomePage"); + return RedirectToHomePageWithError("Email"); var cPrt = customer.GetAttribute(SystemCustomerAttributeNames.PasswordRecoveryToken); if (String.IsNullOrEmpty(cPrt)) - return RedirectToRoute("HomePage"); + return RedirectToHomePageWithError("Token"); if (!cPrt.Equals(token, StringComparison.InvariantCultureIgnoreCase)) - return RedirectToRoute("HomePage"); + return RedirectToHomePageWithError("Token"); if (ModelState.IsValid) { 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 01a4528ad5..e122492826 100644 --- a/src/Presentation/SmartStore.Web/Controllers/FilterController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/FilterController.cs @@ -18,6 +18,14 @@ public FilterController(IFilterService filterService, CatalogSettings catalogSet _catalogSettings = catalogSettings; } + private bool IsShowAllText(ICollection criteriaGroup) + { + if (criteriaGroup.Any(c => c.Entity.IsCaseInsensitiveEqual(FilterService.ShortcutPrice))) + return false; + + return (criteriaGroup.Count >= _catalogSettings.MaxFilterItemsToDisplay || criteriaGroup.Any(c => !c.IsInactive)); + } + public ActionResult Products(string filter, int categoryID, string path, int? pagesize, int? orderby, string viewmode) { var context = _filterService.CreateFilterProductContext(filter, categoryID, path, pagesize, orderby, viewmode); @@ -39,7 +47,7 @@ public ActionResult Products(string active, string inactive, int categoryID, int // TODO: needed later for ajax based filtering... see example below //System.Threading.Thread.Sleep(3000); - var context = new FilterProductContext() + var context = new FilterProductContext { ParentCategoryID = categoryID, CategoryIds = new List { categoryID }, @@ -74,20 +82,13 @@ public ActionResult ProductsMultiSelect(string filter, int categoryID, string pa _filterService.ProductFilterableMultiSelect(context, filterMultiSelect); - return PartialView(new ProductFilterModel { + return PartialView(new ProductFilterModel + { Context = context, IsShowAllText = IsShowAllText(context.Criteria), MaxFilterItemsToDisplay = _catalogSettings.MaxFilterItemsToDisplay, ExpandAllFilterGroups = _catalogSettings.ExpandAllFilterCriteria }); } - - private bool IsShowAllText(ICollection criteriaGroup) - { - if (criteriaGroup.Any(c => c.Entity == 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 745da0006d..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,14 +71,11 @@ public HomeController( this._sitemapGenerator = sitemapGenerator; this._captchaSettings = captchaSettings; this._commonSettings = commonSettings; - - T = NullLocalizer.Instance; + this._customerSettings = customerSettings; } #endregion - public Localizer T { get; set; } - [RequireHttpsByConfigAttribute(SslRequirement.No)] public ActionResult Index() { @@ -90,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 && @@ -123,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); @@ -145,9 +147,7 @@ public ActionResult ContactUsSend(ContactUsModel model, bool captchaValid) string fullName = model.FullName; string subject = T("ContactUs.EmailSubject", _services.StoreContext.CurrentStore.Name); - var emailAccount = _emailAccountService.Value.GetEmailAccountById(EngineContext.Current.Resolve().DefaultEmailAccountId); - if (emailAccount == null) - emailAccount = _emailAccountService.Value.GetAllEmailAccounts().FirstOrDefault(); + var emailAccount = _emailAccountService.Value.GetDefaultEmailAccount(); string from = null; string fromName = null; @@ -254,7 +254,6 @@ public ActionResult Sitemap() Id = product.Id, Name = product.GetLocalized(x => x.Name).EmptyNull(), ShortDescription = product.GetLocalized(x => x.ShortDescription), - FullDescription = product.GetLocalized(x => x.FullDescription), SeName = product.GetSeName(), }).ToList(); } diff --git a/src/Presentation/SmartStore.Web/Controllers/InstallController.cs b/src/Presentation/SmartStore.Web/Controllers/InstallController.cs index 1ed1d7d418..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; @@ -545,7 +540,7 @@ protected virtual InstallationResult InstallCore(ILifetimeScope scope, InstallMo var pluginsCount = plugins.Count; var idx = 0; - using (var dbScope = new DbContextScope(autoDetectChanges: false)) { + using (var dbScope = new DbContextScope(autoDetectChanges: false, hooksEnabled: false)) { foreach (var plugin in plugins) { try diff --git a/src/Presentation/SmartStore.Web/Controllers/KeepAliveController.cs b/src/Presentation/SmartStore.Web/Controllers/KeepAliveController.cs deleted file mode 100644 index 3093193eb9..0000000000 --- a/src/Presentation/SmartStore.Web/Controllers/KeepAliveController.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Web.Mvc; - -namespace SmartStore.Web.Controllers -{ - public partial class KeepAliveController : Controller - { - public ActionResult Index() - { - return Content("I am alive!"); - } - } -} diff --git a/src/Presentation/SmartStore.Web/Controllers/NewsController.cs b/src/Presentation/SmartStore.Web/Controllers/NewsController.cs index 84d2ac0257..b285065191 100644 --- a/src/Presentation/SmartStore.Web/Controllers/NewsController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/NewsController.cs @@ -5,23 +5,24 @@ using System.Web.Mvc; using SmartStore.Core; using SmartStore.Core.Caching; -using SmartStore.Core.Domain; using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Media; using SmartStore.Core.Domain.News; +using SmartStore.Core.Logging; 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.News; 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; @@ -46,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; @@ -64,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) @@ -80,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,28 +212,43 @@ public ActionResult List(NewsPagingFilteringModel command) return View(model); } - [ActionName("rss")] + [ActionName("rss"), Compress] public ActionResult ListRss(int languageId) { - var feed = new SyndicationFeed( - string.Format("{0}: News", _storeContext.CurrentStore.Name), - "News", - new Uri(_webHelper.GetStoreLocation(false)), - "NewsRSS", - DateTime.UtcNow); + 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); - if (!_newsSettings.Enabled) - return new RssActionResult() { Feed = feed }; + var title = "{0} - News".FormatInvariant(_storeContext.CurrentStore.Name); - var items = new List(); - var newsItems = _newsService.GetAllNews(languageId, _storeContext.CurrentStore.Id, 0, int.MaxValue); - foreach (var n in newsItems) - { - string newsUrl = Url.RouteUrl("NewsItem", new { SeName = n.GetSeName(n.LanguageId, ensureTwoPublishedLanguages: false) }, "http"); - items.Add(new SyndicationItem(n.Title, n.Short, new Uri(newsUrl), String.Format("Blog:{0}", n.Id), n.CreatedOnUtc)); - } - feed.Items = items; - return new RssActionResult() { Feed = feed }; + 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 }; } public ActionResult NewsItem(int newsItemId) @@ -300,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); @@ -319,7 +336,8 @@ public ActionResult RssHeaderLink() return Content(""); string link = string.Format("", - Url.Action("rss", null, new { languageId = _workContext.WorkingLanguage.Id }, _webHelper.IsCurrentConnectionSecured() ? "https" : "http"), _storeContext.CurrentStore.Name); + Url.Action("rss", null, new { languageId = _workContext.WorkingLanguage.Id }, _webHelper.IsCurrentConnectionSecured() ? "https" : "http"), + _storeContext.CurrentStore.Name); return Content(link); } 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 57c7c32649..448a1983d3 100644 --- a/src/Presentation/SmartStore.Web/Controllers/OrderController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/OrderController.cs @@ -10,7 +10,6 @@ using SmartStore.Core.Domain.Shipping; using SmartStore.Core.Domain.Tax; using SmartStore.Core.Html; -using SmartStore.Core.Localization; using SmartStore.Services; using SmartStore.Services.Catalog; using SmartStore.Services.Directory; @@ -25,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; @@ -48,6 +48,7 @@ public partial class OrderController : PublicControllerBase private readonly ICountryService _countryService; private readonly IProductService _productService; private readonly IProductAttributeFormatter _productAttributeFormatter; + private readonly IProductAttributeParser _productAttributeParser; private readonly IStoreService _storeService; private readonly ICheckoutAttributeFormatter _checkoutAttributeFormatter; private readonly PluginMediator _pluginMediator; @@ -73,6 +74,7 @@ public OrderController( IStoreService storeService, IProductService productService, IProductAttributeFormatter productAttributeFormatter, + IProductAttributeParser productAttributeParser, Lazy pictureService, PluginMediator pluginMediator, ICommonServices services, @@ -90,16 +92,14 @@ public OrderController( this._countryService = countryService; this._productService = productService; this._productAttributeFormatter = productAttributeFormatter; + this._productAttributeParser = productAttributeParser; this._storeService = storeService; this._checkoutAttributeFormatter = checkoutAttributeFormatter; this._pluginMediator = pluginMediator; this._services = services; this._quantityUnitService = quantityUnitService; - T = NullLocalizer.Instance; } - public Localizer T { get; set; } - #endregion #region Utilities @@ -111,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); @@ -171,7 +172,7 @@ protected OrderDetailsModel PrepareOrderDetailsModel(Order order) model.CanRePostProcessPayment = _paymentService.CanRePostProcessPayment(order); //purchase order number (we have to find a better to inject this information because it's related to a certain plugin) - if (paymentMethod != null && paymentMethod.Metadata.SystemName.Equals("Payments.PurchaseOrder", StringComparison.InvariantCultureIgnoreCase)) + if (paymentMethod != null && paymentMethod.Metadata.SystemName.Equals("SmartStore.PurchaseOrderNumber", StringComparison.InvariantCultureIgnoreCase)) { model.DisplayPurchaseOrderNumber = true; model.PurchaseOrderNumber = order.PurchaseOrderNumber; @@ -184,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; @@ -252,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)); @@ -338,23 +355,26 @@ 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); var shippingSettings = _services.Settings.LoadSetting(store.Id); - var model = new ShipmentDetailsModel(); - - model.Id = shipment.Id; + var model = new ShipmentDetailsModel + { + Id = shipment.Id, + TrackingNumber = shipment.TrackingNumber + }; + if (shipment.ShippedDateUtc.HasValue) model.ShippedDate = _dateTimeHelper.ConvertToUserTime(shipment.ShippedDateUtc.Value, DateTimeKind.Utc); + if (shipment.DeliveryDateUtc.HasValue) model.DeliveryDate = _dateTimeHelper.ConvertToUserTime(shipment.DeliveryDateUtc.Value, DateTimeKind.Utc); - //tracking number and shipment information - model.TrackingNumber = shipment.TrackingNumber; var srcm = _shippingService.LoadShippingRateComputationMethodBySystemName(order.ShippingRateComputationMethodSystemName); + if (srcm != null && srcm.IsShippingRateComputationMethodActive(shippingSettings)) { var shipmentTracker = srcm.Value.ShipmentTracker; @@ -364,25 +384,30 @@ protected ShipmentDetailsModel PrepareShipmentDetailsModel(Shipment shipment) if (shippingSettings.DisplayShipmentEventsToCustomers) { var shipmentEvents = shipmentTracker.GetShipmentEvents(shipment.TrackingNumber); - if (shipmentEvents != null) - foreach (var shipmentEvent in shipmentEvents) - { - var shipmentStatusEventModel = new ShipmentDetailsModel.ShipmentStatusEventModel(); - var shipmentEventCountry = _countryService.GetCountryByTwoLetterIsoCode(shipmentEvent.CountryCode); - shipmentStatusEventModel.Country = shipmentEventCountry != null - ? shipmentEventCountry.GetLocalized(x => x.Name) - : shipmentEvent.CountryCode; - shipmentStatusEventModel.Date = shipmentEvent.Date; - shipmentStatusEventModel.EventName = shipmentEvent.EventName; - shipmentStatusEventModel.Location = shipmentEvent.Location; - model.ShipmentStatusEvents.Add(shipmentStatusEventModel); - } + if (shipmentEvents != null) + { + foreach (var shipmentEvent in shipmentEvents) + { + var shipmentEventCountry = _countryService.GetCountryByTwoLetterIsoCode(shipmentEvent.CountryCode); + + var shipmentStatusEventModel = new ShipmentDetailsModel.ShipmentStatusEventModel + { + Country = (shipmentEventCountry != null ? shipmentEventCountry.GetLocalized(x => x.Name) : shipmentEvent.CountryCode), + Date = shipmentEvent.Date, + EventName = shipmentEvent.EventName, + Location = shipmentEvent.Location + }; + + model.ShipmentStatusEvents.Add(shipmentStatusEventModel); + } + } } } } //products in this shipment model.ShowSku = catalogSettings.ShowProductSku; + foreach (var shipmentItem in shipment.ShipmentItems) { var orderItem = _orderService.GetOrderItemById(shipmentItem.OrderItemId); @@ -390,7 +415,10 @@ protected ShipmentDetailsModel PrepareShipmentDetailsModel(Shipment shipment) continue; orderItem.Product.MergeWithCombination(orderItem.AttributesXml); - var shipmentItemModel = new ShipmentDetailsModel.ShipmentItemModel() + + var attributeQueryData = new List>(); + + var shipmentItemModel = new ShipmentDetailsModel.ShipmentItemModel { Id = shipmentItem.Id, Sku = orderItem.Product.Sku, @@ -399,12 +427,25 @@ protected ShipmentDetailsModel PrepareShipmentDetailsModel(Shipment shipment) ProductSeName = orderItem.Product.GetSeName(), AttributeInfo = orderItem.AttributeDescription, QuantityOrdered = orderItem.Quantity, - QuantityShipped = shipmentItem.Quantity, + QuantityShipped = shipmentItem.Quantity }; + + if (orderItem.Product.ProductType != ProductType.BundledProduct) + { + _productAttributeParser.DeserializeQueryData(attributeQueryData, orderItem.AttributesXml, orderItem.ProductId); + } + else if (orderItem.Product.BundlePerItemPricing && orderItem.BundleData.HasValue()) + { + var bundleData = orderItem.GetBundleData(); + + bundleData.ForEach(x => _productAttributeParser.DeserializeQueryData(attributeQueryData, x.AttributesXml, x.ProductId, x.BundleItemId)); + } + + shipmentItemModel.ProductUrl = _productAttributeParser.GetProductUrlWithAttributes(attributeQueryData, shipmentItemModel.ProductSeName); + model.Items.Add(shipmentItemModel); } - //order details model model.Order = PrepareOrderDetailsModel(order); return model; @@ -412,9 +453,11 @@ protected ShipmentDetailsModel PrepareShipmentDetailsModel(Shipment shipment) private OrderDetailsModel.OrderItemModel PrepareOrderItemModel(Order order, OrderItem orderItem) { + var attributeQueryData = new List>(); + orderItem.Product.MergeWithCombination(orderItem.AttributesXml); - var model = new OrderDetailsModel.OrderItemModel() + var model = new OrderDetailsModel.OrderItemModel { Id = orderItem.Id, Sku = orderItem.Product.Sku, @@ -426,8 +469,13 @@ private OrderDetailsModel.OrderItemModel PrepareOrderItemModel(Order order, Orde AttributeInfo = orderItem.AttributeDescription }; + if (orderItem.Product.ProductType != ProductType.BundledProduct) + { + _productAttributeParser.DeserializeQueryData(attributeQueryData, orderItem.AttributesXml, orderItem.ProductId); + } + var quantityUnit = _quantityUnitService.GetQuantityUnitById(orderItem.Product.QuantityUnitId); - model.QuantityUnit = quantityUnit == null ? "" : quantityUnit.GetLocalized(x => x.Name); + model.QuantityUnit = (quantityUnit == null ? "" : quantityUnit.GetLocalized(x => x.Name)); if (orderItem.Product.ProductType == ProductType.BundledProduct && orderItem.BundleData.HasValue()) { @@ -438,7 +486,7 @@ private OrderDetailsModel.OrderItemModel PrepareOrderItemModel(Order order, Orde foreach (var bundleItem in bundleData) { - var bundleItemModel = new OrderDetailsModel.BundleItemModel() + var bundleItemModel = new OrderDetailsModel.BundleItemModel { Sku = bundleItem.Sku, ProductName = bundleItem.ProductName, @@ -449,6 +497,13 @@ private OrderDetailsModel.OrderItemModel PrepareOrderItemModel(Order order, Orde AttributeInfo = bundleItem.AttributesInfo }; + bundleItemModel.ProductUrl = _productAttributeParser.GetProductUrlWithAttributes(bundleItem.AttributesXml, bundleItem.ProductId, bundleItemModel.ProductSeName); + + if (orderItem.Product.BundlePerItemPricing) + { + _productAttributeParser.DeserializeQueryData(attributeQueryData, bundleItem.AttributesXml, bundleItem.ProductId, bundleItem.BundleItemId); + } + if (model.BundlePerItemShoppingCart) { decimal priceWithDiscount = _currencyService.ConvertCurrency(bundleItem.PriceWithDiscount, order.CurrencyRate); @@ -471,6 +526,7 @@ private OrderDetailsModel.OrderItemModel PrepareOrderItemModel(Order order, Orde model.SubTotal = _priceFormatter.FormatPrice(priceExclTaxInCustomerCurrency, true, order.CustomerCurrencyCode, _services.WorkContext.WorkingLanguage, false, false); } break; + case TaxDisplayType.IncludingTax: { var unitPriceInclTaxInCustomerCurrency = _currencyService.ConvertCurrency(orderItem.UnitPriceInclTax, order.CurrencyRate); @@ -481,6 +537,9 @@ private OrderDetailsModel.OrderItemModel PrepareOrderItemModel(Order order, Orde } break; } + + model.ProductUrl = _productAttributeParser.GetProductUrlWithAttributes(attributeQueryData, model.ProductSeName); + return model; } @@ -516,8 +575,9 @@ public ActionResult Print(int id, bool pdf = false) return new HttpUnauthorizedResult(); var model = PrepareOrderDetailsModel(order); + var fileName = T("Order.PdfInvoiceFileName", order.Id); - return PrintCore(new List { model }, pdf, "order-{0}.pdf".FormatWith(order.Id)); + return PrintCore(new List { model }, pdf, fileName); } [AdminAuthorize] @@ -526,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(); @@ -612,27 +680,30 @@ public ActionResult RePostPayment(int id) if (IsUnauthorizedOrder(order)) return new HttpUnauthorizedResult(); - if (!_paymentService.CanRePostProcessPayment(order)) - return RedirectToAction("Details", "Order", new { id = order.Id }); + try + { + if (_paymentService.CanRePostProcessPayment(order)) + { + var postProcessPaymentRequest = new PostProcessPaymentRequest + { + Order = order, + IsRePostProcessPayment = true + }; - var postProcessPaymentRequest = new PostProcessPaymentRequest() - { - Order = order, - IsRePostProcessPayment = true - }; - _paymentService.PostProcessPayment(postProcessPaymentRequest); + _paymentService.PostProcessPayment(postProcessPaymentRequest); - if (_services.WebHelper.IsRequestBeingRedirected || _services.WebHelper.IsPostBeingDone) - { - //redirection or POST has been done in PostProcessPayment - return Content("Redirected"); - } - else - { - //if no redirection has been done (to a third-party payment page) - //theoretically it's not possible - return RedirectToAction("Details", "Order", new { id = order.Id }); - } + if (postProcessPaymentRequest.RedirectUrl.HasValue()) + { + return Redirect(postProcessPaymentRequest.RedirectUrl); + } + } + } + catch (Exception exception) + { + NotifyError(exception); + } + + return RedirectToAction("Details", "Order", new { id = order.Id }); } [RequireHttpsByConfigAttribute(SslRequirement.Yes)] 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 ff7660fd01..79497418a9 100644 --- a/src/Presentation/SmartStore.Web/Controllers/ProductController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/ProductController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using System.Web.Mvc; using SmartStore.Core.Domain.Catalog; @@ -7,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; @@ -22,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; @@ -29,7 +30,7 @@ namespace SmartStore.Web.Controllers { - public partial class ProductController : PublicControllerBase + public partial class ProductController : PublicControllerBase { #region Fields @@ -122,17 +123,12 @@ public ProductController( this._localizationSettings = localizationSettings; this._captchaSettings = captchaSettings; this._helper = helper; - this._downloadService = downloadService; - this._localizationService = localizationService; - - T = NullLocalizer.Instance; + this._downloadService = downloadService; + this._localizationService = localizationService; } #endregion - public Localizer T { get; set; } - - #region Products [RequireHttpsByConfigAttribute(SslRequirement.No)] @@ -171,11 +167,16 @@ public ActionResult ProductDetails(int productId, string attributes) } } - //prepare the model - var selectedAttributes = new FormCollection(); - selectedAttributes.ConvertAttributeQueryData(_productAttributeParser.DeserializeQueryData(attributes), product.Id); + var selectedAttributes = new NameValueCollection(); - var model = _helper.PrepareProductDetailsPageModel(product, selectedAttributes: selectedAttributes); + // get selected attributes from query string + selectedAttributes.GetSelectedAttributes( + Request.QueryString, + _productAttributeParser.DeserializeQueryData(attributes), + product.ProductType == ProductType.BundledProduct && product.BundlePerItemPricing ? 0 : product.Id); + + // prepare the view model + var model = _helper.PrepareProductDetailsPageModel(product, selectedAttributes: selectedAttributes, queryData: Request.QueryString); //save as recently viewed _recentlyViewedProductsService.AddProductToRecentlyViewedList(product.Id); @@ -306,9 +307,10 @@ public ActionResult AddProductToCart(int productId, FormCollection form) else { //Errors - foreach (string error in addToCartContext.Warnings) - ModelState.AddModelError("", error); - + foreach (string error in addToCartContext.Warnings) + { + this.NotifyError(error); + } //If we got this far, something failed, redisplay form var model = _helper.PrepareProductDetailsPageModel(parentProduct); @@ -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 @@ -445,7 +453,7 @@ public ActionResult RelatedProducts(int productId, int? productThumbPictureSize) if (products.Count == 0) return Content(""); - var model = _helper.PrepareProductOverviewModels(products, true, true, productThumbPictureSize).ToList(); + var model = _helper.PrepareProductOverviewModels(products, true, true, productThumbPictureSize, false, false, false, false, true).ToList(); return PartialView(model); } @@ -472,7 +480,7 @@ public ActionResult ProductsAlsoPurchased(int productId, int? productThumbPictur return Content(""); // prepare model - var model = _helper.PrepareProductOverviewModels(products, true, true, productThumbPictureSize).ToList(); + var model = _helper.PrepareProductOverviewModels(products, true, true, productThumbPictureSize, false, false, false, false, true).ToList(); return PartialView(model); } @@ -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")); @@ -628,9 +636,10 @@ public ActionResult UpdateProductDetails(int productId, string itemType, int bun bundleItems = _productService.GetBundleItems(product.Id); if (form.Count > 0) { + // may add elements to form if they are preselected by bundle item filter foreach (var itemData in bundleItems) { - var tempModel = _helper.PrepareProductDetailsPageModel(itemData.Item.Product, false, itemData, null, form); + var unused = _helper.PrepareProductDetailsPageModel(itemData.Item.Product, false, itemData, null, form); } } } @@ -662,14 +671,16 @@ public ActionResult UpdateProductDetails(int productId, string itemType, int bun } else { - var allCombinationImageIds = new List(); - - _productAttributeService - .GetAllProductVariantAttributeCombinations(product.Id) - .GetAllCombinationImageIds(allCombinationImageIds); + var allCombinationPictureIds = _productAttributeService.GetAllProductVariantAttributeCombinationPictureIds(product.Id); - _helper.PrepareProductDetailsPictureModel(pictureModel, pictures, product.GetLocalized(x => x.Name), allCombinationImageIds, - false, bundleItem, m.CombinationSelected); + _helper.PrepareProductDetailsPictureModel( + pictureModel, + pictures, + product.GetLocalized(x => x.Name), + allCombinationPictureIds, + false, + bundleItem, + m.SelectedCombination); galleryStartIndex = pictureModel.GalleryStartIndex; galleryHtml = this.RenderPartialViewToString("_PictureGallery", pictureModel); @@ -758,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, () => @@ -889,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) { @@ -1028,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/ReturnRequestController.cs b/src/Presentation/SmartStore.Web/Controllers/ReturnRequestController.cs index 95d0aa59af..d7b5e454d6 100644 --- a/src/Presentation/SmartStore.Web/Controllers/ReturnRequestController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/ReturnRequestController.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Web.Mvc; using SmartStore.Core; +using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Tax; @@ -11,9 +13,9 @@ using SmartStore.Services.Messages; using SmartStore.Services.Orders; using SmartStore.Services.Seo; +using SmartStore.Web.Framework.Controllers; using SmartStore.Web.Framework.Security; using SmartStore.Web.Models.Order; -using SmartStore.Web.Framework.Controllers; namespace SmartStore.Web.Controllers { @@ -30,6 +32,7 @@ public partial class ReturnRequestController : PublicControllerBase private readonly ILocalizationService _localizationService; private readonly ICustomerService _customerService; private readonly IWorkflowMessageService _workflowMessageService; + private readonly IProductAttributeParser _productAttributeParser; private readonly LocalizationSettings _localizationSettings; private readonly OrderSettings _orderSettings; @@ -46,6 +49,7 @@ public ReturnRequestController( ILocalizationService localizationService, ICustomerService customerService, IWorkflowMessageService workflowMessageService, + IProductAttributeParser productAttributeParser, LocalizationSettings localizationSettings, OrderSettings orderSettings) { @@ -58,6 +62,7 @@ public ReturnRequestController( this._localizationService = localizationService; this._customerService = customerService; this._workflowMessageService = workflowMessageService; + this._productAttributeParser = productAttributeParser; this._localizationSettings = localizationSettings; this._orderSettings = orderSettings; @@ -95,18 +100,33 @@ protected SubmitReturnRequestModel PrepareReturnRequestModel(SubmitReturnRequest //products var orderItems = _orderService.GetAllOrderItems(order.Id, null, null, null, null, null, null); + foreach (var orderItem in orderItems) { - var orderItemModel = new SubmitReturnRequestModel.OrderItemModel() + var attributeQueryData = new List>(); + + var orderItemModel = new SubmitReturnRequestModel.OrderItemModel { Id = orderItem.Id, ProductId = orderItem.Product.Id, ProductName = orderItem.Product.GetLocalized(x => x.Name), - ProductSeName = orderItem.Product.GetSeName(), + ProductSeName = orderItem.Product.GetSeName(), AttributeInfo = orderItem.AttributeDescription, Quantity = orderItem.Quantity }; - model.Items.Add(orderItemModel); + + if (orderItem.Product.ProductType != ProductType.BundledProduct) + { + _productAttributeParser.DeserializeQueryData(attributeQueryData, orderItem.AttributesXml, orderItem.ProductId); + } + else if (orderItem.Product.BundlePerItemPricing && orderItem.BundleData.HasValue()) + { + var bundleData = orderItem.GetBundleData(); + + bundleData.ForEach(x => _productAttributeParser.DeserializeQueryData(attributeQueryData, x.AttributesXml, x.ProductId, x.BundleItemId)); + } + + orderItemModel.ProductUrl = _productAttributeParser.GetProductUrlWithAttributes(attributeQueryData, orderItemModel.ProductSeName); //unit price switch (order.CustomerTaxDisplayType) @@ -124,6 +144,8 @@ protected SubmitReturnRequestModel PrepareReturnRequestModel(SubmitReturnRequest } break; } + + model.Items.Add(orderItemModel); } return model; diff --git a/src/Presentation/SmartStore.Web/Controllers/ShoppingCartController.cs b/src/Presentation/SmartStore.Web/Controllers/ShoppingCartController.cs index 9229462276..e4ff2cb6a7 100644 --- a/src/Presentation/SmartStore.Web/Controllers/ShoppingCartController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/ShoppingCartController.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; using System.Web; using System.Web.Mvc; @@ -33,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; @@ -91,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, @@ -121,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; @@ -167,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 @@ -174,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"); @@ -205,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), }; @@ -223,7 +232,7 @@ private ShoppingCartModel.ShoppingCartItemModel PrepareShoppingCartItemModel(Org product.MergeWithCombination(item.AttributesXml); - var model = new ShoppingCartModel.ShoppingCartItemModel() + var model = new ShoppingCartModel.ShoppingCartItemModel { Id = item.Id, Sku = product.Sku, @@ -235,17 +244,22 @@ private ShoppingCartModel.ShoppingCartItemModel PrepareShoppingCartItemModel(Org IsShipEnabled = product.IsShipEnabled, ShortDesc = product.GetLocalized(x => x.ShortDescription), ProductType = product.ProductType, - BasePrice = product.GetBasePriceInfo(_localizationService, _priceFormatter), - Weight = product.Weight + BasePrice = product.GetBasePriceInfo(_localizationService, _priceFormatter, _currencyService, _taxService, _priceCalculationService, _workContext.WorkingCurrency), + Weight = product.Weight, + IsDownload = product.IsDownload, + HasUserAgreement = product.HasUserAgreement, + IsEsd = product.IsEsd }; + model.ProductUrl = GetProductUrlWithAttributes(sci, model.ProductSeName); + if (item.BundleItem != null) { model.BundlePerItemPricing = item.BundleItem.BundleProduct.BundlePerItemPricing; 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()) @@ -271,6 +285,13 @@ private ShoppingCartModel.ShoppingCartItemModel PrepareShoppingCartItemModel(Org else { model.AttributeInfo = _productAttributeFormatter.FormatAttributes(product, item.AttributesXml); + + var selectedAttributeValues = _productAttributeParser.ParseProductVariantAttributeValues(item.AttributesXml).ToList(); + if (selectedAttributeValues != null) + { + foreach (var attributeValue in selectedAttributeValues) + model.Weight = decimal.Add(model.Weight, attributeValue.WeightAdjustment); + } } if (product.DisplayDeliveryTimeAccordingToStock(_catalogSettings)) @@ -360,23 +381,33 @@ private ShoppingCartModel.ShoppingCartItemModel PrepareShoppingCartItemModel(Org decimal shoppingCartItemDiscount = _currencyService.ConvertFromPrimaryStoreCurrency(shoppingCartItemDiscountBase, _workContext.WorkingCurrency); model.Discount = _priceFormatter.FormatPrice(shoppingCartItemDiscount); } + + model.BasePrice = product.GetBasePriceInfo( + _localizationService, + _priceFormatter, + _currencyService, + _taxService, + _priceCalculationService, + _workContext.WorkingCurrency, + (product.Price - _priceCalculationService.GetUnitPrice(sci, true)) * (-1) + ); } //picture 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) { @@ -395,6 +426,7 @@ private ShoppingCartModel.ShoppingCartItemModel PrepareShoppingCartItemModel(Org return model; } + private WishlistModel.ShoppingCartItemModel PrepareWishlistCartItemModel(OrganizedShoppingCartItem sci) { var item = sci.Item; @@ -402,7 +434,7 @@ private WishlistModel.ShoppingCartItemModel PrepareWishlistCartItemModel(Organiz product.MergeWithCombination(item.AttributesXml); - var model = new WishlistModel.ShoppingCartItemModel() + var model = new WishlistModel.ShoppingCartItemModel { Id = item.Id, Sku = product.Sku, @@ -415,6 +447,8 @@ private WishlistModel.ShoppingCartItemModel PrepareWishlistCartItemModel(Organiz VisibleIndividually = product.VisibleIndividually }; + model.ProductUrl = GetProductUrlWithAttributes(sci, model.ProductSeName); + if (item.BundleItem != null) { model.BundlePerItemPricing = item.BundleItem.BundleProduct.BundlePerItemPricing; @@ -452,7 +486,7 @@ private WishlistModel.ShoppingCartItemModel PrepareWishlistCartItemModel(Organiz var allowedQuantities = product.ParseAllowedQuatities(); foreach (var qty in allowedQuantities) { - model.AllowedQuantities.Add(new SelectListItem() + model.AllowedQuantities.Add(new SelectListItem { Text = qty.ToString(), Value = qty.ToString(), @@ -511,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 @@ -545,9 +579,10 @@ private void PrepareButtonPaymentMethodModel(ButtonPaymentMethodModel model, ILi { model.Items.Clear(); + var paymentTypes = new PaymentMethodType[] { PaymentMethodType.Button, PaymentMethodType.StandardAndButton }; + var boundPaymentMethods = _paymentService - .LoadActivePaymentMethods(_workContext.CurrentCustomer.Id, _storeContext.CurrentStore.Id) - .Where(pm => pm.Value.PaymentMethodType == PaymentMethodType.Button || pm.Value.PaymentMethodType == PaymentMethodType.StandardAndButton) + .LoadActivePaymentMethods(_workContext.CurrentCustomer, cart, _storeContext.CurrentStore.Id, paymentTypes, false) .ToList(); foreach (var pm in boundPaymentMethods) @@ -560,7 +595,7 @@ private void PrepareButtonPaymentMethodModel(ButtonPaymentMethodModel model, ILi RouteValueDictionary routeValues; pm.Value.GetPaymentInfoRoute(out actionName, out controllerName, out routeValues); - model.Items.Add(new ButtonPaymentMethodModel.ButtonPaymentMethodItem() + model.Items.Add(new ButtonPaymentMethodModel.ButtonPaymentMethodItem { ActionName = actionName, ControllerName = controllerName, @@ -608,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) { @@ -626,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 @@ -892,7 +950,7 @@ protected void PrepareWishlistModel(WishlistModel model, IList x.Item.Id) .Take(_shoppingCartSettings.MiniShoppingCartProductNumber) .ToList()) { - var cartItemModel = new MiniShoppingCartModel.ShoppingCartItemModel() + var item = sci.Item; + var product = sci.Item.Product; + + var cartItemModel = new MiniShoppingCartModel.ShoppingCartItemModel { - Id = sci.Item.Id, - ProductId = sci.Item.Product.Id, - ProductName = sci.Item.Product.GetLocalized(x => x.Name), - ProductSeName = sci.Item.Product.GetSeName(), - Quantity = sci.Item.Quantity, + Id = item.Id, + ProductId = product.Id, + ProductName = product.GetLocalized(x => x.Name), + ProductSeName = product.GetSeName(), + Quantity = item.Quantity, AttributeInfo = _productAttributeFormatter.FormatAttributes( - sci.Item.Product, - sci.Item.AttributesXml, + product, + item.AttributesXml, null, serapator: ", ", renderPrices: false, @@ -959,35 +1019,40 @@ protected MiniShoppingCartModel PrepareMiniShoppingCartModel() allowHyperlinks: false) }; - if (sci.Item.Product.ProductType == ProductType.BundledProduct) - { - var bundleItems = _productService.GetBundleItems(sci.Item.Product.Id); - foreach (var bundleItem in bundleItems) - { - var bundleItemModel = new MiniShoppingCartModel.ShoppingCartItemBundleItem(); - bundleItemModel.ProductName = bundleItem.Item.Product.GetLocalized(x => x.Name); - var bundlePic = _pictureService.GetPicturesByProductId(bundleItem.Item.ProductId).FirstOrDefault(); - if(bundlePic != null) - bundleItemModel.PictureUrl = _pictureService.GetPictureUrl(bundlePic.Id, 32); + cartItemModel.ProductUrl = GetProductUrlWithAttributes(sci, cartItemModel.ProductSeName); - bundleItemModel.ProductSeName = bundleItem.Item.Product.GetSeName(); + if (sci.ChildItems != null && _shoppingCartSettings.ShowProductBundleImagesOnShoppingCart) + { + foreach (var childItem in sci.ChildItems.Where(x => x.Item.Id != item.Id && x.Item.BundleItem != null && !x.Item.BundleItem.HideThumbnail)) + { + var bundleItemModel = new MiniShoppingCartModel.ShoppingCartItemBundleItem + { + ProductName = childItem.Item.Product.GetLocalized(x => x.Name), + ProductSeName = childItem.Item.Product.GetSeName(), + }; - if (!bundleItem.Item.HideThumbnail) - cartItemModel.BundleItems.Add(bundleItemModel); - } - } + bundleItemModel.ProductUrl = _productAttributeParser.GetProductUrlWithAttributes( + childItem.Item.AttributesXml, childItem.Item.ProductId, bundleItemModel.ProductSeName); + + var itemPicture = _pictureService.GetPicturesByProductId(childItem.Item.ProductId, 1).FirstOrDefault(); + if (itemPicture != null) + bundleItemModel.PictureUrl = _pictureService.GetPictureUrl(itemPicture.Id, 32); + + cartItemModel.BundleItems.Add(bundleItemModel); + } + } //unit prices - if (sci.Item.Product.CallForPrice) + if (product.CallForPrice) { cartItemModel.UnitPrice = _localizationService.GetResource("Products.CallForPrice"); } else { - sci.Item.Product.MergeWithCombination(sci.Item.AttributesXml); + product.MergeWithCombination(item.AttributesXml); decimal taxRate = decimal.Zero; - decimal shoppingCartUnitPriceWithDiscountBase = _taxService.GetProductPrice(sci.Item.Product, _priceCalculationService.GetUnitPrice(sci, true), out taxRate); + decimal shoppingCartUnitPriceWithDiscountBase = _taxService.GetProductPrice(product, _priceCalculationService.GetUnitPrice(sci, true), out taxRate); decimal shoppingCartUnitPriceWithDiscount = _currencyService.ConvertFromPrimaryStoreCurrency(shoppingCartUnitPriceWithDiscountBase, _workContext.WorkingCurrency); cartItemModel.UnitPrice = _priceFormatter.FormatPrice(shoppingCartUnitPriceWithDiscount); @@ -996,8 +1061,7 @@ protected MiniShoppingCartModel PrepareMiniShoppingCartModel() //picture if (_shoppingCartSettings.ShowProductImagesInMiniShoppingCart) { - cartItemModel.Picture = PrepareCartItemPictureModel(sci.Item.Product, - _mediaSettings.MiniCartThumbPictureSize, true, cartItemModel.ProductName, sci.Item.AttributesXml); + cartItemModel.Picture = PrepareCartItemPictureModel(product, _mediaSettings.MiniCartThumbPictureSize, cartItemModel.ProductName, item.AttributesXml); } model.Items.Add(cartItemModel); @@ -1011,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: @@ -1029,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]; @@ -1043,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: { @@ -1058,36 +1125,38 @@ 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 httpPostedFile = this.Request.Files[controlId]; - if ((httpPostedFile != null) && (!String.IsNullOrEmpty(httpPostedFile.FileName))) + var postedFile = this.Request.Files[controlId].ToPostedFileResult(); + if (postedFile != null && postedFile.FileName.HasValue()) { int fileMaxSize = _catalogSettings.FileUploadMaximumSizeBytes; - if (httpPostedFile.ContentLength > fileMaxSize) + if (postedFile.Size > fileMaxSize) { //TODO display warning //warnings.Add(string.Format(_localizationService.GetResource("ShoppingCart.MaximumUploadedFileSize"), (int)(fileMaxSize / 1024))); @@ -1095,25 +1164,25 @@ protected void ParseAndSaveCheckoutAttributes(List ca else { //save an uploaded file - var download = new Download() + var download = new Download { DownloadGuid = Guid.NewGuid(), UseDownloadUrl = false, DownloadUrl = "", - DownloadBinary = httpPostedFile.GetDownloadBits(), - ContentType = httpPostedFile.ContentType, - Filename = System.IO.Path.GetFileNameWithoutExtension(httpPostedFile.FileName), - Extension = System.IO.Path.GetExtension(httpPostedFile.FileName), + DownloadBinary = postedFile.Buffer, + ContentType = postedFile.ContentType, + Filename = postedFile.FileTitle, + Extension = postedFile.FileExtension, IsNew = true }; _downloadService.InsertDownload(download); //save attribute - selectedAttributes = _checkoutAttributeParser.AddCheckoutAttribute(selectedAttributes, - attribute, download.DownloadGuid.ToString()); + selectedAttributes = _checkoutAttributeParser.AddCheckoutAttribute(selectedAttributes, attribute, download.DownloadGuid.ToString()); } } } break; + default: break; } @@ -1123,6 +1192,27 @@ protected void ParseAndSaveCheckoutAttributes(List ca _genericAttributeService.SaveAttribute(_workContext.CurrentCustomer, SystemCustomerAttributeNames.CheckoutAttributes, selectedAttributes); } + private string GetProductUrlWithAttributes(OrganizedShoppingCartItem cartItem, string productSeName) + { + var attributeQueryData = new List>(); + var product = cartItem.Item.Product; + + if (product.ProductType != ProductType.BundledProduct) + { + _productAttributeParser.DeserializeQueryData(attributeQueryData, cartItem.Item.AttributesXml, product.Id); + } + else if (cartItem.ChildItems != null && product.BundlePerItemPricing) + { + foreach (var childItem in cartItem.ChildItems.Where(x => x.Item.Id != cartItem.Item.Id)) + { + _productAttributeParser.DeserializeQueryData(attributeQueryData, childItem.Item.AttributesXml, childItem.Item.ProductId, childItem.BundleItemData.Item.Id); + } + } + + var url = _productAttributeParser.GetProductUrlWithAttributes(attributeQueryData, productSeName); + return url; + } + #endregion #region Shopping cart @@ -1141,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) }); } @@ -1368,86 +1458,62 @@ public ActionResult UploadFileProductAttribute(int productId, int productAttribu { success = false, downloadGuid = Guid.Empty, - }, "text/plain"); + }); } - //ensure that this attribute belong to this product and has "file upload" type + + // ensure that this attribute belong to this product and has "file upload" type var pva = _productAttributeService .GetProductVariantAttributesByProductId(productId) .Where(pa => pa.ProductAttributeId == productAttributeId) .FirstOrDefault(); + if (pva == null || pva.AttributeControlType != AttributeControlType.FileUpload) { return Json(new { success = false, downloadGuid = Guid.Empty, - }, "text/plain"); - } - - //we process it distinct ways based on a browser - //find more info here http://stackoverflow.com/questions/4884920/mvc3-valums-ajax-file-upload - Stream stream = null; - var fileName = ""; - var contentType = ""; - if (String.IsNullOrEmpty(Request["qqfile"])) - { - // IE - HttpPostedFileBase httpPostedFile = Request.Files[0]; - if (httpPostedFile == null) - throw new ArgumentException("No file uploaded"); - stream = httpPostedFile.InputStream; - fileName = Path.GetFileName(httpPostedFile.FileName); - contentType = httpPostedFile.ContentType; - } - else - { - //Webkit, Mozilla - stream = Request.InputStream; - fileName = Request["qqfile"]; + }); } - var fileBinary = new byte[stream.Length]; - stream.Read(fileBinary, 0, fileBinary.Length); - - var fileExtension = Path.GetExtension(fileName); - if (!String.IsNullOrEmpty(fileExtension)) - fileExtension = fileExtension.ToLowerInvariant(); + var postedFile = Request.ToPostedFileResult(); + if (postedFile == null) + { + throw new ArgumentException(T("Common.NoFileUploaded")); + } int fileMaxSize = _catalogSettings.FileUploadMaximumSizeBytes; - if (fileBinary.Length > fileMaxSize) + if (postedFile.Size > fileMaxSize) { - //when returning JSON the mime-type must be set to text/plain - //otherwise some browsers will pop-up a "Save As" dialog. return Json(new { success = false, message = string.Format(_localizationService.GetResource("ShoppingCart.MaximumUploadedFileSize"), (int)(fileMaxSize / 1024)), downloadGuid = Guid.Empty, - }, "text/plain"); + }); } - var download = new Download() + var download = new Download { DownloadGuid = Guid.NewGuid(), UseDownloadUrl = false, DownloadUrl = "", - DownloadBinary = fileBinary, - ContentType = contentType, - //we store filename without extension for downloads - Filename = Path.GetFileNameWithoutExtension(fileName), - Extension = fileExtension, - IsNew = true + DownloadBinary = postedFile.Buffer, + ContentType = postedFile.ContentType, + // we store filename without extension for downloads + Filename = postedFile.FileTitle, + Extension = postedFile.FileExtension, + IsNew = true, + IsTransient = true }; _downloadService.InsertDownload(download); - //when returning JSON the mime-type must be set to text/plain - //otherwise some browsers will pop-up a "Save As" dialog. return Json(new { success = true, message = _localizationService.GetResource("ShoppingCart.FileUploaded"), downloadGuid = download.DownloadGuid, - }, "text/plain"); + }); } @@ -1682,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)] @@ -1807,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); @@ -1816,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, @@ -1828,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 { @@ -1843,12 +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( @@ -1857,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")); } } } @@ -2561,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 new file mode 100644 index 0000000000..68c94b3fe7 --- /dev/null +++ b/src/Presentation/SmartStore.Web/Controllers/TaskSchedulerController.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Mvc; +using System.Web.SessionState; +using SmartStore.Core.Domain.Tasks; +using SmartStore.Services.Tasks; +using SmartStore.Web.Framework.Controllers; +using SmartStore.Services.Security; +using SmartStore.Services; +using SmartStore.Collections; + +namespace SmartStore.Web.Controllers +{ + + [SessionState(SessionStateBehavior.ReadOnly)] + public class TaskSchedulerController : Controller + { + private readonly ITaskScheduler _taskScheduler; + private readonly IScheduleTaskService _scheduledTaskService; + private readonly ITaskExecutor _taskExecutor; + private readonly ICommonServices _services; + + public TaskSchedulerController( + ITaskScheduler taskScheduler, + IScheduleTaskService scheduledTaskService, + ITaskExecutor taskExecutor, + ICommonServices services) + { + this._taskScheduler = taskScheduler; + this._scheduledTaskService = scheduledTaskService; + this._taskExecutor = taskExecutor; + this._services = services; + } + + [HttpPost] + public ActionResult Sweep() + { + if (!_taskScheduler.VerifyAuthToken(Request.Headers["X-AUTH-TOKEN"])) + return new HttpUnauthorizedResult(); + + var pendingTasks = _scheduledTaskService.GetPendingTasks(); + var prevTaskStart = DateTime.UtcNow; + var count = 0; + + for (var i = 0; i < pendingTasks.Count; i++) + { + var task = pendingTasks[i]; + + if (i > 0) + { + // 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); + } + + if (task.IsPending) + { + prevTaskStart = DateTime.UtcNow; + _taskExecutor.Execute(task); + count++; + } + } + + return Content("{0} of {1} pending tasks executed".FormatInvariant(count, pendingTasks.Count)); + } + + [HttpPost] + public ActionResult Execute(int id /* taskId */) + { + if (!_taskScheduler.VerifyAuthToken(Request.Headers["X-AUTH-TOKEN"])) + return new HttpUnauthorizedResult(); + + var task = _scheduledTaskService.GetTaskById(id); + if (task == null) + return HttpNotFound(); + + _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 180440df34..74461e92ef 100644 --- a/src/Presentation/SmartStore.Web/Controllers/WidgetController.cs +++ b/src/Presentation/SmartStore.Web/Controllers/WidgetController.cs @@ -9,10 +9,16 @@ namespace SmartStore.Web.Controllers public partial class WidgetController : PublicControllerBase { [ChildActionOnly] - public ActionResult WidgetsByZone(IEnumerable widgets) + public ActionResult WidgetsByZone(WidgetZoneModel zoneModel) { - return PartialView(widgets); + 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/Content/Images/Thumbs/placeholder.txt b/src/Presentation/SmartStore.Web/Exchange/placeholder similarity index 100% rename from src/Presentation/SmartStore.Web/Content/Images/Thumbs/placeholder.txt rename to src/Presentation/SmartStore.Web/Exchange/placeholder diff --git a/src/Presentation/SmartStore.Web/Extensions/ProductDetailsExtensions.cs b/src/Presentation/SmartStore.Web/Extensions/ProductDetailsExtensions.cs index 7b82ca9ec0..18356aa3cb 100644 --- a/src/Presentation/SmartStore.Web/Extensions/ProductDetailsExtensions.cs +++ b/src/Presentation/SmartStore.Web/Extensions/ProductDetailsExtensions.cs @@ -11,6 +11,7 @@ namespace SmartStore.Web { public static class ProductDetailsExtensions { + public static string UpdateProductDetailsUrl(this ProductDetailsModel model, string itemType = null) { var urlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext); @@ -24,16 +25,18 @@ public static string UpdateProductDetailsUrl(this ProductDetailsModel model, str return url; } + public static bool RenderBundleTitle(this ProductDetailsModel model) { return model.BundleTitleText.HasValue() && model.BundledItems.Where(x => x.BundleItem.Visible).Count() > 0; } + public static Picture GetAssignedPicture(this ProductDetailsModel model, IPictureService pictureService, IList pictures, int productId = 0) { - if (model != null && model.CombinationSelected != null) + if (model != null && model.SelectedCombination != null) { Picture picture = null; - var combiAssignedImages = model.CombinationSelected.GetAssignedPictureIds(); + var combiAssignedImages = model.SelectedCombination.GetAssignedPictureIds(); if (combiAssignedImages.Length > 0) { @@ -51,6 +54,7 @@ public static Picture GetAssignedPicture(this ProductDetailsModel model, IPictur } return null; } + public static string GetAttributeValueInfo(this ProductDetailsModel.ProductVariantAttributeValueModel model) { string result = ""; @@ -77,6 +81,7 @@ public static bool ShouldBeRendered(this ProductDetailsModel.ProductVariantAttri return true; } } + public static bool ShouldBeRendered(this IEnumerable variantAttributes) { if (variantAttributes != null) diff --git a/src/Presentation/SmartStore.Web/Global.asax.cs b/src/Presentation/SmartStore.Web/Global.asax.cs index 4fc9355f74..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,9 +116,8 @@ protected void Application_Start() HostingEnvironment.RegisterVirtualPathProvider(new PluginDebugViewVirtualPathProvider()); } - // start scheduled tasks - TaskManager.Instance.Initialize(); - TaskManager.Instance.Start(); + // "throw-away" filter for task scheduler initialization (the filter removes itself when processed) + GlobalFilters.Filters.Add(new InitializeSchedulerFilter()); } else { @@ -161,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/DefaultWidgetSelector.cs b/src/Presentation/SmartStore.Web/Infrastructure/DefaultWidgetSelector.cs index 1bf8f34b3c..48ce046bc7 100644 --- a/src/Presentation/SmartStore.Web/Infrastructure/DefaultWidgetSelector.cs +++ b/src/Presentation/SmartStore.Web/Infrastructure/DefaultWidgetSelector.cs @@ -82,7 +82,6 @@ public virtual IEnumerable GetWidgets(string widgetZone, object #endregion - #region Topic Widgets // add special "topic widgets" to the list diff --git a/src/Presentation/SmartStore.Web/Infrastructure/Installation/DeDESeedData.cs b/src/Presentation/SmartStore.Web/Infrastructure/Installation/DeDESeedData.cs index 54df8e0005..3896e5c829 100644 --- a/src/Presentation/SmartStore.Web/Infrastructure/Installation/DeDESeedData.cs +++ b/src/Presentation/SmartStore.Web/Infrastructure/Installation/DeDESeedData.cs @@ -1,35 +1,28 @@ using System; -using System.Linq; -using System.Linq.Expressions; using System.Collections.Generic; -using System.IO; -using SmartStore.Core; +using System.Linq; using SmartStore.Core.Configuration; -using SmartStore.Core.Domain; using SmartStore.Core.Domain.Blogs; +using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Cms; using SmartStore.Core.Domain.Common; +using SmartStore.Core.Domain.Customers; using SmartStore.Core.Domain.Directory; using SmartStore.Core.Domain.Discounts; -using SmartStore.Core.Domain.Messages; using SmartStore.Core.Domain.Forums; using SmartStore.Core.Domain.Localization; using SmartStore.Core.Domain.Logging; +using SmartStore.Core.Domain.Media; +using SmartStore.Core.Domain.Messages; using SmartStore.Core.Domain.News; +using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Polls; -using SmartStore.Core.Domain.Tax; -using SmartStore.Core.Domain.Topics; using SmartStore.Core.Domain.Seo; -using SmartStore.Core.Domain.Orders; using SmartStore.Core.Domain.Shipping; -using SmartStore.Core.Domain.Tasks; -using SmartStore.Core.Domain.Payments; -using SmartStore.Core.Infrastructure; -using SmartStore.Core.Data; -using SmartStore.Core.Domain.Customers; -using SmartStore.Core.Domain.Catalog; using SmartStore.Core.Domain.Stores; -using SmartStore.Core.Domain.Media; +using SmartStore.Core.Domain.Tasks; +using SmartStore.Core.Domain.Tax; +using SmartStore.Core.Domain.Topics; using SmartStore.Data.Setup; namespace SmartStore.Web.Infrastructure.Installation @@ -265,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 => { @@ -290,7 +283,7 @@ protected override void Alter(IList entities) .Alter("Product.AskQuestion", x => { x.Subject = "%Store.Name% - Frage zu '%Product.Name%' von %ProductQuestion.SenderName%"; - x.Body = templateHeader + "

    %ProductQuestion.Message%

    %ProductQuestion.Message%

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

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

    %ProductQuestion.Message%

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

    " + templateFooter; }) @@ -2112,12 +2105,6 @@ protected override void Alter(IList settings) x.BaseWeightId = base.DbContext.Set().Where(m => m.SystemKeyword == "kg").Single().Id; }) - .Alter(x => - { - x.PrimaryStoreCurrencyId = base.DbContext.Set().Where(c => c.CurrencyCode == "EUR").Single().Id; - x.PrimaryExchangeRateCurrencyId = base.DbContext.Set().Where(c => c.CurrencyCode == "EUR").Single().Id; - }) - .Alter(x => { x.DefaultTitle = "Mein Shop"; @@ -2525,30 +2512,38 @@ protected override void Alter(IList entities) { base.Alter(entities); - entities.WithKey(x => x.Name) - .Alter("Send emails", x => + entities.WithKey(x => x.Type) + .Alter("SmartStore.Services.Messages.QueuedMessagesSendTask, SmartStore.Services", x => { x.Name = "E-Mail senden"; }) - .Alter("Keep alive", x => - { - x.Name = "Keep alive"; - }) - .Alter("Delete guests", x => + .Alter("SmartStore.Services.Messages.QueuedMessagesClearTask, SmartStore.Services", x => + { + x.Name = "E-Mail Queue bereinigen"; + }) + .Alter("SmartStore.Services.Media.TransientMediaClearTask, SmartStore.Services", x => + { + x.Name = "Temporäre Uploads bereinigen"; + }) + .Alter("SmartStore.Services.Customers.DeleteGuestsTask, SmartStore.Services", x => { x.Name = "Gastbenutzer löschen"; }) - .Alter("Clear cache", x => + .Alter("SmartStore.Services.Caching.ClearCacheTask, SmartStore.Services", x => { x.Name = "Cache bereinigen"; }) - .Alter("Send emails", x => + .Alter("SmartStore.Services.Messages.QueuedMessagesSendTask, SmartStore.Services", x => { x.Name = "E-Mail senden"; }) - .Alter("Update currency exchange rates", x => + .Alter("SmartStore.Services.Directory.UpdateExchangeRateTask, SmartStore.Services", x => { x.Name = "Wechselkurse aktualisieren"; + }) + .Alter("SmartStore.Services.Common.TempFileCleanupTask, SmartStore.Services", x => + { + x.Name = "Temporäre Dateien bereinigen"; }); } diff --git a/src/Presentation/SmartStore.Web/Infrastructure/Installation/InstallDataSeeder.cs b/src/Presentation/SmartStore.Web/Infrastructure/Installation/InstallDataSeeder.cs index 60107decb1..4e964cc565 100644 --- a/src/Presentation/SmartStore.Web/Infrastructure/Installation/InstallDataSeeder.cs +++ b/src/Presentation/SmartStore.Web/Infrastructure/Installation/InstallDataSeeder.cs @@ -539,6 +539,7 @@ public virtual void Seed(SmartObjectContext context) _ctx.Configuration.AutoDetectChangesEnabled = false; _ctx.Configuration.ValidateOnSaveEnabled = false; + _ctx.HooksEnabled = false; _config.ProgressMessageCallback("Progress.CreatingRequiredData"); @@ -549,12 +550,12 @@ public virtual void Seed(SmartObjectContext context) }); Populate("PopulatePictures", _data.Pictures()); + Populate("PopulateCurrencies", PopulateCurrencies); Populate("PopulateStores", PopulateStores); Populate("InstallLanguages", () => PopulateLanguage(_config.Language)); Populate("PopulateMeasureDimensions", _data.MeasureDimensions()); Populate("PopulateMeasureWeights", _data.MeasureWeights()); Populate("PopulateTaxCategories", PopulateTaxCategories); - Populate("PopulateCurrencies", PopulateCurrencies); Populate("PopulateCountriesAndStates", PopulateCountriesAndStates); Populate("PopulateShippingMethods", PopulateShippingMethods); Populate("PopulateDeliveryTimes", _data.DeliveryTimes()); @@ -614,9 +615,11 @@ private void SetModified(TEntity entity) private string ValidateSeName(TEntity entity, string name) where TEntity : BaseEntity, ISlugSupported { + var seoSettings = new SeoSettings { LoadAllUrlAliasesOnStartup = false }; + if (_urlRecordService == null) { - _urlRecordService = new UrlRecordService(NullCache.Instance, new EfRepository(_ctx) { AutoCommitEnabled = false }); + _urlRecordService = new UrlRecordService(NullCache.Instance, new EfRepository(_ctx) { AutoCommitEnabled = false }, seoSettings); } return entity.ValidateSeName("", name, true, _urlRecordService, new SeoSettings()); 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 dcbefd212d..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 { @@ -13,6 +13,10 @@ public ManufacturerNavigationModel() public IList Manufacturers { get; set; } public int TotalManufacturers { get; set; } + + public bool DisplayManufacturers { get; set; } + + public bool DisplayImages { get; set; } } public partial class ManufacturerBriefInfoModel : EntityModelBase @@ -20,7 +24,9 @@ public partial class ManufacturerBriefInfoModel : EntityModelBase public string Name { get; set; } public string SeName { get; set; } - + + public string PictureUrl { get; set; } + public bool IsActive { get; set; } } } \ No newline at end of file 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 fdfce95aff..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; @@ -21,7 +20,6 @@ public ProductDetailsModel() ProductPrice = new ProductPriceModel(); AddToCart = new AddToCartModel(); ProductVariantAttributes = new List(); - Combinations = new List(); AssociatedProducts = new List(); BundledItems = new List(); BundleItem = new ProductBundleItemModel(); @@ -100,8 +98,7 @@ public ProductDetailsPictureModel DetailsPictureModel public bool BundlePerItemPricing { get; set; } public bool BundlePerItemShoppingCart { get; set; } - public IList Combinations { get; set; } - public ProductVariantAttributeCombination CombinationSelected { get; set; } + public ProductVariantAttributeCombination SelectedCombination { get; set; } public IList Manufacturers { get; set; } public int ReviewCount { get; set; } 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 eb39ca78ee..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 { @@ -22,7 +20,7 @@ public ProductOverviewModel() public string Name { get; set; } public string ShortDescription { get; set; } - public string FullDescription { get; set; } + public string FullDescription { get; set; } public string SeName { get; set; } public int ThumbDimension { get; set; } @@ -51,7 +49,10 @@ public ProductOverviewModel() public string StockAvailablity { get; set; } public bool DisplayBasePrice { get; set; } public string BasePriceInfo { get; set; } - public int ProductMinPriceId { get; set; } + /// + /// For internal use + /// + public int MinPriceProductId { get; set; } public bool CompareEnabled { get; set; } public bool IsNew { get; set; } public bool HideBuyButtonInLists { get; set; } 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 585658b0ad..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 { @@ -24,12 +24,14 @@ public partial class PaymentMethodModel : ModelBase public string PaymentMethodSystemName { get; set; } public string Name { get; set; } public string Description { get; set; } + public string FullDescription { get; set; } public string BrandUrl { get; set; } public string Fee { get; set; } public bool Selected { get; set; } public RouteInfo PaymentInfoRoute { get; set; } public bool RequiresInteraction { get; set; } } + #endregion } } \ No newline at end of file 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 bb81c51809..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 { @@ -19,6 +19,7 @@ public CheckoutShippingMethodModel() public partial class ShippingMethodModel : ModelBase { + public int ShippingMethodId { get; set; } public string ShippingRateComputationMethodSystemName { get; set; } public string Name { get; set; } public string BrandUrl { get; set; } 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 346e102504..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 { @@ -15,6 +15,7 @@ public CustomerDownloadableProductsModel() public CustomerNavigationModel NavigationModel { get; set; } #region Nested classes + public partial class DownloadableProductsModel : ModelBase { public Guid OrderItemGuid { get; set; } @@ -24,6 +25,7 @@ public partial class DownloadableProductsModel : ModelBase public int ProductId { get; set; } public string ProductName { get; set; } public string ProductSeName { get; set; } + public string ProductUrl { get; set; } public string ProductAttributes { get; set; } public int DownloadId { get; set; } @@ -31,6 +33,7 @@ public partial class DownloadableProductsModel : ModelBase public DateTime CreatedOn { get; set; } } + #endregion } 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 ed98738e60..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,14 @@ 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] + public string CustomerNumber { get; set; } + public bool CustomerNumberEnabled { get; set; } + public bool DisplayCustomerNumber { get; set; } public bool CheckUsernameAvailabilityEnabled { get; set; } public bool AllowUsersToChangeUsernames { get; set; } @@ -94,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 d41eb9aefa..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 { @@ -15,12 +15,14 @@ public CustomerReturnRequestsModel() public CustomerNavigationModel NavigationModel { get; set; } #region Nested classes + public partial class ReturnRequestModel : EntityModelBase { public string ReturnRequestStatus { get; set; } public int ProductId { get; set; } public string ProductName { get; set; } public string ProductSeName { get; set; } + public string ProductUrl { get; set; } public int Quantity { get; set; } public string ReturnReason { get; set; } @@ -29,6 +31,7 @@ public partial class ReturnRequestModel : EntityModelBase public DateTime CreatedOn { get; set; } } + #endregion } } \ No newline at end of file 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 5a79ee2706..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 @@ -89,6 +89,7 @@ public OrderItemModel() public int ProductId { get; set; } public string ProductName { get; set; } public string ProductSeName { get; set; } + public string ProductUrl { get; set; } public ProductType ProductType { get; set; } public string UnitPrice { get; set; } public string SubTotal { get; set; } @@ -106,6 +107,7 @@ public partial class BundleItemModel : ModelBase public string Sku { get; set; } public string ProductName { get; set; } public string ProductSeName { get; set; } + public string ProductUrl { get; set; } public bool VisibleIndividually { get; set; } public int Quantity { get; set; } public int DisplayOrder { get; set; } @@ -124,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 1ffdd4270e..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 { @@ -30,6 +30,7 @@ public partial class ShipmentItemModel : EntityModelBase public int ProductId { get; set; } public string ProductName { get; set; } public string ProductSeName { get; set; } + public string ProductUrl { get; set; } public string AttributeInfo { get; set; } public int QuantityOrdered { get; set; } diff --git a/src/Presentation/SmartStore.Web/Models/Order/SubmitReturnRequestModel.cs b/src/Presentation/SmartStore.Web/Models/Order/SubmitReturnRequestModel.cs index 7592a3c785..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 { @@ -44,6 +44,8 @@ public partial class OrderItemModel : EntityModelBase public string ProductSeName { get; set; } + public string ProductUrl { get; set; } + public string AttributeInfo { get; set; } public string UnitPrice { get; set; } 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 aed18dadf1..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; } @@ -36,6 +38,8 @@ public EstimateShippingModel() public partial class ShippingOptionModel : ModelBase { + public int ShippingMethodId { get; set; } + public string Name { get; set; } public string Description { get; set; } diff --git a/src/Presentation/SmartStore.Web/Models/ShoppingCart/MiniShoppingCartModel.cs b/src/Presentation/SmartStore.Web/Models/ShoppingCart/MiniShoppingCartModel.cs index d7093304de..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 @@ -40,6 +39,8 @@ public ShoppingCartItemModel() public string ProductSeName { get; set; } + public string ProductUrl { get; set; } + public int Quantity { get; set; } public string UnitPrice { get; set; } @@ -57,6 +58,7 @@ public partial class ShoppingCartItemBundleItem : ModelBase public string PictureUrl { get; set; } public string ProductName { get; set; } public string ProductSeName { get; set; } + public string ProductUrl { get; set; } } #endregion 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 6416310328..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() { @@ -73,6 +84,8 @@ public ShoppingCartItemModel() public string ProductSeName { get; set; } + public string ProductUrl { get; set; } + public bool VisibleIndividually { get; set; } public ProductType ProductType { get; set; } @@ -106,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 f1d0053cf7..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 @@ -51,6 +51,7 @@ public ShoppingCartItemModel() ChildItems = new List(); BundleItem = new BundleItemModel(); } + public string Sku { get; set; } public PictureModel Picture {get;set;} @@ -61,6 +62,8 @@ public ShoppingCartItemModel() public string ProductSeName { get; set; } + public string ProductUrl { get; set; } + public bool VisibleIndividually { get; set; } public ProductType ProductType { get; set; } 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 c86e55904f..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); @@ -62,7 +72,7 @@ if (data.GalleryHtml) { var cnt = $('#pd-gallery-container'); cnt.stop(true, true).transition({ opacity: 0 }, 300, "ease-out", function () { - gallery.reset(); + gallery.reset(); cnt.html(data.GalleryHtml); self.createGallery(data.GalleryStartIndex); @@ -169,7 +179,8 @@ zoomType: opts.zoomType }, box: { - enabled: true + enabled: true, + hidePageScrollbars: false } }); } 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 0552e31a90..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,13 +132,16 @@ }; }; - this.loading(true); + if (this.images[startAt]) { + this.loading(true); + } this.showImage(startAt); if (opts.responsive && !isRefresh) { EventBroker.subscribe("page.resized", function (data) { - self.reset(); + self.reset(); + self.inTransition = false; self.init(true); }); } @@ -152,7 +159,7 @@ loader: null, preloads: null, thumbsWrapper: null, - box: null, + box: null, // (blueImp) image gallery element imageWrapperWidth: 0, imageWrapperHeight: 0, @@ -179,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); }, @@ -209,14 +215,20 @@ } self.imageWrapper.find('.sg-image').remove(); + + $('.smartgallery-overlay').remove(); + if (self.box) { + self.box.remove(); + } + self.box = null; }, loading: function(value) { if (value) { - this.loader.show(); + this.loader.addClass('active'); } else { - this.loader.hide(); + this.loader.removeClass('active'); }; }, @@ -425,6 +437,8 @@ .append('
      '); widget.appendTo('body'); + self.box = widget; + var prevY = null; var onMouseMove = function (e) { @@ -452,6 +466,14 @@ return widget; }; + var getOverlay = function (widget) { + var gov = $('.smartgallery-overlay'); + if (gov.length == 0) { + gov = $('').insertBefore(widget[0]); + } + return gov; + }; + if (this.options.displayImage) { // Global click handler to open links with data-gallery attribute // in the Gallery lightbox: @@ -462,10 +484,15 @@ widget = getWidget(id == 'default' ? 'image-gallery-default' : id), container = (widget.length && widget) || $(Gallery.prototype.options.container), callbacks = { - onopen: function () { - container.data('gallery', this).trigger('open'); + onopen: function () { + var gov = getOverlay(widget); + gov.on('click', function (e) { + widget.data('gallery').close(); + }); + gov.show().addClass("in"); + container.data('gallery', this).trigger('open'); }, - onopened: function () { + onopened: function () { container.trigger('opened'); }, onslide: function () { @@ -478,9 +505,11 @@ container.trigger('slidecomplete', arguments); }, onclose: function () { - container.trigger('close'); + getOverlay(widget).removeClass("in"); + container.trigger('close'); }, onclosed: function () { + getOverlay(widget).css('display', 'none'); container.trigger('closed').removeData('gallery'); } }, @@ -494,7 +523,7 @@ event: e }, callbacks, - self.options + self.options.box || {} ), // Select all links with the same data-gallery attribute: links = $('[data-gallery="' + id + '"]').not($(this)); @@ -862,6 +891,7 @@ // full size image box options box: { enabled: true, + closeOnSlideClick: false /* {...} blueimp image gallery options are passed through */ }, callbacks: { 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 fa9ed189b2..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 @@ -96,30 +97,31 @@ False - ..\..\packages\EntityFramework.6.1.0\lib\net45\EntityFramework.dll + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll False - ..\..\packages\EntityFramework.6.1.0\lib\net45\EntityFramework.SqlServer.dll + ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll False - ..\..\packages\EntityFramework.SqlServerCompact.6.1.0\lib\net45\EntityFramework.SqlServerCompact.dll + ..\..\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 @@ -253,10 +253,10 @@ - + @@ -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,9 +1578,13 @@ + + + + @@ -1982,7 +1699,6 @@ - @@ -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 f59456e104..60e01fb235 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/checkout.less +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/checkout.less @@ -21,7 +21,8 @@ background-color: #f9f9f9; border-color: #f9f9f9; } - .opt-info { + .opt-info, + .opt-info-item { margin-top: 6px; margin-left: 32px; } @@ -239,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 4b7f87e7bf..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; @@ -167,3 +168,17 @@ vertical-align: top; } +/* Manufacturers on homepage +================================================ */ + +.block-manufacturer-navigation .manufacturer-pic { + float: left; + width: 50px; + height: 50px; + margin: 0 0 10px 10px; + + img { + max-width: 50px; + max-height: 50px; + } +} \ No newline at end of file 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/profile.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/profile.less index 2af25a3deb..19cd3e181b 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/profile.less +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/profile.less @@ -88,14 +88,18 @@ /* Return Requests ================================================ */ -.return-request-list-page .request-item .title{ - font-size: 13px; +.return-request-list-page .request-item { + margin-bottom: 20px; + + .title { + margin-bottom: 10px; + } } -.reward-points-page .reward-points-overview{ +.reward-points-page .reward-points-overview { padding: 10px 10px 5px 0; width: 100%; } -.return-request-page .why .comment{ +.return-request-page .why .comment { width: 350px; height: 150px; } diff --git a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/shopbar.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/shopbar.less index d221631a55..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%); } @@ -254,7 +242,6 @@ margin: 0; list-style: none; -webkit-background-clip: padding-box; - -moz-background-clip: padding; background-clip: padding-box; height: 50px; @@ -273,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/slider.less b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/slider.less index 6528c573b0..22a5a10f90 100644 --- a/src/Presentation/SmartStore.Web/Themes/Alpha/Content/slider.less +++ b/src/Presentation/SmartStore.Web/Themes/Alpha/Content/slider.less @@ -71,7 +71,7 @@ Aside from these comments, you may modify and distribute this file as you please .transition(background-position 1.5s ease-out); .transform(translate3d(0, 0, 0)); - -webkit-perspective: 1000; -moz-perspective: 100; perspective: 1000; + -webkit-perspective: 1000; -moz-perspective: 1000; perspective: 1000; -webkit-transform-style: preserve-3d; transform-style: preserve-3d; // backface-visibility prevents graphical glitches when frames are animating @@ -297,7 +297,7 @@ Aside from these comments, you may modify and distribute this file as you please height: 100%; top: 0; .opacity(0); - -webkit-perspective: 1000; -moz-perspective: 100; perspective: 1000; + -webkit-perspective: 1000; -moz-perspective: 1000; perspective: 1000; -webkit-transform-style: preserve-3d; transform-style: preserve-3d; } } 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/Content/styles.css b/src/Presentation/SmartStore.Web/Themes/Mobile/Content/styles.css index bbc2b1f597..dbf4b3d68c 100644 --- a/src/Presentation/SmartStore.Web/Themes/Mobile/Content/styles.css +++ b/src/Presentation/SmartStore.Web/Themes/Mobile/Content/styles.css @@ -156,7 +156,7 @@ img { max-width: 100% } .checkout-data .use-reward-points{margin:0;padding:10px;background:#F7F5E8;border:dotted 1px #d3d3d3;} .checkout-data .payment-methods{text-align:left;height:auto;} .checkout-data .payment-methods .payment-method-item{text-align:left;vertical-align:text-top;} -.checkout-data .payment-methods .payment-method-item .payment-method-desc{margin:0 0 14px 46px;} +.checkout-data .payment-methods .payment-method-item .payment-method-description{margin:0 0 15px 15px;} .checkout-data .payment-methods .select-button{text-align:left;} .checkout-data .payment-methods .message-error{text-align:left;} .checkout-data .payment-info{text-align:left;height:auto;} @@ -689,3 +689,10 @@ pre{white-space:pre-wrap;/* css-3 */white-space:0;/* Mozilla, since 1999 */white .legal-hints .footer-legal { color: green; } +.change-device button{ + display: inline; + margin: 0; + border: 0; + background: none; + padding: 7px; +} 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/Boards/TopicMove.Mobile.cshtml b/src/Presentation/SmartStore.Web/Views/Boards/TopicMove.Mobile.cshtml index c9ad06e2b3..ca23d8a705 100644 --- a/src/Presentation/SmartStore.Web/Views/Boards/TopicMove.Mobile.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Boards/TopicMove.Mobile.cshtml @@ -16,7 +16,7 @@
      @T("Forum.SelectTheForumToMoveTopic"): - @Html.DropDownList("ForumSelected", new SelectList(Model.ForumList, "Value", "Text")) + @Html.DropDownList("ForumSelected", new SelectList(Model.ForumList, "Value", "Text"), new { data_native_menu = "false" })
      @T("Forum.Cancel") diff --git a/src/Presentation/SmartStore.Web/Views/Boards/_CreateUpdateTopic.Mobile.cshtml b/src/Presentation/SmartStore.Web/Views/Boards/_CreateUpdateTopic.Mobile.cshtml index d0e2084883..1b13d8dda7 100644 --- a/src/Presentation/SmartStore.Web/Views/Boards/_CreateUpdateTopic.Mobile.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Boards/_CreateUpdateTopic.Mobile.cshtml @@ -41,7 +41,7 @@ @if (Model.IsCustomerAllowedToSetTopicPriority) {
      - @Html.DropDownList("TopicTypeId", new SelectList(@Model.TopicPriorities, "Value", "Text", @Model.TopicTypeId)) + @Html.DropDownList("TopicTypeId", new SelectList(@Model.TopicPriorities, "Value", "Text", @Model.TopicTypeId), new { data_native_menu = "false" })
      } @if (Model.IsCustomerAllowedToSubscribe) 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 @@ {
      -

      - @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 +

      + @if (product.BasePriceInfo.HasValue()) + { +
      + @product.BasePriceInfo +
      + } } 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 @@
    1. - - @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 f408a7bf41..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) {
      @@ -13,10 +13,28 @@ + + @if (Model.DisplayImages) + { +
       
      + } + @if (Model.TotalManufacturers > Model.Manufacturers.Count) {
      diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/RecentlyAddedProducts.Mobile.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/RecentlyAddedProducts.Mobile.cshtml index 34bcc4772a..1f19e9cadf 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/RecentlyAddedProducts.Mobile.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/RecentlyAddedProducts.Mobile.cshtml @@ -1,4 +1,4 @@ -@model IList +@model RecentlyAddedProductsModel @{ Layout = "~/Views/Shared/_Root.cshtml"; @@ -13,11 +13,11 @@

      @T("Products.NewProducts")

      - @if (Model.Count > 0) + @if (Model.Products.Count > 0) {
        - @foreach (var product in Model) + @foreach (var product in Model.Products) {
      • @Html.Partial("_ProductBox", product) diff --git a/src/Presentation/SmartStore.Web/Views/Catalog/RecentlyAddedProducts.cshtml b/src/Presentation/SmartStore.Web/Views/Catalog/RecentlyAddedProducts.cshtml index db5b531560..f4aa453cce 100644 --- a/src/Presentation/SmartStore.Web/Views/Catalog/RecentlyAddedProducts.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Catalog/RecentlyAddedProducts.cshtml @@ -1,5 +1,4 @@ -@*codehint: sm-edit*@ -@model RecentlyAddedProductsModel +@model RecentlyAddedProductsModel @{ Layout = "~/Views/Shared/_ColumnsThree.cshtml"; @@ -13,27 +12,18 @@ @using SmartStore.Core.Domain.Catalog; @using SmartStore.Web.Framework.UI; -@*@functions{ - private bool ShowListOptions() { - return Model.Products.Count > 0 && - (Model.PagingFilteringContext.AllowProductViewModeChanging || - Model.PagingFilteringContext.AllowProductSorting || - Model.PagingFilteringContext.AllowCustomersToSelectPageSize); - } -}*@ -
        -
        -
        + +
        +
        - - @*@if (ShowListOptions()) { - @Html.Partial("_ProductListOptions", Model.PagingFilteringContext) - }*@ @if (Model.Products.Count > 0) { @@ -65,7 +55,5 @@
        - @*@Html.SmartStore().Pager(Model.PagingFilteringContext).Name("pagination-bottom").QueryParam("pagenumber")*@ -
        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 45bfb15e42..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 5bc4697448..cab3cdbc8c 100644 --- a/src/Presentation/SmartStore.Web/Views/Checkout/PaymentMethod.Mobile.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Checkout/PaymentMethod.Mobile.cshtml @@ -47,13 +47,13 @@ - @if (!String.IsNullOrWhiteSpace(paymentMethod.Description)) - { -
      - @Html.Raw(paymentMethod.Description) -
      - }
      + @if (paymentMethod.FullDescription.HasValue()) + { +
      + @Html.Raw(paymentMethod.FullDescription) +
      + } @if (!paymentMethod.RequiresInteraction || (paymentMethod.Selected && infoRoute != null)) {
      } - @Html.Action(infoRoute.Action, infoRoute.Controller, infoRoute.RouteValues) + @if (!(paymentMethod.FullDescription.HasValue() && !paymentMethod.RequiresInteraction)) + { + @Html.Action(infoRoute.Action, infoRoute.Controller, infoRoute.RouteValues) + }
      +
      }
      } diff --git a/src/Presentation/SmartStore.Web/Views/Checkout/PaymentMethod.cshtml b/src/Presentation/SmartStore.Web/Views/Checkout/PaymentMethod.cshtml index 0d8df1ae8a..569189379a 100644 --- a/src/Presentation/SmartStore.Web/Views/Checkout/PaymentMethod.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Checkout/PaymentMethod.cshtml @@ -57,6 +57,12 @@
      + @if (paymentMethod.FullDescription.HasValue()) + { +
      + @Html.Raw(paymentMethod.FullDescription) +
      + } @if (!paymentMethod.RequiresInteraction || (paymentMethod.Selected && infoRoute != null)) {
      } - @Html.Action(infoRoute.Action, infoRoute.Controller, infoRoute.RouteValues) + @if(!(paymentMethod.FullDescription.HasValue() && !paymentMethod.RequiresInteraction)) + { + @Html.Action(infoRoute.Action, infoRoute.Controller, infoRoute.RouteValues) + }
      }
      diff --git a/src/Presentation/SmartStore.Web/Views/Checkout/ShippingAddress.cshtml b/src/Presentation/SmartStore.Web/Views/Checkout/ShippingAddress.cshtml index 1d7d4e6552..e003882015 100644 --- a/src/Presentation/SmartStore.Web/Views/Checkout/ShippingAddress.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Checkout/ShippingAddress.cshtml @@ -45,8 +45,8 @@ @item.PhoneNumber
      } - @if (item.FaxEnabled) - { + @if (item.FaxEnabled && !String.IsNullOrEmpty(item.FaxNumber)) +{
      @T("Address.Fields.FaxNumber"): @item.FaxNumber 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 8e4a0e5ce4..96b1274211 100644 --- a/src/Presentation/SmartStore.Web/Views/Common/AccountDropdown.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Common/AccountDropdown.cshtml @@ -17,9 +17,10 @@ @if (Model.IsAuthenticated) { } else diff --git a/src/Presentation/SmartStore.Web/Views/Common/ChangeDeviceBlock.Mobile.cshtml b/src/Presentation/SmartStore.Web/Views/Common/ChangeDeviceBlock.Mobile.cshtml index 7d7fb0d8ad..a6be354634 100644 --- a/src/Presentation/SmartStore.Web/Views/Common/ChangeDeviceBlock.Mobile.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Common/ChangeDeviceBlock.Mobile.cshtml @@ -1,5 +1,6 @@ 
      - - @T("Mobile.ViewFullSite") - + @using (Html.BeginRouteForm("ChangeDevice", new { dontusemobileversion = true }, FormMethod.Post)) + { + + }
      diff --git a/src/Presentation/SmartStore.Web/Views/Common/ChangeDeviceBlock.cshtml b/src/Presentation/SmartStore.Web/Views/Common/ChangeDeviceBlock.cshtml index b380d92dc5..3c92219985 100644 --- a/src/Presentation/SmartStore.Web/Views/Common/ChangeDeviceBlock.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Common/ChangeDeviceBlock.cshtml @@ -1,5 +1,6 @@ 
      - - @T("Mobile.ViewMobileVersion") - + @using (Html.BeginRouteForm("ChangeDevice", new { dontusemobileversion = false }, FormMethod.Post)) + { + + }
      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 ed4d34d103..cb9d4ab796 100644 --- a/src/Presentation/SmartStore.Web/Views/Common/Footer.cshtml +++ b/src/Presentation/SmartStore.Web/Views/Common/Footer.cshtml @@ -52,28 +52,45 @@